fix(maria): acoperirea se judeca pe ce a intrebat omul acum, si pe radacina cuvantului

Doua reparatii pe poarta de acoperire, amandoua masurate pe setul de calibrare
extins (31 de cazuri, de la 26).

**Termenii distinctivi se comparau pe cuvantul intreg.** Documentatia si clientul
nu folosesc aceleasi forme: documentul scrie „token" si „tokenuri", omul scrie
„tokenul"; documentul „Generare", omul „generez". Trei continuari legitime din
cinci escaladau desi chunk-ul cu raspunsul era chiar in context. Comparatia se
face acum pe primele 5 litere; codurile raman intregi (ORA-01722 si ORA-01720 au
aceleasi cinci).

**Interogarea avea doua roluri amestecate.** Pe un fir, regasirea are nevoie de
ancora („da, ma blocheaza" singur nu gaseste nimic), dar acoperirea nu: judecata
pe ancora, ea cere sa apara in documente si salutul, si numele firmei, si luna,
deci raportul scade la fiecare replica si firul lung escaladeaza degeaba.
`rank.assess` primeste `dovada` — mesajul nou, plus OCR-ul capturii lui; cand
acesta n-are termeni proprii, se cade inapoi pe interogarea intreaga.

Setul de calibrare: 31/31 cu reparatia, 30/31 fara.

Ce NU s-a facut, desi era propus in handover: lista de formule de politete in
`rare_terms`. Masuratoarea nu o justifica — cu potrivirea pe radacina, cazurile cu
ancora politicoasa trec si fara ea, iar intr-una din configuratii facea rau.
Cauza reala nu era politetea din ancora, ci morfologia.

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:57:49 +00:00
parent 89145d437e
commit 653f5cbba9
6 changed files with 184 additions and 20 deletions

View File

@@ -209,6 +209,31 @@ def rank(
return sorted(fused, key=lambda i: (fused[i], cosines[i]), reverse=True)
RADACINA = 5 # cate litere din inceputul unui cuvant se compara cu documentele
def _apare(term: str, blob: str) -> bool:
"""Termenul e in text — si la alta forma gramaticala decat a scris-o omul.
Romana articuleaza si conjuga, documentatia si clientul nu folosesc aceleasi
forme: documentul scrie „token" si „tokenuri", omul scrie „tokenul"; documentul
„Generare", omul „generez"; documentul „trimise", omul „trimiterea". Comparat
pe cuvantul intreg, exact cuvantul care conteaza nu se gaseste — masurat pe
setul de calibrare (2026-09-01), trei continuari legitime din cinci escaladau
numai din cauza asta, desi chunk-ul cu raspunsul era chiar in context.
Codurile se compara intregi: `ora-01722` si `ora-01720` au aceleasi 5 litere
de la inceput si n-au nimic in comun.
# ponytail: prefix fix, nu stemmer. Poate potrivi gresit cuvinte scurte cu
# aceeasi radacina („capitala" ~ „capitaluri"); daca apare in practica, aici
# se pune un stemmer romanesc, nu un prag mai mare.
"""
if _COD.match(term):
return term in blob
return term[:RADACINA] in blob
def assess(
query: str,
best_cosine: float,
@@ -217,6 +242,7 @@ def assess(
*,
weak: float | None = None,
min_ratio: float | None = None,
dovada: str | None = None,
) -> tuple[bool, str]:
"""(avem acoperire in documente?, motivul) — decizia de a raspunde sau de a escalada.
@@ -232,6 +258,19 @@ def assess(
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.
`dovada` desparte cele doua roluri ale interogarii pe un fir de discutie.
Regasirea are nevoie de ancora („da, ma blocheaza" singur nu gaseste nimic),
dar acoperirea nu: judecata pe ancora + mesaj nou, ea raspunde la ce a intrebat
omul acum SI la formulele si numele proprii din primul mesaj — „buna", „august",
„AUTO SULEA" nu apar in niciun document, deci raportul scade la fiecare replica.
Cand `dovada` (mesajul nou, plus OCR-ul capturii lui) are termeni distinctivi
proprii, acoperirea se judeca pe ei. Cand n-are, se cade inapoi pe interogarea
intreaga — comportamentul de dinainte; altfel o continuare fara niciun cuvant
al ei ar escalada.
Codurile (cazul 0) raman pe interogarea intreaga: codul din ancora e chiar
subiectul firului, iar continuarea nu-l repeta.
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
@@ -256,17 +295,19 @@ def assess(
)
if best_cosine < weak:
return False, f"cosinus {best_cosine:.3f} < {weak}"
rare = bm25.rare_terms(query)
rare = bm25.rare_terms(dovada) if dovada else []
if not rare:
rare = bm25.rare_terms(query)
if not rare:
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]
gasiti = [t for t in rare if _apare(t, blob)]
if gasiti and len(gasiti) / len(rare) >= min_ratio:
return True, (
f"cosinus {best_cosine:.3f} + {len(gasiti)}/{len(rare)} termeni distinctivi "
f"in context: {', '.join(gasiti)}"
)
lipsa = [t for t in rare if t not in blob]
lipsa = [t for t in rare if not _apare(t, blob)]
return False, (
f"cosinus {best_cosine:.3f}, iar termenii distinctivi "
f"({', '.join(lipsa[:5])}) nu apar in documente"