Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py
Claude Agent 0ace9f97ac fix(maria): fara "caut informatia" la completari, si trei feluri de raspuns
Din firul testat in grup: la "Urgent este", "Nu m-a contactat nimeni. Cat mai
dureaza?" si "Tot nimic", Maria trimitea de fiecare data "Caut informatia, revin
imediat...", calcula un embedding pe care il arunca, si raspundea acelasi sablon
("am adaugat si asta la M-..."). La intrebarea "cat mai dureaza" nu raspundea deloc,
iar "urgent" nu schimba nimic nicaieri.

- Completarea se decide INAINTEA confirmarii si a cautarii: raspunsul vine instant,
  deci nici promisiune, nici embedding degeaba (~1-2s per mesaj).
- triaj.fel_completare: urgenta / stare / detaliu.
  - urgenta -> escaladarea se marcheaza si suportul primeste un mesaj cu URGENT;
  - stare -> raspuns cu ce stim (de cat timp asteapta) si reamintire catre echipa,
    cel mult una la 15 minute, ca sa nu sune telefonul la fiecare "tot nimic";
  - detaliu -> confirmare scurta, cu formularea rotita.
- Peste 30 de minute fara raspuns Maria o spune pe fata si indruma spre telefon
  (SUPPORT_PHONE), in loc sa repete ca "echipa vede detaliul".
- Gramatica: "acum 10 minute", nu "acum 10 de minute".

Teste: 99 pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-09-01 07:19:06 +00:00

