feat(maria): accepta .xml in depozit, preferat peste .md, cu chunking pe probleme

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
This commit is contained in:
Claude Agent
2026-08-31 18:15:03 +00:00
parent c42b95b7db
commit 1c7899753a
11 changed files with 400 additions and 61 deletions

View File

@@ -56,6 +56,9 @@ SELF_SERVICE = "claude-discord-dashboard.service"
# pe numele de modul "config" cu discord-bridge/config.py, deja importat mai
# sus) — citim direct caile si folosim subprocess/HTTP, exact ca la SERVICE.
MARIA_UNITS = {"bridge": "maria-whatsapp.service", "rag": "maria-rag.service"}
# Oglinda lui rag/store.py: DOC_EXTENSIONS si ordinea de preferinta (.xml cel mai bogat).
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"
MARIA_INDEX_FILE = MARIA_STATE_DIR / "rag_index.json"
@@ -113,20 +116,39 @@ def maria_sync_state() -> dict:
def maria_documents() -> list[dict]:
"""Oglinda lui `maria-whatsapp-bridge/rag/store.py:list_documents` — tine-le la fel.
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
]
winners: dict[str, str] = {}
for f in files:
best = winners.get(f.stem)
if best is None or _MARIA_FORMAT_RANK[f.suffix] < _MARIA_FORMAT_RANK[Path(best).suffix]:
winners[f.stem] = f.name
out = []
for f in sorted(MARIA_DOCS_DIR.glob("*")):
if f.is_file() and f.suffix in (".txt", ".md"):
st = f.stat()
out.append({"name": f.name, "size": st.st_size, "mtime": st.st_mtime})
for f in files:
st = f.stat()
winner = winners[f.stem]
out.append({
"name": f.name,
"size": st.st_size,
"mtime": st.st_mtime,
"shadowed_by": None if winner == f.name else winner,
})
return out
def _maria_validate_name(name: str) -> str:
if not name or not _MARIA_SAFE_NAME.match(name) or ".." in name or "/" in name:
raise ValueError(f"nume de document invalid: {name!r}")
if not name.endswith((".txt", ".md")):
raise ValueError("doar fisiere .txt sau .md")
if not name.endswith(MARIA_DOC_EXTENSIONS):
raise ValueError("doar fisiere " + ", ".join(MARIA_DOC_EXTENSIONS))
return name

View File

@@ -378,7 +378,6 @@ async function mRefresh() {
document.getElementById('mIndexDetail').textContent =
s.index.chunks + ' chunk-uri din ' + s.index.documents_indexed + ' documente indexate' +
(s.index.mtime ? ' · indexat ' + new Date(s.index.mtime * 1000).toLocaleString('ro-RO') : '');
document.getElementById('mDocCount').textContent = s.documents + ' în depozit';
var sync = s.sync || {};
document.getElementById('mSyncDetail').textContent = sync.drive_remote
@@ -387,8 +386,14 @@ async function mRefresh() {
var docsRes = await api('api/maria/documents');
var docs = docsRes.ok ? (docsRes.data.documents || []) : [];
var indexate = docs.filter(function (d) { return !d.shadowed_by; }).length;
document.getElementById('mDocCount').textContent =
docs.length + ' în depozit' + (indexate !== docs.length ? ' · ' + indexate + ' indexate' : '');
document.getElementById('mDocs').innerHTML = docs.length ? docs.map(function (d) {
return '<tr><td data-label="Nume" class="mono">' + esc(d.name) + '</td>' +
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>'
: '';
return '<tr><td data-label="Nume" class="mono">' + esc(d.name) + umbrit + '</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>';
}).join('') : '<tr><td colspan="3" class="empty" data-label="">niciun document</td></tr>';