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:
@@ -358,6 +358,22 @@ Fuziunea da mereu un clasament, si la o intrebare complet straina. De aceea deci
|
|||||||
distinctivi ai intrebarii (coduri, cuvinte rare in corpus) apar chiar in
|
distinctivi ai intrebarii (coduri, cuvinte rare in corpus) apar chiar in
|
||||||
chunk-urile gasite.
|
chunk-urile gasite.
|
||||||
|
|
||||||
|
Termenii se compara **pe primele 5 litere**, nu pe cuvantul intreg (codurile fac
|
||||||
|
exceptie: `ORA-01722` si `ORA-01720` incep la fel). Documentatia si clientul nu
|
||||||
|
folosesc aceleasi forme — documentul scrie „token" si „tokenuri", omul scrie
|
||||||
|
„tokenul"; documentul „Generare", omul „generez". Comparate intregi, exact
|
||||||
|
cuvintele care conteaza nu se gaseau: pe setul de calibrare, trei continuari
|
||||||
|
legitime escaladau desi chunk-ul cu raspunsul era chiar in context.
|
||||||
|
|
||||||
|
**Pe un fir de discutie, interogarea are doua roluri diferite.** Regasirea se face
|
||||||
|
pe ancora plus mesajul nou — ancora chiar ajuta, „da, ma blocheaza" singur nu
|
||||||
|
gaseste nimic. Acoperirea se judeca insa pe **mesajul nou** (`dovada`, in
|
||||||
|
`rank.assess`): altfel 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), iar raportul scade la fiecare replica — firul lung escaladeaza degeaba.
|
||||||
|
Cand mesajul nou n-are termeni distinctivi proprii, se cade inapoi pe interogarea
|
||||||
|
intreaga, ca inainte.
|
||||||
|
|
||||||
Fractiunea din pasul 2 nu e cosmetica: cu un singur termen gasit, „cum imi resetez
|
Fractiunea din pasul 2 nu e cosmetica: cu un singur termen gasit, „cum imi resetez
|
||||||
parola de la Windows" trecea drept acoperita fiindca „parola" apare in documente.
|
parola de la Windows" trecea drept acoperita fiindca „parola" apare in documente.
|
||||||
|
|
||||||
@@ -373,7 +389,7 @@ L-am scos, nu urcat, fiindca **cosinusul creste cu lungimea interogarii**: pe un
|
|||||||
de discutie se cauta dupa ancora plus mesajul nou, deci scorul urca la fiecare
|
de discutie se cauta dupa ancora plus mesajul nou, deci scorul urca la fiecare
|
||||||
replica fara sa apara vreo dovada in plus. Aceeasi captura: 0,778 singura, 0,836
|
replica fara sa apara vreo dovada in plus. Aceeasi captura: 0,778 singura, 0,836
|
||||||
impreuna cu mesajul de dinainte. Orice prag fix de sus e trecut de o discutie destul
|
impreuna cu mesajul de dinainte. Orice prag fix de sus e trecut de o discutie destul
|
||||||
de lunga. `ops/calibrate-rank.py`: 26/26, si niciunul dintre cazurile cu raspuns in
|
de lunga. `ops/calibrate-rank.py`: 31/31, si niciunul dintre cazurile cu raspuns in
|
||||||
documente nu avea nevoie de scurtatura — toate trec pe dovada lexicala.
|
documente nu avea nevoie de scurtatura — toate trec pe dovada lexicala.
|
||||||
|
|
||||||
Modelul primeste `temperature=0` (`LLM_TEMPERATURE`). Fara ea, llama.cpp raspunde
|
Modelul primeste `temperature=0` (`LLM_TEMPERATURE`). Fara ea, llama.cpp raspunde
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ siguranta: daca un prag nou strica un caz care mergea, se vede imediat.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
@@ -34,8 +35,10 @@ import config # noqa: E402
|
|||||||
import consumer # noqa: E402
|
import consumer # noqa: E402
|
||||||
import rank # noqa: E402
|
import rank # noqa: E402
|
||||||
|
|
||||||
# (intrebare, are raspuns in documente?)
|
# (intrebare, are raspuns in documente?) sau, pentru continuari pe un fir,
|
||||||
CAZURI: list[tuple[str, bool]] = [
|
# (ancora, mesaj_nou, are raspuns in documente?): regasirea se face pe ancora plus
|
||||||
|
# mesajul nou (exact ce produce fir.interogare), dovada doar pe mesajul nou.
|
||||||
|
CAZURI: list[tuple] = [
|
||||||
("token efactura expirat", True),
|
("token efactura expirat", True),
|
||||||
("Nu exista nici un CIF pentru care sa aveti drept in SPV", True),
|
("Nu exista nici un CIF pentru care sa aveti drept in SPV", True),
|
||||||
("cum trimit declaratia D406 SAF-T", True),
|
("cum trimit declaratia D406 SAF-T", True),
|
||||||
@@ -80,6 +83,31 @@ CAZURI: list[tuple[str, bool]] = [
|
|||||||
("vremea de maine in Bucuresti", False),
|
("vremea de maine in Bucuresti", False),
|
||||||
("cum imi resetez parola de la Windows", False),
|
("cum imi resetez parola de la Windows", False),
|
||||||
("imi da eroare 0x80070005 la instalarea unui joc pe Steam", False),
|
("imi da eroare 0x80070005 la instalarea unui joc pe Steam", False),
|
||||||
|
# --- continuari pe fir, fara cod de eroare: aici decid termenii distinctivi ---
|
||||||
|
# Ancora politicoasa aduce in interogare „buna/ziua/puteti/ajutati", cuvinte care
|
||||||
|
# nu apar niciodata in documente. Raspunsul (token expirat) e in
|
||||||
|
# efactura_knowledge.md:118-134 (TOPIC token_anaf, expirare la 90 de zile).
|
||||||
|
("Buna ziua . Am o problema la trimiterea in SPV . Puteti sa ma ajutati?",
|
||||||
|
"imi zice ca tokenul e expirat", True),
|
||||||
|
# Aceeasi continuare, ancora fara formule de politete: verdictul trebuie sa fie
|
||||||
|
# acelasi. Daca difera, politetea din ancora e cea care escaladeaza.
|
||||||
|
("Am o problema la trimiterea in SPV",
|
||||||
|
"imi zice ca tokenul e expirat", True),
|
||||||
|
# Aceeasi ancora politicoasa, dar raspunsul NU e in documente: „CUI cumparator
|
||||||
|
# incorect" nu apare in niciun document (grep peste documents/, 2026-09-01).
|
||||||
|
("Buna ziua . Am o problema la trimiterea in SPV . Puteti sa ma ajutati?",
|
||||||
|
"imi da eroare CUI cumparator incorect la validare", False),
|
||||||
|
# Fir lung: a patra replica, ancora crescuta cu schimburile de dinainte.
|
||||||
|
# Verdictul nu are voie sa se degradeze cu lungimea (cosinusul urca oricum).
|
||||||
|
("Buna ziua . Am o problema la trimiterea in SPV . Puteti sa ma ajutati?\n"
|
||||||
|
"imi zice ca tokenul e expirat\n"
|
||||||
|
"am apasat Actualizare in borderoul eFactura dar tot nu merge",
|
||||||
|
"unde apas ca sa generez tokenul nou?", True),
|
||||||
|
# Continuare pe un fir deschis de o captura (escaladarea M-260901-06EB): ancora
|
||||||
|
# e mesajul omului plus OCR-ul, plin de zgomot de interfata. Eroarea din captura
|
||||||
|
# nu e in documente, deci si continuarea trebuie sa plece la suport.
|
||||||
|
("Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULEA MOBILE SRL . Poti te rog sa ma ajuti?\nTotal\nLit\nO) Pers. Juridice (798) O Netrimise DI Trimise (fara raspuns) [5 Timis (cu raspuns Erori) (1). O Tamise (cu diferente Reg. V2.) (1)\nNr Articol Cantitate UM | Pret unitar Valoare fara TVA Discount fara TVA % TVA Descriere Detalii\n5 Detalii raspuns\n663| | <2xml version=\"1.0” encoding=\"UTF-8\" standalone=\"yes\"?>\n<header xmins=\"mfp:anaf-dgtefactura:mesajroriFactuta:v1” Index_incarcare=\"\nMesaj raspuns. | Factura cu id incarcare=6638295792 nu po] | | <Error errorMessage=\"CUI cumparator incorect\"/>\nTri\nMesaj trimitere | Transmis cu succes. Index incarcare\n1670404158\" Cif_emitent=\"\n3137200\">\n@ inclusiv Pers. Fizice\nSelecteaz!",
|
||||||
|
"si acum ce fac cu factura asta?", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
CACHE = config.STATE_DIR / "calibrare-cache.json"
|
CACHE = config.STATE_DIR / "calibrare-cache.json"
|
||||||
@@ -92,14 +120,27 @@ def incarca_cache() -> dict[str, list[float]]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def pregateste(cazuri: list[tuple[str, bool]]) -> tuple[consumer.Index, list[dict]]:
|
def desfa(caz: tuple) -> tuple[str, str | None, bool]:
|
||||||
|
"""(interogare de regasire, interogare de dovada, asteptat).
|
||||||
|
|
||||||
|
Un 3-tuplu e o continuare pe fir: se cauta dupa ancora plus mesajul nou (ca in
|
||||||
|
fir.interogare), dar dovada e doar ce a intrebat omul acum.
|
||||||
|
"""
|
||||||
|
if len(caz) == 3:
|
||||||
|
ancora, mesaj, asteptat = caz
|
||||||
|
return f"{ancora}\n{mesaj}", mesaj, asteptat
|
||||||
|
return caz[0], None, caz[1]
|
||||||
|
|
||||||
|
|
||||||
|
def pregateste(cazuri: list[tuple]) -> tuple[consumer.Index, list[dict]]:
|
||||||
index = consumer.load_index()
|
index = consumer.load_index()
|
||||||
if not len(index):
|
if not len(index):
|
||||||
sys.exit(f"index gol sau lipsa: {config.INDEX_FILE}")
|
sys.exit(f"index gol sau lipsa: {config.INDEX_FILE}")
|
||||||
cache = incarca_cache()
|
cache = incarca_cache()
|
||||||
date = []
|
date = []
|
||||||
nou = 0
|
nou = 0
|
||||||
for intrebare, asteptat in cazuri:
|
for caz in cazuri:
|
||||||
|
intrebare, dovada, asteptat = desfa(caz)
|
||||||
vec = cache.get(intrebare)
|
vec = cache.get(intrebare)
|
||||||
if vec is None:
|
if vec is None:
|
||||||
print(f" embed: {intrebare[:50]}…", file=sys.stderr)
|
print(f" embed: {intrebare[:50]}…", file=sys.stderr)
|
||||||
@@ -112,6 +153,7 @@ def pregateste(cazuri: list[tuple[str, bool]]) -> tuple[consumer.Index, list[dic
|
|||||||
: config.get_int("TOP_K", 3)]
|
: config.get_int("TOP_K", 3)]
|
||||||
date.append({
|
date.append({
|
||||||
"intrebare": intrebare,
|
"intrebare": intrebare,
|
||||||
|
"dovada": dovada,
|
||||||
"asteptat": asteptat,
|
"asteptat": asteptat,
|
||||||
# ca in consumer.search: cosinusul chunk-urilor date modelului
|
# ca in consumer.search: cosinusul chunk-urilor date modelului
|
||||||
"best": max((cos[i] for i in ordine), default=0.0),
|
"best": max((cos[i] for i in ordine), default=0.0),
|
||||||
@@ -124,13 +166,19 @@ def pregateste(cazuri: list[tuple[str, bool]]) -> tuple[consumer.Index, list[dic
|
|||||||
return index, date
|
return index, date
|
||||||
|
|
||||||
|
|
||||||
|
# rank.assess primeste `dovada` doar dupa reparatia termenilor distinctivi; pana
|
||||||
|
# atunci se poate rula numai varianta de azi.
|
||||||
|
ARE_DOVADA = "dovada" in inspect.signature(rank.assess).parameters
|
||||||
|
|
||||||
|
|
||||||
def evalueaza(index: consumer.Index, date: list[dict], weak: float,
|
def evalueaza(index: consumer.Index, date: list[dict], weak: float,
|
||||||
min_ratio: float) -> tuple[int, list[str]]:
|
min_ratio: float, dovada_separata: bool = False) -> tuple[int, list[str]]:
|
||||||
corecte, gresite = 0, []
|
corecte, gresite = 0, []
|
||||||
for d in date:
|
for d in date:
|
||||||
|
extra = {"dovada": d["dovada"]} if dovada_separata else {}
|
||||||
ok, _ = rank.assess(
|
ok, _ = rank.assess(
|
||||||
d["intrebare"], d["best"], d["top_texts"], index.bm25,
|
d["intrebare"], d["best"], d["top_texts"], index.bm25,
|
||||||
weak=weak, min_ratio=min_ratio,
|
weak=weak, min_ratio=min_ratio, **extra,
|
||||||
)
|
)
|
||||||
if ok == d["asteptat"]:
|
if ok == d["asteptat"]:
|
||||||
corecte += 1
|
corecte += 1
|
||||||
@@ -147,7 +195,7 @@ def main() -> None:
|
|||||||
|
|
||||||
cazuri = CAZURI
|
cazuri = CAZURI
|
||||||
if args.cases:
|
if args.cases:
|
||||||
cazuri = [(q, bool(a)) for q, a in json.loads(pathlib.Path(args.cases).read_text())]
|
cazuri = [tuple(c) for c in json.loads(pathlib.Path(args.cases).read_text())]
|
||||||
|
|
||||||
index, date = pregateste(cazuri)
|
index, date = pregateste(cazuri)
|
||||||
print(f"index: {len(index)} chunk-uri | cazuri: {len(date)}\n")
|
print(f"index: {len(index)} chunk-uri | cazuri: {len(date)}\n")
|
||||||
@@ -156,9 +204,11 @@ def main() -> None:
|
|||||||
rezultate = []
|
rezultate = []
|
||||||
for weak in (0.50, 0.54, 0.58, 0.60, 0.64):
|
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):
|
for ratio in (0.0, 0.34, 0.5, 0.67, 1.0):
|
||||||
corecte, _ = evalueaza(index, date, weak, ratio)
|
corecte, _ = evalueaza(index, date, weak, ratio, ARE_DOVADA)
|
||||||
rezultate.append((corecte, weak, ratio))
|
rezultate.append((corecte, weak, ratio))
|
||||||
rezultate.sort(key=lambda r: (-r[0], r[1]))
|
rezultate.sort(key=lambda r: (-r[0], r[1]))
|
||||||
|
mod = "cu dovada separata" if ARE_DOVADA else "fara separare (rank.assess nu are inca `dovada`)"
|
||||||
|
print(f"mod: {mod}\n")
|
||||||
print(f"{'corecte':>8} weak ratio")
|
print(f"{'corecte':>8} weak ratio")
|
||||||
for corecte, weak, ratio in rezultate[:15]:
|
for corecte, weak, ratio in rezultate[:15]:
|
||||||
print(f"{corecte:>5}/{len(date)} {weak:.2f} {ratio:.2f}")
|
print(f"{corecte:>5}/{len(date)} {weak:.2f} {ratio:.2f}")
|
||||||
@@ -171,10 +221,14 @@ def main() -> None:
|
|||||||
semn = "OK " if ok == d["asteptat"] else "GRESIT"
|
semn = "OK " if ok == d["asteptat"] else "GRESIT"
|
||||||
stare = "ACOPERIT " if ok else "ESCALADAT"
|
stare = "ACOPERIT " if ok else "ESCALADAT"
|
||||||
print(f"{semn} [{stare}] {d['intrebare'][:44]:46} {motiv}")
|
print(f"{semn} [{stare}] {d['intrebare'][:44]:46} {motiv}")
|
||||||
corecte, gresite = evalueaza(index, date, weak, ratio)
|
for eticheta, separata in (("fara separare (azi)", False), ("cu dovada separata ", True)):
|
||||||
print(f"\n{corecte}/{len(date)} corecte")
|
if separata and not ARE_DOVADA:
|
||||||
for g in gresite:
|
print("\ncu dovada separata : rank.assess nu accepta inca `dovada`, sarit")
|
||||||
print(f" - {g}")
|
continue
|
||||||
|
corecte, gresite = evalueaza(index, date, weak, ratio, separata)
|
||||||
|
print(f"\n{eticheta}: {corecte}/{len(date)} corecte")
|
||||||
|
for g in gresite:
|
||||||
|
print(f" - {g}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -128,8 +128,13 @@ def cosine(a: list[float], b: list[float]) -> float:
|
|||||||
return dot / (na * nb) if na and nb else 0.0
|
return dot / (na * nb) if na and nb else 0.0
|
||||||
|
|
||||||
|
|
||||||
def search(index: Index, query: str, top_k: int) -> dict:
|
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."""
|
"""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):
|
if not len(index):
|
||||||
return {"chunks": [], "covered": False, "reason": "index gol",
|
return {"chunks": [], "covered": False, "reason": "index gol",
|
||||||
"best_cosine": 0.0, "top": []}
|
"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:
|
# 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.
|
# altfel acoperirea se judeca pe o dovada care nu ajunge in context.
|
||||||
best = max((cosines[i] for i in order), default=0.0)
|
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 {
|
return {
|
||||||
"chunks": texts,
|
"chunks": texts,
|
||||||
"covered": covered,
|
"covered": covered,
|
||||||
@@ -558,6 +563,7 @@ def handle(index: Index, msg: dict) -> None:
|
|||||||
if not question:
|
if not question:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
dovada = None
|
||||||
if continuare:
|
if continuare:
|
||||||
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
|
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
|
||||||
fir_mod.extinde_ancora(fir, msg.get("ocr_text") or "")
|
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)
|
send_reply(sender, raspuns)
|
||||||
return
|
return
|
||||||
fir["ref"] = None # escaladarea nu mai exista; tratam mesajul normal
|
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)
|
search_query = fir_mod.interogare(fir, search_query)
|
||||||
print(f"[consumer] continuare pe firul deschis; caut dupa {search_query[:90]!r}",
|
print(f"[consumer] continuare pe firul deschis; caut dupa {search_query[:90]!r}",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
@@ -579,7 +588,7 @@ def handle(index: Index, msg: dict) -> None:
|
|||||||
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
|
fir_mod.adauga(fir, "client", text or "[captura de ecran]")
|
||||||
|
|
||||||
try:
|
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
|
except Exception as exc: # noqa: BLE001
|
||||||
print(f"[consumer] cautare esuata: {exc}", file=sys.stderr)
|
print(f"[consumer] cautare esuata: {exc}", file=sys.stderr)
|
||||||
send_reply(sender, "Scuze, am o problema tehnica momentan. Cineva din echipa te va contacta.")
|
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
|
la fiecare continuare — asa „da, ma blocheaza" se cauta impreuna cu eroarea, nu
|
||||||
singur.
|
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
|
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
|
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
|
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)
|
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(
|
def assess(
|
||||||
query: str,
|
query: str,
|
||||||
best_cosine: float,
|
best_cosine: float,
|
||||||
@@ -217,6 +242,7 @@ def assess(
|
|||||||
*,
|
*,
|
||||||
weak: float | None = None,
|
weak: float | None = None,
|
||||||
min_ratio: float | None = None,
|
min_ratio: float | None = None,
|
||||||
|
dovada: str | None = None,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
"""(avem acoperire in documente?, motivul) — decizia de a raspunde sau de a escalada.
|
"""(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.
|
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.
|
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"
|
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
|
(`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
|
sa treaca doua raspunsuri inventate pe 2026-09-01: **cosinusul creste cu
|
||||||
@@ -256,17 +295,19 @@ def assess(
|
|||||||
)
|
)
|
||||||
if best_cosine < weak:
|
if best_cosine < weak:
|
||||||
return False, f"cosinus {best_cosine:.3f} < {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:
|
if not rare:
|
||||||
return False, f"cosinus {best_cosine:.3f}, 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))
|
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:
|
if gasiti and len(gasiti) / len(rare) >= min_ratio:
|
||||||
return True, (
|
return True, (
|
||||||
f"cosinus {best_cosine:.3f} + {len(gasiti)}/{len(rare)} termeni distinctivi "
|
f"cosinus {best_cosine:.3f} + {len(gasiti)}/{len(rare)} termeni distinctivi "
|
||||||
f"in context: {', '.join(gasiti)}"
|
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, (
|
return False, (
|
||||||
f"cosinus {best_cosine:.3f}, iar termenii distinctivi "
|
f"cosinus {best_cosine:.3f}, iar termenii distinctivi "
|
||||||
f"({', '.join(lipsa[:5])}) nu apar in documente"
|
f"({', '.join(lipsa[:5])}) nu apar in documente"
|
||||||
|
|||||||
@@ -45,6 +45,45 @@ def test_rare_terms_prinde_codurile_si_ignora_cuvintele_comune():
|
|||||||
assert "ora-01722" in rare
|
assert "ora-01722" in rare
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_gaseste_termenul_la_alta_forma_gramaticala():
|
||||||
|
# Documentul scrie „token"/„tokenuri", omul scrie „tokenul". Comparat pe cuvantul
|
||||||
|
# intreg, singurul cuvant care conteaza nu se gaseste si intrebarea escaladeaza.
|
||||||
|
bm = rank.Bm25(DOCS)
|
||||||
|
covered, motiv = rank.assess("imi zice ca tokenul a expirat", 0.64, DOCS[1:2], bm)
|
||||||
|
assert covered
|
||||||
|
assert "tokenul" in motiv
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_codurile_se_compara_intregi_nu_pe_radacina():
|
||||||
|
# „ora-01722" si „ora-01720" au aceleasi cinci litere de la inceput.
|
||||||
|
bm = rank.Bm25(DOCS)
|
||||||
|
covered, _ = rank.assess("eroarea ORA-01720", 0.64, DOCS[:1], bm)
|
||||||
|
assert not covered
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_dovada_judeca_mesajul_nou_nu_ancora():
|
||||||
|
# Continuare pe un fir: ancora e salutul plus reclamatia vaga, mesajul nou e
|
||||||
|
# chiar intrebarea. Fara dovada separata, „buna/august" (care nu apar nicaieri)
|
||||||
|
# trag raportul sub prag si escaladeaza degeaba.
|
||||||
|
bm = rank.Bm25(DOCS)
|
||||||
|
ancora = "Buna ziua . Am o problema pe luna august la firma AUTO SULEA . Ma ajutati?"
|
||||||
|
query = f"{ancora}\ntokenul a expirat"
|
||||||
|
fara, _ = rank.assess(query, 0.64, DOCS[1:2], bm)
|
||||||
|
cu, motiv = rank.assess(query, 0.64, DOCS[1:2], bm, dovada="tokenul a expirat")
|
||||||
|
assert not fara
|
||||||
|
assert cu and "expirat" in motiv
|
||||||
|
|
||||||
|
|
||||||
|
def test_assess_continuare_scurta_cade_inapoi_pe_interogarea_intreaga():
|
||||||
|
# „da, ma blocheaza complet" n-are termeni distinctivi proprii. Judecata singura
|
||||||
|
# ar escalada orice continuare scurta — exact regresia pentru care exista ancora.
|
||||||
|
bm = rank.Bm25(DOCS)
|
||||||
|
query = "token efactura expirat\nda"
|
||||||
|
covered, _ = rank.assess(query, 0.64, DOCS[1:2], bm, dovada="da")
|
||||||
|
fara, _ = rank.assess(query, 0.64, DOCS[1:2], bm)
|
||||||
|
assert covered is fara is True
|
||||||
|
|
||||||
|
|
||||||
def test_rank_urca_chunkul_confirmat_de_ambele_metode():
|
def test_rank_urca_chunkul_confirmat_de_ambele_metode():
|
||||||
# cosinusul favorizeaza documentul 3, dar lexical se potriveste documentul 0;
|
# cosinusul favorizeaza documentul 3, dar lexical se potriveste documentul 0;
|
||||||
# documentul 0 e sustinut de amandoua (cosinus decent + BM25 mare) si iese primul
|
# documentul 0 e sustinut de amandoua (cosinus decent + BM25 mare) si iese primul
|
||||||
|
|||||||
Reference in New Issue
Block a user