Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py
Claude Agent b7d75f05ee fix(maria): codul de eroare decide acoperirea, nu cosinusul
O captura cu ORA-06550 / PLS-00906 primea raspunsul despre ORA-12541 (TNS no
listener) si nu pleca la suport. Cauza: doua erori Oracle diferite se scriu
aproape la fel ("[Oracle][ODBC][Ora]ORA-..."), asa ca chunk-ul gresit a iesit la
cosinus 0,736 — peste RANK_STRONG_COSINE, care scurtcircuita orice alta dovada.
Chunk-ul corect din dictionar exista, dar statea pe pozitia 4, in afara TOP_K:
OCR-ul unei ferestre intregi aduce zeci de tokeni de zgomot, iar BM25 ineca tocmai
codul.

- `rank.codes()` scoate codurile de oriunde din text, nu doar ca token intreg —
  OCR-ul le lipeste de ce e langa ("OraJORA-06550").
- `rank.by_codes()` e un al treilea clasament in fuziune: chunk-ul care poarta
  codul cerut urca peste cele care doar seamana. Acum iese primul.
- `rank.assess()`: cand intrebarea are coduri, decid doar ele. Niciun cod gasit =
  escaladare la suport, oricat de mare ar fi cosinusul.
- `consumer.search()` judeca acoperirea pe cosinusul chunk-urilor date chiar
  modelului, nu pe cel mai bun din tot indexul.

Calibrare 21/21 (cele 19 cazuri + captura reala + ORA-00600, cod care nu e in
documente si trebuie sa plece la suport). Teste: 79 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-08-31 22:25:45 +00:00

168 lines
6.6 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 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?)
CAZURI: list[tuple[str, bool]] = [
("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),
("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),
]
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 pregateste(cazuri: list[tuple[str, bool]]) -> 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 intrebare, asteptat in cazuri:
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,
"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
def evalueaza(index: consumer.Index, date: list[dict], strong: float, weak: float,
min_ratio: float) -> tuple[int, list[str]]:
corecte, gresite = 0, []
for d in date:
ok, _ = rank.assess(
d["intrebare"], d["best"], d["top_texts"], index.bm25,
strong=strong, weak=weak, min_ratio=min_ratio,
)
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 = [(q, bool(a)) for q, a 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 strong in (0.62, 0.66, 0.68, 0.70, 0.72, 0.75):
for weak in (0.54, 0.58, 0.60):
for ratio in (0.0, 0.34, 0.5, 0.67, 1.0):
if weak >= strong:
continue
corecte, _ = evalueaza(index, date, strong, weak, ratio)
rezultate.append((corecte, strong, weak, ratio))
rezultate.sort(key=lambda r: (-r[0], r[1]))
print(f"{'corecte':>8} strong weak ratio")
for corecte, strong, weak, ratio in rezultate[:15]:
print(f"{corecte:>5}/{len(date)} {strong:.2f} {weak:.2f} {ratio:.2f}")
return
strong, weak = rank.strong_cosine(), rank.weak_cosine()
ratio = rank.min_rare_ratio()
print(f"praguri curente: strong={strong} 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}")
corecte, gresite = evalueaza(index, date, strong, weak, ratio)
print(f"\n{corecte}/{len(date)} corecte")
for g in gresite:
print(f" - {g}")
if __name__ == "__main__":
main()