Initial commit of integrated agent system
This commit is contained in:
0
backend/config/__init__.py
Normal file
0
backend/config/__init__.py
Normal file
37
backend/config/agent_profiles.yaml
Normal file
37
backend/config/agent_profiles.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
fundamentals_analyst:
|
||||
skills:
|
||||
- fundamental_review
|
||||
active_tool_groups:
|
||||
- fundamentals
|
||||
- valuation
|
||||
|
||||
technical_analyst:
|
||||
skills:
|
||||
- technical_review
|
||||
active_tool_groups:
|
||||
- technical
|
||||
|
||||
sentiment_analyst:
|
||||
skills:
|
||||
- sentiment_review
|
||||
active_tool_groups:
|
||||
- sentiment
|
||||
|
||||
valuation_analyst:
|
||||
skills:
|
||||
- valuation_review
|
||||
active_tool_groups:
|
||||
- valuation
|
||||
- fundamentals
|
||||
|
||||
portfolio_manager:
|
||||
skills:
|
||||
- portfolio_decisioning
|
||||
active_tool_groups:
|
||||
- portfolio_ops
|
||||
|
||||
risk_manager:
|
||||
skills:
|
||||
- risk_review
|
||||
active_tool_groups:
|
||||
- risk_ops
|
||||
179
backend/config/bootstrap_config.py
Normal file
179
backend/config/bootstrap_config.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# -*- 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 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", 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)),
|
||||
}
|
||||
76
backend/config/constants.py
Normal file
76
backend/config/constants.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa: E501
|
||||
# pylint: disable=C0301
|
||||
|
||||
# Agent configuration for dashboard display
|
||||
AGENT_CONFIG = {
|
||||
"portfolio_manager": {
|
||||
"name": "投资经理",
|
||||
"role": "投资经理",
|
||||
"avatar": "pm",
|
||||
"is_team_role": True,
|
||||
},
|
||||
"risk_manager": {
|
||||
"name": "风控经理",
|
||||
"role": "风控经理",
|
||||
"avatar": "risk",
|
||||
"is_team_role": True,
|
||||
},
|
||||
"sentiment_analyst": {
|
||||
"name": "情绪分析师",
|
||||
"role": "情绪分析师",
|
||||
"avatar": "sentiment",
|
||||
"is_team_role": False,
|
||||
},
|
||||
"technical_analyst": {
|
||||
"name": "技术分析师",
|
||||
"role": "技术分析师",
|
||||
"avatar": "technical",
|
||||
"is_team_role": False,
|
||||
},
|
||||
"fundamentals_analyst": {
|
||||
"name": "基本面分析师",
|
||||
"role": "基本面分析师",
|
||||
"avatar": "fundamentals",
|
||||
"is_team_role": False,
|
||||
},
|
||||
"valuation_analyst": {
|
||||
"name": "估值分析师",
|
||||
"role": "估值分析师",
|
||||
"avatar": "valuation",
|
||||
"is_team_role": False,
|
||||
},
|
||||
}
|
||||
|
||||
ANALYST_TYPES = {
|
||||
"fundamentals_analyst": {
|
||||
"display_name": "Fundamentals Analyst",
|
||||
"agent_id": "fundamentals_analyst",
|
||||
"description": "Uses LLM to intelligently select analysis tools, focuses on financial data and company fundamental analysis",
|
||||
"order": 12,
|
||||
},
|
||||
"technical_analyst": {
|
||||
"display_name": "Technical Analyst",
|
||||
"agent_id": "technical_analyst",
|
||||
"description": "Uses LLM to intelligently select analysis tools, focuses on technical indicators and chart analysis",
|
||||
"order": 11,
|
||||
},
|
||||
"sentiment_analyst": {
|
||||
"display_name": "Sentiment Analyst",
|
||||
"agent_id": "sentiment_analyst",
|
||||
"description": "Uses LLM to intelligently select analysis tools, analyzes market sentiment and news sentiment",
|
||||
"order": 13,
|
||||
},
|
||||
"valuation_analyst": {
|
||||
"display_name": "Valuation Analyst",
|
||||
"agent_id": "valuation_analyst",
|
||||
"description": "Uses LLM to intelligently select analysis tools, focuses on company valuation and value assessment",
|
||||
"order": 14,
|
||||
},
|
||||
# "comprehensive_analyst": {
|
||||
# "display_name": "Comprehensive Analyst",
|
||||
# "agent_id": "comprehensive_analyst",
|
||||
# "description": "Uses LLM to intelligently select analysis tools, performs comprehensive analysis",
|
||||
# "order": 15
|
||||
# }
|
||||
}
|
||||
128
backend/config/data_config.py
Normal file
128
backend/config/data_config.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Centralized data source configuration and fallback ordering."""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional
|
||||
|
||||
DataSource = Literal["finnhub", "financial_datasets", "yfinance", "local_csv"]
|
||||
_KNOWN_SOURCES: tuple[DataSource, ...] = (
|
||||
"finnhub",
|
||||
"financial_datasets",
|
||||
"yfinance",
|
||||
"local_csv",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataSourceConfig:
|
||||
"""Resolved data source configuration."""
|
||||
|
||||
source: DataSource
|
||||
api_key: str
|
||||
sources: list[DataSource]
|
||||
|
||||
|
||||
# Module-level cache for the resolved configuration
|
||||
_config_cache: Optional[DataSourceConfig] = None
|
||||
|
||||
|
||||
def _parse_enabled_sources() -> list[DataSource]:
|
||||
"""Parse optional enabled source allowlist from the environment."""
|
||||
raw_value = os.getenv("ENABLED_DATA_SOURCES", "").strip().lower()
|
||||
if not raw_value:
|
||||
return []
|
||||
|
||||
enabled: list[DataSource] = []
|
||||
for item in raw_value.split(","):
|
||||
candidate = item.strip()
|
||||
if not candidate or candidate not in _KNOWN_SOURCES:
|
||||
continue
|
||||
if candidate not in enabled:
|
||||
enabled.append(candidate)
|
||||
return enabled
|
||||
|
||||
|
||||
def _ordered_sources() -> list[DataSource]:
|
||||
"""Resolve source preference and available fallbacks."""
|
||||
preferred = os.getenv("FIN_DATA_SOURCE", "").strip().lower()
|
||||
finnhub_key = os.getenv("FINNHUB_API_KEY", "").strip()
|
||||
fd_key = os.getenv("FINANCIAL_DATASETS_API_KEY", "").strip()
|
||||
enabled_sources = _parse_enabled_sources()
|
||||
wants_yfinance = preferred == "yfinance" or "yfinance" in enabled_sources
|
||||
|
||||
available: list[DataSource] = []
|
||||
if finnhub_key:
|
||||
available.append("finnhub")
|
||||
if fd_key:
|
||||
available.append("financial_datasets")
|
||||
if wants_yfinance:
|
||||
available.append("yfinance")
|
||||
available.append("local_csv")
|
||||
|
||||
if enabled_sources:
|
||||
filtered = [source for source in enabled_sources if source in available]
|
||||
if filtered:
|
||||
available = filtered
|
||||
|
||||
if preferred in available:
|
||||
ordered = [preferred]
|
||||
ordered.extend(source for source in available if source != preferred)
|
||||
return ordered
|
||||
return available
|
||||
|
||||
|
||||
def _resolve_config() -> DataSourceConfig:
|
||||
"""
|
||||
Resolve data source configuration based on available API keys.
|
||||
|
||||
The effective source should always match the first item in the resolved
|
||||
ordered source list.
|
||||
"""
|
||||
sources = _ordered_sources()
|
||||
source = sources[0] if sources else "local_csv"
|
||||
|
||||
api_key = ""
|
||||
if source == "finnhub":
|
||||
api_key = os.getenv("FINNHUB_API_KEY", "").strip()
|
||||
elif source == "financial_datasets":
|
||||
api_key = os.getenv("FINANCIAL_DATASETS_API_KEY", "").strip()
|
||||
|
||||
return DataSourceConfig(source=source, api_key=api_key, sources=sources)
|
||||
|
||||
|
||||
def get_config() -> DataSourceConfig:
|
||||
"""
|
||||
Get the resolved data source configuration (cached).
|
||||
|
||||
Returns:
|
||||
DataSourceConfig with source and api_key
|
||||
|
||||
Raises:
|
||||
ValueError: If no API key is configured
|
||||
"""
|
||||
global _config_cache
|
||||
if _config_cache is None:
|
||||
_config_cache = _resolve_config()
|
||||
return _config_cache
|
||||
|
||||
|
||||
def get_data_source() -> DataSource:
|
||||
"""Get the configured data source name."""
|
||||
return get_config().source
|
||||
|
||||
|
||||
def get_data_sources() -> list[DataSource]:
|
||||
"""Get preferred source ordering including fallbacks."""
|
||||
return get_config().sources
|
||||
|
||||
|
||||
def get_api_key() -> str:
|
||||
"""Get the API key for the configured data source."""
|
||||
return get_config().api_key
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset the cached configuration (useful for testing)."""
|
||||
global _config_cache
|
||||
_config_cache = None
|
||||
103
backend/config/env_config.py
Normal file
103
backend/config/env_config.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Environment config helpers with light validation and normalization."""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
FALSEY_ENV_VALUES = {"0", "false", "no", "off"}
|
||||
PROVIDER_ALIASES = {
|
||||
"openai_compatible": "OPENAI",
|
||||
"openai_compat": "OPENAI",
|
||||
"claude": "ANTHROPIC",
|
||||
"google": "GEMINI",
|
||||
"vertex": "GEMINI",
|
||||
"vertexai": "GEMINI",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentModelConfig:
|
||||
"""Resolved model config for one agent."""
|
||||
|
||||
model_name: str
|
||||
provider: str
|
||||
|
||||
|
||||
def _get_env_raw(key: str) -> Optional[str]:
|
||||
value = os.getenv(key)
|
||||
if value is None:
|
||||
return None
|
||||
value = value.strip()
|
||||
return value or None
|
||||
|
||||
|
||||
def get_env_str(key: str, default: str = "") -> str:
|
||||
"""Get trimmed string from env."""
|
||||
value = _get_env_raw(key)
|
||||
return value if value is not None else default
|
||||
|
||||
|
||||
def get_env_list(key: str, default: list = None) -> list:
|
||||
"""Get comma-separated list from env."""
|
||||
value = _get_env_raw(key)
|
||||
if not value:
|
||||
return default or []
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
|
||||
|
||||
def get_env_float(key: str, default: float = 0.0) -> float:
|
||||
"""Get float from env."""
|
||||
value = _get_env_raw(key)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def get_env_int(key: str, default: int = 0) -> int:
|
||||
"""Get int from env."""
|
||||
value = _get_env_raw(key)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def get_env_bool(key: str, default: bool = False) -> bool:
|
||||
"""Parse common truthy/falsey env values."""
|
||||
value = _get_env_raw(key)
|
||||
if value is None:
|
||||
return default
|
||||
return value.lower() not in FALSEY_ENV_VALUES
|
||||
|
||||
|
||||
def canonicalize_model_provider(provider: Optional[str]) -> str:
|
||||
"""Normalize provider labels to stable uppercase names."""
|
||||
if not provider:
|
||||
return "OPENAI"
|
||||
normalized = provider.strip().lower().replace("-", "_")
|
||||
normalized = PROVIDER_ALIASES.get(normalized, normalized)
|
||||
return normalized.upper()
|
||||
|
||||
|
||||
def get_agent_model_config(agent_id: str) -> AgentModelConfig:
|
||||
"""Resolve model config with agent-specific override and global fallback."""
|
||||
agent_key = agent_id.upper().replace("-", "_")
|
||||
model_name = get_env_str(f"AGENT_{agent_key}_MODEL_NAME")
|
||||
provider = get_env_str(f"AGENT_{agent_key}_MODEL_PROVIDER")
|
||||
|
||||
if not model_name:
|
||||
model_name = get_env_str("MODEL_NAME", "gpt-4o")
|
||||
if not provider:
|
||||
provider = get_env_str("MODEL_PROVIDER", "OPENAI")
|
||||
|
||||
return AgentModelConfig(
|
||||
model_name=model_name,
|
||||
provider=canonicalize_model_provider(provider),
|
||||
)
|
||||
Reference in New Issue
Block a user