2026-06-09 00:41:27 +00:00
"""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
2026-07-19 18:25:01 +00:00
from app.halal_reco_engine import live_halal_recommendations
2026-06-09 00:41:27 +00:00
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 )
2026-07-19 18:25:01 +00:00
@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 )
2026-06-09 00:41:27 +00:00
@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 )