{"id":50984725,"url":"https://github.com/bayars/azure-foundry","last_synced_at":"2026-06-19T18:02:03.747Z","repository":{"id":342443811,"uuid":"1173978260","full_name":"bayars/azure-foundry","owner":"bayars","description":"Just a small azure foundry demo ","archived":false,"fork":false,"pushed_at":"2026-03-06T01:09:16.000Z","size":61,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-06T04:54:16.430Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bayars.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-06T00:14:23.000Z","updated_at":"2026-03-06T01:09:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bayars/azure-foundry","commit_stats":null,"previous_names":["bayars/azure-foundry"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bayars/azure-foundry","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bayars%2Fazure-foundry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bayars%2Fazure-foundry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bayars%2Fazure-foundry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bayars%2Fazure-foundry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bayars","download_url":"https://codeload.github.com/bayars/azure-foundry/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bayars%2Fazure-foundry/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34542467,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-19T18:02:03.215Z","updated_at":"2026-06-19T18:02:03.741Z","avatar_url":"https://github.com/bayars.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LangGraph Demo — Azure AI Foundry\n\nA LangGraph FastAPI service with two agentic graphs deployed to Azure Container Apps and registered as a native agent in Azure AI Foundry via the AAAS protocol.\n\n---\n\n## Contents\n\n1. [What this project is](#what-this-project-is)\n2. [Local development](#local-development)\n3. [Architecture](#architecture)\n4. [AI Foundry concepts](#ai-foundry-concepts)\n5. [Agent tool types explained](#agent-tool-types-explained)\n6. [Foundry integration options](#foundry-integration-options)\n7. [AAAS protocol — Option C implementation](#aaas-protocol--option-c-implementation)\n8. [LangGraph API reference](#langgraph-api-reference)\n9. [AAAS sessions API reference](#aaas-sessions-api-reference)\n10. [Azure deployment](#azure-deployment)\n\n---\n\n## What this project is\n\nTwo LangGraph graphs exposed as a FastAPI service:\n\n| Graph | What it does |\n|---|---|\n| `support_agent` | Customer support — clarification loop + escalation human-in-the-loop interrupt |\n| `code_review` | Code review — re-review loop + accept/reject/re_review interrupt |\n\nBoth graphs use **Azure OpenAI `gpt-4o-mini`** as the LLM backend (Ollama used locally only).\n\nThe service is registered in **Azure AI Foundry** as a native `container_app` kind agent via the AAAS (Azure AI Agent Service) protocol — meaning Foundry manages threads and tracing while LangGraph handles all execution logic.\n\n---\n\n## Local development\n\n```bash\ncp .env.example .env\n# Set OLLAMA_BASE_URL to your local Ollama instance\n\ndocker compose up\n```\n\n| Service | URL |\n|---|---|\n| LangGraph API | http://localhost:8000 |\n| API docs | http://localhost:8000/docs |\n\n---\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────┐\n│                  AZURE AI FOUNDRY                   │\n│                                                     │\n│  Agent registry   Thread storage   Traces/evals     │\n│                                                     │\n│  langgraph-demo-agent (container_app kind)          │\n│         │                                           │\n└─────────┼───────────────────────────────────────────┘\n          │  AAAS protocol (sessions/turns)\n          │  POST /sessions/{id}/turns\n          │  GET  /sessions/{id}/turns/{turn_id}\n          ▼\n┌─────────────────────────────────────────────────────┐\n│          AZURE CONTAINER APPS (cae-langgraph)       │\n│                                                     │\n│  langgraph-api  (external, port 8000)               │\n│  ├── /runs/*          → LangGraph run management    │\n│  └── /sessions/*      → AAAS protocol endpoints     │\n│                                                     │\n│  redis  (internal, port 6379)                       │\n│  └── future persistent checkpointer                 │\n└─────────────────────────────────────────────────────┘\n          │\n          │  Azure OpenAI (gpt-4o-mini)\n          ▼\n┌─────────────────────────────────────────────────────┐\n│  AI Hub: safabayar   Model: gpt-4o-mini             │\n│  https://safabayar.cognitiveservices.azure.com/     │\n└─────────────────────────────────────────────────────┘\n```\n\n---\n\n## AI Foundry concepts\n\n### What Foundry provides\n\nFoundry is a **platform layer** that sits in front of your agents. It does not run your agent code — your Container App does that.\n\n| Foundry feature | What it does |\n|---|---|\n| **Agent Registry** | Catalog of all agents — name, URL, version, metadata |\n| **Thread management** | Owns conversation history, routes messages to the right agent |\n| **Multi-agent orchestration** | Agent A can call Agent B as a sub-agent |\n| **Portal playground** | Test any registered agent from the UI without writing code |\n| **Tracing \u0026 evaluation** | Every turn logged — latency, token cost, quality scores |\n| **Unified auth** | All agents secured via Azure AD, no individual API keys per agent |\n| **Versioning** | Roll back an agent to a previous version at any time |\n\n### Foundry does not care what is inside your agent\n\nWhether your agent contains:\n- A GPT-4o wrapper\n- A LangGraph state machine with an LLM\n- A LangGraph graph with **no LLM** (pure rules/deterministic)\n- A legacy REST API\n\n...Foundry treats them identically. It sends a message, expects a response. The internals are invisible to Foundry.\n\n### Where the workflow lives\n\n```\n┌──────────────────────────────────┐\n│         AZURE AI FOUNDRY         │\n│                                  │\n│  Stores:                         │\n│  - Agent registration (URL)      │\n│  - Conversation threads          │\n│  - Traces                        │\n└──────────────┬───────────────────┘\n               │  sends user message\n               ▼\n┌──────────────────────────────────┐\n│       YOUR CONTAINER APP         │\n│                                  │\n│  Runs:                           │\n│  - LangGraph graphs              │  ← workflow lives here\n│  - LLM calls                     │\n│  - State machine logic           │\n│  - Business rules                │\n└──────────────────────────────────┘\n```\n\nThe workflow is **not** in Foundry. Foundry is the front door and bus. Your container is the brain.\n\n### Multi-agent orchestration\n\nOnce multiple agents are registered, Foundry routes between them:\n\n```\nUser: \"I have a bug causing customer complaints\"\n               │\n               ▼\n      FOUNDRY ORCHESTRATOR\n               │\n        ┌──────┴──────┐\n        ▼             ▼\n  code_review    support_agent      ← both your LangGraph containers\n     agent          agent\n        │             │\n        └──────┬───────┘\n               ▼\n        combined response\n```\n\nThe orchestration — which agent runs, what gets passed between them — is defined either in Foundry's workflow system or in a dedicated orchestrator agent.\n\n---\n\n## Agent tool types explained\n\n### Why function tools don't exist in the GUI\n\nA `function` tool in the Agents API is **not server-side execution**. It is a contract between the LLM and your client application:\n\n```\nAgent LLM → decides to call invoke_langgraph\n    → returns tool_call JSON to YOUR code\n        → YOUR code calls the LangGraph API\n            → YOUR code sends result back to agent\n```\n\nThe portal has no way to configure this because **your application is the executor**, not Azure. The GUI only shows tools that Azure executes entirely on its side:\n\n| Tool | Who executes |\n|---|---|\n| Web search | Azure (Bing) |\n| Code interpreter | Azure (sandboxed container) |\n| Azure AI Search | Azure (your index) |\n| **Function** | **Your application code** |\n\nTo use a `function` tool, your client must run a polling loop:\n\n```python\nfrom azure.ai.projects import AIProjectClient\nfrom azure.identity import DefaultAzureCredential\nimport httpx, json, time\n\nclient = AIProjectClient(\n    endpoint=\"https://safabayar.services.ai.azure.com/api/projects/proj-default\",\n    credential=DefaultAzureCredential(),\n    api_version=\"2025-05-15-preview\",\n)\n\nLANGGRAPH_URL = \"https://langgraph-api.ambitiousglacier-23b2e299.eastus2.azurecontainerapps.io\"\n\ndef handle_tool_call(name: str, arguments: dict) -\u003e str:\n    if name == \"invoke_langgraph\":\n        r = httpx.post(f\"{LANGGRAPH_URL}/runs\", json={\n            \"graph_id\": arguments[\"graph_name\"],\n            \"input\": arguments[\"input\"],\n        })\n        return json.dumps(r.json())\n    return \"{}\"\n\nthread = client.agents.create_thread()\nclient.agents.create_message(thread.id, role=\"user\",\n    content=\"Review my Python code: def add(a,b): return a+b\")\n\nrun = client.agents.create_run(thread_id=thread.id, agent_id=\"langgraph-demo-agent\")\n\nwhile run.status in (\"queued\", \"in_progress\", \"requires_action\"):\n    time.sleep(1)\n    run = client.agents.get_run(thread_id=thread.id, run_id=run.id)\n\n    if run.status == \"requires_action\":\n        tool_outputs = []\n        for tc in run.required_action.submit_tool_outputs.tool_calls:\n            result = handle_tool_call(tc.function.name, json.loads(tc.function.arguments))\n            tool_outputs.append({\"tool_call_id\": tc.id, \"output\": result})\n        run = client.agents.submit_tool_outputs_to_run(\n            thread_id=thread.id, run_id=run.id, tool_outputs=tool_outputs\n        )\n```\n\n### OpenAPI tool — GUI visible, no client loop\n\nThe `openapi` tool type is visible in the Foundry portal GUI and lets Foundry call the API directly. No client-side execution loop needed. The agent handles `start_run → poll state → resume on interrupt` autonomously.\n\n| | Function tool | OpenAPI tool | AAAS (container_app) |\n|---|---|---|---|\n| GUI visible | No | Yes | Yes (as a full agent) |\n| Execution | Client code | Foundry → your API | Foundry → your API |\n| Extra LLM layer | Yes (orchestrator) | Yes (orchestrator) | **No** |\n| Operations exposed | 1 combined | 3 separate | Protocol-native |\n| Thread management | Client | Client | **Foundry** |\n| Tracing | None | None | **Full Foundry traces** |\n\n---\n\n## Foundry integration options\n\n### Option A — Foundry agent wraps LangGraph via OpenAPI tool (current v2)\n\n```\nFoundry agent (gpt-4o-mini) → decides to call OpenAPI tool → LangGraph API (gpt-4o-mini)\n```\n\n- Two LLM layers — redundant for a single-purpose agent\n- No native Foundry thread ownership or tracing\n- Good if you need Foundry to choose between multiple tools\n\n### Option B — Connection only, call LangGraph directly\n\nRegister the URL as a Foundry connection. Visible in Management → Connections. No Foundry features (no threads, tracing, portal playground). Call the LangGraph API directly.\n\n### Option C — container_app kind agent (recommended)\n\n```\nFoundry (manages threads + traces) → AAAS protocol → LangGraph API (brain)\n```\n\n- Single LLM layer — LangGraph does all the work\n- Foundry owns threads: history stored and visible in portal\n- Full tracing per turn\n- Portal playground works natively\n- Foundry can orchestrate this alongside other agents\n- Requires implementing 4 AAAS protocol endpoints (see below)\n\n---\n\n## AAAS protocol — Option C implementation\n\n### What the protocol is\n\nAAAS is a small REST contract. Foundry calls these endpoints on your Container App:\n\n```\nPOST   /sessions                          → Foundry creates a conversation session\nPOST   /sessions/{id}/turns               → Foundry sends a user message; your agent runs\nGET    /sessions/{id}/turns/{turn_id}     → Foundry polls for the result (async)\nDELETE /sessions/{id}                     → Foundry ends the session\n```\n\nThat is the entire protocol. Your LangGraph API maps them like this:\n\n```\nPOST /sessions              → store session with graph_id + new thread_id\nPOST /sessions/{id}/turns   → start (or resume) a LangGraph run; return run_id as turn_id\nGET  /sessions/{id}/turns/{turn_id}  → check run status; map to AAAS turn status\nDELETE /sessions/{id}       → clean up session record\n```\n\n### How LangGraph interrupts map to AAAS turns\n\nLangGraph human-in-the-loop interrupts become a two-turn exchange in AAAS:\n\n```\nTurn 1:  user sends message\n         → LangGraph runs, hits interrupt (e.g. escalation approval needed)\n         → turn completes with status=\"completed\", output = the interrupt question\n\nTurn 2:  user sends answer (true/false or text)\n         → session detects pending interrupt, resumes the run\n         → turn completes with status=\"completed\", output = final result\n```\n\n### Register as container_app kind\n\nAfter implementing the protocol endpoints:\n\n```python\nimport json, subprocess, urllib.request\n\nCA_RESOURCE_ID = (\n    \"/subscriptions/5ec3a6f9-978c-4e02-9d96-135dbc85269e\"\n    \"/resourceGroups/rg-bayarsafa-7080\"\n    \"/providers/Microsoft.App/containerapps/langgraph-api\"\n)\n\npayload = {\n    \"name\": \"langgraph-demo-agent\",\n    \"description\": \"LangGraph demo — support_agent and code_review graphs\",\n    \"definition\": {\n        \"kind\": \"container_app\",\n        \"container_app_resource_id\": CA_RESOURCE_ID,\n        \"container_protocol_versions\": [\n            {\"protocol\": \"AzureAIAgentService\", \"version\": \"1.0\"}\n        ],\n    }\n}\n\ntoken = subprocess.check_output(\n    [\"az\", \"account\", \"get-access-token\", \"--resource\", \"https://ai.azure.com/\",\n     \"--query\", \"accessToken\", \"-o\", \"tsv\"], text=True\n).strip()\n\nbody = json.dumps(payload).encode()\nreq = urllib.request.Request(\n    \"https://safabayar.services.ai.azure.com/api/projects/proj-default\"\n    \"/agents/langgraph-demo-agent/versions?api-version=2025-05-15-preview\",\n    data=body, method=\"POST\",\n    headers={\"Authorization\": f\"Bearer {token}\", \"Content-Type\": \"application/json\"}\n)\nwith urllib.request.urlopen(req) as resp:\n    print(json.dumps(json.loads(resp.read()), indent=2))\n```\n\n### Add via GUI (after implementing the protocol)\n\n1. https://ai.azure.com → hub `safabayar` → project `proj-default` → **Agents**\n2. Open `langgraph-demo-agent` → **Edit**\n3. Change type to **Container App**\n4. Select the `langgraph-api` Container App from the dropdown\n5. Save\n\n---\n\n## LangGraph API reference\n\nBase URL: `https://langgraph-api.ambitiousglacier-23b2e299.eastus2.azurecontainerapps.io`\n\n### `GET /health`\n\n```json\n{\"status\": \"ok\", \"graphs\": [\"support_agent\", \"code_review\"]}\n```\n\n### `POST /runs` — start a run\n\n**support_agent input:**\n```json\n{\n  \"graph_id\": \"support_agent\",\n  \"input\": {\n    \"user_name\": \"Alice\",\n    \"message\": \"My login is broken\"\n  }\n}\n```\n\n**code_review input:**\n```json\n{\n  \"graph_id\": \"code_review\",\n  \"input\": {\n    \"code_snippet\": \"def add(a, b):\\n    return a + b\",\n    \"language\": \"python\"\n  }\n}\n```\n\nResponse:\n```json\n{\"run_id\": \"...\", \"thread_id\": \"...\", \"graph_id\": \"...\", \"status\": \"running\"}\n```\n\n### `GET /runs/{run_id}/state` — poll status\n\n```json\n{\n  \"run_id\": \"...\",\n  \"status\": \"interrupted\",\n  \"interrupt_payload\": {\n    \"type\": \"escalation_approval\",\n    \"severity\": \"high\",\n    \"summary\": \"User Alice has a HIGH severity issue...\"\n  }\n}\n```\n\nStatus values: `running` | `interrupted` | `complete` | `error`\n\n### `POST /runs/{run_id}/resume` — resume after interrupt\n\n```json\n{\"resume_value\": true}\n```\n\n| Interrupt type | resume_value |\n|---|---|\n| `clarification_needed` | `\"\u003canswer text\u003e\"` |\n| `escalation_approval` | `true` or `false` |\n| `review_decision` | `\"accept\"`, `\"reject\"`, or `\"re_review\"` |\n\n### `GET /runs/{run_id}/stream` — SSE stream\n\nEvents: `token`, `node_update`, `interrupted`, `complete`, `error`, `heartbeat`\n\n### `POST /runs/{run_id}/feedback`\n\n```json\n{\"score\": 0.9, \"comment\": \"Very helpful\", \"key\": \"user_feedback\"}\n```\n\n---\n\n## AAAS sessions API reference\n\nBase URL: `https://langgraph-api.ambitiousglacier-23b2e299.eastus2.azurecontainerapps.io`\n\nThese endpoints implement the Azure AI Agent Service protocol for Foundry integration.\n\n### `POST /sessions` — create session\n\n```json\n{\n  \"graph_id\": \"support_agent\",\n  \"metadata\": {\"user_name\": \"Alice\"}\n}\n```\n\nResponse:\n```json\n{\"id\": \"\u003csession_id\u003e\", \"graph_id\": \"support_agent\", \"status\": \"created\"}\n```\n\n### `POST /sessions/{session_id}/turns` — send a message\n\n```json\n{\n  \"input\": [{\"role\": \"user\", \"content\": \"My login is broken\"}]\n}\n```\n\nIf the session has a pending interrupt (from a previous turn), the content is used as the `resume_value` automatically.\n\nResponse:\n```json\n{\"id\": \"\u003cturn_id\u003e\", \"session_id\": \"\u003csession_id\u003e\", \"status\": \"in_progress\"}\n```\n\n### `GET /sessions/{session_id}/turns/{turn_id}` — poll turn result\n\nResponse when in progress:\n```json\n{\"id\": \"\u003cturn_id\u003e\", \"status\": \"in_progress\"}\n```\n\nResponse when interrupted (turn completes, interrupt becomes the agent's reply):\n```json\n{\n  \"id\": \"\u003cturn_id\u003e\",\n  \"status\": \"completed\",\n  \"output\": {\n    \"messages\": [\n      {\n        \"role\": \"assistant\",\n        \"content\": \"Do you approve escalation to the on-call team? (true/false)\"\n      }\n    ]\n  },\n  \"interrupt_type\": \"escalation_approval\"\n}\n```\n\nResponse when complete:\n```json\n{\n  \"id\": \"\u003cturn_id\u003e\",\n  \"status\": \"completed\",\n  \"output\": {\n    \"messages\": [\n      {\"role\": \"assistant\", \"content\": \"Ticket TKT-A1B2C3D4 created. Severity: high.\"}\n    ]\n  }\n}\n```\n\nResponse on error:\n```json\n{\"id\": \"\u003cturn_id\u003e\", \"status\": \"failed\", \"error\": \"...\"}\n```\n\n### `DELETE /sessions/{session_id}` — end session\n\n```json\n{\"deleted\": true}\n```\n\n---\n\n## Azure deployment\n\n### Resources\n\n| Resource | Name | Location |\n|---|---|---|\n| Resource Group | `rg-bayarsafa-7080` | eastus2 |\n| ACR | `safademo.azurecr.io` | eastus2 |\n| Container Apps Env | `cae-langgraph` | eastus2 |\n| Container App | `langgraph-api` | eastus2 |\n| Container App | `redis` (internal) | eastus2 |\n| AI Hub | `safabayar` | eastus2 |\n| AI Project | `proj-default` | — |\n| Model | `gpt-4o-mini` GlobalStandard 10K TPM | eastus2 |\n| AI Agent | `langgraph-demo-agent` kind: container_app | — |\n\n### Key endpoints\n\n| Service | URL |\n|---|---|\n| LangGraph API | `https://langgraph-api.ambitiousglacier-23b2e299.eastus2.azurecontainerapps.io` |\n| AI Foundry Portal | https://ai.azure.com → `safabayar` → `proj-default` → Agents |\n| AI Foundry Agent | `https://safabayar.services.ai.azure.com/api/projects/proj-default/agents/langgraph-demo-agent` |\n\n### Image update\n\n```bash\naz acr login --name safademo\ndocker build -t safademo.azurecr.io/langgraph-api:latest ./langgraph-api\ndocker push safademo.azurecr.io/langgraph-api:latest\naz containerapp update \\\n  --name langgraph-api \\\n  --resource-group rg-bayarsafa-7080 \\\n  --image safademo.azurecr.io/langgraph-api:latest\n```\n\n\u003e **Note:** `az acr build` (ACR Tasks) is disabled on this subscription. Always build locally.\n\n### Full deployment guide\n\nSee [`docs/azure-deployment.md`](docs/azure-deployment.md) for the complete step-by-step guide including all `az` commands.\n\n### Known limitations\n\n**State is in-memory.** `MemorySaver` loses all run state on container restart. Redis is deployed internally but not yet wired in. To enable persistence replace `MemorySaver` with `AsyncRedisSaver` and set:\n```\nREDIS_URL=redis://redis.internal.ambitiousglacier-23b2e299.eastus2.azurecontainerapps.io:6379\n```\n\n**Single replica only** until Redis checkpointer is integrated — multiple replicas cannot share in-memory state.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbayars%2Fazure-foundry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbayars%2Fazure-foundry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbayars%2Fazure-foundry/lists"}