Initial commit of integrated agent system

This commit is contained in:
cillin
2026-03-30 17:46:44 +08:00
commit 0fa413380c
337 changed files with 75268 additions and 0 deletions

View File

View 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

View 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)),
}

View 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
# }
}

View 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

View 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),
)