from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from typing import List
from sqlalchemy.orm import Session
from app.api.dependencies import ServiceContainer, get_db
from app.api import schemas
from app.db.models.agent import AgentTemplate, AgentInstance, AgentTrigger
from app.db.models import Message
from app.api.schemas import (
AgentTemplateCreate, AgentTemplateResponse,
AgentInstanceCreate, AgentInstanceResponse, AgentInstanceStatusUpdate
)
import uuid
import json
from app.core.orchestration.agent_loop import AgentExecutor
from sqlalchemy.orm import joinedload
def create_agents_router(services: ServiceContainer) -> APIRouter:
router = APIRouter()
@router.get("", response_model=List[AgentInstanceResponse])
def get_agents(db: Session = Depends(get_db)):
return db.query(AgentInstance).options(joinedload(AgentInstance.template)).all()
@router.post("/templates", response_model=AgentTemplateResponse)
def create_template(request: AgentTemplateCreate, db: Session = Depends(get_db)):
template = AgentTemplate(**request.model_dump())
db.add(template)
db.commit()
db.refresh(template)
return template
@router.post("/instances", response_model=AgentInstanceResponse)
def create_instance(request: AgentInstanceCreate, db: Session = Depends(get_db)):
# Verify template exists
template = db.query(AgentTemplate).filter(AgentTemplate.id == request.template_id).first()
if not template:
raise HTTPException(status_code=404, detail="Template not found")
instance = AgentInstance(**request.model_dump())
db.add(instance)
db.commit()
db.refresh(instance)
return instance
@router.patch("/{id}/status", response_model=AgentInstanceResponse)
def update_status(id: str, request: AgentInstanceStatusUpdate, db: Session = Depends(get_db)):
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
instance.status = request.status
db.commit()
db.refresh(instance)
return instance
@router.patch("/{id}/config", response_model=AgentInstanceResponse)
def update_config(id: str, request: schemas.AgentConfigUpdate, db: Session = Depends(get_db)):
from app.db.models.session import Session as SessionModel
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
template = db.query(AgentTemplate).filter(AgentTemplate.id == instance.template_id).first()
if request.name is not None and template:
template.name = request.name
if request.system_prompt is not None and template:
template.system_prompt_path = request.system_prompt
if request.max_loop_iterations is not None and template:
template.max_loop_iterations = request.max_loop_iterations
if request.mesh_node_id is not None:
instance.mesh_node_id = request.mesh_node_id
# Update the Session overriding prompt so the running loop picks it up instantly!
if instance.session_id:
session = db.query(SessionModel).filter(SessionModel.id == instance.session_id).first()
if session:
if request.system_prompt is not None:
session.system_prompt_override = request.system_prompt
if hasattr(request, 'provider_name') and request.provider_name is not None:
session.provider_name = request.provider_name
if request.mesh_node_id is not None:
session.attached_node_ids = [request.mesh_node_id] if request.mesh_node_id else []
db.commit()
db.refresh(instance)
return instance
@router.post("/{id}/webhook", status_code=202)
def webhook_receiver(id: str, payload: dict, background_tasks: BackgroundTasks, token: str = None, db: Session = Depends(get_db)):
# Validate instance
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
# Pass webhook event directly to the Agent Executor to process
prompt = f"Webhook Event: {json.dumps(payload)}"
background_tasks.add_task(AgentExecutor.run, instance.id, prompt, services.rag_service, services.user_service)
return {"message": "Accepted"}
@router.get("/{id}/triggers", response_model=List[schemas.AgentTriggerResponse])
def get_agent_triggers(id: str, db: Session = Depends(get_db)):
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
return db.query(AgentTrigger).filter(AgentTrigger.instance_id == id).all()
@router.post("/{id}/triggers", response_model=schemas.AgentTriggerResponse)
def create_agent_trigger(id: str, request: schemas.AgentTriggerCreate, db: Session = Depends(get_db)):
trigger = AgentTrigger(**request.model_dump())
trigger.instance_id = id # Ensure it maps safely
if trigger.trigger_type == "webhook" and not trigger.webhook_secret:
import secrets
trigger.webhook_secret = secrets.token_hex(16)
db.add(trigger)
db.commit()
db.refresh(trigger)
return trigger
@router.delete("/triggers/{trigger_id}")
def delete_agent_trigger(trigger_id: str, db: Session = Depends(get_db)):
trigger = db.query(AgentTrigger).filter(AgentTrigger.id == trigger_id).first()
if not trigger:
raise HTTPException(status_code=404, detail="Trigger not found")
db.delete(trigger)
db.commit()
return {"message": "Trigger deleted successfully"}
@router.get("/{id}/telemetry")
def get_telemetry(id: str, db: Session = Depends(get_db)):
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
# For MVP/Area 3, return mock telemetry data (e.g. baseline or from cgroup)
# Real cgroup-based metrics will come in Phase 2
return {
"cpu_usage": 2.5,
"memory_usage": 512,
"network_tx": 120,
"network_rx": 450
}
@router.get("/{id}/dependencies")
def get_dependencies(id: str, db: Session = Depends(get_db)):
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
return {
"dependencies": [],
"edges": []
}
@router.post("/deploy")
def deploy_agent(
request: schemas.DeployAgentRequest,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db)
):
"""
One-click agent deployment (Design Doc CUJ 1).
Atomically creates: Template → Session → Instance → Locks Session → Injects initial prompt → Starts loop.
"""
from app.db import models as db_models
# 1. Create Template
template = AgentTemplate(
name=request.name,
description=request.description,
system_prompt_path=request.system_prompt,
max_loop_iterations=request.max_loop_iterations
)
db.add(template)
db.flush()
# Resolve default provider mapping if user didn't select one
resolved_provider = request.provider_name
if not resolved_provider:
sys_prefs = services.user_service.get_system_settings(db)
resolved_provider = sys_prefs.get('llm', {}).get('default_provider', 'gemini')
# 2. Create a locked Session for the agent
new_session = db_models.Session(
user_id="agent-system",
provider_name=resolved_provider,
feature_name="agent_harness",
is_locked=True,
system_prompt_override=request.system_prompt
)
db.add(new_session)
db.flush()
# 3. Create AgentInstance
workspace_jail = f"/tmp/cortex/agent_{template.id[:8]}/"
instance = AgentInstance(
template_id=template.id,
session_id=new_session.id,
mesh_node_id=request.mesh_node_id,
status="idle",
current_workspace_jail=workspace_jail
)
db.add(instance)
db.flush()
# 4. Kick off agent loop if initial prompt was provided
# (Message insertion is handled automatically by the RAG service execution)
if request.initial_prompt:
instance.status = "active"
db.commit()
async def run_wrapper():
await AgentExecutor.run(instance.id, request.initial_prompt, services.rag_service, services.user_service)
background_tasks.add_task(run_wrapper)
else:
db.commit()
return {
"template_id": template.id,
"template_name": template.name,
"instance_id": instance.id,
"session_id": new_session.id,
"status": instance.status,
"workspace_jail": workspace_jail,
"message": f"Agent '{request.name}' deployed successfully"
}
@router.delete("/{id}")
def delete_agent(id: str, db: Session = Depends(get_db)):
from app.db.models.agent import AgentInstance
instance = db.query(AgentInstance).filter(AgentInstance.id == id).first()
if not instance:
raise HTTPException(status_code=404, detail="Agent not found")
# Stop the agent loop if it was active by deleting it (the loop will hit a None instance and return)
db.delete(instance)
db.commit()
return {"message": "Agent deleted successfully"}
return router