- Add agent core modules (agent_core, factory, registry, skill_loader) - Add runtime system for agent execution management - Add REST API for agents, workspaces, and runtime control - Add process supervisor for agent lifecycle management - Add workspace template system with agent profiles - Add frontend RuntimeView and runtime API integration - Add per-agent skill workspaces for smoke_fullstack run - Refactor skill system with active/installed separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
406 lines
11 KiB
Python
406 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Agent API Routes
|
|
|
|
Provides REST API endpoints for agent management within workspaces.
|
|
"""
|
|
from typing import Any, Dict, List, Optional
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Body
|
|
from pydantic import BaseModel, Field
|
|
|
|
from backend.agents import AgentFactory, WorkspaceManager, get_registry
|
|
from backend.agents.skills_manager import SkillsManager
|
|
|
|
router = APIRouter(prefix="/api/workspaces/{workspace_id}/agents", tags=["agents"])
|
|
|
|
|
|
# Request/Response Models
|
|
class CreateAgentRequest(BaseModel):
|
|
"""Request to create a new agent."""
|
|
agent_id: str = Field(..., description="Unique agent identifier")
|
|
agent_type: str = Field(..., description="Type of agent (e.g., technical_analyst)")
|
|
name: Optional[str] = Field(None, description="Display name")
|
|
description: Optional[str] = Field(None, description="Agent description")
|
|
clone_from: Optional[str] = Field(None, description="Agent ID to clone from")
|
|
llm_model_config: Optional[Dict[str, Any]] = Field(None, description="LLM model configuration")
|
|
|
|
|
|
class UpdateAgentRequest(BaseModel):
|
|
"""Request to update an agent."""
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
enabled_skills: Optional[List[str]] = None
|
|
disabled_skills: Optional[List[str]] = None
|
|
|
|
|
|
class AgentResponse(BaseModel):
|
|
"""Agent information response."""
|
|
agent_id: str
|
|
agent_type: str
|
|
workspace_id: str
|
|
config_path: str
|
|
agent_dir: str
|
|
status: str = "inactive"
|
|
|
|
|
|
class AgentFileResponse(BaseModel):
|
|
"""Agent file content response."""
|
|
filename: str
|
|
content: str
|
|
|
|
|
|
# Dependencies
|
|
def get_agent_factory():
|
|
"""Get AgentFactory instance."""
|
|
return AgentFactory()
|
|
|
|
|
|
def get_workspace_manager():
|
|
"""Get WorkspaceManager instance."""
|
|
return WorkspaceManager()
|
|
|
|
|
|
def get_skills_manager():
|
|
"""Get SkillsManager instance."""
|
|
return SkillsManager()
|
|
|
|
|
|
# Routes
|
|
@router.post("", response_model=AgentResponse)
|
|
async def create_agent(
|
|
workspace_id: str,
|
|
request: CreateAgentRequest,
|
|
factory: AgentFactory = Depends(get_agent_factory),
|
|
registry = Depends(get_registry),
|
|
):
|
|
"""
|
|
Create a new agent in a workspace.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
request: Agent creation parameters
|
|
|
|
Returns:
|
|
Created agent information
|
|
"""
|
|
# Check workspace exists
|
|
if not factory.workspaces_root.exists():
|
|
raise HTTPException(status_code=404, detail="Workspaces root not found")
|
|
|
|
workspace_dir = factory.workspaces_root / workspace_id
|
|
if not workspace_dir.exists():
|
|
raise HTTPException(status_code=404, detail=f"Workspace '{workspace_id}' not found")
|
|
|
|
try:
|
|
# Create agent
|
|
agent = factory.create_agent(
|
|
agent_id=request.agent_id,
|
|
agent_type=request.agent_type,
|
|
workspace_id=workspace_id,
|
|
clone_from=request.clone_from,
|
|
)
|
|
|
|
# Register in registry
|
|
registry.register(
|
|
agent_id=request.agent_id,
|
|
agent_type=request.agent_type,
|
|
workspace_id=workspace_id,
|
|
config_path=str(agent.config_path),
|
|
agent_dir=str(agent.agent_dir),
|
|
status="inactive",
|
|
)
|
|
|
|
return AgentResponse(
|
|
agent_id=agent.agent_id,
|
|
agent_type=agent.agent_type,
|
|
workspace_id=agent.workspace_id,
|
|
config_path=str(agent.config_path),
|
|
agent_dir=str(agent.agent_dir),
|
|
status="inactive",
|
|
)
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("", response_model=List[AgentResponse])
|
|
async def list_agents(
|
|
workspace_id: str,
|
|
factory: AgentFactory = Depends(get_agent_factory),
|
|
):
|
|
"""
|
|
List all agents in a workspace.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
|
|
Returns:
|
|
List of agents
|
|
"""
|
|
try:
|
|
agents_data = factory.list_agents(workspace_id=workspace_id)
|
|
return [
|
|
AgentResponse(
|
|
agent_id=agent["agent_id"],
|
|
agent_type=agent["agent_type"],
|
|
workspace_id=workspace_id,
|
|
config_path=agent["config_path"],
|
|
agent_dir=str(Path(agent["config_path"]).parent),
|
|
status="inactive",
|
|
)
|
|
for agent in agents_data
|
|
]
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
|
|
@router.get("/{agent_id}", response_model=AgentResponse)
|
|
async def get_agent(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
registry = Depends(get_registry),
|
|
):
|
|
"""
|
|
Get agent details.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
|
|
Returns:
|
|
Agent information
|
|
"""
|
|
agent_info = registry.get(agent_id)
|
|
|
|
if not agent_info or agent_info.workspace_id != workspace_id:
|
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
|
|
return AgentResponse(
|
|
agent_id=agent_info.agent_id,
|
|
agent_type=agent_info.agent_type,
|
|
workspace_id=agent_info.workspace_id,
|
|
config_path=agent_info.config_path,
|
|
agent_dir=agent_info.agent_dir,
|
|
status=agent_info.status,
|
|
)
|
|
|
|
|
|
@router.delete("/{agent_id}")
|
|
async def delete_agent(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
factory: AgentFactory = Depends(get_agent_factory),
|
|
registry = Depends(get_registry),
|
|
):
|
|
"""
|
|
Delete an agent.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
# Check agent exists in registry
|
|
agent_info = registry.get(agent_id)
|
|
if not agent_info or agent_info.workspace_id != workspace_id:
|
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
|
|
# Delete from factory
|
|
success = factory.delete_agent(agent_id, workspace_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
|
|
# Unregister
|
|
registry.unregister(agent_id)
|
|
|
|
return {"message": f"Agent '{agent_id}' deleted successfully"}
|
|
|
|
|
|
@router.patch("/{agent_id}", response_model=AgentResponse)
|
|
async def update_agent(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
request: UpdateAgentRequest,
|
|
registry = Depends(get_registry),
|
|
):
|
|
"""
|
|
Update agent configuration.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
request: Update parameters
|
|
|
|
Returns:
|
|
Updated agent information
|
|
"""
|
|
agent_info = registry.get(agent_id)
|
|
if not agent_info or agent_info.workspace_id != workspace_id:
|
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
|
|
# Update metadata in registry
|
|
metadata_updates = {}
|
|
if request.name:
|
|
metadata_updates["name"] = request.name
|
|
if request.description:
|
|
metadata_updates["description"] = request.description
|
|
|
|
if metadata_updates:
|
|
registry.update_metadata(agent_id, metadata_updates)
|
|
|
|
# Update skills if provided
|
|
if request.enabled_skills or request.disabled_skills:
|
|
skills_manager = SkillsManager()
|
|
skills_manager.update_agent_skill_overrides(
|
|
config_name=workspace_id,
|
|
agent_id=agent_id,
|
|
enable=request.enabled_skills or [],
|
|
disable=request.disabled_skills or [],
|
|
)
|
|
|
|
# Get updated info
|
|
agent_info = registry.get(agent_id)
|
|
return AgentResponse(
|
|
agent_id=agent_info.agent_id,
|
|
agent_type=agent_info.agent_type,
|
|
workspace_id=agent_info.workspace_id,
|
|
config_path=agent_info.config_path,
|
|
agent_dir=agent_info.agent_dir,
|
|
status=agent_info.status,
|
|
)
|
|
|
|
|
|
@router.post("/{agent_id}/skills/{skill_name}/enable")
|
|
async def enable_skill(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
skill_name: str,
|
|
registry = Depends(get_registry),
|
|
):
|
|
"""
|
|
Enable a skill for an agent.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
skill_name: Skill name to enable
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
agent_info = registry.get(agent_id)
|
|
if not agent_info or agent_info.workspace_id != workspace_id:
|
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
|
|
skills_manager = SkillsManager()
|
|
result = skills_manager.update_agent_skill_overrides(
|
|
config_name=workspace_id,
|
|
agent_id=agent_id,
|
|
enable=[skill_name],
|
|
)
|
|
|
|
return {
|
|
"message": f"Skill '{skill_name}' enabled for agent '{agent_id}'",
|
|
"enabled_skills": result["enabled_skills"],
|
|
}
|
|
|
|
|
|
@router.post("/{agent_id}/skills/{skill_name}/disable")
|
|
async def disable_skill(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
skill_name: str,
|
|
registry = Depends(get_registry),
|
|
):
|
|
"""
|
|
Disable a skill for an agent.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
skill_name: Skill name to disable
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
agent_info = registry.get(agent_id)
|
|
if not agent_info or agent_info.workspace_id != workspace_id:
|
|
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
|
|
skills_manager = SkillsManager()
|
|
result = skills_manager.update_agent_skill_overrides(
|
|
config_name=workspace_id,
|
|
agent_id=agent_id,
|
|
disable=[skill_name],
|
|
)
|
|
|
|
return {
|
|
"message": f"Skill '{skill_name}' disabled for agent '{agent_id}'",
|
|
"disabled_skills": result["disabled_skills"],
|
|
}
|
|
|
|
|
|
@router.get("/{agent_id}/files/{filename}", response_model=AgentFileResponse)
|
|
async def get_agent_file(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
filename: str,
|
|
workspace_manager: WorkspaceManager = Depends(get_workspace_manager),
|
|
):
|
|
"""
|
|
Read an agent's workspace file.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
filename: File to read (e.g., SOUL.md, ROLE.md)
|
|
|
|
Returns:
|
|
File content
|
|
"""
|
|
try:
|
|
content = workspace_manager.load_agent_file(
|
|
config_name=workspace_id,
|
|
agent_id=agent_id,
|
|
filename=filename,
|
|
)
|
|
return AgentFileResponse(filename=filename, content=content)
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=404, detail=f"File '{filename}' not found")
|
|
|
|
|
|
@router.put("/{agent_id}/files/{filename}", response_model=AgentFileResponse)
|
|
async def update_agent_file(
|
|
workspace_id: str,
|
|
agent_id: str,
|
|
filename: str,
|
|
content: str = Body(..., media_type="text/plain"),
|
|
workspace_manager: WorkspaceManager = Depends(get_workspace_manager),
|
|
):
|
|
"""
|
|
Update an agent's workspace file.
|
|
|
|
Args:
|
|
workspace_id: Workspace identifier
|
|
agent_id: Agent identifier
|
|
filename: File to update
|
|
content: New file content
|
|
|
|
Returns:
|
|
Updated file information
|
|
"""
|
|
try:
|
|
workspace_manager.update_agent_file(
|
|
config_name=workspace_id,
|
|
agent_id=agent_id,
|
|
filename=filename,
|
|
content=content,
|
|
)
|
|
return AgentFileResponse(filename=filename, content=content)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|