Skip to content

passadis/foundry-ticketing

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Event-Driven AI Service Desk with Microsoft Foundry Agent and Azure Integration

Microsoft Foundry Agent Service Azure Container Apps MIT License

HelpDesk Copilot is a realistic, production-shaped IT service desk for a fictional company, Contoso. An employee describes an IT problem in a browser chat; a Microsoft Foundry agent searches curated IT documentation for a grounded, cited answer, or — when the issue genuinely needs a human — creates a ticket that flows asynchronously to a worker, gets persisted, and shows up as a real Adaptive Card in a Microsoft Teams channel. Every piece runs on Azure Container Apps, provisioned entirely by Terraform, with zero API keys: every service-to-service call authenticates with a user-assigned managed identity.

This isn't a chatbot demo wired to a single script. It's built, deployed, and debugged against real Azure infrastructure — including the RBAC gap and the reverse-proxy quirk documented in Troubleshooting below, both only found by actually driving traffic through the live deployment.

✨ Why this solution stands out

  • 🔍 Grounded, cited answers — one Foundry Prompt Agent with a File Search tool over ten synthetic Contoso IT docs. No hallucinated policy; every self-service answer names its source file.
  • 🎫 Real escalation, not a toy — the agent decides when a documented procedure requires a ticket (e.g., restricted-data access, lost devices) and calls a validated, idempotent create_ticket function tool.
  • 📨 A real human sees it — no simulated ITSM status-cycling. The ticket worker posts a real Adaptive Card to a Microsoft Teams channel via a Power Automate flow.
  • 🔑 Zero API keys anywhere — Foundry, Key Vault, Service Bus, Storage, and ACR are all reached through DefaultAzureCredential and per-service user-assigned managed identities. The only unavoidable secret (a Teams webhook URL) lives in Key Vault, not in code or env files.
  • 🪫 Least privilege, verified live — the built-in Foundry Agent Consumer role turned out not to cover conversations.create(); rather than over-grant Foundry User, this solution defines a custom Foundry role with exactly the three data actions the API actually calls.
  • ⚡ Dapr-native, no connection strings — the API publishes and the worker subscribes to ticket-events through an environment-level Dapr pub/sub component. Local dev without a sidecar falls back to the Service Bus SDK automatically.
  • 📉 Scale-to-zero by design — frontend and API scale 0→3 on HTTP load; the worker scales 0→5 on a KEDA Service Bus queue-depth rule. Idle cost approaches zero.
  • 🏗️ Terraform only, no clickops — every resource, role assignment, and Dapr component definition is in infra/. azurerm where it's covered, azapi/custom role definitions where it isn't (and it's commented explaining why).

Architecture

Architecture

Frontend, API, and ticket-worker are three Container Apps in one ACA environment. The frontend is the only one with external ingress; the API is internal-only; the worker has no ingress at all — it's woken purely by Service Bus backlog via KEDA. See docs/architecture.md for the full ADR-style write-up (what's actually GA in Foundry Agent Service today, why one Prompt Agent instead of Connected Agents/Workflows, provider-coverage decisions) and solution/solution-overview.md for a deep, sequence-diagram-level walkthrough of every flow.

Knowledge Q&A flow (click to expand)

Knowledge answer flow

Escalation & ticket flow (click to expand)

Ticket flow

Prerequisites

  1. An Azure subscription
  2. Terraform ≥ 1.9
  3. Azure CLI, logged in (az login) with az account set --subscription <id>
  4. Python 3.12 (for scripts/seed_knowledge.py and scripts/evaluate_agent.py)
  5. No local Docker required — images are built in the cloud with az acr build

Deploy

1. Provision the infrastructure

cd infra
cp terraform.tfvars.example terraform.tfvars
# edit terraform.tfvars: deployer_principal_id (az ad signed-in-user show --query id -o tsv),
# location, and optionally teams_webhook_url once you have a Power Automate flow (see below)

terraform init
terraform plan -out=tfplan.out
terraform apply tfplan.out

This provisions the resource group, ACR, Key Vault, Service Bus, Storage, the Foundry account/project/model deployment, the custom least-privilege Foundry role, and Log Analytics/App Insights — but not the three Container Apps yet, since they need images to exist first.

2. Build and push the three images

REGISTRY=$(terraform output -raw container_registry_login_server)

az acr build --registry "$REGISTRY" --image api:latest ../src/api
az acr build --registry "$REGISTRY" --image ticket-worker:latest ../src/ticket-worker
az acr build --registry "$REGISTRY" --image frontend:latest ../src/frontend

3. Deploy the Container Apps

Uncomment/apply the aca module in infra/main.tf (present in this repo) and re-run:

terraform apply

4. Seed the knowledge base and register the agent

cd ..
python -m venv .venv && source .venv/bin/activate   # or .venv\Scripts\activate on Windows
pip install -r src/api/requirements.txt python-dotenv
PROJECT_ENDPOINT=$(terraform -chdir=infra output -raw foundry_project_endpoint) python scripts/seed_knowledge.py

