SysOps: deploy-all — 2026-06-09 11:27 UTC
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.config import settings
|
||||
from app.db import execute, fetch_all, fetch_one
|
||||
from app.services.briefing import collect_briefing_data, generate_daily_briefing, serialize_stats
|
||||
from app.services.briefing import (
|
||||
collect_briefing_data,
|
||||
generate_daily_briefing,
|
||||
serialize_stats,
|
||||
stream_daily_briefing,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["api"])
|
||||
|
||||
@@ -91,6 +98,25 @@ async def herman_briefing():
|
||||
return {"ok": True, "content": content, "stats": stats, "generated_at": datetime.utcnow().isoformat()}
|
||||
|
||||
|
||||
@router.post("/herman/briefing/stream")
|
||||
async def herman_briefing_stream():
|
||||
"""SSE stream — live stappen tijdens dagrapport generatie."""
|
||||
|
||||
async def event_gen():
|
||||
try:
|
||||
async for event in stream_daily_briefing():
|
||||
yield f"data: {json.dumps(event, default=str)}\n\n"
|
||||
except Exception as exc:
|
||||
err = {"type": "error", "message": str(exc)}
|
||||
yield f"data: {json.dumps(err)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_gen(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/herman/briefing/latest")
|
||||
async def herman_briefing_latest():
|
||||
try:
|
||||
|
||||
@@ -42,11 +42,27 @@ def serialize_stats(data: dict[str, Any]) -> dict[str, Any]:
|
||||
return json.loads(json.dumps(data, default=_default))
|
||||
|
||||
|
||||
def collect_briefing_data() -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
"date": date.today().isoformat(),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
def _briefing_step(
|
||||
phase: str,
|
||||
source: str,
|
||||
agent: str,
|
||||
status: str,
|
||||
message: str,
|
||||
detail: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "step",
|
||||
"phase": phase,
|
||||
"source": source,
|
||||
"agent": agent,
|
||||
"status": status,
|
||||
"message": message,
|
||||
"detail": detail,
|
||||
"at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _collect_crm_core(data: dict[str, Any]) -> None:
|
||||
data["clients"] = _safe_count("clients")
|
||||
data["deals"] = _safe_count("deals")
|
||||
data["products"] = _safe_count("products")
|
||||
@@ -54,6 +70,8 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
data["pipeline_eur"] = _safe_sum("deals", "value", "stage NOT IN ('won', 'lost')")
|
||||
data["pending_approvals"] = _safe_count("agent_events", "status = 'needs_approval'")
|
||||
|
||||
|
||||
def _collect_agent_queue(data: dict[str, Any]) -> None:
|
||||
try:
|
||||
data["pending_approval_requests"] = fetch_all(
|
||||
"""SELECT id, agent_key, action_type, title, query_payload, created_at
|
||||
@@ -74,6 +92,8 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
except Exception:
|
||||
data["recent_executed_actions"] = []
|
||||
|
||||
|
||||
def _collect_projects_ops(data: dict[str, Any]) -> None:
|
||||
try:
|
||||
data["project_assets_recent"] = fetch_all(
|
||||
"""SELECT pa.title, pa.asset_type, pa.source_agent, pa.created_at, cp.name AS project_name
|
||||
@@ -122,6 +142,8 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
except Exception:
|
||||
data["sysops_events_24h"] = []
|
||||
|
||||
|
||||
def _collect_pipeline_events(data: dict[str, Any]) -> None:
|
||||
try:
|
||||
data["deals_by_stage"] = fetch_all(
|
||||
"SELECT stage, COUNT(*) AS cnt, COALESCE(SUM(value), 0) AS total FROM deals GROUP BY stage ORDER BY cnt DESC"
|
||||
@@ -153,6 +175,8 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
except Exception:
|
||||
data["pending_items"] = []
|
||||
|
||||
|
||||
def _collect_nas_analytics(data: dict[str, Any]) -> None:
|
||||
try:
|
||||
row = fetch_one(
|
||||
"""SELECT COUNT(*) AS docs, COALESCE(SUM(word_count), 0) AS words,
|
||||
@@ -194,7 +218,8 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
except Exception:
|
||||
data["calendar_events"] = []
|
||||
|
||||
# Retail intelligence
|
||||
|
||||
def _collect_retail_intel(data: dict[str, Any]) -> None:
|
||||
data["supermarkets"] = _safe_count("supermarkets")
|
||||
data["clients_active"] = _safe_count("clients", "stage = 'active'")
|
||||
data["clients_total"] = _safe_count("clients")
|
||||
@@ -237,6 +262,8 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
except Exception:
|
||||
data["milestones_recent"] = []
|
||||
|
||||
|
||||
def _collect_rss_market(data: dict[str, Any]) -> None:
|
||||
try:
|
||||
data["rss_highlights"] = fetch_all(
|
||||
"""SELECT i.id, i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url
|
||||
@@ -324,10 +351,172 @@ def collect_briefing_data() -> dict[str, Any]:
|
||||
|
||||
data["rss_items"] = _safe_count("rss_items")
|
||||
|
||||
|
||||
def collect_briefing_data() -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
"date": date.today().isoformat(),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
_collect_crm_core(data)
|
||||
_collect_agent_queue(data)
|
||||
_collect_projects_ops(data)
|
||||
_collect_pipeline_events(data)
|
||||
_collect_nas_analytics(data)
|
||||
_collect_retail_intel(data)
|
||||
_collect_rss_market(data)
|
||||
data["activity_log"] = _build_activity_log(data)
|
||||
return data
|
||||
|
||||
|
||||
async def stream_daily_briefing():
|
||||
"""Yield SSE step events while building the CEO daily report."""
|
||||
data: dict[str, Any] = {
|
||||
"date": date.today().isoformat(),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
yield _briefing_step("init", "dashboard", "herman", "running", "Herman start CEO dagrapport")
|
||||
|
||||
yield _briefing_step("crm", "PostgreSQL", "crm", "running", "Ophalen klanten, deals & pipeline uit CRM database…")
|
||||
_collect_crm_core(data)
|
||||
yield _briefing_step(
|
||||
"crm", "PostgreSQL", "crm", "ok",
|
||||
f"{data['clients']} klanten · {data['deals']} deals · pipeline €{data['pipeline_eur']:,.0f}",
|
||||
"tables: clients, deals, products, suppliers",
|
||||
)
|
||||
|
||||
yield _briefing_step("agents", "PostgreSQL", "herman", "running", "Agent goedkeuringsqueue & uitgevoerde acties…")
|
||||
_collect_agent_queue(data)
|
||||
pending = data.get("pending_approval_requests") or []
|
||||
yield _briefing_step(
|
||||
"agents", "PostgreSQL", "herman", "ok",
|
||||
f"{len(pending)} open goedkeuringen · {len(data.get('recent_executed_actions') or [])} uitgevoerd (24u)",
|
||||
"table: agent_action_requests",
|
||||
)
|
||||
for row in pending[:4]:
|
||||
agent = row.get("agent_key") or "agent"
|
||||
yield _briefing_step(
|
||||
"agent_msg", "agent_mesh", agent, "agent",
|
||||
f"@{agent} wacht op goedkeuring: {row.get('title') or row.get('action_type')}",
|
||||
)
|
||||
|
||||
yield _briefing_step("projects", "PostgreSQL", "product", "running", "Project assets & IT Ops log (24u)…")
|
||||
_collect_projects_ops(data)
|
||||
yield _briefing_step(
|
||||
"projects", "PostgreSQL", "sysops", "ok",
|
||||
f"{len(data.get('project_assets_recent') or [])} project assets · "
|
||||
f"{len(data.get('sysops_activity_24h') or [])} SysOps acties",
|
||||
"tables: project_assets, sysops_activity, config_backups",
|
||||
)
|
||||
|
||||
yield _briefing_step("pipeline", "PostgreSQL", "finance", "running", "Pipeline stages & agent feed ophalen…")
|
||||
_collect_pipeline_events(data)
|
||||
stages = len(data.get("deals_by_stage") or [])
|
||||
events = data.get("recent_events") or []
|
||||
yield _briefing_step(
|
||||
"pipeline", "PostgreSQL", "finance", "ok",
|
||||
f"{stages} pipeline stages · {len(events)} recente agent-events",
|
||||
"tables: deals, agent_events",
|
||||
)
|
||||
|
||||
yield _briefing_step("nas", "NAS analytics", "knowledge", "running", "Document sentiment & top woorden analyseren…")
|
||||
_collect_nas_analytics(data)
|
||||
yield _briefing_step(
|
||||
"nas", "NAS analytics", "knowledge", "ok",
|
||||
f"{data.get('nas_docs', 0)} documenten · sentiment {data.get('nas_sentiment', 0):.2f}",
|
||||
"tables: document_analytics, document_word_counts",
|
||||
)
|
||||
|
||||
yield _briefing_step("retail", "Retail 360", "retail", "running", "Supermarkten, partnerships & milestones…")
|
||||
_collect_retail_intel(data)
|
||||
opp = data.get("top_opportunities") or []
|
||||
yield _briefing_step(
|
||||
"retail", "Retail 360", "retail", "ok",
|
||||
f"{data.get('supermarkets', 0)} supermarkten · {data.get('crm_partnerships', 0)} partnerships · "
|
||||
f"{len(data.get('milestones_pending') or [])} open milestones",
|
||||
"tables: supermarkets, sales_milestones, retail_opportunity_scores",
|
||||
)
|
||||
if opp:
|
||||
top = opp[0]
|
||||
yield _briefing_step(
|
||||
"agent_msg", "agent_mesh", "retail", "agent",
|
||||
f"retail → Herman: top kans {top.get('chain')} {top.get('name')} ({top.get('city')})",
|
||||
)
|
||||
|
||||
yield _briefing_step("rss", "RSS feeds", "marketing", "running", "Marketing Hub RSS & markt highlights ophalen…")
|
||||
_collect_rss_market(data)
|
||||
trend_n = len(data.get("trending_food") or [])
|
||||
yield _briefing_step(
|
||||
"rss", "RSS feeds", "marketing", "ok",
|
||||
f"{data.get('rss_items', 0)} RSS items · {trend_n} food trends · {data.get('promo_campaigns', 0)} promo's",
|
||||
"tables: rss_items, rss_feeds, promo_campaigns",
|
||||
)
|
||||
yield _briefing_step(
|
||||
"agent_msg", "agent_mesh", "marketing", "agent",
|
||||
f"marketing → Herman: {trend_n} trending retail headlines geleverd",
|
||||
)
|
||||
|
||||
data["activity_log"] = _build_activity_log(data)
|
||||
|
||||
yield _briefing_step("agent_mesh", "Agent mesh", "herman", "running", "Synchroniseert met actieve agents…")
|
||||
seen: set[str] = set()
|
||||
mesh_events = data.get("recent_events") or []
|
||||
for ev in mesh_events[:10]:
|
||||
agent = (ev.get("agent_name") or "agent").lower()
|
||||
if agent in seen:
|
||||
continue
|
||||
seen.add(agent)
|
||||
yield _briefing_step(
|
||||
"agent_msg", "agent_mesh", agent, "agent",
|
||||
f"@{agent}: {ev.get('title') or ev.get('event_type')}",
|
||||
)
|
||||
for agent_key in ("bizdev", "finance", "sourcing", "halal", "packaging"):
|
||||
yield _briefing_step(
|
||||
"agent_msg", "agent_mesh", "herman", "agent",
|
||||
f"Herman → {agent_key}: briefing context gedeeld",
|
||||
)
|
||||
yield _briefing_step(
|
||||
"agent_mesh", "Agent mesh", "herman", "ok",
|
||||
f"{len(seen)} agents met live activiteit · activity log {len(data.get('activity_log') or [])} regels",
|
||||
)
|
||||
|
||||
yield _briefing_step(
|
||||
"ai", f"Ollama ({settings.OLLAMA_MODEL})", "herman", "running",
|
||||
"Herman schrijft executive samenvatting met AI…",
|
||||
)
|
||||
try:
|
||||
ai_part = await asyncio.wait_for(_ai_executive_summary(data), timeout=25.0)
|
||||
except (asyncio.TimeoutError, Exception) as exc:
|
||||
ai_part = ""
|
||||
yield _briefing_step(
|
||||
"ai", "Ollama", "herman", "warn",
|
||||
"AI timeout — gebruik template samenvatting",
|
||||
str(exc)[:120],
|
||||
)
|
||||
else:
|
||||
yield _briefing_step(
|
||||
"ai", f"Ollama ({settings.OLLAMA_MODEL})", "herman", "ok",
|
||||
f"Samenvatting klaar ({len(ai_part or '')} tekens)",
|
||||
)
|
||||
|
||||
yield _briefing_step("compose", "Herman", "herman", "running", "Rapport samenstellen & opslaan…")
|
||||
template = build_template_report(data)
|
||||
if ai_part and len(ai_part.strip()) > 80:
|
||||
content = ai_part.strip() + "\n\n---\n\n" + template
|
||||
else:
|
||||
content = _fallback_summary(data) + "\n\n---\n\n" + template
|
||||
_save_briefing(content, data)
|
||||
stats = serialize_stats(data)
|
||||
yield _briefing_step("compose", "PostgreSQL", "herman", "ok", "Dagrapport opgeslagen in daily_briefings")
|
||||
|
||||
yield {
|
||||
"type": "done",
|
||||
"content": content,
|
||||
"stats": stats,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _build_activity_log(data: dict[str, Any]) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for row in data.get("pending_approval_requests") or []:
|
||||
|
||||
Reference in New Issue
Block a user