Trei probleme legate, gasite testand cu capturi reale. 1. RASPUNSURI GENERICE. Cand raspunsul nu era in documente, modelul primea oricum top-3 chunk-uri si compunea ceva plauzibil — utilizatorul pleca cu impresia ca a primit ajutor. Acum, daca nu avem acoperire, modelul nu mai e intrebat deloc: intrebarea pleaca la suport, cu referinta (M-260831-A1B2) si confirmare onesta — "am trimis" doar daca notificarea chiar a plecat, altfel "am inregistrat, dar nu am putut trimite". Jurnalul se scrie intotdeauna, in ~/.maria-bridge/escalations/, si se vede in dashboard. Daca a fost o captura, imaginea insasi se retrimite la suport (endpoint nou /send-image, limitat la MEDIA_DIR). 2. ORDONARE (rank.py). Un prag pe cosinus nu putea decide "avem raspunsul?": masurat pe indexul viu, intrebarile bune dau 0,600-0,816 si cele straine 0,534-0,685 — intervale care SE SUPRAPUN. Lipseau codurile: ORA-01722, D406, CIF sunt tokeni exacti pe care cautarea semantica ii topeste. Acum se ordoneaza de doua ori — embeddings si BM25 — si se fuzioneaza clasamentele (RRF), iar decizia de acoperire se ia pe dovezi: cosinus mare, sau cosinus de mijloc cu o fractiune din termenii distinctivi gasita chiar in chunk-uri. Fractiunea conteaza: cu un singur termen, "cum imi resetez parola de la Windows" trecea drept acoperita fiindca "parola" apare in documente. Pragurile sunt masurate, nu alese: ops/calibrate-rank.py ruleaza un set de cazuri pe indexul real si matura grila. 15/15 la strong=0.70 weak=0.58 ratio=0.5. 3. DICTIONAR DE ERORI ORACLE. 29 de erori uzuale, scrise pentru utilizatorul din fata aplicatiei: ce inseamna, ce poate incerca singur, cand sa sune. Fara nume de servere, IP-uri sau pasi de administrare — Maria e chatbot pentru clienti. Fisierul din git e SAMANTA: depozitul e oglinda Drive-ului, deci trebuie pus in document_store ca sa nu dispara la sincronizare. Pe drum, doua lucruri gasite in log: - fiecare mesaj din self-chat sosea de DOUA ori, pe @s.whatsapp.net si pe @lid, deci Maria raspundea de doua ori. Dedup pe key.id in punte. - reindexarea reface toate embeddings-urile (~7 s fiecare pe CPU): adaugarea unui document la 170 de chunk-uri insemna 20 de minute. Acum refoloseste vectorii chunk-urilor nemodificate din indexul precedent, fara vreun fisier nou de stare. Textul OCR se logheaza acum INTEGRAL, cu interogarea de cautare si sursele alese cu scorurile lor — fara asta un raspuns gresit nu se poate explica: nu stiai daca a citit prost captura sau a cautat prost in documente. 6 fisiere de test noi/extinse, 68 pass (de la 42). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
123 lines
4.2 KiB
Python
123 lines
4.2 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():
|
|
bm = rank.Bm25(DOCS)
|
|
_, motiv = rank.assess("ORA-01722 la imprimanta LaserJet", 0.61, DOCS[:1], bm)
|
|
# codul chiar apare in context, deci nu are ce cauta in lista lipsurilor
|
|
assert "ora-01722" 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_STRONG_COSINE", "0.50")
|
|
covered, _ = rank.assess("orice", 0.55, DOCS[:1], bm)
|
|
assert covered
|