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

@@ -12,9 +12,10 @@ import asyncio
import os
from contextlib import AsyncExitStack
from pathlib import Path
from typing import Any, Dict, Optional, Callable
from typing import Any, Dict, List, Optional, Callable
from backend.agents import AnalystAgent, PMAgent, RiskAgent
from backend.agents import AnalystAgent, EvoAgent, PMAgent, RiskAgent
from backend.agents.agent_workspace import load_agent_workspace_config
from backend.agents.skills_manager import SkillsManager
from backend.agents.toolkit_factory import create_agent_toolkit, load_agent_profiles
from backend.agents.prompt_loader import get_prompt_loader
@@ -41,6 +42,9 @@ _prompt_loader = get_prompt_loader()
# Global gateway reference for cleanup
_gateway_instance: Optional[Gateway] = None
# Global long-term memory references for persistence
_long_term_memories: List[Any] = []
def _set_gateway(gateway: Optional[Gateway]) -> None:
"""Set global gateway reference."""
@@ -61,6 +65,101 @@ def stop_gateway() -> None:
_gateway_instance = None
def _set_long_term_memories(memories: List[Any]) -> None:
"""Set global long-term memory references."""
global _long_term_memories
_long_term_memories = memories
def _clear_long_term_memories() -> None:
"""Clear global long-term memory references."""
global _long_term_memories
_long_term_memories = []
def _persist_long_term_memories_sync() -> None:
"""
Synchronously persist all long-term memories before shutdown.
This function ensures all memory data is flushed to disk/vector store
before the process exits. Should be called during cleanup.
"""
global _long_term_memories
if not _long_term_memories:
return
import logging
logger = logging.getLogger(__name__)
logger.info(f"[MemoryPersistence] Persisting {len(_long_term_memories)} memory instances...")
for i, memory in enumerate(_long_term_memories):
try:
# Try to save memory if it has a save method
if hasattr(memory, 'save') and callable(getattr(memory, 'save')):
if hasattr(memory, 'sync') and callable(getattr(memory, 'sync')):
# Use sync version if available
memory.sync()
logger.debug(f"[MemoryPersistence] Synced memory {i}")
else:
# Try async save with event loop
import asyncio
try:
loop = asyncio.get_event_loop()
if loop.is_running():
# Schedule save in running loop
loop.create_task(memory.save())
logger.debug(f"[MemoryPersistence] Scheduled save for memory {i}")
else:
loop.run_until_complete(memory.save())
logger.debug(f"[MemoryPersistence] Saved memory {i}")
except RuntimeError:
# No event loop, skip async save
pass
# Try to flush any pending writes
if hasattr(memory, 'flush') and callable(getattr(memory, 'flush')):
memory.flush()
logger.debug(f"[MemoryPersistence] Flushed memory {i}")
except Exception as e:
logger.warning(f"[MemoryPersistence] Failed to persist memory {i}: {e}")
logger.info("[MemoryPersistence] Memory persistence complete")
async def _persist_long_term_memories_async() -> None:
"""
Asynchronously persist all long-term memories.
This is the preferred method for persisting memories when
an async context is available.
"""
global _long_term_memories
if not _long_term_memories:
return
import logging
logger = logging.getLogger(__name__)
logger.info(f"[MemoryPersistence] Persisting {len(_long_term_memories)} memory instances async...")
for i, memory in enumerate(_long_term_memories):
try:
# Try async save first
if hasattr(memory, 'save') and callable(getattr(memory, 'save')):
await memory.save()
logger.debug(f"[MemoryPersistence] Saved memory {i} (async)")
# Try flush if available
if hasattr(memory, 'flush') and callable(getattr(memory, 'flush')):
memory.flush()
logger.debug(f"[MemoryPersistence] Flushed memory {i}")
except Exception as e:
logger.warning(f"[MemoryPersistence] Failed to persist memory {i}: {e}")
logger.info("[MemoryPersistence] Async memory persistence complete")
def create_long_term_memory(agent_name: str, run_id: str, run_dir: Path):
"""Create ReMeTaskLongTermMemory for an agent."""
try:
@@ -96,6 +195,179 @@ def create_long_term_memory(agent_name: str, run_id: str, run_dir: Path):
)
def _resolve_evo_agent_ids() -> set[str]:
"""Return agent ids selected to use EvoAgent.
By default, all supported roles use EvoAgent.
"""
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"}
}
def _create_analyst_agent(
*,
analyst_type: str,
run_id: str,
model,
formatter,
skills_manager: SkillsManager,
active_skill_map: Dict[str, list[Path]],
long_term_memory=None,
):
"""Create one analyst agent, optionally using EvoAgent."""
active_skill_dirs = active_skill_map.get(analyst_type, [])
toolkit = create_agent_toolkit(
analyst_type,
run_id,
active_skill_dirs=active_skill_dirs,
)
use_evo_agent = analyst_type in _resolve_evo_agent_ids()
if use_evo_agent:
workspace_dir = skills_manager.get_agent_asset_dir(run_id, analyst_type)
agent_config = load_agent_workspace_config(workspace_dir / "agent.yaml")
agent = EvoAgent(
agent_id=analyst_type,
config_name=run_id,
workspace_dir=workspace_dir,
model=model,
formatter=formatter,
skills_manager=skills_manager,
prompt_files=agent_config.prompt_files,
long_term_memory=long_term_memory,
)
agent.toolkit = toolkit
setattr(agent, "workspace_id", run_id)
return agent
return AnalystAgent(
analyst_type=analyst_type,
toolkit=toolkit,
model=model,
formatter=formatter,
agent_id=analyst_type,
config={"config_name": run_id},
long_term_memory=long_term_memory,
)
def _create_risk_manager_agent(
*,
run_id: str,
model,
formatter,
skills_manager: SkillsManager,
active_skill_map: Dict[str, list[Path]],
long_term_memory=None,
):
"""Create the risk manager, optionally using EvoAgent."""
active_skill_dirs = active_skill_map.get("risk_manager", [])
toolkit = create_agent_toolkit(
"risk_manager",
run_id,
active_skill_dirs=active_skill_dirs,
)
use_evo_agent = "risk_manager" in _resolve_evo_agent_ids()
if use_evo_agent:
workspace_dir = skills_manager.get_agent_asset_dir(run_id, "risk_manager")
agent_config = load_agent_workspace_config(workspace_dir / "agent.yaml")
agent = EvoAgent(
agent_id="risk_manager",
config_name=run_id,
workspace_dir=workspace_dir,
model=model,
formatter=formatter,
skills_manager=skills_manager,
prompt_files=agent_config.prompt_files,
long_term_memory=long_term_memory,
)
agent.toolkit = toolkit
setattr(agent, "workspace_id", run_id)
return agent
return RiskAgent(
model=model,
formatter=formatter,
name="risk_manager",
config={"config_name": run_id},
long_term_memory=long_term_memory,
toolkit=toolkit,
)
def _create_portfolio_manager_agent(
*,
run_id: str,
model,
formatter,
initial_cash: float,
margin_requirement: float,
skills_manager: SkillsManager,
active_skill_map: Dict[str, list[Path]],
long_term_memory=None,
):
"""Create the portfolio manager, optionally using EvoAgent."""
active_skill_dirs = active_skill_map.get("portfolio_manager", [])
use_evo_agent = "portfolio_manager" in _resolve_evo_agent_ids()
if use_evo_agent:
workspace_dir = skills_manager.get_agent_asset_dir(
run_id,
"portfolio_manager",
)
agent_config = load_agent_workspace_config(workspace_dir / "agent.yaml")
agent = EvoAgent(
agent_id="portfolio_manager",
config_name=run_id,
workspace_dir=workspace_dir,
model=model,
formatter=formatter,
skills_manager=skills_manager,
prompt_files=agent_config.prompt_files,
initial_cash=initial_cash,
margin_requirement=margin_requirement,
long_term_memory=long_term_memory,
)
agent.toolkit = create_agent_toolkit(
"portfolio_manager",
run_id,
owner=agent,
active_skill_dirs=active_skill_dirs,
)
setattr(agent, "workspace_id", run_id)
return agent
return PMAgent(
name="portfolio_manager",
model=model,
formatter=formatter,
initial_cash=initial_cash,
margin_requirement=margin_requirement,
config={"config_name": run_id},
long_term_memory=long_term_memory,
toolkit_factory=create_agent_toolkit,
toolkit_factory_kwargs={
"active_skill_dirs": active_skill_dirs,
},
)
def create_agents(
run_id: str,
run_dir: Path,
@@ -129,11 +401,6 @@ def create_agents(
for analyst_type in ANALYST_TYPES:
model = get_agent_model(analyst_type)
formatter = get_agent_formatter(analyst_type)
toolkit = create_agent_toolkit(
analyst_type,
run_id,
active_skill_dirs=active_skill_map.get(analyst_type, []),
)
long_term_memory = None
if enable_long_term_memory:
@@ -141,13 +408,13 @@ def create_agents(
if long_term_memory:
long_term_memories.append(long_term_memory)
analyst = AnalystAgent(
analyst = _create_analyst_agent(
analyst_type=analyst_type,
toolkit=toolkit,
run_id=run_id,
model=model,
formatter=formatter,
agent_id=analyst_type,
config={"config_name": run_id},
skills_manager=skills_manager,
active_skill_map=active_skill_map,
long_term_memory=long_term_memory,
)
analysts.append(analyst)
@@ -159,17 +426,13 @@ def create_agents(
if risk_long_term_memory:
long_term_memories.append(risk_long_term_memory)
risk_manager = RiskAgent(
risk_manager = _create_risk_manager_agent(
run_id=run_id,
model=get_agent_model("risk_manager"),
formatter=get_agent_formatter("risk_manager"),
name="risk_manager",
config={"config_name": run_id},
skills_manager=skills_manager,
active_skill_map=active_skill_map,
long_term_memory=risk_long_term_memory,
toolkit=create_agent_toolkit(
"risk_manager",
run_id,
active_skill_dirs=active_skill_map.get("risk_manager", []),
),
)
# Create portfolio manager
@@ -179,18 +442,15 @@ def create_agents(
if pm_long_term_memory:
long_term_memories.append(pm_long_term_memory)
portfolio_manager = PMAgent(
name="portfolio_manager",
portfolio_manager = _create_portfolio_manager_agent(
run_id=run_id,
model=get_agent_model("portfolio_manager"),
formatter=get_agent_formatter("portfolio_manager"),
initial_cash=initial_cash,
margin_requirement=margin_requirement,
config={"config_name": run_id},
skills_manager=skills_manager,
active_skill_map=active_skill_map,
long_term_memory=pm_long_term_memory,
toolkit_factory=create_agent_toolkit,
toolkit_factory_kwargs={
"active_skill_dirs": active_skill_map.get("portfolio_manager", []),
},
)
return analysts, risk_manager, portfolio_manager, long_term_memories
@@ -400,6 +660,9 @@ async def run_pipeline(
)
_set_gateway(gateway)
# Set global memory references for persistence
_set_long_term_memories(long_term_memories)
# Start pipeline execution
async with AsyncExitStack() as stack:
# Enter long-term memory contexts
@@ -467,6 +730,12 @@ async def run_pipeline(
# Cleanup
logger.info("[Pipeline] Cleaning up...")
# Persist long-term memories before cleanup
try:
await _persist_long_term_memories_async()
except Exception as e:
logger.warning(f"[Pipeline] Memory persistence error: {e}")
# Stop Gateway
try:
stop_gateway()
@@ -474,6 +743,9 @@ async def run_pipeline(
except Exception as e:
logger.error(f"[Pipeline] Error stopping gateway: {e}")
# Clear memory references
_clear_long_term_memories()
clear_shutdown_event()
clear_global_runtime_manager()
from backend.api.runtime import unregister_runtime_manager