from __future__ import annotations import asyncio import json from datetime import date, datetime, timezone from typing import Any from app.config import settings from app.db import execute, fetch_all, fetch_one from app.services import market_stocks, ollama def _safe_count(table: str, where: str = "", params: tuple = ()) -> int: try: clause = f" WHERE {where}" if where else "" row = fetch_one(f"SELECT COUNT(*) AS c FROM {table}{clause}", params or None) return int(row["c"]) if row else 0 except Exception: return 0 def _safe_sum(table: str, column: str, where: str = "", params: tuple = ()) -> float: try: clause = f" WHERE {where}" if where else "" row = fetch_one(f"SELECT COALESCE(SUM({column}), 0) AS total FROM {table}{clause}", params or None) return float(row["total"]) if row else 0.0 except Exception: return 0.0 def serialize_stats(data: dict[str, Any]) -> dict[str, Any]: def _default(o: Any) -> Any: if hasattr(o, "isoformat"): return o.isoformat() if hasattr(o, "__float__"): try: return float(o) except (TypeError, ValueError): pass return str(o) 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(), } data["clients"] = _safe_count("clients") data["deals"] = _safe_count("deals") data["products"] = _safe_count("products") data["suppliers"] = _safe_count("suppliers") data["pipeline_eur"] = _safe_sum("deals", "value", "stage NOT IN ('won', 'lost')") data["pending_approvals"] = _safe_count("agent_events", "status = 'needs_approval'") try: data["pending_approval_requests"] = fetch_all( """SELECT id, agent_key, action_type, title, query_payload, created_at FROM agent_action_requests WHERE status = 'pending' ORDER BY created_at ASC LIMIT 15""" ) data["pending_approvals"] = len(data["pending_approval_requests"]) except Exception: data["pending_approval_requests"] = [] try: data["recent_executed_actions"] = fetch_all( """SELECT id, agent_key, action_type, title, result, executed_at, approved_by FROM agent_action_requests WHERE status = 'executed' AND executed_at >= NOW() - INTERVAL '24 hours' ORDER BY executed_at DESC LIMIT 12""" ) except Exception: data["recent_executed_actions"] = [] try: data["project_assets_recent"] = fetch_all( """SELECT pa.title, pa.asset_type, pa.source_agent, pa.created_at, cp.name AS project_name FROM project_assets pa JOIN cockpit_projects cp ON cp.id = pa.project_id WHERE pa.created_at >= NOW() - INTERVAL '24 hours' ORDER BY pa.created_at DESC LIMIT 15""" ) except Exception: data["project_assets_recent"] = [] try: data["ops_maintenance_open"] = fetch_all( """SELECT severity, title, body, created_at FROM ops_maintenance_notes WHERE resolved = false ORDER BY created_at DESC LIMIT 8""" ) except Exception: data["ops_maintenance_open"] = [] try: data["config_backups_recent"] = fetch_all( """SELECT status, message, commit_ref, created_at FROM config_backups ORDER BY created_at DESC LIMIT 5""" ) except Exception: data["config_backups_recent"] = [] try: data["sysops_activity_24h"] = fetch_all( """SELECT action_type, title, body, commit_ref, files_changed, status, created_at FROM sysops_activity WHERE created_at >= NOW() - INTERVAL '24 hours' ORDER BY created_at DESC LIMIT 20""" ) except Exception: data["sysops_activity_24h"] = [] try: data["sysops_events_24h"] = fetch_all( """SELECT event_type, title, body, status, created_at, metadata FROM agent_events WHERE LOWER(agent_name) = 'sysops' AND created_at >= NOW() - INTERVAL '24 hours' ORDER BY created_at DESC LIMIT 15""" ) except Exception: data["sysops_events_24h"] = [] 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" ) except Exception: data["deals_by_stage"] = [] try: data["recent_clients"] = fetch_all( "SELECT name, stage, email, created_at FROM clients ORDER BY created_at DESC LIMIT 5" ) except Exception: data["recent_clients"] = [] try: data["recent_events"] = fetch_all( """SELECT agent_name, event_type, title, status, created_at FROM agent_events ORDER BY created_at DESC LIMIT 12""" ) except Exception: data["recent_events"] = [] try: data["pending_items"] = fetch_all( """SELECT agent_name, title, event_type, created_at FROM agent_events WHERE status = 'needs_approval' ORDER BY created_at DESC LIMIT 8""" ) except Exception: data["pending_items"] = [] try: row = fetch_one( """SELECT COUNT(*) AS docs, COALESCE(SUM(word_count), 0) AS words, COALESCE(AVG(sentiment_compound), 0) AS avg_sentiment FROM document_analytics""" ) data["nas_docs"] = int(row["docs"] or 0) if row else 0 data["nas_words"] = int(row["words"] or 0) if row else 0 data["nas_sentiment"] = round(float(row["avg_sentiment"] or 0), 3) if row else 0.0 except Exception: data["nas_docs"] = data["nas_words"] = 0 data["nas_sentiment"] = 0.0 try: data["nas_files"] = fetch_all( """SELECT filename, doc_type, sentiment_label, word_count FROM document_analytics ORDER BY analyzed_at DESC LIMIT 8""" ) except Exception: data["nas_files"] = [] try: data["top_words"] = fetch_all( """SELECT lemma, SUM(count) AS total FROM document_word_counts WHERE NOT is_stopword GROUP BY lemma ORDER BY total DESC LIMIT 10""" ) except Exception: data["top_words"] = [] try: data["calendar_events"] = fetch_all( """SELECT ce.title, ce.starts_at, ce.ends_at, c.name AS client_name FROM calendar_events ce LEFT JOIN clients c ON c.id = ce.client_id WHERE ce.starts_at >= NOW() - INTERVAL '1 day' AND ce.starts_at <= NOW() + INTERVAL '7 days' ORDER BY ce.starts_at ASC LIMIT 10""" ) except Exception: data["calendar_events"] = [] # Retail intelligence data["supermarkets"] = _safe_count("supermarkets") data["clients_active"] = _safe_count("clients", "stage = 'active'") data["clients_total"] = _safe_count("clients") data["crm_partnerships"] = _safe_count("supermarkets", "partnership_status = 'active'") data["wholesalers"] = _safe_count("wholesalers") data["rss_bookmarks"] = _safe_count("rss_bookmarks") data["promo_campaigns"] = _safe_count("promo_campaigns", "status = 'active'") try: data["top_opportunities"] = fetch_all( """SELECT s.name, s.chain, s.city, ros.halal_opportunity_score FROM retail_opportunity_scores ros JOIN supermarkets s ON s.id = ros.supermarket_id ORDER BY ros.halal_opportunity_score DESC LIMIT 5""" ) except Exception: data["top_opportunities"] = [] try: data["milestones_pending"] = fetch_all( """SELECT sm.title, sm.milestone_type, sm.status, sm.target_date, sm.value_eur, s.name AS store_name, s.chain, c.name AS client_name FROM sales_milestones sm LEFT JOIN supermarkets s ON s.id = sm.supermarket_id LEFT JOIN clients c ON c.id = sm.client_id WHERE sm.status IN ('pending', 'in_progress') ORDER BY sm.target_date ASC NULLS LAST, sm.created_at DESC LIMIT 8""" ) except Exception: data["milestones_pending"] = [] try: data["milestones_recent"] = fetch_all( """SELECT sm.title, sm.milestone_type, sm.status, sm.completed_at, sm.value_eur, s.name AS store_name, s.chain FROM sales_milestones sm LEFT JOIN supermarkets s ON s.id = sm.supermarket_id ORDER BY sm.created_at DESC LIMIT 5""" ) except Exception: data["milestones_recent"] = [] 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 FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE WHERE i.title ILIKE ANY (ARRAY['%kant%','%maaltijd%','%supermarkt%','%retail%','%halal%','%jumbo%','%meal%']) ORDER BY i.published_at DESC NULLS LAST LIMIT 8""" ) except Exception: data["rss_highlights"] = [] try: data["market_trends"] = fetch_all( "SELECT trend_name, description, opportunity_score FROM market_trends ORDER BY updated_at DESC LIMIT 4" ) except Exception: data["market_trends"] = [] try: quotes = market_stocks.fetch_retail_quotes() data["market_stocks"] = quotes data["market_summary"] = market_stocks.market_summary(quotes) except Exception: data["market_stocks"] = [] data["market_summary"] = {} try: data["regulation_highlights"] = fetch_all( """SELECT i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url, f.category FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE WHERE f.category IN ('regelgeving', 'cbs') ORDER BY i.published_at DESC NULLS LAST LIMIT 8""" ) except Exception: data["regulation_highlights"] = [] try: data["trending_food"] = fetch_all( """SELECT i.id, i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url, f.category, i.published_at FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE WHERE f.category IN ('food', 'markt', 'supermarkt', 'retail', 'kant-en-klaar') OR f.name ILIKE '%retaildetail%' ORDER BY i.published_at DESC NULLS LAST, i.fetched_at DESC LIMIT 10""" ) except Exception: data["trending_food"] = [] try: data["food_market_highlights"] = fetch_all( """SELECT i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url, f.category FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE WHERE f.category IN ('food', 'markt', 'supermarkt', 'kant-en-klaar', 'retail') OR f.name ILIKE '%retaildetail%' ORDER BY i.published_at DESC NULLS LAST LIMIT 10""" ) except Exception: data["food_market_highlights"] = [] data["activity_log"] = _build_activity_log(data) return data def _build_activity_log(data: dict[str, Any]) -> list[str]: lines: list[str] = [] for row in data.get("pending_approval_requests") or []: agent = row.get("agent_key") or "agent" action = row.get("action_type") or "actie" title = row.get("title") or "" if action == "maintenance_scan": lines.append(f"⏳ SysOps vraagt toestemming voor update-scan: {title}") elif action == "config_backup": lines.append(f"⏳ SysOps vraagt goedkeuring backup: {title}") else: lines.append(f"⏳ {agent} wacht op goedkeuring ({action}): {title}") for row in data.get("recent_executed_actions") or []: lines.append(f"✓ Uitgevoerd door {row.get('agent_key')}: {row.get('title')}") for row in data.get("project_assets_recent") or []: ts = row.get("created_at") ts_s = ts.isoformat()[:16] if hasattr(ts, "isoformat") else str(ts or "")[:16] lines.append( f"📁 Project asset ({row.get('project_name')}): {row.get('title')} " f"[{row.get('asset_type')} · {row.get('source_agent')}] {ts_s}" ) for row in data.get("ops_maintenance_open") or []: lines.append(f"🔧 IT Ops [{row.get('severity')}]: {row.get('title')}") for row in data.get("config_backups_recent") or []: lines.append(f"💾 Backup {row.get('status')}: {row.get('message') or row.get('commit_ref')}") for row in data.get("sysops_activity_24h") or []: ts = row.get("created_at") ts_s = ts.isoformat()[11:16] if hasattr(ts, "isoformat") else "" cref = f" [{row.get('commit_ref')}]" if row.get("commit_ref") else "" lines.append(f"🖥️ SysOps {row.get('action_type')}: {row.get('title')}{cref} ({ts_s})") for row in data.get("sysops_events_24h") or []: if (row.get("event_type") or "") == "gitea_sync": continue ts = row.get("created_at") ts_s = ts.isoformat()[11:16] if hasattr(ts, "isoformat") else "" lines.append(f"🔧 SysOps: {row.get('title')} ({ts_s})") for row in (data.get("recent_events") or [])[:8]: ts = row.get("created_at") ts_s = ts.isoformat()[11:16] if hasattr(ts, "isoformat") else "" lines.append(f"⚡ {row.get('agent_name')}: {row.get('title')} ({ts_s})") return lines[:25] def build_template_report(data: dict[str, Any]) -> str: lines = [ f"# Foodlinkk Dagrapport — {data['date']}", "", f"*Gegenereerd: {data['generated_at'][:19]} UTC · Model: {settings.OLLAMA_MODEL}*", "", "## KPI's", f"- **Klanten:** {data['clients']} · **Deals:** {data['deals']} · **Pipeline:** €{data['pipeline_eur']:,.0f}", f"- **Supermarkten in DB:** {data.get('supermarkets', 0)} · **CRM partnerships:** {data.get('crm_partnerships', 0)}", f"- **Groothandels:** {data.get('wholesalers', 0)} · **Goedkeuringen open:** {data['pending_approvals']}", "", ] if data.get("top_opportunities"): lines.extend(["## Top halal-markt kansen (Retail 360)"]) for row in data["top_opportunities"]: score = round(float(row.get("halal_opportunity_score") or 0)) lines.append(f"- **{row.get('chain')} · {row.get('name')}** ({row.get('city')}) — score {score}/100") lines.append("") if data.get("milestones_pending"): lines.extend(["## Sales milestones — open"]) for row in data["milestones_pending"]: td = row.get("target_date") td_s = td.isoformat()[:10] if hasattr(td, "isoformat") else str(td or "—")[:10] lines.append(f"- [{td_s}] **{row.get('title')}** · {row.get('chain') or ''} {row.get('store_name') or ''} · €{row.get('value_eur') or '—'}") lines.append("") if data.get("rss_highlights"): lines.extend(["## Kant-en-klaar & supermarkt nieuws"]) for row in data["rss_highlights"]: lines.append(f"- [{row.get('feed_name')}] {row.get('title')}") lines.append("") if data.get("market_trends"): lines.extend(["## Markt trends"]) for row in data["market_trends"]: pct = round(float(row.get("opportunity_score") or 0) * 100) lines.append(f"- **{row.get('trend_name')}** ({pct}% kans) — {row.get('description') or ''}") lines.append("") lines.extend(["## Pipeline per stage"]) for row in data.get("deals_by_stage") or []: lines.append(f"- **{row.get('stage')}:** {row.get('cnt')} deals · €{float(row.get('total') or 0):,.0f}") if not data.get("deals_by_stage"): lines.append("- Geen deals in database.") if data.get("calendar_events"): lines.extend(["", "## Agenda (7 dagen)"]) for row in data["calendar_events"]: ts = row.get("starts_at") ts_s = ts.isoformat()[:16] if hasattr(ts, "isoformat") else str(ts)[:16] lines.append(f"- [{ts_s}] {row.get('title')} ({row.get('client_name') or '-'})") if data.get("sysops_activity_24h"): lines.extend(["", "## SysOps IT — laatste 24 uur"]) for row in data["sysops_activity_24h"][:12]: ts = row.get("created_at") ts_s = ts.isoformat()[11:16] if hasattr(ts, "isoformat") else "" cref = f" · commit `{row.get('commit_ref')}`" if row.get("commit_ref") else "" lines.append(f"- [{ts_s}] **{row.get('title')}**{cref}") if row.get("body"): lines.append(f" {str(row.get('body'))[:200]}") if data.get("pending_approval_requests"): lines.extend(["", "## ⏳ Wacht op jouw goedkeuring (agents)"]) for row in data["pending_approval_requests"]: action = row.get("action_type") or "" label = "Update-scan VM106" if action == "maintenance_scan" else action lines.append(f"- **{row.get('agent_key')}** · {label}: {row.get('title')}") if data.get("activity_log"): lines.extend(["", "## Herman activiteitenlog (24u)"]) for entry in data["activity_log"][:20]: lines.append(f"- {entry}") if data.get("pending_items"): lines.extend(["", "## Legacy goedkeuringen"]) for row in data["pending_items"]: lines.append(f"- {row.get('agent_name')}: {row.get('title')}") return "\n".join(lines) async def _ai_executive_summary(data: dict[str, Any]) -> str: opp_lines = "" for row in data.get("top_opportunities") or []: opp_lines += f"- {row.get('chain')} {row.get('name')} ({row.get('city')}): score {round(float(row.get('halal_opportunity_score') or 0))}\n" ms_lines = "" for row in data.get("milestones_pending") or []: ms_lines += f"- {row.get('title')} ({row.get('chain') or 'CRM'}) deadline {row.get('target_date') or '?'}\n" activity = "\n".join((data.get("activity_log") or [])[:15]) or "- Geen recente agent-acties" prompt = ( "Schrijf in het Nederlands (markdown) voor CEO Aïssa van Foodlinkk (halal kant-en-klaar maaltijden):\n\n" "## Samenvatting\n(5-7 zinnen: pipeline, retail, IT ops, agent activiteit vandaag)\n\n" "## Actiepunten vandaag — korte termijn\n(minimaal 5 bullets — incl. open goedkeuringen SysOps scan/backup)\n\n" "## Lange termijn focus\n(3-5 bullets)\n\n" "## Herman documentatie — wat er gebeurde\n(korte chronologische samenvatting van agent-acties, project assets, backups)\n\n" f"Data vandaag ({data['date']}):\n" f"- Pipeline €{data['pipeline_eur']:,.0f}, {data['clients']} klanten, {data['deals']} deals\n" f"- {data.get('supermarkets',0)} supermarkten, {data.get('crm_partnerships',0)} actieve CRM partnerships\n" f"- {data['pending_approvals']} goedkeuringen open in approval queue\n" f"Activiteitenlog:\n{activity}\n" f"Top kansen:\n{opp_lines or '- geen data'}\n" f"Milestones open:\n{ms_lines or '- geen milestones'}\n" ) system = ( "Je bent Herman, AI co-CEO van Foodlinkk. Documenteer en vat samen wat agents en IT hebben gedaan. " "Noem expliciet openstaande SysOps scan/backup verzoeken als die in de log staan. " "Schrijf warm, professioneel, actionable." ) try: return await ollama.generate(prompt, system=system, timeout=120.0) except Exception: return "" def _fallback_summary(data: dict[str, Any]) -> str: opp = data.get("top_opportunities") or [] ms = data.get("milestones_pending") or [] lines = [ "## Samenvatting", f"Vandaag ({data['date']}) heb je **€{data['pipeline_eur']:,.0f}** in je pipeline en **{data.get('crm_partnerships',0)} actieve supermarkt-partnerships**. " f"In Retail 360 staan **{data.get('supermarkets',0)} filialen** met live CBS-data.", ] if opp: top = opp[0] lines.append( f"De grootste halal-kans is **{top.get('chain')} · {top.get('name')}** in {top.get('city')} " f"(score {round(float(top.get('halal_opportunity_score') or 0))}/100)." ) lines.extend(["", "## Actiepunten vandaag — korte termijn"]) actions = [ f"Keur {data['pending_approvals']} open agent-verzoeken goed (dashboard → Goedkeuringen)", "Open Retail 360 en benader top-3 halal-gap filialen via CRM koppeling", "Check Marketing Live Feed voor kant-en-klaar trends", ] for row in data.get("pending_approval_requests") or []: if row.get("action_type") == "maintenance_scan": actions.insert(0, f"**SysOps update-scan:** {row.get('title')} — keur goed op dashboard") break if ms: actions.insert(0, f"Follow-up milestone: **{ms[0].get('title')}**") for a in actions[:6]: lines.append(f"- {a}") lines.extend(["", "## Lange termijn focus"]) lines.extend([ "- Schaal CRM partnerships van proposal naar actief in top-10 kans-filialen", "- Halal kant-en-klaar listing bij Jumbo/AH regio's met hoogste demografische vraag", "- Wekelijks milestones review in Retail 360 sales tab", ]) return "\n".join(lines) def _save_briefing(content: str, data: dict[str, Any]) -> None: safe = serialize_stats(data) metadata = {"stats": safe, "model": settings.OLLAMA_MODEL, "type": "daily_ceo_report"} try: execute( "INSERT INTO daily_briefings (content, generated_by, metadata) VALUES (%s, %s, %s::jsonb)", (content, "herman", json.dumps(metadata)), ) except Exception: pass try: execute( """INSERT INTO llm_memory (category, subject, content, source, metadata, updated_at) VALUES ('herman_daily', %s, %s, 'herman', %s::jsonb, NOW())""", ( f"Briefing {data['date']}", content[:8000], json.dumps({"date": data["date"], "activity_count": len(data.get("activity_log") or [])}), ), ) except Exception: pass try: execute( """INSERT INTO agent_events (agent_name, agent_type, event_type, title, body, status, channel, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb)""", ( "herman", "herman_delegate", "briefing", f"CEO dagrapport {data['date']}", content[:2000], "completed", "dashboard", json.dumps({"stats": safe, "activity_log": data.get("activity_log", [])}), ), ) except Exception: pass async def generate_daily_briefing() -> tuple[str, dict[str, Any]]: data = collect_briefing_data() template = build_template_report(data) try: ai_part = await asyncio.wait_for(_ai_executive_summary(data), timeout=25.0) except (asyncio.TimeoutError, Exception): ai_part = "" 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) return content, serialize_stats(data)