feat(maria): depozit local, in afara oglinzii Drive

Dictionarul de erori Oracle pus in `documents/` ar fi disparut in 10 minute:
`rclone sync` sterge de acolo tot ce nu exista in Drive. Aceeasi problema o avea
si butonul de adaugare document din dashboard — documentul traia pana la
urmatorul tur de sincronizare, ceea ce README-ul mentiona ca pe o ciudatenie, nu
ca pe un bug.

Acum sunt doua directoare cu un singur spatiu de nume: `documents/` (oglinda
Drive) si `documents-local/` (ce nu vine din Drive). La acelasi nume castiga
Drive-ul — e sursa comuna a echipei, iar copia locala poate fi o versiune veche
uitata acolo. Dashboard-ul scrie in cel local si marcheaza documentele „local".

Recalibrat cu dictionarul indexat (169 chunk-uri): 19/19, cele patru intrebari
Oracle noi ies la 0,73-0,75. Reindexarea a durat cat cele 29 de chunk-uri noi,
nu cat toate 169 — refolosirea vectorilor isi face treaba (log: "29 embeddings
noi, 140 refolosite").

Verificat pe canalul viu: o escaladare cu captura chiar pleaca pe WhatsApp,
imagine + rezumat + referinta, si apare in dashboard.

73 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
Claude Agent
2026-08-31 22:13:09 +00:00
parent ca6b53e0ca
commit c514818ebe
8 changed files with 173 additions and 39 deletions

View File

@@ -61,6 +61,9 @@ MARIA_DOC_EXTENSIONS = (".txt", ".md", ".xml")
_MARIA_FORMAT_RANK = {".xml": 0, ".md": 1, ".txt": 2} _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_STATE_DIR = Path(os.environ.get("MARIA_BRIDGE_DIR") or (Path.home() / ".maria-bridge"))
MARIA_DOCS_DIR = MARIA_STATE_DIR / "documents" 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_INDEX_FILE = MARIA_STATE_DIR / "rag_index.json"
MARIA_ENV_FILE = MARIA_STATE_DIR / "env" MARIA_ENV_FILE = MARIA_STATE_DIR / "env"
MARIA_LOG_DIR = MARIA_STATE_DIR / "logs" 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`. Nu importam modulul acela: si el, si `discord-bridge/config.py` s-ar numi `config`.
""" """
MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True) MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True)
files = [ vazute: dict[str, Path] = {}
f for f in sorted(MARIA_DOCS_DIR.glob("*")) for d in (MARIA_DOCS_LOCAL_DIR, MARIA_DOCS_DIR): # Drive ultimul = castiga la acelasi nume
if f.is_file() and f.suffix in MARIA_DOC_EXTENSIONS 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] = {} winners: dict[str, str] = {}
for f in files: for f in files:
best = winners.get(f.stem) best = winners.get(f.stem)
@@ -162,6 +169,7 @@ def maria_documents() -> list[dict]:
"name": f.name, "name": f.name,
"size": st.st_size, "size": st.st_size,
"mtime": st.st_mtime, "mtime": st.st_mtime,
"local": f.parent == MARIA_DOCS_LOCAL_DIR,
"shadowed_by": None if winner == f.name else winner, "shadowed_by": None if winner == f.name else winner,
}) })
return out return out
@@ -206,18 +214,29 @@ def _maria_validate_name(name: str) -> str:
def maria_write_document(name: str, content: str) -> None: 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) name = _maria_validate_name(name)
MARIA_DOCS_DIR.mkdir(parents=True, exist_ok=True) MARIA_DOCS_LOCAL_DIR.mkdir(parents=True, exist_ok=True)
(MARIA_DOCS_DIR / name).write_text(content, encoding="utf-8") (MARIA_DOCS_LOCAL_DIR / name).write_text(content, encoding="utf-8")
def maria_delete_document(name: str) -> bool: 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) name = _maria_validate_name(name)
path = MARIA_DOCS_DIR / name for d in (MARIA_DOCS_DIR, MARIA_DOCS_LOCAL_DIR):
if not path.exists(): path = d / name
return False if path.exists():
path.unlink() path.unlink()
return True return True
return False
def _maria_run(script: str, timeout: float = 600.0, extra_args: list[str] | None = None) -> dict: def _maria_run(script: str, timeout: float = 600.0, extra_args: list[str] | None = None) -> dict:

View File

@@ -466,10 +466,11 @@ async function mRefresh() {
document.getElementById('mDocCount').textContent = document.getElementById('mDocCount').textContent =
docs.length + ' în depozit' + (indexate !== docs.length ? ' · ' + indexate + ' indexate' : ''); docs.length + ' în depozit' + (indexate !== docs.length ? ' · ' + indexate + ' indexate' : '');
document.getElementById('mDocs').innerHTML = docs.length ? docs.map(function (d) { document.getElementById('mDocs').innerHTML = docs.length ? docs.map(function (d) {
var local = d.local ? ' <span class="muted" title="nu vine din Drive; sincronizarea nu il sterge">· local</span>' : '';
var umbrit = d.shadowed_by var umbrit = d.shadowed_by
? ' <span class="muted" title="acelaşi document exista intr-un format mai bogat">· umbrit de ' + esc(d.shadowed_by) + '</span>' ? ' <span class="muted" title="acelaşi document exista intr-un format mai bogat">· umbrit de ' + esc(d.shadowed_by) + '</span>'
: ''; : '';
return '<tr><td data-label="Nume" class="mono">' + esc(d.name) + umbrit + '</td>' + return '<tr><td data-label="Nume" class="mono">' + esc(d.name) + local + umbrit + '</td>' +
'<td data-label="Mărime" class="num">' + d.size + ' B</td>' + '<td data-label="Mărime" class="num">' + d.size + ' B</td>' +
'<td><button class="small danger" onclick="mDelDoc(\'' + esc(d.name) + '\')">Șterge</button></td></tr>'; '<td><button class="small danger" onclick="mDelDoc(\'' + esc(d.name) + '\')">Șterge</button></td></tr>';
}).join('') : '<tr><td colspan="3" class="empty" data-label="">niciun document</td></tr>'; }).join('') : '<tr><td colspan="3" class="empty" data-label="">niciun document</td></tr>';

View File

@@ -197,10 +197,21 @@ puntii Discord — vezi `../discord-bridge/README.md` pentru URL si autentificar
## Depozitul de documente ## Depozitul de documente
Fisiere `.txt`/`.md`/`.xml` in `~/.maria-bridge/documents/` (lista exacta: Fisiere `.txt`/`.md`/`.xml` (lista exacta: `store.DOC_EXTENSIONS`), in **doua
`store.DOC_EXTENSIONS`). Se pot administra: directoare care formeaza un singur spatiu de nume**:
1. **manual din dashboard-ul comun** (adaugare/stergere text, reindexare automata la salvare);
2. **prin sincronizare Google Drive** — vezi mai jos. | 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 ## Sincronizare cu Google Drive
@@ -245,9 +256,9 @@ esuata — reia pasii 1-2.
Restul din dosar — chatflow-uri Flowise `.json`, scripturi `.ps1`, `.docx` — sunt Restul din dosar — chatflow-uri Flowise `.json`, scripturi `.ps1`, `.docx` — sunt
ignorate deliberat: nu sunt cunostinte de suport. ignorate deliberat: nu sunt cunostinte de suport.
**`sync` sterge local ce nu mai exista in Drive**, deci depozitul e o oglinda a **`sync` sterge local ce nu mai exista in Drive**, deci `documents/` e o oglinda a
dosarului din Drive, nu o colectie care creste. Documentele adaugate manual din dosarului din Drive, nu o colectie care creste. De aceea exista `documents-local/`:
dashboard dispar la prima sincronizare daca nu exista si in Drive. ce e adaugat de mana sau din dashboard sta acolo si nu e atins de sincronizare.
### Dictionarul de erori Oracle ### Dictionarul de erori Oracle
@@ -260,10 +271,13 @@ din captura.
Doua lucruri de stiut: Doua lucruri de stiut:
- **E samanta, nu sursa.** Fisierul din git nu ajunge singur in index. Depozitul e - **Fisierul din git nu ajunge singur in index.** Copia care ruleaza acum sta in
o oglinda a Drive-ului, deci copia pusa direct in `~/.maria-bridge/documents/` `~/.maria-bridge/documents-local/`, in afara oglinzii Drive. Asa supravietuieste
**dispare la prima sincronizare**. Ca sa ramana, pune-l in `document_store` din sincronizarilor, dar **exista doar pe acest container**. Locul lui pe termen lung
Drive (pe Windows: `D:\GoogleDrive\romfast\document_store`). 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 - **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 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 se rezolva doar tehnic (ORA-01652 spatiu plin, ORA-28000 cont blocat) textul spune

View File

@@ -44,6 +44,11 @@ CAZURI: list[tuple[str, bool]] = [
("cum inregistrez o factura de achizitie in ROA", True), ("cum inregistrez o factura de achizitie in ROA", True),
("bilant contabil", True), ("bilant contabil", True),
("import de date", 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), ("care e capitala Frantei", False),
("cat costa un bilet de avion la Paris", False), ("cat costa un bilet de avion la Paris", False),
("imi da eroare la imprimanta HP LaserJet, nu trage hartia", False), ("imi da eroare la imprimanta HP LaserJet, nu trage hartia", False),

View File

@@ -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) STATE_DIR: pathlib.Path = pathlib.Path(os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR)
DOCS_DIR: pathlib.Path = STATE_DIR / "documents" 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" INDEX_FILE: pathlib.Path = STATE_DIR / "rag_index.json"
LOCK_FILE: pathlib.Path = STATE_DIR / ".rag.lock" LOCK_FILE: pathlib.Path = STATE_DIR / ".rag.lock"
LOG_DIR: pathlib.Path = STATE_DIR / "logs" 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]: def reload(base_dir: str | os.PathLike | None = None) -> dict[str, str]:
"""Recalculeaza caile si reciteste env-ul. Returneaza dictionarul incarcat.""" """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: if base_dir is None:
base_dir = os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR base_dir = os.environ.get("MARIA_BRIDGE_DIR") or _DEFAULT_DIR
STATE_DIR = pathlib.Path(base_dir) STATE_DIR = pathlib.Path(base_dir)
DOCS_DIR = STATE_DIR / "documents" DOCS_DIR = STATE_DIR / "documents"
DOCS_LOCAL_DIR = STATE_DIR / "documents-local"
INDEX_FILE = STATE_DIR / "rag_index.json" INDEX_FILE = STATE_DIR / "rag_index.json"
LOCK_FILE = STATE_DIR / ".rag.lock" LOCK_FILE = STATE_DIR / ".rag.lock"
LOG_DIR = STATE_DIR / "logs" LOG_DIR = STATE_DIR / "logs"

View File

@@ -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 subdirectoare), ca numele afisat in dashboard sa fie neambiguu si sa poata servi
direct ca parametru de request fara riscuri de traversare de cale. 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 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 `d406_saft_knowledge.md`), la indexare intra UNUL SINGUR, cel cu formatul cel mai
bogat: .xml > .md > .txt. Sursele .xml sunt structurate pe probleme bogat: .xml > .md > .txt. Sursele .xml sunt structurate pe probleme
@@ -38,13 +46,32 @@ def validate_name(name: str) -> str:
return name 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]: def list_documents() -> list[dict]:
"""Tot ce e in depozit. `shadowed_by` spune de ce un fisier nu ajunge in index.""" """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) config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
files = [ files = _all_files()
f for f in sorted(config.DOCS_DIR.glob("*"))
if f.is_file() and f.suffix in DOC_EXTENSIONS
]
# castigatorul per nume de baza: formatul cel mai bogat # castigatorul per nume de baza: formatul cel mai bogat
winners: dict[str, str] = {} winners: dict[str, str] = {}
for f in files: for f in files:
@@ -60,6 +87,7 @@ def list_documents() -> list[dict]:
"name": f.name, "name": f.name,
"size": st.st_size, "size": st.st_size,
"mtime": st.st_mtime, "mtime": st.st_mtime,
"local": f.parent == config.DOCS_LOCAL_DIR,
"shadowed_by": None if winner == f.name else winner, "shadowed_by": None if winner == f.name else winner,
}) })
return out return out
@@ -71,20 +99,27 @@ def documents_for_index() -> list[dict]:
def read_document(name: str) -> str: def read_document(name: str) -> str:
name = validate_name(name) path = path_for(name)
return (config.DOCS_DIR / name).read_text(encoding="utf-8") if path is None:
raise FileNotFoundError(name)
return path.read_text(encoding="utf-8")
def write_document(name: str, content: str) -> None: 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) name = validate_name(name)
config.DOCS_DIR.mkdir(parents=True, exist_ok=True) config.DOCS_LOCAL_DIR.mkdir(parents=True, exist_ok=True)
(config.DOCS_DIR / name).write_text(content, encoding="utf-8") (config.DOCS_LOCAL_DIR / name).write_text(content, encoding="utf-8")
def delete_document(name: str) -> bool: def delete_document(name: str) -> bool:
name = validate_name(name) """Sterge documentul de unde se afla. Cel din Drive revine la sincronizare."""
path = config.DOCS_DIR / name path = path_for(name)
if not path.exists(): if path is None:
return False return False
path.unlink() path.unlink()
return True return True

View File

@@ -32,8 +32,9 @@ def _fingerprint() -> str:
doar cand s-a schimbat efectiv ceva, nu la fiecare tur de sincronizare.""" doar cand s-a schimbat efectiv ceva, nu la fiecare tur de sincronizare."""
config.DOCS_DIR.mkdir(parents=True, exist_ok=True) config.DOCS_DIR.mkdir(parents=True, exist_ok=True)
h = hashlib.sha256() h = hashlib.sha256()
for f in sorted(config.DOCS_DIR.glob("*")): # amandoua directoarele: si un document local adaugat din dashboard trebuie sa
if f.is_file() and f.suffix in store.DOC_EXTENSIONS: # declanseze reindexarea, nu doar ce vine din Drive
for f in store._all_files():
st = f.stat() st = f.stat()
h.update(f"{f.name}:{st.st_mtime_ns}:{st.st_size}\n".encode()) h.update(f"{f.name}:{st.st_mtime_ns}:{st.st_size}\n".encode())
return h.hexdigest() return h.hexdigest()

View File

@@ -237,3 +237,57 @@ def test_textul_modificat_se_reembedeaza(monkeypatch, write):
assert calculate == ["varianta modificata"] assert calculate == ["varianta modificata"]
assert rezultat["embeddings_refolosite"] == 0 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