# -*- 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 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 workspace.""" 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", ["AAPL", "MSFT"]), "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)), }