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

@@ -68,6 +68,11 @@ CAZURI: list[tuple[str, bool]] = [
("O) Pers. Juridice (798) O Netrimise DI Trimise (fara raspuns) [5 Timis (cu raspuns Erori) (1). "
"O Tamise (cu diferente Reg. V2.) (1)\nMesaj raspuns. | Factura cu id incarcare=6638295792 nu po] "
"| | <Error errorMessage=\"CUI cumparator incorect\"/>", False),
# aceeasi captura, dar cautata IMPREUNA cu mesajul care a precedat-o (fir.interogare):
# textul in plus urca cosinusul la 0,836 fara sa aduca vreo dovada noua.
("Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULE\n"
"Mesaj raspuns. | Factura cu id incarcare=6638295792 nu po] "
"| | <Error errorMessage=\"CUI cumparator incorect\"/>", False),
("care e capitala Frantei", False),
("cat costa un bilet de avion la Paris", False),
("imi da eroare la imprimanta HP LaserJet, nu trage hartia", False),
@@ -119,13 +124,13 @@ def pregateste(cazuri: list[tuple[str, bool]]) -> tuple[consumer.Index, list[dic
return index, date
def evalueaza(index: consumer.Index, date: list[dict], strong: float, weak: float,
def evalueaza(index: consumer.Index, date: list[dict], weak: float,
min_ratio: float) -> tuple[int, list[str]]:
corecte, gresite = 0, []
for d in date:
ok, _ = rank.assess(
d["intrebare"], d["best"], d["top_texts"], index.bm25,
strong=strong, weak=weak, min_ratio=min_ratio,
weak=weak, min_ratio=min_ratio,
)
if ok == d["asteptat"]:
corecte += 1
@@ -149,28 +154,24 @@ def main() -> None:
if args.sweep:
rezultate = []
for strong in (0.62, 0.66, 0.68, 0.70, 0.72, 0.75):
for weak in (0.54, 0.58, 0.60):
for ratio in (0.0, 0.34, 0.5, 0.67, 1.0):
if weak >= strong:
continue
corecte, _ = evalueaza(index, date, strong, weak, ratio)
rezultate.append((corecte, strong, weak, ratio))
for weak in (0.50, 0.54, 0.58, 0.60, 0.64):
for ratio in (0.0, 0.34, 0.5, 0.67, 1.0):
corecte, _ = evalueaza(index, date, weak, ratio)
rezultate.append((corecte, weak, ratio))
rezultate.sort(key=lambda r: (-r[0], r[1]))
print(f"{'corecte':>8} strong weak ratio")
for corecte, strong, weak, ratio in rezultate[:15]:
print(f"{corecte:>5}/{len(date)} {strong:.2f} {weak:.2f} {ratio:.2f}")
print(f"{'corecte':>8} weak ratio")
for corecte, weak, ratio in rezultate[:15]:
print(f"{corecte:>5}/{len(date)} {weak:.2f} {ratio:.2f}")
return
strong, weak = rank.strong_cosine(), rank.weak_cosine()
ratio = rank.min_rare_ratio()
print(f"praguri curente: strong={strong} weak={weak} ratio={ratio}\n")
weak, ratio = rank.weak_cosine(), rank.min_rare_ratio()
print(f"praguri curente: weak={weak} ratio={ratio}\n")
for d in date:
ok, motiv = rank.assess(d["intrebare"], d["best"], d["top_texts"], index.bm25)
semn = "OK " if ok == d["asteptat"] else "GRESIT"
stare = "ACOPERIT " if ok else "ESCALADAT"
print(f"{semn} [{stare}] {d['intrebare'][:44]:46} {motiv}")
corecte, gresite = evalueaza(index, date, strong, weak, ratio)
corecte, gresite = evalueaza(index, date, weak, ratio)
print(f"\n{corecte}/{len(date)} corecte")
for g in gresite:
print(f" - {g}")

View File

@@ -32,10 +32,9 @@ OCR_TIMEOUT_S=60
# Chunk-urile se ordoneaza de doua ori — semantic (embeddings) si lexical (BM25) —
# iar clasamentele se fuzioneaza. Vezi rag/rank.py; valorile de mai jos sunt
# calibrate pe indexul real, nu alese din burta.
# Peste RANK_STRONG_COSINE raspundem din documente fara alte conditii; sub
# RANK_WEAK_COSINE escaladam direct; intre ele, raspundem doar daca termenii rari
# Sub RANK_WEAK_COSINE escaladam direct; peste, raspundem doar daca termenii rari
# din intrebare (coduri de eroare, in general) chiar apar in chunk-urile gasite.
RANK_STRONG_COSINE=0.80
# Nu exista prag „cosinus destul de mare ca sa nu mai verificam" — vezi rag/rank.py.
RANK_WEAK_COSINE=0.58
RANK_RECALL_N=12
# Ce fractiune din termenii distinctivi ai intrebarii trebuie sa apara in