fix(maria): mesajul si captura sunt acelasi lucru, iar cosinusul singur nu mai e dovada

Doua greseli din grupul "Maria Test", 2026-09-01, pe care 27409fb le-a atenuat dar
nu le-a rezolvat.

1. La o reclamatie vaga Maria cauta, in loc sa intrebe. "Buna . Am si eu o factura
   pe luna august cu eroare in spv la trimitere AUTO SULE" nu spune CE eroare e —
   n-are ce cauta in documente si n-are ce trimite la suport, fiindca programatorul
   ar pune exact aceeasi intrebare. Gardul (triaj.prea_vag) exista, dar cerea text
   sub 12 cuvinte; mesajul are 14. Gresea si invers: "nu pot incarca factura in SPV,
   imi da eroare de certificat" are 9 cuvinte si ARE raspuns in documente, si era
   oprita degeaba. Lungimea nu masoara cat de precis e mesajul.
   -> gardul nu mai numara cuvinte si se aplica DUPA cautare, doar cand nu exista
      acoperire. O singura data pe fir: daca nici intrebat omul nu spune mai mult,
      mesajul pleaca la suport.

2. Textul si captura, trimise una dupa alta, erau tratate ca doua probleme fara
   legatura. `este_continuare` rupea firul la ORICE imagine — dar cazul frecvent e
   tocmai omul care scrie problema si trimite captura imediat dupa, sau care
   raspunde la "trimite-mi o captura". Textul se pierdea, captura se cauta doar pe
   OCR-ul ei, se deschideau doua fire si se puteau deschide doua escaladari pentru
   aceeasi problema.
   -> o captura in primele FIR_IMAGINE_MIN (5) minute continua firul: textul citit
      din ea intra in ancora (cu tot cu coduri), cautarea se face pe mesaj +
      captura, iar pe o escaladare deschisa pleaca la aceeasi referinta — cu
      imaginea, nu doar cu textul citit din ea (_notifica_suport ia si media).

Si, ca urmare a lui (2): RANK_STRONG_COSINE dispare. Interogarea pe un fir e ancora
plus mesajul nou, deci cosinusul urca la fiecare replica fara sa apara vreo dovada
noua — aceeasi captura da 0,778 singura si 0,836 cu mesajul de dinainte, adica peste
0,80 pus ieri. Orice prag fix de sus e trecut de o discutie destul de lunga. Acoperirea
ramane pe dovada lexicala (termenii distinctivi ai intrebarii chiar in chunk-uri),
care e stabila la lungime. Niciun caz cu raspuns in documente nu avea nevoie de
scurtatura.

ops/calibrate-rank.py: 26/26, cu interogarea combinata adaugata la set. Teste: 102 pass.
Verificat end-to-end pe scenariul real (mesaj, apoi captura la 10 secunde): intrebare
de detalii, apoi o singura escaladare care poarta si textul si captura.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
Claude Agent
2026-09-01 19:17:09 +00:00
parent 45b36591c9
commit 47110d71bf
11 changed files with 192 additions and 94 deletions

View File

