diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md index 5bc8632..0e7d45b 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md @@ -559,6 +559,30 @@ 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. +### Si ce a raspuns omul + +Cat tine preluarea, **fiecare mesaj al omului din echipa se adauga la fisierul +escaladarii** (`completari`, cu `"fel": "raspuns_om"` si cine a scris). Inainte, +replica lui traia doar in firul din `conversations/`, care se sterge dupa +`FIR_TTL_MIN` — jurnalul pastra deci numai intrebarile fara raspuns, si niciun +raspuns. Or perechea eroare + rezolvare e chiar materia prima pentru documentele +RAG: o eroare ca „CUI cumparator incorect" ajunge in `document_store` doar daca o +scrie cineva acolo, iar cel mai ieftin loc de unde poate fi scrisa e ce a raspuns +deja omul o data. + +De unde se citesc, pentru o trecere in revista: + +```bash +jq -r 'select(.completari) | .ref + "\n ? " + (.text // .ocr_text // "")[0:120] + + "\n" + ([.completari[] | select(.fel == "raspuns_om") + | " > " + .text] | join("\n"))' ~/.maria-bridge/escalations/*-M-*.json +``` + +**Atentie la limita:** preluarea se detecteaza doar in grupuri cu cel putin +`FIR_PRELUARE_MIN_PARTICIPANTI` (2) participanti. In self-chat si in grupul de test +tot ce se scrie e `fromMe`, deci acolo raspunsurile nu se inregistreaza automat — +doar in grupurile reale de client. + ## Ce a citit Maria dintr-o captura Textul OCR se logheaza **integral** in `~/.maria-bridge/logs/rag.log`, cu prefix diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py index 3f01c04..740d9f4 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py @@ -450,6 +450,38 @@ def completeaza(ref: str, msg: dict, text: str) -> str | None: return raspuns +def raspuns_de_om(ref: str, msg: dict, text: str) -> None: + """Ce a raspuns omul de la suport, in fisierul escaladarii. + + Fara asta, jurnalul pastra doar intrebarile FARA raspuns: rezolvarea se scria + numai in firul din `conversations/`, care se sterge dupa `FIR_TTL_MIN`. Or + perechea eroare + rezolvare e chiar materia prima pentru documentele RAG — + „CUI cumparator incorect" e in documente doar daca cineva o scrie acolo, si + cel mai ieftin loc de unde se poate scrie e ce a raspuns omul deja o data. + + Se cheama la fiecare mesaj al omului cat tine preluarea, nu doar la primul: + rezolvarea vine de obicei in doua-trei replici. + """ + text = (text or "").strip() + if not text: + return + fisier = _fisier_escaladare(ref) + if fisier is None: + return + try: + record = json.loads(fisier.read_text(encoding="utf-8")) + record.setdefault("completari", []).append({ + "at": time.time(), "text": text, "fel": "raspuns_om", + "cine": msg.get("pushName") or msg.get("from") or "necunoscut", + "message_id": msg.get("id"), + }) + fisier.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8") + except (OSError, ValueError) as exc: + print(f"[consumer] nu pot salva raspunsul omului la {ref}: {exc}", file=sys.stderr) + return + print(f"[consumer] raspuns de la om, pastrat la {ref}: {text[:60]!r}", file=sys.stderr) + + _GRUPURI_CACHE: dict = {"at": 0.0, "date": {}} GRUPURI_TTL_S = 600 @@ -527,6 +559,8 @@ def handle(index: Index, msg: dict) -> None: if preluare_de_om(msg): fir = fir or fir_mod.deschide(sender, "") fir_mod.adauga(fir, "om", text) + if fir.get("ref"): + raspuns_de_om(fir["ref"], msg, text) fir_mod.taci(fir) fir_mod.salveaza(fir) print(f"[consumer] {sender}: a preluat un om, tac {fir_mod.tacere_s() // 60} min", diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_escaladare.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_escaladare.py index 8764a7a..4e82ef2 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_escaladare.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_escaladare.py @@ -144,3 +144,36 @@ def test_puntea_respinge_mesajul_nu_promitem_ca_a_plecat(rezultat_fara_acoperire inregistrare = json.loads(next((config.STATE_DIR / "escalations").glob("*.json")).read_text()) assert inregistrare["notified"] is False assert "503" in inregistrare["notify_error"] + + +def test_raspunsul_omului_intra_in_fisierul_escaladarii(tmp_path, monkeypatch): + """Perechea eroare + rezolvare, in acelasi fisier. + + Inainte, replica omului traia doar in firul din `conversations/`, care se + sterge la TTL: jurnalul pastra intrebarile fara raspuns si niciun raspuns. + """ + import json + import time + import config + import consumer + + d = config.STATE_DIR / "escalations" + d.mkdir(parents=True, exist_ok=True) + (d / f"{int(time.time())}-M-TEST-OM.json").write_text( + json.dumps({"ref": "M-TEST-OM", "text": "CUI cumparator incorect"}), encoding="utf-8") + + consumer.raspuns_de_om("M-TEST-OM", {"id": "1", "pushName": "Suport"}, + "Se corecteaza CUI-ul partenerului si se retrimite factura.") + consumer.raspuns_de_om("M-TEST-OM", {"id": "2", "pushName": "Suport"}, " ") + + record = json.loads(next(d.glob("*-M-TEST-OM.json")).read_text(encoding="utf-8")) + completari = record["completari"] + assert len(completari) == 1, "mesajul gol nu are ce cauta in jurnal" + assert completari[0]["fel"] == "raspuns_om" + assert completari[0]["cine"] == "Suport" + assert "retrimite" in completari[0]["text"] + + +def test_raspunsul_omului_pe_o_escaladare_care_nu_exista_nu_crapa(): + import consumer + consumer.raspuns_de_om("M-NU-EXISTA", {"id": "1"}, "orice")