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
180 lines
6.3 KiB
Python
180 lines
6.3 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_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
|