SQL router

This commit is contained in:
2026-04-27 16:51:16 +02:00
parent 881c49c0c4
commit dfdc8d26bb
2 changed files with 196 additions and 23 deletions

141
app.py
View File

@@ -17,9 +17,11 @@ from memory import (
add_task,
close_task,
deactivate_client,
execute_read_query,
get_memory_snapshot,
init_db,
list_clients,
list_open_tasks,
search_memory,
upsert_client,
upsert_project,
@@ -72,9 +74,7 @@ Actions disponibles :
{"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"}
{"action": "interroger_base", "sql": "SELECT ...", "question": "..."}
Si une phrase contient plusieurs choses a enregistrer, utilise :
{"actions": [{...}, {...}]}
@@ -110,16 +110,30 @@ Actions disponibles :
- terminer_tache: {"action":"terminer_tache","id":123}
- ajouter_repo: {"action":"ajouter_repo","nom":"...","projet":"...","chemin":"...","url":"...","branche":"..."}
- ajouter_note: {"action":"ajouter_note","contenu":"...","sujet":"...","client":"...","projet":"...","type":"note"}
- chercher_memoire: {"action":"chercher_memoire","requete":"..."}
- lister_clients: {"action":"lister_clients"}
- analyser_taches: {"action":"analyser_taches","type":"client_plus_taches"}
- interroger_base: {"action":"interroger_base","sql":"SELECT ...","question":"..."}
Schema SQLite disponible pour interroger_base :
- clients(id, name, status, notes, created_at, updated_at)
- projects(id, client_id, name, status, summary, next_action, created_at, updated_at)
- tasks(id, client_id, project_id, title, details, due_at, done, created_at, updated_at)
- repositories(id, project_id, name, local_path, remote_url, main_branch, last_indexed_at, created_at, updated_at)
- memories(id, client_id, project_id, kind, subject, content, created_at)
Relations :
- projects.client_id -> clients.id
- tasks.client_id -> clients.id
- tasks.project_id -> projects.id
- repositories.project_id -> projects.id
- memories.client_id -> clients.id
- memories.project_id -> projects.id
Regles :
- Choisis une action seulement si l'intention et les arguments sont clairs.
- Si plusieurs actions sont necessaires, utilise {"actions":[...]}.
- Si l'utilisateur demande une modification mais que la cible est ambigue, type="clarification".
- Si l'utilisateur pose une question generale et que le contexte suffit, type="answer" et reponds directement.
- Si une action de lecture convient mieux qu'une reponse libre, type="action".
- Pour toute question de lecture ou d'analyse sur la memoire professionnelle, prefere interroger_base avec un SELECT.
- Pour interroger_base, SQL doit etre une seule requete SELECT, sans point-virgule, sans PRAGMA, sans ecriture, avec LIMIT si le resultat peut etre long.
- Si aucune action ne correspond, type="answer".
- confidence >= 0.80 pour executer une action sans confirmation.
- confidence entre 0.45 et 0.79 : type="clarification".
@@ -127,9 +141,10 @@ Regles :
- Pour "nouveaux clients", ne suppose pas que tous les clients sont nouveaux. Demande si Laurent veut les derniers ajoutes, tous les clients, ou une notion precise de nouveau.
- Pour supprimer/terminer une tache, utilise l'id si le contexte de conversation contient les dernieres taches affichees.
- Pour retirer/enlever/supprimer un client de la liste, utilise supprimer_client.
- Pour "quel client a le plus de taches", utilise analyser_taches.
- Pour "quel client a le plus de taches", utilise interroger_base avec COUNT, GROUP BY, ORDER BY.
- Pour "quelles sont mes taches", "mes taches a faire", "taches pour mes clients", utilise interroger_base sur tasks avec clients/projects.
- Si l'utilisateur dit "lui", "ce client", "ce projet" ou "cette tache", utilise le contexte de conversation en cours.
- Si l'utilisateur demande "que dois-je faire pour lui ?", "c'est quoi comme tache ?" ou "quelle est la tache ?", utilise chercher_memoire avec le dernier client/projet connu.
- Si l'utilisateur demande "que dois-je faire pour lui ?", "c'est quoi comme tache ?" ou "quelle est la tache ?", utilise interroger_base avec le dernier client/projet connu.
"""
@@ -242,7 +257,9 @@ ACTIONS = {
"ajouter_note",
"chercher_memoire",
"lister_clients",
"lister_taches",
"analyser_taches",
"interroger_base",
}
@@ -819,6 +836,14 @@ def executer_action(action):
"memoryQuery": "",
}
if nom_action == "lister_taches":
taches = list_open_tasks()
return formater_liste_taches(taches), {
"memoryRead": True,
"memoryResult": {"clients": [], "projects": [], "tasks": taches, "repositories": [], "memories": []},
"memoryQuery": "",
}
if nom_action == "analyser_taches":
if action.get("type") == "client_plus_taches":
snapshot = get_memory_snapshot(limit=100)
@@ -836,6 +861,17 @@ def executer_action(action):
"memoryQuery": "",
}
if nom_action == "interroger_base":
sql = action.get("sql", "")
question = action.get("question") or action.get("requete") or ""
rows = execute_read_query(sql)
reponse = resumer_resultat_sql(question, sql, rows)
return reponse, {
"memoryRead": True,
"memoryResult": {"clients": [], "projects": [], "tasks": rows, "repositories": [], "memories": []},
"memoryQuery": question,
}
return "", {}
@@ -889,8 +925,12 @@ def mettre_a_jour_contexte(session_id, texte, reponse, flags):
contexte["last_client"] = resultat["clients"][0].get("name", "")
elif flags.get("memoryQuery"):
contexte["last_client"] = flags["memoryQuery"]
elif resultat.get("tasks") and resultat["tasks"][0].get("client_name"):
contexte["last_client"] = resultat["tasks"][0].get("client_name", "")
if resultat.get("projects"):
contexte["last_project"] = resultat["projects"][0].get("name", "")
elif resultat.get("tasks") and resultat["tasks"][0].get("project_name"):
contexte["last_project"] = resultat["tasks"][0].get("project_name", "")
contexte["tasks"] = resultat.get("tasks", [])
@@ -944,6 +984,64 @@ def formater_liste_clients(clients_mem):
return "Voici tes clients :\n" + "\n".join(f"- {nom}" for nom in noms)
def formater_liste_taches(taches):
if not taches:
return "Tu n'as aucune tache ouverte dans la memoire professionnelle."
groupes = {}
for tache in taches:
cle = tache.get("client_name") or tache.get("project_name") or "General"
groupes.setdefault(cle, []).append(tache)
lignes = ["Voici tes taches ouvertes :"]
for sujet, items in groupes.items():
lignes.append(f"- {sujet} :")
for tache in items:
lignes.append(f" - #{tache['id']} {tache['title']}")
return "\n".join(lignes)
def resumer_resultat_sql(question, sql, rows):
if not rows:
return "Je n'ai rien trouve dans la memoire professionnelle."
if not GROQ_API_KEY:
return formater_lignes_sql(rows)
payload = json.dumps(rows[:50], ensure_ascii=False, indent=2)
messages = [
{
"role": "system",
"content": (
"Tu transformes un resultat SQL en reponse courte en francais pour Laurent. "
"Ne mentionne pas SQL. Ne rajoute rien qui n'est pas dans les lignes."
),
},
{
"role": "user",
"content": (
f"Question utilisateur: {question or 'question non fournie'}\n"
f"Requete executee: {sql}\n"
f"Resultats JSON:\n{payload}"
),
},
]
reponse = client.chat.completions.create(
model=CHAT_MODEL,
messages=messages,
temperature=0.2,
max_tokens=500,
)
return (reponse.choices[0].message.content or "").strip() or formater_lignes_sql(rows)
def formater_lignes_sql(rows):
lignes = ["Voici ce que j'ai trouve :"]
for row in rows[:20]:
valeurs = [f"{cle}: {valeur}" for cle, valeur in row.items()]
lignes.append("- " + ", ".join(valeurs))
return "\n".join(lignes)
def formater_client_plus_taches(snapshot):
clients_avec_taches = [
client_mem for client_mem in snapshot["clients"]
@@ -986,20 +1084,6 @@ def traiter_message(session_id, texte, avec_audio=False):
"memoryRead": flags["memoryRead"],
}
action_suivi = detecter_suivi_contexte(session_id, texte)
if action_suivi:
reponse, flags = executer_actions(action_suivi)
mettre_a_jour_contexte(session_id, texte, reponse, flags)
audio_url = generer_audio(reponse) if avec_audio and TTS_PROVIDER == "groq" else None
return {
"reply": reponse,
"audioUrl": audio_url,
"emailSent": flags["emailSent"],
"contactSaved": flags["contactSaved"],
"memorySaved": flags["memorySaved"],
"memoryRead": flags["memoryRead"],
}
decision = router_intention(session_id, texte)
flags = {
"emailSent": False,
@@ -1039,6 +1123,17 @@ def traiter_message(session_id, texte, avec_audio=False):
}
def detecter_lecture_simple(texte):
texte_min = texte.strip().lower()
if re.search(r"\b(taches?|tâches?)\b", texte_min) and re.search(r"\b(quelles?|liste|affiche|montre|mes|a faire|à faire|ouvertes?)\b", texte_min):
if not re.search(r"\b(ajoute|note|notes|supprime|enleve|enlève|retire|termine|marque)\b", texte_min):
return {"action": "lister_taches"}
if re.search(r"\bclients?\b", texte_min) and re.search(r"\b(qui|quels?|liste|affiche|montre|mes|tous)\b", texte_min):
if not re.search(r"\b(ajoute|note|notes|supprime|enleve|enlève|retire|desactive|désactive|nouveaux?)\b", texte_min):
return {"action": "lister_clients"}
return None
def detecter_suivi_contexte(session_id, texte):
contexte = session_contexts.get(session_id, {})
dernier_client = contexte.get("last_client")