feat(maria): raspunsul omului de la suport se pastreaza in jurnalul escaladarii
Jurnalul pastra doar intrebarile FARA raspuns. Replica omului care prelua discutia se scria numai in firul din `conversations/`, care se sterge dupa FIR_TTL_MIN — deci exact rezolvarea disparea, iar perechea eroare + rezolvare trebuia recuperata manual din WhatsApp. Acum, cat tine preluarea, fiecare mesaj al omului se adauga la fisierul escaladarii (`completari`, `"fel": "raspuns_om"`, cu cine a scris). La fiecare mesaj, nu doar la primul: rezolvarea vine de obicei in doua-trei replici. De aici se pot extrage erorile si rezolvarile pentru `document_store` — „CUI cumparator incorect" ajunge in documente doar daca o scrie cineva, si cel mai ieftin loc de unde poate fi scrisa e ce a raspuns deja omul o data. Limita, documentata in README: preluarea se detecteaza doar in grupuri cu cel putin FIR_PRELUARE_MIN_PARTICIPANTI participanti, deci nu in self-chat si nu in grupul de test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
@@ -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:
|
„Trimise la suport". Jurnalul se scrie intotdeauna, si cand notificarea esueaza:
|
||||||
altfel exact intrebarile fara raspuns ar disparea fara urma.
|
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
|
## Ce a citit Maria dintr-o captura
|
||||||
|
|
||||||
Textul OCR se logheaza **integral** in `~/.maria-bridge/logs/rag.log`, cu prefix
|
Textul OCR se logheaza **integral** in `~/.maria-bridge/logs/rag.log`, cu prefix
|
||||||
|
|||||||
@@ -450,6 +450,38 @@ def completeaza(ref: str, msg: dict, text: str) -> str | None:
|
|||||||
return raspuns
|
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_CACHE: dict = {"at": 0.0, "date": {}}
|
||||||
GRUPURI_TTL_S = 600
|
GRUPURI_TTL_S = 600
|
||||||
|
|
||||||
@@ -527,6 +559,8 @@ def handle(index: Index, msg: dict) -> None:
|
|||||||
if preluare_de_om(msg):
|
if preluare_de_om(msg):
|
||||||
fir = fir or fir_mod.deschide(sender, "")
|
fir = fir or fir_mod.deschide(sender, "")
|
||||||
fir_mod.adauga(fir, "om", text)
|
fir_mod.adauga(fir, "om", text)
|
||||||
|
if fir.get("ref"):
|
||||||
|
raspuns_de_om(fir["ref"], msg, text)
|
||||||
fir_mod.taci(fir)
|
fir_mod.taci(fir)
|
||||||
fir_mod.salveaza(fir)
|
fir_mod.salveaza(fir)
|
||||||
print(f"[consumer] {sender}: a preluat un om, tac {fir_mod.tacere_s() // 60} min",
|
print(f"[consumer] {sender}: a preluat un om, tac {fir_mod.tacere_s() // 60} min",
|
||||||
|
|||||||
@@ -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())
|
inregistrare = json.loads(next((config.STATE_DIR / "escalations").glob("*.json")).read_text())
|
||||||
assert inregistrare["notified"] is False
|
assert inregistrare["notified"] is False
|
||||||
assert "503" in inregistrare["notify_error"]
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user