Secure internal AI assistant for law firms, built on Azure. Attorneys ask questions in a web chat or Microsoft Teams; the system retrieves relevant content from firm-approved documents (SharePoint/OneDrive) using RAG and returns grounded answers via Azure OpenAI.
✔ Citation-based answers
✔ Conversation memory
✔ Role-based document access
✔ Audit logging
✔ Structured AI responses
┌─────────────────────┐
│ Attorneys / Staff │
└──────────┬──────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼──────┐ ┌─────▼──────┐
│ Web Chat │ │ Teams │ │ Slack │
│ (Custom) │ │ Bot │ │ Bot │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└──────────────┼───────────────┘
│
┌─────────▼─────────┐
│ Azure App Service │
│ (.NET 8 + Auth) │
└────┬─────────┬─────┘
│ │
┌─────────▼──┐ ┌───▼──────────┐
│ Azure AI │ │ Azure OpenAI │
│ Search │ │ (GPT-4o) │
│ (RAG) │ │ │
└─────┬──────┘ └──────────────┘
│
┌─────▼──────┐
│ SharePoint │
│ / OneDrive │
└────────────┘
| # | Component | Azure Service | Purpose |
|---|---|---|---|
| 1 | AI Brain | Azure OpenAI (GPT-4o) | Generates answers from retrieved legal context |
| 2 | Document Storage | SharePoint / OneDrive | Stores firm-approved legal documents |
| 3 | Search Layer | Azure AI Search | Indexes documents, vector + keyword search (RAG) |
| 4 | Access Control | Microsoft Entra ID | Role-based access, audit trail, SSO |
| 5 | Interface | Web Chat + Teams Bot | How attorneys interact with the assistant |
- Azure Key Vault - Secrets management for non-code secrets with RBAC
- Application Insights - Monitoring, logging, audit trail
- Azure VNet - Network isolation with private endpoints (prod)
- Budget Alerts - Cost controls per environment
- Authentication + authorization: API validates Entra token scope/role (
ApiAccessPolicy) and matter claims. - Query security + rewrite: prompt security middleware sanitizes input, then query rewrite service normalizes legal phrasing.
- Retrieval pipeline (
IRetrievalService): vector/hybrid search in Azure AI Search retrieves chunks with metadata. - Document-level authorization (
IAuthorizationFilter): chunks are filtered bymatterId,accessGroup, and required metadata before prompt assembly. - Prompt assembly (
IPromptBuilder): grounded context + conversation history are composed into strict JSON-only legal prompt instructions. - Generation (
IChatService): Azure OpenAI produces structured answer; insufficient context falls back toInsufficient information in approved documents. - Provenance + response shaping: citations/source metadata are enriched and returned in
/askresponse (answer,sources,sourceMetadata). - Audit + metrics: structured audit logs include correlation ID, user/claims summary, retrieval counts, source metadata summary, and final answer status.
- Matter-level access control: requests must include
matterId; user claims must include permitted matter values. - Group-level document filtering: retrieved chunks are filtered against
accessGroupclaims before model context is built. - Grounded-only answering: system prompt forbids unsupported legal conclusions when approved context is missing.
- Sensitive data minimization: diagnostics and audit logs include metadata/snippets, not full document bodies.
- Secure defaults: managed identity and environment-driven configuration are used for Azure Search/OpenAI connectivity (no hardcoded secrets).
- Endpoint:
POST /debug/retrieval - Gated by configuration:
DebugRag:Enabled=trueorDEBUG_RAG=true - Intended for protected debugging in controlled environments.
- Response includes query, user claims summary, raw/filtered counts, source metadata, prompt context preview, and fallback reason.
Example config (local):
export DEBUG_RAG=trueNote: This repository is a monorepo containing backend API code, infrastructure-as-code (Bicep), and a frontend SPA (
agent13-frontend/). Seedocs/MONOREPO_ARCHITECTURE.mdfor an explanation of this structure anddocs/REPOSITORY_SEPARATION_GUIDE.mdfor the recommended migration path to separate the frontend into its own repository.
infra/
main.bicep # Orchestrator - wires all modules
modules/
appservice.bicep # App Service + Entra ID auth (EasyAuth v2)
botservice.bicep # Azure Bot Service (Teams + Web Chat)
keyvault.bicep # Key Vault (RBAC-based)
monitoring.bicep # Log Analytics + App Insights
networking.bicep # VNet, NSGs, private endpoints, DNS
openai.bicep # Azure OpenAI (GPT-4o + embeddings)
search.bicep # Azure AI Search + private endpoints
storage.bicep # Storage Account
budget.bicep # Cost management budgets
params/
dev.bicepparam # Dev environment parameters
prod.bicepparam # Prod environment parameters
src/
Program.cs # .NET 8 API (web chat + /ask endpoint)
wwwroot/ # Web chat frontend (HTML/CSS/JS)
scripts/
setup-entra.sh # Create Entra ID app registration
setup-sharepoint-indexer.sh # Configure SharePoint search indexer
bootstrap.sh # Full environment bootstrap
deploy.sh # App deployment
full-deploy.sh # Build + configure + deploy in one step
tenant-deploy.sh # Multi-tenant deployment
run-local.sh # Run the app locally
run-query.sh # Run interactive Python query tool via shared env adapter
run-test-embedding.sh # Run Python embedding connectivity test via shared env adapter
ingest.py # Document ingestion (embeddings + upload)
query.py # Interactive RAG query CLI
- Azure CLI (
az) installed and logged in - .NET 8 SDK
- If
dotnetis missing in your runner/container, run./scripts/preflight-env.shfirst, then use./scripts/setup-dotnet.shand seedocs/DOTNET_SETUP.md.
- If
- Python 3.8+ (for document ingestion)
- An Azure subscription with Owner/Contributor access
This repo now supports a canonical shared env file and adapters for each runtime:
cp .env.shared.example .env.shared
# Edit .env.shared with real endpoints, deployment names, and IDs- .NET adapter:
scripts/adapters/dotnet-env.sh - Python adapter:
scripts/adapters/python-env.sh - Vite adapter generator:
agent13-frontend/scripts/generate-vite-env.sh
# Dev environment (no VNet, no auth)
az deployment group create \
--resource-group <rg-name> \
--template-file infra/main.bicep \
--parameters infra/params/dev.bicepparam
# Prod environment (VNet + private endpoints enabled automatically)
az deployment group create \
--resource-group <rg-name> \
--template-file infra/main.bicep \
--parameters infra/params/prod.bicepparam# Register the app in Entra ID (stores secret in Key Vault automatically)
./scripts/setup-entra.sh -n agent13-app-prod -v agent13-kv-prod
# Redeploy with the client ID from the output
az deployment group create \
--resource-group <rg-name> \
--template-file infra/main.bicep \
--parameters environment=prod entraClientId='<client-id>'# Configure the SharePoint indexer (uses Entra creds from step 2)
./scripts/setup-sharepoint-indexer.sh \
-s agent13-search-prod \
-k <search-admin-key> \
-t contoso \
-u /sites/LegalDocs \
-a <entra-app-id> \
-p <entra-app-secret> \
-rThe ingestion pipeline supports:
- Local folder source (
--source folder, default) - SharePoint source via Graph connector credentials (
--source sharepoint)
It performs chunking, embedding generation via Azure OpenAI, batched upload to Azure AI Search, metadata enrichment, retry logic, and structured logging.
cp .env.shared.example .env.shared
# Edit .env.shared with your endpoints/deployments
# Copy documents/metadata.example.json to documents/metadata.json and
# set matterId/practiceArea/client/confidentialityLevel per file
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
bash -lc 'source ./scripts/adapters/python-env.sh ./.env.shared && .venv/bin/python scripts/ingest.py --source folder --documents-path documents'scripts/ingest.py now requires documents/metadata.json entries with matterId for each ingested file.
If Search RBAC/token auth is restricted in your environment, you can provide an admin key at runtime:
AZURE_SEARCH_KEY='<search-admin-key>' .venv/bin/python scripts/ingest.pySet these variables in .env.shared first:
SHAREPOINT_TENANT_IDSHAREPOINT_CLIENT_IDSHAREPOINT_CLIENT_SECRETSHAREPOINT_DRIVE_ID- Optional:
SHAREPOINT_FOLDER_PATH
Then run:
bash -lc 'source ./scripts/adapters/python-env.sh ./.env.shared && .venv/bin/python scripts/ingest.py --source sharepoint'Notes:
- SharePoint ingestion currently processes UTF-8 text-like files (
.txt,.md,.csv,.json). - The pipeline requires a
matterId; if not present in metadata/content/filename, setRAG_DEFAULT_MATTER_IDin.env.shared.
Use wrapper scripts so Python tools always consume variables from .env.shared through the adapter:
./scripts/run-test-embedding.sh ./.env.shared
./scripts/run-query.sh ./.env.shared./scripts/deploy.sh
# Or use the full deployment script
./scripts/tenant-deploy.sh -t <tenant-id> -s <subscription-id> -e prodIf you deploy manually with az webapp deploy, package the publish output as a ZIP first.
Passing a folder path defaults to --type static, which fails for ASP.NET app content.
dotnet publish ./src/LegalRagApp.csproj -c Release -o ./publish
(cd publish && zip -r ../publish.zip .)
az webapp deploy \
--resource-group <rg-name> \
--name <webapp-name> \
--src-path ./publish.zip \
--type zipmatterId is required on every request to enforce matter-level retrieval filtering.
The caller token must also contain a permitted matter claim (configurable claim types under Authorization:MatterIdClaimTypes), otherwise the API returns 403 Forbidden.
{
"question": "Summarize indemnification obligations.",
"matterId": "MATTER-001",
"practiceArea": "Corporate",
"client": "Contoso",
"confidentialityLevel": "Internal"
}Development-only bypasses (do not use in production):
Authorization:BypassAuthInDevelopmentAuthorization:BypassMatterAuthorizationInDevelopment
dotnet build
dotnet testIntegration tests use fakes/stubs (no live Azure dependency required) via WebApplicationFactory and test auth handlers.
- Bicep deployment config supports managed identity app settings and environment-specific toggles.
- Production templates are designed for private networking readiness (VNet/private endpoints modules) and secure transport defaults.
- Keep debug diagnostics disabled in production unless temporarily required for incident triage.
- Entra group overage handling and graph-backed claim expansion.
- Per-document legal hold and retention policy enforcement in retrieval filters.
- Signed audit export pipeline for downstream compliance systems.
az deployment group create \
--resource-group <rg-name> \
--template-file infra/main.bicep \
--parameters environment=prod \
entraClientId='<app-client-id>' \
botEntraAppId='<bot-app-client-id>'Workflows use Azure OIDC (federated identity), not AZURE_CREDENTIALS secrets.
Configure these repository/environment variables in GitHub:
AZURE_CLIENT_IDAZURE_TENANT_IDAZURE_SUBSCRIPTION_ID
- Entra ID SSO - Only authenticated firm members can access the assistant
- Role-based access - Controlled via Entra ID groups and App Service EasyAuth
- Network isolation - VNet with private endpoints in production
- HTTPS only - TLS 1.2 minimum, FTPS disabled
- Key Vault - All secrets stored centrally with RBAC
- Audit trail - Application Insights logs all queries and responses
- Data residency - Documents stay in your Azure tenant and SharePoint
| Parameter | Description | Default |
|---|---|---|
environment |
dev or prod |
Required |
location |
Azure region | westus3 |
namePrefix |
Resource name prefix | agent13 |
entraClientId |
Entra app client ID (enables auth) | '' (disabled) |
botEntraAppId |
Bot app client ID (enables Teams bot) | '' (disabled) |
enableNetworking |
VNet + private endpoints | true in prod |
deployRoleAssignments |
Create OpenAI/Search RBAC assignments from IaC (requires Microsoft.Authorization/roleAssignments/write) |
true |
AskControllervalidates auth and input.IQueryRewriteServicerewrites the user query.IRetrievalServiceretrieves candidate chunks from Azure AI Search.IAuthorizationFilterenforces document-level security (matterId,accessGroup, claims).IPromptBuilderconstructs a grounded prompt from authorized chunks only.IChatServicegenerates a structured response.- Response includes answer + citation/source metadata; fallback is used when context is insufficient.
- Structured audit events are emitted for Azure Monitor / App Insights ingestion.
- Default auth: Entra ID JWT validation (
ApiAccessPolicy) with required delegated scope/app role. - Matter authorization: request
matterIdmust be in user permitted-matter claims. - Document authorization: retrieval results are filtered by metadata (
matterId,accessGroup) and claims (groups, identity). - Grounded fallback: if context is missing/filtered, API returns:
"Insufficient information in approved documents."
Protected diagnostics endpoint:
POST /debug/retrieval
Enable only when explicitly needed:
DebugRag:Enabled=true(or env varDEBUG_RAG=true)
Diagnostics returns compact retrieval metadata only (query, claims summary, counts, source IDs/files, matter/document metadata, prompt preview, fallback reason), and intentionally avoids full document-body leakage.
# If dotnet is missing, bootstrap first
./scripts/setup-dotnet.sh
dotnet build legal-rag-platform.sln
dotnet test legal-rag-platform.slnIf SDK installation fails behind a proxy, follow docs/DOTNET_SETUP.md (allowlist + pre-baked image guidance).
New to this? Start with docs/FIRST_STEPS_LOCAL_SETUP.md for a beginner-friendly walkthrough of the first step.
Quick automation for newcomers:
./scripts/do-next-steps.shThis runs setup, build/test (when dotnet is available), and branch cleanup dry-run (when origin is configured).
Integration tests use fake test doubles (retrieval/chat/auth identity simulation) and do not require live Azure OpenAI or Azure Search.
- Hardened production template available at
infra/main.hardened.bicep(forcesenvironment=prod, private networking, managed-identity role assignments, and diagnostics disabled by default). - Hardening review and deployment guidance:
docs/BICEP_HARDENING.md. - Key Vault is deployed with RBAC, purge protection, 90-day soft delete retention, and private endpoint support in production.
- App Service is configured to use managed identity for Azure OpenAI/Azure AI Search access (no API keys required).
- App Service uses system-assigned managed identity and RBAC assignments for Azure OpenAI/Search access.
- Infrastructure supports private networking and private endpoints in production mode.
- Storage is hardened with HTTPS-only + TLS1.2 minimum + blob public access disabled; production mode uses private endpoint access for Blob.
DebugRagis wired as an explicit IaC/app setting toggle and defaults tofalse.- Keep production and development parameters separated (
infra/params/dev.bicepparam,infra/params/prod.bicepparam).
- Entra ID group-overage handling and Graph-based group expansion.
- Per-document legal hold and retention policy enforcement.
- Signed answer provenance bundles for eDiscovery workflows.
- Policy-as-code authorization rules using externalized entitlement engine.
Use scripts/cleanup-branches.sh to identify and optionally delete remote branches that are already merged into the base branch.
# Dry-run (default)
./scripts/cleanup-branches.sh --remote origin --base main
# Delete non-protected merged branches
./scripts/cleanup-branches.sh --remote origin --base main --applyThe script intentionally skips protected branch names (for example main, master, develop, dev, and work) and supports an optional --merge mode for exceptional cases.