Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_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

219 lines
8.1 KiB
Python

"""Ordonarea hibrida si decizia de escaladare.
Fara retea: embeddings-urile sunt inlocuite cu scoruri cosinus date direct, ca sa
se testeze fuziunea si pragurile, nu Ollama.
"""
from __future__ import annotations
import rank
DOCS = [
"mesaj eroare: ORA-01722 invalid number. rezolvare: verifica formatul coloanei numerice",
"mesaj eroare: token efactura expirat. rezolvare: reautorizeaza aplicatia in SPV",
"cum se trimite declaratia D406 SAF-T catre ANAF din meniul Rapoarte",
"instructiuni pentru inregistrarea unei facturi de achizitie in modulul Facturare",
]
def test_tokenize_pastreaza_codurile_intregi():
assert "ora-01722" in rank.tokenize("Imi da ORA-01722 la salvare")
# diacriticele nu trebuie sa rupa potrivirea
assert rank.tokenize("factură") == rank.tokenize("factura")
def test_tokenize_scoate_cuvintele_de_umplutura():
assert rank.tokenize("cum se salveaza si de ce nu") == ["salveaza"]
def test_bm25_gaseste_codul_de_eroare():
bm = rank.Bm25(DOCS)
scoruri = bm.scores("ORA-01722")
assert scoruri[0] > 0
assert max(scoruri) == scoruri[0]
def test_bm25_zero_cand_nu_exista_niciun_token_comun():
bm = rank.Bm25(DOCS)
assert all(s == 0 for s in bm.scores("uleiul de motor al masinii"))
def test_rare_terms_prinde_codurile_si_ignora_cuvintele_comune():
bm = rank.Bm25(DOCS)
rare = bm.rare_terms("ORA-01722 la factura")
assert "ora-01722" in rare
def test_assess_gaseste_termenul_la_alta_forma_gramaticala():
# Documentul scrie „token"/„tokenuri", omul scrie „tokenul". Comparat pe cuvantul
# intreg, singurul cuvant care conteaza nu se gaseste si intrebarea escaladeaza.
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("imi zice ca tokenul a expirat", 0.64, DOCS[1:2], bm)
assert covered
assert "tokenul" in motiv
def test_assess_codurile_se_compara_intregi_nu_pe_radacina():
# „ora-01722" si „ora-01720" au aceleasi cinci litere de la inceput.
bm = rank.Bm25(DOCS)
covered, _ = rank.assess("eroarea ORA-01720", 0.64, DOCS[:1], bm)
assert not covered
def test_assess_dovada_judeca_mesajul_nou_nu_ancora():
# Continuare pe un fir: ancora e salutul plus reclamatia vaga, mesajul nou e
# chiar intrebarea. Fara dovada separata, „buna/august" (care nu apar nicaieri)
# trag raportul sub prag si escaladeaza degeaba.
bm = rank.Bm25(DOCS)
ancora = "Buna ziua . Am o problema pe luna august la firma AUTO SULEA . Ma ajutati?"
query = f"{ancora}\ntokenul a expirat"
fara, _ = rank.assess(query, 0.64, DOCS[1:2], bm)
cu, motiv = rank.assess(query, 0.64, DOCS[1:2], bm, dovada="tokenul a expirat")
assert not fara
assert cu and "expirat" in motiv
def test_assess_continuare_scurta_cade_inapoi_pe_interogarea_intreaga():
# „da, ma blocheaza complet" n-are termeni distinctivi proprii. Judecata singura
# ar escalada orice continuare scurta — exact regresia pentru care exista ancora.
bm = rank.Bm25(DOCS)
query = "token efactura expirat\nda"
covered, _ = rank.assess(query, 0.64, DOCS[1:2], bm, dovada="da")
fara, _ = rank.assess(query, 0.64, DOCS[1:2], bm)
assert covered is fara is True
def test_rank_urca_chunkul_confirmat_de_ambele_metode():
# cosinusul favorizeaza documentul 3, dar lexical se potriveste documentul 0;
# documentul 0 e sustinut de amandoua (cosinus decent + BM25 mare) si iese primul
cos = [0.62, 0.30, 0.31, 0.64]
bm = [8.0, 0.0, 0.0, 0.0]
assert rank.rank(cos, bm)[0] == 0
def test_rank_fara_potrivire_lexicala_ramane_ordinea_semantica():
cos = [0.30, 0.40, 0.80, 0.20]
assert rank.rank(cos, [0.0, 0.0, 0.0, 0.0])[0] == 2
def test_assess_cosinus_mare_raspunde():
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("token efactura expirat", 0.77, DOCS[1:2], bm)
assert covered
assert "0.770" in motiv or "0.77" in motiv
def test_assess_cosinus_mic_escaladeaza():
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("cum schimb uleiul la masina", 0.53, DOCS[:1], bm)
assert not covered
assert "0.53" in motiv
def test_assess_zona_de_mijloc_salvata_de_codul_de_eroare():
# Exact cazul pentru care exista partea lexicala: cosinusul e mediocru, dar
# codul din intrebare apare textual in chunk.
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("ORA-01722", 0.61, DOCS[:1], bm)
assert covered
assert "ora-01722" in motiv
def test_assess_zona_de_mijloc_fara_termeni_in_context_escaladeaza():
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("ORA-99999", 0.61, DOCS[:1], bm)
assert not covered
assert "nu apar in documente" in motiv
def test_assess_un_singur_termen_din_multi_nu_e_dovada():
# Cazul care a scos la iveala nevoia de fractiune: „resetez parola de la Windows"
# era salvata gresit doar fiindca „parola" apare in documente. Un termen din
# trei nu inseamna ca intrebarea e acoperita.
bm = rank.Bm25(DOCS + ["reautorizeaza aplicatia si schimba parola in SPV"])
covered, motiv = rank.assess("cum resetez parola de la Windows", 0.64,
["reautorizeaza aplicatia si schimba parola in SPV"], bm)
assert not covered
assert "windows" in motiv
def test_assess_motivul_numeste_termenii_lipsa_nu_pe_toti():
# Fara cod in intrebare: acolo decid termenii rari, nu regula codurilor.
bm = rank.Bm25(DOCS)
_, motiv = rank.assess("factura la imprimanta LaserJet", 0.61, DOCS[3:], bm)
# „factura" chiar apare in context, deci nu are ce cauta in lista lipsurilor
assert "factura," not in motiv
assert "imprimanta" in motiv
def test_assess_intrebare_vaga_in_zona_de_mijloc_escaladeaza():
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("nu merge", 0.61, DOCS[:1], bm)
assert not covered
assert "distinctivi" in motiv
def test_pragurile_vin_din_env(monkeypatch):
import config
bm = rank.Bm25(DOCS)
monkeypatch.setitem(config._env, "RANK_WEAK_COSINE", "0.90")
covered, motiv = rank.assess("factura", 0.85, DOCS[:1], bm)
assert not covered and "0.9" in motiv
def test_cosinusul_mare_singur_nu_mai_e_acoperire():
"""Interogarea creste pe fir si cosinusul cu ea; dovada ramane cea lexicala.
Cazul real: aceeasi captura a dat 0,778 singura si 0,836 cu mesajul de
dinainte, fara sa apara vreo dovada noua in documente.
"""
bm = rank.Bm25(DOCS)
covered, motiv = rank.assess("cumparator incorect la incarcare", 0.84, DOCS[:1], bm)
assert not covered
assert "cumparator" in motiv
# --- coduri de eroare: identificatori exacti, nu cuvinte ---------------------
def test_codul_lipit_de_ocr_e_tot_gasit():
"""OCR-ul da „[Oracle][ODBC][OraJORA-06550"; codul nu are voie sa se piarda."""
import rank
assert rank.codes("[Oracle][ODBC][OraJORA-06550: line 1, column 7") == ["ora-06550"]
assert rank.codes("PLS-00906: object X is invalid") == ["pls-00906"]
assert rank.codes("cum trimit D406") == ["d406"]
assert rank.codes("nu merge nimic") == []
def test_chunkul_cu_codul_cerut_urca_in_clasament():
import rank
texts = [
"ORA-12541 TNS no listener, serverul nu raspunde",
"ORA-03113 end-of-file on communication channel",
"ORA-06550 eroare de compilare, procedura lipseste",
]
assert rank.by_codes("imi da ORA-06550 la salvare", texts) == [2]
assert rank.by_codes("intrebare fara coduri", texts) == []
def test_cosinus_mare_nu_acopera_alt_cod(monkeypatch):
"""Doua erori Oracle diferite se scriu la fel; cosinusul nu le deosebeste.
Cazul real din 2026-08-31: captura cu ORA-06550 primea raspunsul despre
ORA-12541, cu cosinus 0,736 — peste pragul „sigur".
"""
import rank
bm = rank.Bm25(["ORA-12541 TNS no listener"])
ok, motiv = rank.assess("ORA-06550 PLS-00906 la salvare", 0.736,
["ORA-12541 TNS no listener"], bm)
assert ok is False
assert "ora-06550" in motiv
def test_codul_gasit_acopera_si_la_cosinus_mic():
import rank
bm = rank.Bm25(["ORA-06550 eroare de compilare"])
ok, motiv = rank.assess("ORA-06550", 0.42, ["ORA-06550 eroare de compilare"], bm)
assert ok is True
assert "ora-06550" in motiv