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
This commit is contained in:
@@ -6,6 +6,7 @@ Handles reading/writing dashboard JSON files and portfolio state
|
||||
# pylint: disable=R0904
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -21,25 +22,31 @@ class StorageService:
|
||||
Storage service for data persistence
|
||||
|
||||
Responsibilities:
|
||||
1. Export dashboard JSON files
|
||||
1. Export dashboard JSON files (compatibility layer)
|
||||
(summary, holdings, stats, trades, leaderboard)
|
||||
2. Load/save internal state (_internal_state.json)
|
||||
3. Load/save server state (server_state.json) with feed history
|
||||
4. Manage portfolio state persistence
|
||||
5. Support loading from saved state to resume execution
|
||||
|
||||
Notes:
|
||||
- team_dashboard/*.json is treated as an export/compatibility layer
|
||||
rather than the authoritative runtime source of truth.
|
||||
- authoritative runtime reads should prefer in-memory state, server_state,
|
||||
runtime.db, and market_research.db.
|
||||
Architecture Notes:
|
||||
- runs/<run_id>/ is the authoritative runtime state root
|
||||
- team_dashboard/*.json is a NON-AUTHORITATIVE export/compatibility layer
|
||||
for external consumers (frontend, reports, etc.)
|
||||
- Authoritative runtime reads should prefer:
|
||||
1. In-memory state (runtime manager)
|
||||
2. state/server_state.json
|
||||
3. state/runtime.db
|
||||
4. market_research.db
|
||||
- Compatibility exports can be disabled via ENABLE_DASHBOARD_COMPAT_EXPORTS=false
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dashboard_dir: Path,
|
||||
initial_cash: float = 100000.0,
|
||||
config_name: str = "live",
|
||||
config_name: str = "runtime",
|
||||
enable_compat_exports: Optional[bool] = None,
|
||||
):
|
||||
"""
|
||||
Initialize storage service
|
||||
@@ -47,12 +54,18 @@ class StorageService:
|
||||
Args:
|
||||
dashboard_dir: Directory for dashboard files
|
||||
initial_cash: Initial cash amount
|
||||
config_name: Configuration name for state directory
|
||||
config_name: Logical runtime config/run label for state directory context
|
||||
enable_compat_exports: Whether to keep writing team_dashboard/*.json
|
||||
"""
|
||||
self.dashboard_dir = Path(dashboard_dir)
|
||||
self.dashboard_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.initial_cash = initial_cash
|
||||
self.config_name = config_name
|
||||
self.enable_compat_exports = (
|
||||
self._resolve_compat_exports_default()
|
||||
if enable_compat_exports is None
|
||||
else bool(enable_compat_exports)
|
||||
)
|
||||
|
||||
# Dashboard export file paths
|
||||
self.files = {
|
||||
@@ -88,6 +101,12 @@ class StorageService:
|
||||
|
||||
logger.info(f"Storage service initialized: {self.dashboard_dir}")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_compat_exports_default() -> bool:
|
||||
"""Default compatibility export policy, overridable via env."""
|
||||
raw = str(os.getenv("ENABLE_DASHBOARD_COMPAT_EXPORTS", "true")).strip().lower()
|
||||
return raw not in {"0", "false", "no", "off"}
|
||||
|
||||
def load_export_file(self, file_type: str) -> Optional[Any]:
|
||||
"""Load dashboard export JSON file."""
|
||||
file_path = self.files.get(file_type)
|
||||
@@ -106,7 +125,9 @@ class StorageService:
|
||||
return self.load_export_file(file_type)
|
||||
|
||||
def save_export_file(self, file_type: str, data: Any):
|
||||
"""Save dashboard export JSON file."""
|
||||
"""Save one compatibility dashboard export JSON file."""
|
||||
if not self.enable_compat_exports:
|
||||
return
|
||||
file_path = self.files.get(file_type)
|
||||
if not file_path:
|
||||
logger.error(f"Unknown file type: {file_type}")
|
||||
@@ -127,17 +148,79 @@ class StorageService:
|
||||
"""Backward-compatible alias for export-layer JSON writes."""
|
||||
self.save_export_file(file_type, data)
|
||||
|
||||
def save_dashboard_exports(self, exports: Dict[str, Any]) -> None:
|
||||
"""Persist compatibility dashboard exports from a normalized snapshot."""
|
||||
if not self.enable_compat_exports:
|
||||
return
|
||||
for file_type in ("summary", "holdings", "stats", "trades", "leaderboard"):
|
||||
if file_type in exports:
|
||||
self.save_export_file(file_type, exports[file_type])
|
||||
|
||||
def read_persisted_server_state(self) -> Dict[str, Any]:
|
||||
"""Read server_state.json without logging or DB side effects."""
|
||||
if not self.server_state_file.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(self.server_state_file, "r", encoding="utf-8") as f:
|
||||
payload = json.load(f)
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to read persisted server state: %s", exc)
|
||||
return {}
|
||||
|
||||
def load_runtime_leaderboard(self, state: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||
"""Prefer runtime state for leaderboard reads, fall back to export JSON."""
|
||||
runtime_state = state or self.read_persisted_server_state()
|
||||
leaderboard = runtime_state.get("leaderboard")
|
||||
if isinstance(leaderboard, list) and leaderboard:
|
||||
return leaderboard
|
||||
return self.load_export_file("leaderboard") or []
|
||||
|
||||
def persist_runtime_leaderboard(
|
||||
self,
|
||||
leaderboard: List[Dict[str, Any]],
|
||||
state: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Persist leaderboard to runtime state first, keeping JSON export for compatibility."""
|
||||
self.save_export_file("leaderboard", leaderboard)
|
||||
runtime_state = state or self.read_persisted_server_state()
|
||||
if not runtime_state:
|
||||
runtime_state = self.load_server_state()
|
||||
runtime_state["leaderboard"] = leaderboard
|
||||
self.save_server_state(runtime_state)
|
||||
|
||||
def build_dashboard_snapshot_from_state(
|
||||
self,
|
||||
state: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build dashboard view data from runtime state instead of JSON exports."""
|
||||
runtime_state = state or self.load_server_state()
|
||||
portfolio = dict(runtime_state.get("portfolio") or {})
|
||||
holdings = list(runtime_state.get("holdings") or [])
|
||||
stats = runtime_state.get("stats") or self._get_default_stats()
|
||||
trades = list(runtime_state.get("trades") or [])
|
||||
leaderboard = list(runtime_state.get("leaderboard") or [])
|
||||
persisted_state = self.read_persisted_server_state() if state is not None else {}
|
||||
portfolio = dict(
|
||||
runtime_state.get("portfolio")
|
||||
or persisted_state.get("portfolio")
|
||||
or {},
|
||||
)
|
||||
holdings = list(
|
||||
runtime_state.get("holdings")
|
||||
or persisted_state.get("holdings")
|
||||
or [],
|
||||
)
|
||||
stats = (
|
||||
runtime_state.get("stats")
|
||||
or persisted_state.get("stats")
|
||||
or self._get_default_stats()
|
||||
)
|
||||
trades = list(
|
||||
runtime_state.get("trades")
|
||||
or persisted_state.get("trades")
|
||||
or [],
|
||||
)
|
||||
leaderboard = list(
|
||||
runtime_state.get("leaderboard")
|
||||
or persisted_state.get("leaderboard")
|
||||
or [],
|
||||
)
|
||||
|
||||
summary = {
|
||||
"totalAssetValue": portfolio.get("total_value", self.initial_cash),
|
||||
@@ -331,48 +414,38 @@ class StorageService:
|
||||
self.save_internal_state(internal_state)
|
||||
|
||||
def initialize_empty_dashboard(self):
|
||||
"""Initialize empty dashboard files with default values"""
|
||||
# Summary
|
||||
self.save_export_file(
|
||||
"summary",
|
||||
"""Initialize compatibility dashboard exports with default values."""
|
||||
self.save_dashboard_exports(
|
||||
{
|
||||
"totalAssetValue": self.initial_cash,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": self.initial_cash,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"pnlPct": 0.0,
|
||||
"balance": self.initial_cash,
|
||||
"equity": [],
|
||||
"baseline": [],
|
||||
"baseline_vw": [],
|
||||
"momentum": [],
|
||||
},
|
||||
)
|
||||
|
||||
# Holdings
|
||||
self.save_export_file("holdings", [])
|
||||
|
||||
# Stats
|
||||
self.save_export_file(
|
||||
"stats",
|
||||
{
|
||||
"totalAssetValue": self.initial_cash,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": self.initial_cash,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"winRate": 0.0,
|
||||
"bullBear": {
|
||||
"bull": {"n": 0, "win": 0},
|
||||
"bear": {"n": 0, "win": 0},
|
||||
"summary": {
|
||||
"totalAssetValue": self.initial_cash,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": self.initial_cash,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"pnlPct": 0.0,
|
||||
"balance": self.initial_cash,
|
||||
"equity": [],
|
||||
"baseline": [],
|
||||
"baseline_vw": [],
|
||||
"momentum": [],
|
||||
},
|
||||
"holdings": [],
|
||||
"stats": {
|
||||
"totalAssetValue": self.initial_cash,
|
||||
"totalReturn": 0.0,
|
||||
"cashPosition": self.initial_cash,
|
||||
"tickerWeights": {},
|
||||
"totalTrades": 0,
|
||||
"winRate": 0.0,
|
||||
"bullBear": {
|
||||
"bull": {"n": 0, "win": 0},
|
||||
"bear": {"n": 0, "win": 0},
|
||||
},
|
||||
},
|
||||
"trades": [],
|
||||
},
|
||||
)
|
||||
|
||||
# Trades
|
||||
self.save_export_file("trades", [])
|
||||
|
||||
# Leaderboard with model info
|
||||
self.generate_leaderboard()
|
||||
|
||||
@@ -411,7 +484,7 @@ class StorageService:
|
||||
ranking_entries.append(entry)
|
||||
|
||||
leaderboard = team_entries + ranking_entries
|
||||
self.save_export_file("leaderboard", leaderboard)
|
||||
self.persist_runtime_leaderboard(leaderboard)
|
||||
logger.info("Leaderboard generated with model info")
|
||||
|
||||
def update_leaderboard_model_info(self):
|
||||
@@ -421,7 +494,7 @@ class StorageService:
|
||||
from ..config.constants import AGENT_CONFIG
|
||||
from ..llm.models import get_agent_model_info
|
||||
|
||||
existing = self.load_file("leaderboard") or []
|
||||
existing = self.load_runtime_leaderboard()
|
||||
|
||||
if not existing:
|
||||
self.generate_leaderboard()
|
||||
@@ -434,7 +507,7 @@ class StorageService:
|
||||
entry["modelName"] = model_name
|
||||
entry["modelProvider"] = model_provider
|
||||
|
||||
self.save_export_file("leaderboard", existing)
|
||||
self.persist_runtime_leaderboard(existing)
|
||||
logger.info("Leaderboard model info updated")
|
||||
|
||||
def get_current_timestamp_ms(self, date: str = None) -> int:
|
||||
@@ -640,21 +713,21 @@ class StorageService:
|
||||
state["last_update_date"] = date
|
||||
|
||||
self.save_internal_state(state)
|
||||
|
||||
self._generate_summary(state, net_value, prices)
|
||||
self._generate_holdings(state, prices)
|
||||
self._generate_stats(state, net_value)
|
||||
self._generate_trades(state)
|
||||
self.export_dashboard_compatibility_files(
|
||||
state,
|
||||
net_value=net_value,
|
||||
prices=prices,
|
||||
)
|
||||
|
||||
logger.info(f"Dashboard updated: net_value=${net_value:,.2f}")
|
||||
|
||||
def _generate_summary(
|
||||
def _build_summary_export(
|
||||
self,
|
||||
state: Dict[str, Any],
|
||||
net_value: float,
|
||||
prices: Dict[str, float],
|
||||
):
|
||||
"""Generate summary.json"""
|
||||
) -> Dict[str, Any]:
|
||||
"""Build compatibility summary export payload."""
|
||||
portfolio_state = state.get("portfolio_state", {})
|
||||
cash = portfolio_state.get("cash", self.initial_cash)
|
||||
|
||||
@@ -675,7 +748,7 @@ class StorageService:
|
||||
(net_value - self.initial_cash) / self.initial_cash
|
||||
) * 100
|
||||
|
||||
summary = {
|
||||
return {
|
||||
"totalAssetValue": round(net_value, 2),
|
||||
"totalReturn": round(total_return, 2),
|
||||
"cashPosition": round(cash, 2),
|
||||
@@ -689,14 +762,12 @@ class StorageService:
|
||||
"momentum": state.get("momentum_history", []),
|
||||
}
|
||||
|
||||
self.save_export_file("summary", summary)
|
||||
|
||||
def _generate_holdings(
|
||||
def _build_holdings_export(
|
||||
self,
|
||||
state: Dict[str, Any],
|
||||
prices: Dict[str, float],
|
||||
):
|
||||
"""Generate holdings.json"""
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Build compatibility holdings export payload."""
|
||||
portfolio_state = state.get("portfolio_state", {})
|
||||
positions = portfolio_state.get("positions", {})
|
||||
cash = portfolio_state.get("cash", self.initial_cash)
|
||||
@@ -750,18 +821,17 @@ class StorageService:
|
||||
|
||||
# Sort by weight
|
||||
holdings.sort(key=lambda x: abs(x["weight"]), reverse=True)
|
||||
return holdings
|
||||
|
||||
self.save_export_file("holdings", holdings)
|
||||
|
||||
def _generate_stats(self, state: Dict[str, Any], net_value: float):
|
||||
"""Generate stats.json"""
|
||||
def _build_stats_export(self, state: Dict[str, Any], net_value: float) -> Dict[str, Any]:
|
||||
"""Build compatibility stats export payload."""
|
||||
portfolio_state = state.get("portfolio_state", {})
|
||||
cash = portfolio_state.get("cash", self.initial_cash)
|
||||
total_return = (
|
||||
(net_value - self.initial_cash) / self.initial_cash
|
||||
) * 100
|
||||
|
||||
stats = {
|
||||
return {
|
||||
"totalAssetValue": round(net_value, 2),
|
||||
"totalReturn": round(total_return, 2),
|
||||
"cashPosition": round(cash, 2),
|
||||
@@ -774,10 +844,8 @@ class StorageService:
|
||||
},
|
||||
}
|
||||
|
||||
self.save_export_file("stats", stats)
|
||||
|
||||
def _generate_trades(self, state: Dict[str, Any]):
|
||||
"""Generate trades.json"""
|
||||
def _build_trades_export(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Build compatibility trades export payload."""
|
||||
all_trades = state.get("all_trades", [])
|
||||
|
||||
sorted_trades = sorted(
|
||||
@@ -800,7 +868,24 @@ class StorageService:
|
||||
},
|
||||
)
|
||||
|
||||
self.save_export_file("trades", trades)
|
||||
return trades
|
||||
|
||||
def export_dashboard_compatibility_files(
|
||||
self,
|
||||
state: Dict[str, Any],
|
||||
*,
|
||||
net_value: float,
|
||||
prices: Dict[str, float],
|
||||
) -> None:
|
||||
"""Write compatibility dashboard exports from current runtime state."""
|
||||
self.save_dashboard_exports(
|
||||
{
|
||||
"summary": self._build_summary_export(state, net_value, prices),
|
||||
"holdings": self._build_holdings_export(state, prices),
|
||||
"stats": self._build_stats_export(state, net_value),
|
||||
"trades": self._build_trades_export(state),
|
||||
},
|
||||
)
|
||||
|
||||
# Server State Management Methods
|
||||
|
||||
|
||||
Reference in New Issue
Block a user