diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py index f13ee01..e494bdc 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py @@ -61,6 +61,9 @@ MARIA_DOC_EXTENSIONS = (".txt", ".md", ".xml") _MARIA_FORMAT_RANK = {".xml": 0, ".md": 1, ".txt": 2} MARIA_STATE_DIR = Path(os.environ.get("MARIA_BRIDGE_DIR") or (Path.home() / ".maria-bridge")) MARIA_DOCS_DIR = MARIA_STATE_DIR / "documents" +# Documentele care nu vin din Drive (rclone sync sterge din MARIA_DOCS_DIR tot ce +# nu exista in Drive). Oglinda lui rag/config.py:DOCS_LOCAL_DIR. +MARIA_DOCS_LOCAL_DIR = MARIA_STATE_DIR / "documents-local" MARIA_INDEX_FILE = MARIA_STATE_DIR / "rag_index.json" MARIA_ENV_FILE = MARIA_STATE_DIR / "env" MARIA_LOG_DIR = MARIA_STATE_DIR / "logs" @@ -144,10 +147,14 @@ def maria_documents() -> list[dict]: Nu importam modulul acela: si el, si `discord-bridge/config.py` s-ar numi `config`. """ MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True) - files = [ - f for f in sorted(MARIA_DOCS_DIR.glob("*")) - if f.is_file() and f.suffix in MARIA_DOC_EXTENSIONS - ] + vazute: dict[str, Path] = {} + for d in (MARIA_DOCS_LOCAL_DIR, MARIA_DOCS_DIR): # Drive ultimul = castiga la acelasi nume + if not d.is_dir(): + continue + for f in sorted(d.glob("*")): + if f.is_file() and f.suffix in MARIA_DOC_EXTENSIONS: + vazute[f.name] = f + files = [vazute[n] for n in sorted(vazute)] winners: dict[str, str] = {} for f in files: best = winners.get(f.stem) @@ -162,6 +169,7 @@ def maria_documents() -> list[dict]: "name": f.name, "size": st.st_size, "mtime": st.st_mtime, + "local": f.parent == MARIA_DOCS_LOCAL_DIR, "shadowed_by": None if winner == f.name else winner, }) return out @@ -206,18 +214,29 @@ def _maria_validate_name(name: str) -> str: def maria_write_document(name: str, content: str) -> None: + """Scrie in documents-local/, nu in oglinda Drive. + + Pana acum ajungea in `documents/` si disparea la urmatorul `rclone sync` — + documentul adaugat din dashboard traia pana la 10 minute. + """ name = _maria_validate_name(name) - MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True) - (MARIA_DOCS_DIR / name).write_text(content, encoding="utf-8") + MARIA_DOCS_LOCAL_DIR.mkdir(parents=True, exist_ok=True) + (MARIA_DOCS_LOCAL_DIR / name).write_text(content, encoding="utf-8") def maria_delete_document(name: str) -> bool: + """Sterge documentul din oricare din cele doua directoare. + + Cel din Drive revine la urmatoarea sincronizare — sterge-l din Drive daca vrei + sa dispara definitiv. + """ name = _maria_validate_name(name) - path = MARIA_DOCS_DIR / name - if not path.exists(): - return False - path.unlink() - return True + for d in (MARIA_DOCS_DIR, MARIA_DOCS_LOCAL_DIR): + path = d / name + if path.exists(): + path.unlink() + return True + return False def _maria_run(script: str, timeout: float = 600.0, extra_args: list[str] | None = None) -> dict: diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html index 16f8904..870d6df 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/index.html @@ -466,10 +466,11 @@ async function mRefresh() { document.getElementById('mDocCount').textContent = docs.length + ' în depozit' + (indexate !== docs.length ? ' · ' + indexate + ' indexate' : ''); document.getElementById('mDocs').innerHTML = docs.length ? docs.map(function (d) { + var local = d.local ? ' · local' : ''; var umbrit = d.shadowed_by ? ' · umbrit de ' + esc(d.shadowed_by) + '' : ''; - return '' + esc(d.name) + umbrit + '' + + return '' + esc(d.name) + local + umbrit + '' + '' + d.size + ' B' + ''; }).join('') : 'niciun document'; diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md index 6a8944b..3f0df5a 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md @@ -197,10 +197,21 @@ puntii Discord — vezi `../discord-bridge/README.md` pentru URL si autentificar ## Depozitul de documente -Fisiere `.txt`/`.md`/`.xml` in `~/.maria-bridge/documents/` (lista exacta: -`store.DOC_EXTENSIONS`). Se pot administra: -1. **manual din dashboard-ul comun** (adaugare/stergere text, reindexare automata la salvare); -2. **prin sincronizare Google Drive** — vezi mai jos. +Fisiere `.txt`/`.md`/`.xml` (lista exacta: `store.DOC_EXTENSIONS`), in **doua +directoare care formeaza un singur spatiu de nume**: + +| Director | Ce e acolo | +|---|---| +| `~/.maria-bridge/documents/` | **oglinda Google Drive.** `rclone sync` sterge de aici tot ce nu exista in Drive — nu pune nimic de mana. | +| `~/.maria-bridge/documents-local/` | documente care **nu** vin din Drive: dictionare proprii, ce se adauga din dashboard. Sincronizarea nu se atinge de ele. | + +Cand acelasi nume apare in ambele, castiga cel din Drive (e sursa comuna a echipei; +copia locala ar putea fi o versiune veche uitata acolo). In dashboard, documentele +locale sunt marcate „local". + +Directorul local exista fiindca altfel **tot ce se adauga din dashboard traia pana +la urmatorul tur de sincronizare** — maximum 10 minute. Acum adaugarea din dashboard +scrie direct acolo. ## Sincronizare cu Google Drive @@ -245,9 +256,9 @@ esuata — reia pasii 1-2. Restul din dosar — chatflow-uri Flowise `.json`, scripturi `.ps1`, `.docx` — sunt ignorate deliberat: nu sunt cunostinte de suport. -**`sync` sterge local ce nu mai exista in Drive**, deci depozitul e o oglinda a -dosarului din Drive, nu o colectie care creste. Documentele adaugate manual din -dashboard dispar la prima sincronizare daca nu exista si in Drive. +**`sync` sterge local ce nu mai exista in Drive**, deci `documents/` e o oglinda a +dosarului din Drive, nu o colectie care creste. De aceea exista `documents-local/`: +ce e adaugat de mana sau din dashboard sta acolo si nu e atins de sincronizare. ### Dictionarul de erori Oracle @@ -260,10 +271,13 @@ din captura. Doua lucruri de stiut: -- **E samanta, nu sursa.** Fisierul din git nu ajunge singur in index. Depozitul e - o oglinda a Drive-ului, deci copia pusa direct in `~/.maria-bridge/documents/` - **dispare la prima sincronizare**. Ca sa ramana, pune-l in `document_store` din - Drive (pe Windows: `D:\GoogleDrive\romfast\document_store`). +- **Fisierul din git nu ajunge singur in index.** Copia care ruleaza acum sta in + `~/.maria-bridge/documents-local/`, in afara oglinzii Drive. Asa supravietuieste + sincronizarilor, dar **exista doar pe acest container**. Locul lui pe termen lung + e `document_store` din Drive (pe Windows: `D:\GoogleDrive\romfast\document_store`), + unde il vede si restul echipei si de unde se poate edita fara acces la server. + Cand ajunge in Drive, sterge copia locala — altfel raman doua versiuni si castiga + tacut cea din Drive. - **E scris pentru clienti, nu pentru administratori.** Nicio rezolvare nu contine nume de servere, IP-uri, porturi sau pasi de administrare — la erorile care chiar se rezolva doar tehnic (ORA-01652 spatiu plin, ORA-28000 cont blocat) textul spune diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py index 48273a3..1acd0e3 100755 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/ops/calibrate-rank.py @@ -44,6 +44,11 @@ CAZURI: list[tuple[str, bool]] = [ ("cum inregistrez o factura de achizitie in ROA", True), ("bilant contabil", True), ("import de date", True), + # dupa adaugarea dictionarului de erori Oracle (oracle-erori-uzuale.xml) + ("ORA-01722 invalid number la salvarea facturii", True), + ("imi da ORA-12154 cand deschid programul", True), + ("ORA-00001 unique constraint violated", True), + ("eroare ORA-28000 the account is locked", True), ("care e capitala Frantei", False), ("cat costa un bilet de avion la Paris", False), ("imi da eroare la imprimanta HP LaserJet, nu trage hartia", False), diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py index 4a6cdae..ee1ee24 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/config.py @@ -17,6 +17,10 @@ _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" @@ -87,11 +91,12 @@ def parse_env(text: str) -> dict[str, str]: 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 + global STATE_DIR, DOCS_DIR, DOCS_LOCAL_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" + 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" diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py index 5cfb2cb..bab27f0 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/store.py @@ -4,6 +4,14 @@ Fisiere text (.txt/.md/.xml) sub STATE_DIR/documents/, un singur nivel (fara subdirectoare), ca numele afisat in dashboard sa fie neambiguu si sa poata servi direct ca parametru de request fara riscuri de traversare de cale. +DOUA DIRECTOARE, un singur spatiu de nume: + documents/ oglinda Google Drive. `rclone sync` STERGE de aici tot ce nu + exista in Drive — nu pune nimic de mana. + documents-local/ documente care nu vin din Drive (dictionare proprii, ce se + adauga din dashboard). Sincronizarea nu se atinge de ele. +Cand acelasi nume apare in ambele, castiga cel din Drive: e sursa comuna a +echipei, iar copia locala ar putea fi o versiune veche uitata acolo. + Cand acelasi document exista in mai multe formate (`d406_saft_knowledge.xml` si `d406_saft_knowledge.md`), la indexare intra UNUL SINGUR, cel cu formatul cel mai bogat: .xml > .md > .txt. Sursele .xml sunt structurate pe probleme @@ -38,13 +46,32 @@ def validate_name(name: str) -> str: return name +def _all_files() -> list[pathlib.Path]: + """Fisierele din ambele directoare. La acelasi nume, cel din Drive castiga.""" + vazute: dict[str, pathlib.Path] = {} + for d in (config.DOCS_LOCAL_DIR, config.DOCS_DIR): # Drive ultimul = suprascrie + if not d.is_dir(): + continue + for f in sorted(d.glob("*")): + if f.is_file() and f.suffix in DOC_EXTENSIONS: + vazute[f.name] = f + return [vazute[n] for n in sorted(vazute)] + + +def path_for(name: str) -> pathlib.Path | None: + """Unde e documentul, in oricare din cele doua directoare.""" + name = validate_name(name) + for d in (config.DOCS_DIR, config.DOCS_LOCAL_DIR): + p = d / name + if p.is_file(): + return p + return None + + def list_documents() -> list[dict]: """Tot ce e in depozit. `shadowed_by` spune de ce un fisier nu ajunge in index.""" config.DOCS_DIR.mkdir(parents=True, exist_ok=True) - files = [ - f for f in sorted(config.DOCS_DIR.glob("*")) - if f.is_file() and f.suffix in DOC_EXTENSIONS - ] + files = _all_files() # castigatorul per nume de baza: formatul cel mai bogat winners: dict[str, str] = {} for f in files: @@ -60,6 +87,7 @@ def list_documents() -> list[dict]: "name": f.name, "size": st.st_size, "mtime": st.st_mtime, + "local": f.parent == config.DOCS_LOCAL_DIR, "shadowed_by": None if winner == f.name else winner, }) return out @@ -71,20 +99,27 @@ def documents_for_index() -> list[dict]: def read_document(name: str) -> str: - name = validate_name(name) - return (config.DOCS_DIR / name).read_text(encoding="utf-8") + path = path_for(name) + if path is None: + raise FileNotFoundError(name) + return path.read_text(encoding="utf-8") def write_document(name: str, content: str) -> None: + """Scrie in documents-local/, NU in oglinda Drive. + + Un document scris in `documents/` dispare la urmatorul `rclone sync` — asta + se intampla pana acum cu tot ce se adauga din dashboard. + """ name = validate_name(name) - config.DOCS_DIR.mkdir(parents=True, exist_ok=True) - (config.DOCS_DIR / name).write_text(content, encoding="utf-8") + config.DOCS_LOCAL_DIR.mkdir(parents=True, exist_ok=True) + (config.DOCS_LOCAL_DIR / name).write_text(content, encoding="utf-8") def delete_document(name: str) -> bool: - name = validate_name(name) - path = config.DOCS_DIR / name - if not path.exists(): + """Sterge documentul de unde se afla. Cel din Drive revine la sincronizare.""" + path = path_for(name) + if path is None: return False path.unlink() return True diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py index 3fd57d6..300d037 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag/sync.py @@ -32,10 +32,11 @@ def _fingerprint() -> str: 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()) + # amandoua directoarele: si un document local adaugat din dashboard trebuie sa + # declanseze reindexarea, nu doar ce vine din Drive + for f in store._all_files(): + st = f.stat() + h.update(f"{f.name}:{st.st_mtime_ns}:{st.st_size}\n".encode()) return h.hexdigest() diff --git a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py index d463659..ec17477 100644 --- a/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py +++ b/proxmox/lxc171-claude-agent/maria-whatsapp-bridge/tests/test_store_si_chunking.py @@ -237,3 +237,57 @@ def test_textul_modificat_se_reembedeaza(monkeypatch, write): assert calculate == ["varianta modificata"] assert rezultat["embeddings_refolosite"] == 0 + + +# --- documente locale, in afara oglinzii Drive ------------------------------- + +def _scrie_local(name: str, text: str): + import config + config.DOCS_LOCAL_DIR.mkdir(parents=True, exist_ok=True) + p = config.DOCS_LOCAL_DIR / name + p.write_text(text, encoding="utf-8") + return p + + +def test_documentele_locale_intra_in_index(write): + import store + write("din-drive.md", "vine din Drive") + _scrie_local("al-meu.md", "adaugat local") + + nume = {d["name"] for d in store.documents_for_index()} + assert nume == {"din-drive.md", "al-meu.md"} + + +def test_se_vede_care_e_local(write): + import store + write("din-drive.md", "x") + _scrie_local("al-meu.md", "y") + dupa_nume = {d["name"]: d for d in store.list_documents()} + assert dupa_nume["al-meu.md"]["local"] is True + assert dupa_nume["din-drive.md"]["local"] is False + + +def test_la_acelasi_nume_castiga_versiunea_din_drive(write): + import store + _scrie_local("acelasi.md", "copie locala veche") + write("acelasi.md", "versiunea din Drive") + assert store.read_document("acelasi.md") == "versiunea din Drive" + assert len(store.list_documents()) == 1 + + +def test_scrierea_din_dashboard_merge_in_directorul_local(): + # Pana acum ajungea in oglinda Drive si disparea la urmatorul rclone sync. + import config + import store + store.write_document("nota.md", "ceva de tinut minte") + assert (config.DOCS_LOCAL_DIR / "nota.md").exists() + assert not (config.DOCS_DIR / "nota.md").exists() + + +def test_stergerea_gaseste_documentul_in_ambele_directoare(write): + import store + _scrie_local("local.md", "x") + write("drive.md", "y") + assert store.delete_document("local.md") is True + assert store.delete_document("drive.md") is True + assert store.delete_document("inexistent.md") is False