from fastapi import FastAPI, Form, Response, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import requests import datetime import hashlib import os app = FastAPI(title="NEXUS COMMAND CENTER") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID", "AC3f5f0eb3da2c3f839fb021ac203ad781") TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN", "88e8a43f9abb88f9abdbf7867c80ab30") TWILIO_WHATSAPP_NUMBER = "whatsapp:+14155238886" tickets_db = {} compteur_ticket = 1 class DispatchOrdre(BaseModel): service: str class DemandeWeb(BaseModel): description: str def analyser_et_repondre_ia(texte_message: str, numero_expediteur: str, ticket_id: int): """Fonction exécutée en arrière-plan : interroge Ollama et met à jour le Dashboard""" try: url_ollama = "http://127.0.0.1:11434/api/generate" payload = { "model": "gemma4:e4b", "prompt": f"Tu es le dispatcher d'urgence NEXUS. Analyse ce message et réponds en incluant le service d'urgence le plus approprié (POLICE, POMPIERS, ou SAMU) : {texte_message}", "stream": False } reponse_ollama = requests.post(url_ollama, json=payload, timeout=180) analyse_ia = reponse_ollama.json().get("response", "Erreur IA") domaine_ia = "INCONNU" texte_maj = analyse_ia.upper() if "POLICE" in texte_maj: domaine_ia = "POLICE" elif "POMPIERS" in texte_maj: domaine_ia = "POMPIERS" elif "SAMU" in texte_maj: domaine_ia = "SAMU" tickets_db[ticket_id]["domaine_ia"] = domaine_ia tickets_db[ticket_id]["score"] = 8 if domaine_ia != "INCONNU" else 5 tickets_db[ticket_id]["status"] = "EN ATTENTE DISPATCH" except Exception as e: analyse_ia = f"⚠️ Échec du traitement local : {str(e)}" tickets_db[ticket_id]["domaine_ia"] = "ERREUR IA" url_twilio = f"https://api.twilio.com/2010-04-01/Accounts/{TWILIO_ACCOUNT_SID}/Messages.json" donnees_whatsapp = { "From": TWILIO_WHATSAPP_NUMBER, "To": numero_expediteur, "Body": f"🧠 [NEXUS PRIME - ANALYSE IA]\n\n{analyse_ia}" } requests.post(url_twilio, data=donnees_whatsapp, auth=(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)) @app.post("/api/twilio") async def twilio_webhook(background_tasks: BackgroundTasks, Body: str = Form(...), From: str = Form(...)): """Route Webhook : Répond immédiatement, sauvegarde le ticket et lance l'IA""" global compteur_ticket msg_hash = hashlib.md5(f"{From}:{Body}".encode()).hexdigest() if any(t.get("hash") == msg_hash for t in tickets_db.values()): return Response(content="", media_type="application/xml") ticket_id = compteur_ticket compteur_ticket += 1 tickets_db[ticket_id] = { "id": ticket_id, "heure": datetime.datetime.now().strftime("%H:%M:%S"), "telephone": From, "message": Body, "domaine_ia": "Analyse en cours... ⏳", "score": 0, "status": "NOUVEAU", "hash": msg_hash } background_tasks.add_task(analyser_et_repondre_ia, Body, From, ticket_id) reponse_twiml = f""" NEXUS a bien reçu votre signalement (Dossier #{ticket_id}). Analyse IA en cours... ⏳ """ return Response(content=reponse_twiml, media_type="application/xml") @app.get("/api/tickets") def get_tickets(): """Route pour que le Dashboard HTML récupère la liste des urgences""" return sorted(list(tickets_db.values()), key=lambda x: x["id"], reverse=True) @app.post("/api/tickets/{ticket_id}/dispatch") def dispatch_ticket(ticket_id: int, ordre: DispatchOrdre): """Route appelée par le bouton du Dashboard pour envoyer les secours""" if ticket_id not in tickets_db: return {"error": "Ticket introuvable"} ticket = tickets_db[ticket_id] service_cible = ordre.service ticket["status"] = f"TRANSFÉRÉ -> {service_cible}" message_citoyen = f"✅ NEXUS INFO : Votre urgence a été transmise aux {service_cible}. Les secours sont en route." url_twilio = f"https://api.twilio.com/2010-04-01/Accounts/{TWILIO_ACCOUNT_SID}/Messages.json" donnees = {"From": TWILIO_WHATSAPP_NUMBER, "To": ticket["telephone"], "Body": message_citoyen} requests.post(url_twilio, data=donnees, auth=(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)) return {"status": "success", "ticket": ticket} @app.post("/api/evaluer") def evaluer_ticket_web(demande: DemandeWeb): """Route réelle : interroge l'IA locale (Ollama) en direct pour le site web""" try: url_ollama = "http://127.0.0.1:11434/api/generate" payload = { "model": "gemma4:e4b", "prompt": f"Tu es le dispatcher d'urgence NEXUS. Analyse ce message et réponds en incluant le service d'urgence le plus approprié (POLICE, POMPIERS, ou SAMU) : {demande.description}", "stream": False } reponse_ollama = requests.post(url_ollama, json=payload, timeout=180) analyse_ia = reponse_ollama.json().get("response", "INCONNU").upper() domaine = "INCONNU" score = 5 friction = "COMPLET" question = "" if "POLICE" in analyse_ia: domaine = "POLICE" score = 8 elif "POMPIERS" in analyse_ia: domaine = "POMPIERS" score = 9 elif "SAMU" in analyse_ia: domaine = "SAMU" score = 10 else: friction = "INCOMPLET" score = 3 question = "L'analyse IA nécessite plus de contexte. Pouvez-vous préciser la nature exacte de l'urgence ?" return { "domaine": domaine, "score": score, "friction": friction, "question_ia": question } except Exception as e: return { "domaine": "ERREUR", "score": 0, "friction": "ERREUR SERVEUR", "question_ia": f"Impossible de joindre le modèle IA local : {str(e)}" }