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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user