Files
Aissa 4dd1b8265f Add live CEO briefing, Wereldexport CRM, halal engine, and realtime cockpit.
Ship export intel with contact filters and CRM push, Herman live digest with Telegram briefing, halal recommendation engine, revenue cockpit, and dashboard polling/WebSocket fixes.
2026-07-19 18:25:21 +00:00

182 lines
7.1 KiB
Python

"""AI recommendations — Herman filter output."""
from __future__ import annotations
import json
import os
import urllib.request
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Query
from app.db import execute, execute_returning, fetch_all, fetch_one, json_param
from app.halal_reco_engine import live_halal_recommendations
from app.middleware import log_agent_event
router = APIRouter(prefix="/recommendations", tags=["recommendations"])
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://10.4.7.19:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3:8b")
HERMAN_URL = os.getenv("HERMAN_ORCHESTRATOR_URL", "http://10.4.7.19:8090")
def _serialize(row: dict) -> dict[str, Any]:
out = {}
for k, v in row.items():
if hasattr(v, "isoformat"):
out[k] = v.isoformat()
elif type(v).__name__ == "Decimal":
out[k] = float(v)
else:
out[k] = v
return out
@router.get("/pending")
def pending_recommendations(limit: int = Query(10, ge=1, le=50)) -> dict[str, Any]:
rows = fetch_all(
"""SELECT * FROM ai_recommendations WHERE status = 'pending'
ORDER BY impact_score DESC NULLS LAST, created_at DESC LIMIT %s""",
(limit,),
)
return {"items": [_serialize(r) for r in rows]}
@router.get("/strategies")
def list_strategies(active_only: bool = True) -> dict[str, Any]:
q = "SELECT * FROM marketing_strategies"
if active_only:
q += " WHERE is_active = true"
q += " ORDER BY updated_at DESC LIMIT 20"
return {"items": [_serialize(r) for r in fetch_all(q)]}
@router.post("/generate")
def generate_recommendations() -> dict[str, Any]:
briefs = fetch_all("SELECT domain, title, summary FROM research_briefs ORDER BY generated_at DESC LIMIT 5")
deals = fetch_all(
"SELECT d.id, d.title, d.value, d.stage, d.next_action, c.name AS client_name FROM deals d LEFT JOIN clients c ON c.id = d.client_id WHERE d.stage NOT IN ('won','lost')"
)
pending_approvals = fetch_all(
"SELECT id, agent_name, title FROM agent_events WHERE status = 'needs_approval' LIMIT 5"
)
context = {
"briefs": [dict(b) for b in briefs],
"deals": [dict(d) for d in deals],
"approvals": [dict(a) for a in pending_approvals],
}
created = []
for deal in deals:
val = float(deal.get("value") or 0)
title = f"{deal.get('client_name') or 'Deal'}{deal.get('next_action') or 'follow-up'}"
existing = fetch_one(
"SELECT id FROM ai_recommendations WHERE title = %s AND status = 'pending'",
(title[:255],),
)
if existing:
continue
row = execute_returning(
"""INSERT INTO ai_recommendations
(recommendation_type, title, description, priority, impact_score, confidence_score,
data_sources, action_items, generated_by, related_entity_type, related_entity_id, status, expires_at)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'pending',%s) RETURNING id""",
(
"deal_action",
title[:255],
f"Deal {deal.get('title')} — stage {deal.get('stage')} — €{val:,.0f}",
"high" if val >= 50000 else "medium",
min(0.95, 0.5 + val / 200000),
0.85,
json_param({"source": "crm", "deal_id": deal.get("id")}),
[deal.get("next_action") or "Follow-up"],
"herman",
"deal",
deal.get("id"),
datetime.now(timezone.utc) + timedelta(days=7),
),
)
created.append(row["id"])
for appr in pending_approvals:
title = f"Goedkeuren: {appr.get('title')}"
existing = fetch_one(
"SELECT id FROM ai_recommendations WHERE title = %s AND status = 'pending'",
(title[:255],),
)
if not existing:
row = execute_returning(
"""INSERT INTO ai_recommendations
(recommendation_type, title, description, priority, impact_score, confidence_score,
generated_by, status, expires_at)
VALUES ('operational', %s, %s, 'high', 0.80, 0.90, 'herman', 'pending', %s)
RETURNING id""",
(
title[:255],
f"Agent {appr.get('agent_name')} wacht op CEO",
datetime.now(timezone.utc) + timedelta(days=3),
),
)
created.append(row["id"])
if not created and not fetch_one("SELECT id FROM ai_recommendations WHERE status='pending' LIMIT 1"):
row = execute_returning(
"""INSERT INTO ai_recommendations
(recommendation_type, title, description, priority, impact_score, confidence_score,
generated_by, status)
VALUES ('retail_target', 'Retail intelligence uitbreiden', 'Run AH/Jumbo scrapers voor volledige kaart', 'medium', 0.70, 0.75, 'herman', 'pending')
RETURNING id"""
)
created.append(row["id"])
log_agent_event(
agent_name="herman",
event_type="recommendations_generated",
title=f"Generated {len(created)} recommendations",
metadata={"ids": created, "context_keys": list(context.keys())},
)
return {"created": created, "pending_count": len(fetch_all("SELECT id FROM ai_recommendations WHERE status='pending'"))}
@router.post("/{rec_id}/approve")
def approve_recommendation(rec_id: int) -> dict[str, Any]:
row = execute_returning(
"UPDATE ai_recommendations SET status = 'approved', updated_at = NOW() WHERE id = %s RETURNING *",
(rec_id,),
)
if not row:
raise HTTPException(404, "Recommendation not found")
execute_returning(
"""INSERT INTO herman_actions (action_type, title, description, status, completed_at)
VALUES ('recommendation_approved', %s, %s, 'completed', NOW()) RETURNING id""",
(row["title"], row.get("description")),
)
log_agent_event(agent_name="herman", event_type="approval", title=f"Approved recommendation {rec_id}", status="completed")
return _serialize(row)
@router.get("/halal/live")
def halal_recommendations_live(
limit: int = Query(5, ge=1, le=20),
refresh: bool = False,
brand: str = Query("cucina"),
) -> dict[str, Any]:
"""Live halal retail kansen — divers, geen actieve partnerships, composite score."""
return live_halal_recommendations(limit=limit, refresh_scores=refresh, brand=brand)
@router.post("/halal/refresh")
def halal_recommendations_refresh(limit: int = Query(5, ge=1, le=20)) -> dict[str, Any]:
return live_halal_recommendations(limit=limit, refresh_scores=True)
@router.post("/{rec_id}/dismiss")
def dismiss_recommendation(rec_id: int) -> dict[str, Any]:
row = execute_returning(
"UPDATE ai_recommendations SET status = 'dismissed', updated_at = NOW() WHERE id = %s RETURNING *",
(rec_id,),
)
if not row:
raise HTTPException(404, "Recommendation not found")
return _serialize(row)