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

@@ -90,6 +90,8 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
sys_prompt: Optional[str] = None,
max_iters: int = 10,
memory: Optional[Any] = None,
long_term_memory: Optional[Any] = None,
long_term_memory_mode: str = "static_control",
enable_tool_guard: bool = True,
enable_bootstrap_hook: bool = True,
enable_memory_compaction: bool = False,
@@ -97,6 +99,9 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
memory_compact_threshold: Optional[int] = None,
env_context: Optional[str] = None,
prompt_files: Optional[List[str]] = None,
# Portfolio manager specific parameters
initial_cash: Optional[float] = None,
margin_requirement: Optional[float] = None,
):
"""Initialize EvoAgent.
@@ -144,16 +149,24 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
# Initialize hook manager
self._hook_manager = HookManager()
# Build kwargs for parent ReActAgent
kwargs = {
"name": agent_id,
"model": model,
"sys_prompt": self._sys_prompt,
"toolkit": toolkit,
"memory": memory or InMemoryMemory(),
"formatter": formatter,
"max_iters": max_iters,
}
# Add long-term memory if provided
if long_term_memory:
kwargs["long_term_memory"] = long_term_memory
kwargs["long_term_memory_mode"] = long_term_memory_mode
# Initialize parent ReActAgent
super().__init__(
name=agent_id,
model=model,
sys_prompt=self._sys_prompt,
toolkit=toolkit,
memory=memory or InMemoryMemory(),
formatter=formatter,
max_iters=max_iters,
)
super().__init__(**kwargs)
# Register hooks
self._register_hooks(
@@ -366,6 +379,110 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
self.toolkit = new_toolkit
logger.info("Skills reloaded for agent: %s", self.agent_id)
def _make_decision(
self,
ticker: str,
action: str,
quantity: int,
confidence: int = 50,
reasoning: str = "",
) -> "ToolResponse":
"""Record a trading decision for a ticker (PM agent compatibility).
Args:
ticker: Stock ticker symbol (e.g., "AAPL")
action: Decision - "long", "short" or "hold"
quantity: Number of shares to trade (0 for hold)
confidence: Confidence level 0-100
reasoning: Explanation for this decision
Returns:
ToolResponse confirming decision recorded
"""
from agentscope.message import TextBlock
from agentscope.tool import ToolResponse
if action not in ["long", "short", "hold"]:
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Invalid action: {action}. Must be 'long', 'short', or 'hold'.",
),
],
)
# Store decision in metadata for retrieval
if not hasattr(self, "_decisions"):
self._decisions = {}
self._decisions[ticker] = {
"action": action,
"quantity": quantity if action != "hold" else 0,
"confidence": confidence,
"reasoning": reasoning,
}
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Decision recorded: {action} {quantity} shares of {ticker} "
f"(confidence: {confidence}%)",
),
],
)
def get_decisions(self) -> Dict[str, Dict]:
"""Get decisions from current cycle (PM compatibility)."""
return getattr(self, "_decisions", {}).copy()
def get_portfolio_state(self) -> Dict[str, Any]:
"""Get current portfolio state (PM compatibility)."""
return getattr(self, "_portfolio", {}).copy()
def load_portfolio_state(self, portfolio: Dict[str, Any]) -> None:
"""Load portfolio state (PM compatibility).
Args:
portfolio: Portfolio state dict with cash, positions, margin_used
"""
if not portfolio:
return
if not hasattr(self, "_portfolio"):
self._portfolio = {
"cash": 100000.0,
"positions": {},
"margin_used": 0.0,
"margin_requirement": 0.25,
}
self._portfolio = {
"cash": portfolio.get("cash", self._portfolio["cash"]),
"positions": portfolio.get("positions", {}).copy(),
"margin_used": portfolio.get("margin_used", 0.0),
"margin_requirement": portfolio.get(
"margin_requirement",
self._portfolio["margin_requirement"],
),
}
def update_portfolio(self, portfolio: Dict[str, Any]) -> None:
"""Update portfolio after external execution (PM compatibility).
Args:
portfolio: Portfolio updates to apply
"""
if not hasattr(self, "_portfolio"):
self._portfolio = {
"cash": 100000.0,
"positions": {},
"margin_used": 0.0,
"margin_requirement": 0.25,
}
self._portfolio.update(portfolio)
def rebuild_sys_prompt(self) -> None:
"""Rebuild and replace the system prompt at runtime.