Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py
Claude Agent 45b36591c9 fix(maria): pragul de acoperire la 0,80 si temperatura 0 — doua raspunsuri inventate
Grupul "Maria Test", 2026-09-01, ambele mesaje au primit raspunsuri inventate:

- "am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULE"
  (cosinus 0,768) — mesajul nu spune CE eroare e; Maria a explicat cum se reface
  o factura stearsa si a trimis omul sa verifice drepturile pe CIF in SPV.
- captura cu `<Error errorMessage="CUI cumparator incorect"/>` (0,778) — eroarea
  nu exista in niciun document; Maria a inventat o procedura cu "ID de descarcare",
  Borderou eFactura si retrimitere manuala.

Cauza: `RANK_STRONG_COSINE=0,70` inseamna "cosinusul singur e destul", dar toate
chunk-urile eFactura seamana intre ele — la 0,7x cosinusul nu mai distinge "e in
documente" de "e despre eFactura". Aceeasi greseala ca la codurile Oracle
(ORA-06550 vs ORA-12541, b7d75f0), doar ca fara cod pe care sa te sprijini.

- RANK_STRONG_COSINE 0,70 -> 0,80: peste atat trec doar potrivirile aproape
  textuale, restul cad pe dovada lexicala (termenii rari ai intrebarii chiar in
  chunk-uri). Ambele mesaje escaladeaza acum la suport.
- LLM_TEMPERATURE=0 la apelul modelului. Fara el llama.cpp foloseste 0,8 —
  creativitate exact acolo unde vrem doar ce scrie in context.
- Cele doua cazuri reale sunt adaugate in ops/calibrate-rank.py: 25/25.

Teste: 99 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-09-01 19:44:24 +00:00

181 lines
7.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),
# 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),
("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()