Doua greseli din grupul "Maria Test", 2026-09-01, pe care 27409fb le-a atenuat dar
nu le-a rezolvat.
1. La o reclamatie vaga Maria cauta, in loc sa intrebe. "Buna . Am si eu o factura
pe luna august cu eroare in spv la trimitere AUTO SULE" nu spune CE eroare e —
n-are ce cauta in documente si n-are ce trimite la suport, fiindca programatorul
ar pune exact aceeasi intrebare. Gardul (triaj.prea_vag) exista, dar cerea text
sub 12 cuvinte; mesajul are 14. Gresea si invers: "nu pot incarca factura in SPV,
imi da eroare de certificat" are 9 cuvinte si ARE raspuns in documente, si era
oprita degeaba. Lungimea nu masoara cat de precis e mesajul.
-> gardul nu mai numara cuvinte si se aplica DUPA cautare, doar cand nu exista
acoperire. O singura data pe fir: daca nici intrebat omul nu spune mai mult,
mesajul pleaca la suport.
2. Textul si captura, trimise una dupa alta, erau tratate ca doua probleme fara
legatura. `este_continuare` rupea firul la ORICE imagine — dar cazul frecvent e
tocmai omul care scrie problema si trimite captura imediat dupa, sau care
raspunde la "trimite-mi o captura". Textul se pierdea, captura se cauta doar pe
OCR-ul ei, se deschideau doua fire si se puteau deschide doua escaladari pentru
aceeasi problema.
-> o captura in primele FIR_IMAGINE_MIN (5) minute continua firul: textul citit
din ea intra in ancora (cu tot cu coduri), cautarea se face pe mesaj +
captura, iar pe o escaladare deschisa pleaca la aceeasi referinta — cu
imaginea, nu doar cu textul citit din ea (_notifica_suport ia si media).
Si, ca urmare a lui (2): RANK_STRONG_COSINE dispare. Interogarea pe un fir e ancora
plus mesajul nou, deci cosinusul urca la fiecare replica fara sa apara vreo dovada
noua — aceeasi captura da 0,778 singura si 0,836 cu mesajul de dinainte, adica peste
0,80 pus ieri. Orice prag fix de sus e trecut de o discutie destul de lunga. Acoperirea
ramane pe dovada lexicala (termenii distinctivi ai intrebarii chiar in chunk-uri),
care e stabila la lungime. Niciun caz cu raspuns in documente nu avea nevoie de
scurtatura.
ops/calibrate-rank.py: 26/26, cu interogarea combinata adaugata la set. Teste: 102 pass.
Verificat end-to-end pe scenariul real (mesaj, apoi captura la 10 secunde): intrebare
de detalii, apoi o singura escaladare care poarta si textul si captura.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
167 lines
6.1 KiB
Python
167 lines
6.1 KiB
Python
"""Configuratie comuna a puntii WhatsApp+RAG pentru Maria (LXC 171 claude-agent).
|
|
|
|
Citeste ~/.maria-bridge/env (KEY=value, tolerant la comentarii si ghilimele).
|
|
Mirrors deliberat conventiile din discord-bridge/config.py, ca sa fie un singur
|
|
model de citit pentru cine intretine ambele punti pe acest container.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import fcntl
|
|
import os
|
|
import pathlib
|
|
|
|
# Directorul de baza poate fi mutat in teste prin MARIA_BRIDGE_DIR.
|
|
_DEFAULT_DIR = pathlib.Path.home() / ".maria-bridge"
|
|
|
|
STATE_DIR: pathlib.Path = pathlib.Path(os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR)
|
|
DOCS_DIR: pathlib.Path = STATE_DIR / "documents"
|
|
# Documente care NU vin din Drive. `rclone sync` sterge din DOCS_DIR tot ce nu
|
|
# exista in Drive, deci orice fisier pus acolo de mana dispare la urmatorul tur.
|
|
# Ce sta aici e in afara oglinzii si supravietuieste.
|
|
DOCS_LOCAL_DIR: pathlib.Path = STATE_DIR / "documents-local"
|
|
INDEX_FILE: pathlib.Path = STATE_DIR / "rag_index.json"
|
|
LOCK_FILE: pathlib.Path = STATE_DIR / ".rag.lock"
|
|
LOG_DIR: pathlib.Path = STATE_DIR / "logs"
|
|
ENV_FILE: pathlib.Path = STATE_DIR / "env"
|
|
AUTH_DIR: pathlib.Path = STATE_DIR / "whatsapp-auth"
|
|
|
|
_env: dict[str, str] = {}
|
|
|
|
# Valori implicite. LLM_URL/OLLAMA_URL presupun tunel/proxy local catre backend-ul
|
|
# real (vezi docs/maria-whatsapp-rag-prototype.md) — de completat in env dupa caz.
|
|
DEFAULTS: dict[str, str] = {
|
|
"BRIDGE_HOST": "127.0.0.1",
|
|
"BRIDGE_PORT": "8099",
|
|
"LLM_URL": "http://127.0.0.1:8091",
|
|
"OLLAMA_URL": "http://127.0.0.1:11434",
|
|
"EMBED_MODEL": "nomic-embed-text",
|
|
"TOP_K": "3",
|
|
"MAX_TOKENS": "250",
|
|
"LLM_TEMPERATURE": "0",
|
|
"POLL_INTERVAL_S": "2",
|
|
"TEST_MODE_SELF_CHAT_ONLY": "true",
|
|
# Grupuri in care Maria are voie sa raspunda (JID-uri separate prin virgula).
|
|
# Restul grupurilor sunt ignorate, si de punte, si de consumer.
|
|
"ALLOWED_GROUP_JIDS": "",
|
|
# Firul de discutie (vezi rag/fir.py): cat tine minte, cat tace dupa ce a
|
|
# preluat un om, si de cati participanti e nevoie ca preluarea sa se aplice
|
|
# automat (in self-chat si in grupul de test totul e `fromMe`).
|
|
"FIR_TTL_MIN": "120",
|
|
"FIR_TACERE_MIN": "60",
|
|
"FIR_PRELUARE_MIN_PARTICIPANTI": "2",
|
|
# OCR pentru capturile de ecran primite pe WhatsApp (vezi rag/ocr.py).
|
|
"OCR_LANGS": "ron+eng",
|
|
"OCR_MAX_CHARS": "1500",
|
|
"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_WEAK_COSINE": "0.58",
|
|
"RANK_RECALL_N": "12",
|
|
"RANK_MIN_RARE_RATIO": "0.5",
|
|
# Unde pleaca intrebarile fara raspuns: JID de WhatsApp, ex.
|
|
# "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).
|
|
# Gol = sincronizare dezactivata, doar upload manual din dashboard.
|
|
"DRIVE_REMOTE": "",
|
|
}
|
|
|
|
|
|
def parse_env(text: str) -> dict[str, str]:
|
|
"""Parseaza un fisier de tip KEY=value. Nu arunca niciodata."""
|
|
out: dict[str, str] = {}
|
|
for raw in text.splitlines():
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if line.startswith("export "):
|
|
line = line[len("export "):].strip()
|
|
if "=" not in line:
|
|
continue
|
|
key, _, val = line.partition("=")
|
|
key = key.strip()
|
|
if not key:
|
|
continue
|
|
val = val.strip()
|
|
if val[:1] not in ("'", '"'):
|
|
cut = val.find(" #")
|
|
if cut >= 0:
|
|
val = val[:cut].rstrip()
|
|
if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
|
|
val = val[1:-1]
|
|
out[key] = val
|
|
return out
|
|
|
|
|
|
def reload(base_dir: str | os.PathLike | None = None) -> dict[str, str]:
|
|
"""Recalculeaza caile si reciteste env-ul. Returneaza dictionarul incarcat."""
|
|
global STATE_DIR, DOCS_DIR, DOCS_LOCAL_DIR, INDEX_FILE, LOCK_FILE, LOG_DIR, ENV_FILE, AUTH_DIR, _env
|
|
if base_dir is None:
|
|
base_dir = os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR
|
|
STATE_DIR = pathlib.Path(base_dir)
|
|
DOCS_DIR = STATE_DIR / "documents"
|
|
DOCS_LOCAL_DIR = STATE_DIR / "documents-local"
|
|
INDEX_FILE = STATE_DIR / "rag_index.json"
|
|
LOCK_FILE = STATE_DIR / ".rag.lock"
|
|
LOG_DIR = STATE_DIR / "logs"
|
|
ENV_FILE = STATE_DIR / "env"
|
|
AUTH_DIR = STATE_DIR / "whatsapp-auth"
|
|
try:
|
|
_env = parse_env(ENV_FILE.read_text(encoding="utf-8"))
|
|
except OSError:
|
|
_env = {}
|
|
return _env
|
|
|
|
|
|
def get(key: str, default: str | None = None) -> str | None:
|
|
if key in _env:
|
|
return _env[key]
|
|
if key in DEFAULTS:
|
|
return DEFAULTS[key]
|
|
return default
|
|
|
|
|
|
def get_int(key: str, default: int) -> int:
|
|
try:
|
|
return int(get(key, str(default)) or default)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
reload()
|
|
|
|
|
|
class Busy(RuntimeError):
|
|
"""Alta reindexare e deja in curs."""
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def exclusive():
|
|
"""Lacat intre PROCESE pentru reindexare (timer vs dashboard vs rulare manuala).
|
|
|
|
Reindexarea dureaza minute (embeddings pe CPU). Fara lacat, `maria-sync.timer`
|
|
poate porni peste o sincronizare manuala si ambele scriu acelasi rag_index.json.
|
|
Nu asteptam: a doua rulare se anuleaza curat, fiindca oricum ar reface acelasi
|
|
lucru imediat dupa.
|
|
"""
|
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
fh = open(LOCK_FILE, "w", encoding="utf-8")
|
|
try:
|
|
try:
|
|
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError:
|
|
raise Busy("o reindexare e deja in curs")
|
|
fh.write(str(os.getpid()))
|
|
fh.flush()
|
|
yield
|
|
finally:
|
|
fh.close()
|