This uploads data/it-docs/*.md into a File Search vector store and registers the helpdesk-agent Prompt Agent version with the File Search tool plus the create_ticket/get_ticket_status function tools.

5. Open the app

terraform -chdir=infra output frontend_url

Optional: wire up the Teams notification

The ticket-worker posts a plain JSON ticket payload to a Power Automate HTTP-triggered flow, which renders the Adaptive Card — so restyling the card is a flow edit, never a redeploy. See docs/architecture.md §7 for the exact trigger schema and card template to paste in. Without it configured, ticket creation and persistence still work end-to-end; the worker just logs a warning and skips the notification.

Demo script

  1. Ask a covered question — "My VPN keeps dropping every hour" — and show the streamed, grounded answer with its source-document citation.
  2. Ask something that needs escalation — "I need access to the finance shared drive for an audit" — and show the agent calling create_ticket and returning a ticket ID mid-conversation.
  3. Point at the ticket side panel: the new ticket appears within its next 5-second poll, status New.
  4. Switch to Microsoft Teams and show the Adaptive Card landing in the channel in real time.
  5. Open Application Insights: show the agent.invoke custom span with conversation ID, selected tools, and token counts for both requests.

API surface

Endpoint Purpose
POST /chat Starts or continues a Foundry conversation; streams SSE events (conversation, delta, tool_call, citation, error, done).
GET /tickets?conversation_id=... Lists tickets in a conversation, used by the UI's polling ticket panel.
GET /tickets/{ticket_id} Looks up one ticket by ID.
GET /healthz Liveness/readiness probe target.
GET /dapr/subscribe / POST /events/ticket-created Dapr pub/sub contract between the API and the ticket worker.

Configuration

Key infra/terraform.tfvars values:

Variable Purpose
prefix, environment, location Naming and region for every resource.
deployer_principal_id Your Entra object ID — granted Foundry Project Manager so you can run seed_knowledge.py.
teams_webhook_url Optional Power Automate trigger URL; left null stores a harmless placeholder.
foundry_model_name / _version / _sku_name / _capacity Model deployment; GlobalStandard bills per-token, so capacity only sets a throttling ceiling, not idle cost.

Application configuration is entirely environment variables set by Terraform in the ACA module — see src/api/.env.example and src/ticket-worker for the equivalents used in local development.

Security & identity

Every Azure call uses DefaultAzureCredential. In ACA, AZURE_CLIENT_ID pins each app to its own user-assigned managed identity:

Identity Access Notes
API UAMI AcrPull, custom Foundry agent-runtime role, Storage Table Data Reader, Key Vault Secrets User, Service Bus Sender The custom role grants exactly endpoints/interact/action + agents/read + agents/write — the built-in Foundry Agent Consumer role is missing the last one, which conversations.create() needs.
Worker UAMI AcrPull, Storage Table Data Contributor, Key Vault Secrets User, Service Bus Receiver Sole writer of the tickets table.
Frontend UAMI AcrPull only Serves static assets and proxies to the API via the Dapr sidecar — never touches Azure data planes directly.
Shared Dapr UAMI Service Bus Data Owner Scoped narrowly to the Dapr pub/sub component and the KEDA scaler, since those need one shared identity across the Sender-only/Receiver-only app identities.

The Foundry account has local_auth_enabled = false — key-based auth is rejected outright. The only unavoidable secret, the Teams webhook URL, lives in Key Vault.

Scope note: the app does not implement browser authentication or employee identity binding. Conversation IDs partition the UI's ticket list but aren't an authorization boundary — add real auth before exposing this beyond a demo.

Observability

  • ACA diagnostic settings stream platform logs/metrics to a 30-day Log Analytics workspace.
  • The API initializes Azure Monitor OpenTelemetry; each Foundry call gets a custom agent.invoke span with conversation ID, selected tools, and token counts (when the SDK exposes them).
  • scripts/evaluate_agent.py runs ten fixed questions through Foundry's cloud evaluation API, scoring intent resolution, coherence, and task adherence — small and deliberately not gold-plated into a CI gate.

Scaling

App Range Trigger
Frontend 0 → 3 HTTP requests
API 0 → 3 HTTP requests
Ticket worker 0 → 5 KEDA Azure Service Bus rule, target 5 messages on the ticket-worker subscription

Cost (Sweden Central, low demo traffic)

Resource SKU / tier Approx. monthly idle cost
Container Apps (×3) Consumption workload profile ~$0 (scale to zero)
Foundry model deployment gpt-4o-mini, GlobalStandard Pay-per-token only, no idle cost
Service Bus Standard namespace ~$10 base
Storage (Table) Standard LRS Pennies at this volume
Key Vault Standard Pennies (per-operation)
Log Analytics + App Insights Pay-as-you-go, 30-day retention A few dollars at low log volume
Container Registry Basic ~$5

Total idle cost is roughly the Service Bus namespace plus ACR — a few dollars a month. Traffic-driven costs (model tokens, ACA vCPU-seconds) scale with actual usage. Run terraform destroy when you're done to zero it out entirely.

Troubleshooting

Two real issues were found by driving traffic through the live deployment, not by reading docs — worth knowing if you hit them too:

  • 426 Upgrade Required from the frontend to the API. nginx's proxy_pass defaults to HTTP/1.0, and ACA's internal ingress requires HTTP/1.1+. Rather than chase proxy_http_version/SNI/FQDN issues through nginx, this solution proxies through the frontend's own Dapr sidecar instead (http://localhost:3500/v1.0/invoke/api/method/...) — same-environment service invocation, no FQDN or TLS plumbing needed at all.
  • 403 PermissionDenied on conversations.create() even with the built-in Foundry Agent Consumer role assigned. That role only grants endpoints/interact/action; creating a conversation needs agents/write too. Fixed with the custom role described in Security & identity above, rather than over-granting Foundry User.

Cleanup

cd infra
terraform destroy

License

MIT — see LICENSE. All sample data (Contoso IT docs, tickets) is synthetic.


Made with ❤️ by Konstantinos Passadis

About

HelpDesk Copilot: An Event-Driven AI Service Desk with Microsoft Foundry Agents, Azure Container Apps, Dapr — and Zero API Keys

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages