RAG-Agent: Production-Grade Document Ingestion Service¶
Context¶
You have a mature RAG system in agent_alpha (/home/user/projects/agent_alpha/backend/services/rag/) with Milvus, multi-format parsing, hybrid search, and cross-encoder reranking. The rag-agent project is greenfield — this plan extracts agent_alpha's battle-tested patterns into a standalone, production-grade ingestion service that any pydantic-deepagents agent can call via HTTP API.
Key improvements over agent_alpha: ARQ task queue (replacing asyncio.create_task), batched embedding with retry/backoff, fine-grained SSE progress events, structured logging via structlog, proper dependency injection, and a clean connector plugin system.
Project Structure¶
rag-agent/
├── pyproject.toml
├── Dockerfile
├── Dockerfile.worker
├── docker-compose.yml
├── .env.example
├── alembic.ini
├── rag_agent/
│ ├── __init__.py
│ ├── app.py # FastAPI app factory + lifespan
│ ├── core/
│ │ ├── config.py # pydantic-settings: Settings
│ │ ├── database.py # SQLAlchemy async engine + session
│ │ ├── valkey.py # Valkey client singleton
│ │ ├── exceptions.py # Exception hierarchy
│ │ ├── logging.py # structlog setup
│ │ └── deps.py # FastAPI DI providers
│ ├── models/ # Pydantic domain models
│ │ ├── document.py # Document, DocumentPage, DocumentChunk, DocumentMetadata
│ │ ├── search.py # SearchResult, SearchRequest
│ │ ├── ingestion.py # IngestionStatus, IngestionProgress
│ │ ├── collection.py # CollectionInfo
│ │ └── common.py # DocumentInfo, PaginationParams
│ ├── db/ # SQLAlchemy ORM + Alembic
│ │ ├── base.py
│ │ ├── models/
│ │ │ ├── document.py # TrackedDocument (adds retry_count, content_hash, indexes)
│ │ │ └── sync_log.py # SyncLog
│ │ └── migrations/
│ ├── repositories/
│ │ ├── document_repo.py # TrackedDocument CRUD
│ │ └── sync_log_repo.py
│ ├── parsers/ # Strategy pattern (from agent_alpha/services/rag/documents.py)
│ │ ├── base.py # BaseDocumentParser ABC
│ │ ├── text.py # .txt, .md
│ │ ├── docx.py # .docx via python-docx
│ │ ├── pdf.py # .pdf via PyMuPDF (text+tables+images+OCR)
│ │ └── registry.py # PARSER_REGISTRY: ext -> parser
│ ├── chunkers/
│ │ ├── base.py # BaseChunker ABC
│ │ ├── recursive.py # RecursiveCharacterTextSplitter
│ │ └── markdown.py # MarkdownHeaderTextSplitter
│ ├── embeddings/
│ │ ├── base.py # BaseEmbeddingProvider ABC
│ │ ├── openai_compat.py # OpenAI-compatible (works with vLLM/Ollama)
│ │ └── service.py # EmbeddingService: batching + dim validation
│ ├── vectorstore/
│ │ ├── base.py # BaseVectorStore ABC
│ │ ├── milvus.py # MilvusVectorStore (AsyncMilvusClient)
│ │ └── service.py # Collection lifecycle, metadata building
│ ├── rerankers/
│ │ ├── base.py # BaseReranker ABC
│ │ ├── cross_encoder.py # CrossEncoder (ms-marco-MiniLM-L6-v2)
│ │ └── service.py # RerankService orchestrator
│ ├── connectors/ # Plugin system (from agent_alpha/rag/connectors.py)
│ │ ├── base.py # BaseConnector ABC + CONNECTOR_REGISTRY
│ │ └── local.py # LocalFilesystemConnector
│ ├── pipeline/
│ │ ├── processor.py # DocumentProcessor: parse -> describe -> chunk
│ │ ├── ingestion.py # IngestionService: full pipeline with dedup
│ │ ├── image_describer.py # LLM vision for extracted images
│ │ └── file_storage.py # LocalFileStorage
│ ├── retrieval/
│ │ ├── service.py # RetrievalService: vector -> BM25 -> rerank -> filter -> dedup
│ │ └── bm25.py # BM25Okapi keyword search
│ ├── worker/
│ │ ├── settings.py # ARQ WorkerSettings
│ │ ├── tasks.py # ingest_document_task, sync_collection_task
│ │ └── dispatcher.py # Enqueue wrapper
│ ├── routes/
│ │ ├── health.py # /health, /ready, /live
│ │ ├── collections.py # /api/v1/collections CRUD
│ │ ├── documents.py # /api/v1/documents: upload, list, get, delete, retry
│ │ ├── search.py # /api/v1/search
│ │ ├── sync.py # /api/v1/sync
│ │ ├── connectors.py # /api/v1/connectors
│ │ └── status.py # /api/v1/status (SSE)
│ ├── schemas/ # API wire-format models
│ │ ├── document.py
│ │ ├── collection.py
│ │ ├── search.py
│ │ ├── sync.py
│ │ └── common.py # ErrorResponse, PaginatedResponse
│ └── services/
│ ├── document_service.py # Document lifecycle orchestration
│ ├── sync_service.py # Sync orchestration
│ └── status_service.py # SSE via Valkey pub/sub
├── tests/
│ ├── conftest.py
│ ├── unit/ # No external deps
│ ├── integration/ # Requires Docker services
│ └── api/ # FastAPI TestClient
└── scripts/
├── wait_for_services.py
└── seed_test_data.py
Ingestion Pipeline¶
Upload -> Validate -> Store File -> DB Track (queued) -> ARQ Enqueue
┌─── ARQ Worker ───────────────────────────────────────────────┐
│ [1] Parse — Route to parser by ext (PyMuPDF/docx/text) │
│ [2] Describe — LLM vision for extracted images (optional) │
│ [3] Chunk — RecursiveCharacter or Markdown splitter │
│ [4] Dedup — Check by source_path then content_hash (SHA-256)│
│ [5] Embed — Batched OpenAI-compatible API (100/batch) │
│ [6] Store — Milvus insert + flush │
│ [7] Complete — DB update (status=done, chunk_count=N) │
└──────────────────────────────────────────────────────────────┘
Each stage emits IngestionProgress to Valkey pub/sub -> SSE stream
Improvements over agent_alpha:
- Batched embedding: agent_alpha sends all chunks in one call — new design batches at 100
- Retry with backoff: 3 retries, 1s base / 30s max delay on all external calls
- Fine-grained progress: QUEUED → PARSING → CHUNKING → EMBEDDING → STORING → DONE (agent_alpha only has coarse PROCESSING/ADDING/DONE)
- Indexed dedup: DB index on content_hash instead of linear scan
Retrieval Pipeline¶
Query -> Embed -> Milvus cosine search (limit × fetch_multiplier)
-> [optional] BM25 + RRF fusion (k=60)
-> [optional] CrossEncoder rerank
-> Score filter (min_score)
-> Dedup (per parent_doc_id:chunk_num)
-> Truncate to limit
Multi-collection search runs each collection independently, tags results with collection metadata, merges and deduplicates.
API Design¶
All endpoints prefixed with /api/v1/. JSON envelope: {"data": ...} / {"error": "...", "details": {...}}.
| Method | Path | Purpose |
|---|---|---|
| GET | /health |
Liveness (always 200) |
| GET | /ready |
Readiness (checks Milvus, PG, Valkey) |
| POST | /api/v1/documents/upload |
Upload file (multipart) |
| POST | /api/v1/documents/upload/{collection} |
Upload to specific collection |
| GET | /api/v1/documents |
List tracked documents (filterable) |
| GET | /api/v1/documents/{id} |
Document detail |
| DELETE | /api/v1/documents/{id} |
Cascade delete (DB + Milvus + file) |
| POST | /api/v1/documents/{id}/retry |
Re-queue failed ingestion |
| GET | /api/v1/documents/{id}/download |
Download original file |
| GET | /api/v1/collections |
List collections |
| POST | /api/v1/collections?name=... |
Create collection |
| GET | /api/v1/collections/{name} |
Collection stats |
| DELETE | /api/v1/collections/{name} |
Drop collection |
| POST | /api/v1/search |
Vector search |
| POST | /api/v1/search/multi |
Multi-collection search |
| POST | /api/v1/sync |
Trigger directory sync |
| GET | /api/v1/sync/logs |
Sync history |
| GET | /api/v1/connectors |
List available connectors |
| GET | /api/v1/status |
SSE stream for ingestion progress |
Infrastructure (Docker Compose)¶
| Service | Image | Port | Role |
|---|---|---|---|
api |
Custom (Python 3.12) | 8100 | FastAPI application |
worker |
Same image | — | ARQ worker process |
postgres |
postgres:16-alpine |
5433 | Document tracking DB |
valkey |
valkey/valkey:8-alpine |
6379 | Cache + pub/sub + ARQ queue |
milvus |
milvusdb/milvus:v2.5.4 |
19530 | Vector database (standalone) |
milvus-etcd |
quay.io/coreos/etcd:v3.5.18 |
2379 | Milvus metadata |
milvus-minio |
minio/minio:latest |
9000 | Milvus object storage |
Shared volumes: media_data (uploaded files), model_cache (sentence-transformers models), postgres_data, milvus_data.
ARQ Worker Config¶
class WorkerSettings:
functions = [ingest_document_task, sync_collection_task]
max_jobs = 4 # concurrent ingestion tasks
job_timeout = 600 # 10 min per document
retry_jobs = True
max_tries = 3
# on_startup: build IngestionService singleton in worker process
# on_job_start: bind structlog context (job_id, doc_id)
DB Schema Improvements (over agent_alpha)¶
tracked_documents table adds:
- retry_count: int (default 0) — tracks ARQ retries
- last_error: str | None — most recent error message
- source_type: str — local/s3/gdrive/web (for connector expansion)
- content_hash: str — SHA-256 for dedup queries
- Indexes on (collection_name, status), (content_hash,), (source_path,)
pydantic-deepagents Integration¶
A thin RAGAgentClient (httpx-based) wraps the API. A create_rag_toolset() factory produces a pydantic-ai FunctionToolset with tools:
- rag_search(query, collection, limit) — search the knowledge base
- rag_upload(file_path, collection) — upload a document
- rag_status(doc_id) — check ingestion status
- rag_list_collections() — list available collections
Agents pass this toolset to create_deep_agent(toolsets=[create_rag_toolset("http://rag-agent:8100")]).
Key Dependencies¶
fastapi, uvicorn[standard], python-multipart
pydantic, pydantic-settings
sqlalchemy[asyncio], asyncpg, alembic
redis (Valkey protocol), arq
pymilvus, pymupdf, python-docx
langchain-text-splitters, openai (compatible client)
sentence-transformers, rank-bm25
structlog, httpx
Implementation Order¶
- Foundation (config, models, DB, Docker Compose infra, health endpoints)
- Parsing + Chunking (parsers, chunkers, DocumentProcessor, unit tests)
- Embedding + Vector Store (OpenAI-compat provider, Milvus adapter, integration test)
- Ingestion Pipeline (IngestionService, file storage, document API routes)
- Task Queue (ARQ worker, task definitions, worker Dockerfile)
- Search + Retrieval (RetrievalService, BM25, reranker, search routes)
- Status + Sync (SSE streaming, local connector, sync routes)
- Hardening (retry/backoff, graceful degradation, input validation, integration tests, pydantic-deepagents client library)
Source Files to Extract From¶
| rag-agent module | agent_alpha source |
|---|---|
parsers/pdf.py |
backend/services/rag/documents.py (PyMuPDFParser class) |
parsers/docx.py, text.py |
backend/services/rag/documents.py (DocxDocumentParser, TextDocumentParser) |
chunkers/recursive.py |
backend/services/rag/documents.py (_create_splitter) |
embeddings/openai_compat.py |
backend/services/rag/embeddings.py (OpenAIEmbeddingProvider) |
vectorstore/milvus.py |
backend/services/rag/vectorstore.py (MilvusVectorStore) |
retrieval/service.py |
backend/services/rag/retrieval.py (RetrievalService) |
rerankers/cross_encoder.py |
backend/services/rag/reranker.py (CrossEncoderReranker) |
pipeline/ingestion.py |
backend/services/rag/ingestion.py (IngestionService) |
pipeline/image_describer.py |
backend/services/rag/image_describer.py (PydanticAIImageDescriber) |
connectors/base.py |
backend/rag/connectors.py (BaseConnector + CONNECTOR_REGISTRY) |
db/models/document.py |
backend/db/models/rag_document.py (RAGDocument) |
services/status_service.py |
backend/services/rag_status.py (SSE streaming) |
worker/tasks.py |
backend/worker/tasks/rag_tasks.py (task functions) |
core/config.py |
backend/services/rag/config.py (RAGSettings, EMBEDDING_DIMENSIONS) |
Verification¶
docker-compose up— all 7 services healthycurl http://localhost:8100/ready— all dependency checks pass- Upload a PDF:
curl -X POST http://localhost:8100/api/v1/documents/upload -F "file=@test.pdf" - Monitor SSE:
curl -N http://localhost:8100/api/v1/status— see progress events - Verify in DB:
GET /api/v1/documents— document shows status=done with chunk_count - Search:
POST /api/v1/searchwith a query about the PDF content — get relevant chunks back - Run test suite:
pytest tests/ -v