Files
evotraders/backend/api/workspaces.py
cillin 16b54d5ccc 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
2026-04-02 00:55:08 +08:00

198 lines
5.5 KiB
Python

# -*- coding: utf-8 -*-
"""
Workspace API Routes.
These routes manage the design-time `workspaces/` registry, not the run-scoped
runtime data under `runs/<run_id>/`.
"""
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from backend.agents import WorkspaceManager
router = APIRouter(prefix="/api/workspaces", tags=["workspaces"])
# Request/Response Models
class CreateWorkspaceRequest(BaseModel):
"""Request to create a new workspace."""
workspace_id: str = Field(..., description="Unique workspace identifier")
name: Optional[str] = Field(None, description="Display name")
description: Optional[str] = Field(None, description="Workspace description")
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata")
class UpdateWorkspaceRequest(BaseModel):
"""Request to update a workspace."""
name: Optional[str] = None
description: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
class WorkspaceResponse(BaseModel):
"""Design-time workspace information response."""
workspace_id: str
name: str
description: str
created_at: Optional[str] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
class WorkspaceListResponse(BaseModel):
"""List of workspaces response."""
workspaces: List[WorkspaceResponse]
total: int
# Dependencies
def get_workspace_manager():
"""Get WorkspaceManager instance."""
return WorkspaceManager()
# Routes
@router.post("", response_model=WorkspaceResponse)
async def create_workspace(
request: CreateWorkspaceRequest,
manager: WorkspaceManager = Depends(get_workspace_manager),
):
"""
Create a new workspace.
Args:
request: Workspace creation parameters
Returns:
Created workspace information
"""
try:
config = manager.create_workspace(
workspace_id=request.workspace_id,
name=request.name,
description=request.description,
metadata=request.metadata or {},
)
return WorkspaceResponse(
workspace_id=config.workspace_id,
name=config.name,
description=config.description,
created_at=config.created_at,
metadata=config.metadata,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("", response_model=WorkspaceListResponse)
async def list_workspaces(
manager: WorkspaceManager = Depends(get_workspace_manager),
):
"""
List all design-time workspaces.
Returns:
List of design-time workspaces
"""
workspaces = manager.list_workspaces()
return WorkspaceListResponse(
workspaces=[
WorkspaceResponse(
workspace_id=ws.workspace_id,
name=ws.name,
description=ws.description,
created_at=ws.created_at,
metadata=ws.metadata,
)
for ws in workspaces
],
total=len(workspaces),
)
@router.get("/{workspace_id}", response_model=WorkspaceResponse)
async def get_workspace(
workspace_id: str,
manager: WorkspaceManager = Depends(get_workspace_manager),
):
"""
Get workspace details.
Args:
workspace_id: Workspace identifier
Returns:
Workspace information
"""
workspace = manager.get_workspace(workspace_id)
if not workspace:
raise HTTPException(status_code=404, detail=f"Workspace '{workspace_id}' not found")
return WorkspaceResponse(
workspace_id=workspace["workspace_id"],
name=workspace.get("name", workspace_id),
description=workspace.get("description", ""),
created_at=workspace.get("created_at"),
metadata=workspace.get("metadata", {}),
)
@router.patch("/{workspace_id}", response_model=WorkspaceResponse)
async def update_workspace(
workspace_id: str,
request: UpdateWorkspaceRequest,
manager: WorkspaceManager = Depends(get_workspace_manager),
):
"""
Update workspace configuration.
Args:
workspace_id: Workspace identifier
request: Update parameters
Returns:
Updated workspace information
"""
try:
config = manager.update_workspace_config(
workspace_id=workspace_id,
name=request.name,
description=request.description,
metadata=request.metadata,
)
return WorkspaceResponse(
workspace_id=config.workspace_id,
name=config.name,
description=config.description,
created_at=config.created_at,
metadata=config.metadata,
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.delete("/{workspace_id}")
async def delete_workspace(
workspace_id: str,
force: bool = False,
manager: WorkspaceManager = Depends(get_workspace_manager),
):
"""
Delete a workspace.
Args:
workspace_id: Workspace identifier
force: If True, delete even if workspace has agents
Returns:
Success message
"""
try:
success = manager.delete_workspace(workspace_id, force=force)
if not success:
raise HTTPException(status_code=404, detail=f"Workspace '{workspace_id}' not found")
return {"message": f"Workspace '{workspace_id}' deleted successfully"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))