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

@@ -43,6 +43,11 @@ def ttl_s() -> int:
return config.get_int("FIR_TTL_MIN", 120) * 60
def imagine_fereastra_s() -> int:
"""Cat de repede dupa un mesaj o captura mai e „acelasi lucru" (FIR_IMAGINE_MIN)."""
return config.get_int("FIR_IMAGINE_MIN", 5) * 60
def tacere_s() -> int:
"""Cat sta Maria deoparte dupa ce a preluat un om (FIR_TACERE_MIN)."""
return config.get_int("FIR_TACERE_MIN", 60) * 60
@@ -96,20 +101,42 @@ def adauga(fir: dict, rol: str, text: str) -> None:
def este_continuare(fir: dict | None, text: str, are_imagine: bool) -> bool:
"""Mesajul duce mai departe firul deschis, sau incepe altceva?
O captura noua sau un cod pe care firul nu-l are inseamna subiect nou. In rest
— inclusiv la un mesaj lung — e continuare: omul raspunde la ce l-a intrebat
Maria, iar ancora nu strica o cautare care oricum are destul text al ei.
Un cod pe care firul nu-l are inseamna subiect nou. In rest — inclusiv la un
mesaj lung — e continuare: omul raspunde la ce l-a intrebat Maria, iar ancora
nu strica o cautare care oricum are destul text al ei.
Captura de ecran a fost la inceput semn sigur de subiect nou. E fals exact in
cazul cel mai frecvent: omul scrie „am o factura cu eroare la trimitere" si
trimite imediat captura, sau Maria tocmai i-a cerut-o. WhatsApp le livreaza ca
doua mesaje, dar e un singur lucru — iar tratate separat, textul se pierde si
captura se cauta pe OCR-ul ei singur (2026-09-01, grupul „Maria Test").
Departajarea o face timpul: o captura in primele `FIR_IMAGINE_MIN` minute
continua firul, una venita mult mai tarziu deschide altul.
"""
if not fir:
return False
if are_imagine:
return False
return time.time() - fir.get("at", 0) <= imagine_fereastra_s()
noi = rank.codes(text)
if noi and any(c not in (fir.get("coduri") or []) for c in noi):
return False
return True
def extinde_ancora(fir: dict, text: str, maxim: int = 2000) -> None:
"""Captura sosita pe firul deschis aduce chiar textul erorii — intra in ancora.
Fara asta, ancora ramane reclamatia vaga care a deschis firul („am o eroare la
o factura"), iar continuarile de dupa captura se cauta tot dupa ea.
"""
text = (text or "").strip()
if not text:
return
ancora = (fir.get("ancora") or "").strip()
fir["ancora"] = f"{ancora}\n{text}".strip()[-maxim:]
fir["coduri"] = list(dict.fromkeys((fir.get("coduri") or []) + rank.codes(text)))
def interogare(fir: dict, text: str) -> str:
"""Ce se cauta in index la o continuare: ancora firului plus mesajul nou."""
ancora = (fir.get("ancora") or "").strip()