From 6be3f78123e2548ad179547d64db54cd5dea26ff Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:22:16 +0800 Subject: [PATCH] Add native LangGraph user-identity toolbox sample (#969) * Add native LangGraph user identity agent Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Document native user identity toolbox sample Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Format native LangGraph sample Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Address hosted sample review feedback Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Regenerate LangGraph sample requirements Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Layer LangGraph connection deployment Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Remove root layered provider Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Remove GitHub MCP integration Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Update AgentServer Responses dependency Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> * Restore GitHub MCP integration Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ankitbko <3169316+ankitbko@users.noreply.github.com> --- CONTRIBUTING.md | 28 ++- README.md | 3 +- .../requirements.txt | 2 +- .../AGENTS.md | 40 ++++ .../CLAUDE.md | 5 + .../README.md | 159 ++++++++++++++ .../azure.yaml | 80 +++++++ .../infra/connections/main.bicep | 49 +++++ .../infra/connections/main.parameters.json | 9 + .../infra/foundry/README.md | 4 + .../.azdignore | 1 + .../.dockerignore | 26 +++ .../.env.example | 9 + .../Dockerfile | 14 ++ .../toolbox-langgraph-user-identity/main.py | 200 ++++++++++++++++++ .../requirements.in | 16 ++ .../requirements.txt | 132 ++++++++++++ 17 files changed, 760 insertions(+), 17 deletions(-) create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/AGENTS.md create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/CLAUDE.md create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/README.md create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/azure.yaml create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.bicep create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.parameters.json create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/foundry/README.md create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.azdignore create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.dockerignore create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.env.example create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/Dockerfile create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/main.py create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.in create mode 100644 samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c48f895de..e320f1aaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,29 @@ # Contributing to Microsoft Foundry Samples -This repository contains official Microsoft Foundry documentation samples. Changes are submitted as pull requests directly to this repository. +This repository contains official Microsoft Foundry documentation samples. The contents are published automatically from a private staging repository and are not edited directly here. ## Reporting Issues If you find a bug, have a question, or want to suggest an improvement to an existing sample, please [open an issue](https://github.com/microsoft-foundry/foundry-samples/issues/new) on this repository. We welcome feedback from everyone! -Before starting a substantial change, check for an existing issue. Open one when discussion or design agreement would help avoid duplicate work. +## Contributing Changes (Microsoft Contributors) -## Contributing Changes +Sample contributions are currently limited to Microsoft Foundry teams. -Sample contributions are currently limited to Microsoft Foundry teams with permission to create a branch in this repository. Fork pull requests cannot satisfy the intentionally failing `trusted` gate, so sample changes must use a same-repository branch. +All changes — new samples, updates, and bug fixes — are submitted through the private staging repository [`foundry-samples-pr`](https://github.com/microsoft-foundry/foundry-samples-pr). Changes merged there are automatically synced to this public repository on a nightly basis. -Contributors should always submit publishable changes through a public same-repository pull request. Maintainers may independently operate a separate bridge for approved eligible content already in the private repository; contributors must not dispatch it. +> [!NOTE] +> The link above will return a **404** until you've completed step 1 below. -1. **Create a branch in this repository.** Use a same-repository branch for all sample changes. -2. **Make a focused change.** Keep each pull request scoped to one sample, fix, or related set of updates. Follow the conventions in the surrounding sample. -3. **Respect file ownership.** Review [CODEOWNERS](.github/CODEOWNERS) before editing. The listed owners will be requested when their files are changed. -4. **Validate locally.** Run the setup, build, test, or sample-specific validation documented by the affected sample. Never commit credentials, local environment files, or generated secrets. -5. **Open a pull request against `main`.** In the pull request description, explain what changed, why it changed, and the local validation you ran. Link the relevant issue when one exists. +### How to get started -### Pull request checks +1. **Join the `microsoft-foundry` GitHub organization.** Navigate to the organization page on the Open Source Management Portal and click **Join**: -Pull requests run repository validation automatically: + -- The required `trusted` check must pass. -- Review and address the other checks reported on the pull request. -- Contributor pull requests are not merged automatically; after required checks and review, a maintainer triggers the merge. +2. **Access the staging repository.** Once you've joined the org, you'll be able to view [`foundry-samples-pr`](https://github.com/microsoft-foundry/foundry-samples-pr). + +3. **Follow the contributing guide there.** The `foundry-samples-pr` repository has its own [`CONTRIBUTING.md`](https://github.com/microsoft-foundry/foundry-samples-pr/blob/main/CONTRIBUTING.md) with full instructions for setting up write access, creating a branch, and submitting a pull request. ## Contributor License Agreement @@ -35,3 +32,4 @@ This project requires a Contributor License Agreement (CLA). When you submit a p ## Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com). + diff --git a/README.md b/README.md index a6d5db521..3ca3252ff 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,5 @@ Use the samples in this repository to try out Microsoft Foundry scenarios on you Found a bug or have a suggestion? [Open an issue](https://github.com/microsoft-foundry/foundry-samples/issues/new) — we welcome feedback from everyone! -Microsoft contributors with permission to create a branch in this repository can contribute a sample or fix by opening a pull request directly against `main`. Pull requests must pass the required `trusted` check and are merged by a maintainer. See the [contributing guidelines](CONTRIBUTING.md) for setup, validation, and review details. +Sample contributions are submitted through a private staging repository. If you're a Microsoft employee or contractor, see the [contributing guidelines](CONTRIBUTING.md) for how to get started. + diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt index 69ab10e45..e1deb8690 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --no-annotate --output-file=requirements.txt requirements.in diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/AGENTS.md b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/AGENTS.md new file mode 100644 index 000000000..34c60898b --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/AGENTS.md @@ -0,0 +1,40 @@ +# Coding Agent Instructions + +This project is a **Microsoft Foundry hosted agent** — a containerized AI agent that runs in [Foundry Agent Service](https://learn.microsoft.com/en-us/azure/foundry/agents/concepts/hosted-agents). The platform handles containerization, hosting, security, scaling, and observability so you can focus on agent logic. + +## Key files + +- `Dockerfile` — container definition + +## Development workflow + +The **Azure Developer CLI (`azd`)** manages the full lifecycle: + +```bash +azd ai agent run # Run locally on http://localhost:8088 +azd ai agent invoke --local "your message" # Test the local agent +azd deploy # Deploy to Foundry +azd ai agent invoke "your message" # Invoke the deployed agent +``` + +## Microsoft Foundry Skill + +Install the **Microsoft Foundry Skill** for guided deployment, evaluation, and troubleshooting workflows. + +Direct install (preferred, works with any coding agent): + +```bash +npx skills add https://github.com/microsoft/azure-skills --skill microsoft-foundry +``` + +Or install the Azure Skills Plugin: + +- **Copilot CLI**: `/plugin marketplace add microsoft/azure-skills` then `/plugin install azure@azure-skills` +- **Claude Code**: `/plugin install azure@claude-plugins-official` + +Then ask naturally, e.g. `Use the Microsoft Foundry Skill to deploy this agent.` + +## References + +- [Hosted agents overview](https://learn.microsoft.com/en-us/azure/foundry/agents/concepts/hosted-agents) +- [Microsoft Foundry Skill](https://learn.microsoft.com/en-us/azure/foundry/how-to/develop/use-microsoft-foundry-skill) diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/CLAUDE.md b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/CLAUDE.md new file mode 100644 index 000000000..f96352fea --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +This project uses [AGENTS.md](./AGENTS.md) as the single source of truth for coding agent instructions. The import below loads it into Claude Code's context. + +@AGENTS.md diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/README.md b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/README.md new file mode 100644 index 000000000..d4ad0a9bf --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/README.md @@ -0,0 +1,159 @@ + +**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). + +Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. + +Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. + +Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. + + +# LangGraph Toolbox User Identity Agent (Responses) + +This sample hosts a LangGraph ReAct agent on Microsoft Foundry over the +Responses protocol using +[`langchain_azure_ai.agents.hosting.ResponsesHostServer`](https://github.com/langchain-ai/langchain-azure/tree/main/libs/azure-ai/langchain_azure_ai/agents/hosting). +It loads tools from a Foundry Toolbox through +`langchain_azure_ai.tools.AzureAIProjectToolbox`. + +The layered deployment provisions three MCP integrations: + +- WorkIQ Mail with `UserEntraToken` +- WorkIQ Calendar with `UserEntraToken` +- GitHub MCP with managed OAuth2 + +The toolbox is named `langgraph-toolbox-user-identity-tools` so it does not +collide with shared toolboxes in the Foundry project. + +## How it works + +1. The Foundry infrastructure layer provisions the project and model. +2. The dependent Bicep layer provisions the connections before the toolbox and + hosted agent are deployed. +3. `ResponsesHostServer` exposes the OpenAI-compatible `/responses` endpoint + and manages Responses streaming and conversation history. +4. On the first request, `AzureAIProjectToolbox` resolves `TOOLBOX_NAME` and + loads the toolbox tools as LangChain tools. +5. `langchain.agents.create_agent` builds the LangGraph ReAct agent. +6. If a connection requires consent, the tool error handler recognizes MCP + error `-32006` and returns the validated consent URL to the caller. + +Tool loading is lazy so the hosted agent can pass readiness checks before an +upstream MCP server finishes warming up. + +## Prerequisites + +- Python 3.12+ +- Azure Developer CLI (`azd`) 1.25 or later +- The Microsoft Foundry `azd` extension +- An Azure subscription where you can create Foundry resources + +Install the extension and sign in: + +```bash +azd ext install microsoft.foundry +azd auth login +``` + +## Initialize and deploy + +Create a directory and initialize it from this sample: + +```bash +mkdir langgraph-toolbox-user-identity +cd langgraph-toolbox-user-identity +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/azure.yaml +``` + +Provision the Foundry layer, followed by the dependent +[connections Bicep layer](infra/connections/main.bicep): + +```bash +azd provision +``` + +Deploy the hosted agent: + +```bash +azd deploy +``` + +Invoke it: + +```bash +azd ai agent invoke "Summarize my upcoming calendar events." +``` + +## Run locally + +From `src/toolbox-langgraph-user-identity`, create a local environment: + +```bash +cp .env.example .env +python -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements.txt +``` + +Set `FOUNDRY_PROJECT_ENDPOINT`, `AZURE_AI_MODEL_DEPLOYMENT_NAME`, and +`TOOLBOX_NAME` in `.env`. The project must already contain the connections and +toolbox declared by the sample; running `azd provision` creates them. + +Start the server: + +```bash +python main.py +``` + +In another terminal, invoke the local Responses endpoint: + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input":"Summarize my upcoming calendar events."}' +``` + +You can also use `azd`: + +```bash +azd ai agent run +azd ai agent invoke --local "Summarize my upcoming calendar events." +``` + +## User consent + +The WorkIQ connections use the calling user's Microsoft Entra identity. The +GitHub connection uses Foundry-managed OAuth2. A user may need to grant consent +before a tool can access their data. + +When the Foundry MCP gateway returns consent error `-32006`, the agent responds +with a URL on `consent.azure-apim.net`. Open that URL, complete the consent +flow, and retry the request. The implementation accepts only URLs whose host is +exactly `consent.azure-apim.net`. + +## Configuration + +| Variable | Description | +| --- | --- | +| `FOUNDRY_PROJECT_ENDPOINT` | Foundry project endpoint; injected in hosted containers. | +| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Chat model deployment name. | +| `TOOLBOX_NAME` | Foundry Toolbox name; defaults to `langgraph-toolbox-user-identity-tools` through `azure.yaml`. | +| `PORT` | Local listening port; defaults to `8088`. | + +## Troubleshooting + +### The agent reports that consent is required + +Open the returned consent URL, authorize the connection, and retry the same +request. + +### The agent loads no tools + +Verify that `TOOLBOX_NAME` matches a toolbox in the project identified by +`FOUNDRY_PROJECT_ENDPOINT`, and confirm that the toolbox version containing the +three MCP tools is the default version. + +### A tool schema is rejected + +The sample repairs the common case where an object schema omits `properties`. +For other schema errors, inspect the schema returned by the upstream MCP server. diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/azure.yaml b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/azure.yaml new file mode 100644 index 000000000..de8796ded --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/azure.yaml @@ -0,0 +1,80 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +requiredVersions: + extensions: + azure.ai.agents: '>=1.0.0-beta.6' +name: toolbox-langgraph-user-identity +services: + ai-project: + host: azure.ai.project + deployments: + - name: gpt-5.4-mini + model: + format: OpenAI + name: gpt-5.4-mini + version: '2026-03-17' + sku: + name: GlobalStandard + capacity: 10 + langgraph-toolbox-user-identity-tools: + host: azure.ai.toolbox + uses: + - ai-project + tools: + - type: mcp + server_label: workiq-mail + connection: workiq-mail-conn + server_url: https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools + - type: mcp + server_label: workiq-calendar + connection: workiq-calendar-conn + server_url: https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools + - type: mcp + server_label: github + connection: github-oauth-conn + server_url: https://api.githubcopilot.com/mcp + toolbox-langgraph-user-identity: + host: azure.ai.agent + metadata: + tags: + - AI Agent Hosting + - LangGraph + - Toolbox + - MCP + - OAuth2 + - User Identity + - Microsoft Foundry + project: src/toolbox-langgraph-user-identity + language: python + codeConfiguration: + runtime: python_3_13 + entryPoint: main.py + uses: + - ai-project + - langgraph-toolbox-user-identity-tools + kind: hosted + name: toolbox-langgraph-user-identity + displayName: LangGraph Toolbox User Identity Agent + description: LangGraph agent with toolbox MCP tools using user identity and OAuth. + protocols: + - protocol: responses + version: 2.0.0 + environmentVariables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: TOOLBOX_NAME + value: langgraph-toolbox-user-identity-tools + container: + resources: + cpu: '0.5' + memory: 1Gi +infra: + layers: + - provider: microsoft.foundry + path: infra/foundry + name: foundry + - provider: bicep + path: infra/connections + name: connections + dependsOn: + - foundry diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.bicep b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.bicep new file mode 100644 index 000000000..52fb5aa55 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.bicep @@ -0,0 +1,49 @@ +@description('Name of the existing Microsoft Foundry account.') +param accountName string + +resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: accountName +} + +// The preview API supports UserEntraToken and managed connector properties +// that are not yet represented in the Bicep resource type. +resource workiqMailConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + parent: aiFoundry + name: 'workiq-mail-conn' + properties: any({ + category: 'RemoteTool' + target: 'https://agent365.svc.cloud.microsoft/agents/servers/mcp_MailTools' + authType: 'UserEntraToken' + audience: 'ea9ffc3e-8a23-4a7d-836d-234d7c7565c1' + isSharedToAll: true + }) +} + +resource workiqCalendarConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + parent: aiFoundry + name: 'workiq-calendar-conn' + properties: any({ + category: 'RemoteTool' + target: 'https://agent365.svc.cloud.microsoft/agents/servers/mcp_CalendarTools' + authType: 'UserEntraToken' + audience: 'ea9ffc3e-8a23-4a7d-836d-234d7c7565c1' + isSharedToAll: true + }) +} + +resource githubOAuthConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + parent: aiFoundry + name: 'github-oauth-conn' + properties: any({ + category: 'RemoteTool' + target: 'https://api.githubcopilot.com/mcp' + authType: 'OAuth2' + connectorName: 'foundrygithubmcp' + isSharedToAll: true + credentials: { + type: 'OAuth2' + clientId: 'managed' + clientSecret: 'managed' + } + }) +} diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.parameters.json b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.parameters.json new file mode 100644 index 000000000..5a32a4953 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/connections/main.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "accountName": { + "value": "${AZURE_AI_ACCOUNT_NAME}" + } + } +} diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/foundry/README.md b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/foundry/README.md new file mode 100644 index 000000000..b282419ae --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/infra/foundry/README.md @@ -0,0 +1,4 @@ +# Foundry infrastructure layer + +The `microsoft.foundry` provider provisions the Foundry project and model for +this layer. No Bicep template is required. diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.azdignore b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.azdignore new file mode 100644 index 000000000..0c34457b0 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.azdignore @@ -0,0 +1 @@ +.env.example diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.dockerignore b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.dockerignore new file mode 100644 index 000000000..b709ec79b --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.dockerignore @@ -0,0 +1,26 @@ +**/__pycache__/ +**/*.py[cod] +**/*.egg-info/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# IDE settings +.vscode/ +.idea/ + +# Version control +.git/ +.gitignore + +# Docker files +.dockerignore + +# Docs +README.md + +# Local environment (never bake credentials into the image) +.env diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.env.example b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.env.example new file mode 100644 index 000000000..2c945574d --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/.env.example @@ -0,0 +1,9 @@ +# Foundry project endpoint — auto-injected in hosted containers. +# Only set manually if running without `azd ai agent run`. +FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name — must match a deployment in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME= + +# Foundry Toolbox name — azure.yaml provisions this toolbox by default. +TOOLBOX_NAME=langgraph-toolbox-user-identity-tools diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/Dockerfile b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/Dockerfile new file mode 100644 index 000000000..0c5f8d931 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13-slim +WORKDIR /app +COPY . user_agent/ +WORKDIR /app/user_agent +RUN pip install --upgrade pip && if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +EXPOSE 8088 + +# Precompile Python bytecode at build time so cold starts don't pay for it. +# Dependencies are best-effort so vendored files cannot fail the build; +# application code is compiled strictly so syntax errors surface at build time. +RUN PYTHONDONTWRITEBYTECODE= python -m compileall -q $(python -c "import sysconfig as s; print(s.get_paths()['stdlib'], s.get_paths()['purelib'])") || true; \ + PYTHONDONTWRITEBYTECODE= python -m compileall -q . + +CMD ["python", "main.py"] diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/main.py b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/main.py new file mode 100644 index 000000000..4db3f09c4 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/main.py @@ -0,0 +1,200 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""LangGraph toolbox agent with user-identity MCP tools (Responses protocol). + +Hosts a LangGraph agent built with `langchain.agents.create_agent` on +Foundry over the Responses protocol, using +`langchain_azure_ai.agents.hosting.ResponsesHostServer`. Tools are loaded +at startup from a Foundry Toolbox via +`langchain_azure_ai.tools.AzureAIProjectToolbox`. + +The toolbox contains WorkIQ Mail, WorkIQ Calendar, and GitHub MCP +connections that authenticate on behalf of the calling user. When a +connection returns an OAuth consent error (MCP code ``-32006``), the sample +surfaces the consent URL through a tool message instead of crashing the turn. +""" + +from __future__ import annotations + +import asyncio +import os +import re +from collections.abc import AsyncIterator +from typing import Any +from urllib.parse import urlparse + +from azure.ai.agentserver.responses import CreateResponse, ResponseContext +from azure.ai.projects import AIProjectClient +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from dotenv import load_dotenv +from langchain.agents import create_agent +from langchain_azure_ai.agents.hosting import ResponsesHostServer +from langchain_azure_ai.tools import AzureAIProjectToolbox +from langchain_core.tools import BaseTool +from langchain_openai import ChatOpenAI + +load_dotenv() + +_AZURE_AI_SCOPE = "https://ai.azure.com/.default" + +SYSTEM_PROMPT = """You are a helpful assistant with access to the calling user's +mail, calendar, and GitHub tools through Foundry Toolbox. + +Use a tool whenever the request depends on the user's data. Summarize only the +information needed to answer the request. When a tool returns source URLs, +include a brief "Sources" section listing the titles and URLs you used. Do not +invent citations. +""" + + +# ── Chat model ─────────────────────────────────────────────────────── +def _build_chat_model() -> ChatOpenAI: + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") + deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o") + credential = DefaultAzureCredential() + project = AIProjectClient(endpoint=project_endpoint, credential=credential) + openai_client = project.get_openai_client() + token_provider = get_bearer_token_provider(credential, _AZURE_AI_SCOPE) + + return ChatOpenAI( + model=deployment, + base_url=str(openai_client.base_url), + api_key=token_provider, + use_responses_api=True, + output_version="responses/v1", + ) + + +# ── OAuth consent handling ─────────────────────────────────────────── +_CONSENT_ERROR_CODE = -32006 +_CONSENT_HOST = "consent.azure-apim.net" + + +def _contains_consent_host(text: str) -> bool: + for token in re.findall(r"https?://[^\s'\"<>]+", text): + host = urlparse(token).hostname + if host and (host == _CONSENT_HOST or host.endswith(f".{_CONSENT_HOST}")): + return True + return False + + +def _extract_consent_url(text: str) -> str | None: + for candidate in re.findall(r"https?://[^\s)>\]\"']+", text): + if urlparse(candidate).hostname == _CONSENT_HOST: + return candidate + return None + + +def _is_consent_error(exc: BaseException) -> bool: + error_data = getattr(exc, "error", None) + if ( + error_data is not None + and getattr(error_data, "code", None) == _CONSENT_ERROR_CODE + ): + return True + if _contains_consent_host(str(exc)): + return True + sub_exceptions = getattr(exc, "exceptions", None) + if sub_exceptions: + return any(_is_consent_error(sub) for sub in sub_exceptions) + return False + + +def _consent_url_from_exception(exc: BaseException) -> str: + error_data = getattr(exc, "error", None) + if ( + error_data is not None + and getattr(error_data, "code", None) == _CONSENT_ERROR_CODE + ): + message = getattr(error_data, "message", str(exc)) + return _extract_consent_url(message) or message + url = _extract_consent_url(str(exc)) + if url: + return url + sub_exceptions = getattr(exc, "exceptions", None) + if sub_exceptions: + for sub in sub_exceptions: + nested = _consent_url_from_exception(sub) + if nested: + return nested + return str(exc) + + +def _consent_aware_error_handler(error: Exception) -> str: + if _is_consent_error(error): + url = _consent_url_from_exception(error) + return ( + "OAuth consent required. Open this URL in a browser to authorize the " + f"toolbox connection, then retry the request: {url}" + ) + return f"Tool error: {error}" + + +# ── Tool-schema sanitization ───────────────────────────────────────── +def _sanitize_tool_schema(tool: BaseTool) -> None: + schema: Any = tool.args_schema if isinstance(tool.args_schema, dict) else None + if schema is None: + return + if schema.get("type") == "object" and "properties" not in schema: + schema["properties"] = {} + props = schema.get("properties", {}) + required = schema.get("required", []) + if required and not props: + for field_name in required: + props[field_name] = {"type": "string"} + schema["properties"] = props + + +# ── Toolbox tools ──────────────────────────────────────────────────── +async def _load_toolbox_tools() -> list[BaseTool]: + toolbox = AzureAIProjectToolbox(toolbox_name=os.environ["TOOLBOX_NAME"]) + tools = await toolbox.get_tools() + print(f"Loaded {len(tools)} tool(s) from Foundry Toolbox '{toolbox.toolbox_name}':") + for tool in tools: + _sanitize_tool_schema(tool) + tool.handle_tool_error = _consent_aware_error_handler + print(f" - {tool.name}") + return tools + + +# ── Lazy host ──────────────────────────────────────────────────────── +class _LazyToolboxHostServer(ResponsesHostServer): + def __init__(self, chat_model: ChatOpenAI) -> None: + super().__init__( + create_agent(chat_model, tools=[], system_prompt=SYSTEM_PROMPT) + ) + self._chat_model = chat_model + self._ready_lock = asyncio.Lock() + self._ready = False + + async def _ensure_real_graph(self) -> None: + if self._ready: + return + async with self._ready_lock: + if self._ready: + return + tools = await _load_toolbox_tools() + self._graph = create_agent( + self._chat_model, tools=tools, system_prompt=SYSTEM_PROMPT + ) + self._ready = True + + async def handle_create( + self, + request: CreateResponse, + context: ResponseContext, + cancellation_signal: asyncio.Event, + ) -> AsyncIterator[Any]: + await self._ensure_real_graph() + async for event in super().handle_create(request, context, cancellation_signal): + yield event + + +# ── Entrypoint ─────────────────────────────────────────────────────── +def main() -> None: + port = int(os.environ.get("PORT", "8088")) + _LazyToolboxHostServer(_build_chat_model()).run(port=port) + + +if __name__ == "__main__": + main() diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.in b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.in new file mode 100644 index 000000000..e1953d7fc --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.in @@ -0,0 +1,16 @@ +azure-ai-agentserver-responses==2.1.0b2 +azure-ai-agentserver-invocations==1.1.0b1 +langchain-azure-ai[hosting]>=1.2.9 +azure-ai-projects +# azure-ai-projects imports httpx directly, while OpenAI 3 uses httpx2. +openai<3 +httpx<1 +langchain +langchain-openai +langchain-mcp-adapters +# langchain-mcp-adapters currently uses APIs removed in MCP 2. +mcp>=1.24,<2 +python-dotenv + +# debugpy enables local debugging of this agent with the Foundry Toolkit VS Code extension. +debugpy diff --git a/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt new file mode 100644 index 000000000..8e8fdbebc --- /dev/null +++ b/samples/python/hosted-agents/langgraph/responses/03-langgraph-toolbox-user-identity/src/toolbox-langgraph-user-identity/requirements.txt @@ -0,0 +1,132 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --no-annotate --output-file=requirements.txt requirements.in +# +aiohappyeyeballs==2.7.1 +aiohttp==3.14.3 +aiosignal==1.4.0 +annotated-types==0.8.0 +anyio==4.14.2 +asgiref==3.12.1 +attrs==26.1.0 +azure-ai-agentserver-core==2.1.0b2 +azure-ai-agentserver-invocations==1.1.0b1 +azure-ai-agentserver-responses==2.1.0b2 +azure-ai-contentsafety==1.0.0 +azure-ai-contentunderstanding==1.2.0b3 +azure-ai-projects==2.4.0 +azure-core==1.41.0 +azure-core-tracing-opentelemetry==1.0.0b13 +azure-identity==1.25.3 +azure-monitor-opentelemetry-exporter==1.0.0b56 +azure-search-documents==12.0.0 +azure-storage-blob==12.30.0 +certifi==2026.7.22 +cffi==2.1.1 +charset-normalizer==3.5.1 +click==8.4.2 +cryptography==50.0.0 +debugpy==1.8.21 +distro==1.9.0 +frozenlist==1.8.0 +googleapis-common-protos==1.75.1 +h11==0.16.0 +h2==4.4.1 +hpack==4.2.0 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.3 +hypercorn==0.18.0 +hyperframe==6.1.0 +idna==3.18 +isodate==0.7.2 +jiter==0.16.0 +jsonpatch==1.33 +jsonpointer==3.1.1 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +langchain==1.3.15 +langchain-azure-ai[hosting]==1.2.9 +langchain-core==1.5.5 +langchain-mcp-adapters==0.3.2 +langchain-openai==1.5.1 +langchain-protocol==0.0.18 +langgraph==1.2.11 +langgraph-checkpoint==4.2.0 +langgraph-prebuilt==1.1.0 +langgraph-sdk==0.4.2 +langsmith==0.11.0 +mcp==1.29.0 +microsoft-opentelemetry==1.3.7 +msal==1.37.0 +msal-extensions==1.3.1 +msrest==0.7.1 +multidict==6.7.1 +numpy==2.5.2 +oauthlib==3.3.1 +openai==2.54.0 +opentelemetry-api==1.43.0 +opentelemetry-exporter-otlp-proto-common==1.43.0 +opentelemetry-exporter-otlp-proto-http==1.43.0 +opentelemetry-instrumentation==0.64b0 +opentelemetry-instrumentation-asgi==0.64b0 +opentelemetry-instrumentation-dbapi==0.64b0 +opentelemetry-instrumentation-django==0.64b0 +opentelemetry-instrumentation-fastapi==0.64b0 +opentelemetry-instrumentation-flask==0.64b0 +opentelemetry-instrumentation-httpx==0.64b0 +opentelemetry-instrumentation-logging==0.64b0 +opentelemetry-instrumentation-openai-agents-v2==0.1.0 +opentelemetry-instrumentation-openai-v2==2.3b0 +opentelemetry-instrumentation-psycopg2==0.64b0 +opentelemetry-instrumentation-requests==0.64b0 +opentelemetry-instrumentation-urllib==0.64b0 +opentelemetry-instrumentation-urllib3==0.64b0 +opentelemetry-instrumentation-wsgi==0.64b0 +opentelemetry-proto==1.43.0 +opentelemetry-resource-detector-azure==0.1.5 +opentelemetry-sdk==1.43.0 +opentelemetry-semantic-conventions==0.64b0 +opentelemetry-util-genai==0.3b0 +opentelemetry-util-http==0.64b0 +orjson==3.12.0 +ormsgpack==1.12.2 +packaging==26.3 +priority==2.0.0 +propcache==0.5.2 +protobuf==7.35.1 +psutil==7.2.2 +pycparser==3.0 +pydantic==2.13.4 +pydantic-core==2.46.4 +pydantic-settings==2.15.0 +pyjwt[crypto]==2.13.0 +python-dotenv==1.2.2 +python-multipart==0.0.32 +pyyaml==6.0.3 +referencing==0.37.0 +regex==2026.7.19 +requests==2.34.2 +requests-oauthlib==2.0.0 +requests-toolbelt==1.0.0 +rpds-py==2026.6.3 +six==1.17.0 +sniffio==1.3.1 +sse-starlette==3.4.8 +starlette==1.6.0 +tenacity==9.1.4 +tiktoken==0.13.0 +tqdm==4.70.0 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +urllib3==2.7.0 +uuid-utils==0.17.0 +uvicorn==0.52.3 +websockets==15.0.1 +wrapt==1.17.3 +wsproto==1.3.2 +xxhash==4.0.0 +yarl==1.24.5 +zstandard==0.25.0