docs(maria): handover — cazuri de continuare in calibrate si termenii distinctivi

Ce ramane deschis dupa sesiunea de azi: pe un fir, acoperirea se judeca pe
interogarea combinata (ancora + mesaj nou), deci cuvintele de politete din primul
mesaj ajung "termeni distinctivi" si nu apar niciodata in documente. Efectul e
escaladare in plus pe discutiile lungi — directie sigura, dar inutila.

Fisierul spune si de ce nu se incepe cu reparatia: continuarile scurte n-au termeni
distinctivi proprii, deci reparatia poate strica exact ce a construit firul. Intai
cazuri de continuare fara cod de eroare in ops/calibrate-rank.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
Claude Agent
2026-09-01 19:47:35 +00:00
parent af04d97de9
commit 89145d437e

View File

@@ -0,0 +1,144 @@
# Context handover — 2026-09-01, seara
**Ce e de făcut:** două lucruri legate între ele, în `proxmox/lxc171-claude-agent/maria-whatsapp-bridge/`:
1. adaugă **cazuri de continuare** în `ops/calibrate-rank.py` (azi setul n-are decât două, ambele cu cod de eroare);
2. repară **termenii distinctivi**: `rank.rare_terms()` judecă acoperirea pe *interogarea combinată* (ancora firului + mesajul nou), nu pe ce a întrebat omul acum.
(1) există ca să poți face (2) fără să ghicești. **Nu începe cu (2).**
Tot ce e mai jos e comis și împins: `ROMFASTSQL` `5e60394..af04d97`.
---
## 1. De unde vine
Sesiunea de azi a plecat de la: *„am trimis 2 mesaje pe grupul maria test whatsapp și ambele răspunsuri sunt eronate"*. Cauza reală s-a dovedit dublă și e reparată (vezi §5), dar reparația a scos la iveală problema care rămâne deschisă.
Din firul de test real (escaladarea `M-260901-06EB`, `~/.maria-bridge/escalations/1788291399-M-260901-06EB.json`):
```
[consumer] rank: efactura_knowledge.md(0.836), … | cosinus 0.836, iar termenii
distinctivi (buna, luna, august, trimitere, auto) nu apar in documente
```
**„buna" e tratat drept termen distinctiv.** E rar în corpusul de ERP, deci `rare_terms()` îl consideră discriminant, deși e o formulă de salut.
De ce contează acum și nu conta înainte: de azi, la o continuare se caută după **ancoră + mesajul nou** (`consumer.py:574` → `fir.interogare()`), iar ancora e primul mesaj al omului, cu tot cu „Buna . … Poti te rog sa ma ajuti?". Ancora nu se șterge cât trăiește firul (2h), deci **fiecare continuare de pe firul ăla e judecată și după „buna/luna/august"**, cuvinte care nu vor apărea niciodată în documente.
### Efectul
`assess()` cere ca cel puțin `RANK_MIN_RARE_RATIO` (0,5) din termenii distinctivi să apară chiar în chunk-uri. Cu 3 cuvinte de politețe care nu apar niciodată, raportul scade artificial la fiecare replică → **escaladare în plus**.
Direcția e sigură (un om vede întrebarea, nu primește un răspuns inventat), deci **nu e urgent**. Dar face Maria inutilă exact pe discuțiile lungi, unde ar trebui să fie cea mai bună.
În cazul testat n-a stricat nimic — răspunsul chiar nu era în documente, escaladarea a fost corectă. Deci **nu ai încă nicio dovadă că regresia se produce în practică**; asta e chiar ce trebuie măsurat la pasul (1).
---
## 2. Reparația propusă (nu implementată)
**Separă cele două roluri ale interogării:**
| Rol | Ce folosește azi | Ce ar trebui |
|---|---|---|
| **regăsire** (ce chunk-uri aduc) | ancoră + mesaj nou | rămâne așa — ancora chiar ajută, „da, mă blochează" singur nu găsește nimic |
| **acoperire** (răspund sau escaladez) | ancoră + mesaj nou | doar mesajul nou (+ OCR-ul capturii), adică ce a întrebat omul acum |
Concret, în `consumer.py:131 search()`, `rank.assess()` (`consumer.py:144`) primește azi același `query` ca și regăsirea. Ar trebui să primească separat interogarea de dovadă.
**Capcana, și motivul pentru care n-am făcut-o azi:** o continuare scurtă („da, mă blochează complet", „eram la salvarea unei facturi") **n-are termeni distinctivi proprii**. Judecată singură, escaladează — exact regresia pe care `fir.py` a fost scris ca s-o prevină (vezi tabelul din docstring-ul lui `rag/fir.py`, cazurile măsurate). Deci reparația poate strica mai mult decât repară, și nu se poate valida pe setul actual de cazuri.
Variante de luat în calcul, în ordinea preferinței:
- **a)** acoperirea se judecă pe mesajul nou, dar dacă acesta n-are termeni distinctivi proprii se cade înapoi pe interogarea combinată (continuările scurte se comportă ca azi);
- **b)** `rare_terms()` ignoră formulele de politețe — listă mică de stopwords („buna", „salut", "buna ziua", „multumesc", „va rog", „poti", „ajuti"). Whack-a-mole, dar o linie și zero risc;
- **c)** ancora se curăță la creare: se păstrează doar liniile care arată a eroare, cum face deja `ocr.retrieval_query()` pentru capturi (`rag/ocr.py`).
(b) e cel mai ieftin și poate fi făcut singur, imediat. (a) e cel corect. Nu le confunda: (b) ascunde simptomul măsurat azi, (a) rezolvă clasa.
---
## 3. Ce trebuie adăugat în `ops/calibrate-rank.py` — pasul (1)
Setul (`CAZURI`, o listă de `(intrebare, are_raspuns_in_documente)`) are azi **26 de cazuri, 26/26 corecte**. Continuări are doar două, și **ambele conțin un cod de eroare**, deci trec pe regula codurilor și nu spun nimic despre termenii distinctivi:
```python
("ORA-06550: line 1, column 7 PLS-00906 object invalid\nda, ma blocheaza complet", True),
("ORA-12154 TNS could not resolve the connect identifier\neram la salvarea unei facturi", True),
```
**Lipsesc continuările fără cod de eroare** — exact cazul în care termenii distinctivi decid. De adăugat, în forma pe care o produce `fir.interogare()` (ancoră `\n` mesaj nou):
- ancoră politicoasă + continuare scurtă, cu răspuns în documente → `True`
(ex. `"Buna ziua . Am o problema la trimiterea in SPV . Puteti sa ma ajutati?\nimi zice ca tokenul e expirat"`)
- ancoră politicoasă + continuare scurtă, fără răspuns în documente → `False`
- aceeași continuare **fără** formula de salut în ancoră → același verdict ca varianta cu salut
(dacă verdictele diferă, ai măsurat exact regresia)
- ancoră + a treia, a patra replică (fir lung) → verdictul nu trebuie să se degradeze cu lungimea
- continuare pe un fir deschis de o **captură** (ancora = OCR, plin de zgomot: „Total", „Lit", „Selecteaz!")
Scrie-le pornind de la fire reale, nu inventate: `~/.maria-bridge/escalations/*.json` are câmpul `fir` cu schimburile, iar `~/.maria-bridge/conversations/*.json` are ancora exactă.
**Atenție:** fiecare caz nou costă un embedding (~7s pe CPU, fără GPU pe LXC 171). Se pun în cache automat în `~/.maria-bridge/calibrare-cache.json`, deci doar prima rulare e lentă.
### Cum se rulează
```bash
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/maria-whatsapp-bridge
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
python3 -m pytest -q # 103 teste; venv-ul n-are pytest, se ruleaza cu python3 de sistem
```
`--sweep` mătură `RANK_WEAK_COSINE` × `RANK_MIN_RARE_RATIO` (nu mai există prag „strong", vezi §5).
**Criteriu de acceptare pentru (2):** setul extins rămâne 100% corect *și* cu reparația, *și* fără ea nu e — altfel n-ai dovedit că reparația face ceva.
---
## 4. Fișiere care contează
| Fișier | De ce |
|---|---|
| `rag/rank.py:168 rare_terms()` | inima problemei: `len(t) >= 4 and df <= 25% din corpus` → „buna" trece |
| `rag/rank.py:212 assess()` | decizia acoperirii; docstring-ul explică de ce nu mai există prag pe cosinus |
| `rag/consumer.py:131 search()` | aici `assess()` primește același query ca regăsirea — punctul de separat |
| `rag/consumer.py:574` | `search_query = fir_mod.interogare(fir, search_query)` — unde se combină |
| `rag/fir.py:140 interogare()` | ancoră + mesaj nou |
| `rag/fir.py:126 extinde_ancora()` | ancora crește cu OCR-ul capturii (adăugat azi) |
| `rag/fir.py` docstring | tabelul cu răspunsurile greșite care justifică existența ancorei — citește-l înainte să scoți ancora din ceva |
| `ops/calibrate-rank.py` | setul de cazuri + `--sweep` |
| `tests/test_rank.py`, `tests/test_fir.py` | testele care prind regresiile |
| `README.md` §„Cum se ordonează rezultatele (rank)" | documentația de actualizat la final |
---
## 5. Ce s-a reparat azi (nu reface)
Trei comiteri, toate împinse, serviciile repornite, verificate pe viu în grupul „Maria Test".
**`45b3659`** — două răspunsuri inventate: mesaj vag (cosinus 0,768) și captură cu `errorMessage="CUI cumparator incorect"` (0,778, eroare inexistentă în documente). Cauza: `RANK_STRONG_COSINE=0,70` însemna „cosinusul singur e destul", dar toate chunk-urile eFactura seamănă între ele. Plus `LLM_TEMPERATURE=0` (llama.cpp folosea 0,8).
**`47110d7`** — două probleme de fond:
- gardul „mesaj prea vag" (`triaj.prea_vag`) cerea text sub 12 cuvinte; mesajul real avea 14. Acum nu mai numără cuvinte și se aplică **după căutare, doar când nu există acoperire**, o singură dată pe fir (`fir["detalii_cerute"]`);
- `fir.este_continuare()` rupea firul la **orice** imagine. Cazul frecvent e tocmai text + captură trimise una după alta. Acum o captură în `FIR_IMAGINE_MIN` (5 min) continuă firul, OCR-ul intră în ancoră, iar pe o escaladare deschisă captura pleacă la aceeași referință;
- **`RANK_STRONG_COSINE` a dispărut de tot.** Motivul e important pentru (2): *cosinusul crește cu lungimea interogării*. Aceeași captură: 0,778 singură, **0,836** împreună cu mesajul de dinainte. Orice prag fix de sus e trecut de o discuție destul de lungă. Acoperirea se sprijină acum exclusiv pe dovada lexicală — adică pe chiar `rare_terms()`, ceea ce face (2) mai important decât părea.
**`af04d97`** — găsite citind logul: `requests` nu ridică excepție la 4xx/5xx, deci escaladarea se scria `notified: true` chiar când puntea răspundea 503; puntea nu loga nimic la trimitere; mesajele primite se logau tăiate la 80 de caractere fără semn (m-a dus la o concluzie greșită în diagnostic).
---
## 6. Constrângeri de mediu
- **Modelul de chat e Qwen3.5-2B Q4** pe LXC 104 (`LLM_URL=http://10.0.20.161:8091`). Inventează dacă îl lași. Poarta de acoperire e singura apărare reală — de asta merită reparată bine.
- **Nu încerca un model mai mare pe LXC 171.** Testat azi: `qwen3:4b-instruct` prin Ollama, 4 nuclee, fără GPU → 600s timeout fără niciun token; un prompt de 15 tokeni a scos 6 tokeni în 10 min 20s. Modelul a fost șters. Fără GPU sau API extern (vezi `docs/supliment-glm-zai.md`, neimplementat) nu e altă variantă.
- **Ollama pe 171 are doar `nomic-embed-text`**, pentru embeddings. Un embedding ≈ 7s.
- Servicii: `systemctl --user restart maria-rag maria-whatsapp`; loguri în `~/.maria-bridge/logs/{rag,whatsapp}.log`.
- Grup de test: `120363409761730101@g.us` („Maria Test"), `SUPPORT_JID` e chiar numărul propriu, deci escaladările se văd.
- **Șterge firele vechi înainte de a testa** (`rm -f ~/.maria-bridge/conversations/*.json`), altfel primul mesaj e luat drept continuare a discuției precedente — TTL 120 min.
---
## 7. De reținut, separat de task
**„CUI cumparator incorect" nu există în niciun document** (`grep -ri "cumparator incorect" ~/.maria-bridge/documents*` → zero). Maria escaladează corect, dar ar putea răspunde. E o eroare eFactura reală și frecventă. N-am scris textul — nu inventez procedură ROA. De pus în `document_store` (Google Drive), de către cineva care știe procedura.