Files
ROMFASTSQL/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py
Claude Agent 3b7069e954 fix(maria): lacat intre reindexari, scriere atomica a indexului, XML invalid raportat
Descoperit la prima sincronizare reala din Drive: `maria-sync.timer` a pornit
peste rularea manuala si doua procese faceau embeddings in paralel pe acelasi
Ollama, ambele urmand sa scrie acelasi rag_index.json. Embedding-ul a incetinit
de la ~7s la ~20s din concurenta, iar ultimul care termina ar fi suprascris
munca celuilalt.

- config.exclusive(): lacat `flock` intre procese, luat la intrarea in sync.py si
  indexer.py. Nu asteapta — a doua rulare iese curat cu "o reindexare e deja in
  curs", fiindca ar reface exact acelasi lucru. Verificat pe procese reale.
- indexer scrie indexul atomic (tmp + os.replace): consumer-ul reciteste fisierul
  la 30s si putea prinde un JSON pe jumatate scris.
- build() intoarce `warnings` pentru XML-urile care nu se pot parsa, iar rularea
  din linia de comanda le scrie in stderr. Pana acum, un XML invalid se indexa
  tacut ca text simplu, cu o singura linie pierduta in log.

Context de performanta, masurat pe LXC 171 fara alta incarcare: un embedding
`nomic-embed-text` ia ~7,3s, deci o reindexare completa a celor 173 de chunk-uri
dureaza ~21 de minute — mai mult decat intervalul timer-ului. Nu e o problema
practica (amprenta reindexeaza doar la schimbare, iar lacatul opreste
suprapunerea), dar explica de ce prima rulare pare blocata.

docs/rclone-google-drive-headless.md: procedura de conectare a unui container
headless la Drive prin `rclone authorize`, cu transcriptul rularii reale de pe
Windows, capcanele (sync e distructiv pe destinatie, connection string in loc de
cale pe nume, unde stau secretele) si de ce nu contul de serviciu. Indexata in
CLAUDE.md.

6 teste noi (lacat, eliberare la exceptie, scriere atomica, avertismente).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-08-31 18:42:01 +00:00

98 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Sincronizeaza depozitul de documente cu un remote rclone (Google Drive) si
reconstruieste indexul RAG DOAR daca s-a schimbat efectiv ceva pe disc.
De ce prin rclone si nu direct cu Google Drive API: containerul e headless
(fara browser pentru OAuth interactiv) — rclone se configureaza o data cu un
cont de serviciu (`rclone config`, tip `drive`, `service_account_file=...`),
vezi README.md, sectiunea "Sincronizare cu Google Drive".
Config (`~/.maria-bridge/env`, vezi ops/env.example):
DRIVE_REMOTE - tinta rclone, ex: gdrive:romfast/document_store
(gol = sincronizare dezactivata, doar upload manual din dashboard)
"""
from __future__ import annotations
import hashlib
import json
import subprocess
import sys
import time
import config
import indexer
import store
STATE_FILE_NAME = ".sync_state.json"
def _fingerprint() -> str:
"""Amprenta continutului depozitului (nume+mtime+marime), ca sa reindexam
doar cand s-a schimbat efectiv ceva, nu la fiecare tur de sincronizare."""
config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
h = hashlib.sha256()
for f in sorted(config.DOCS_DIR.glob("*")):
if f.is_file() and f.suffix in store.DOC_EXTENSIONS:
st = f.stat()
h.update(f"{f.name}:{st.st_mtime_ns}:{st.st_size}\n".encode())
return h.hexdigest()
def _sync_state_path():
return config.STATE_DIR / STATE_FILE_NAME
def read_sync_state() -> dict:
try:
return json.loads(_sync_state_path().read_text(encoding="utf-8"))
except (OSError, ValueError):
return {}
def _write_state(fingerprint: str, extra: dict | None = None) -> None:
data = {"fingerprint": fingerprint, "synced_at": time.time()}
if extra:
data.update(extra)
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
_sync_state_path().write_text(json.dumps(data), encoding="utf-8")
def pull_from_drive() -> dict:
"""`rclone sync <DRIVE_REMOTE> -> DOCS_DIR`. Fara remote configurat, e no-op."""
remote = config.get("DRIVE_REMOTE")
if not remote:
return {"ok": True, "skipped": "DRIVE_REMOTE nesetat in env"}
config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
try:
r = subprocess.run(
["rclone", "sync", remote, str(config.DOCS_DIR)]
+ [arg for ext in store.DOC_EXTENSIONS for arg in ("--include", f"*{ext}")],
capture_output=True, text=True, timeout=300,
)
except FileNotFoundError:
return {"ok": False, "error": "rclone nu e instalat — vezi README.md"}
except subprocess.TimeoutExpired:
return {"ok": False, "error": "rclone a depasit timpul (300s)"}
return {"ok": r.returncode == 0, "stdout": r.stdout[-2000:], "stderr": r.stderr[-2000:]}
def sync_and_reindex(force: bool = False) -> dict:
pulled = pull_from_drive()
fp = _fingerprint()
last = read_sync_state().get("fingerprint")
if not force and fp == last:
return {"pulled": pulled, "reindexed": False, "reason": "fara schimbari"}
result = indexer.build()
_write_state(fp, {"last_build": result})
return {"pulled": pulled, "reindexed": True, "build": result}
if __name__ == "__main__":
try:
with config.exclusive():
out = sync_and_reindex(force="--force" in sys.argv[1:])
except config.Busy as exc:
out = {"skipped": str(exc)}
print(json.dumps(out, ensure_ascii=False), file=sys.stderr)