notes memory

This commit is contained in:
2026-04-27 15:42:17 +02:00
parent 94e14a3674
commit 410f1acd5d
9 changed files with 755 additions and 20 deletions

265
app.py
View File

@@ -1,6 +1,7 @@
import base64
import json
import os
import re
import smtplib
import tempfile
from email.mime.multipart import MIMEMultipart
@@ -10,6 +11,15 @@ 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,
init_db,
search_memory,
upsert_client,
upsert_project,
)
load_dotenv()
@@ -29,6 +39,7 @@ 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 = {}
@@ -46,6 +57,21 @@ Quand l'utilisateur veut ajouter, memoriser ou modifier un contact email, collec
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": "ajouter_repo", "nom": "...", "projet": "...", "chemin": "...", "url": "...", "branche": "..."}
{"action": "ajouter_note", "contenu": "...", "sujet": "...", "client": "...", "projet": "...", "type": "note"}
{"action": "chercher_memoire", "requete": "..."}
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.
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."""
@@ -87,11 +113,23 @@ def extraire_action(reponse):
data, _ = decodeur.raw_decode(reponse[index:])
except json.JSONDecodeError:
continue
if isinstance(data, dict) and data.get("action") in {"envoyer_email", "ajouter_contact"}:
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",
"ajouter_repo",
"ajouter_note",
"chercher_memoire",
}
def ajouter_contact(nom, email):
contacts = charger_contacts()
nom_normalise = " ".join(nom.strip().lower().split())
@@ -137,6 +175,78 @@ def demander_au_llm(session_id, texte):
return contenu
def detecter_action_directe(texte):
texte_nettoye = texte.strip()
texte_min = texte_nettoye.lower()
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 re.search(r"\b(liste|affiche|montre)\b.*\bclients?\b", texte_min):
return {"action": "chercher_memoire", "requete": ""}
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 transcrire_audio(fichier):
suffix = Path(fichier.filename or "voice.webm").suffix or ".webm"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
@@ -186,20 +296,145 @@ def generer_audio(texte):
pass
def traiter_message(session_id, texte, avec_audio=False):
reponse = demander_au_llm(session_id, texte)
action = extraire_action(reponse)
email_sent = False
contact_saved = False
def executer_action(action):
nom_action = action.get("action")
if action and action.get("action") == "envoyer_email":
if nom_action == "envoyer_email":
destinataire = envoyer_email(action["destinataire"], action["sujet"], action["corps"])
reponse = f"Email envoye a {destinataire}."
email_sent = True
elif action and action.get("action") == "ajouter_contact":
return f"Email envoye a {destinataire}.", {"emailSent": True}
if nom_action == "ajouter_contact":
nom, email = ajouter_contact(action["nom"], action["email"])
reponse = f"Contact ajoute : {nom.title()}."
contact_saved = True
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 == "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}
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,
}
for action in actions:
if not isinstance(action, dict):
continue
reponse, action_flags = executer_action(action)
if reponse:
reponses.append(reponse)
for cle in flags:
flags[cle] = flags[cle] or bool(action_flags.get(cle))
return " ".join(reponses), flags
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 traiter_message(session_id, texte, avec_audio=False):
action_directe = detecter_action_directe(texte)
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,
}
if action:
reponse, flags = executer_actions(action)
audio_url = None
if avec_audio and TTS_PROVIDER == "groq":
@@ -208,8 +443,10 @@ def traiter_message(session_id, texte, avec_audio=False):
return {
"reply": reponse,
"audioUrl": audio_url,
"emailSent": email_sent,
"contactSaved": contact_saved,
"emailSent": flags["emailSent"],
"contactSaved": flags["contactSaved"],
"memorySaved": flags["memorySaved"],
"memoryRead": flags["memoryRead"],
}