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:
@@ -64,6 +64,9 @@ DEFAULTS: dict[str, str] = {
|
||||
# "40712345678@s.whatsapp.net" sau "1203...@g.us" pentru un grup.
|
||||
# Gol = escaladarile se scriu doar in jurnal (~/.maria-bridge/escalations/).
|
||||
"SUPPORT_JID": "",
|
||||
# Numarul de telefon al suportului, spus omului cand asteapta de prea mult
|
||||
# timp fara ca cineva sa preia (vezi triaj.raspuns_stare). Gol = nu se da.
|
||||
"SUPPORT_PHONE": "",
|
||||
# Tinta rclone pentru sincronizarea depozitului de documente, ex:
|
||||
# "gdrive,root_folder_id=1C4e75zgH1_7ZK-_oBP5ZZBvUPh3iEo1O:" (dosarul
|
||||
# document_store din Drive, vazut pe Windows ca D:\GoogleDrive\romfast\document_store).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -127,3 +127,93 @@ def raspuns_escaladat(chunk: str, nivel: str, ref: str, notificat: bool,
|
||||
)
|
||||
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."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user