Doua greseli din grupul "Maria Test", 2026-09-01, pe care 27409fb le-a atenuat dar
nu le-a rezolvat.
1. La o reclamatie vaga Maria cauta, in loc sa intrebe. "Buna . Am si eu o factura
pe luna august cu eroare in spv la trimitere AUTO SULE" nu spune CE eroare e —
n-are ce cauta in documente si n-are ce trimite la suport, fiindca programatorul
ar pune exact aceeasi intrebare. Gardul (triaj.prea_vag) exista, dar cerea text
sub 12 cuvinte; mesajul are 14. Gresea si invers: "nu pot incarca factura in SPV,
imi da eroare de certificat" are 9 cuvinte si ARE raspuns in documente, si era
oprita degeaba. Lungimea nu masoara cat de precis e mesajul.
-> gardul nu mai numara cuvinte si se aplica DUPA cautare, doar cand nu exista
acoperire. O singura data pe fir: daca nici intrebat omul nu spune mai mult,
mesajul pleaca la suport.
2. Textul si captura, trimise una dupa alta, erau tratate ca doua probleme fara
legatura. `este_continuare` rupea firul la ORICE imagine — dar cazul frecvent e
tocmai omul care scrie problema si trimite captura imediat dupa, sau care
raspunde la "trimite-mi o captura". Textul se pierdea, captura se cauta doar pe
OCR-ul ei, se deschideau doua fire si se puteau deschide doua escaladari pentru
aceeasi problema.
-> o captura in primele FIR_IMAGINE_MIN (5) minute continua firul: textul citit
din ea intra in ancora (cu tot cu coduri), cautarea se face pe mesaj +
captura, iar pe o escaladare deschisa pleaca la aceeasi referinta — cu
imaginea, nu doar cu textul citit din ea (_notifica_suport ia si media).
Si, ca urmare a lui (2): RANK_STRONG_COSINE dispare. Interogarea pe un fir e ancora
plus mesajul nou, deci cosinusul urca la fiecare replica fara sa apara vreo dovada
noua — aceeasi captura da 0,778 singura si 0,836 cu mesajul de dinainte, adica peste
0,80 pus ieri. Orice prag fix de sus e trecut de o discutie destul de lunga. Acoperirea
ramane pe dovada lexicala (termenii distinctivi ai intrebarii chiar in chunk-uri),
care e stabila la lungime. Niciun caz cu raspuns in documente nu avea nevoie de
scurtatura.
ops/calibrate-rank.py: 26/26, cu interogarea combinata adaugata la set. Teste: 102 pass.
Verificat end-to-end pe scenariul real (mesaj, apoi captura la 10 secunde): intrebare
de detalii, apoi o singura escaladare care poarta si textul si captura.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
182 lines
7.7 KiB
Python
Executable File
182 lines
7.7 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),
|
|
# 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),
|
|
]
|
|
|
|
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], 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,
|
|
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 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)
|
|
rezultate.append((corecte, weak, ratio))
|
|
rezultate.sort(key=lambda r: (-r[0], r[1]))
|
|
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}")
|
|
corecte, gresite = evalueaza(index, date, weak, ratio)
|
|
print(f"\n{corecte}/{len(date)} corecte")
|
|
for g in gresite:
|
|
print(f" - {g}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|