Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py
Claude Agent 610638ad30 feat(maria): profil de client (firma), asociat cu mai multe numere
Un client poate avea mai multi angajati care scriu pe WhatsApp de pe
numere diferite; toti gasesc acum acelasi profil (rag/client.py,
~/.maria-bridge/clients.json). Profilul intra in prompt sub o sectiune
DESPRE CLIENT, la fiecare mesaj, stateless ca restul Mariei — dar nu
atinge cautarea/scorul din rag/rank.py, ca sa nu strice pragurile deja
calibrate. Administrare din dashboard (sectiune noua "Clienti cunoscuti"),
nu din chat, ca sa ramana pe canalul autentificat.

117/117 teste maria-whatsapp-bridge + teste noi pentru rutele de dashboard.

Co-Authored-By: Claude Agent <noreply@anthropic.com>
2026-09-08 10:35:30 +00:00

169 lines
6.2 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"
CLIENTS_FILE: pathlib.Path = STATE_DIR / "clients.json"
_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, CLIENTS_FILE, _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"
CLIENTS_FILE = STATE_DIR / "clients.json"
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()