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:
@@ -1,38 +1,75 @@
|
||||
"""Depozitul de documente pentru indexarea RAG a lui Maria.
|
||||
|
||||
Fisiere text (.txt/.md) sub STATE_DIR/documents/, un singur nivel (fara
|
||||
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.
|
||||
|
||||
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
|
||||
(`<mesaj_eroare>`, `<rezolvare>`) si de regula mai noi decat exporturile .md.
|
||||
Celalalt ramane pe disc si se vede in dashboard, marcat ca umbrit — altfel acelasi
|
||||
raspuns ar aparea de doua ori in rezultatele RAG.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
import config
|
||||
|
||||
_SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,200}$")
|
||||
|
||||
# Extensiile acceptate in depozit. Sursa unica de adevar pentru sync.py si indexer.py;
|
||||
# dashboard-ul (discord-bridge/dashboard/api.py) tine o copie, fiindca nu importa
|
||||
# module din acest proiect (coliziune de nume pe `config`) — tine-le sincronizate.
|
||||
DOC_EXTENSIONS = (".txt", ".md", ".xml")
|
||||
|
||||
# Mai mic = preferat cand acelasi nume de baza exista in mai multe formate.
|
||||
_FORMAT_RANK = {".xml": 0, ".md": 1, ".txt": 2}
|
||||
|
||||
|
||||
def validate_name(name: str) -> str:
|
||||
if not name or not _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(DOC_EXTENSIONS):
|
||||
raise ValueError("doar fisiere " + ", ".join(DOC_EXTENSIONS))
|
||||
return name
|
||||
|
||||
|
||||
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
|
||||
]
|
||||
# castigatorul per nume de baza: formatul cel mai bogat
|
||||
winners: dict[str, str] = {}
|
||||
for f in files:
|
||||
best = winners.get(f.stem)
|
||||
if best is None or _FORMAT_RANK[f.suffix] < _FORMAT_RANK[pathlib.Path(best).suffix]:
|
||||
winners[f.stem] = f.name
|
||||
|
||||
out = []
|
||||
for f in sorted(config.DOCS_DIR.glob("*")):
|
||||
if not f.is_file() or f.suffix not in (".txt", ".md"):
|
||||
continue
|
||||
for f in files:
|
||||
st = f.stat()
|
||||
out.append({"name": f.name, "size": st.st_size, "mtime": st.st_mtime})
|
||||
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 documents_for_index() -> list[dict]:
|
||||
"""Doar documentele care ajung efectiv in index (fara cele umbrite)."""
|
||||
return [d for d in list_documents() if not d["shadowed_by"]]
|
||||
|
||||
|
||||
def read_document(name: str) -> str:
|
||||
name = validate_name(name)
|
||||
return (config.DOCS_DIR / name).read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user