'
+ + '
' + stare + ' ' + esc(e.ref || '') + ' ' + esc(e.from || '?') + ' '
+ + '' + (e.at ? new Date(e.at * 1000).toLocaleString('ro-RO') : '') + ''
+ + (e.had_image ? ' · captură' : '') + '
'
+ + '
' + esc(e.text || '(fără text)') + '
'
+ + '
' + esc(e.reason || '') + '
';
+ }).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;
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
index ff1a956..6a8944b 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
@@ -70,6 +70,9 @@ rag/consumer.py -- polling la /messages, RAG stateless (FARA memorie intre mesa
rag/ocr.py -- capturile de ecran -> text (tesseract), inainte de RAG
|
v
+rag/rank.py -- ordonare hibrida (embeddings + BM25) si decizia
+ | "avem raspunsul in documente?"; daca nu, mesajul pleaca la suport
+ v
rag/store.py -- depozit documente (.txt/.md) in ~/.maria-bridge/documents/
rag/indexer.py -- chunking + embeddings Ollama -> rag_index.json
rag/sync.py -- rclone pull din Google Drive + reindexare conditionata
@@ -187,6 +190,7 @@ puntii Discord — vezi `../discord-bridge/README.md` pentru URL si autentificar
`/claude`). Acolo, sectiunea "Maria — WhatsApp + RAG":
- start/stop/restart pentru puntea WhatsApp si consumer-ul RAG
- starea conexiunii WhatsApp si asocierea (cod QR sau cod de 8 caractere), cand nu e conectat
+- ultimele intrebari trimise la suport, cu textul citit din captura si motivul
- listare, adaugare si stergere documente din depozitul RAG
- reconstruire index manual, sau sincronizare Drive imediata
- ultimele linii din logurile fiecarui serviciu Maria (`whatsapp.log`/`rag.log`)
@@ -245,6 +249,27 @@ ignorate deliberat: nu sunt cunostinte de suport.
dosarului din Drive, nu o colectie care creste. Documentele adaugate manual din
dashboard dispar la prima sincronizare daca nu exista si in Drive.
+### Dictionarul de erori Oracle
+
+`knowledge/oracle-erori-uzuale.xml` — 29 de erori Oracle uzuale (ORA-00001,
+ORA-01722, ORA-12154, ORA-28040, ORA-12954 …), fiecare cu ce inseamna, ce poate
+incerca utilizatorul singur si cand sa sune la suport. A fost scris fiindca
+utilizatorii trimit capturi cu erori Oracle, iar restul documentelor sunt despre
+e-Factura si SAF-T: modelul raspundea „ceva despre Oracle", dar nu despre eroarea
+din captura.
+
+Doua lucruri de stiut:
+
+- **E samanta, nu sursa.** Fisierul din git nu ajunge singur in index. Depozitul e
+ o oglinda a Drive-ului, deci copia pusa direct in `~/.maria-bridge/documents/`
+ **dispare la prima sincronizare**. Ca sa ramana, pune-l in `document_store` din
+ Drive (pe Windows: `D:\GoogleDrive\romfast\document_store`).
+- **E scris pentru clienti, nu pentru administratori.** Nicio rezolvare nu contine
+ nume de servere, IP-uri, porturi sau pasi de administrare — la erorile care chiar
+ se rezolva doar tehnic (ORA-01652 spatiu plin, ORA-28000 cont blocat) textul spune
+ explicit „suna la suport", nu explica ce sa faca pe server. Cand adaugi erori noi,
+ pastreaza regula.
+
### Acelasi document in doua formate
Cand exista `X.xml` si `X.md`, **in index intra doar `.xml`** (ordinea de
@@ -269,14 +294,106 @@ Dupa configurare, `maria-sync.timer` trage la fiecare 10 minute; reindexarea
ruleaza DOAR daca s-a schimbat efectiv ceva in depozit (amprenta pe nume +
mtime + marime, vezi `rag/sync.py`), ca sa nu reface embeddings degeaba.
+Iar cand chiar reindexeaza, **refoloseste vectorii chunk-urilor nemodificate** din
+indexul precedent (`rag/indexer.py:_vectori_existenti`). Un embedding costa ~7
+secunde pe CPU: fara refolosire, adaugarea unui singur document la 170 de chunk-uri
+insemna 20 de minute de reconstruit tot. Cheia e chiar textul chunk-ului — daca nu
+s-a schimbat niciun caracter, vectorul e acelasi. Logul spune de fiecare data cate
+au fost calculate si cate refolosite.
+
## Teste
```bash
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/maria-whatsapp-bridge
-python3 -m pytest # preferinta de format, taierea XML, OCR si mesajele cu imagine
- # — fara retea, fara Ollama si fara tesseract
+python3 -m pytest # format, taierea XML, OCR, mesajele cu imagine, ordonarea
+ # hibrida si escaladarea — fara retea, fara Ollama, fara tesseract
```
+## Cum se ordoneaza rezultatele (rank)
+
+Cautarea are doua etape. Intai **recall**: primele `RANK_RECALL_N` chunk-uri dupa
+cosinus (semantic) si primele dupa BM25 (lexical). Apoi **fuziune**: cele doua
+clasamente se combina prin Reciprocal Rank Fusion — un chunk urcat de ambele metode
+iese primul, unul urcat doar de una ramane in cursa.
+
+De ce nu doar embeddings. Masurat pe indexul viu:
+
+| Intrebare | Cel mai bun cosinus |
+|---|---|
+| „Nu exista nici un CIF pentru care sa aveti drept in SPV" (in documente) | 0,816 |
+| „cum trimit declaratia D406 SAF-T" (in documente) | 0,689 |
+| „imi da eroare la imprimanta HP LaserJet" (strain) | 0,641 |
+| „cum schimb uleiul la masina" (strain) | 0,534 |
+
+Marginea dintre o intrebare buna si una straina e de cateva sutimi. Ce lipseste din
+cosinus e exact ce conteaza la suport: **codurile**. `ORA-01722`, `D406`, `CIF` sunt
+tokeni exacti — cautarea lexicala ii prinde, cea semantica ii topeste in „ceva despre
+facturi". La „bilant contabil" (cosinus 0,600) si „import de date" (0,635) partea
+lexicala e singura care le tine in joc.
+
+Fuziunea da mereu un clasament, si la o intrebare complet straina. De aceea decizia
+**„avem sau nu acoperire"** se ia separat, pe dovezi:
+
+1. cosinus ≥ `RANK_STRONG_COSINE` → raspundem;
+2. cosinus < `RANK_WEAK_COSINE` → escaladam direct;
+3. intre ele → raspundem doar daca cel putin `RANK_MIN_RARE_RATIO` din termenii
+ distinctivi ai intrebarii (coduri, cuvinte rare in corpus) apar chiar in
+ chunk-urile gasite.
+
+Fractiunea din pasul 3 nu e cosmetica: cu un singur termen gasit, „cum imi resetez
+parola de la Windows" trecea drept acoperita fiindca „parola" apare in documente.
+
+### Recalibrarea, cand se schimba documentele
+
+Pragurile sunt masurate, nu alese din burta — si se **muta** cand se schimba
+depozitul. Exista un script pentru asta:
+
+```bash
+MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python ops/calibrate-rank.py
+MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python ops/calibrate-rank.py --sweep
+```
+
+Primul ruleaza setul de cazuri cu pragurile din `env` si arata ce ar gresi; al doilea
+matura o grila si listeaza combinatiile cu cele mai putine greseli. Embeddings-urile
+intrebarilor raman in cache (`~/.maria-bridge/calibrare-cache.json`) — pe CPU costa
+secunde fiecare. Setul de cazuri e in capul scriptului: **adauga acolo intrebarile
+reale la care Maria a gresit**, e singurul mod in care calibrarea ramane onesta.
+
+## Cand nu stie raspunsul: escaladarea la suport
+
+Un raspuns generic e mai rau decat niciun raspuns — utilizatorul pleaca cu impresia
+ca a primit ajutor. Cand `rank.assess` spune ca nu avem acoperire, Maria **nu mai
+intreaba modelul deloc**: raspunde ca trimite intrebarea la suport, si chiar o
+trimite.
+
+Ce pleaca: cine a intrebat, textul (sau textul citit din captura), motivul deciziei
+si scorurile. Daca a fost o captura de ecran, **imaginea insasi se retrimite** la
+suport, cu rezumatul ca legenda — de aceea captura se sterge abia dupa ce mesajul e
+complet tratat, nu imediat dupa OCR.
+
+Destinatia e `SUPPORT_JID` din `env`: `