220 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Triaj: cine primeste raspuns, cine primeste intrebari, cine cheama programatorii.
Trei decizii pe care cautarea in documente nu le poate lua singura:
1. **Mesaj prea vag.** „Am o eroare" nu e o intrebare — e inceputul uneia. Fara
operatiunea, ecranul si textul erorii, si un om ar ghici. Maria intreaba, nu
ghiceste (si nu deranjeaza suportul cu „am o eroare").
2. **Erori care oricum ajung la programatori.** Dictionarul spune, pentru fiecare
eroare, `cand suni suportul`. Noua dintre ele incep cu „Intotdeauna" sau
„Imediat": nu se rezolva din aplicatie, orice explicatie i-ar da utilizatorului
nu-l ajuta sa iasa din ea. Pentru astea raspunsul se compune din chiar campurile
dictionarului, iar mesajul pleaca automat la suport.
De ce fara model: pe captura cu ORA-06550, avand in context exact intrarea care
spune „nu se rezolva din aplicatie", modelul a raspuns „verifica schema de date
din aplicatia ROA (...) sau foloseste procedura alternativa MI_pack_parteneri_old".
A inventat o procedura, a cerut unui contabil sa umble in schema si a contrazis
documentul. Textul din dictionar e deja scris pentru clienti; parafrazarea lui
nu adauga nimic si poate strica tot.
3. **Cat e de urgent.** Aceeasi eroare poate bloca omul complet sau poate fi ocolita
pana maine. Nu se deduce din text, deci se intreaba — iar raspunsul lui se
ataseaza escaladarii deja deschise, nu porneste alta.
"""
from __future__ import annotations
import re
import rank
# Prima vorba din `cand suni suportul`. Restul („Daca revine dupa restart") sunt
# conditionate: acolo utilizatorul are ce incerca, deci raspunde modelul.
_INTOTDEAUNA = ("intotdeauna", "imediat")
_CAMP = re.compile(r"^([a-z ]+):\s*(.*)$")
# Cuvintele cu care oamenii anunta o problema fara sa spuna care.
_ACUZA = re.compile(
r"\b(eroare|erori|eroarea|problema|probleme|nu merge|nu functioneaza|"
r"nu mai merge|s-a blocat|crapa|imi da|nu pot)\b"
)
def camp(chunk: str, nume: str) -> str:
"""Valoarea unui camp din chunk-ul XML randat (`mesaj eroare: ...`)."""
tinta = nume.replace("_", " ")
for line in chunk.splitlines():
m = _CAMP.match(line.strip())
if m and m.group(1).strip() == tinta:
return m.group(2).strip()
return ""
def nivel_suport(chunk: str) -> str | None:
"""`"intotdeauna"` / `"imediat"` daca eroarea cere suport oricum, altfel `None`."""
valoare = camp(chunk, "cand suni suportul").lower()
for cuvant in _INTOTDEAUNA:
if valoare.startswith(cuvant):
return cuvant
return None
def prea_vag(text: str, are_imagine: bool) -> bool:
"""Mesajul anunta o problema fara sa spuna care.
Deliberat ingust: doar text scurt, fara captura si fara cod de eroare. O
intrebare adevarata („cum trimit D406") nu contine cuvintele de acuza, iar una
cu un cod are deja tot ce trebuie ca sa fie cautata.
"""
if are_imagine:
return False
t = rank.fold(text.strip())
if not t or rank.codes(t):
return False
if not _ACUZA.search(t):
return False
return len(rank.tokenize(t)) <= 12
CERE_DETALII = (
"Ca sa-mi dau seama despre ce e vorba, spune-mi te rog:\n"
"1. ce faceai exact cand a aparut (ce operatiune),\n"
"2. in ce ecran sau meniu erai,\n"
"3. ce scrie exact in mesaj — cel mai simplu, trimite o captura de ecran cu "
"fereastra de eroare."
)
# Ce se trimite la o eroare care oricum ajunge la programatori. Explicatia si
# indicatia sunt luate ca atare din dictionar, nu reformulate.
_URGENT = ("Este o eroare care afecteaza toti utilizatorii, nu doar pe tine, asa ca "
"am anuntat echipa tehnica imediat si i-am trimis {ce}")
_NORMAL = "Am trimis {ce} mai departe, catre echipa tehnica"
INTREBARE_URGENTA = (
"Spune-mi te rog daca te blocheaza complet sau poti continua pe alta "
"operatiune — asa stiu cat e de urgent."
)
def raspuns_escaladat(chunk: str, nivel: str, ref: str, notificat: bool,
cu_imagine: bool = False) -> str:
"""Raspunsul pentru o eroare care oricum ajunge la programatori.
Compus din campurile dictionarului, fara model: textul de acolo e deja scris
pentru clienti, iar aici nu e nimic de dedus — doar de spus limpede.
"""
parti: list[str] = []
explicatie = camp(chunk, "ce inseamna")
if explicatie:
parti.append(explicatie)
rezolvare = camp(chunk, "rezolvare")
if rezolvare:
parti.append(rezolvare)
ce = "mesajul si captura" if cu_imagine else "mesajul"
trimis = (_URGENT if nivel == "imediat" else _NORMAL).format(ce=ce)
if notificat:
parti.append(f"{trimis} (referinta {ref}). Te contacteaza cineva.")
else:
# Nu promitem ce nu s-a intamplat: notificarea a esuat, dar e inregistrata.
parti.append(
f"Am inregistrat problema pentru echipa tehnica (referinta {ref}), dar "
f"nu am putut sa o trimit chiar acum. Daca te blocheaza, suna-ne."
)
parti.append(INTREBARE_URGENTA)
return "\n\n".join(parti)
# --- ce fel de completare e raspunsul omului --------------------------------
_URGENTA = re.compile(
r"\b(urgent|urgenta|foarte urgent|ma blocheaza|blocheaza|nu pot lucra|"
r"nu pot factura|nu merge nimic|sta toata firma|s-a oprit tot)\b"
)
_STARE = re.compile(
r"(cat mai dureaza|cat dureaza|cand ma contacteaza|nu m-?a contactat|"
r"nu m-?a sunat|tot nimic|nimic inca|chiar nimic|ati uitat|mai dureaza|"
r"s-a rezolvat|ce se intampla|vreo veste|veste|noutati|s-a miscat ceva|"
r"aveti un raspuns)"
)
def fel_completare(text: str) -> str:
"""`"urgenta"` / `"stare"` / `"detaliu"` — ce vrea omul cu mesajul asta.
Trei lucruri diferite primeau acelasi raspuns sablon („am adaugat si asta la
M-…"): un semnal de urgenta, o intrebare la care Maria nici nu raspundea, si un
detaliu in plus. Ordinea conteaza: „urgent, cat mai dureaza?" e intai urgenta.
"""
t = fold_text(text)
if _URGENTA.search(t):
return "urgenta"
if _STARE.search(t):
return "stare"
return "detaliu"
def fold_text(text: str) -> str:
return rank.fold(text or "")
# Confirmari rotite: trei mesaje identice unul dupa altul suna a robot.
_CONFIRMARI = (
"Am adaugat si asta la {ref}.",
"Notat — am trecut si asta in {ref}.",
"Am completat {ref} cu ce mi-ai spus.",
)
URGENTA_MARCATA = (
"Am marcat problema ca urgenta si am anuntat echipa ({ref}). "
"Daca intre timp te blocheaza complet, spune-mi."
)
def confirmare(ref: str, a_cata: int) -> str:
return _CONFIRMARI[a_cata % len(_CONFIRMARI)].format(ref=ref)
def minute_text(n: int) -> str:
"""„3 minute", dar „25 de minute" — regula romaneasca a lui „de" peste 19."""
return f"{n} minute" if n < 20 else f"{n} de minute"
# Cand omul intreaba a doua oara si nu s-a schimbat nimic, nu repetam acelasi text.
_ASTEPTARE = (
"Inca nimic nou de la echipa. Te anunt imediat ce am un raspuns.",
"Inca nu mi-a raspuns nimeni. Problema e in lista lor, cu {ref}.",
)
def raspuns_stare(ref: str, minute: int, reamintit: bool, telefon: str = "",
a_cata: int = 0) -> str:
"""Ce se raspunde la „cat mai dureaza?" — cu ce stim, nu cu formule.
Peste o jumatate de ora fara raspuns, „echipa vede detaliul cand preia
problema" nu mai e un raspuns onest: omul asteapta de o jumatate de ora si o
stie mai bine decat noi.
"""
unde = f" la {telefon}" if telefon else ""
if minute >= 30:
return (
f"Au trecut {minute_text(minute)} de cand am trimis problema ({ref}) si inca "
f"nu mi-a raspuns nimeni. Nu vreau sa te tin cu vorbe: daca te blocheaza, "
f"suna direct la suport{unde}."
)
if reamintit:
return (
f"Am trimis problema acum {minute_text(minute)} (referinta {ref}) si tocmai "
f"am reamintit echipei. Inca nu am un raspuns de la ei."
)
if a_cata > 0:
return _ASTEPTARE[(a_cata - 1) % len(_ASTEPTARE)].format(ref=ref)
return (
f"Am trimis problema acum {minute_text(minute)} (referinta {ref}) si inca nu am "
f"un raspuns de la ei. Echipa e deja anuntata."
)