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
This commit is contained in:
Claude Agent
2026-09-01 07:19:00 +00:00
parent 4d6ff1940f
commit 0ace9f97ac
6 changed files with 280 additions and 50 deletions

View File

@@ -80,10 +80,6 @@ TACERE_OK = (
'Am inteles, ma opresc. Scrie-mi "Maria, continua" cand vrei sa reiau.'
)
REVENIRE_OK = "Sunt aici. Cu ce te ajut?"
FOLLOWUP_OK = (
"Multumesc, am adaugat si asta la {ref} — echipa vede detaliul cand preia "
"problema."
)
def bridge_url() -> str:
@@ -346,40 +342,84 @@ def escalate(msg: dict, question: str, search_result: dict, fir: dict | None = N
return record
def append_followup(ref: str, msg: dict, text: str) -> bool:
"""Adauga raspunsul omului la escaladarea deschisa si il trimite la suport.
REAMINTIRE_MIN = 15 # cat de rar poate Maria sa reaminteasca aceeasi problema
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.
"""
def _fisier_escaladare(ref: str):
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:
if not d.exists():
return None
return next(iter(sorted(d.glob(f"*-{ref}.json"))), None)
def _notifica_suport(text: str) -> bool:
jid = config.get("SUPPORT_JID") or ""
if not jid:
return False
try:
requests.post(f"{bridge_url()}/send", json={"to": jid, "text": text}, timeout=20)
return True
except Exception as exc: # noqa: BLE001
print(f"[consumer] mesaj catre suport netrimis: {exc}", file=sys.stderr)
return False
def completeaza(ref: str, msg: dict, text: str) -> str | None:
"""Adauga mesajul la escaladarea deschisa si intoarce ce i se raspunde omului.
`None` daca escaladarea nu mai exista (atunci mesajul se trateaza normal).
Trei feluri de completare, cu trei raspunsuri diferite: un semnal de urgenta
schimba escaladarea si anunta echipa; o intrebare de stare primeste ce stim
(de cat timp asteapta, daca am reamintit); un detaliu primeste o confirmare
scurta, rotita, ca sa nu sune a robot la a treia oara.
"""
fisier = _fisier_escaladare(ref)
if fisier is None:
return None
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")})
return None
fel = triaj.fel_completare(text)
completari = record.setdefault("completari", [])
completari.append({"at": time.time(), "text": text, "fel": fel,
"message_id": msg.get("id")})
cine = msg.get("pushName") or msg.get("from") or "necunoscut"
minute = int((time.time() - record.get("at", time.time())) / 60)
if fel == "urgenta":
record["urgenta"] = True
trimis = _notifica_suport(
f"[Maria] URGENT — {ref} (de la {cine})\n{text[:800]}")
record["ultima_reamintire"] = time.time()
raspuns = triaj.URGENTA_MARCATA.format(ref=ref)
elif fel == "stare":
# Reamintirea e limitata: la fiecare „tot nimic" nu suna telefonul echipei.
de_reamintit = time.time() - record.get("ultima_reamintire", record.get("at", 0)) \
> REAMINTIRE_MIN * 60
reamintit = False
if de_reamintit:
reamintit = _notifica_suport(
f"[Maria] {ref}: clientul intreaba de {minute} minute daca s-a "
f"rezolvat.\n{text[:400]}")
if reamintit:
record["ultima_reamintire"] = time.time()
intrebari = sum(1 for c in completari if c.get("fel") == "stare")
raspuns = triaj.raspuns_stare(ref, minute, reamintit,
config.get("SUPPORT_PHONE", ""),
a_cata=intrebari - 1)
else:
_notifica_suport(f"[Maria] Completare la {ref} (de la {cine}):\n{text[:800]}")
raspuns = triaj.confirmare(ref, len(completari) - 1)
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
print(f"[consumer] completare '{fel}' la {ref}: {text[:60]!r}", file=sys.stderr)
return raspuns
_GRUPURI_CACHE: dict = {"at": 0.0, "date": {}}
@@ -483,6 +523,20 @@ def handle(index: Index, msg: dict) -> None:
send_reply(sender, triaj.CERE_DETALII)
return
# Firul are o escaladare deschisa: mesajul e completare la ea, nu intrebare
# noua. Inaintea confirmarii si a cautarii — raspunsul vine instant, deci un
# „caut informatia, revin imediat" ar fi o promisiune inutila, iar embedding-ul
# s-ar calcula degeaba.
if continuare and fir.get("ref") and text.strip():
raspuns = completeaza(fir["ref"], msg, text.strip())
if raspuns:
fir_mod.adauga(fir, "client", text)
fir_mod.adauga(fir, "maria", raspuns)
fir_mod.salveaza(fir)
send_reply(sender, raspuns)
return
fir["ref"] = None # escaladarea nu mai exista; tratam mesajul normal
send_reply(sender, ACK_IMAGE_TEXT if has_image else ACK_TEXT)
question, search_query, immediate = prepare_query(msg)
@@ -511,15 +565,6 @@ def handle(index: Index, msg: dict) -> None:
surse = ", ".join(f"{t['source']}({t['cosine']})" for t in result["top"]) or "-"
print(f"[consumer] rank: {surse} | {result['reason']}", file=sys.stderr)
# Firul are deja o escaladare deschisa: ce spune omul acum e completare la ea,
# nu o problema noua. De cate ori e nevoie, nu o singura data.
if continuare and fir.get("ref") and append_followup(fir["ref"], msg, text.strip() or "(captura)"):
raspuns = FOLLOWUP_OK.format(ref=fir["ref"])
fir_mod.adauga(fir, "maria", raspuns)
fir_mod.salveaza(fir)
send_reply(sender, raspuns)
return
if not result["covered"]:
print(f"[consumer] fara acoperire in documente -> suport ({result['reason']})", file=sys.stderr)
record = escalate(msg, question, result, fir)