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
236 lines
12 KiB
Python
Executable File
236 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Calibreaza pragurile de escaladare pe indexul REAL, nu pe intuitie.
|
|
|
|
De ce exista: pragul care decide "avem raspunsul in documente" nu se poate alege
|
|
din burta. Marginea dintre o intrebare buna si una straina e de cateva sutimi de
|
|
cosinus si se MUTA cand se schimba documentele — un prag bun azi poate escalada
|
|
maine jumatate din intrebarile legitime.
|
|
|
|
Cum se foloseste:
|
|
|
|
MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python \\
|
|
ops/calibrate-rank.py # raport pe setul de cazuri
|
|
... ops/calibrate-rank.py --sweep # matura grila de praguri
|
|
... ops/calibrate-rank.py --cases fisier.json
|
|
|
|
Embeddings-urile intrebarilor se tin in cache pe disc (`~/.maria-bridge/
|
|
calibrare-cache.json`): pe CPU, o intrebare costa secunde, iar maturarea grilei
|
|
le-ar recalcula de zeci de ori degeaba.
|
|
|
|
Setul de cazuri e deliberat mic si scris de mana. Nu e un benchmark, e o plasa de
|
|
siguranta: daca un prag nou strica un caz care mergea, se vede imediat.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import inspect
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "rag"))
|
|
|
|
import config # noqa: E402
|
|
import consumer # noqa: E402
|
|
import rank # noqa: E402
|
|
|
|
# (intrebare, are raspuns in documente?) sau, pentru continuari pe un fir,
|
|
# (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),
|
|
("Nu exista nici un CIF pentru care sa aveti drept in SPV", True),
|
|
("cum trimit declaratia D406 SAF-T", True),
|
|
("eroare la semnatura electronica in SPV", True),
|
|
("nu pot incarca factura in SPV, imi da eroare de certificat", True),
|
|
("cum inregistrez o factura de achizitie in ROA", True),
|
|
("bilant contabil", True),
|
|
("import de date", True),
|
|
# dupa adaugarea dictionarului de erori Oracle (oracle-erori-uzuale.xml)
|
|
("ORA-01722 invalid number la salvarea facturii", True),
|
|
("imi da ORA-12154 cand deschid programul", True),
|
|
("ORA-00001 unique constraint violated", True),
|
|
("eroare ORA-28000 the account is locked", True),
|
|
# captura reala (OCR), 2026-08-31: raspundea cu ORA-03113/ORA-12541, alta eroare
|
|
("[Oracle][ODBC][OraJORA-06550: line 1, column 7\n"
|
|
"PLS-00906: object VADECO PACK PARTENERI is invalid\n"
|
|
"ORA-06550: line 1, column 7:", True),
|
|
# cod Oracle real, dar care NU e in dictionar: trebuie sa plece la suport
|
|
("ORA-00600 internal error code arguments", False),
|
|
# continuari pe un fir deschis: asa arata interogarea dupa fir.interogare()
|
|
# — ancora plus raspunsul scurt al omului. Fara ancora, „da, ma blocheaza"
|
|
# nimerea ordinul de plata la Trezorerie, cu cosinus 0,642.
|
|
("ORA-06550: line 1, column 7 PLS-00906 object invalid\nda, ma blocheaza complet", True),
|
|
("ORA-12154 TNS could not resolve the connect identifier\n"
|
|
"eram la salvarea unei facturi", True),
|
|
# 2026-09-01, grup „Maria Test": ambele au primit raspunsuri inventate.
|
|
# Prima e o reclamatie vaga (nu spune CE eroare), a doua e o eroare eFactura
|
|
# care nu exista in documente — dar toate chunk-urile eFactura ii seamana.
|
|
("Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULE", False),
|
|
("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),
|
|
("cum schimb uleiul la masina", False),
|
|
("vremea de maine in Bucuresti", False),
|
|
("cum imi resetez parola de la Windows", 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"
|
|
|
|
|
|
def incarca_cache() -> dict[str, list[float]]:
|
|
try:
|
|
return json.loads(CACHE.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return {}
|
|
|
|
|
|
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()
|
|
if not len(index):
|
|
sys.exit(f"index gol sau lipsa: {config.INDEX_FILE}")
|
|
cache = incarca_cache()
|
|
date = []
|
|
nou = 0
|
|
for caz in cazuri:
|
|
intrebare, dovada, asteptat = desfa(caz)
|
|
vec = cache.get(intrebare)
|
|
if vec is None:
|
|
print(f" embed: {intrebare[:50]}…", file=sys.stderr)
|
|
vec = consumer.embed(intrebare)
|
|
cache[intrebare] = vec
|
|
nou += 1
|
|
cos = [consumer.cosine(vec, e["embedding"]) for e in index.entries]
|
|
bm = index.bm25.scores(intrebare)
|
|
ordine = rank.rank(cos, bm, rank.by_codes(intrebare, index.texts))[
|
|
: config.get_int("TOP_K", 3)]
|
|
date.append({
|
|
"intrebare": intrebare,
|
|
"dovada": dovada,
|
|
"asteptat": asteptat,
|
|
# ca in consumer.search: cosinusul chunk-urilor date modelului
|
|
"best": max((cos[i] for i in ordine), default=0.0),
|
|
"top_texts": [index.texts[i] for i in ordine],
|
|
"top_surse": [index.sources[i] for i in ordine],
|
|
})
|
|
if nou:
|
|
CACHE.write_text(json.dumps(cache), encoding="utf-8")
|
|
print(f" ({nou} embeddings noi puse in cache)", file=sys.stderr)
|
|
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,
|
|
min_ratio: float, dovada_separata: bool = False) -> tuple[int, list[str]]:
|
|
corecte, gresite = 0, []
|
|
for d in date:
|
|
extra = {"dovada": d["dovada"]} if dovada_separata else {}
|
|
ok, _ = rank.assess(
|
|
d["intrebare"], d["best"], d["top_texts"], index.bm25,
|
|
weak=weak, min_ratio=min_ratio, **extra,
|
|
)
|
|
if ok == d["asteptat"]:
|
|
corecte += 1
|
|
else:
|
|
gresite.append(f"{'ar fi raspuns' if ok else 'ar fi escaladat'}: {d['intrebare']}")
|
|
return corecte, gresite
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--sweep", action="store_true", help="matura grila de praguri")
|
|
ap.add_argument("--cases", help="JSON cu [[intrebare, true/false], ...]")
|
|
args = ap.parse_args()
|
|
|
|
cazuri = CAZURI
|
|
if args.cases:
|
|
cazuri = [tuple(c) for c in json.loads(pathlib.Path(args.cases).read_text())]
|
|
|
|
index, date = pregateste(cazuri)
|
|
print(f"index: {len(index)} chunk-uri | cazuri: {len(date)}\n")
|
|
|
|
if args.sweep:
|
|
rezultate = []
|
|
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, ARE_DOVADA)
|
|
rezultate.append((corecte, weak, ratio))
|
|
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")
|
|
for corecte, weak, ratio in rezultate[:15]:
|
|
print(f"{corecte:>5}/{len(date)} {weak:.2f} {ratio:.2f}")
|
|
return
|
|
|
|
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}")
|
|
for eticheta, separata in (("fara separare (azi)", False), ("cu dovada separata ", True)):
|
|
if separata and not ARE_DOVADA:
|
|
print("\ncu dovada separata : rank.assess nu accepta inca `dovada`, sarit")
|
|
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__":
|
|
main()
|