Files
evotraders/backend/config/bootstrap_config.py
cillin 16b54d5ccc feat(agent): complete EvoAgent integration for all 6 agent roles
Migrate all agent roles from Legacy to EvoAgent architecture:
- fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst
- risk_manager, portfolio_manager

Key changes:
- EvoAgent now supports Portfolio Manager compatibility methods (_make_decision,
  get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio)
- Add UnifiedAgentFactory for centralized agent creation
- ToolGuard with batch approval API and WebSocket broadcast
- Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent)
- Remove backend/agents/compat.py migration shim
- Add run_id alongside workspace_id for semantic clarity
- Complete integration test coverage (13 tests)
- All smoke tests passing for 6 agent roles

Constraint: Must maintain backward compatibility with existing run configs
Constraint: Memory support must work with EvoAgent (no fallback to Legacy)
Rejected: Separate PM implementation for EvoAgent | unified approach cleaner
Confidence: high
Scope-risk: broad
Directive: EVO_AGENT_IDS env var still respected but defaults to all roles
Not-tested: Kubernetes sandbox mode for skill execution
2026-04-02 00:55:08 +08:00

180 lines
5.1 KiB
Python

# -*- coding: utf-8 -*-
"""Parse run-scoped BOOTSTRAP.md into structured and runtime config."""
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict
DEFAULT_TICKERS = [
"AAPL",
"MSFT",
"GOOGL",
"AMZN",
"NVDA",
"META",
"TSLA",
"AMD",
"NFLX",
"AVGO",
"PLTR",
"COIN",
]
import re
import yaml
from backend.config.env_config import get_env_float, get_env_int, get_env_list
BOOTSTRAP_FRONT_MATTER_RE = re.compile(
r"^---\s*\n(.*?)\n---\s*\n?(.*)$",
re.DOTALL,
)
@dataclass(frozen=True)
class BootstrapConfig:
"""Structured configuration extracted from BOOTSTRAP.md."""
values: Dict[str, Any] = field(default_factory=dict)
prompt_body: str = ""
def get(self, key: str, default: Any = None) -> Any:
return self.values.get(key, default)
def agent_override(self, agent_id: str) -> Dict[str, Any]:
overrides = self.values.get("agent_overrides", {})
if not isinstance(overrides, dict):
return {}
override = overrides.get(agent_id, {})
return override if isinstance(override, dict) else {}
def load_bootstrap_config(bootstrap_path: Path) -> BootstrapConfig:
"""Load structured bootstrap config and free-form prompt body."""
if not bootstrap_path.exists():
return BootstrapConfig()
raw = bootstrap_path.read_text(encoding="utf-8").strip()
if not raw:
return BootstrapConfig()
match = BOOTSTRAP_FRONT_MATTER_RE.match(raw)
if not match:
return BootstrapConfig(prompt_body=raw)
front_matter = match.group(1).strip()
body = match.group(2).strip()
parsed = yaml.safe_load(front_matter) or {}
if not isinstance(parsed, dict):
parsed = {}
return BootstrapConfig(values=parsed, prompt_body=body)
def get_bootstrap_config_for_run(
project_root: Path,
config_name: str,
) -> BootstrapConfig:
"""Load BOOTSTRAP.md from the run-scoped asset tree."""
return load_bootstrap_config(
project_root / "runs" / config_name / "BOOTSTRAP.md",
)
def save_bootstrap_config(bootstrap_path: Path, config: BootstrapConfig) -> None:
"""Persist structured bootstrap config back to BOOTSTRAP.md."""
bootstrap_path.parent.mkdir(parents=True, exist_ok=True)
values = config.values if isinstance(config.values, dict) else {}
front_matter = yaml.safe_dump(
values,
allow_unicode=True,
sort_keys=False,
).strip()
body = (config.prompt_body or "").strip()
content = f"---\n{front_matter}\n---"
if body:
content += f"\n\n{body}\n"
else:
content += "\n"
bootstrap_path.write_text(content, encoding="utf-8")
def update_bootstrap_values_for_run(
project_root: Path,
config_name: str,
updates: Dict[str, Any],
) -> BootstrapConfig:
"""Patch selected front matter keys for a run and persist them."""
bootstrap_path = project_root / "runs" / config_name / "BOOTSTRAP.md"
existing = load_bootstrap_config(bootstrap_path)
values = dict(existing.values)
values.update(updates)
updated = BootstrapConfig(values=values, prompt_body=existing.prompt_body)
save_bootstrap_config(bootstrap_path, updated)
return updated
def _coerce_bool(value: Any) -> bool:
"""Parse booleans from bootstrap-friendly string values."""
if isinstance(value, bool):
return value
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
return bool(value)
def resolve_runtime_config(
project_root: Path,
config_name: str,
enable_memory: bool = False,
schedule_mode: str = "daily",
interval_minutes: int = 60,
trigger_time: str = "09:30",
) -> Dict[str, Any]:
"""Merge env defaults with run-scoped bootstrap front matter."""
bootstrap = get_bootstrap_config_for_run(project_root, config_name)
return {
"tickers": bootstrap.get("tickers")
or get_env_list("TICKERS", DEFAULT_TICKERS),
"initial_cash": float(
bootstrap.get(
"initial_cash",
get_env_float("INITIAL_CASH", 100000.0),
),
),
"margin_requirement": float(
bootstrap.get(
"margin_requirement",
get_env_float("MARGIN_REQUIREMENT", 0.0),
),
),
"max_comm_cycles": int(
bootstrap.get(
"max_comm_cycles",
get_env_int("MAX_COMM_CYCLES", 2),
),
),
"schedule_mode": str(
bootstrap.get("schedule_mode", schedule_mode),
).strip().lower() or schedule_mode,
"interval_minutes": int(
bootstrap.get(
"interval_minutes",
interval_minutes or get_env_int("INTERVAL_MINUTES", 60),
),
),
"trigger_time": str(
bootstrap.get("trigger_time", trigger_time),
).strip() or trigger_time,
"enable_memory": bool(enable_memory)
or _coerce_bool(bootstrap.get("enable_memory", False)),
}