Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
Claude Agent 0ace9f97ac 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
2026-09-01 07:19:06 +00:00

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",
"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_STRONG_COSINE": "0.70",
"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()