"""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" 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", # 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, 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" 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()