import base64 import json import os import re import smtplib import tempfile from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from pathlib import Path from dotenv import load_dotenv from flask import Flask, jsonify, request, send_from_directory from groq import Groq from memory import ( add_memory, add_repository, add_task, close_task, get_memory_snapshot, init_db, list_clients, search_memory, upsert_client, upsert_project, ) load_dotenv() ROOT = Path(__file__).resolve().parent STATIC_DIR = ROOT / "static" CONTACTS_FILE = Path(os.getenv("CONTACTS_FILE", ROOT / "contacts.json")) GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") GMAIL_EMAIL = os.getenv("GMAIL_EMAIL", "") GMAIL_PASSWORD = os.getenv("GMAIL_PASSWORD", "") CHAT_MODEL = "llama-3.3-70b-versatile" STT_MODEL = "whisper-large-v3-turbo" TTS_MODEL = "canopylabs/orpheus-v1-english" TTS_VOICE = "troy" TTS_PROVIDER = os.getenv("TTS_PROVIDER", "browser").lower() app = Flask(__name__, static_folder=str(STATIC_DIR), static_url_path="") app.secret_key = os.getenv("APP_SECRET", "dev-secret-change-me") init_db() client = Groq(api_key=GROQ_API_KEY) histories = {} session_contexts = {} SYSTEM_PROMPT = """Tu es l'assistant personnel vocal de Laurent. Tu peux discuter normalement en francais et aider a envoyer des emails via Gmail. Quand l'utilisateur veut envoyer un email, collecte les informations manquantes : destinataire, sujet, contenu. Quand tu as toutes les informations pour envoyer un email, reponds uniquement avec ce JSON : {"action": "envoyer_email", "destinataire": "...", "sujet": "...", "corps": "..."} Quand l'utilisateur veut ajouter, memoriser ou modifier un contact email, collecte le nom et l'adresse email. Quand tu as les deux informations, reponds uniquement avec ce JSON : {"action": "ajouter_contact", "nom": "...", "email": "..."} Tu disposes aussi d'une memoire professionnelle persistante pour les clients, projets, taches, notes et depots Git. Quand l'utilisateur te demande de noter, memoriser, ajouter ou associer une information professionnelle, reponds uniquement avec un JSON. Actions disponibles : {"action": "ajouter_client", "nom": "...", "notes": "...", "statut": "actif"} {"action": "ajouter_projet", "nom": "...", "client": "...", "statut": "...", "resume": "...", "prochaine_action": "..."} {"action": "ajouter_tache", "titre": "...", "client": "...", "projet": "...", "details": "...", "echeance": "..."} {"action": "terminer_tache", "id": 123} {"action": "ajouter_repo", "nom": "...", "projet": "...", "chemin": "...", "url": "...", "branche": "..."} {"action": "ajouter_note", "contenu": "...", "sujet": "...", "client": "...", "projet": "...", "type": "note"} {"action": "chercher_memoire", "requete": "..."} {"action": "lister_clients"} Si une phrase contient plusieurs choses a enregistrer, utilise : {"actions": [{...}, {...}]} Quand l'utilisateur demande ce que tu sais d'un client, d'un projet, d'une tache ou d'un depot, utilise chercher_memoire. Si l'utilisateur dit "pour CLIENT" en ajoutant une tache, renseigne toujours le champ client avec CLIENT. Pour tout le reste, reponds en francais naturel, avec des phrases courtes. N'utilise pas d'anglais sauf si l'utilisateur le demande explicitement. En mode voiture, sois bref : une ou deux phrases maximum.""" def charger_contacts(): try: return json.loads(CONTACTS_FILE.read_text(encoding="utf-8")) except FileNotFoundError: return {"laurent": "barontil@hotmail.com"} def sauvegarder_contacts(contacts): CONTACTS_FILE.parent.mkdir(parents=True, exist_ok=True) CONTACTS_FILE.write_text( json.dumps(contacts, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def construire_system_prompt(session_id=None): contacts = charger_contacts() lignes = [f"- {nom.title()} : {email}" for nom, email in contacts.items()] return ( SYSTEM_PROMPT + "\n\nContacts connus :\n" + "\n".join(lignes) + "\n\nMemoire professionnelle actuelle :\n" + construire_contexte_memoire(session_id) ) def construire_contexte_memoire(session_id=None): snapshot = get_memory_snapshot() contexte = session_contexts.get(session_id or "", {}) lignes = [] clients_mem = snapshot["clients"] if clients_mem: lignes.append("Clients connus et taches ouvertes :") for client_mem in clients_mem: lignes.append(f"- {client_mem['name']} : {client_mem['open_task_count']} tache(s) ouverte(s)") else: lignes.append("Aucun client connu.") if snapshot["tasks"]: lignes.append("Taches ouvertes recentes :") for tache in snapshot["tasks"][:15]: sujet = tache.get("client_name") or tache.get("project_name") or "general" lignes.append(f"- #{tache['id']} {sujet} : {tache['title']}") if snapshot["projects"]: lignes.append("Projets recents :") for projet in snapshot["projects"][:10]: client_nom = projet.get("client_name") or "sans client" statut = projet.get("status") or "actif" lignes.append(f"- {projet['name']} ({client_nom}) : {statut}") if snapshot["repositories"]: lignes.append("Depots Git associes :") for repo in snapshot["repositories"][:10]: cible = repo.get("project_name") or "sans projet" chemin = repo.get("local_path") or repo.get("remote_url") or "chemin non renseigne" lignes.append(f"- {repo['name']} -> {cible} : {chemin}") if contexte: lignes.append("Contexte de la conversation en cours :") if contexte.get("last_client"): lignes.append(f"- dernier client mentionne : {contexte['last_client']}") if contexte.get("last_project"): lignes.append(f"- dernier projet mentionne : {contexte['last_project']}") if contexte.get("tasks"): ids = ", ".join(f"#{tache['id']} {tache['title']}" for tache in contexte["tasks"][:5]) lignes.append(f"- dernieres taches affichees : {ids}") lignes.append( "Utilise ce contexte pour repondre aux questions globales. " "Pour modifier la memoire, reponds avec une action JSON." ) return "\n".join(lignes) def resoudre_contact(destinataire): contacts = charger_contacts() valeur = destinataire.strip() return contacts.get(valeur.lower(), valeur) def extraire_action(reponse): decodeur = json.JSONDecoder() for index, caractere in enumerate(reponse): if caractere != "{": continue try: data, _ = decodeur.raw_decode(reponse[index:]) except json.JSONDecodeError: continue if isinstance(data, dict) and (data.get("action") in ACTIONS or isinstance(data.get("actions"), list)): return data return None ACTIONS = { "envoyer_email", "ajouter_contact", "ajouter_client", "ajouter_projet", "ajouter_tache", "terminer_tache", "ajouter_repo", "ajouter_note", "chercher_memoire", "lister_clients", } def ajouter_contact(nom, email): contacts = charger_contacts() nom_normalise = " ".join(nom.strip().lower().split()) email_normalise = email.strip() if not nom_normalise or "@" not in email_normalise: raise ValueError("Contact incomplet") contacts[nom_normalise] = email_normalise sauvegarder_contacts(contacts) return nom_normalise, email_normalise def envoyer_email(destinataire, sujet, corps): destinataire = resoudre_contact(destinataire) msg = MIMEMultipart() msg["From"] = GMAIL_EMAIL msg["To"] = destinataire msg["Subject"] = sujet msg.attach(MIMEText(corps, "plain", "utf-8")) with smtplib.SMTP("smtp.gmail.com", 587) as serveur: serveur.starttls() serveur.login(GMAIL_EMAIL, GMAIL_PASSWORD) serveur.sendmail(GMAIL_EMAIL, destinataire, msg.as_string()) return destinataire def demander_au_llm(session_id, texte): historique = histories.setdefault(session_id, []) historique.append({"role": "user", "content": texte}) del historique[:-20] reponse = client.chat.completions.create( model=CHAT_MODEL, messages=[{"role": "system", "content": construire_system_prompt(session_id)}] + historique, temperature=0.45, max_tokens=900, ) contenu = reponse.choices[0].message.content historique.append({"role": "assistant", "content": contenu}) return contenu def detecter_action_directe(texte, session_id=None): texte_nettoye = texte.strip() texte_min = texte_nettoye.lower() clients_a_ajouter = extraire_liste_clients(texte_nettoye) if clients_a_ajouter: return { "actions": [ { "action": "ajouter_client", "nom": nom, "notes": "Ajoute depuis une liste fournie par Laurent.", "statut": "actif", } for nom in clients_a_ajouter ] } tache_client = detecter_tache_client(texte_nettoye) if tache_client: return tache_client recherche_taches = detecter_recherche_taches(texte_nettoye) if recherche_taches: return recherche_taches suppression_tache = detecter_suppression_tache(texte_nettoye, session_id) if suppression_tache: return suppression_tache recherche_match = re.search( r"(?:qu(?:'| )?est[- ]?ce que tu sais sur|que sais[- ]?tu sur|cherche dans la memoire|memoire sur)\s+(.+)", texte_nettoye, flags=re.IGNORECASE, ) if recherche_match: return { "action": "chercher_memoire", "requete": recherche_match.group(1).strip(" ?.,;:"), } if detecter_liste_clients_demandee(texte_min): return {"action": "lister_clients"} if re.search(r"\b(liste|affiche|montre)\b.*\b(projets?|taches?|repos?|depots?)\b", texte_min): return {"action": "chercher_memoire", "requete": ""} repo_match = re.search( r"(?:associe|ajoute|note)\s+(?:le\s+)?repo\s+(\S+)(?:\s+au\s+projet\s+(.+))?", texte_nettoye, flags=re.IGNORECASE, ) if repo_match: chemin_ou_url = repo_match.group(1).strip(" .,;:") projet = (repo_match.group(2) or "").strip(" .,;:") return { "action": "ajouter_repo", "chemin": chemin_ou_url if not chemin_ou_url.startswith(("http://", "https://", "git@")) else "", "url": chemin_ou_url if chemin_ou_url.startswith(("http://", "https://", "git@")) else "", "projet": projet, } client_match = re.search( r"\b([A-Z0-9][A-Z0-9&.\- ]{2,})\s+est\s+(?:un|une)\s+nouveau\s+client\b", texte_nettoye, flags=re.IGNORECASE, ) if not client_match: return None nom_client = client_match.group(1).strip(" .,;:") actions = [ { "action": "ajouter_client", "nom": nom_client, "notes": texte_nettoye, "statut": "actif", } ] rappel_match = re.search( r"(?:je\s+dois\s+|il\s+faut\s+|note\s+(?:quelque\s+part\s+)?que\s+je\s+dois\s+)(rappeler\s+.+)", texte_nettoye, flags=re.IGNORECASE, ) if rappel_match: actions.append( { "action": "ajouter_tache", "titre": rappel_match.group(1).strip(" .,;:"), "client": nom_client, "details": texte_nettoye, } ) return {"actions": actions} def detecter_tache_client(texte): if not re.search(r"\b(note|notes|noter|tache|doit|dois|a faire|à faire)\b", texte, flags=re.IGNORECASE): return None match = re.search( r"\bpour\s+(?:mon\s+client\s+|le\s+client\s+)?([A-Z0-9][A-Z0-9&.\- ]{1,40}?)[,\s]+(?:on\s+)?(?:doit|dois|devra|faut)\s+(.+?)(?:\s+tu\s+peux\s+noter.*|\s+note.*|\s+notes.*|\s*$)", texte, flags=re.IGNORECASE, ) if not match: return None client_nom = nettoyer_nom_client(match.group(1)) titre = nettoyer_titre_tache(match.group(2)) if not client_nom or not titre: return None return { "action": "ajouter_tache", "titre": titre, "client": client_nom, "details": texte, } def detecter_recherche_taches(texte): match = re.search( r"\b(?:que\s+dois[- ]?je\s+faire|quoi\s+faire|taches?|a faire|à faire)\b.*\b(?:pour|sur)\s+(?:mon\s+client\s+|le\s+client\s+)?([A-Z0-9][A-Z0-9&.\- ]{1,40})", texte, flags=re.IGNORECASE, ) if not match: return None client_nom = nettoyer_nom_client(match.group(1)) if not client_nom: return None return {"action": "chercher_memoire", "requete": client_nom} def detecter_suppression_tache(texte, session_id): if not re.search(r"\b(enleve|enlève|supprime|retire|efface|termine|marque.*faite)\b", texte, flags=re.IGNORECASE): return None if not re.search(r"\btaches?\b", texte, flags=re.IGNORECASE): return None contexte = session_contexts.get(session_id or "", {}) taches = contexte.get("tasks") or [] if not taches: return None client_match = re.search( r"\b(?:pour|sur)\s+(?:mon\s+client\s+|le\s+client\s+)?([A-Z0-9][A-Z0-9&.\- ]{1,40})", texte, flags=re.IGNORECASE, ) client_nom = nettoyer_nom_client(client_match.group(1)) if client_match else contexte.get("last_client", "") candidates = [ tache for tache in taches if not client_nom or (tache.get("client_name") or "").lower() == client_nom.lower() ] if not candidates: candidates = taches if re.search(r"\b(derniere|dernière|dernier|derniere affichee|dernière affichée)\b", texte, flags=re.IGNORECASE): task_id = candidates[-1].get("id") else: task_id = candidates[0].get("id") if len(candidates) == 1 else None if not task_id: return None return {"action": "terminer_tache", "id": task_id} def detecter_liste_clients_demandee(texte_min): if not re.search(r"\bclients?\b", texte_min): return False return bool( re.search(r"\b(liste|affiche|montre|donne)\b.*\bclients?\b", texte_min) or re.search(r"\bclients?\b.*\b(liste|affiche|montre|donne)\b", texte_min) or re.search(r"\b(quels?|qui)\s+sont\b.*\bclients?\b", texte_min) or re.search(r"\btous\s+mes\s+clients?\b", texte_min) or re.search(r"\bmes\s+clients?\b", texte_min) ) def extraire_liste_clients(texte): if not re.search(r"\bclients?\b", texte, flags=re.IGNORECASE): return [] if not re.search(r"\b(voici|liste|note|notes|noter|memorise|ajoute|ajoutes)\b", texte, flags=re.IGNORECASE): return [] lignes = [ligne.strip(" -\t\r.,;") for ligne in texte.splitlines()] candidats = [] apres_entete = False for ligne in lignes: if not ligne: continue ligne_min = ligne.lower() if "client" in ligne_min: apres_entete = True apres_deux_points = ligne.split(":", 1)[1].strip() if ":" in ligne else "" if apres_deux_points: candidats.extend(separer_noms_clients(apres_deux_points)) continue if not apres_entete: continue if re.search(r"\b(note|notes|noter|quelque part|merci|stp|s'il te plait)\b", ligne_min): continue candidats.extend(separer_noms_clients(ligne)) noms = [] deja_vus = set() for candidat in candidats: nom = nettoyer_nom_client(candidat) if not nom: continue cle = nom.lower() if cle not in deja_vus: noms.append(nom) deja_vus.add(cle) return noms def separer_noms_clients(texte): morceaux = re.split(r"[,;/]|\s+\bet\b\s+", texte) return [morceau.strip() for morceau in morceaux if morceau.strip()] def nettoyer_nom_client(nom): nom = nom.strip(" -\t\r.,;:") if not nom or len(nom) < 2: return "" if re.search(r"\b(liste|clients?|voici|note|notes|noter|quelque|part)\b", nom, flags=re.IGNORECASE): return "" if len(nom.split()) > 4: return "" return nom def nettoyer_titre_tache(titre): titre = titre.strip(" -\t\r.,;:?") titre = re.sub(r"^de\s+", "", titre, flags=re.IGNORECASE) titre = re.sub(r"\s+", " ", titre) return titre[:1].upper() + titre[1:] if titre else "" def transcrire_audio(fichier): suffix = Path(fichier.filename or "voice.webm").suffix or ".webm" with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: fichier.save(tmp) chemin = tmp.name try: with open(chemin, "rb") as audio: transcription = client.audio.transcriptions.create( model=STT_MODEL, file=audio, language="fr", response_format="text", ) return transcription if isinstance(transcription, str) else transcription.text finally: try: os.remove(chemin) except OSError: pass def generer_audio(texte): texte = texte.strip() if len(texte) > 900: texte = texte[:897] + "..." reponse_audio = client.audio.speech.create( model=TTS_MODEL, voice=TTS_VOICE, input=texte, response_format="wav", speed=1.0, ) with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp: chemin = tmp.name try: reponse_audio.write_to_file(chemin) audio = Path(chemin).read_bytes() return "data:audio/wav;base64," + base64.b64encode(audio).decode("ascii") finally: try: os.remove(chemin) except OSError: pass def executer_action(action): nom_action = action.get("action") if nom_action == "envoyer_email": destinataire = envoyer_email(action["destinataire"], action["sujet"], action["corps"]) return f"Email envoye a {destinataire}.", {"emailSent": True} if nom_action == "ajouter_contact": nom, email = ajouter_contact(action["nom"], action["email"]) return f"Contact ajoute : {nom.title()}.", {"contactSaved": True} if nom_action == "ajouter_client": client_mem = upsert_client( action.get("nom", ""), notes=action.get("notes", ""), status=action.get("statut", "actif"), ) return f"C'est note : {client_mem['name']} est dans tes clients.", {"memorySaved": True} if nom_action == "ajouter_projet": projet = upsert_project( action.get("nom", ""), client_name=action.get("client", ""), status=action.get("statut", "actif"), summary=action.get("resume", ""), next_action=action.get("prochaine_action", ""), ) return f"C'est note pour le projet {projet['name']}.", {"memorySaved": True} if nom_action == "ajouter_tache": tache = add_task( action.get("titre", ""), client_name=action.get("client", ""), project_name=action.get("projet", ""), details=action.get("details", ""), due_at=action.get("echeance", ""), ) return f"Tache ajoutee : {tache['title']}.", {"memorySaved": True} if nom_action == "terminer_tache": tache = close_task(action.get("id")) if not tache: return "Je n'ai pas retrouve cette tache ouverte.", {"memoryRead": True} contexte = tache.get("client_name") or tache.get("project_name") or "memoire professionnelle" return f"C'est fait : j'ai retire la tache {tache['title']} pour {contexte}.", {"memorySaved": True} if nom_action == "ajouter_repo": repo = add_repository( action.get("nom", ""), project_name=action.get("projet", ""), local_path=action.get("chemin", ""), remote_url=action.get("url", ""), main_branch=action.get("branche", ""), ) return f"Depot associe : {repo['name']}.", {"memorySaved": True} if nom_action == "ajouter_note": note = add_memory( action.get("contenu", ""), kind=action.get("type", "note"), subject=action.get("sujet", ""), client_name=action.get("client", ""), project_name=action.get("projet", ""), ) sujet = note["subject"] or "memoire professionnelle" return f"C'est note dans {sujet}.", {"memorySaved": True} if nom_action == "chercher_memoire": resultat = search_memory(action.get("requete", "")) return formater_memoire(resultat), {"memoryRead": True, "memoryResult": resultat, "memoryQuery": action.get("requete", "")} if nom_action == "lister_clients": clients_mem = list_clients() return formater_liste_clients(clients_mem), { "memoryRead": True, "memoryResult": {"clients": clients_mem, "projects": [], "tasks": [], "repositories": [], "memories": []}, "memoryQuery": "", } return "", {} def executer_actions(action_data): actions = action_data.get("actions") if not isinstance(actions, list): actions = [action_data] reponses = [] flags = { "emailSent": False, "contactSaved": False, "memorySaved": False, "memoryRead": False, "memoryResult": None, "memoryQuery": "", } actions_executees = [] for action in actions: if not isinstance(action, dict): continue reponse, action_flags = executer_action(action) actions_executees.append(action) if reponse: reponses.append(reponse) for cle in ("emailSent", "contactSaved", "memorySaved", "memoryRead"): flags[cle] = flags[cle] or bool(action_flags.get(cle)) if action_flags.get("memoryResult"): flags["memoryResult"] = action_flags["memoryResult"] flags["memoryQuery"] = action_flags.get("memoryQuery", "") if actions_executees and all(action.get("action") == "ajouter_client" for action in actions_executees): noms = [action.get("nom", "").strip() for action in actions_executees if action.get("nom", "").strip()] if len(noms) > 1: return f"C'est note : {len(noms)} clients ajoutes ({', '.join(noms)}).", flags return " ".join(reponses), flags def mettre_a_jour_contexte(session_id, texte, reponse, flags): historique = histories.setdefault(session_id, []) if not historique or historique[-1].get("content") != texte: historique.append({"role": "user", "content": texte}) historique.append({"role": "assistant", "content": reponse}) del historique[:-20] resultat = flags.get("memoryResult") if not resultat: return contexte = session_contexts.setdefault(session_id, {}) if flags.get("memoryQuery"): contexte["last_query"] = flags["memoryQuery"] if resultat.get("clients"): contexte["last_client"] = resultat["clients"][0].get("name", "") elif flags.get("memoryQuery"): contexte["last_client"] = flags["memoryQuery"] if resultat.get("projects"): contexte["last_project"] = resultat["projects"][0].get("name", "") contexte["tasks"] = resultat.get("tasks", []) def formater_memoire(resultat): lignes = [] for client_mem in resultat["clients"][:5]: detail = f"Client : {client_mem['name']}" if client_mem.get("notes"): detail += f" - {client_mem['notes']}" lignes.append(detail) for projet in resultat["projects"][:5]: detail = f"Projet : {projet['name']}" if projet.get("client_name"): detail += f" ({projet['client_name']})" if projet.get("status"): detail += f" - statut : {projet['status']}" if projet.get("next_action"): detail += f" - prochaine action : {projet['next_action']}" lignes.append(detail) for tache in resultat["tasks"][:5]: contexte = tache.get("client_name") or tache.get("project_name") or "general" lignes.append(f"Tache : {tache['title']} ({contexte})") for repo in resultat["repositories"][:5]: chemin = repo.get("local_path") or repo.get("remote_url") or "chemin non renseigne" lignes.append(f"Depot : {repo['name']} - {chemin}") for note in resultat["memories"][:5]: sujet = note.get("subject") or note.get("client_name") or note.get("project_name") or "note" lignes.append(f"Note : {sujet} - {note['content']}") if not lignes: return "Je n'ai rien trouve dans la memoire professionnelle." return "Voici ce que j'ai trouve :\n" + "\n".join(f"- {ligne}" for ligne in lignes[:10]) def formater_liste_clients(clients_mem): if not clients_mem: return "Je n'ai encore aucun client dans la memoire professionnelle." noms = [client_mem["name"] for client_mem in clients_mem] return "Voici tes clients :\n" + "\n".join(f"- {nom}" for nom in noms) def traiter_message(session_id, texte, avec_audio=False): action_directe = detecter_action_directe(texte, session_id) reponse = json.dumps(action_directe, ensure_ascii=False) if action_directe else demander_au_llm(session_id, texte) action = extraire_action(reponse) flags = { "emailSent": False, "contactSaved": False, "memorySaved": False, "memoryRead": False, "memoryResult": None, "memoryQuery": "", } if action: reponse, flags = executer_actions(action) mettre_a_jour_contexte(session_id, texte, reponse, flags) audio_url = None if avec_audio and TTS_PROVIDER == "groq": audio_url = generer_audio(reponse) return { "reply": reponse, "audioUrl": audio_url, "emailSent": flags["emailSent"], "contactSaved": flags["contactSaved"], "memorySaved": flags["memorySaved"], "memoryRead": flags["memoryRead"], } @app.get("/") def index(): return send_from_directory(STATIC_DIR, "index.html") @app.get("/health") def health(): return jsonify({"ok": True}) @app.post("/api/chat") def chat(): data = request.get_json(force=True) texte = (data.get("text") or "").strip() session_id = data.get("sessionId") or "default" avec_audio = bool(data.get("withAudio")) if not texte: return jsonify({"error": "Message vide"}), 400 try: return jsonify(traiter_message(session_id, texte, avec_audio)) except Exception as e: print(f"Erreur /api/chat : {e}", flush=True) return jsonify({"error": str(e)}), 500 @app.post("/api/voice") def voice(): fichier = request.files.get("audio") session_id = request.form.get("sessionId") or "default" avec_audio = request.form.get("withAudio", "false").lower() == "true" if not fichier: return jsonify({"error": "Audio manquant"}), 400 try: texte = transcrire_audio(fichier).strip() resultat = traiter_message(session_id, texte, avec_audio=avec_audio) resultat["transcript"] = texte return jsonify(resultat) except Exception as e: print(f"Erreur /api/voice : {e}", flush=True) return jsonify({"error": str(e)}), 500 @app.post("/api/reset") def reset(): data = request.get_json(silent=True) or {} session_id = data.get("sessionId") or "default" histories.pop(session_id, None) session_contexts.pop(session_id, None) return jsonify({"ok": True}) if __name__ == "__main__": app.run( host="0.0.0.0", port=int(os.getenv("PORT", "8080")), debug=os.getenv("FLASK_DEBUG", "0") == "1", )