Dosarul document_store din Drive are 3 surse .xml pe care depozitul le ignora
complet, fiindca store.py accepta doar .txt/.md. La d406_saft_knowledge exista
ambele formate, iar .xml e cu trei luni mai nou (2026-01-28 vs 2025-10-15) si cu
50% mai mare (64 KB vs 41 KB) — deci indexam varianta mai saraca.
- store.py devine sursa unica pentru extensii (DOC_EXTENSIONS = .txt/.md/.xml).
Cand acelasi nume de baza exista in mai multe formate, la indexare intra unul
singur, cel mai bogat (.xml > .md > .txt); celalalt ramane pe disc, marcat
`shadowed_by`. Fara asta, acelasi raspuns ar aparea de doua ori in rezultate.
`list_documents()` arata tot (dashboard), `documents_for_index()` doar
castigatorii (indexer).
- indexer.py taie XML-ul altfel: un chunk per element de nivel 1, adica o
problema = un chunk, cu <mesaj_eroare> si <rezolvare> impreuna. Taierea pe
linii goale le-ar separa si cautarea ar returna eroarea fara raspuns.
Etichetele raman prefixe lizibile ("mesaj eroare: ..."), fara paranteze
unghiulare care doar dilueaza embedding-ul. XML invalid nu opreste indexarea:
cade pe taierea obisnuita, cu o linie in log. Elementele peste 4000 de
caractere se taie mai departe pe granite de cuvant — `chunk_text` imparte doar
pe linii goale, deci un element scris ca un paragraf lung ar fi ramas intreg
(prins de test).
- sync.py: amprenta si `rclone --include` derivate din DOC_EXTENSIONS.
- dashboard: acelasi filtru si aceeasi preferinta (copie, fiindca nu poate
importa `store` — coliziune de nume pe `config`), plus marcajul "umbrit de X"
in tabelul de documente si numarul de documente chiar indexate.
- README: sectiunea Drive rescrisa pe `rclone authorize` (autorizezi pe o masina
cu browser, muti tokenul) in loc de cont de serviciu — mai putini pasi, fara
consola Google Cloud. Documentat si ca `sync` sterge local ce nu mai e in Drive.
tests/ nou (20 de teste, fara retea si fara Ollama): preferinta de format,
vizibilitatea in dashboard, taierea XML, entitati, comentarii, XML invalid,
elemente uriase. Suita puntii Discord: 426 pass, neafectata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
94 lines
3.3 KiB
Python
94 lines
3.3 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__":
|
|
out = sync_and_reindex(force="--force" in sys.argv[1:])
|
|
print(json.dumps(out, ensure_ascii=False), file=sys.stderr)
|