Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user