Clawith/backend/app/api/advanced.py

293 lines
9.7 KiB
Python

"""Agent collaboration and template market API routes."""
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.permissions import check_agent_access
from app.core.security import get_current_user, get_current_admin
from app.database import get_db
from app.models.agent import Agent, AgentTemplate
from app.models.user import User
from app.services.collaboration import collaboration_service
router = APIRouter(tags=["advanced"])
# ─── Collaboration ──────────────────────────────────────
class DelegateRequest(BaseModel):
to_agent_id: uuid.UUID
task_title: str
task_description: str = ""
class InterAgentMessage(BaseModel):
to_agent_id: uuid.UUID
message: str
msg_type: str = "notify" # notify | consult
@router.get("/agents/{agent_id}/collaborators")
async def list_collaborators(
agent_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List agents that can collaborate with this agent."""
await check_agent_access(db, current_user, agent_id)
return await collaboration_service.list_collaborators(db, agent_id)
@router.post("/agents/{agent_id}/collaborate/delegate")
async def delegate_task(
agent_id: uuid.UUID,
data: DelegateRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delegate a task from one agent to another."""
await check_agent_access(db, current_user, agent_id)
try:
result = await collaboration_service.delegate_task(
db, agent_id, data.to_agent_id, data.task_title, data.task_description
)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/agents/{agent_id}/collaborate/message")
async def send_inter_agent_message(
agent_id: uuid.UUID,
data: InterAgentMessage,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Send a message between agents."""
await check_agent_access(db, current_user, agent_id)
return await collaboration_service.send_message_between_agents(
db, agent_id, data.to_agent_id, data.message, data.msg_type
)
# ─── Template Market ────────────────────────────────────
class TemplateCreate(BaseModel):
name: str
description: str = ""
icon: str = "🤖"
category: str = "general"
soul_template: str = ""
default_skills: list[str] = []
default_autonomy_policy: dict = {}
class TemplateOut(BaseModel):
id: uuid.UUID
name: str
description: str
icon: str
category: str
soul_template: str
default_skills: list
default_autonomy_policy: dict
is_builtin: bool
created_at: str | None = None
model_config = {"from_attributes": True}
@router.get("/templates", response_model=list[TemplateOut])
async def list_templates(
category: str | None = None,
db: AsyncSession = Depends(get_db),
):
"""List available agent templates."""
query = select(AgentTemplate).order_by(AgentTemplate.name)
if category:
query = query.where(AgentTemplate.category == category)
result = await db.execute(query)
return [TemplateOut.model_validate(t) for t in result.scalars().all()]
@router.get("/templates/{template_id}", response_model=TemplateOut)
async def get_template(template_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
"""Get template details."""
result = await db.execute(select(AgentTemplate).where(AgentTemplate.id == template_id))
template = result.scalar_one_or_none()
if not template:
raise HTTPException(status_code=404, detail="Template not found")
return TemplateOut.model_validate(template)
@router.post("/templates", response_model=TemplateOut, status_code=status.HTTP_201_CREATED)
async def create_template(
data: TemplateCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new agent template (share to template market)."""
template = AgentTemplate(
name=data.name,
description=data.description,
icon=data.icon,
category=data.category,
soul_template=data.soul_template,
default_skills=data.default_skills,
default_autonomy_policy=data.default_autonomy_policy,
created_by=current_user.id,
)
db.add(template)
await db.flush()
return TemplateOut.model_validate(template)
@router.delete("/templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_template(
template_id: uuid.UUID,
current_user: User = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Delete a template (admin or creator)."""
result = await db.execute(select(AgentTemplate).where(AgentTemplate.id == template_id))
template = result.scalar_one_or_none()
if not template:
raise HTTPException(status_code=404, detail="Template not found")
await db.delete(template)
# ─── Agent Handover ─────────────────────────────────────
class HandoverRequest(BaseModel):
new_creator_id: uuid.UUID
@router.post("/agents/{agent_id}/handover")
async def handover_agent(
agent_id: uuid.UUID,
data: HandoverRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Transfer ownership of a digital employee to another user."""
from app.core.permissions import is_agent_creator
from app.models.audit import AuditLog
agent, _access = await check_agent_access(db, current_user, agent_id)
if not is_agent_creator(current_user, agent):
raise HTTPException(status_code=403, detail="Only creator can handover agent")
# Verify new creator exists
new_creator_result = await db.execute(select(User).where(User.id == data.new_creator_id))
new_creator = new_creator_result.scalar_one_or_none()
if not new_creator:
raise HTTPException(status_code=404, detail="Target user not found")
old_creator_id = agent.creator_id
agent.creator_id = data.new_creator_id
db.add(AuditLog(
user_id=current_user.id,
agent_id=agent_id,
action="agent:handover",
details={
"from_creator": str(old_creator_id),
"to_creator": str(data.new_creator_id),
},
))
await db.flush()
return {
"status": "transferred",
"agent_name": agent.name,
"new_creator": new_creator.display_name,
}
# ─── Observability ──────────────────────────────────────
@router.get("/agents/{agent_id}/metrics")
async def get_agent_metrics(
agent_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get observability metrics for an agent."""
from sqlalchemy import func
from app.models.task import Task
from app.models.audit import AuditLog, ApprovalRequest
agent, _access = await check_agent_access(db, current_user, agent_id)
# Task stats
total_tasks = await db.execute(select(func.count(Task.id)).where(Task.agent_id == agent_id))
done_tasks = await db.execute(
select(func.count(Task.id)).where(Task.agent_id == agent_id, Task.status == "done")
)
pending_tasks = await db.execute(
select(func.count(Task.id)).where(Task.agent_id == agent_id, Task.status == "pending")
)
# Approval stats
total_approvals = await db.execute(
select(func.count(ApprovalRequest.id)).where(ApprovalRequest.agent_id == agent_id)
)
pending_approvals = await db.execute(
select(func.count(ApprovalRequest.id)).where(
ApprovalRequest.agent_id == agent_id, ApprovalRequest.status == "pending"
)
)
# Recent activity count (last 24h)
from datetime import datetime, timedelta, timezone
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
recent_actions = await db.execute(
select(func.count(AuditLog.id)).where(
AuditLog.agent_id == agent_id, AuditLog.created_at >= cutoff
)
)
# Container status
from app.services.agent_manager import agent_manager
container_status = agent_manager.get_container_status(agent)
# Extract scalar values (each result can only be consumed once)
_total_tasks = total_tasks.scalar() or 0
_done_tasks = done_tasks.scalar() or 0
_pending_tasks = pending_tasks.scalar() or 0
_total_approvals = total_approvals.scalar() or 0
_pending_approvals = pending_approvals.scalar() or 0
_recent_actions = recent_actions.scalar() or 0
return {
"agent_id": str(agent_id),
"agent_name": agent.name,
"status": agent.status,
"container": container_status,
"tokens": {
"used_today": agent.tokens_used_today,
"used_month": agent.tokens_used_month,
"limit_day": agent.max_tokens_per_day,
"limit_month": agent.max_tokens_per_month,
},
"tasks": {
"total": _total_tasks,
"done": _done_tasks,
"pending": _pending_tasks,
"completion_rate": round(
_done_tasks / max(_total_tasks, 1) * 100, 1
),
},
"approvals": {
"total": _total_approvals,
"pending": _pending_approvals,
},
"activity": {
"actions_last_24h": _recent_actions,
},
}