PRZEWODNIK · PIPELINE

Parsuj dokumenty na hoście, szukaj ich jak pamięci.

Document Processor zamienia PDF / DOCX / TXT w strukturalny markdown + JSON plus obrazy z kontekstem. Consciousness Server trzyma chunki jako rekordy treningowe. semantic-search odczytuje je po znaczeniu. Nic nie wychodzi z maszyny. Ten przewodnik spina trzy klocki end-to-end.

Wymagania

  • Działający ekosystem z Szybkiego startu (CS na :3032, semantic-search na :3037).
  • Zainstalowany Document Processor (aplikacja desktopowa Tauri; instalatory dla Linuksa — .deb / AppImage — na innych platformach build ze źródeł).
  • Sparsowany jeden dokument. Przeciągnij PDF na okno Document Processora; poczekaj na zielony check.

Co wytwarza Document Processor

Każdy sparsowany dokument ląduje w katalogu pod katalogiem danych aplikacji, wyznaczanym przez resolver ścieżek Tauri — na Linuksie ~/.local/share/com.buildonai.document-processor/przetworzone/<document-id>/, na innych platformach w ich odpowiedniku. Kształt:

filesystem
# Document Processor zapisuje jeden katalog per sparsowany dokument,
# pod katalogiem danych aplikacji (pokazany Linux):
~/.local/share/com.buildonai.document-processor/przetworzone/
└── <document-id>/
    ├── document.md          # markdown czytelny dla człowieka
    ├── document.json        # metadane + full_text + images[] z kontekstem
    ├── images/
    │   ├── img_001.png      # wyekstrahowane obrazy
    │   └── thumb_img_001.png
    └── original.pdf         # kopia pliku wejściowego

Przekazujesz katalog dalej. Plik document.json to koń pociągowy — metadane dokumentu, pełny wyekstrahowany tekst i rekordy kontekstu per obraz. Gotowego pliku z chunkami nie ma; podzielenie full_text na jednostki wyszukiwalne to zadanie skryptu ingestującego.

Wgranie do Consciousness Server

Każdy chunk staje się rekordem treningowym. Pole type jest wymagane — dla prozy użyj explanation, dla treści typu klauzula / sekcja strukturalna użyj architecture. Otaguj każdy rekord identyfikatorem dokumentu, żeby później móc filtrować search.

ingest.py
import json, sys, requests
from pathlib import Path

CS = "http://127.0.0.1:3032"
DATA = Path.home() / ".local/share/com.buildonai.document-processor/przetworzone"
# Katalog z argv albo wpisz id ręcznie: zajrzyj ls-em do przetworzone/
DOC = Path(sys.argv[1]) if len(sys.argv) > 1 else DATA / "<document-id>"

doc = json.loads((DOC / "document.json").read_text())

# document.json zawiera full_text, nie gotowe chunki — dzielenie na
# kawałki to zadanie skryptu. Podział po akapitach to dobry punkt wyjścia.
chunks = [c.strip() for c in (doc.get("full_text") or "").split("\n\n") if c.strip()]

# Każdy chunk staje się rekordem treningowym. Pole "type" jest WYMAGANE.
# Dla treści dokumentowych użyj "explanation" (samowystarczalny chunk
# znaczeniowy) lub "architecture" (sekcja strukturalna, np. klauzula umowy).
for chunk in chunks:
    requests.post(f"{CS}/api/memory/training", json={
        "agent": "doc-pipeline",
        "type": "explanation",
        "goal": f"ingest:{doc['filename']}",
        "instruction": "search-retrievable chunk",
        "input": doc.get("doc_type") or "",
        "output": chunk,
        "tags": [doc.get("doc_type") or "document", "doc:" + doc["id"]],
    }).raise_for_status()

print(f"Ingested {doc['filename']} — {len(chunks)} chunks indexed.")

CS embeduje każdy rekord do ChromaDB przez Ollamę na hoście. Indeks rośnie liniowo z liczbą chunków; embeddingi to ~1.5 KB każdy, więc korpus 10 000 chunków to ~15 MB plus narzut ChromaDB. Wszystko na lokalnym dysku.

Wyszukiwanie po sensie, nie po nazwie pliku

Po wgraniu agent znajduje właściwą klauzulę bez wiedzy, w jakim pliku ona była:

search.py
# Teraz agent znajdzie tę umowę po sensie, nie po nazwie pliku.
hits = requests.post("http://127.0.0.1:3037/api/search", json={
    "query": "what penalty applies if delivery slips by 30 days",
    "limit": 5,
    "filters": {"tags": ["doc:" + doc["id"]]},
}).json()

for h in hits["results"]:
    print(f"score={h['score']:.2f}  {h['snippet'][:120]}")

filters.tags zawęża search do jednego dokumentu; usuń filter, żeby zapytać po całym korpusie. score to cosine similarity (0..1).

Zautomatyzuj połowę ingestową

Jedna rzecz wymaga jasności: Document Processor nie obserwuje folderów w tle. Jego „watch folder" to tylko zapamiętany katalog do ręcznego ponownego skanu — parsowanie rusza, gdy upuścisz plik albo klikniesz „Skanuj ponownie". Zautomatyzować da się natomiast przekazanie dalej: każde parsowanie tworzy nowy katalog w przetworzone/, więc watcher po stronie wyjścia zamienia „sparsowałem plik" w „chunki są wyszukiwalne" bez dodatkowej pracy w UI:

watch.sh
# Document Processor sam NIE obserwuje folderów — parsowanie rusza,
# gdy upuścisz plik na okno albo klikniesz "Skanuj ponownie". Ale każde
# parsowanie tworzy nowy katalog w przetworzone/, więc można obserwować
# stronę WYJŚCIOWĄ i automatycznie ingestować wszystko, co sparsujesz:
DATA=~/.local/share/com.buildonai.document-processor/przetworzone
inotifywait -m -e create "$DATA" | while read dir _ name; do
    # document.json powstaje pod koniec parsowania — poczekaj na niego
    for i in $(seq 1 30); do
        if [ -f "$dir$name/document.json" ]; then
            python ingest.py "$dir$name"; break
        fi
        sleep 1
    done
done

Owiń to w jednostkę systemd usera (~/.config/systemd/user/doc-ingest.service), żeby przeżyła reboot. Z punktu widzenia operatora: przeciągnięcie PDF na Document Processor wrzuca go też do korpusu — samo parsowanie pozostaje czynnością ręczną.

Dalsze kroki