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:
@@ -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`: `<numar>@s.whatsapp.net` pentru o persoana sau
|
||||
`<id>@g.us` pentru un grup. **Nesetat = nimeni nu e anuntat**, dar escaladarea tot se
|
||||
inregistreaza in `~/.maria-bridge/escalations/` si apare in dashboard, sectiunea
|
||||
„Trimise la suport". Jurnalul se scrie intotdeauna, si cand notificarea esueaza:
|
||||
altfel exact intrebarile fara raspuns ar disparea fara urma.
|
||||
|
||||
## Ce a citit Maria dintr-o captura
|
||||
|
||||
Textul OCR se logheaza **integral** in `~/.maria-bridge/logs/rag.log`, cu prefix
|
||||
`|`, urmat de interogarea folosita la cautare si de sursele alese cu scorurile lor:
|
||||
|
||||
```
|
||||
[consumer] OCR (703 caractere):
|
||||
| Eroare la salvarea facturii:
|
||||
| ORA-01722: invalid number
|
||||
[consumer] caut dupa: 'Eroare la salvarea facturii:\nORA-01722: invalid number'
|
||||
[consumer] rank: efactura_knowledge.md(0.771), d406_saft_knowledge.xml(0.62) | cosinus 0.771 >= 0.66
|
||||
```
|
||||
|
||||
Fara asta un raspuns gresit nu se poate explica: nu se stie daca a citit prost
|
||||
captura sau a cautat prost in documente. Aceleasi linii se vad in dashboard, la
|
||||
logurile serviciului `rag`.
|
||||
|
||||
## Context conversational
|
||||
|
||||
`rag/consumer.py` nu retine memorie intre mesaje — fiecare intrebare e o
|
||||
|
||||
Reference in New Issue
Block a user