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

@@ -38,20 +38,6 @@ import config
RRF_K = 60 # constanta standard din Reciprocal Rank Fusion
def strong_cosine() -> float:
"""Peste atat, singur cosinusul e de ajuns (RANK_STRONG_COSINE).
Era 0,70 si a fost prea jos: pe 2026-09-01, „am o factura cu eroare in SPV"
(0,768) si o captura cu `errorMessage="CUI cumparator incorect"` (0,778) au
trecut drept acoperite, desi prima nu spune ce eroare e, iar a doua nu exista
in documente. Toate chunk-urile eFactura seamana intre ele: 0,7x nu distinge
„e in documente" de „e despre eFactura". La 0,80 trec doar potrivirile
aproape textuale, restul cad pe dovada lexicala (termenii rari din intrebare
chiar in chunk-uri). Verificat cu ops/calibrate-rank.py: 25/25.
"""
return float(config.get("RANK_STRONG_COSINE", "0.80"))
def weak_cosine() -> float:
"""Sub atat nu salvam raspunsul nici cu potrivire lexicala (RANK_WEAK_COSINE)."""
return float(config.get("RANK_WEAK_COSINE", "0.58"))
@@ -229,7 +215,6 @@ def assess(
top_texts: list[str],
bm25: Bm25,
*,
strong: float | None = None,
weak: float | None = None,
min_ratio: float | None = None,
) -> tuple[bool, str]:
@@ -237,18 +222,26 @@ def assess(
Cazuri, in ordine:
0. intrebarea contine coduri de eroare -> decide DOAR prezenta lor in
chunk-uri, si cand cosinusul e mare (vezi mai jos);
1. cosinus mare -> raspundem, indiferent de lexical;
2. cosinus la mijloc, dar o fractiune suficienta din termenii rari ai
intrebarii apare chiar in chunk-uri (tipic: un cod de eroare) -> raspundem;
3. altfel -> nu avem acoperire, intrebarea pleaca la suport.
chunk-uri;
1. cosinus sub `weak` -> nici nu ne uitam mai departe, pleaca la suport;
2. altfel -> raspundem doar daca o fractiune suficienta din termenii rari ai
intrebarii apare chiar in chunk-urile gasite.
Cazul 0 exista fiindca cosinusul mare NU e o dovada cand e vorba de coduri:
doua erori Oracle diferite se scriu aproape la fel („[Oracle][ODBC][Ora]ORA-…"),
deci un chunk despre ORA-12541 sta la 0,74 de o intrebare despre ORA-06550.
Fara asta, Maria raspundea sigur pe ea cu eroarea gresita, in loc sa escaladeze.
A existat si un prag „cosinus mare -> raspundem, indiferent de lexical"
(`RANK_STRONG_COSINE`, 0,70). L-am scos, nu urcat, si nu doar fiindca a lasat
sa treaca doua raspunsuri inventate pe 2026-09-01: **cosinusul creste cu
lungimea interogarii**. Pe un fir de discutie interogarea e ancora plus
mesajul nou, deci urca la fiecare replica fara sa aduca vreo dovada — aceeasi
captura a dat 0,778 singura si 0,836 cu mesajul de dinainte. Orice prag fix
de sus e trecut mai devreme sau mai tarziu de o discutie destul de lunga.
Verificat pe setul de cazuri: niciunul dintre cele cu raspuns in documente nu
avea nevoie de scurtatura — toate trec pe dovada lexicala.
"""
strong = strong_cosine() if strong is None else strong
weak = weak_cosine() if weak is None else weak
min_ratio = min_rare_ratio() if min_ratio is None else min_ratio
cerute = codes(query)
@@ -261,13 +254,11 @@ def assess(
f"intrebarea are coduri ({', '.join(cerute[:4])}) care nu apar in "
f"documente (cosinus {best_cosine:.3f})"
)
if best_cosine >= strong:
return True, f"cosinus {best_cosine:.3f} >= {strong}"
if best_cosine < weak:
return False, f"cosinus {best_cosine:.3f} < {weak}"
rare = bm25.rare_terms(query)
if not rare:
return False, f"cosinus {best_cosine:.3f} la limita, iar intrebarea nu are termeni distinctivi"
return False, f"cosinus {best_cosine:.3f}, iar intrebarea nu are termeni distinctivi"
blob = fold("\n".join(top_texts))
gasiti = [t for t in rare if t in blob]
if gasiti and len(gasiti) / len(rare) >= min_ratio:
@@ -277,6 +268,6 @@ def assess(
)
lipsa = [t for t in rare if t not in blob]
return False, (
f"cosinus {best_cosine:.3f} la limita, iar termenii distinctivi "
f"cosinus {best_cosine:.3f}, iar termenii distinctivi "
f"({', '.join(lipsa[:5])}) nu apar in documente"
)