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

@@ -2,7 +2,10 @@
"""
Agent API Routes
Provides REST API endpoints for agent management within workspaces.
Provides REST API endpoints for both:
- design-time agent management under `workspaces/`
- run-scoped agent asset access under `runs/<run_id>/`
"""
import logging
import os
@@ -24,6 +27,30 @@ from backend.llm.models import get_agent_model_info
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/workspaces/{workspace_id}/agents", tags=["agents"])
DESIGN_SCOPE = "design_workspace"
RUNTIME_SCOPE = "runtime_run"
RUNTIME_SCOPE_NOTE = (
"For profile, skills, and editable agent files, `workspace_id` is treated "
"as the active run id under `runs/<run_id>/`, not as the design-time "
"`workspaces/` registry."
)
def _runtime_scope_fields() -> dict[str, str]:
return {
"scope_type": RUNTIME_SCOPE,
"scope_note": RUNTIME_SCOPE_NOTE,
}
def _design_scope_fields() -> dict[str, str]:
return {
"scope_type": DESIGN_SCOPE,
"scope_note": (
"For design-time CRUD routes on this surface, `workspace_id` refers "
"to the persistent registry under `workspaces/`."
),
}
# Request/Response Models
@@ -68,30 +95,40 @@ class AgentResponse(BaseModel):
config_path: str
agent_dir: str
status: str = "inactive"
scope_type: str = DESIGN_SCOPE
scope_note: Optional[str] = None
class AgentFileResponse(BaseModel):
"""Agent file content response."""
filename: str
content: str
scope_type: str = RUNTIME_SCOPE
scope_note: Optional[str] = None
class AgentProfileResponse(BaseModel):
agent_id: str
workspace_id: str
profile: Dict[str, Any]
scope_type: str = RUNTIME_SCOPE
scope_note: Optional[str] = None
class AgentSkillsResponse(BaseModel):
agent_id: str
workspace_id: str
skills: List[Dict[str, Any]]
scope_type: str = RUNTIME_SCOPE
scope_note: Optional[str] = None
class SkillDetailResponse(BaseModel):
agent_id: str
workspace_id: str
skill: Dict[str, Any]
scope_type: str = RUNTIME_SCOPE
scope_note: Optional[str] = None
# Dependencies
@@ -101,7 +138,7 @@ def get_agent_factory():
def get_workspace_manager():
"""Get run-scoped workspace manager instance."""
"""Get run-scoped asset manager for one runtime workspace/run id."""
return RunWorkspaceManager()
@@ -119,7 +156,7 @@ async def create_agent(
registry = Depends(get_registry),
):
"""
Create a new agent in a workspace.
Create a new agent in a design-time workspace registry entry.
Args:
workspace_id: Workspace identifier
@@ -162,6 +199,7 @@ async def create_agent(
config_path=str(agent.config_path),
agent_dir=str(agent.agent_dir),
status="inactive",
**_design_scope_fields(),
)
except ValueError as e:
@@ -174,7 +212,7 @@ async def list_agents(
factory: AgentFactory = Depends(get_agent_factory),
):
"""
List all agents in a workspace.
List all agents in a design-time workspace registry entry.
Args:
workspace_id: Workspace identifier
@@ -192,6 +230,7 @@ async def list_agents(
config_path=agent["config_path"],
agent_dir=str(Path(agent["config_path"]).parent),
status="inactive",
**_design_scope_fields(),
)
for agent in agents_data
]
@@ -206,7 +245,7 @@ async def get_agent(
registry = Depends(get_registry),
):
"""
Get agent details.
Get design-time agent details from the persistent workspace registry.
Args:
workspace_id: Workspace identifier
@@ -227,6 +266,7 @@ async def get_agent(
config_path=agent_info.config_path,
agent_dir=agent_info.agent_dir,
status=agent_info.status,
**_design_scope_fields(),
)
@@ -275,6 +315,7 @@ async def get_agent_profile(
"enabled_skills": agent_config.enabled_skills,
"disabled_skills": agent_config.disabled_skills,
},
**_runtime_scope_fields(),
)
@@ -310,7 +351,12 @@ async def get_agent_skills(
"status": status,
})
return AgentSkillsResponse(agent_id=agent_id, workspace_id=workspace_id, skills=payload)
return AgentSkillsResponse(
agent_id=agent_id,
workspace_id=workspace_id,
skills=payload,
**_runtime_scope_fields(),
)
@router.get("/{agent_id}/skills/{skill_name}", response_model=SkillDetailResponse)
@@ -329,7 +375,12 @@ async def get_agent_skill_detail(
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Unknown skill: {skill_name}")
return SkillDetailResponse(agent_id=agent_id, workspace_id=workspace_id, skill=detail)
return SkillDetailResponse(
agent_id=agent_id,
workspace_id=workspace_id,
skill=detail,
**_runtime_scope_fields(),
)
@router.delete("/{agent_id}")
@@ -416,6 +467,7 @@ async def update_agent(
config_path=agent_info.config_path,
agent_dir=agent_info.agent_dir,
status=agent_info.status,
**_design_scope_fields(),
)
@@ -656,7 +708,7 @@ async def get_agent_file(
workspace_manager: RunWorkspaceManager = Depends(get_workspace_manager),
):
"""
Read an agent's workspace file.
Read an agent file from the run-scoped asset tree under `runs/<run_id>/`.
Args:
workspace_id: Workspace identifier
@@ -672,7 +724,11 @@ async def get_agent_file(
agent_id=agent_id,
filename=filename,
)
return AgentFileResponse(filename=filename, content=content)
return AgentFileResponse(
filename=filename,
content=content,
**_runtime_scope_fields(),
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"File '{filename}' not found")
@@ -686,7 +742,7 @@ async def update_agent_file(
workspace_manager: RunWorkspaceManager = Depends(get_workspace_manager),
):
"""
Update an agent's workspace file.
Update an agent file in the run-scoped asset tree under `runs/<run_id>/`.
Args:
workspace_id: Workspace identifier
@@ -704,6 +760,10 @@ async def update_agent_file(
filename=filename,
content=content,
)
return AgentFileResponse(filename=filename, content=content)
return AgentFileResponse(
filename=filename,
content=content,
**_runtime_scope_fields(),
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))