diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
index 37dd625..88f11ef 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md
@@ -353,24 +353,28 @@ Fuziunea da mereu un clasament, si la o intrebare complet straina. De aceea deci
0. **intrebarea contine un cod de eroare** (`ORA-…`, `PLS-…`, `D406`) → decide
doar prezenta lui in chunk-uri, indiferent de cosinus;
-1. cosinus ≥ `RANK_STRONG_COSINE` → raspundem;
-2. cosinus < `RANK_WEAK_COSINE` → escaladam direct;
-3. intre ele → raspundem doar daca cel putin `RANK_MIN_RARE_RATIO` din termenii
+1. cosinus < `RANK_WEAK_COSINE` → escaladam direct;
+2. peste → raspundem doar daca cel putin `RANK_MIN_RARE_RATIO` din termenii
distinctivi ai intrebarii (coduri, cuvinte rare in corpus) apar chiar in
chunk-urile gasite.
-Fractiunea din pasul 3 nu e cosmetica: cu un singur termen gasit, „cum imi resetez
+Fractiunea din pasul 2 nu e cosmetica: cu un singur termen gasit, „cum imi resetez
parola de la Windows" trecea drept acoperita fiindca „parola" apare in documente.
-`RANK_STRONG_COSINE` a fost 0,70 si a lasat sa treaca doua raspunsuri inventate
+**Nu exista prag „cosinus destul de mare ca sa nu mai verificam".** A existat
+(`RANK_STRONG_COSINE`, 0,70) si a lasat sa treaca doua raspunsuri inventate
(2026-09-01, grupul „Maria Test"): „am o factura pe luna august cu eroare in SPV"
(0,768 — nu spune ce eroare e) si o captura cu `errorMessage="CUI cumparator
-incorect"` (0,778 — eroarea nu exista in documente). **Toate chunk-urile eFactura
-seamana intre ele**, deci pe 0,7x cosinusul nu mai distinge „e in documente" de „e
-despre eFactura"; e acelasi tip de greseala ca la codurile Oracle de mai sus, doar
-ca fara cod pe care sa te sprijini. La **0,80** trec doar potrivirile aproape
-textuale, iar restul cad pe dovada lexicala de la pasul 3. `ops/calibrate-rank.py`:
-25/25, cu ambele cazuri adaugate la set.
+incorect"` (0,778 — eroarea nu exista in documente). Toate chunk-urile eFactura
+seamana intre ele, deci pe 0,7x cosinusul nu mai distinge „e in documente" de „e
+despre eFactura".
+
+L-am scos, nu urcat, fiindca **cosinusul creste cu lungimea interogarii**: pe un fir
+de discutie se cauta dupa ancora plus mesajul nou, deci scorul urca la fiecare
+replica fara sa apara vreo dovada in plus. Aceeasi captura: 0,778 singura, 0,836
+impreuna cu mesajul de dinainte. Orice prag fix de sus e trecut de o discutie destul
+de lunga. `ops/calibrate-rank.py`: 26/26, si niciunul dintre cazurile cu raspuns in
+documente nu avea nevoie de scurtatura — toate trec pe dovada lexicala.
Modelul primeste `temperature=0` (`LLM_TEMPERATURE`). Fara ea, llama.cpp raspunde
cu 0,8 — creativitate exact acolo unde vrem sa se rezume la context.
@@ -393,9 +397,18 @@ Nu orice mesaj e o intrebare, si nu orice raspuns e util. `rag/triaj.py` ia trei
decizii inaintea modelului:
**Mesaj prea vag.** „Am o eroare" nu spune nimic. Maria cere operatiunea, ecranul si
-textul erorii (sau o captura) — nu ghiceste si nu deranjeaza suportul cu atat. Regula
-e ingusta deliberat: doar text scurt, fara captura si fara cod de eroare, in care apar
-cuvinte de acuza („nu merge", „imi da"). „Cum trimit D406" trece nestingherit.
+textul erorii (sau o captura) — nu ghiceste si nu deranjeaza suportul cu atat, fiindca
+programatorul ar pune exact aceeasi intrebare.
+
+Decizia se ia **dupa cautare, si numai cand nu exista acoperire in documente**. Inainte
+se lua inaintea cautarii, dupa lungime: text scurt (sub 12 cuvinte), fara captura si
+fara cod, cu cuvinte de acuza („nu merge", „imi da"). Gardul gresea in ambele sensuri
+— „Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULE"
+are 14 cuvinte si tot nu zice CE eroare e (a trecut, si a primit un raspuns inventat),
+iar „nu pot incarca factura in SPV, imi da eroare de certificat" are 9 si e in
+documente (era oprita degeaba). Lungimea nu masoara cat de precis e mesajul;
+acoperirea in documente da. Se cere o singura data pe fir: daca nici dupa ce a fost
+intrebat omul nu spune mai mult, mesajul pleaca la suport.
**Erori care oricum ajung la programatori.** Dictionarul de erori Oracle are pe fiecare
intrare campul `cand suni suportul`. Noua incep cu „Intotdeauna" sau „Imediat" — nu se
@@ -437,10 +450,20 @@ La o continuare: cautarea se face pe **ancora + mesajul nou**, modelul primeste
istoricul, mesajul se adauga la escaladarea deschisa (`completari`, de cate ori e
nevoie), iar „mesaj prea vag" nu se mai aplica — detaliile au fost deja cerute.
-**Ce rupe firul, necontestat:** o captura noua sau un cod de eroare diferit. Sunt
-singurele semne sigure ca omul a trecut la altceva. Schimbarea subiectului in cuvinte
-NU rupe firul: e prea usor de confundat cu o continuare, iar greseala aia produce
-exact tabelul de mai sus. In rest, firul expira dupa `FIR_TTL_MIN` (120 min).
+**Ce rupe firul:** un cod de eroare pe care firul nu-l are, sau o captura venita la
+mai mult de `FIR_IMAGINE_MIN` (5 min) dupa ultimul mesaj. Schimbarea subiectului in
+cuvinte NU rupe firul: e prea usor de confundat cu o continuare, iar greseala aia
+produce exact tabelul de mai sus. In rest, firul expira dupa `FIR_TTL_MIN` (120 min).
+
+Orice captura a rupt firul pana pe 2026-09-01, si era gresit exact in cazul cel mai
+frecvent: **omul scrie problema si trimite captura imediat dupa** (sau Maria tocmai
+i-a cerut-o). WhatsApp le livreaza ca doua mesaje, dar e un singur lucru. Tratate
+separat, textul se pierdea si captura se cauta doar pe OCR-ul ei; se deschideau doua
+fire, se puteau deschide doua escaladari pentru aceeasi problema, iar suportul primea
+doua mesaje fara legatura. Acum captura din fereastra de 5 minute continua firul:
+textul citit din ea intra in ancora (cu tot cu codurile lui), cautarea se face pe
+mesaj + captura, iar daca firul are deja o escaladare deschisa captura pleaca la
+aceeasi referinta — cu imaginea, nu doar cu textul citit din ea.
### Completarile: trei feluri, trei raspunsuri
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py
index c581da5..3feecc4 100755
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py
@@ -68,6 +68,11 @@ CAZURI: list[tuple[str, bool]] = [
("O) Pers. Juridice (798) O Netrimise DI Trimise (fara raspuns) [5 Timis (cu raspuns Erori) (1). "
"O Tamise (cu diferente Reg. V2.) (1)\nMesaj raspuns. | Factura cu id incarcare=6638295792 nu po] "
"| | ", False),
+ # aceeasi captura, dar cautata IMPREUNA cu mesajul care a precedat-o (fir.interogare):
+ # textul in plus urca cosinusul la 0,836 fara sa aduca vreo dovada noua.
+ ("Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULE\n"
+ "Mesaj raspuns. | Factura cu id incarcare=6638295792 nu po] "
+ "| | ", False),
("care e capitala Frantei", False),
("cat costa un bilet de avion la Paris", False),
("imi da eroare la imprimanta HP LaserJet, nu trage hartia", False),
@@ -119,13 +124,13 @@ def pregateste(cazuri: list[tuple[str, bool]]) -> tuple[consumer.Index, list[dic
return index, date
-def evalueaza(index: consumer.Index, date: list[dict], strong: float, weak: float,
+def evalueaza(index: consumer.Index, date: list[dict], weak: float,
min_ratio: float) -> tuple[int, list[str]]:
corecte, gresite = 0, []
for d in date:
ok, _ = rank.assess(
d["intrebare"], d["best"], d["top_texts"], index.bm25,
- strong=strong, weak=weak, min_ratio=min_ratio,
+ weak=weak, min_ratio=min_ratio,
)
if ok == d["asteptat"]:
corecte += 1
@@ -149,28 +154,24 @@ def main() -> None:
if args.sweep:
rezultate = []
- for strong in (0.62, 0.66, 0.68, 0.70, 0.72, 0.75):
- for weak in (0.54, 0.58, 0.60):
- for ratio in (0.0, 0.34, 0.5, 0.67, 1.0):
- if weak >= strong:
- continue
- corecte, _ = evalueaza(index, date, strong, weak, ratio)
- rezultate.append((corecte, strong, weak, ratio))
+ for weak in (0.50, 0.54, 0.58, 0.60, 0.64):
+ for ratio in (0.0, 0.34, 0.5, 0.67, 1.0):
+ corecte, _ = evalueaza(index, date, weak, ratio)
+ rezultate.append((corecte, weak, ratio))
rezultate.sort(key=lambda r: (-r[0], r[1]))
- print(f"{'corecte':>8} strong weak ratio")
- for corecte, strong, weak, ratio in rezultate[:15]:
- print(f"{corecte:>5}/{len(date)} {strong:.2f} {weak:.2f} {ratio:.2f}")
+ print(f"{'corecte':>8} weak ratio")
+ for corecte, weak, ratio in rezultate[:15]:
+ print(f"{corecte:>5}/{len(date)} {weak:.2f} {ratio:.2f}")
return
- strong, weak = rank.strong_cosine(), rank.weak_cosine()
- ratio = rank.min_rare_ratio()
- print(f"praguri curente: strong={strong} weak={weak} ratio={ratio}\n")
+ weak, ratio = rank.weak_cosine(), rank.min_rare_ratio()
+ print(f"praguri curente: weak={weak} ratio={ratio}\n")
for d in date:
ok, motiv = rank.assess(d["intrebare"], d["best"], d["top_texts"], index.bm25)
semn = "OK " if ok == d["asteptat"] else "GRESIT"
stare = "ACOPERIT " if ok else "ESCALADAT"
print(f"{semn} [{stare}] {d['intrebare'][:44]:46} {motiv}")
- corecte, gresite = evalueaza(index, date, strong, weak, ratio)
+ corecte, gresite = evalueaza(index, date, weak, ratio)
print(f"\n{corecte}/{len(date)} corecte")
for g in gresite:
print(f" - {g}")
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example
index 6f959af..a36a33d 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/env.example
@@ -32,10 +32,9 @@ OCR_TIMEOUT_S=60
# Chunk-urile se ordoneaza de doua ori — semantic (embeddings) si lexical (BM25) —
# iar clasamentele se fuzioneaza. Vezi rag/rank.py; valorile de mai jos sunt
# calibrate pe indexul real, nu alese din burta.
-# Peste RANK_STRONG_COSINE raspundem din documente fara alte conditii; sub
-# RANK_WEAK_COSINE escaladam direct; intre ele, raspundem doar daca termenii rari
+# Sub RANK_WEAK_COSINE escaladam direct; peste, raspundem doar daca termenii rari
# din intrebare (coduri de eroare, in general) chiar apar in chunk-urile gasite.
-RANK_STRONG_COSINE=0.80
+# Nu exista prag „cosinus destul de mare ca sa nu mai verificam" — vezi rag/rank.py.
RANK_WEAK_COSINE=0.58
RANK_RECALL_N=12
# Ce fractiune din termenii distinctivi ai intrebarii trebuie sa apara in
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
index 92ca654..a5299b4 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
@@ -57,7 +57,6 @@ DEFAULTS: dict[str, str] = {
"OCR_TIMEOUT_S": "60",
# Ordonarea chunk-urilor si pragul de la care se considera ca raspunsul chiar
# e in documente (vezi rag/rank.py pentru masuratoarea din spatele valorilor).
- "RANK_STRONG_COSINE": "0.80",
"RANK_WEAK_COSINE": "0.58",
"RANK_RECALL_N": "12",
"RANK_MIN_RARE_RATIO": "0.5",
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py
index 473b0d6..861d696 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/consumer.py
@@ -357,12 +357,16 @@ def _fisier_escaladare(ref: str):
return next(iter(sorted(d.glob(f"*-{ref}.json"))), None)
-def _notifica_suport(text: str) -> bool:
+def _notifica_suport(text: str, imagine: str | None = None) -> bool:
+ """Mesaj catre suport; cu `imagine`, chiar captura primita, nu doar textul citit."""
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)
+ if imagine and os.path.exists(imagine):
+ send_image(jid, imagine, text[:1000])
+ else:
+ 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)
@@ -416,7 +420,8 @@ def completeaza(ref: str, msg: dict, text: str) -> str | None:
config.get("SUPPORT_PHONE", ""),
a_cata=intrebari - 1)
else:
- _notifica_suport(f"[Maria] Completare la {ref} (de la {cine}):\n{text[:800]}")
+ _notifica_suport(f"[Maria] Completare la {ref} (de la {cine}):\n{text[:800]}",
+ imagine=(msg.get("media") or {}).get("path"))
raspuns = triaj.confirmare(ref, len(completari) - 1)
try:
@@ -517,22 +522,11 @@ def handle(index: Index, msg: dict) -> None:
react_seen(sender, msg.get("id"), msg.get("fromMe", False))
continuare = fir_mod.este_continuare(fir, text, has_image)
- # „Am o eroare", fara sa spuna care: nu ghicim si nu deranjam suportul — intrebam.
- # Doar la deschiderea firului: la o continuare, detaliile au fost deja cerute.
- if not continuare and triaj.prea_vag(text, has_image):
- print("[consumer] mesaj prea vag -> cer detalii", file=sys.stderr)
- fir = fir_mod.deschide(sender, text)
- fir_mod.adauga(fir, "client", text)
- fir_mod.adauga(fir, "maria", triaj.CERE_DETALII)
- fir_mod.salveaza(fir)
- 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():
+ if continuare and fir.get("ref") and text.strip() and not has_image:
raspuns = completeaza(fir["ref"], msg, text.strip())
if raspuns:
fir_mod.adauga(fir, "client", text)
@@ -552,7 +546,18 @@ def handle(index: Index, msg: dict) -> None:
return
if continuare:
- fir_mod.adauga(fir, "client", text)
+ fir_mod.adauga(fir, "client", text or "[captura de ecran]")
+ fir_mod.extinde_ancora(fir, msg.get("ocr_text") or "")
+ # Captura pe o escaladare deschisa e chiar detaliul care lipsea, nu o
+ # problema noua: pleaca la aceeasi referinta, cu tot cu imagine.
+ if fir.get("ref") and has_image:
+ raspuns = completeaza(fir["ref"], msg, msg.get("ocr_text") or text)
+ if raspuns:
+ 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
search_query = fir_mod.interogare(fir, search_query)
print(f"[consumer] continuare pe firul deschis; caut dupa {search_query[:90]!r}",
file=sys.stderr)
@@ -571,6 +576,17 @@ def handle(index: Index, msg: dict) -> None:
print(f"[consumer] rank: {surse} | {result['reason']}", file=sys.stderr)
if not result["covered"]:
+ # „Am o eroare la o factura", fara sa spuna care: nu avem ce cauta si nici
+ # ce trimite la suport — programatorul ar pune exact aceeasi intrebare.
+ # O singura data pe fir: daca nici dupa ce a fost intrebat nu spune mai
+ # mult, mesajul pleaca la om, care poate insista altfel.
+ if triaj.prea_vag(text, has_image) and not fir.get("detalii_cerute"):
+ fir["detalii_cerute"] = True
+ fir_mod.adauga(fir, "maria", triaj.CERE_DETALII)
+ fir_mod.salveaza(fir)
+ print("[consumer] mesaj prea vag -> cer detalii", file=sys.stderr)
+ send_reply(sender, triaj.CERE_DETALII)
+ return
print(f"[consumer] fara acoperire in documente -> suport ({result['reason']})", file=sys.stderr)
record = escalate(msg, question, result, fir)
sablon = ESCALATED_SENT if record["notified"] else ESCALATED_RECORDED
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/fir.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/fir.py
index ef73156..f38c6b4 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/fir.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/fir.py
@@ -43,6 +43,11 @@ def ttl_s() -> int:
return config.get_int("FIR_TTL_MIN", 120) * 60
+def imagine_fereastra_s() -> int:
+ """Cat de repede dupa un mesaj o captura mai e „acelasi lucru" (FIR_IMAGINE_MIN)."""
+ return config.get_int("FIR_IMAGINE_MIN", 5) * 60
+
+
def tacere_s() -> int:
"""Cat sta Maria deoparte dupa ce a preluat un om (FIR_TACERE_MIN)."""
return config.get_int("FIR_TACERE_MIN", 60) * 60
@@ -96,20 +101,42 @@ def adauga(fir: dict, rol: str, text: str) -> None:
def este_continuare(fir: dict | None, text: str, are_imagine: bool) -> bool:
"""Mesajul duce mai departe firul deschis, sau incepe altceva?
- O captura noua sau un cod pe care firul nu-l are inseamna subiect nou. In rest
- — inclusiv la un mesaj lung — e continuare: omul raspunde la ce l-a intrebat
- Maria, iar ancora nu strica o cautare care oricum are destul text al ei.
+ Un cod pe care firul nu-l are inseamna subiect nou. In rest — inclusiv la un
+ mesaj lung — e continuare: omul raspunde la ce l-a intrebat Maria, iar ancora
+ nu strica o cautare care oricum are destul text al ei.
+
+ Captura de ecran a fost la inceput semn sigur de subiect nou. E fals exact in
+ cazul cel mai frecvent: omul scrie „am o factura cu eroare la trimitere" si
+ trimite imediat captura, sau Maria tocmai i-a cerut-o. WhatsApp le livreaza ca
+ doua mesaje, dar e un singur lucru — iar tratate separat, textul se pierde si
+ captura se cauta pe OCR-ul ei singur (2026-09-01, grupul „Maria Test").
+ Departajarea o face timpul: o captura in primele `FIR_IMAGINE_MIN` minute
+ continua firul, una venita mult mai tarziu deschide altul.
"""
if not fir:
return False
if are_imagine:
- return False
+ return time.time() - fir.get("at", 0) <= imagine_fereastra_s()
noi = rank.codes(text)
if noi and any(c not in (fir.get("coduri") or []) for c in noi):
return False
return True
+def extinde_ancora(fir: dict, text: str, maxim: int = 2000) -> None:
+ """Captura sosita pe firul deschis aduce chiar textul erorii — intra in ancora.
+
+ Fara asta, ancora ramane reclamatia vaga care a deschis firul („am o eroare la
+ o factura"), iar continuarile de dupa captura se cauta tot dupa ea.
+ """
+ text = (text or "").strip()
+ if not text:
+ return
+ ancora = (fir.get("ancora") or "").strip()
+ fir["ancora"] = f"{ancora}\n{text}".strip()[-maxim:]
+ fir["coduri"] = list(dict.fromkeys((fir.get("coduri") or []) + rank.codes(text)))
+
+
def interogare(fir: dict, text: str) -> str:
"""Ce se cauta in index la o continuare: ancora firului plus mesajul nou."""
ancora = (fir.get("ancora") or "").strip()
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/rank.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/rank.py
index ad17234..ea69854 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/rank.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/rank.py
@@ -38,20 +38,6 @@ import config
RRF_K = 60 # constanta standard din Reciprocal Rank Fusion
-def strong_cosine() -> float:
- """Peste atat, singur cosinusul e de ajuns (RANK_STRONG_COSINE).
-
- Era 0,70 si a fost prea jos: pe 2026-09-01, „am o factura cu eroare in SPV"
- (0,768) si o captura cu `errorMessage="CUI cumparator incorect"` (0,778) au
- trecut drept acoperite, desi prima nu spune ce eroare e, iar a doua nu exista
- in documente. Toate chunk-urile eFactura seamana intre ele: 0,7x nu distinge
- „e in documente" de „e despre eFactura". La 0,80 trec doar potrivirile
- aproape textuale, restul cad pe dovada lexicala (termenii rari din intrebare
- chiar in chunk-uri). Verificat cu ops/calibrate-rank.py: 25/25.
- """
- return float(config.get("RANK_STRONG_COSINE", "0.80"))
-
-
def weak_cosine() -> float:
"""Sub atat nu salvam raspunsul nici cu potrivire lexicala (RANK_WEAK_COSINE)."""
return float(config.get("RANK_WEAK_COSINE", "0.58"))
@@ -229,7 +215,6 @@ def assess(
top_texts: list[str],
bm25: Bm25,
*,
- strong: float | None = None,
weak: float | None = None,
min_ratio: float | None = None,
) -> tuple[bool, str]:
@@ -237,18 +222,26 @@ def assess(
Cazuri, in ordine:
0. intrebarea contine coduri de eroare -> decide DOAR prezenta lor in
- chunk-uri, si cand cosinusul e mare (vezi mai jos);
- 1. cosinus mare -> raspundem, indiferent de lexical;
- 2. cosinus la mijloc, dar o fractiune suficienta din termenii rari ai
- intrebarii apare chiar in chunk-uri (tipic: un cod de eroare) -> raspundem;
- 3. altfel -> nu avem acoperire, intrebarea pleaca la suport.
+ chunk-uri;
+ 1. cosinus sub `weak` -> nici nu ne uitam mai departe, pleaca la suport;
+ 2. altfel -> raspundem doar daca o fractiune suficienta din termenii rari ai
+ intrebarii apare chiar in chunk-urile gasite.
Cazul 0 exista fiindca cosinusul mare NU e o dovada cand e vorba de coduri:
doua erori Oracle diferite se scriu aproape la fel („[Oracle][ODBC][Ora]ORA-…"),
deci un chunk despre ORA-12541 sta la 0,74 de o intrebare despre ORA-06550.
Fara asta, Maria raspundea sigur pe ea cu eroarea gresita, in loc sa escaladeze.
+
+ A existat si un prag „cosinus mare -> raspundem, indiferent de lexical"
+ (`RANK_STRONG_COSINE`, 0,70). L-am scos, nu urcat, si nu doar fiindca a lasat
+ sa treaca doua raspunsuri inventate pe 2026-09-01: **cosinusul creste cu
+ lungimea interogarii**. Pe un fir de discutie interogarea e ancora plus
+ mesajul nou, deci urca la fiecare replica fara sa aduca vreo dovada — aceeasi
+ captura a dat 0,778 singura si 0,836 cu mesajul de dinainte. Orice prag fix
+ de sus e trecut mai devreme sau mai tarziu de o discutie destul de lunga.
+ Verificat pe setul de cazuri: niciunul dintre cele cu raspuns in documente nu
+ avea nevoie de scurtatura — toate trec pe dovada lexicala.
"""
- strong = strong_cosine() if strong is None else strong
weak = weak_cosine() if weak is None else weak
min_ratio = min_rare_ratio() if min_ratio is None else min_ratio
cerute = codes(query)
@@ -261,13 +254,11 @@ def assess(
f"intrebarea are coduri ({', '.join(cerute[:4])}) care nu apar in "
f"documente (cosinus {best_cosine:.3f})"
)
- if best_cosine >= strong:
- return True, f"cosinus {best_cosine:.3f} >= {strong}"
if best_cosine < weak:
return False, f"cosinus {best_cosine:.3f} < {weak}"
rare = bm25.rare_terms(query)
if not rare:
- return False, f"cosinus {best_cosine:.3f} la limita, iar intrebarea nu are termeni distinctivi"
+ return False, f"cosinus {best_cosine:.3f}, iar intrebarea nu are termeni distinctivi"
blob = fold("\n".join(top_texts))
gasiti = [t for t in rare if t in blob]
if gasiti and len(gasiti) / len(rare) >= min_ratio:
@@ -277,6 +268,6 @@ def assess(
)
lipsa = [t for t in rare if t not in blob]
return False, (
- f"cosinus {best_cosine:.3f} la limita, iar termenii distinctivi "
+ f"cosinus {best_cosine:.3f}, iar termenii distinctivi "
f"({', '.join(lipsa[:5])}) nu apar in documente"
)
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py
index 4953b90..2b6c68f 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/triaj.py
@@ -66,18 +66,22 @@ def nivel_suport(chunk: str) -> str | 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.
+ Se intreaba DUPA cautare, si numai cand nu exista acoperire in documente —
+ altfel gardul taia si intrebari cu raspuns („nu pot incarca factura in SPV,
+ imi da eroare de certificat" e in documente, dar suna a reclamatie).
+
+ Avea si un plafon de 12 cuvinte, pe ideea ca un mesaj lung spune destul. Nu
+ spune: „Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere
+ AUTO SULE" are 14 si tot nu zice CE eroare e — a trecut de gard si a primit un
+ raspuns inventat (2026-09-01). Lungimea nu masoara cat de precis e mesajul;
+ acoperirea in documente da.
"""
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
+ return bool(_ACUZA.search(t))
CERE_DETALII = (
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_fir.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_fir.py
index 30af005..0c79267 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_fir.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_fir.py
@@ -27,14 +27,35 @@ def test_un_raspuns_scurt_e_continuare_nu_eroare_noua():
assert "ORA-06550" in q and "ma blocheaza" in q
-def test_captura_noua_sau_alt_cod_rup_firul():
+def test_alt_cod_rupe_firul():
f = fir.deschide(JID, EROARE)
assert not fir.este_continuare(f, "imi da acum ORA-12154", False)
- assert not fir.este_continuare(f, "si asta ce e?", True) # captura noua
# acelasi cod nu rupe firul
assert fir.este_continuare(f, "tot ORA-06550 imi da", False)
+def test_captura_imediat_dupa_mesaj_continua_firul():
+ """Cazul real: omul scrie problema si trimite captura la cateva secunde."""
+ f = fir.deschide(JID, "am o factura pe august cu eroare la trimitere")
+ assert fir.este_continuare(f, "", True)
+
+
+def test_captura_mult_mai_tarziu_deschide_alt_fir():
+ f = fir.deschide(JID, EROARE)
+ f["at"] = time.time() - fir.imagine_fereastra_s() - 1
+ assert not fir.este_continuare(f, "", True)
+
+
+def test_captura_pe_fir_aduce_textul_erorii_in_ancora():
+ """Ancora era reclamatia vaga; captura e cea care spune ce eroare e."""
+ f = fir.deschide(JID, "am o factura cu eroare la trimitere")
+ fir.extinde_ancora(f, "ORA-06550: line 1, column 7")
+ assert "ora-06550" in f["coduri"]
+ assert "eroare la trimitere" in f["ancora"] and "ORA-06550" in f["ancora"]
+ # continuarile de dupa captura se cauta cu tot cu eroare, nu doar cu reclamatia
+ assert "ORA-06550" in fir.interogare(f, "da, ma blocheaza")
+
+
def test_fara_fir_nu_exista_continuare():
assert not fir.este_continuare(None, "da", False)
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_rank.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_rank.py
index 9a7f333..9f523aa 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_rank.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_rank.py
@@ -118,9 +118,21 @@ def test_assess_intrebare_vaga_in_zona_de_mijloc_escaladeaza():
def test_pragurile_vin_din_env(monkeypatch):
import config
bm = rank.Bm25(DOCS)
- monkeypatch.setitem(config._env, "RANK_STRONG_COSINE", "0.50")
- covered, _ = rank.assess("orice", 0.55, DOCS[:1], bm)
- assert covered
+ monkeypatch.setitem(config._env, "RANK_WEAK_COSINE", "0.90")
+ covered, motiv = rank.assess("factura", 0.85, DOCS[:1], bm)
+ assert not covered and "0.9" in motiv
+
+
+def test_cosinusul_mare_singur_nu_mai_e_acoperire():
+ """Interogarea creste pe fir si cosinusul cu ea; dovada ramane cea lexicala.
+
+ Cazul real: aceeasi captura a dat 0,778 singura si 0,836 cu mesajul de
+ dinainte, fara sa apara vreo dovada noua in documente.
+ """
+ bm = rank.Bm25(DOCS)
+ covered, motiv = rank.assess("cumparator incorect la incarcare", 0.84, DOCS[:1], bm)
+ assert not covered
+ assert "cumparator" in motiv
# --- coduri de eroare: identificatori exacti, nu cuvinte ---------------------
diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_triaj.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_triaj.py
index b1c66eb..5cb3fcb 100644
--- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_triaj.py
+++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_triaj.py
@@ -49,6 +49,11 @@ def test_cand_notificarea_nu_a_plecat_nu_promitem_ca_a_plecat():
def test_prea_vag_doar_pentru_acuzatii_fara_continut():
assert triaj.prea_vag("am o eroare", False)
assert triaj.prea_vag("nu merge nimic", False)
+ # cazul real din 2026-09-01: lung, dar tot nu spune CE eroare e. Plafonul de
+ # 12 cuvinte il lasa sa treaca, si a primit un raspuns inventat.
+ assert triaj.prea_vag(
+ "Buna . Am si eu o factura pe luna august cu eroare in spv la trimitere AUTO SULE",
+ False)
# o intrebare adevarata nu se opreste ca sa i se ceara detalii
assert not triaj.prea_vag("cum trimit declaratia D406 SAF-T", False)
# un cod inseamna ca avem deja dupa ce cauta