ligbox-ops-platform/projects/ops-desk/api/app/agents/routes.py
Ligbox Spec Hub e0959e6fd7 Add Agentic Ops Spec 029: wire API, worker tick, T0/T1, staging stack.
Mounts agents router and schema init, adds VM123 checks, chat copilot,
Desk UI module, isolated docker-compose staging on ports 8180/8192,
and full spec documentation without touching production ports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 23:22:33 +00:00

145 lines
4.5 KiB
Python

"""Agentic API — Spec 029."""
from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from app import auth
from app.agents import llm_client, runner, store
router = APIRouter(prefix="/api/v1/agents", tags=["agents"])
def _db():
conn = auth.db()
try:
yield conn
finally:
conn.close()
def _ops_view(user):
if user.role not in (
"super_admin",
"ops_lead",
"technician",
"noc",
"agentic_operator",
"developer",
"devops",
"security_analyst",
):
raise HTTPException(403, "insufficient permissions")
class ChatRequest(BaseModel):
question: str = Field(..., min_length=2, max_length=4000)
include_findings: bool = True
@router.get("/health")
def agents_health():
return {
"status": "ok",
"tier": "t1" if llm_client.AGENTIC_LLM_ENABLED else "t0",
"ollama": llm_client.ollama_available(),
"ollama_url": llm_client.OLLAMA_BASE_URL,
"model": llm_client.AGENTIC_LLM_MODEL,
"embed_model": llm_client.AGENTIC_EMBED_MODEL,
}
@router.get("/scenarios")
def list_scenarios(user=Depends(auth.get_current_user), conn=Depends(_db)):
_ops_view(user)
runner.sync_registry(conn)
conn.commit()
return {"scenarios": store.list_scenarios(conn)}
@router.get("/findings")
def list_findings(
user=Depends(auth.get_current_user),
conn=Depends(_db),
severity: str | None = None,
limit: int = Query(50, ge=1, le=200),
open_only: bool = True,
):
_ops_view(user)
return {"findings": store.list_findings(conn, severity=severity, limit=limit, open_only=open_only)}
@router.post("/findings/{finding_id}/ack")
def ack_finding(finding_id: int, user=Depends(auth.get_current_user), conn=Depends(_db)):
_ops_view(user)
if not conn.execute("SELECT id FROM agent_findings WHERE id=?", (finding_id,)).fetchone():
raise HTTPException(404, "not found")
now = datetime.now(timezone.utc).isoformat()
conn.execute(
"UPDATE agent_findings SET acknowledged_at=?, acknowledged_by=? WHERE id=?",
(now, user.username, finding_id),
)
store.log_event(conn, event_type="finding.ack", message=f"#{finding_id}", payload={"by": user.username})
conn.commit()
return {"ok": True, "id": finding_id}
@router.get("/action-log")
def action_log(user=Depends(auth.get_current_user), conn=Depends(_db), limit: int = Query(100, ge=1, le=500)):
_ops_view(user)
return {"events": store.list_action_log(conn, limit=limit)}
@router.post("/runs/{scenario_id}")
def trigger_run(scenario_id: str, user=Depends(auth.get_current_user), conn=Depends(_db)):
if user.role not in ("super_admin", "ops_lead", "agentic_operator"):
raise HTTPException(403, "insufficient permissions")
r = runner.run_scenario(conn, scenario_id, trigger=f"manual:{user.username}")
conn.commit()
return r
@router.post("/chat")
def agent_chat(body: ChatRequest, user=Depends(auth.get_current_user), conn=Depends(_db)):
"""T1 context window — copiloto ops para utilizadores Desk."""
_ops_view(user)
kb = store.search_kb(conn, body.question)
findings_summary = ""
if body.include_findings:
open_f = store.list_findings(conn, limit=8, open_only=True)
if open_f:
findings_summary = "\n".join(
f"- [{f['severity']}] {f['title']}: {f.get('suggested_human_action') or ''}" for f in open_f
)
answer, model = llm_client.chat_context(
question=body.question,
kb_snippets=[k["snippet"] for k in kb],
findings_summary=findings_summary,
user_role=user.role,
)
store.log_event(
conn,
event_type="chat.query",
message=body.question[:120],
agent_id="advisor",
payload={"user": user.username, "model": model},
)
conn.commit()
return {"answer": answer, "model": model, "kb_hits": len(kb)}
@router.post("/internal/tick")
def internal_tick(user=Depends(auth.require_internal_or_user), conn=Depends(_db)):
kb = runner.index_specs_kb(conn)
result = runner.run_all_enabled(conn, trigger="cron")
store.log_event(
conn,
event_type="tick.complete",
message=f"kb={kb} runs={result['total']}",
agent_id="orchestrator",
payload={"kb": kb, **result},
)
conn.commit()
return {"ok": True, "kb_indexed": kb, **result}