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
This commit is contained in:
@@ -167,6 +167,36 @@ def maria_documents() -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def maria_escalations(limit: int = 12) -> list[dict]:
|
||||
"""Intrebarile pe care Maria le-a trimis la suport, cele mai noi primele.
|
||||
|
||||
Sursa: fisierele scrise de `rag/consumer.py:escalate`. Se citesc de pe disc la
|
||||
fiecare cerere — sunt putine si e mai bine decat inca o stare de tinut sincron.
|
||||
"""
|
||||
d = MARIA_STATE_DIR / "escalations"
|
||||
if not d.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for f in sorted(d.glob("*.json"), reverse=True)[:limit]:
|
||||
try:
|
||||
rec = json.loads(f.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
out.append({
|
||||
"ref": rec.get("ref"),
|
||||
"at": rec.get("at"),
|
||||
"from": rec.get("push_name") or rec.get("from"),
|
||||
"reason": rec.get("reason"),
|
||||
"best_cosine": rec.get("best_cosine"),
|
||||
"had_image": rec.get("had_image"),
|
||||
"notified": rec.get("notified"),
|
||||
"notify_error": rec.get("notify_error"),
|
||||
# textul citit din captura, daca a fost una: exact ce a "vazut" Maria
|
||||
"text": (rec.get("ocr_text") or rec.get("text") or "")[:600],
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _maria_validate_name(name: str) -> str:
|
||||
if not name or not _MARIA_SAFE_NAME.match(name) or ".." in name or "/" in name:
|
||||
raise ValueError(f"nume de document invalid: {name!r}")
|
||||
@@ -696,9 +726,12 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
"index": maria_index_info(),
|
||||
"documents": len(maria_documents()),
|
||||
"sync": {**maria_sync_state(), "drive_remote": maria_get("DRIVE_REMOTE") or None},
|
||||
"support_jid": maria_get("SUPPORT_JID") or None,
|
||||
})
|
||||
if path == "/api/maria/documents":
|
||||
return self.send_json({"documents": maria_documents()})
|
||||
if path == "/api/maria/escalations":
|
||||
return self.send_json({"escalations": maria_escalations()})
|
||||
if path == "/api/maria/logs":
|
||||
try:
|
||||
n = min(max(int(qs.get("lines", ["200"])[0]), 1), 2000)
|
||||
|
||||
@@ -114,6 +114,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Trimise la suport <span class="count" id="mEscCount"></span></h2>
|
||||
<div class="detail" id="mSupportDetail"></div>
|
||||
<div id="mEsc" class="empty">se încarcă…</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Documente indexate <span class="count" id="mDocCount"></span></h2>
|
||||
<div class="detail" id="mIndexDetail"></div>
|
||||
@@ -434,6 +440,26 @@ async function mRefresh() {
|
||||
? 'Drive: ' + sync.drive_remote + (sync.synced_at ? ' · ultima sincronizare ' + new Date(sync.synced_at * 1000).toLocaleString('ro-RO') : ' · încă nesincronizat')
|
||||
: 'Sincronizare Drive dezactivată (DRIVE_REMOTE nesetat) — doar upload manual';
|
||||
|
||||
document.getElementById('mSupportDetail').textContent = s.support_jid
|
||||
? 'Canal de suport: ' + s.support_jid
|
||||
: 'SUPPORT_JID nesetat — escaladările se scriu doar în jurnal, nu anunță pe nimeni';
|
||||
|
||||
var escRes = await api('api/maria/escalations');
|
||||
var escaladari = escRes.ok ? (escRes.data.escalations || []) : [];
|
||||
document.getElementById('mEscCount').textContent = escaladari.length ? escaladari.length + ' recente' : '';
|
||||
document.getElementById('mEsc').className = escaladari.length ? '' : 'empty';
|
||||
document.getElementById('mEsc').innerHTML = escaladari.length ? escaladari.map(function (e) {
|
||||
var stare = e.notified
|
||||
? '<span class="pill ok">trimis</span>'
|
||||
: '<span class="pill bad">netrimis' + (e.notify_error ? ' — ' + esc(e.notify_error) : '') + '</span>';
|
||||
return '<div style="border-top:1px solid var(--border);padding:var(--space-4) 0">'
|
||||
+ '<div>' + stare + ' <code>' + esc(e.ref || '') + '</code> <strong>' + esc(e.from || '?') + '</strong> '
|
||||
+ '<span class="muted">' + (e.at ? new Date(e.at * 1000).toLocaleString('ro-RO') : '') + '</span>'
|
||||
+ (e.had_image ? ' <span class="muted">· captură</span>' : '') + '</div>'
|
||||
+ '<pre style="white-space:pre-wrap;margin:var(--space-3) 0;font-size:.85em">' + esc(e.text || '(fără text)') + '</pre>'
|
||||
+ '<div class="muted">' + esc(e.reason || '') + '</div></div>';
|
||||
}).join('') : 'nicio întrebare trimisă la suport';
|
||||
|
||||
var docsRes = await api('api/maria/documents');
|
||||
var docs = docsRes.ok ? (docsRes.data.documents || []) : [];
|
||||
var indexate = docs.filter(function (d) { return !d.shadowed_by; }).length;
|
||||
|
||||
Reference in New Issue
Block a user