An event-driven document processing pipeline running as a FastAPI Container App on Azure. The system automates the ingestion, extraction, analysis, and indexing of documents — turning unstructured files into structured, searchable data with no manual intervention.
- A user uploads a document (PDF, Word, image, etc.) to the Azure Blob Storage
ingestcontainer. - Azure Event Grid detects the
BlobCreatedevent and sends a webhook to the Container App. - The app queues a processing job and a background worker runs a 5-step pipeline:
- Move the blob from
ingest→processing(prevents duplicate triggers). - Extract text, tables, barcodes, and layout using Azure Document Intelligence (prebuilt-layout model).
- Analyze the extracted content with Azure OpenAI GPT-4o — entity extraction, category classification, keyword tagging, and hierarchical summarization (page-level → section → document).
- Generate vector embeddings using Azure OpenAI text-embedding-ada-002 (1536-dimensional) for the document summary, purpose, and individual text chunks.
- Store structured results and embeddings in Azure Cosmos DB (NoSQL, serverless) with vector indexes for semantic search.
- Move the blob from
- The original document is archived to the
originaldocumentcontainer. - Users can monitor jobs on the status dashboard, search documents semantically via the vector search API, and download original files via SAS-protected URLs.
| Service | Role |
|---|---|
| Azure Container Apps | Hosts the FastAPI application (Uvicorn, 2 workers, VNet-integrated) |
| Azure Blob Storage | Document lifecycle — ingest, processing, completed, originaldocument containers |
| Azure Event Grid | Triggers processing on blob upload (BlobCreated system topic) |
| Azure Document Intelligence | OCR / layout extraction from 13+ file types |
| Azure OpenAI Service | GPT-4o for analysis; text-embedding-ada-002 for vector embeddings |
| Azure Cosmos DB (NoSQL, Serverless) | Job tracking, analysis results, and vector search via VectorDistance() |
| Azure Container Registry | Stores the Docker image; pulled by Container Apps via managed identity |
| Microsoft Entra ID | Easy Auth on the Container App; RBAC for all service-to-service auth (no API keys) |
- Automated ingestion — drop a file in the
ingestcontainer, processing starts automatically via Event Grid - Document Intelligence extraction — text, tables, barcodes, layout from PDFs, images, Office docs (13+ file types)
- LLM analysis — per-page entity/category/keyword extraction, hierarchical summarization for large documents
- Vector embeddings — summary, purpose, and chunk-level embeddings (text-embedding-ada-002, 1536 dimensions)
- Cosmos DB vector search — semantic search across documents and chunks using
VectorDistance() - Status dashboard — real-time job monitoring, retry failed jobs, delete jobs
- Zero API keys — 100% managed identity with RBAC, no secrets in config
- Entra ID Easy Auth — all endpoints (except webhooks and health) require AAD login
The pipeline accepts any file type supported by the Azure Document Intelligence prebuilt-layout model. Text is extracted along with layout structure, tables, and barcodes where applicable.
| Format | Extensions | Notes |
|---|---|---|
.pdf |
Native text and scanned (OCR) | |
| Microsoft Word | .docx, .doc |
Text and embedded tables |
| Microsoft Excel | .xlsx |
Tabular data extraction |
| Microsoft PowerPoint | .pptx |
Slide text and tables |
| Plain Text | .txt |
Direct text ingestion |
| Images | .jpg, .jpeg, .png, .bmp, .tiff, .tif, .gif |
OCR-based text extraction |
| Method | Path | Description |
|---|---|---|
GET |
/ |
Dashboard (redirect to status UI) |
GET |
/health |
Health check |
POST |
/api/events/blob |
Event Grid webhook (BlobCreated) |
GET |
/api/status |
List jobs (?status=queued|processing|completed|failed, ?limit=) |
GET |
/api/status/{job_id} |
Job detail with processing steps |
POST |
/api/status/{job_id}/retry |
Re-queue a failed job |
DELETE |
/api/status/{job_id} |
Delete job and its results |
GET |
/api/documents |
List completed analysis results |
GET |
/api/documents/{job_id} |
Get full analysis result |
GET |
/api/documents/{job_id}/original |
Download original document (SAS URL redirect) |
POST |
/api/search |
Vector similarity search ({query, vector_field, top, filter}) |
POST |
/api/search/chunks |
Chunk-level vector search ({query, top}) |
All endpoints except /api/events/* and /health are protected by Entra ID Easy Auth.
- Azure subscription with permissions to create resources
- Azure CLI (
az) — install - Terraform >= 1.5 — install
- Docker (for local development) — install
- Python 3.11+ (for local development without Docker)
💡 Tip — Azure Cloud Shell: If you don't want to install the Azure CLI or Terraform locally, you can run the infrastructure deployment entirely from Azure Cloud Shell (Bash). It comes with
az,terraform, andgitpre-installed. Clone this repo in Cloud Shell, then follow the Terraform steps below. Docker and Python are only needed for local development and are not required for deploying infrastructure.
cd terraform
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars with your values:
| Variable | Example | Description |
|---|---|---|
location |
"eastus2" |
Azure region for all resources |
project_name |
"docpipeline" |
Short name used in resource naming (e.g. rg-docpipeline-dev-xxxx) |
environment |
"dev" |
Environment label (dev, staging, prod) |
container_image |
"docpipeline:latest" |
ACR image tag — leave commented out on first run, update after the initial ACR build in step 3 |
tags |
{ owner = "your-team" } |
Tags applied to every resource |
Then run:
terraform init
terraform plan # review what will be created
terraform apply # type "yes" to confirmImportant: After
terraform apply, note the outputs — you'll needacr_login_server,resource_group_name, and the service endpoints for configuration. Runterraform outputat any time to see them again.
📘 See terraform/README.md for the full list of resources created, all configurable variables, post-deployment steps, and file-by-file documentation of the Terraform modules.
Two places have hardcoded IP addresses that you need to update for your network:
terraform/cosmos.tf—ip_range_filteron the Cosmos DB accountterraform/nsp.tf—addressPrefixesin the NSP home access rule
Replace these with your public IP (find it at https://ifconfig.me).
# From the repo root
az acr build \
--registry <acr_login_server> \
--resource-group <resource_group_name> \
--image docpipeline:latest .The Container App will automatically pull the new image. Check the dashboard:
https://<container_app_url>/
You can also verify the health endpoint:
curl https://<container_app_url>/health
# {"status":"healthy"}Upload any supported file to the ingest blob container:
az storage blob upload \
--account-name <storage_account_name> \
--container-name ingest \
--file ./my-document.pdf \
--auth-mode loginThe pipeline will trigger automatically. Monitor progress on the dashboard.
cp .env.example .env
# Fill in values from your Terraform outputs (run `terraform -chdir=terraform output`)
docker compose up --buildThe app will be available at http://localhost:8000. Note that Event Grid webhooks won't fire locally — you can test the API directly.
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Fill in values
# Authenticate to Azure for DefaultAzureCredential
az login
uvicorn app.main:app --reload --port 8000# Health check
curl http://localhost:8000/health
# List jobs
curl http://localhost:8000/api/status
# Semantic search
curl -X POST http://localhost:8000/api/search \
-H "Content-Type: application/json" \
-d '{"query": "water rights agreement", "top": 5}'All configuration is via environment variables. No API keys — the app uses DefaultAzureCredential (managed identity in Azure, az login locally).
| Variable | Required | Default | Description |
|---|---|---|---|
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT |
Yes | — | Document Intelligence endpoint URL |
AZURE_OPENAI_ENDPOINT |
Yes | — | Azure OpenAI endpoint URL |
AZURE_STORAGE_ACCOUNT_URL |
Yes | — | Storage account blob endpoint |
COSMOS_ENDPOINT |
Yes | — | Cosmos DB account endpoint |
AZURE_OPENAI_API_VERSION |
No | 2024-12-01-preview |
OpenAI API version |
AZURE_OPENAI_MODEL_NAME |
No | gpt-4o |
Chat model deployment name |
AZURE_OPENAI_EMBEDDING_MODEL |
No | text-embedding-ada-002 |
Embedding model deployment |
COSMOS_DATABASE_NAME |
No | docprocessing |
Cosmos DB database name |
MAX_CONCURRENT_JOBS |
No | 5 |
Max parallel processing jobs |
LOG_LEVEL |
No | INFO |
Logging level |
AZURE_TENANT_ID |
No | — | For local dev if auto-detect fails |
AZURE_CLIENT_ID |
No | — | For local dev if auto-detect fails |
├── app/ # FastAPI application (runs in the container)
│ ├── main.py # App entry point, lifespan, router registration
│ ├── config.py # Settings from environment variables
│ ├── agents/
│ │ └── planner.py # PipelineOrchestrator — 5-step processing pipeline
│ ├── models/
│ │ ├── document.py # DocumentAnalysisResult schema (output)
│ │ └── processing.py # ProcessingJob, ProcessingStep (job tracking)
│ ├── plugins/ # Azure service wrappers
│ │ ├── blob_plugin.py # Azure Blob Storage operations
│ │ ├── cosmos_plugin.py # Cosmos DB CRUD + vector search
│ │ ├── doc_intel_plugin.py # Azure Document Intelligence
│ │ └── llm_plugin.py # Azure OpenAI analysis + embeddings
│ ├── routers/ # API endpoint handlers
│ │ ├── events.py # Event Grid webhook
│ │ ├── status.py # Job list / detail / retry / delete
│ │ ├── documents.py # Analysis results + original download
│ │ └── search.py # Vector similarity search
│ ├── services/
│ │ └── processing.py # Background async worker (asyncio.Queue)
│ └── web/static/
│ └── index.html # Status dashboard (single-page HTML)
├── terraform/ # Infrastructure as Code (see terraform/README.md)
├── legacy/ # Archived Azure Functions code (reference only)
├── Dockerfile # Multi-stage Python 3.11 build
├── docker-compose.yml # Local development
├── requirements.txt # Python dependencies
└── .env.example # Environment variable template
| Container | Purpose |
|---|---|
ingest |
Landing zone — upload files here to trigger processing |
processing |
Working area — files moved here during pipeline execution |
completed |
Results — JSON analysis output stored as {job_id}/{filename}.json |
originaldocument |
Archive — original files moved here after successful processing |
Database: docprocessing
| Container | Partition Key | Contents |
|---|---|---|
jobs |
/id |
Processing job records (status, steps, timestamps) |
results |
/jobId |
Analysis results with vector embeddings |
The results container has vector indexes (quantizedFlat, cosine, 1536d) on:
/summary_vector— embedding of the document summary/purpose_vector— embedding of the document purpose/chunks/vector— per-chunk embeddings for granular retrieval
The Container App uses system-assigned managed identity with these roles:
| Role | Scope |
|---|---|
| Storage Blob Data Contributor | Storage Account |
| Storage Blob Delegator | Storage Account |
| Cosmos DB Built-in Data Contributor | Cosmos DB |
| Cognitive Services OpenAI User | Azure OpenAI |
| Cognitive Services User | Document Intelligence |
| AcrPull (user-assigned MI) | Container Registry |
Entra ID Easy Auth protects the web UI and API. Event Grid webhook and health endpoints are excluded from auth.