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
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""Escaladarea la suport: jurnalul si notificarea.
|
|
|
|
Nu atinge reteaua — `requests.post` e inlocuit si se verifica CE s-ar fi trimis.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
import config
|
|
import consumer
|
|
|
|
|
|
@pytest.fixture
|
|
def apeluri(monkeypatch):
|
|
"""Prinde apelurile HTTP catre punte, fara sa le execute."""
|
|
prinse = []
|
|
|
|
class Raspuns:
|
|
status_code = 200
|
|
|
|
def raise_for_status(self):
|
|
pass
|
|
|
|
def fals_post(url, json=None, timeout=None):
|
|
prinse.append((url, json))
|
|
return Raspuns()
|
|
|
|
monkeypatch.setattr(consumer.requests, "post", fals_post)
|
|
return prinse
|
|
|
|
|
|
@pytest.fixture
|
|
def rezultat_fara_acoperire():
|
|
return {
|
|
"chunks": [], "covered": False, "best_cosine": 0.55,
|
|
"reason": "cosinus 0.550 < 0.58",
|
|
"top": [{"source": "efactura_knowledge.md", "cosine": 0.55, "bm25": 0.0}],
|
|
}
|
|
|
|
|
|
def _mesaj(**kw):
|
|
baza = {"from": "40712345678@s.whatsapp.net", "pushName": "Ion",
|
|
"id": "ABC123", "text": "cum schimb uleiul la masina"}
|
|
baza.update(kw)
|
|
return baza
|
|
|
|
|
|
def test_jurnalul_se_scrie_si_fara_canal_de_suport(apeluri, rezultat_fara_acoperire, tmp_path):
|
|
# SUPPORT_JID nesetat: nu notificam pe nimeni, dar intrebarea nu are voie sa
|
|
# dispara fara urma — asta e chiar problema pe care escaladarea o rezolva.
|
|
consumer.escalate(_mesaj(), "cum schimb uleiul la masina", rezultat_fara_acoperire)
|
|
|
|
fisiere = list((config.STATE_DIR / "escalations").glob("*.json"))
|
|
assert len(fisiere) == 1
|
|
inregistrare = json.loads(fisiere[0].read_text(encoding="utf-8"))
|
|
assert inregistrare["notified"] is False
|
|
assert inregistrare["reason"] == "cosinus 0.550 < 0.58"
|
|
assert inregistrare["best_cosine"] == 0.55
|
|
assert not apeluri
|
|
|
|
|
|
def test_referinta_e_stabila_pentru_acelasi_mesaj():
|
|
# Se poate cita la telefon si nu se schimba daca escaladarea se reia.
|
|
a = consumer.reference(1788212023.0, "ABC123")
|
|
b = consumer.reference(1788212023.0, "ABC123")
|
|
assert a == b
|
|
assert a.startswith("M-")
|
|
assert consumer.reference(1788212023.0, "ALT") != a
|
|
|
|
|
|
def test_referinta_ajunge_si_la_suport_si_in_jurnal(apeluri, rezultat_fara_acoperire, monkeypatch):
|
|
monkeypatch.setitem(config._env, "SUPPORT_JID", "40799999999@s.whatsapp.net")
|
|
inreg = consumer.escalate(_mesaj(), "intrebare", rezultat_fara_acoperire)
|
|
_, corp = apeluri[0]
|
|
assert inreg["ref"] in corp["text"]
|
|
pe_disc = json.loads(next((config.STATE_DIR / "escalations").glob("*.json")).read_text())
|
|
assert pe_disc["ref"] == inreg["ref"]
|
|
|
|
|
|
def test_notifica_suportul_ca_text(apeluri, rezultat_fara_acoperire, monkeypatch):
|
|
monkeypatch.setitem(config._env, "SUPPORT_JID", "40799999999@s.whatsapp.net")
|
|
consumer.escalate(_mesaj(), "cum schimb uleiul la masina", rezultat_fara_acoperire)
|
|
|
|
url, corp = apeluri[0]
|
|
assert url.endswith("/send")
|
|
assert corp["to"] == "40799999999@s.whatsapp.net"
|
|
assert "Ion" in corp["text"]
|
|
assert "cum schimb uleiul" in corp["text"]
|
|
|
|
inregistrare = json.loads(next((config.STATE_DIR / "escalations").glob("*.json")).read_text())
|
|
assert inregistrare["notified"] is True
|
|
|
|
|
|
def test_captura_pleaca_la_suport_ca_imagine(apeluri, rezultat_fara_acoperire, monkeypatch, tmp_path):
|
|
monkeypatch.setitem(config._env, "SUPPORT_JID", "40799999999@s.whatsapp.net")
|
|
captura = tmp_path / "captura.png"
|
|
captura.write_bytes(b"png")
|
|
msg = _mesaj(text="", media={"path": str(captura)}, ocr_text="Eroare necunoscuta XYZ")
|
|
|
|
consumer.escalate(msg, "intrebare", rezultat_fara_acoperire)
|
|
|
|
url, corp = apeluri[0]
|
|
assert url.endswith("/send-image")
|
|
assert corp["path"] == str(captura)
|
|
assert "Eroare necunoscuta XYZ" in corp["caption"]
|
|
|
|
inregistrare = json.loads(next((config.STATE_DIR / "escalations").glob("*.json")).read_text())
|
|
assert inregistrare["had_image"] is True
|
|
assert inregistrare["ocr_text"] == "Eroare necunoscuta XYZ"
|
|
|
|
|
|
def test_notificarea_esuata_nu_pierde_jurnalul(rezultat_fara_acoperire, monkeypatch):
|
|
monkeypatch.setitem(config._env, "SUPPORT_JID", "40799999999@s.whatsapp.net")
|
|
|
|
def cade(*a, **kw):
|
|
raise RuntimeError("puntea nu raspunde")
|
|
|
|
monkeypatch.setattr(consumer.requests, "post", cade)
|
|
consumer.escalate(_mesaj(), "intrebare", rezultat_fara_acoperire)
|
|
|
|
inregistrare = json.loads(next((config.STATE_DIR / "escalations").glob("*.json")).read_text())
|
|
assert inregistrare["notified"] is False
|
|
assert "puntea nu raspunde" in inregistrare["notify_error"]
|