Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
Claude Agent 7a1d2a2073 feat(maria): asociere prin cod de telefon si citirea capturilor cu erori
Doua lucruri cerute de utilizator, ambele pe acelasi drum: sa poti lega
puntea fara sa ai un ecran de scanat, si sa poti trimite Mariei o poza cu
eroarea in loc sa transcrii mesajul.

Asociere prin cod de 8 caractere (alternativa la QR):
- endpoint-ul /pair exista, dar nu era folosibil: fara `browser` explicit
  WhatsApp refuza codul, iar dupa introducerea lui corecta serverul cere un
  restart (515) care consuma din bugetul de reincercari si putea opri puntea.
  Acum descriptorul e Browsers.ubuntu('Chrome') si restartRequired reconecteaza
  imediat, fara sa numere.
- codul are TTL de 3 minute, iar /status il da doar cat timp e valabil —
  un cod expirat afisat in dashboard trimite omul sa tasteze degeaba.
- dashboard: camp pentru numar + buton, in acelasi card cu QR-ul.

Imagini cu erori (capturi de ecran):
- puntea descarca imaginile in ~/.maria-bridge/media/ (imageMessage sau
  document cu mimetype image/*, si prin ambalajele efemer/"vezi o data" —
  fara despachetare pareau mesaje fara continut si se aruncau tacut).
- rag/ocr.py: tesseract ron+eng. Modelul de raspuns e strict text, deci OCR
  nu e o optiune de calitate, e singura cale.
- cautarea in index merge DOAR pe liniile care arata a eroare; o fereastra
  intreaga de meniuri si totaluri dilueaza embedding-ul si scoate chunk-uri
  fara legatura. Modelul primeste captura intreaga, marcata ca text OCR.
- capturile se sterg imediat dupa citire (pot contine date de client).
- cand nu se citeste nimic si nu exista legenda, Maria cere textul erorii
  in loc sa raspunda in gol.

16 teste noi (42 in total). install.sh verifica tesseract; README documenteaza
ambele metode de asociere si drumul unei capturi.

Separat, in docs/chatboti-si-punti.md: chatul "Eu" e vazut de AMBELE punti de pe
numar. Puntea lui Echo (LXC 110) e asociata ca dispozitiv :11 al aceluiasi cont,
momentan nelegata dar pornita — daca se reasociaza, raspunde in "Eu" langa Maria.
Notat si ca serviciile lui Echo sunt unitati de UTILIZATOR: `systemctl is-active`
ca root raspunde "inactive" desi botul ruleaza.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-08-31 21:22:42 +00:00

13 KiB

Punte WhatsApp + RAG pentru Maria (LXC 171)

Care chatbot e care? Exista trei lucruri numite „Maria", doi boti pe Discord si doua punti WhatsApp legate la acelasi numar. Inainte de a depana, citeste docs/chatboti-si-punti.md — spune care instanta raspunde de fapt.

Bot de suport ROA pe WhatsApp: puntea WhatsApp (Baileys) primeste mesajele, consumer-ul RAG raspunde folosind DOAR informatiile dintr-un depozit de documente indexat cu embeddings (Ollama). Controlul (start/stop/restart, documente, reindexare, sincronizare Google Drive) se face din dashboard-ul puntii Discord (../discord-bridge/dashboard/), sectiunea "Maria — WhatsApp

  • RAG" — un singur panou comun pentru ambele punti de pe acest container, nu un dashboard separat per serviciu.

Continua prototipul descris in claude-agent/docs/maria-whatsapp-rag-prototype.md (construit initial in /tmp/maria-bridge/) — aici e mutat in git, ca serviciu persistent.

Nu confunda cu:

  • Maria pe Flowise (vfp_roaauto/COMUN/utile/chatbot/) — chatbot web separat.
  • Echo / echo-whatsapp-bridge.service (LXC 110 moltbot) — alt bot, alta punte.
  • Punte Discord -> Claude Code (discord-bridge/, acelasi container) — alt proiect, alt scop (comanda Claude Code de pe Discord, nu suport RAG).

ATENTIE: a existat o a doua instalare, pe LXC 104

Prototipul din care a plecat acest proiect a fost lasat sa ruleze pe LXC 104 (Flowise), in /root/maria-whatsapp-bridge/ — in afara git-ului, cu propriul consumer.py, propriul rag_index.json si propriul auth/ de WhatsApp.

Pana pe 2026-08-31, aceea era Maria care raspundea de fapt pe WhatsApp, iar cea din git (LXC 171) nu fusese niciodata asociata. In acea zi rolul a trecut aici: LXC 171 a fost asociat la acelasi numar, iar pe 104 serviciile maria-whatsapp-bridge si maria-whatsapp-consumer au fost oprite si dezactivate. llama-qwen35.service (LLM-ul de raspuns) si flowise.service raman pornite acolo. Indexul prototipului avea 24 de chunk-uri dintr-un singur fisier (roaauto_instructiuni.txt), deci raspundea inventand la orice intrebare din afara acelui subiect — inclusiv e-Factura.

Confuzia a costat timp de diagnostic: pe LXC 171 totul parea rupt (niciun LLM pe 8091, WhatsApp neasociat, rag.log fara activitate) desi utilizatorul primea raspunsuri. Explicatia era ca ne uitam la containerul gresit.

Daca depanezi Maria, verifica intai pe ce container ruleaza instanta conectata:

# aici (LXC 171)
curl -s http://127.0.0.1:8099/status
# pe LXC 104, daca mai exista
ssh root@10.0.20.201 "pct exec 104 -- curl -s http://127.0.0.1:8099/status"

LLM-ul de raspuns (llama.cpp cu Qwen3.5-2B-Q4_K_M, port 8091) ruleaza tot pe LXC 104 si e folosit prin retea: LLM_URL=http://10.0.20.161:8091. Pe LXC 171 nu exista niciun model de chat — Ollama de aici are doar nomic-embed-text, pentru embeddings.

Arhitectura

WhatsApp (self-chat, sau numarul legat)
   |
   v
whatsapp/index.js (Baileys)  -- API HTTP :8099 (/status /send /messages /react /qr /pair)
   |                             descarca imaginile primite in ~/.maria-bridge/media/
   |
   v
rag/consumer.py  -- polling la /messages, RAG stateless (FARA memorie intre mesaje)
   |                  vezi docs/maria-whatsapp-rag-prototype.md pentru motiv
   v
rag/ocr.py       -- capturile de ecran -> text (tesseract), inainte de RAG
   |
   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

../discord-bridge/dashboard/api.py -- panou comun; sectiunea Maria controleaza
                    maria-whatsapp.service + maria-rag.service, gestioneaza
                    documentele si declanseaza sincronizare/reindexare

Servicii systemctl --user (vezi ops/):

Unitate Ce face
maria-whatsapp.service Puntea Baileys (Node), port 8099
maria-rag.service Consumer RAG (Python), polling + raspunsuri
maria-sync.service + .timer Sincronizare Drive + reindexare, la 10 min

Instalare

cd maria-whatsapp-bridge
./ops/install.sh            # creeaza ~/.maria-bridge/, venv, npm install, symlink-uri unit

Completeaza manual ~/.maria-bridge/env (copiat din ops/env.example la prima rulare): cel putin LLM_URL (backend-ul de chat) si, daca vrei sincronizare automata cu Drive, DRIVE_REMOTE.

OLLAMA_URL (implicit http://127.0.0.1:11434, folosit pentru embeddings la indexare) presupune un Ollama instalat local pe container — nu exista alt Ollama documentat in infrastructura. Instalare (facuta deja pe LXC 171, 2026-08-31):

sudo apt-get install -y zstd   # dependinta a instalatorului Ollama
curl -fsSL https://ollama.com/install.sh | sh
ollama pull nomic-embed-text   # ~274 MB, CPU-only pe acest container

Pentru citirea capturilor de ecran primite (vezi „Imagini cu erori" mai jos):

sudo apt-get install -y tesseract-ocr tesseract-ocr-ron

Bridge-ul WhatsApp NU porneste automat la instalare — cere asocierea cu telefonul (actiune manuala, o singura data):

./ops/install.sh --start
# apoi deschide dashboard-ul puntii Discord, sectiunea "Maria — WhatsApp + RAG"
# -> cardul "Conectare WhatsApp", si alege una din cele doua metode (mai jos)

Asocierea cu telefonul: QR sau cod

Ambele duc la acelasi rezultat — puntea devine un dispozitiv conectat al contului. Alegerea e practica, nu tehnica:

Cand se foloseste
Cod QR Ai dashboard-ul deschis pe un ecran pe care telefonul il poate fotografia.
Cod de asociere (8 caractere) Esti pe telefon, sau ecranul cu QR-ul e la distanta: scrii numarul in dashboard, primesti un cod si il tastezi pe telefon.

Pentru codul de asociere, in dashboard: scrie numarul in format international, fara + si fara 00 (ex. 40723197939), apasa „Cere cod de asociere", apoi pe telefon: WhatsApp -> Dispozitive conectate -> Conecteaza un dispozitiv -> Conecteaza cu numar de telefon -> tastezi codul.

Codul e valabil ~3 minute; dupa ce expira, dashboard-ul il marcheaza ca expirat si trebuie cerut altul. Dupa introducerea corecta, WhatsApp inchide conexiunea cu codul restartRequired (515) — puntea se reconecteaza singura, imediat; nu e o eroare si nu consuma din bugetul de reincercari.

Din linia de comanda, aceleasi lucruri:

curl -s -X POST -H 'Content-Type: application/json' \
     -d '{"phone":"40723197939"}' http://127.0.0.1:8099/pair
curl -s http://127.0.0.1:8099/status | python3 -m json.tool   # cod + secunde ramase

Codul se poate cere doar cat timp sesiunea NU e inregistrata. Daca puntea raspunde „sesiunea e deja inregistrata", opreste-o, sterge ~/.maria-bridge/whatsapp-auth/ si porneste-o din nou — dar atentie, asta desface asocierea existenta.

Imagini cu erori

Utilizatorii trimit aproape intotdeauna o captura cu fereastra de eroare, nu textul ei. Puntea descarca imaginea in ~/.maria-bridge/media/, iar consumer-ul o trece prin tesseract (ron+eng) inainte de RAG. Modelul de raspuns (Qwen3.5-2B pe LXC 104) e strict text, deci OCR-ul e singura cale — nu e o optiune de calitate.

Doua detalii care nu se vad din cod la prima citire:

  • Cautarea in index nu foloseste toata captura. Un ecran intreg de meniuri, coloane si totaluri dilueaza embedding-ul si scoate chunk-uri fara legatura. Se cauta doar dupa liniile care arata a eroare (ORA-…, „eroare", „nu exista", …); modelul primeste totusi fereastra intreaga, marcata explicit ca text OCR, ca sa nu trateze greselile de recunoastere ca date exacte. Vezi rag/ocr.py.
  • Capturile se sterg imediat dupa citire. Pot contine date de client si nu exista niciun motiv sa ramana pe disc. Ce ramane dupa un restart in mijlocul procesarii se curata la pornirea puntii (dupa 24h).

Legenda imaginii, daca exista, conteaza: intra si in intrebare si in cautare. Daca OCR-ul nu gaseste nimic lizibil si nu exista legenda, Maria cere textul erorii in loc sa inventeze un raspuns.

Limite: MAX_MEDIA_MB (implicit 8) pentru imaginea bruta, OCR_MAX_CHARS (implicit 1500) pentru textul trimis modelului, OCR_TIMEOUT_S (60). Videoclipurile, audio si documentele non-imagine sunt in continuare ignorate.

Dashboard (comun cu puntea Discord)

Nu exista un dashboard separat pentru Maria. Controlul se face din dashboard-ul puntii Discord — vezi ../discord-bridge/README.md pentru URL si autentificare (DASHBOARD_TOKEN din ~/.claude-discord/env, tunel SSH sau Tailscale la /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
  • 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)

Depozitul de documente

Fisiere .txt/.md/.xml in ~/.maria-bridge/documents/ (lista exacta: store.DOC_EXTENSIONS). Se pot administra:

  1. manual din dashboard-ul comun (adaugare/stergere text, reindexare automata la salvare);
  2. prin sincronizare Google Drive — vezi mai jos.

Sincronizare cu Google Drive

Sursa: dosarul document_store din Drive-ul contului mmarius28@gmail.com (pe Windows apare ca D:\GoogleDrive\romfast\document_store, prin Google Drive Desktop). ID-ul dosarului: 1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O.

Containerul e headless (fara browser pentru OAuth), deci autorizarea se face pe o masina cu browser si se muta aici ca token — fara cont de serviciu si fara consola Google Cloud. rclone e deja instalat pe LXC 171 (rclone v1.60.1).

Pasi (o singura data):

  1. Pe Windows, ia rclone.exe de la https://rclone.org/downloads/ (arhiva portabila, nu cere instalare) si ruleaza in acel dosar:

    rclone.exe authorize "drive" --drive-scope=drive.readonly
    

    Se deschide browserul; autentifica-te cu mmarius28@gmail.com si accepta. In consola apare un token JSON intre ---> si <---. Copiaza-l intreg.

  2. Pe container, da tokenul scriptului de configurare:

    ops/setup-drive.sh '<TOKEN_JSON>'
    

    Face restul singur: creeaza remote-ul gdrive, verifica accesul la dosar, scrie DRIVE_REMOTE in ~/.maria-bridge/env (cu backup) si ruleaza prima sincronizare cu reindexare. E idempotent — se poate rula din nou oricand.

    Tinta scrisa in env e gdrive,root_folder_id=<ID>: — sintaxa „connection string" a rclone, care fixeaza dosarul dupa ID, fara sa depinda de structura de nume din My Drive.

  3. Verifica in dashboard: sectiunea Maria trebuie sa arate „Drive: …" in loc de „Sincronizare Drive dezactivata".

Tokenul se reimprospateaza singur (refresh token) cat timp aplicatia ramane autorizata in contul Google. Daca expira, dashboard-ul arata sincronizarea ca esuata — reia pasii 1-2.

Ce se sincronizeaza

rclone sync aduce doar *.txt, *.md si *.xml (vezi store.DOC_EXTENSIONS). Restul din dosar — chatflow-uri Flowise .json, scripturi .ps1, .docx — sunt ignorate deliberat: nu sunt cunostinte de suport.

sync sterge local ce nu mai exista in Drive, deci depozitul e o oglinda a dosarului din Drive, nu o colectie care creste. Documentele adaugate manual din dashboard dispar la prima sincronizare daca nu exista si in Drive.

Acelasi document in doua formate

Cand exista X.xml si X.md, in index intra doar .xml (ordinea de preferinta: .xml > .md > .txt, in rag/store.py). Sursele .xml sunt structurate pe probleme si de regula mai noi decat exporturile .md — la d406_saft_knowledge, .xml era cu trei luni mai nou si cu 50% mai mare. Fisierul umbrit ramane pe disc si apare in dashboard marcat „umbrit de …", ca sa se vada de ce nu e indexat; daca ar fi indexate ambele, acelasi raspuns ar aparea de doua ori in rezultatele RAG.

Cum se taie XML-ul in chunk-uri

Un chunk per element de nivel 1 — adica o problema = un chunk, cu mesajul de eroare si rezolvarea impreuna. Taierea pe linii goale (cea folosita la .md) le-ar separa, iar cautarea ar gasi eroarea si ar returna un chunk fara raspuns. Etichetele raman ca prefixe lizibile (mesaj eroare: …, rezolvare: …), fara paranteze unghiulare. Un XML care nu se poate parsa nu opreste indexarea: cade pe taierea obisnuita, cu o linie in log. Vezi rag/indexer.py si tests/test_store_si_chunking.py.

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.

Teste

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

Context conversational

rag/consumer.py nu retine memorie intre mesaje — fiecare intrebare e o interogare RAG independenta (system prompt + top-K chunk-uri + intrebare). Vezi claude-agent/docs/maria-whatsapp-rag-prototype.md pentru motiv si comparatie cu celelalte punti (Discord: context nelimitat + /new; Maria pe Flowise: fereastra fixa de 5 schimburi).