881 lines
30 KiB
Python
881 lines
30 KiB
Python
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"}
|
|
{"action": "analyser_taches", "type": "client_plus_taches"}
|
|
|
|
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",
|
|
"analyser_taches",
|
|
}
|
|
|
|
|
|
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
|
|
]
|
|
}
|
|
|
|
analyse_taches = detecter_analyse_taches(texte_nettoye)
|
|
if analyse_taches:
|
|
return analyse_taches
|
|
|
|
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):
|
|
if re.search(
|
|
r"\b(pour\s+quel\s+client|quel\s+client|plus\s+de\s+taches?|plus\s+de\s+tâches?|combien|classement|top)\b",
|
|
texte,
|
|
flags=re.IGNORECASE,
|
|
):
|
|
return None
|
|
|
|
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_analyse_taches(texte):
|
|
if not re.search(r"\bclients?\b", texte, flags=re.IGNORECASE):
|
|
return None
|
|
if not re.search(r"\b(taches?|tâches?|a faire|à faire|reste)\b", texte, flags=re.IGNORECASE):
|
|
return None
|
|
if re.search(
|
|
r"\b(plus\s+de|le\s+plus|maximum|max|prioritaire|charge|chargé|chargee|chargée)\b",
|
|
texte,
|
|
flags=re.IGNORECASE,
|
|
):
|
|
return {"action": "analyser_taches", "type": "client_plus_taches"}
|
|
return None
|
|
|
|
|
|
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": "",
|
|
}
|
|
|
|
if nom_action == "analyser_taches":
|
|
if action.get("type") == "client_plus_taches":
|
|
snapshot = get_memory_snapshot(limit=100)
|
|
return formater_client_plus_taches(snapshot), {
|
|
"memoryRead": True,
|
|
"memoryResult": {
|
|
"clients": snapshot["clients"],
|
|
"projects": snapshot["projects"],
|
|
"tasks": snapshot["tasks"],
|
|
"repositories": snapshot["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 formater_client_plus_taches(snapshot):
|
|
clients_avec_taches = [
|
|
client_mem for client_mem in snapshot["clients"]
|
|
if int(client_mem.get("open_task_count") or 0) > 0
|
|
]
|
|
if not clients_avec_taches:
|
|
return "Tu n'as aucune tache ouverte dans la memoire professionnelle."
|
|
|
|
max_count = max(int(client_mem["open_task_count"]) for client_mem in clients_avec_taches)
|
|
premiers = [
|
|
client_mem["name"] for client_mem in clients_avec_taches
|
|
if int(client_mem["open_task_count"]) == max_count
|
|
]
|
|
lignes = [
|
|
f"{client_mem['name']} : {client_mem['open_task_count']} tache(s)"
|
|
for client_mem in sorted(
|
|
clients_avec_taches,
|
|
key=lambda item: (-int(item["open_task_count"]), item["name"].lower()),
|
|
)
|
|
]
|
|
if len(premiers) == 1:
|
|
intro = f"Le client avec le plus de taches ouvertes est {premiers[0]} avec {max_count} tache(s)."
|
|
else:
|
|
intro = f"Les clients avec le plus de taches ouvertes sont {', '.join(premiers)} avec {max_count} tache(s) chacun."
|
|
return intro + "\n" + "\n".join(f"- {ligne}" for ligne in lignes)
|
|
|
|
|
|
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",
|
|
)
|