4dd1b8265f
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.
182 lines
7.1 KiB
Python
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)
|