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:
2026-04-02 00:55:08 +08:00
parent 0fa413380c
commit 16b54d5ccc
73 changed files with 9454 additions and 904 deletions

View File

@@ -26,13 +26,45 @@ from backend.agents.team_pipeline_config import (
resolve_active_analysts,
update_active_analysts,
)
from backend.agents import AnalystAgent
from backend.agents import AnalystAgent, EvoAgent
from backend.agents.agent_workspace import load_agent_workspace_config
from backend.agents.toolkit_factory import create_agent_toolkit
from backend.agents.workspace_manager import WorkspaceManager
from backend.agents.prompt_loader import get_prompt_loader
from backend.llm.models import get_agent_formatter, get_agent_model
from backend.config.constants import ANALYST_TYPES
def _resolve_evo_agent_ids() -> set[str]:
"""Return agent ids selected to use EvoAgent.
By default, all supported roles use EvoAgent.
EVO_AGENT_IDS can be used to limit to specific roles.
Supported roles:
- analyst roles (fundamentals, technical, sentiment, valuation)
- risk_manager
- portfolio_manager
Example:
EVO_AGENT_IDS=fundamentals_analyst,risk_manager,portfolio_manager
"""
raw = os.getenv("EVO_AGENT_IDS", "")
if not raw.strip():
# Default: all supported roles use EvoAgent
return set(ANALYST_TYPES) | {"risk_manager", "portfolio_manager"}
requested = {
item.strip()
for item in raw.split(",")
if item.strip()
}
return {
agent_id
for agent_id in requested
if agent_id in ANALYST_TYPES or agent_id in {"risk_manager", "portfolio_manager"}
}
# Team infrastructure imports (graceful import - may not exist yet)
try:
from backend.agents.team.team_coordinator import TeamCoordinator
@@ -140,6 +172,10 @@ class TradingPipeline:
session_key = TradingSessionKey(date=date).key()
self._session_key = session_key
active_analysts = self._get_active_analysts()
self._sync_agent_runtime_context(
agents=active_analysts + [self.risk_manager, self.pm],
session_key=session_key,
)
if self.runtime_manager:
self.runtime_manager.set_session_key(session_key)
self._runtime_log_event("cycle:start", {"tickers": tickers, "date": date})
@@ -1488,108 +1524,6 @@ class TradingPipeline:
return "Decisions: " + "; ".join(decision_texts)
return "Portfolio analysis completed. No trades recommended."
def load_agents_from_workspace(
self,
workspace_id: str,
agent_factory: Optional[Any] = None,
) -> Dict[str, Any]:
"""
Load agents from workspace using AgentFactory.
This method supports the new EvoAgent architecture by loading
agents from a workspace instead of using hardcoded agents.
Args:
workspace_id: Workspace identifier
agent_factory: Optional AgentFactory instance (uses self.agent_factory if None)
Returns:
Dictionary with loaded agents:
{
"analysts": List[EvoAgent],
"risk_manager": EvoAgent,
"portfolio_manager": EvoAgent,
}
Raises:
ValueError: If workspace doesn't exist or no agents found
"""
factory = agent_factory or self.agent_factory
if factory is None:
from backend.agents import AgentFactory
factory = AgentFactory()
# Check workspace exists
if not factory.workspaces_root.exists():
raise ValueError(f"Workspaces root does not exist: {factory.workspaces_root}")
workspace_dir = factory.workspaces_root / workspace_id
if not workspace_dir.exists():
raise ValueError(f"Workspace '{workspace_id}' does not exist")
# Load agents from workspace
agents_data = factory.list_agents(workspace_id=workspace_id)
if not agents_data:
raise ValueError(f"No agents found in workspace '{workspace_id}'")
# Categorize agents by type
analysts = []
risk_manager = None
portfolio_manager = None
for agent_data in agents_data:
agent_type = agent_data.get("agent_type", "unknown")
agent_id = agent_data.get("agent_id")
# Load full agent configuration
config_path = Path(agent_data.get("config_path", ""))
if config_path.exists():
agent = factory.load_agent(agent_id, workspace_id)
if agent_type.endswith("_analyst"):
analysts.append(agent)
elif agent_type == "risk_manager":
risk_manager = agent
elif agent_type == "portfolio_manager":
portfolio_manager = agent
if not analysts:
raise ValueError(f"No analysts found in workspace '{workspace_id}'")
if risk_manager is None:
raise ValueError(f"No risk_manager found in workspace '{workspace_id}'")
if portfolio_manager is None:
raise ValueError(f"No portfolio_manager found in workspace '{workspace_id}'")
return {
"analysts": analysts,
"risk_manager": risk_manager,
"portfolio_manager": portfolio_manager,
}
def reload_agents_from_workspace(self, workspace_id: Optional[str] = None) -> None:
"""
Reload all agents from workspace.
This updates self.analysts, self.risk_manager, and self.pm
with agents loaded from the specified workspace.
Args:
workspace_id: Workspace ID (uses self.workspace_id if None)
"""
ws_id = workspace_id or self.workspace_id
if not ws_id:
raise ValueError("No workspace_id specified")
loaded = self.load_agents_from_workspace(ws_id)
self.analysts = loaded["analysts"]
self.risk_manager = loaded["risk_manager"]
self.pm = loaded["portfolio_manager"]
self.workspace_id = ws_id
logger.info(f"Reloaded {len(self.analysts)} analysts from workspace '{ws_id}'")
def _runtime_update_status(self, agent: Any, status: str) -> None:
if not self.runtime_manager:
return
@@ -1602,6 +1536,28 @@ class TradingPipeline:
for agent in agents:
self._runtime_update_status(agent, status)
def _sync_agent_runtime_context(
self,
agents: List[Any],
session_key: str,
) -> None:
"""Propagate run/session identifiers onto agent instances.
EvoAgent's tool-guard approval records depend on workspace/session
context being present on the agent object at runtime.
"""
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
for agent in agents:
try:
setattr(agent, "session_id", session_key)
if not getattr(agent, "run_id", None):
setattr(agent, "run_id", config_name)
# Keep workspace_id for backward compatibility
if not getattr(agent, "workspace_id", None):
setattr(agent, "workspace_id", config_name)
except Exception:
continue
def _all_analysts(self) -> List[Any]:
"""Return static analysts plus runtime-created analysts."""
return list(self.analysts) + list(self._dynamic_analysts.values())
@@ -1630,18 +1586,46 @@ class TradingPipeline:
),
)
agent = AnalystAgent(
analyst_type=analyst_type,
toolkit=create_agent_toolkit(
# Determine whether to use EvoAgent based on EVO_AGENT_IDS
use_evo_agent = analyst_type in _resolve_evo_agent_ids()
if use_evo_agent:
from backend.agents.skills_manager import SkillsManager
skills_manager = SkillsManager(project_root=project_root)
workspace_dir = skills_manager.get_agent_asset_dir(
config_name,
agent_id,
)
agent_config = load_agent_workspace_config(workspace_dir / "agent.yaml")
agent = EvoAgent(
agent_id=agent_id,
config_name=config_name,
workspace_dir=workspace_dir,
model=get_agent_model(analyst_type),
formatter=get_agent_formatter(analyst_type),
prompt_files=agent_config.prompt_files,
)
agent.toolkit = create_agent_toolkit(
agent_id=agent_id,
config_name=config_name,
active_skill_dirs=[],
),
model=get_agent_model(analyst_type),
formatter=get_agent_formatter(analyst_type),
agent_id=agent_id,
config={"config_name": config_name},
)
)
setattr(agent, "run_id", config_name)
# Keep workspace_id for backward compatibility
setattr(agent, "workspace_id", config_name)
else:
agent = AnalystAgent(
analyst_type=analyst_type,
toolkit=create_agent_toolkit(
agent_id=agent_id,
config_name=config_name,
active_skill_dirs=[],
),
model=get_agent_model(analyst_type),
formatter=get_agent_formatter(analyst_type),
agent_id=agent_id,
config={"config_name": config_name},
)
self._dynamic_analysts[agent_id] = agent
update_active_analysts(
project_root=project_root,