@@ -357,12 +357,16 @@ def _fisier_escaladare(ref: str):
return next(iter(sorted(d.glob(f"*-{ref}.json"))), None)
def _notifica_suport(text: str) -> bool:
def _notifica_suport(text: str, imagine: str | None = None) -> bool:
"""Mesaj catre suport; cu `imagine`, chiar captura primita, nu doar textul citit."""
jid = config.get("SUPPORT_JID") or ""
if not jid:
return False
try:
requests.post(f"{bridge_url()}/send", json={"to": jid, "text": text}, timeout=20)
if imagine and os.path.exists(imagine):
send_image(jid, imagine, text[:1000])
else:
requests.post(f"{bridge_url()}/send", json={"to": jid, "text": text}, timeout=20)
return True
except Exception as exc: # noqa: BLE001
print(f"[consumer] mesaj catre suport netrimis: {exc}", file=sys.stderr)
@@ -416,7 +420,8 @@ def completeaza(ref: str, msg: dict, text: str) -> str | None:
config.get("SUPPORT_PHONE", ""),
a_cata=intrebari - 1)
else:
_notifica_suport(f"[Maria] Completare la {ref} (de la {cine}):\n{text[:800]}")
_notifica_suport(f"[Maria] Completare la {ref} (de la {cine}):\n{text[:800]}",
imagine=(msg.get("media") or {}).get("path"))
raspuns = triaj.confirmare(ref, len(completari) - 1)
try:
@@ -517,22 +522,11 @@ def handle(index: Index, msg: dict) -> None:
react_seen(sender, msg.get("id"), msg.get("fromMe", False))
continuare = fir_mod.este_continuare(fir, text, has_image)
# „Am o eroare", fara sa spuna care: nu ghicim si nu deranjam suportul — intrebam.
# Doar la deschiderea firului: la o continuare, detaliile au fost deja cerute.
if not continuare and triaj.prea_vag(text, has_image):
print("[consumer] mesaj prea vag -> cer detalii", file=sys.stderr)
fir = fir_mod.deschide(sender, text)
fir_mod.adauga(fir, "client", text)
fir_mod.adauga(fir, "maria", triaj.CERE_DETALII)
fir_mod.salveaza(fir)
send_reply(sender, triaj.CERE_DETALII)
return
# Firul are o escaladare deschisa: mesajul e completare la ea, nu intrebare
# noua. Inaintea confirmarii si a cautarii — raspunsul vine instant, deci un
# „caut informatia, revin imediat" ar fi o promisiune inutila, iar embedding-ul
# s-ar calcula degeaba.
if continuare and fir.get("ref") and text.strip():
if continuare and fir.get("ref") and text.strip() and not has_image:
raspuns = completeaza(fir["ref"], msg, text.strip())
if raspuns:
fir_mod.adauga(fir, "client", text)
@@ -552,7 +546,18 @@ def handle(index: Index, msg: dict) -> None:
return
if continuare:
fir_mod.adauga(fir, "client", text)
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
fir_mod.extinde_ancora(fir, msg.get("ocr_text") or "")
# Captura pe o escaladare deschisa e chiar detaliul care lipsea, nu o
# problema noua: pleaca la aceeasi referinta, cu tot cu imagine.
if fir.get("ref") and has_image:
raspuns = completeaza(fir["ref"], msg, msg.get("ocr_text") or text)
if raspuns:
fir_mod.adauga(fir, "maria", raspuns)
fir_mod.salveaza(fir)
send_reply(sender, raspuns)
return
fir["ref"] = None # escaladarea nu mai exista; tratam mesajul normal
search_query = fir_mod.interogare(fir, search_query)
print(f"[consumer] continuare pe firul deschis; caut dupa {search_query[:90]!r}",
file=sys.stderr)
@@ -571,6 +576,17 @@ def handle(index: Index, msg: dict) -> None:
print(f"[consumer] rank: {surse} | {result['reason']}", file=sys.stderr)
if not result["covered"]:
# „Am o eroare la o factura", fara sa spuna care: nu avem ce cauta si nici
# ce trimite la suport — programatorul ar pune exact aceeasi intrebare.
# O singura data pe fir: daca nici dupa ce a fost intrebat nu spune mai
# mult, mesajul pleaca la om, care poate insista altfel.
if triaj.prea_vag(text, has_image) and not fir.get("detalii_cerute"):
fir["detalii_cerute"] = True
fir_mod.adauga(fir, "maria", triaj.CERE_DETALII)
fir_mod.salveaza(fir)
print("[consumer] mesaj prea vag -> cer detalii", file=sys.stderr)
send_reply(sender, triaj.CERE_DETALII)
return
print(f"[consumer] fara acoperire in documente -> suport ({result['reason']})", file=sys.stderr)
record = escalate(msg, question, result, fir)
sablon = ESCALATED_SENT if record["notified"] else ESCALATED_RECORDED