End-to-end ingest for AgentContextOS — the CLI demo milestone of
Phase 1. Step 1.10 wires every Phase 1 stage into a single orchestrator
(rag_ingest.IngestPipeline) and exposes it via the FastAPI gateway
(POST /v1/ingest/document) and the ragctl ingest smoke command.
The ingest pipeline runs each document through:
Connector → Parser → Chunker → Enricher → PII processor → Embedder → Index
↑ ↑ ↑ ↑ ↑ ↑
1.3 1.5 1.6 1.7 1.8 1.1 / 2.x
The pipeline returns IngestResult per document and IngestRunSummary
when driven from a connector. Both are Pydantic v2 frozen models in
rag_core.types; both are exported to JSON Schema under dist/schemas/.
from rag_ingest import IngestPipeline
from rag_chunker import HeadingAwareChunker
from rag_enricher import DefaultEnricher
from rag_pii import PiiProcessor, RegexPIIDetector
from rag_embedders import OpenAIEmbedder
from rag_backends import PgVectorStore
from rag_parsers import default_registry
pipeline = IngestPipeline(
chunker=HeadingAwareChunker(),
enricher=DefaultEnricher(),
pii_processor=PiiProcessor(detector=RegexPIIDetector()),
embedder=OpenAIEmbedder(),
index_backend=PgVectorStore(...),
parser_registry=default_registry().select,
policy_engine=None, # NoopPolicyEngine() in dev; production PDP in prod
)
# Single document (e.g., from an HTTP upload)
result = await pipeline.ingest_document(ctx, document, content_bytes)
# IngestResult(document_id=..., status=ingested, chunk_count=12, ...)
# Connector-driven (filesystem crawl, S3 bucket scan, CDC stream, …)
summary = await pipeline.ingest_connector(ctx, connector)
# IngestRunSummary(total=42, ingested=40, blocked=1, failed=1, ...)| Argument | Type | Required | Notes |
|---|---|---|---|
chunker |
Chunker |
✅ | typically HeadingAwareChunker |
enricher |
Enricher |
✅ | typically DefaultEnricher |
embedder |
Embedder |
✅ | OpenAI / Cohere / BGE / E5 / Noop |
index_backend |
VectorIndexBackend |
✅ | PgVectorStore / Qdrant / Noop |
parser_registry |
ParserSelector |
✅ | usually default_registry().select |
pii_processor |
PiiProcessor | None |
❌ | None skips PII filtering entirely |
policy_engine |
PolicyEngine | None |
❌ | None skips the ingest_doc PDP check |
async def ingest_document(
self,
ctx: RequestContext,
document: Document,
content: bytes,
*,
mime_type: str | None = None,
) -> IngestResultMIME-type fallback chain: explicit mime_type= → document.mime_type →
"application/octet-stream". Returns an IngestResult with one of four
statuses:
| Status | When |
|---|---|
ingested |
Pipeline completed; chunks were embedded and indexed |
blocked_by_policy |
PolicyEngine.evaluate(...) returned deny |
skipped_empty |
Parser produced no chunks (empty file, unparseable content) |
failed |
An unhandled error escaped a stage; error carries the message |
Pipeline failures are caught per-document — one bad document never aborts the rest of a connector run.
async def ingest_connector(
self,
ctx: RequestContext,
connector: Connector,
*,
on_result: Callable[[IngestResult], Awaitable[None]] | None = None,
) -> IngestRunSummaryIterates every document the connector yields. The optional on_result
callback fires after each document so callers can stream progress.
Callback errors are logged and swallowed — they never abort the run.
Multipart upload. Form fields + a single file part.
| Field | Type | Required | Notes |
|---|---|---|---|
tenant_id |
str (form) |
✅ | propagated into RequestContext.tenant_id |
corpus_id |
str (form) |
✅ | target corpus / partition |
principal_id |
str (form) |
❌ | defaults to "gateway-anon" |
source_uri |
str (form) |
❌ | defaults to upload:///<filename> |
file |
binary (file) | ✅ | document bytes; Content-Type taken as MIME hint |
Response (200): IngestResult JSON. Status failed is still 200 — the
upload was received successfully; the result is the pipeline's outcome.
Error responses:
400 empty upload— file part was zero bytes.422— missing required form field (handled by FastAPI form validation).
Lightweight liveness + capability advertisement. /v1/info lists the
endpoints the gateway currently exposes (the surface will grow in Phase
3 with query / retrieve / corpora / SSE).
# Crawl a directory; default tenant + corpus; one-shot summary
ragctl ingest path/to/docs/
# Per-document progress line + custom tenant + corpus
ragctl ingest path/to/docs/ --per-doc --tenant acme --corpus kb
# Single file
ragctl ingest path/to/single.mdUses the same IngestPipeline as the gateway, wired with the default
parser registry + heading-aware chunker + default enricher + regex-PII
processor + noop embedder + noop vector store. No infra required.
Output:
path: docs/
corpus: ragctl-local
tenant: ragctl-local
total: 7
ingested: 6
blocked: 0
skipped: 1
failed: 0
--per-doc streams a one-liner per document as they complete:
ingested chunks=12 emb=12 bytes=2415 doc-1
ingested chunks=4 emb=4 bytes=489 doc-2
skipped_empty chunks=0 emb=0 bytes=0 empty.txt
| Field | Type | Notes |
|---|---|---|
document_id |
DocumentId |
unchanged from the input Document |
status |
IngestStatus |
enum (see above) |
chunk_count |
int |
chunks indexed (post-PII filter) |
embedding_count |
int |
typically equals chunk_count |
blocked_chunk_count |
int |
chunks dropped by PII processor block action |
bytes_in |
int |
size of the source bytes |
error |
str | None |
populated when status == failed or blocked_by_policy |
metadata |
dict[str, Any] |
parser name, MIME type, PII summary, etc. |
| Field | Type | Notes |
|---|---|---|
results |
list[IngestResult] |
full per-document detail |
total / ingested / blocked / skipped / failed |
int |
per-status counters |
Both ship in dist/schemas/IngestResult.json and
dist/schemas/IngestRunSummary.json.
The pipeline is call-site composable — every dependency is a
constructor argument. Custom chunker / enricher / embedder / vector
store implementations plug in without subclassing IngestPipeline
itself.
To inject a custom stage between two existing ones, wrap the relevant
SPI with a decorator that calls through to the wrapped instance. The
upcoming Step 1.10 follow-ups (PolicyEngine wiring, rag.yaml
backend selection, multi-stage observability spans) all land via the
same shape.