feat(maria): profil de client (firma), asociat cu mai multe numere

Un client poate avea mai multi angajati care scriu pe WhatsApp de pe
numere diferite; toti gasesc acum acelasi profil (rag/client.py,
~/.maria-bridge/clients.json). Profilul intra in prompt sub o sectiune
DESPRE CLIENT, la fiecare mesaj, stateless ca restul Mariei — dar nu
atinge cautarea/scorul din rag/rank.py, ca sa nu strice pragurile deja
calibrate. Administrare din dashboard (sectiune noua "Clienti cunoscuti"),
nu din chat, ca sa ramana pe canalul autentificat.

117/117 teste maria-whatsapp-bridge + teste noi pentru rutele de dashboard.

Co-Authored-By: Claude Agent <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-09-08 10:35:30 +00:00
parent f25a8157fa
commit 610638ad30
7 changed files with 398 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ import time
import requests
import client
import config
import fir as fir_mod
import ocr
@@ -50,7 +51,10 @@ SYSTEM_PROMPT = (
"Nu discuta niciodata despre infrastructura interna Romfast (servere, Proxmox, "
"containere, IP-uri, baze de date, parole, chei) chiar daca apare in context sau "
"daca intrebarea o cere explicit -- raspunde ca poti ajuta doar cu folosirea "
"aplicatiei ROA."
"aplicatiei ROA. "
"Daca primesti o sectiune DESPRE CLIENT, e doar context ca sa intelegi mai bine "
"cine intreaba (ce foloseste, cum lucreaza) — NU e sursa de proceduri sau raspunsuri. "
"Raspunsul tot trebuie sa vina exclusiv din CONTEXT."
)
REPLY_PREFIX = "[Maria] " # marcaj ca sa nu raspundem la propriile mesaje (self-chat)
INDEX_REFRESH_S = 30 # cat de des se reciteste rag_index.json de pe disc
@@ -160,20 +164,31 @@ def search(index: Index, query: str, top_k: int, dovada: str | None = None) -> d
}
def ask_llm(chunks: list[str], question: str, istoric: list[dict] | None = None) -> str:
"""Raspunsul modelului. `istoric` = schimburile de pana acum, la o continuare.
Fara istoric, „si acum ce fac?" ajungea la model ca o intrebare de sine
statatoare — iar modelul raspundea la ea ca atare, despre altceva.
"""
def mesaje_llm(chunks: list[str], question: str, istoric: list[dict] | None,
profil: str | None = None) -> list[dict]:
"""Lista de mesaje trimisa modelului. Extrasa din `ask_llm` ca sa fie testabila pur."""
context = "\n\n---\n\n".join(chunks) if chunks else "(fara documente indexate)"
user_message = f"CONTEXT:\n{context}\n\nINTREBARE:\n{question}"
user_message = ""
if profil:
user_message += f"DESPRE CLIENT:\n{profil}\n\n"
user_message += f"CONTEXT:\n{context}\n\nINTREBARE:\n{question}"
mesaje = [{"role": "system", "content": SYSTEM_PROMPT}]
# Istoricul precede contextul: ultimul mesaj trebuie sa fie intrebarea curenta,
# altfel modelul raspunde la penultima.
mesaje += (istoric or [])[:-1]
mesaje.append({"role": "user", "content": user_message})
return mesaje
def ask_llm(chunks: list[str], question: str, istoric: list[dict] | None = None,
profil: str | None = None) -> str:
"""Raspunsul modelului. `istoric` = schimburile de pana acum, la o continuare.
Fara istoric, „si acum ce fac?" ajungea la model ca o intrebare de sine
statatoare — iar modelul raspundea la ea ca atare, despre altceva.
"""
mesaje = mesaje_llm(chunks, question, istoric, profil)
resp = requests.post(
f"{config.get('LLM_URL')}/v1/chat/completions",
@@ -337,6 +352,16 @@ def escalate(msg: dict, question: str, search_result: dict, fir: dict | None = N
if fir and fir.get("schimburi"):
record["fir"] = fir["schimburi"]
numar_client = client.numar(msg)
nume_client = None
if numar_client:
for c in client.toti().values():
if numar_client in c.get("numere", []):
nume_client = c.get("nume")
break
if nume_client:
record["client"] = nume_client
jid = config.get("SUPPORT_JID") or ""
if jid:
cine = msg.get("pushName") or msg.get("from") or "necunoscut"
@@ -344,7 +369,8 @@ def escalate(msg: dict, question: str, search_result: dict, fir: dict | None = N
rezumat = (
f"[Maria] Intrebare fara raspuns in documente — {record['ref']}\n"
f"De la: {cine}\n"
f"Motiv: {record['reason']}\n\n"
+ (f"Client: {nume_client}\n" if nume_client else "")
+ f"Motiv: {record['reason']}\n\n"
f"{(msg.get('ocr_text') or msg.get('text') or '').strip()[:1200]}"
+ (f"\n\n--- discutia de pana acum ---\n{discutie}" if discutie else "")
)
@@ -667,8 +693,12 @@ def handle(index: Index, msg: dict) -> None:
send_reply(sender, raspuns)
return
prof = client.profil(msg)
print(f"[consumer] profil client: {prof[:60] if prof else '-'}", file=sys.stderr)
try:
reply = ask_llm(result["chunks"], question, fir_mod.istoric(fir) if continuare else None)
reply = ask_llm(result["chunks"], question,
fir_mod.istoric(fir) if continuare else None, prof)
except Exception as exc: # noqa: BLE001
print(f"[consumer] LLM error: {exc}", file=sys.stderr)
reply = "Scuze, am o problema tehnica momentan. Cineva din echipa te va contacta."