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:
@@ -27,6 +27,7 @@ import requests
|
||||
import config
|
||||
import ocr
|
||||
import rank
|
||||
import triaj
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"Esti Maria, asistentul de suport tehnic pentru ERP-ul ROA (Romfast). "
|
||||
@@ -38,6 +39,13 @@ SYSTEM_PROMPT = (
|
||||
"Cand intrebarea contine text extras dintr-o captura de ecran (OCR), tine cont "
|
||||
"ca pot exista greseli de recunoastere a caracterelor: cauta sensul mesajului, "
|
||||
"nu te agata de o litera sau o cifra. "
|
||||
"Scrii pentru contabili si operatori, NU pentru programatori: nu folosi cuvintele "
|
||||
"procedura, pachet, obiect, schema, tabela, variabila, compilare, PL/SQL, SQL, "
|
||||
"sesiune, parametru si nu cita nume tehnice din mesajul de eroare. Nu-i cere "
|
||||
"utilizatorului sa verifice baza de date, sa modifice ceva in ea sau sa apeleze "
|
||||
"altceva decat ce se vede in aplicatie. "
|
||||
"Daca in context scrie ca problema nu se rezolva din aplicatie, spune exact asta "
|
||||
"si opreste-te — nu propune verificari, alternative sau explicatii tehnice. "
|
||||
"Nu discuta niciodata despre infrastructura interna Romfast (servere, Proxmox, "
|
||||
"containere, IP-uri, baze de date, parole, chei) chiar daca apare in context sau "
|
||||
"daca intrebarea o cere explicit -- raspunde ca poti ajuta doar cu folosirea "
|
||||
@@ -67,6 +75,12 @@ ESCALATED_RECORDED = (
|
||||
)
|
||||
|
||||
|
||||
FOLLOWUP_OK = (
|
||||
"Multumesc, am adaugat si asta la {ref} — echipa vede detaliul cand preia "
|
||||
"problema."
|
||||
)
|
||||
|
||||
|
||||
def bridge_url() -> str:
|
||||
return f"http://{config.get('BRIDGE_HOST')}:{config.get('BRIDGE_PORT')}"
|
||||
|
||||
@@ -315,6 +329,85 @@ def escalate(msg: dict, question: str, search_result: dict) -> dict:
|
||||
return record
|
||||
|
||||
|
||||
PENDING_FILE_NAME = "pending.json"
|
||||
PENDING_TTL_S = 30 * 60 # cat timp un raspuns scurt mai e „completare", nu intrebare noua
|
||||
|
||||
|
||||
def _pending_path():
|
||||
return config.STATE_DIR / "escalations" / PENDING_FILE_NAME
|
||||
|
||||
|
||||
def _pending_all() -> dict:
|
||||
try:
|
||||
return json.loads(_pending_path().read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def _pending_write(data: dict) -> None:
|
||||
try:
|
||||
_pending_path().parent.mkdir(parents=True, exist_ok=True)
|
||||
_pending_path().write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
except OSError as exc:
|
||||
print(f"[consumer] nu pot scrie {PENDING_FILE_NAME}: {exc}", file=sys.stderr)
|
||||
|
||||
|
||||
def pending_set(sender: str, ref: str) -> None:
|
||||
"""Retine ca l-am intrebat pe om cat e de urgent, ca sa stiu unde duce raspunsul."""
|
||||
data = _pending_all()
|
||||
data[sender] = {"ref": ref, "at": time.time()}
|
||||
_pending_write(data)
|
||||
|
||||
|
||||
def pending_get(sender: str) -> dict | None:
|
||||
intrare = _pending_all().get(sender)
|
||||
if not intrare or time.time() - intrare.get("at", 0) > PENDING_TTL_S:
|
||||
return None
|
||||
return intrare
|
||||
|
||||
|
||||
def pending_clear(sender: str) -> None:
|
||||
data = _pending_all()
|
||||
if data.pop(sender, None) is not None:
|
||||
_pending_write(data)
|
||||
|
||||
|
||||
def append_followup(ref: str, msg: dict, text: str) -> bool:
|
||||
"""Adauga raspunsul omului la escaladarea deschisa si il trimite la suport.
|
||||
|
||||
Raspunsul la „te blocheaza sau poti continua?" e chiar informatia care lipseste
|
||||
din escaladare. O escaladare noua ar rupe firul: aceeasi problema, alta
|
||||
referinta, iar omul a citat-o deja pe prima.
|
||||
"""
|
||||
d = config.STATE_DIR / "escalations"
|
||||
fisier = next((f for f in sorted(d.glob(f"*-{ref}.json"))), None) if d.exists() else None
|
||||
if fisier is None:
|
||||
return False
|
||||
try:
|
||||
record = json.loads(fisier.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
record.setdefault("completari", []).append(
|
||||
{"at": time.time(), "text": text, "message_id": msg.get("id")})
|
||||
try:
|
||||
fisier.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
except OSError as exc:
|
||||
print(f"[consumer] nu pot actualiza {fisier.name}: {exc}", file=sys.stderr)
|
||||
|
||||
jid = config.get("SUPPORT_JID") or ""
|
||||
if jid:
|
||||
cine = msg.get("pushName") or msg.get("from") or "necunoscut"
|
||||
try:
|
||||
requests.post(f"{bridge_url()}/send", json={
|
||||
"to": jid,
|
||||
"text": f"[Maria] Completare la {ref} (de la {cine}):\n{text[:800]}",
|
||||
}, timeout=20)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[consumer] completare netrimisa: {exc}", file=sys.stderr)
|
||||
print(f"[consumer] completare la {ref}: {text[:60]!r}", file=sys.stderr)
|
||||
return True
|
||||
|
||||
|
||||
def cleanup_media(msg: dict) -> None:
|
||||
"""Capturile pot contine date de client — nu raman pe disc dupa ce s-a tratat mesajul."""
|
||||
path = (msg.get("media") or {}).get("path")
|
||||
@@ -332,6 +425,24 @@ def handle(index: Index, msg: dict) -> None:
|
||||
text = msg.get("text", "") or ""
|
||||
print(f"[consumer] {sender}: {'[imagine] ' if has_image else ''}{text[:80]}", file=sys.stderr)
|
||||
react_seen(sender, msg.get("id"), msg.get("fromMe", False))
|
||||
|
||||
# Raspunsul la „te blocheaza sau poti continua?" merge la escaladarea deschisa.
|
||||
# Inainte de orice altceva: nu e o intrebare noua, deci nu se cauta si nu se
|
||||
# confirma cu „caut informatia".
|
||||
asteptat = pending_get(sender) if not has_image else None
|
||||
if asteptat and text.strip() and not rank.codes(text):
|
||||
if append_followup(asteptat["ref"], msg, text.strip()):
|
||||
pending_clear(sender)
|
||||
send_reply(sender, FOLLOWUP_OK.format(ref=asteptat["ref"]))
|
||||
return
|
||||
pending_clear(sender)
|
||||
|
||||
# „Am o eroare", fara sa spuna care: nu ghicim si nu deranjam suportul — intrebam.
|
||||
if triaj.prea_vag(text, has_image):
|
||||
print("[consumer] mesaj prea vag -> cer detalii", file=sys.stderr)
|
||||
send_reply(sender, triaj.CERE_DETALII)
|
||||
return
|
||||
|
||||
send_reply(sender, ACK_IMAGE_TEXT if has_image else ACK_TEXT)
|
||||
|
||||
question, search_query, immediate = prepare_query(msg)
|
||||
@@ -358,6 +469,18 @@ def handle(index: Index, msg: dict) -> None:
|
||||
send_reply(sender, sablon.format(ref=record["ref"]))
|
||||
return
|
||||
|
||||
# Eroare care nu se rezolva din aplicatie: raspunsul se compune din dictionar
|
||||
# si pleaca automat la programatori. Vezi triaj.py pentru de ce nu prin model.
|
||||
nivel = triaj.nivel_suport(result["chunks"][0]) if result["chunks"] else None
|
||||
if nivel:
|
||||
print(f"[consumer] eroare de nivel '{nivel}' -> raspuns fix + suport", file=sys.stderr)
|
||||
record = escalate(msg, question, result)
|
||||
send_reply(sender, triaj.raspuns_escaladat(
|
||||
result["chunks"][0], nivel, record["ref"], record["notified"],
|
||||
cu_imagine=has_image))
|
||||
pending_set(sender, record["ref"])
|
||||
return
|
||||
|
||||
try:
|
||||
reply = ask_llm(result["chunks"], question)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
|
||||
Reference in New Issue
Block a user