Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_mesaje_cu_imagine.py
Claude Agent ca6b53e0ca feat(maria): rank hibrid, escaladare la suport si dictionar de erori Oracle
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
2026-08-31 22:08:50 +00:00

114 lines
4.2 KiB
Python

"""`consumer.prepare_query`: ce ajunge la model cand mesajul e o captura de ecran.
Nu porneste nici puntea, nici LLM-ul — se testeaza doar decizia dintre „raspund
pe baza imaginii", „raspund pe baza legendei" si „cer omului sa scrie textul".
"""
from __future__ import annotations
import pytest
import consumer
@pytest.fixture
def imagine(tmp_path, monkeypatch):
"""Un mesaj cu imagine, cu OCR-ul inlocuit de un text dat."""
def _face(ocr_text: str, caption: str = "", *, esueaza: bool = False):
fisier = tmp_path / "captura.png"
fisier.write_bytes(b"nu conteaza continutul, ocr e inlocuit")
def fals_run(path, langs=None):
if esueaza:
raise RuntimeError("tesseract lipseste")
return ocr_text
monkeypatch.setattr(consumer.ocr, "run", fals_run)
return {"text": caption, "media": {"path": str(fisier), "mimetype": "image/png"}}, fisier
return _face
def test_mesaj_text_simplu_trece_neatins():
intrebare, cautare, imediat = consumer.prepare_query({"text": "cum emit o factura?"})
assert intrebare == "cum emit o factura?"
assert cautare == "cum emit o factura?"
assert imediat is None
def test_captura_cu_eroare_cauta_doar_linia_de_eroare(imagine):
msg, _ = imagine("Meniu Fisier Editare\nORA-01722: invalid number\nRand 4 din 9")
intrebare, cautare, imediat = consumer.prepare_query(msg)
assert imediat is None
# cautarea in index merge pe eroare, nu pe toata fereastra
assert cautare == "ORA-01722: invalid number"
# modelului ii dam captura intreaga, marcata ca OCR
assert "OCR" in intrebare
assert "Meniu Fisier Editare" in intrebare
def test_legenda_intra_si_in_intrebare_si_in_cautare(imagine):
msg, _ = imagine("ORA-00942: table or view does not exist", caption="nu merge raportul")
intrebare, cautare, _ = consumer.prepare_query(msg)
assert intrebare.startswith("nu merge raportul")
assert cautare.splitlines()[0] == "nu merge raportul"
def test_captura_ramane_pe_disc_pana_e_tratat_mesajul(imagine):
# Nu se sterge la OCR: mai e nevoie de ea daca intrebarea pleaca la suport,
# unde se retrimite ca imagine.
msg, fisier = imagine("ORA-01722: invalid number")
consumer.prepare_query(msg)
assert fisier.exists()
def test_cleanup_media_sterge_captura(imagine):
# capturile pot contine date de client — nu raman pe disc dupa tratarea mesajului
msg, fisier = imagine("ORA-01722: invalid number")
consumer.prepare_query(msg)
consumer.cleanup_media(msg)
assert not fisier.exists()
def test_cleanup_media_tolereaza_lipsa_fisierului(imagine):
msg, fisier = imagine("ceva")
fisier.unlink()
consumer.cleanup_media(msg) # nu trebuie sa arunce
consumer.cleanup_media({"text": "fara imagine"})
def test_captura_ilizibila_cere_textul_erorii(imagine):
msg, _ = imagine("")
intrebare, _, imediat = consumer.prepare_query(msg)
assert intrebare is None
assert imediat == consumer.NO_TEXT_IN_IMAGE
def test_textul_ocr_ramane_pe_mesaj_pentru_jurnal(imagine):
# escalate() il scrie in jurnalul de escaladari; fara el, o intrebare trimisa
# la suport ar ajunge acolo fara continutul capturii
msg, _ = imagine("ORA-01722: invalid number")
consumer.prepare_query(msg)
assert msg["ocr_text"] == "ORA-01722: invalid number"
def test_ocr_esuat_dar_cu_legenda_raspunde_pe_legenda(imagine):
msg, _ = imagine("", caption="imi da eroare la salvare", esueaza=True)
intrebare, cautare, imediat = consumer.prepare_query(msg)
assert imediat is None
assert intrebare == cautare == "imi da eroare la salvare"
def test_imagine_nedescarcata_fara_legenda_cere_reincercare():
msg = {"text": "", "media": {"error": "imagine prea mare (12.0 MB > 8 MB)"}}
intrebare, _, imediat = consumer.prepare_query(msg)
assert intrebare is None
assert "scrie-mi" in imediat.lower()
def test_imagine_nedescarcata_cu_legenda_raspunde_pe_legenda():
msg = {"text": "eroare la export SAF-T", "media": {"error": "download failed"}}
intrebare, cautare, imediat = consumer.prepare_query(msg)
assert imediat is None
assert intrebare == cautare == "eroare la export SAF-T"