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