Prerequisites
- A working ecosystem from the Quickstart (CS on
:3032, semantic-search on:3037). - Document Processor installed (Tauri desktop app; installers for Linux —
.deb/ AppImage — other platforms build from source). - One document parsed. Drag a PDF onto the Document Processor window; wait for the green check.
What Document Processor produces
Each parsed document lands in a directory under the app data dir,
resolved by Tauri's path resolver — on Linux
~/.local/share/com.buildonai.document-processor/przetworzone/<document-id>/,
the platform equivalent elsewhere. The shape:
# Document Processor writes one directory per parsed document,
# under the app data dir (Linux shown):
~/.local/share/com.buildonai.document-processor/przetworzone/
└── <document-id>/
├── document.md # human-readable markdown
├── document.json # metadata + full_text + images[] with context
├── images/
│ ├── img_001.png # extracted images
│ └── thumb_img_001.png
└── original.pdf # copy of the input file
You hand the directory off to the next stage. The
document.json file is the workhorse — document
metadata, the full extracted text, and per-image context records.
There is no pre-chunked file; splitting full_text
into retrievable units is the ingest script's job.
Ingest into Consciousness Server
Each chunk becomes a training record. The
type field is required — for prose chunks use
explanation, for clause-like or sectional
content use architecture. Tag every record with
the document id so you can scope searches later.
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"
# Directory as argv, or pick an id by hand: ls the przetworzone/ directory
DOC = Path(sys.argv[1]) if len(sys.argv) > 1 else DATA / "<document-id>"
doc = json.loads((DOC / "document.json").read_text())
# document.json ships full_text, not pre-made chunks — chunking is your
# job. A paragraph split is a fine baseline.
chunks = [c.strip() for c in (doc.get("full_text") or "").split("\n\n") if c.strip()]
# Each chunk becomes a training record. The "type" field is REQUIRED.
# For document content, use "explanation" (a self-contained chunk of
# meaning) or "architecture" (a structural section like a contract clause).
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 embeds each record into ChromaDB via Ollama on the host. Index size grows linearly with chunk count; embeddings are ~1.5 KB each, so a 10 000-chunk corpus is roughly 15 MB plus the ChromaDB overhead. All of it on local disk.
Retrieve by meaning, not filename
Once ingested, an agent finds the right clause without knowing what file it lived in:
# Now an agent can find that contract by meaning, not filename.
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 narrows the search to one
document; remove the filter for a corpus-wide query. The
score is cosine similarity (0..1).
Automate the ingest half
One thing to be clear about: Document Processor does not watch
folders in the background. Its "watch folder" merely remembers a
directory for a manual re-scan — parsing happens when you drop a
file or hit "Scan again". What you can automate is the
hand-off: every parse creates a new directory under
przetworzone/, so a watcher on the output side turns
"I parsed a file" into "the chunks are searchable" without any
extra UI work:
# Document Processor does NOT watch folders by itself — parsing runs
# when you drop a file on the window or trigger "Scan again". But every
# parse creates a new directory under przetworzone/, so you can watch
# the OUTPUT side and auto-ingest whatever you parse:
DATA=~/.local/share/com.buildonai.document-processor/przetworzone
inotifywait -m -e create "$DATA" | while read dir _ name; do
# document.json is written near the end of a parse — wait for it
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
Wrap that in a systemd user unit (~/.config/systemd/user/doc-ingest.service)
so it survives reboots. From the operator's point of view,
dropping a PDF on Document Processor now also drops it into
the corpus — the parse itself stays a manual act.
Next steps
- Write a custom agent → that searches the corpus and answers questions over it.
- Switch on signed requests → before exposing the corpus to multiple agents.
- Document Processor product page → for the parser internals (image-with-context extraction, classifier, hybrid search).