Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py
Claude Agent 653f5cbba9 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
2026-09-01 19:57:49 +00:00

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()