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

@@ -13,7 +13,7 @@ import asyncio
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime
from datetime import UTC, datetime
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Set
@@ -73,11 +73,13 @@ class ApprovalRecord:
self.tool_name = tool_name
self.tool_input = tool_input
self.agent_id = agent_id
# run_id is the new preferred name; workspace_id is kept for backward compatibility
self.run_id = workspace_id
self.workspace_id = workspace_id
self.session_id = session_id
self.status = ApprovalStatus.PENDING
self.findings = findings or []
self.created_at = datetime.utcnow()
self.created_at = datetime.now(UTC)
self.resolved_at: Optional[datetime] = None
self.resolved_by: Optional[str] = None
self.metadata: Dict[str, Any] = {}
@@ -90,6 +92,7 @@ class ApprovalRecord:
"tool_name": self.tool_name,
"tool_input": self.tool_input,
"agent_id": self.agent_id,
"run_id": self.run_id,
"workspace_id": self.workspace_id,
"session_id": self.session_id,
"findings": [f.to_dict() for f in self.findings],
@@ -161,7 +164,7 @@ class ToolGuardStore:
return record
record.status = status
record.resolved_at = datetime.utcnow()
record.resolved_at = datetime.now(UTC)
record.resolved_by = resolved_by
if notify_request and record.pending_request:
if status == ApprovalStatus.APPROVED:
@@ -395,18 +398,34 @@ class ToolGuardMixin:
)
manager = get_global_runtime_manager()
approval_data = {
"tool_name": record.tool_name,
"agent_id": record.agent_id,
"workspace_id": record.workspace_id,
"session_id": record.session_id,
"tool_input": record.tool_input,
}
if manager:
manager.register_pending_approval(
record.approval_id,
{
"tool_name": record.tool_name,
"agent_id": record.agent_id,
"workspace_id": record.workspace_id,
"session_id": record.session_id,
"tool_input": record.tool_input,
},
approval_data,
)
# Broadcast WebSocket event for real-time UI updates
try:
if hasattr(manager, 'broadcast_event'):
await manager.broadcast_event({
"type": "approval_requested",
"approval_id": record.approval_id,
"agent_id": record.agent_id,
"tool_name": record.tool_name,
"timestamp": record.created_at.isoformat(),
"data": approval_data,
})
except Exception as e:
logger.warning(f"Failed to broadcast approval event: {e}")
self._pending_approval = ToolApprovalRequest(
approval_id=record.approval_id,
tool_name=tool_name,