293 lines
9.7 KiB
Python
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,
|
|
},
|
|
}
|