Add Maria WhatsApp+RAG bridge as a service (LXC 171)
Move the /tmp prototype (Baileys bridge + RAG consumer) into git as a proper sibling project to discord-bridge/: own systemd --user units (whatsapp bridge, rag consumer, dashboard, periodic Drive sync timer), a filesystem document store with a stdlib control dashboard (start/ stop/restart, document CRUD, reindex, Google Drive sync via rclone), and an idempotent ops/install.sh following the same conventions. Co-Authored-By: Claude Agent <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
#!/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
|
||||
|
||||
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 (".txt", ".md"):
|
||||
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),
|
||||
"--include", "*.txt", "--include", "*.md"],
|
||||
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__":
|
||||
out = sync_and_reindex(force="--force" in sys.argv[1:])
|
||||
print(json.dumps(out, ensure_ascii=False), file=sys.stderr)
|
||||
Reference in New Issue
Block a user