A production-ready Retrieval-Augmented Generation (RAG) API built with .NET 8 and Azure AI services. Upload documents, ask questions grounded in their content, run semantic search, and use an AI agent that can invoke tools — all through a clean REST API.
| Capability | Description |
|---|---|
| Document ingestion | Upload PDF, DOCX, Markdown, HTML, or plain text files. The API parses, chunks, embeds, and indexes them automatically. |
| RAG Q&A | Ask a question and receive an answer grounded in your uploaded documents, with source citations. |
| Semantic search | Vector similarity search across all indexed content. |
| Multi-turn chat | Maintain conversation history across multiple questions. |
| AI agent (function calling) | An agentic loop that can call registered tools (financial summaries, document search, expense entries, weather) to answer complex queries. |
| Structured extraction | Extract structured data (invoices, receipts) from documents and map them to ledger entries. |
| Predictive analytics | Forecast spending, detect anomalies, and get budget recommendations powered by GPT-4o. |
AzureAI.Api ← Minimal API endpoints, middleware, health checks
AzureAI.Application ← MediatR commands/queries, validation, pipeline behaviors
AzureAI.Core ← Domain entities, value objects, interfaces (no dependencies)
AzureAI.Infrastructure ← Azure OpenAI, Azure AI Search, EF Core (PostgreSQL), document parsers
AzureAI.FunctionCalling ← Tool registry, function-calling orchestrator
AzureAI.Extraction ← Invoice/receipt extraction services
Clean Architecture: Core has zero external dependencies. Infrastructure implements Core interfaces. Application orchestrates use cases. Api is the composition root.
- .NET 8 SDK
- Docker Desktop (for PostgreSQL + Seq)
- An Azure OpenAI resource with two deployed models:
- A chat model —
gpt-4o(recommended) - An embedding model —
text-embedding-3-large(recommended, 3072 dimensions)
- A chat model —
- An Azure AI Search resource (Basic tier or higher for vector search)
git clone https://github.com/nirjash13/azure-openai-rag.git
cd azure-openai-rag
cp .env.example .envEdit .env and fill in your Azure credentials:
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net
AZURE_SEARCH_API_KEY=your-search-api-keydocker compose up postgres seq -ddotnet run --project src/AzureAI.ApiThe API starts at http://localhost:5100. On first run it:
- Applies database migrations automatically
- Creates the Azure AI Search index if it doesn't exist
Swagger UI is available at http://localhost:5100/swagger in Development mode.
docker compose up --build| Service | URL |
|---|---|
| API | http://localhost:5100 |
| Swagger | http://localhost:5100/swagger |
| Seq logs | http://localhost:5341 |
All settings live in src/AzureAI.Api/appsettings.json. Key sections:
{
"AzureOpenAI": {
"Endpoint": "",
"ApiKey": "",
"ChatDeployment": "gpt-4o",
"EmbeddingDeployment": "text-embedding-3-large",
"MaxTokens": 2000,
"Temperature": 0.7
},
"AzureSearch": {
"Endpoint": "",
"ApiKey": "",
"IndexName": "azure-ai-rag-index",
"TopK": 5,
"MinScore": 0.5
},
"Chunking": {
"ChunkSizeTokens": 512,
"OverlapTokens": 64,
"MaxChunksPerDocument": 500
},
"Rag": {
"MaxContextTokens": 8000,
"MaxConversationHistory": 10,
"IncludeCitations": true
}
}In production, supply secrets via environment variables using the __ separator (e.g. AzureOpenAI__ApiKey=...). Never commit real keys.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/documents |
Upload a document (multipart/form-data) |
GET |
/api/v1/documents |
List all documents |
GET |
/api/v1/documents/{id} |
Get a document by ID |
DELETE |
/api/v1/documents/{id} |
Delete a document and its index entries |
Upload a document:
curl -X POST http://localhost:5100/api/v1/documents \
-F "file=@report.pdf;type=application/pdf"| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/chat |
Ask a question; get a grounded answer |
POST |
/api/v1/chat/conversations |
Create a conversation session |
GET |
/api/v1/chat/{conversationId} |
Get conversation history |
DELETE |
/api/v1/chat/{conversationId} |
Delete a conversation |
Ask a question:
curl -X POST http://localhost:5100/api/v1/chat \
-H "Content-Type: application/json" \
-d '{
"question": "What were the key findings in Q3?",
"conversationId": null,
"options": { "topK": 5, "includeCitations": true }
}'Multi-turn conversation:
# 1. Create a conversation
curl -X POST http://localhost:5100/api/v1/chat/conversations \
-H "Content-Type: application/json" \
-d '{"title": "Q3 Review"}'
# 2. Ask questions using the returned conversationId
curl -X POST http://localhost:5100/api/v1/chat \
-H "Content-Type: application/json" \
-d '{"question": "Summarise the risks", "conversationId": "<id>"}'| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/search |
Semantic vector search without generating a completion |
curl -X POST http://localhost:5100/api/v1/search \
-H "Content-Type: application/json" \
-d '{"query": "budget overrun", "topK": 10}'| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/agent/chat |
Send a message to the AI agent; it can call tools to answer |
GET |
/api/v1/agent/tools |
List all registered tools |
curl -X POST http://localhost:5100/api/v1/agent/chat \
-H "Content-Type: application/json" \
-d '{"message": "What is the financial summary for Q3 and what is the weather in London?"}'| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/extract/invoice |
Extract structured data from an invoice image or document |
POST |
/api/v1/extract/receipt |
Extract structured data from a receipt |
POST |
/api/v1/extract/to-ledger-entry |
Convert extracted invoice data to a ledger entry |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/analytics/forecast |
Forecast future spending based on historical data |
POST |
/api/v1/analytics/anomalies |
Detect anomalous transactions using z-score analysis |
POST |
/api/v1/analytics/recommendations |
Get AI-powered budget recommendations |
curl http://localhost:5100/healthReturns the status of PostgreSQL, Azure OpenAI, and Azure AI Search.
dotnet test101 tests across 5 assemblies — unit tests for domain logic, application handlers, and integration tests with a mock-backed WebApplicationFactory.
AzureAI.Core.Tests 36 tests — domain entities, value objects
AzureAI.Application.Tests 24 tests — command/query handlers, behaviors, analytics
AzureAI.Extraction.Tests 16 tests — invoice/receipt extraction logic
AzureAI.FunctionCalling.Tests 13 tests — tool registry, orchestrator, security
AzureAI.Integration.Tests 12 tests — HTTP endpoint contracts
azure-openai-rag/
├── src/
│ ├── AzureAI.Api/ # Web API (entry point)
│ │ ├── Endpoints/ # Minimal API route handlers
│ │ ├── HealthChecks/ # Azure OpenAI + Search health checks
│ │ └── Middleware/ # Exception handler, correlation ID, request logging
│ ├── AzureAI.Application/ # Use cases (MediatR)
│ │ ├── Commands/ # IngestDocument, DeleteDocument, CreateConversation
│ │ ├── Queries/ # AskQuestion, SemanticSearch, Analytics
│ │ └── Behaviors/ # Validation, logging, performance pipeline
│ ├── AzureAI.Core/ # Domain model (no dependencies)
│ │ ├── Domain/Entities/ # Document, DocumentChunk, ConversationSession
│ │ ├── Domain/ValueObjects/ # EmbeddingVector, ChunkMetadata, TokenUsage
│ │ └── Interfaces/ # Repository and service contracts
│ ├── AzureAI.Infrastructure/ # External service implementations
│ │ ├── AzureOpenAI/ # Embedding + completion services
│ │ ├── Search/ # Azure AI Search client, index manager
│ │ ├── DocumentProcessing/ # PDF (PdfPig), DOCX, HTML, Markdown parsers
│ │ └── Persistence/ # EF Core DbContext, repositories, migrations
│ ├── AzureAI.FunctionCalling/ # AI agent with tool registry
│ └── AzureAI.Extraction/ # Structured data extraction
└── tests/
├── AzureAI.Core.Tests/
├── AzureAI.Application.Tests/
├── AzureAI.Extraction.Tests/
├── AzureAI.FunctionCalling.Tests/
└── AzureAI.Integration.Tests/
| Format | Parser | Notes |
|---|---|---|
| UglyToad.PdfPig | Full text extraction per page | |
| DOCX | ZIP + XML | Paragraph boundaries preserved |
| Markdown | Custom | Headings, links, code blocks stripped to plain text |
| HTML | Regex | Scripts, styles, and tags removed; entities decoded |
| Plain text | StreamReader | Raw UTF-8 content |
- Clean Architecture — domain has zero external dependencies; infrastructure is pluggable
- MediatR CQRS — all use cases are commands or queries with pipeline behaviors for validation, logging, and performance monitoring
- Sliding window chunking — overlapping chunks preserve cross-boundary context; token counts use
TiktokenTokenizer(GPT-4o tokeniser) - Polly resilience — retry (exponential back-off + jitter) and circuit-breaker policies on all Azure HTTP calls
- Serilog structured logging — console + Seq sink; correlation ID propagated on every request
- EF Core + PostgreSQL — document metadata and conversation history persisted relationally; embeddings live in Azure AI Search
dotnet run fails with Azure credential errors
Fill in AzureOpenAI:Endpoint, AzureOpenAI:ApiKey, AzureSearch:Endpoint, and AzureSearch:ApiKey in appsettings.Development.json (not committed) or environment variables.
/health returns 503
One or more Azure services is unreachable. The API still starts — 503 from /health just means the background checks are failing. Check the Seq logs at http://localhost:5341 for details.
Postgres migration error on startup
Ensure PostgreSQL is running (docker compose up postgres -d) before starting the API. The API applies migrations automatically but needs a reachable database.
PDF text extraction returns empty content Confirm the PDF contains selectable text (not a scanned image). Scanned PDFs require an OCR step (not included).