SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
+1160
-31
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ AGENT_STATUSES = [
|
||||
@router.get("")
|
||||
async def agents_page(request: Request):
|
||||
active_tab = request.query_params.get("tab", "souls")
|
||||
if active_tab not in {"souls", "mesh", "approvals"}:
|
||||
if active_tab not in {"souls", "mesh", "terminals", "approvals"}:
|
||||
active_tab = "souls"
|
||||
events: list = []
|
||||
try:
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"""Agents API — souls & activity."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.db import fetch_all
|
||||
from app.services import agent_approvals, agent_souls
|
||||
from app.db import fetch_all, fetch_one
|
||||
from app.services import agent_approvals, agent_integration, agent_souls
|
||||
from app.services.agent_names import normalize_agent_key
|
||||
from app.services.agent_terminal import execute_terminal_command
|
||||
|
||||
router = APIRouter(prefix="/api/agents", tags=["agents-api"])
|
||||
|
||||
@@ -35,6 +40,236 @@ class ExecuteBody(BaseModel):
|
||||
result: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class HandoffBody(BaseModel):
|
||||
from_agent: str
|
||||
to_agent: str
|
||||
handoff_type: str = "partner"
|
||||
payload: dict[str, Any] = Field(default_factory=dict)
|
||||
correlation_id: Optional[str] = None
|
||||
|
||||
|
||||
class TerminalCommandBody(BaseModel):
|
||||
command: str = Field(..., min_length=1, max_length=2000)
|
||||
|
||||
|
||||
def _aggregate_agent_stats() -> dict[str, dict[str, Any]]:
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
rows = fetch_all(
|
||||
"""
|
||||
SELECT agent_name, title, status, created_at
|
||||
FROM agent_events
|
||||
WHERE created_at >= NOW() - INTERVAL '7 days'
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
)
|
||||
now = datetime.now(timezone.utc)
|
||||
by_key: dict[str, dict[str, Any]] = {}
|
||||
for r in rows:
|
||||
key = normalize_agent_key(str(r.get("agent_name") or ""))
|
||||
if not key:
|
||||
continue
|
||||
bucket = by_key.setdefault(
|
||||
key,
|
||||
{"events_6h": 0, "errors_24h": 0, "last_event_at": None, "last_event_title": None},
|
||||
)
|
||||
created = r.get("created_at")
|
||||
if bucket["last_event_at"] is None and created is not None:
|
||||
bucket["last_event_at"] = created
|
||||
bucket["last_event_title"] = r.get("title")
|
||||
if created is not None:
|
||||
if hasattr(created, "tzinfo") and created.tzinfo is None:
|
||||
created = created.replace(tzinfo=timezone.utc)
|
||||
if created >= now - timedelta(hours=6):
|
||||
bucket["events_6h"] += 1
|
||||
if created >= now - timedelta(hours=24) and str(r.get("status") or "") in ("error", "rejected"):
|
||||
bucket["errors_24h"] += 1
|
||||
return by_key
|
||||
|
||||
|
||||
def _node_health(events_6h: int, errors_24h: int, event_count: int) -> str:
|
||||
if events_6h > 0 and errors_24h == 0:
|
||||
return "healthy"
|
||||
if events_6h > 0:
|
||||
return "warn"
|
||||
if event_count > 0:
|
||||
return "idle"
|
||||
return "offline"
|
||||
|
||||
|
||||
@router.get("/collaboration")
|
||||
def api_collaboration() -> dict[str, Any]:
|
||||
items = agent_integration.list_collaboration()
|
||||
return {"items": items, "count": len(items)}
|
||||
|
||||
|
||||
@router.post("/handoff")
|
||||
def api_create_handoff(body: HandoffBody) -> dict[str, Any]:
|
||||
try:
|
||||
handoff = agent_integration.create_handoff(
|
||||
body.from_agent,
|
||||
body.to_agent,
|
||||
handoff_type=body.handoff_type,
|
||||
payload=body.payload,
|
||||
correlation_id=body.correlation_id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
return {"ok": True, "handoff": handoff}
|
||||
|
||||
|
||||
def _terminal_payload(ev: dict[str, Any]) -> dict[str, Any]:
|
||||
if ev.get("created_at") and hasattr(ev["created_at"], "isoformat"):
|
||||
ev["created_at"] = ev["created_at"].isoformat()
|
||||
meta = ev.get("metadata")
|
||||
if isinstance(meta, str):
|
||||
try:
|
||||
meta = json.loads(meta)
|
||||
except Exception:
|
||||
meta = {}
|
||||
ev["metadata"] = meta or {}
|
||||
line_type = "handoff" if "handoff" in str(ev.get("event_type") or "") else "action"
|
||||
if str(ev.get("event_type") or "").startswith("terminal_"):
|
||||
line_type = "command" if ev.get("event_type") == "terminal_in" else "output"
|
||||
if ev["metadata"].get("target_agent"):
|
||||
line_type = "handoff_out"
|
||||
if ev["metadata"].get("source_agent"):
|
||||
line_type = "handoff_in"
|
||||
if str(ev.get("status") or "") in ("error", "rejected"):
|
||||
line_type = "error"
|
||||
return {
|
||||
"type": line_type,
|
||||
"id": ev.get("id"),
|
||||
"agent": normalize_agent_key(ev.get("agent_name")),
|
||||
"message": ev.get("title") or ev.get("event_type"),
|
||||
"detail": (ev.get("body") or "")[:500],
|
||||
"status": ev.get("status"),
|
||||
"correlation_id": ev["metadata"].get("correlation_id"),
|
||||
"at": ev.get("created_at"),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/souls/{agent_key}/terminal/stream")
|
||||
async def api_agent_terminal_stream(agent_key: str, correlation_id: Optional[str] = None):
|
||||
key = normalize_agent_key(agent_key)
|
||||
soul = agent_souls.get_soul(key)
|
||||
if not soul:
|
||||
raise HTTPException(404, "Agent not found")
|
||||
|
||||
async def event_gen():
|
||||
last_id = 0
|
||||
try:
|
||||
hist = fetch_all(
|
||||
"""
|
||||
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
|
||||
FROM agent_events
|
||||
WHERE LOWER(agent_name) = %s
|
||||
ORDER BY id DESC
|
||||
LIMIT 30
|
||||
""",
|
||||
(key,),
|
||||
)
|
||||
for row in reversed(hist):
|
||||
last_id = max(last_id, int(row["id"]))
|
||||
payload = _terminal_payload(dict(row))
|
||||
yield f"data: {json.dumps(payload, default=str)}\n\n"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
while True:
|
||||
try:
|
||||
if correlation_id:
|
||||
rows = fetch_all(
|
||||
"""
|
||||
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
|
||||
FROM agent_events
|
||||
WHERE metadata->>'correlation_id' = %s
|
||||
AND id > %s
|
||||
ORDER BY id ASC
|
||||
LIMIT 50
|
||||
""",
|
||||
(correlation_id, last_id),
|
||||
)
|
||||
else:
|
||||
rows = fetch_all(
|
||||
"""
|
||||
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
|
||||
FROM agent_events
|
||||
WHERE LOWER(agent_name) = %s AND id > %s
|
||||
ORDER BY id ASC
|
||||
LIMIT 50
|
||||
""",
|
||||
(key, last_id),
|
||||
)
|
||||
for row in rows:
|
||||
last_id = max(last_id, int(row["id"]))
|
||||
payload = _terminal_payload(dict(row))
|
||||
yield f"data: {json.dumps(payload, default=str)}\n\n"
|
||||
except Exception as exc:
|
||||
err = {"type": "error", "message": str(exc)}
|
||||
yield f"data: {json.dumps(err)}\n\n"
|
||||
await asyncio.sleep(0.4)
|
||||
|
||||
return StreamingResponse(
|
||||
event_gen(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/live/stream")
|
||||
async def api_agents_live_stream():
|
||||
"""Single realtime stream for all agent terminals."""
|
||||
|
||||
async def event_gen():
|
||||
last_id = 0
|
||||
try:
|
||||
recent = fetch_all(
|
||||
"""
|
||||
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
|
||||
FROM agent_events
|
||||
WHERE created_at >= NOW() - INTERVAL '30 minutes'
|
||||
ORDER BY id ASC
|
||||
LIMIT 80
|
||||
"""
|
||||
)
|
||||
for row in recent:
|
||||
last_id = max(last_id, int(row["id"]))
|
||||
payload = _terminal_payload(dict(row))
|
||||
payload["replay"] = True
|
||||
yield f"data: {json.dumps(payload, default=str)}\n\n"
|
||||
except Exception:
|
||||
pass
|
||||
yield f"data: {json.dumps({'type': 'connected', 'last_id': last_id})}\n\n"
|
||||
|
||||
while True:
|
||||
try:
|
||||
rows = fetch_all(
|
||||
"""
|
||||
SELECT id, agent_name, event_type, title, body, status, channel, metadata, created_at
|
||||
FROM agent_events
|
||||
WHERE id > %s
|
||||
ORDER BY id ASC
|
||||
LIMIT 100
|
||||
""",
|
||||
(last_id,),
|
||||
)
|
||||
for row in rows:
|
||||
last_id = max(last_id, int(row["id"]))
|
||||
payload = _terminal_payload(dict(row))
|
||||
yield f"data: {json.dumps(payload, default=str)}\n\n"
|
||||
except Exception as exc:
|
||||
err = {"type": "error", "message": str(exc)}
|
||||
yield f"data: {json.dumps(err)}\n\n"
|
||||
await asyncio.sleep(0.25)
|
||||
|
||||
return StreamingResponse(
|
||||
event_gen(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/souls")
|
||||
def api_list_souls() -> dict[str, Any]:
|
||||
return {"items": agent_souls.list_souls(), "count": len(agent_souls.list_souls())}
|
||||
@@ -57,6 +292,16 @@ def api_list_agent_events(agent_key: str, limit: int = 50) -> dict[str, Any]:
|
||||
return {"agent_key": agent_key.lower(), "items": items, "count": len(items)}
|
||||
|
||||
|
||||
@router.post("/souls/{agent_key}/terminal/command")
|
||||
async def api_terminal_command(agent_key: str, body: TerminalCommandBody) -> dict[str, Any]:
|
||||
try:
|
||||
return await execute_terminal_command(agent_key, body.command.strip())
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/souls/{agent_key}")
|
||||
def api_update_soul(agent_key: str, body: SoulUpdate) -> dict[str, Any]:
|
||||
try:
|
||||
@@ -69,20 +314,7 @@ def api_update_soul(agent_key: str, body: SoulUpdate) -> dict[str, Any]:
|
||||
@router.get("/mesh")
|
||||
def api_agents_mesh() -> dict[str, Any]:
|
||||
souls = agent_souls.list_souls()
|
||||
stats_rows = fetch_all(
|
||||
"""
|
||||
SELECT LOWER(agent_name) AS agent_key,
|
||||
MAX(created_at) AS last_event_at,
|
||||
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '6 hours') AS events_6h,
|
||||
COUNT(*) FILTER (
|
||||
WHERE created_at >= NOW() - INTERVAL '24 hours'
|
||||
AND status IN ('error', 'rejected')
|
||||
) AS errors_24h
|
||||
FROM agent_events
|
||||
GROUP BY LOWER(agent_name)
|
||||
"""
|
||||
)
|
||||
by_key = {str(r["agent_key"]): dict(r) for r in stats_rows}
|
||||
by_key = _aggregate_agent_stats()
|
||||
|
||||
nodes: list[dict[str, Any]] = []
|
||||
for soul in souls:
|
||||
@@ -90,33 +322,112 @@ def api_agents_mesh() -> dict[str, Any]:
|
||||
row = by_key.get(key, {})
|
||||
events_6h = int(row.get("events_6h") or 0)
|
||||
errors_24h = int(row.get("errors_24h") or 0)
|
||||
health = "offline"
|
||||
if events_6h > 0 and errors_24h == 0:
|
||||
health = "healthy"
|
||||
elif events_6h > 0:
|
||||
health = "warn"
|
||||
elif int(soul.get("event_count") or 0) > 0:
|
||||
health = "idle"
|
||||
health = _node_health(events_6h, errors_24h, int(soul.get("event_count") or 0))
|
||||
is_active = health == "healthy" and events_6h > 0
|
||||
node = dict(soul)
|
||||
node["health"] = health
|
||||
node["events_6h"] = events_6h
|
||||
node["errors_24h"] = errors_24h
|
||||
if row.get("last_event_at") is not None and hasattr(row["last_event_at"], "isoformat"):
|
||||
node["last_event_at"] = row["last_event_at"].isoformat()
|
||||
node["is_active"] = is_active
|
||||
node["last_event_title"] = row.get("last_event_title")
|
||||
lat = row.get("last_event_at")
|
||||
if lat is not None and hasattr(lat, "isoformat"):
|
||||
node["last_event_at"] = lat.isoformat()
|
||||
nodes.append(node)
|
||||
|
||||
edge_rows = fetch_all(
|
||||
report_edges: list[dict[str, Any]] = []
|
||||
for node in nodes:
|
||||
key = str(node.get("agent_key") or "").lower()
|
||||
if key == "herman" or not node.get("is_active"):
|
||||
continue
|
||||
report_edges.append(
|
||||
{
|
||||
"source": key,
|
||||
"target": "herman",
|
||||
"type": "report",
|
||||
"active": True,
|
||||
"weight": int(node.get("events_6h") or 1),
|
||||
}
|
||||
)
|
||||
|
||||
delegate_rows = fetch_all(
|
||||
"""
|
||||
SELECT LOWER(agent_name) AS source_key, COUNT(*) AS weight
|
||||
SELECT metadata, created_at
|
||||
FROM agent_events
|
||||
WHERE created_at >= NOW() - INTERVAL '24 hours'
|
||||
AND LOWER(agent_name) <> 'herman'
|
||||
GROUP BY LOWER(agent_name)
|
||||
ORDER BY weight DESC
|
||||
WHERE agent_name = 'herman'
|
||||
AND created_at >= NOW() - INTERVAL '6 hours'
|
||||
AND (
|
||||
event_type IN ('openswarm_delegation', 'delegate', 'packaging_delivered', 'telegram_delegation')
|
||||
OR metadata ? 'delegated'
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
"""
|
||||
)
|
||||
edges = [{"source": str(r["source_key"]), "target": "herman", "weight": int(r["weight"])} for r in edge_rows]
|
||||
return {"nodes": nodes, "edges": edges, "executives": [{"id": "ceo", "label": "CEO", "role": "Aissa"}, {"id": "cto", "label": "CTO", "role": "Platform"}]}
|
||||
delegate_edges: list[dict[str, Any]] = []
|
||||
seen_delegate: set[tuple[str, str]] = set()
|
||||
for r in delegate_rows:
|
||||
meta = r.get("metadata") or {}
|
||||
if isinstance(meta, str):
|
||||
try:
|
||||
meta = json.loads(meta)
|
||||
except Exception:
|
||||
meta = {}
|
||||
delegated = meta.get("delegated") or meta.get("delegated_agents") or []
|
||||
if isinstance(delegated, str):
|
||||
delegated = [delegated]
|
||||
channel = meta.get("channel") or "herman"
|
||||
for agent in delegated:
|
||||
tgt = normalize_agent_key(str(agent))
|
||||
if not tgt or tgt == "herman":
|
||||
continue
|
||||
pair = ("herman", tgt)
|
||||
if pair in seen_delegate:
|
||||
continue
|
||||
seen_delegate.add(pair)
|
||||
delegate_edges.append(
|
||||
{
|
||||
"source": "herman",
|
||||
"target": tgt,
|
||||
"type": "delegate",
|
||||
"active": True,
|
||||
"channel": channel,
|
||||
}
|
||||
)
|
||||
|
||||
peer_static = [
|
||||
{
|
||||
"source": str(r["from_agent"]),
|
||||
"target": str(r["to_agent"]),
|
||||
"type": "static",
|
||||
"handoff_type": r.get("handoff_type"),
|
||||
}
|
||||
for r in agent_integration.list_collaboration()
|
||||
]
|
||||
peer_live = agent_integration.peer_edges_live(hours=6)
|
||||
peer_edges = peer_static + peer_live
|
||||
|
||||
herman_active = any(n.get("agent_key") == "herman" and n.get("is_active") for n in nodes)
|
||||
herman_active = herman_active or bool(delegate_edges) or bool(report_edges)
|
||||
executive_edges = []
|
||||
if herman_active:
|
||||
executive_edges = [
|
||||
{"source": "herman", "target": "ceo", "type": "executive", "active": True},
|
||||
{"source": "herman", "target": "cto", "type": "executive", "active": True},
|
||||
]
|
||||
|
||||
return {
|
||||
"nodes": nodes,
|
||||
"report_edges": report_edges,
|
||||
"delegate_edges": delegate_edges,
|
||||
"peer_edges": peer_edges,
|
||||
"executive_edges": executive_edges,
|
||||
"edges": report_edges,
|
||||
"executives": [
|
||||
{"id": "ceo", "label": "CEO", "role": "Aissa"},
|
||||
{"id": "cto", "label": "CTO", "role": "Platform"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/approvals")
|
||||
@@ -141,7 +452,17 @@ def api_create_action_request(body: ActionRequestBody) -> dict[str, Any]:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
return {"ok": True, "request": req, "message": "Wacht op goedkeuring voordat query uitgevoerd mag worden"}
|
||||
auto = None
|
||||
rid = (req or {}).get("id")
|
||||
if rid:
|
||||
try:
|
||||
auto = agent_approvals.try_auto_approve_and_execute(int(rid))
|
||||
except Exception:
|
||||
auto = None
|
||||
msg = "Wacht op goedkeuring voordat query uitgevoerd mag worden"
|
||||
if auto:
|
||||
msg = "Auto-goedgekeurd en uitgevoerd (SysOps policy)"
|
||||
return {"ok": True, "request": req, "auto_executed": bool(auto), "message": msg}
|
||||
|
||||
|
||||
@router.post("/approvals/{request_id}/approve")
|
||||
|
||||
@@ -145,7 +145,7 @@ async def dashboard(request: Request):
|
||||
"dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Herman · Command Center",
|
||||
"page_title": "CEO Dashboard",
|
||||
"kpis": kpis,
|
||||
"briefing": briefing,
|
||||
"briefing_payload": _briefing_payload(briefing),
|
||||
@@ -158,6 +158,64 @@ async def dashboard(request: Request):
|
||||
)
|
||||
|
||||
|
||||
@router.get("/cto")
|
||||
async def cto_redirect():
|
||||
return RedirectResponse(url="/ops", status_code=302)
|
||||
|
||||
|
||||
@router.get("/cto/dashboard")
|
||||
async def cto_dashboard_legacy(request: Request):
|
||||
kpis = {
|
||||
"pending_approvals": _safe_count("agent_events", "status = 'needs_approval'"),
|
||||
"browser_sessions_24h": 0,
|
||||
}
|
||||
try:
|
||||
kpis["browser_sessions_24h"] = _safe_count(
|
||||
"browser_sessions", "created_at >= NOW() - INTERVAL '24 hours'"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
agent_feed: list = []
|
||||
try:
|
||||
agent_feed = fetch_all(
|
||||
"""
|
||||
SELECT id, agent_name, event_type, title, body, status, created_at
|
||||
FROM agent_events ORDER BY created_at DESC LIMIT 25
|
||||
"""
|
||||
)
|
||||
for ev in agent_feed:
|
||||
if ev.get("created_at"):
|
||||
ev["created_at"] = ev["created_at"].isoformat()
|
||||
except Exception:
|
||||
agent_feed = []
|
||||
|
||||
browser_sessions: list = []
|
||||
try:
|
||||
browser_sessions = fetch_all(
|
||||
"""
|
||||
SELECT id, url, final_url, title, task, status, created_at
|
||||
FROM browser_sessions ORDER BY created_at DESC LIMIT 8
|
||||
"""
|
||||
)
|
||||
for s in browser_sessions:
|
||||
if s.get("created_at"):
|
||||
s["created_at"] = s["created_at"].isoformat()
|
||||
except Exception:
|
||||
browser_sessions = []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"cto-dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "CTO Dashboard",
|
||||
"kpis": kpis,
|
||||
"agent_feed": agent_feed,
|
||||
"browser_sessions": browser_sessions,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/marketing-redirect")
|
||||
async def marketing_redirect():
|
||||
return RedirectResponse(url="/marketing", status_code=302)
|
||||
|
||||
@@ -73,7 +73,8 @@ async def documents_page(request: Request):
|
||||
SELECT filename, storage_path, doc_type, language, word_count,
|
||||
unique_lemmas, sentiment_label, sentiment_compound,
|
||||
sentiment_positive, sentiment_negative, sentiment_neutral,
|
||||
extraction_method, analyzed_at
|
||||
extraction_method, analyzed_at,
|
||||
COALESCE(user_labels, '{}') AS user_labels, label_notes, labeled_at
|
||||
FROM document_analytics
|
||||
ORDER BY analyzed_at DESC
|
||||
LIMIT 50
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Export Intel Cockpit page + API proxy."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
|
||||
router = APIRouter()
|
||||
BASE = Path(__file__).resolve().parent.parent.parent
|
||||
templates = Jinja2Templates(directory=str(BASE / "templates"))
|
||||
TOOLS = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/")
|
||||
|
||||
|
||||
async def _proxy(method: str, path: str, **kwargs) -> Any:
|
||||
url = f"{TOOLS}/export-intel{path}"
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
r = await client.request(method, url, **kwargs)
|
||||
if r.status_code >= 400:
|
||||
return {"error": r.text, "status": r.status_code}
|
||||
if "text/csv" in r.headers.get("content-type", ""):
|
||||
return r
|
||||
return r.json()
|
||||
|
||||
|
||||
@router.get("/foodlinkk")
|
||||
def foodlinkk_home():
|
||||
return RedirectResponse(url="/export-intel", status_code=302)
|
||||
|
||||
|
||||
@router.get("/export-intel")
|
||||
def export_intel_page(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"export_intel.html",
|
||||
{"request": request, "page_title": "Wereldexport"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/stats")
|
||||
async def api_stats(country: Optional[str] = None, region: Optional[str] = None):
|
||||
params = {k: v for k, v in {"country": country, "region": region}.items() if v}
|
||||
return await _proxy("GET", "/stats", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/regions")
|
||||
async def api_regions():
|
||||
return await _proxy("GET", "/regions")
|
||||
|
||||
|
||||
@router.get("/api/export-intel/territories")
|
||||
async def api_territories(region: Optional[str] = None):
|
||||
params = {"region": region} if region else {}
|
||||
return await _proxy("GET", "/territories", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/entities")
|
||||
async def api_entities(
|
||||
country: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
entity_type: Optional[str] = None,
|
||||
entity_types: Optional[str] = None,
|
||||
q: Optional[str] = None,
|
||||
favorite_only: bool = False,
|
||||
crm_linked: Optional[bool] = None,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
):
|
||||
params = {k: v for k, v in {
|
||||
"country": country, "region": region, "entity_type": entity_type,
|
||||
"entity_types": entity_types, "q": q, "limit": limit, "offset": offset,
|
||||
"favorite_only": favorite_only, "crm_linked": crm_linked,
|
||||
}.items() if v is not None and v is not False}
|
||||
return await _proxy("GET", "/entities", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/entities/{entity_id}")
|
||||
async def api_entity(entity_id: int):
|
||||
return await _proxy("GET", f"/entities/{entity_id}")
|
||||
|
||||
|
||||
@router.get("/api/export-intel/contacts")
|
||||
async def api_contacts(
|
||||
country: Optional[str] = None,
|
||||
entity_type: Optional[str] = None,
|
||||
has_email: Optional[bool] = None,
|
||||
q: Optional[str] = None,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
):
|
||||
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
||||
if country:
|
||||
params["country"] = country
|
||||
if entity_type:
|
||||
params["entity_type"] = entity_type
|
||||
if has_email is not None:
|
||||
params["has_email"] = has_email
|
||||
if q:
|
||||
params["q"] = q
|
||||
return await _proxy("GET", "/contacts", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/contacts/export.csv")
|
||||
async def api_contacts_export(country: Optional[str] = None, entity_type: Optional[str] = None):
|
||||
params = {k: v for k, v in {"country": country, "entity_type": entity_type}.items() if v}
|
||||
url = f"{TOOLS}/export-intel/contacts/export.csv"
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
r = await client.get(url, params=params)
|
||||
return StreamingResponse(
|
||||
iter([r.content]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=export-intel-contacts.csv"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/caterers/brands")
|
||||
async def api_caterer_brands():
|
||||
return await _proxy("GET", "/caterers/brands")
|
||||
|
||||
|
||||
@router.get("/api/export-intel/caterers/presence")
|
||||
async def api_caterer_presence(country: Optional[str] = None):
|
||||
params = {"country": country} if country else {}
|
||||
return await _proxy("GET", "/caterers/presence", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/gov-sources")
|
||||
async def api_gov_sources(country: Optional[str] = None):
|
||||
params = {"country": country} if country else {}
|
||||
return await _proxy("GET", "/gov-sources", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/map/bundle")
|
||||
async def api_map_bundle(
|
||||
country: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
entity_type: Optional[str] = None,
|
||||
entity_types: Optional[str] = None,
|
||||
q: Optional[str] = None,
|
||||
halal_min: Optional[float] = None,
|
||||
favorite_only: bool = False,
|
||||
crm_linked: Optional[bool] = None,
|
||||
):
|
||||
params = {k: v for k, v in {
|
||||
"country": country, "region": region, "entity_type": entity_type,
|
||||
"entity_types": entity_types, "q": q, "halal_min": halal_min,
|
||||
"favorite_only": favorite_only, "crm_linked": crm_linked,
|
||||
}.items() if v is not None and v is not False}
|
||||
return await _proxy("GET", "/map/bundle", params=params)
|
||||
|
||||
|
||||
@router.post("/api/export-intel/entities/favorites")
|
||||
async def api_set_favorites(request: Request):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/entities/favorites", json=body)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/entities/favorites")
|
||||
async def api_list_favorites(country: Optional[str] = None, region: Optional[str] = None, limit: int = 200):
|
||||
params = {k: v for k, v in {"country": country, "region": region, "limit": limit}.items() if v is not None}
|
||||
return await _proxy("GET", "/entities/favorites", params=params)
|
||||
|
||||
|
||||
@router.post("/api/export-intel/crm/push")
|
||||
async def api_crm_push(request: Request):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/crm/push", json=body)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/crm/pipeline")
|
||||
async def api_crm_pipeline(country: Optional[str] = None, region: Optional[str] = None, limit: int = 100):
|
||||
params = {k: v for k, v in {"country": country, "region": region, "limit": limit}.items() if v is not None}
|
||||
return await _proxy("GET", "/crm/pipeline", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/halal/markets")
|
||||
async def api_halal_markets(
|
||||
region: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
limit: int = 30,
|
||||
):
|
||||
params = {k: v for k, v in {"region": region, "country": country, "limit": limit}.items() if v is not None}
|
||||
return await _proxy("GET", "/halal/markets", params=params)
|
||||
|
||||
|
||||
@router.get("/api/export-intel/tenders")
|
||||
async def api_tenders(country: Optional[str] = None, status: Optional[str] = "open", limit: int = 100):
|
||||
params = {k: v for k, v in {"country": country, "status": status, "limit": limit}.items() if v is not None}
|
||||
return await _proxy("GET", "/tenders", params=params)
|
||||
|
||||
|
||||
@router.post("/api/export-intel/sync/{kind}")
|
||||
async def api_sync(kind: str, request: Request):
|
||||
body = {}
|
||||
if request.headers.get("content-type", "").startswith("application/json"):
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
if kind not in ("contacts", "caterers", "distributors", "customers", "tenders", "all", "world", "region"):
|
||||
return {"error": "unknown sync kind"}
|
||||
timeout = 7200.0 if kind in ("world", "region", "all") else 600.0
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
url = f"{TOOLS}/export-intel/sync/{kind}"
|
||||
r = await client.post(url, json=body)
|
||||
return r.json()
|
||||
@@ -22,7 +22,7 @@ async def herman_page(request: Request):
|
||||
try:
|
||||
history = _iso_rows(fetch_all(
|
||||
"""SELECT id, agent_name, title, body, metadata, created_at FROM agent_events
|
||||
WHERE channel IN ('herman','dashboard') ORDER BY created_at DESC LIMIT 40"""
|
||||
WHERE channel IN ('herman','dashboard','browser','voice') ORDER BY created_at DESC LIMIT 40"""
|
||||
))
|
||||
except Exception:
|
||||
history = []
|
||||
@@ -38,7 +38,7 @@ async def herman_chat(request: Request, message: str = Form(...)):
|
||||
try:
|
||||
history = _iso_rows(fetch_all(
|
||||
"""SELECT id, agent_name, title, body, metadata, created_at FROM agent_events
|
||||
WHERE channel IN ('herman','dashboard') ORDER BY created_at DESC LIMIT 40"""
|
||||
WHERE channel IN ('herman','dashboard','browser','voice') ORDER BY created_at DESC LIMIT 40"""
|
||||
))
|
||||
except Exception:
|
||||
history = []
|
||||
@@ -62,7 +62,7 @@ async def herman_briefing_page(request: Request):
|
||||
try:
|
||||
history = _iso_rows(fetch_all(
|
||||
"""SELECT id, agent_name, title, body, metadata, created_at FROM agent_events
|
||||
WHERE channel IN ('herman','dashboard') ORDER BY created_at DESC LIMIT 40"""
|
||||
WHERE channel IN ('herman','dashboard','browser','voice') ORDER BY created_at DESC LIMIT 40"""
|
||||
))
|
||||
except Exception:
|
||||
history = []
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Revenue Cockpit — page + API."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.services import revenue_cockpit as svc
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||
|
||||
router = APIRouter(tags=["revenue-cockpit"])
|
||||
api = APIRouter(prefix="/api/revenue-cockpit", tags=["revenue-cockpit-api"])
|
||||
|
||||
|
||||
class ProjectUpdateBody(BaseModel):
|
||||
name: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
margin_month: Optional[float] = None
|
||||
margin_year: Optional[float] = None
|
||||
target_revenue: Optional[float] = None
|
||||
next_steps: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
priority: Optional[str] = None
|
||||
row_style: Optional[str] = None
|
||||
|
||||
|
||||
class GoalsUpdateBody(BaseModel):
|
||||
vision_text: Optional[str] = None
|
||||
horizon_text: Optional[str] = None
|
||||
mid_text: Optional[str] = None
|
||||
tagline: Optional[str] = None
|
||||
|
||||
|
||||
class ObjectiveBody(BaseModel):
|
||||
title: str = Field(..., min_length=1)
|
||||
description: Optional[str] = None
|
||||
priority: str = "normal"
|
||||
due_date: Optional[str] = None
|
||||
|
||||
|
||||
class ObjectiveUpdateBody(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
priority: Optional[str] = None
|
||||
due_date: Optional[str] = None
|
||||
|
||||
|
||||
class AssignTaskBody(BaseModel):
|
||||
agent_name: str = Field(..., min_length=1)
|
||||
title: str = Field(..., min_length=1)
|
||||
description: Optional[str] = None
|
||||
objective_id: Optional[int] = None
|
||||
priority: str = "normal"
|
||||
delegate_herman: bool = True
|
||||
|
||||
|
||||
class DelegateBody(BaseModel):
|
||||
message: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
class ImportBody(BaseModel):
|
||||
path: str = "Succes Sheet .xlsx"
|
||||
sheet: Optional[str] = "Projects next steps revenue"
|
||||
replace: bool = True
|
||||
|
||||
|
||||
@router.get("/revenue-cockpit", response_class=HTMLResponse)
|
||||
async def revenue_cockpit_page(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"revenue_cockpit.html",
|
||||
{"request": request, "page_title": "Revenue Cockpit"},
|
||||
)
|
||||
|
||||
|
||||
@api.get("/live")
|
||||
async def live_from_excel(path: str = "Succes Sheet .xlsx", sheet: Optional[str] = "Projects next steps revenue"):
|
||||
"""Leading data source: fresh parse from NAS Excel."""
|
||||
try:
|
||||
parsed = await svc.fetch_excel_parse_async(path, sheet)
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, str(exc)) from exc
|
||||
projects = parsed.get("projects") or []
|
||||
margin_month = sum(p.get("margin_month") or 0 for p in projects)
|
||||
margin_year = sum(p.get("margin_year") or 0 for p in projects)
|
||||
by_style: dict[str, int] = {}
|
||||
by_category: dict[str, int] = {}
|
||||
for p in projects:
|
||||
by_style[p.get("row_style") or "white"] = by_style.get(p.get("row_style") or "white", 0) + 1
|
||||
by_category[p.get("category") or "deal"] = by_category.get(p.get("category") or "deal", 0) + 1
|
||||
return {
|
||||
"ok": True,
|
||||
"source": "excel",
|
||||
**parsed,
|
||||
"aggregates": {
|
||||
"total_margin_month": margin_month,
|
||||
"total_margin_year": margin_year,
|
||||
"with_margin": sum(1 for p in projects if p.get("margin_month")),
|
||||
"by_style": by_style,
|
||||
"by_category": by_category,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@api.get("/dashboard")
|
||||
async def dashboard():
|
||||
return {"ok": True, "stats": svc.dashboard_stats(), "projects": svc.list_projects()}
|
||||
|
||||
|
||||
@api.get("/projects")
|
||||
def list_projects(status: Optional[str] = None):
|
||||
return {"ok": True, "items": svc.list_projects(status)}
|
||||
|
||||
|
||||
@api.get("/projects/{project_id}")
|
||||
def get_project(project_id: int):
|
||||
p = svc.get_project(project_id)
|
||||
if not p:
|
||||
raise HTTPException(404, "Project not found")
|
||||
return {"ok": True, "project": p}
|
||||
|
||||
|
||||
@api.patch("/projects/{project_id}")
|
||||
def patch_project(project_id: int, body: ProjectUpdateBody):
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
row_style = data.pop("row_style", None)
|
||||
if row_style is not None:
|
||||
svc.set_project_row_style(project_id, row_style)
|
||||
p = svc.update_project(project_id, data)
|
||||
if not p:
|
||||
raise HTTPException(404, "Project not found")
|
||||
svc.take_snapshot()
|
||||
return {"ok": True, "project": p}
|
||||
|
||||
|
||||
@api.patch("/goals")
|
||||
def patch_goals(body: GoalsUpdateBody):
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
g = svc.update_goals(data)
|
||||
if not g:
|
||||
raise HTTPException(404, "Goals not found")
|
||||
return {"ok": True, "goals": g}
|
||||
|
||||
|
||||
@api.post("/projects/{project_id}/objectives")
|
||||
def add_objective(project_id: int, body: ObjectiveBody):
|
||||
try:
|
||||
obj = svc.create_objective(project_id, body.title, body.description, body.priority)
|
||||
except Exception as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
svc.take_snapshot()
|
||||
return {"ok": True, "objective": obj}
|
||||
|
||||
|
||||
@api.patch("/objectives/{objective_id}")
|
||||
def patch_objective(objective_id: int, body: ObjectiveUpdateBody):
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
obj = svc.update_objective(objective_id, data)
|
||||
if not obj:
|
||||
raise HTTPException(404, "Objective not found")
|
||||
svc.take_snapshot()
|
||||
return {"ok": True, "objective": obj}
|
||||
|
||||
|
||||
@api.post("/projects/{project_id}/assign-task")
|
||||
def assign_task(project_id: int, body: AssignTaskBody):
|
||||
try:
|
||||
task = svc.assign_agent_task(
|
||||
project_id,
|
||||
body.agent_name,
|
||||
body.title,
|
||||
body.description,
|
||||
body.objective_id,
|
||||
body.priority,
|
||||
body.delegate_herman,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(404, str(exc)) from exc
|
||||
svc.take_snapshot()
|
||||
return {"ok": True, "task": task}
|
||||
|
||||
|
||||
@api.post("/projects/{project_id}/delegate")
|
||||
async def delegate_project(project_id: int, body: DelegateBody):
|
||||
try:
|
||||
result = await svc.delegate_via_herman(project_id, body.message)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(404, str(exc)) from exc
|
||||
return result
|
||||
|
||||
|
||||
@api.get("/tasks")
|
||||
def list_tasks(limit: int = 50):
|
||||
return {"ok": True, "items": svc.list_agent_tasks(limit)}
|
||||
|
||||
|
||||
@api.get("/snapshots")
|
||||
def snapshots(limit: int = 90):
|
||||
return {"ok": True, "items": svc.list_snapshots(limit)}
|
||||
|
||||
|
||||
@api.post("/snapshot")
|
||||
def create_snapshot():
|
||||
snap = svc.take_snapshot()
|
||||
return {"ok": True, "snapshot": snap}
|
||||
|
||||
|
||||
@api.post("/import-from-excel")
|
||||
async def import_from_excel(body: ImportBody):
|
||||
try:
|
||||
parsed = await svc.fetch_excel_parse_async(body.path, body.sheet)
|
||||
result = svc.import_from_parsed(parsed, imported_by="ceo", replace=body.replace)
|
||||
return {"ok": True, **result, "preview": {"project_count": parsed.get("project_count"), "sheet": parsed.get("sheet_name")}}
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, f"Import failed: {exc}") from exc
|
||||
|
||||
|
||||
@api.get("/preview-excel")
|
||||
async def preview_excel(path: str = "Succes Sheet .xlsx", sheet: Optional[str] = "Projects next steps revenue"):
|
||||
try:
|
||||
parsed = await svc.fetch_excel_parse_async(path, sheet)
|
||||
return {"ok": True, **parsed}
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, str(exc)) from exc
|
||||
@@ -10,7 +10,7 @@ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||
|
||||
@router.get("/settings")
|
||||
async def settings_page(request: Request, tab: str = "email"):
|
||||
allowed = ("email", "general", "permissions", "social")
|
||||
allowed = ("email", "general", "permissions", "social", "ai")
|
||||
return templates.TemplateResponse(
|
||||
"settings.html",
|
||||
{
|
||||
|
||||
@@ -381,3 +381,118 @@ def update_permission(module_key: str, body: PermissionBody) -> dict[str, Any]:
|
||||
def grant_all_permissions() -> dict[str, Any]:
|
||||
n = agent_souls.grant_all_permissions()
|
||||
return {"ok": True, "granted_count": n, "message": f"Herman heeft nu {n} module-rechten"}
|
||||
|
||||
|
||||
# ── LLM providers ──────────────────────────────────────────────────────────
|
||||
|
||||
from app.services import llm_router
|
||||
|
||||
|
||||
class LlmProviderBody(BaseModel):
|
||||
label: str = Field(..., max_length=128)
|
||||
provider_type: str = Field(default="deepseek", max_length=48)
|
||||
api_base_url: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
model: str = Field(default="deepseek-chat", max_length=128)
|
||||
is_active: bool = True
|
||||
is_default: bool = False
|
||||
extra_config: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@settings_router.get("/llm/presets")
|
||||
def llm_presets() -> dict[str, Any]:
|
||||
return {"presets": llm_router.list_presets()}
|
||||
|
||||
|
||||
@settings_router.get("/llm")
|
||||
def llm_list() -> dict[str, Any]:
|
||||
items = llm_router.list_providers()
|
||||
default = next((i for i in items if i.get("is_default")), items[0] if items else None)
|
||||
return {"providers": items, "default": default, "presets": llm_router.list_presets()}
|
||||
|
||||
|
||||
@settings_router.post("/llm")
|
||||
def llm_create(body: LlmProviderBody) -> dict[str, Any]:
|
||||
preset = llm_router.LLM_PRESETS.get(body.provider_type, {})
|
||||
base_url = (body.api_base_url or preset.get("api_base_url") or "").strip() or None
|
||||
model = body.model or (preset.get("models") or ["deepseek-chat"])[0]
|
||||
if body.is_default:
|
||||
execute("UPDATE llm_providers SET is_default = FALSE, updated_at = NOW()")
|
||||
row = fetch_one(
|
||||
"""INSERT INTO llm_providers (
|
||||
label, provider_type, api_base_url, api_key, model,
|
||||
is_active, is_default, extra_config, updated_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, NOW())
|
||||
RETURNING *""",
|
||||
(
|
||||
body.label,
|
||||
body.provider_type,
|
||||
base_url,
|
||||
body.api_key or "",
|
||||
model,
|
||||
body.is_active,
|
||||
body.is_default,
|
||||
json.dumps(body.extra_config or {}),
|
||||
),
|
||||
)
|
||||
return {"provider": llm_router._mask_provider(row)}
|
||||
|
||||
|
||||
@settings_router.put("/llm/{provider_id}")
|
||||
def llm_update(provider_id: int, body: LlmProviderBody) -> dict[str, Any]:
|
||||
existing = fetch_one("SELECT * FROM llm_providers WHERE id = %s", (provider_id,))
|
||||
if not existing:
|
||||
raise HTTPException(404, "Provider niet gevonden")
|
||||
preset = llm_router.LLM_PRESETS.get(body.provider_type, {})
|
||||
base_url = (body.api_base_url or preset.get("api_base_url") or existing.get("api_base_url") or "").strip() or None
|
||||
api_key = body.api_key if body.api_key else existing.get("api_key") or ""
|
||||
if body.is_default:
|
||||
execute("UPDATE llm_providers SET is_default = FALSE, updated_at = NOW()")
|
||||
row = fetch_one(
|
||||
"""UPDATE llm_providers SET
|
||||
label = %s, provider_type = %s, api_base_url = %s, api_key = %s, model = %s,
|
||||
is_active = %s, is_default = %s, extra_config = %s::jsonb, updated_at = NOW()
|
||||
WHERE id = %s RETURNING *""",
|
||||
(
|
||||
body.label,
|
||||
body.provider_type,
|
||||
base_url,
|
||||
api_key,
|
||||
body.model,
|
||||
body.is_active,
|
||||
body.is_default,
|
||||
json.dumps(body.extra_config or {}),
|
||||
provider_id,
|
||||
),
|
||||
)
|
||||
return {"provider": llm_router._mask_provider(row)}
|
||||
|
||||
|
||||
@settings_router.delete("/llm/{provider_id}")
|
||||
def llm_delete(provider_id: int) -> dict[str, Any]:
|
||||
row = fetch_one("SELECT is_default FROM llm_providers WHERE id = %s", (provider_id,))
|
||||
if not row:
|
||||
raise HTTPException(404, "Provider niet gevonden")
|
||||
execute("DELETE FROM llm_providers WHERE id = %s", (provider_id,))
|
||||
if row.get("is_default"):
|
||||
execute(
|
||||
"""UPDATE llm_providers SET is_default = TRUE, updated_at = NOW()
|
||||
WHERE id = (SELECT id FROM llm_providers ORDER BY id LIMIT 1)"""
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@settings_router.post("/llm/{provider_id}/activate")
|
||||
def llm_activate(provider_id: int) -> dict[str, Any]:
|
||||
llm_router.set_default(provider_id)
|
||||
row = fetch_one("SELECT * FROM llm_providers WHERE id = %s", (provider_id,))
|
||||
return {"ok": True, "provider": llm_router._mask_provider(row)}
|
||||
|
||||
|
||||
@settings_router.post("/llm/{provider_id}/test")
|
||||
async def llm_test(provider_id: int) -> dict[str, Any]:
|
||||
ok, msg = await llm_router.test_provider(provider_id)
|
||||
if not ok:
|
||||
raise HTTPException(502, msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user