Route memory action
This commit is contained in:
240
app.py
240
app.py
@@ -16,6 +16,7 @@ from memory import (
|
||||
add_repository,
|
||||
add_task,
|
||||
close_task,
|
||||
deactivate_client,
|
||||
get_memory_snapshot,
|
||||
init_db,
|
||||
list_clients,
|
||||
@@ -65,6 +66,7 @@ Tu disposes aussi d'une memoire professionnelle persistante pour les clients, pr
|
||||
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": "supprimer_client", "nom": "..."}
|
||||
{"action": "ajouter_projet", "nom": "...", "client": "...", "statut": "...", "resume": "...", "prochaine_action": "..."}
|
||||
{"action": "ajouter_tache", "titre": "...", "client": "...", "projet": "...", "details": "...", "echeance": "..."}
|
||||
{"action": "terminer_tache", "id": 123}
|
||||
@@ -84,6 +86,50 @@ 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."""
|
||||
|
||||
ROUTER_PROMPT = """Tu es le routeur d'intention de l'assistant personnel de Laurent.
|
||||
Ton role est de comparer le message utilisateur avec la liste des actions disponibles et le contexte fourni.
|
||||
|
||||
Reponds uniquement avec un JSON valide, sans markdown.
|
||||
|
||||
Format obligatoire :
|
||||
{
|
||||
"type": "action" | "answer" | "clarification",
|
||||
"confidence": 0.0,
|
||||
"action": null | {"action": "..."} | {"actions": [{"action": "..."}]},
|
||||
"reply": "",
|
||||
"question": ""
|
||||
}
|
||||
|
||||
Actions disponibles :
|
||||
- envoyer_email: {"action":"envoyer_email","destinataire":"...","sujet":"...","corps":"..."}
|
||||
- ajouter_contact: {"action":"ajouter_contact","nom":"...","email":"..."}
|
||||
- ajouter_client: {"action":"ajouter_client","nom":"...","notes":"...","statut":"actif"}
|
||||
- supprimer_client: {"action":"supprimer_client","nom":"..."}
|
||||
- ajouter_projet: {"action":"ajouter_projet","nom":"...","client":"...","statut":"...","resume":"...","prochaine_action":"..."}
|
||||
- ajouter_tache: {"action":"ajouter_tache","titre":"...","client":"...","projet":"...","details":"...","echeance":"..."}
|
||||
- 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"}
|
||||
|
||||
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".
|
||||
- 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".
|
||||
- N'invente pas d'information absente du contexte. Si la demande porte sur une categorie inconnue, demande une precision.
|
||||
- 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.
|
||||
"""
|
||||
|
||||
|
||||
def charger_contacts():
|
||||
try:
|
||||
@@ -186,6 +232,7 @@ ACTIONS = {
|
||||
"envoyer_email",
|
||||
"ajouter_contact",
|
||||
"ajouter_client",
|
||||
"supprimer_client",
|
||||
"ajouter_projet",
|
||||
"ajouter_tache",
|
||||
"terminer_tache",
|
||||
@@ -242,6 +289,99 @@ def demander_au_llm(session_id, texte):
|
||||
return contenu
|
||||
|
||||
|
||||
def router_intention(session_id, texte):
|
||||
historique = histories.get(session_id, [])[-12:]
|
||||
messages = [
|
||||
{"role": "system", "content": ROUTER_PROMPT},
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Contexte disponible pour router la demande :\n"
|
||||
+ construire_contexte_routeur(session_id)
|
||||
),
|
||||
},
|
||||
] + historique + [{"role": "user", "content": texte}]
|
||||
|
||||
reponse = client.chat.completions.create(
|
||||
model=CHAT_MODEL,
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
max_tokens=700,
|
||||
)
|
||||
contenu = reponse.choices[0].message.content or ""
|
||||
data = extraire_json_routeur(contenu)
|
||||
if not data:
|
||||
return {
|
||||
"type": "answer",
|
||||
"confidence": 0.0,
|
||||
"action": None,
|
||||
"reply": contenu.strip() or "Je n'ai pas compris. Tu peux reformuler ?",
|
||||
"question": "",
|
||||
}
|
||||
return normaliser_decision_routeur(data)
|
||||
|
||||
|
||||
def construire_contexte_routeur(session_id=None):
|
||||
contacts = charger_contacts()
|
||||
contacts_connus = "\n".join(f"- {nom.title()} : {email}" for nom, email in contacts.items())
|
||||
return (
|
||||
"Contacts email connus :\n"
|
||||
+ (contacts_connus or "- aucun")
|
||||
+ "\n\nMemoire professionnelle actuelle :\n"
|
||||
+ construire_contexte_memoire(session_id)
|
||||
)
|
||||
|
||||
|
||||
def extraire_json_routeur(contenu):
|
||||
decodeur = json.JSONDecoder()
|
||||
for index, caractere in enumerate(contenu):
|
||||
if caractere != "{":
|
||||
continue
|
||||
try:
|
||||
data, _ = decodeur.raw_decode(contenu[index:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def normaliser_decision_routeur(data):
|
||||
type_decision = data.get("type") or "answer"
|
||||
if type_decision not in {"action", "answer", "clarification"}:
|
||||
type_decision = "answer"
|
||||
|
||||
try:
|
||||
confidence = float(data.get("confidence", 0))
|
||||
except (TypeError, ValueError):
|
||||
confidence = 0.0
|
||||
confidence = max(0.0, min(1.0, confidence))
|
||||
|
||||
action = data.get("action")
|
||||
if isinstance(action, dict) and not action_valide(action):
|
||||
action = None
|
||||
|
||||
if type_decision == "action" and (confidence < 0.80 or not action):
|
||||
type_decision = "clarification"
|
||||
|
||||
return {
|
||||
"type": type_decision,
|
||||
"confidence": confidence,
|
||||
"action": action,
|
||||
"reply": str(data.get("reply") or "").strip(),
|
||||
"question": str(data.get("question") or "").strip(),
|
||||
}
|
||||
|
||||
|
||||
def action_valide(action):
|
||||
if action.get("action") in ACTIONS:
|
||||
return True
|
||||
actions = action.get("actions")
|
||||
if isinstance(actions, list) and actions:
|
||||
return all(isinstance(item, dict) and item.get("action") in ACTIONS for item in actions)
|
||||
return False
|
||||
|
||||
|
||||
def detecter_action_directe(texte, session_id=None):
|
||||
texte_nettoye = texte.strip()
|
||||
texte_min = texte_nettoye.lower()
|
||||
@@ -260,6 +400,10 @@ def detecter_action_directe(texte, session_id=None):
|
||||
]
|
||||
}
|
||||
|
||||
suppression_client = detecter_suppression_client(texte_nettoye)
|
||||
if suppression_client:
|
||||
return suppression_client
|
||||
|
||||
analyse_taches = detecter_analyse_taches(texte_nettoye)
|
||||
if analyse_taches:
|
||||
return analyse_taches
|
||||
@@ -369,6 +513,26 @@ def detecter_tache_client(texte):
|
||||
}
|
||||
|
||||
|
||||
def detecter_suppression_client(texte):
|
||||
if not re.search(r"\b(enleve|enlève|supprime|retire|efface|desactive|désactive)\b", texte, flags=re.IGNORECASE):
|
||||
return None
|
||||
if re.search(r"\btaches?\b|\bprojets?\b|\brepos?\b|\bdepots?\b|\bdépôts?\b", texte, flags=re.IGNORECASE):
|
||||
return None
|
||||
|
||||
match = re.search(
|
||||
r"\b(?:enleve|enlève|supprime|retire|efface|desactive|désactive)\s+(?:le\s+client\s+|client\s+)?(.+?)(?:\s+(?:de|dans)\s+(?:la\s+)?liste.*)?$",
|
||||
texte,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
nom = nettoyer_nom_client(match.group(1))
|
||||
if not nom:
|
||||
return None
|
||||
return {"action": "supprimer_client", "nom": nom}
|
||||
|
||||
|
||||
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",
|
||||
@@ -496,6 +660,12 @@ def separer_noms_clients(texte):
|
||||
|
||||
def nettoyer_nom_client(nom):
|
||||
nom = nom.strip(" -\t\r.,;:")
|
||||
nom = re.sub(
|
||||
r"\s+\b(je|j'ai|j ai|stp|svp|s'il te plait|s'il te plaît|merci)\b.*$",
|
||||
"",
|
||||
nom,
|
||||
flags=re.IGNORECASE,
|
||||
).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):
|
||||
@@ -580,6 +750,13 @@ def executer_action(action):
|
||||
)
|
||||
return f"C'est note : {client_mem['name']} est dans tes clients.", {"memorySaved": True}
|
||||
|
||||
if nom_action == "supprimer_client":
|
||||
client_mem = deactivate_client(action.get("nom", ""))
|
||||
nom = action.get("nom", "").strip()
|
||||
if not client_mem:
|
||||
return f"Je n'ai pas trouve le client {nom} dans ta liste.", {"memoryRead": True}
|
||||
return f"C'est fait : {client_mem['name']} n'apparaitra plus dans tes clients actifs.", {"memorySaved": True}
|
||||
|
||||
if nom_action == "ajouter_projet":
|
||||
projet = upsert_project(
|
||||
action.get("nom", ""),
|
||||
@@ -695,11 +872,7 @@ def executer_actions(action_data):
|
||||
|
||||
|
||||
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]
|
||||
ajouter_echange_historique(session_id, texte, reponse)
|
||||
|
||||
resultat = flags.get("memoryResult")
|
||||
if not resultat:
|
||||
@@ -717,6 +890,14 @@ def mettre_a_jour_contexte(session_id, texte, reponse, flags):
|
||||
contexte["tasks"] = resultat.get("tasks", [])
|
||||
|
||||
|
||||
def ajouter_echange_historique(session_id, texte, reponse):
|
||||
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]
|
||||
|
||||
|
||||
def formater_memoire(resultat):
|
||||
lignes = []
|
||||
for client_mem in resultat["clients"][:5]:
|
||||
@@ -787,9 +968,21 @@ def formater_client_plus_taches(snapshot):
|
||||
|
||||
|
||||
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)
|
||||
action_confirmee = recuperer_action_confirmee(session_id, texte)
|
||||
if action_confirmee:
|
||||
reponse, flags = executer_actions(action_confirmee)
|
||||
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,
|
||||
"contactSaved": False,
|
||||
@@ -799,9 +992,20 @@ def traiter_message(session_id, texte, avec_audio=False):
|
||||
"memoryQuery": "",
|
||||
}
|
||||
|
||||
if action:
|
||||
reponse, flags = executer_actions(action)
|
||||
if decision["type"] == "action":
|
||||
reponse, flags = executer_actions(decision["action"])
|
||||
mettre_a_jour_contexte(session_id, texte, reponse, flags)
|
||||
elif decision["type"] == "clarification":
|
||||
reponse = decision["question"] or decision["reply"] or "Tu peux préciser ce que tu veux que je fasse ?"
|
||||
if decision.get("action"):
|
||||
session_contexts.setdefault(session_id, {})["pending_action"] = decision["action"]
|
||||
ajouter_echange_historique(session_id, texte, reponse)
|
||||
else:
|
||||
reponse = decision["reply"]
|
||||
if not reponse:
|
||||
reponse = demander_au_llm(session_id, texte)
|
||||
else:
|
||||
ajouter_echange_historique(session_id, texte, reponse)
|
||||
|
||||
audio_url = None
|
||||
if avec_audio and TTS_PROVIDER == "groq":
|
||||
@@ -817,6 +1021,22 @@ def traiter_message(session_id, texte, avec_audio=False):
|
||||
}
|
||||
|
||||
|
||||
def recuperer_action_confirmee(session_id, texte):
|
||||
contexte = session_contexts.get(session_id, {})
|
||||
pending_action = contexte.get("pending_action")
|
||||
if not pending_action:
|
||||
return None
|
||||
|
||||
texte_min = texte.strip().lower()
|
||||
if re.fullmatch(r"(oui|yes|ok|d'accord|vas-y|confirme|c'est ca|c'est ça|exact|tout a fait|tout à fait)[\s.!?]*", texte_min):
|
||||
contexte.pop("pending_action", None)
|
||||
return pending_action
|
||||
if re.fullmatch(r"(non|no|annule|stop|laisse tomber|pas ca|pas ça)[\s.!?]*", texte_min):
|
||||
contexte.pop("pending_action", None)
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return send_from_directory(STATIC_DIR, "index.html")
|
||||
|
||||
28
memory.py
28
memory.py
@@ -132,6 +132,26 @@ def upsert_client(name, notes="", status="actif"):
|
||||
return row_to_dict(db.execute("SELECT * FROM clients WHERE id = ?", (client_id,)).fetchone())
|
||||
|
||||
|
||||
def deactivate_client(name):
|
||||
name = clean_text(name)
|
||||
if not name:
|
||||
raise ValueError("Nom de client manquant")
|
||||
|
||||
with connect() as db:
|
||||
row = find_client(db, name)
|
||||
if not row:
|
||||
return None
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE clients
|
||||
SET status = 'inactif', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""",
|
||||
(row["id"],),
|
||||
)
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def upsert_project(name, client_name="", status="actif", summary="", next_action=""):
|
||||
name = clean_text(name)
|
||||
if not name:
|
||||
@@ -198,7 +218,12 @@ def add_task(title, client_name="", project_name="", details="", due_at=""):
|
||||
def list_clients(limit=100):
|
||||
with connect() as db:
|
||||
rows = db.execute(
|
||||
"SELECT * FROM clients ORDER BY name COLLATE NOCASE LIMIT ?",
|
||||
"""
|
||||
SELECT * FROM clients
|
||||
WHERE status != 'inactif'
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [row_to_dict(row) for row in rows]
|
||||
@@ -212,6 +237,7 @@ def get_memory_snapshot(limit=25):
|
||||
COUNT(t.id) AS open_task_count
|
||||
FROM clients c
|
||||
LEFT JOIN tasks t ON t.client_id = c.id AND t.done = 0
|
||||
WHERE c.status != 'inactif'
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name COLLATE NOCASE
|
||||
LIMIT ?
|
||||
|
||||
Reference in New Issue
Block a user