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:
@@ -128,8 +128,13 @@ def cosine(a: list[float], b: list[float]) -> float:
|
||||
return dot / (na * nb) if na and nb else 0.0
|
||||
|
||||
|
||||
def search(index: Index, query: str, top_k: int) -> dict:
|
||||
"""Cauta si decide daca avem acoperire. Intoarce si dovezile, pentru log."""
|
||||
def search(index: Index, query: str, top_k: int, dovada: str | None = None) -> dict:
|
||||
"""Cauta si decide daca avem acoperire. Intoarce si dovezile, pentru log.
|
||||
|
||||
`query` regaseste, `dovada` judeca. Pe un fir, `query` e ancora plus mesajul
|
||||
nou (ancora chiar ajuta regasirea), dar acoperirea nu se judeca pe ea — vezi
|
||||
`rank.assess`.
|
||||
"""
|
||||
if not len(index):
|
||||
return {"chunks": [], "covered": False, "reason": "index gol",
|
||||
"best_cosine": 0.0, "top": []}
|
||||
@@ -141,7 +146,7 @@ def search(index: Index, query: str, top_k: int) -> dict:
|
||||
# cosinusul chunk-urilor CHIAR date modelului, nu cel mai bun din tot indexul:
|
||||
# altfel acoperirea se judeca pe o dovada care nu ajunge in context.
|
||||
best = max((cosines[i] for i in order), default=0.0)
|
||||
covered, reason = rank.assess(query, best, texts, index.bm25)
|
||||
covered, reason = rank.assess(query, best, texts, index.bm25, dovada=dovada)
|
||||
return {
|
||||
"chunks": texts,
|
||||
"covered": covered,
|
||||
@@ -558,6 +563,7 @@ def handle(index: Index, msg: dict) -> None:
|
||||
if not question:
|
||||
return
|
||||
|
||||
dovada = None
|
||||
if continuare:
|
||||
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
|
||||
fir_mod.extinde_ancora(fir, msg.get("ocr_text") or "")
|
||||
@@ -571,6 +577,9 @@ def handle(index: Index, msg: dict) -> None:
|
||||
send_reply(sender, raspuns)
|
||||
return
|
||||
fir["ref"] = None # escaladarea nu mai exista; tratam mesajul normal
|
||||
# ce a intrebat omul ACUM (mesajul nou, sau textul retinut din captura lui):
|
||||
# acoperirea se judeca pe asta, nu pe ancora care ramane in interogare.
|
||||
dovada = search_query
|
||||
search_query = fir_mod.interogare(fir, search_query)
|
||||
print(f"[consumer] continuare pe firul deschis; caut dupa {search_query[:90]!r}",
|
||||
file=sys.stderr)
|
||||
@@ -579,7 +588,7 @@ def handle(index: Index, msg: dict) -> None:
|
||||
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
|
||||
|
||||
try:
|
||||
result = search(index, search_query, config.get_int("TOP_K", 3))
|
||||
result = search(index, search_query, config.get_int("TOP_K", 3), dovada)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[consumer] cautare esuata: {exc}", file=sys.stderr)
|
||||
send_reply(sender, "Scuze, am o problema tehnica momentan. Cineva din echipa te va contacta.")
|
||||
|
||||
@@ -17,6 +17,11 @@ escaladarii deschise si ultimele schimburi. Ancora intra in interogarea de cauta
|
||||
la fiecare continuare — asa „da, ma blocheaza" se cauta impreuna cu eroarea, nu
|
||||
singur.
|
||||
|
||||
Ancora ajuta REGASIREA, nu acoperirea. Decizia „raspund sau escaladez" se ia pe
|
||||
mesajul nou (`rank.assess(..., dovada=...)`), fiindca ancora poarta si salutul, si
|
||||
numele firmei, si luna — cuvinte care nu apar in niciun document si care trageau
|
||||
raportul de termeni distinctivi in jos la fiecare replica.
|
||||
|
||||
Ce rupe firul, necontestat: o captura noua sau un cod de eroare diferit. Sunt
|
||||
singurele semne sigure ca omul a trecut la altceva; restul (schimbarea subiectului
|
||||
in cuvinte) e prea usor de confundat cu o continuare, iar greseala aia produce
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user