feat(maria): triaj — raspuns pentru utilizatori, escaladare automata la programatori
Pe captura cu ORA-06550, avand in context chiar intrarea din dictionar care spune
"nu se rezolva din aplicatie", modelul i-a raspuns unui contabil sa verifice
schema de date si sa foloseasca "procedura alternativa MI_pack_parteneri_old" —
inventata. Si nu escalada, fiindca escaladarea se declansa doar cand NU exista
acoperire in documente, iar aici exista.
rag/triaj.py, trei decizii inaintea modelului:
- **Mesaj prea vag.** "Am o eroare" -> Maria cere operatiunea, ecranul si textul
erorii, in loc sa ghiceasca. Regula e ingusta: text scurt, fara captura, fara cod.
- **Erori marcate "Intotdeauna"/"Imediat" in dictionar** (9 din 29) -> raspunsul se
compune din campurile dictionarului, fara model, si pleaca automat la suport cu
referinta si captura. Textul de acolo e scris pentru clienti; parafrazarea lui nu
adauga nimic si poate strica tot.
- **Urgenta se intreaba, nu se deduce.** Raspunsul ("ma blocheaza" / "pot continua")
se ataseaza escaladarii deschise, nu deschide alta referinta. Fereastra 30 min.
In plus: SYSTEM_PROMPT interzice explicit vocabularul tehnic (procedura, schema,
tabela, PL/SQL, compilare, sesiune) si propunerile de verificat baza de date; cele
mai tehnice cinci intrari din dictionar rescrise in limbaj de client.
Teste: 85 pass. Calibrare 21/21. Reindexare: 5 embeddings noi, 164 refolosite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
129
proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py
Normal file
129
proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user