Files
foodlinkk-command-center/tools-api/app/recommendations.py
T

166 lines
6.5 KiB
Python
Raw Normal View History

"""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.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.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)