M2 · Your first hosted LangGraph agent¶
Build a LangGraph agent, run it locally with the Foundry agent server, then deploy it as a Foundry Hosted Agent.
⏱️ Time: ~45 minutes
Objectives¶
- Build a tool-using agent with LangChain's
create_agent(a compiled LangGraph graph). - Understand the hosted-agent project layout:
azure.yaml,main.py,langgraph.json,requirements.txt. - Run it locally with
azd ai agent runand call its OpenAI-compatible/responsesendpoint. - Deploy it with
azd deployand invoke the hosted version with the Python SDK — including multi-turn.
What is a hosted agent?¶
A Foundry Hosted Agent is your agent code (any framework — here LangGraph) that Foundry runs for
you in a per-session, VM-isolated sandbox. Foundry handles packaging, identity, scaling to zero, conversation
storage, tracing and the public endpoint. You bring a graph; the
langchain_azure_ai.agents.hosting
adapter turns it into a server that speaks Foundry's Responses protocol.
agents/m02-first-agent/
├── azure.yaml ← azd manifest: agent name, runtime, protocol, env, CPU/memory
└── src/first-agent/
├── main.py ← exports `graph` (your LangGraph agent)
├── langgraph.json ← tells the host which graph to serve
├── requirements.txt ← pinned deps installed by Foundry's remote build
└── .agentignore ← keeps .venv etc. out of the upload
# ── Workshop bootstrap (same in every lab) ─────────────────────────────
import sys, pathlib
ROOT = next(p for p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents] if (p / "pyproject.toml").exists())
sys.path.insert(0, str(ROOT))
from workshop.config import settings
print("Project :", settings.project_endpoint)
print("Model :", settings.chat_model)
Project : https://<foundry-account>.services.ai.azure.com/api/projects/<project> Model : gpt-5.4-mini
1. Read the agent code¶
from workshop.azd import show_file
from workshop.config import AGENTS_DIR
src = AGENTS_DIR / "m02-first-agent" / "src" / "first-agent"
show_file(src / "main.py")
show_file(src / "langgraph.json")
agents/m02-first-agent/src/first-agent/main.py
"""M2 · Your first hosted LangGraph agent (Responses protocol).
The module exports ``graph`` — a compiled LangGraph agent. The Foundry hosting
adapter (``python -m langchain_azure_ai.agents.hosting.run``) reads
``langgraph.json``, imports ``graph`` and serves it on ``/responses``.
"""
from __future__ import annotations
import os
from datetime import datetime, timezone
from typing import Annotated
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_core.tools import tool
from langchain_openai import ChatOpenAI
load_dotenv()
_AZURE_AI_SCOPE = "https://ai.azure.com/.default"
SYSTEM_PROMPT = (
"You are Contoso's friendly workshop assistant. Keep answers short and "
"use your tools for anything involving time or arithmetic."
)
@tool
def get_current_time() -> str:
"""Return the current UTC date and time."""
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
@tool
def calculator(
expression: Annotated[str, "A math expression to evaluate, e.g. '42 * 17'."],
) -> str:
"""Evaluate a simple math expression and return the result."""
try:
return str(eval(expression, {"__builtins__": {}})) # noqa: S307 - demo only
except Exception as exc: # pragma: no cover - surfaced to the model
return f"Error: {exc}"
def _build_chat_model() -> ChatOpenAI:
# FOUNDRY_PROJECT_ENDPOINT is injected by Foundry when hosted; from .env locally.
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-5.4-mini")
credential = DefaultAzureCredential()
project = AIProjectClient(endpoint=project_endpoint, credential=credential)
return ChatOpenAI(
model=deployment,
base_url=str(project.get_openai_client().base_url),
api_key=get_bearer_token_provider(credential, _AZURE_AI_SCOPE),
use_responses_api=True,
output_version="responses/v1",
)
graph = create_agent(
_build_chat_model(),
tools=[get_current_time, calculator],
system_prompt=SYSTEM_PROMPT,
)
agents/m02-first-agent/src/first-agent/langgraph.json
{
"graphs": {
"agent": "./main.py:graph"
}
}
Key points:
create_agent(model, tools, system_prompt)returns a compiled LangGraph implementing the ReAct loop (model → tool calls → model → … → answer).- Tools are plain Python functions decorated with
@tool; the docstring and type hints become the tool schema. - The module-level
graphobject is whatlanggraph.jsonpoints to. FOUNDRY_PROJECT_ENDPOINTis injected by Foundry at runtime; locally it comes from your azd environment.
2. The deployment manifest¶
show_file(AGENTS_DIR / "m02-first-agent" / "azure.yaml")
agents/m02-first-agent/azure.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
requiredVersions:
azd: '>=1.27.1'
extensions:
azure.ai.agents: '>=1.0.0-beta.9'
name: lgws-m02-first-agent
services:
lgws-m02-first-agent:
host: azure.ai.agent
project: src/first-agent
language: python
kind: hosted
name: lgws-m02-first-agent
displayName: M2 First LangGraph Agent
description: LangGraph ReAct agent with two local tools, hosted on Foundry over the Responses protocol.
codeConfiguration:
runtime: python_3_13
entryPoint: '-m langchain_azure_ai.agents.hosting.run --protocol responses'
# Used by `azd ai agent run` (local). Mirrors the hosted entryPoint.
startupCommand: python -m langchain_azure_ai.agents.hosting.run --protocol responses
protocols:
- protocol: responses
version: 2.0.0
env:
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: '0.5'
memory: 1Gi
| Field | Meaning |
|---|---|
host: azure.ai.agent, kind: hosted |
a Foundry hosted agent service |
codeConfiguration.runtime / entryPoint |
Foundry builds your source on python_3_13 and starts the LangGraph host |
startupCommand |
what azd ai agent run executes locally |
protocols |
responses → OpenAI-compatible /responses endpoint |
env |
environment variables passed to the container |
container.resources |
CPU / memory per session |
3. Initialise the azd project against your Foundry project¶
init_agent runs azd ai agent init --no-prompt with your project's resource ID and model deployment.
It creates a working copy under .work/m02-first-agent/ (git-ignored) with its own azd environment.
from workshop.azd import init_agent
agent = init_agent("m02-first-agent", port=8088)
print("Agent name:", agent.agent_name)
$ azd ai agent init --no-prompt -m C:\Users\osshaikh\foundry-langgraph-workshop\agents\m02-first-agent\azure.yaml --project-id /subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<foundry-account>/projects/<project> --model-deployment gpt-5.4-mini -e lgws-m02-first-agent
azd project ready: C:\Users\osshaikh\foundry-langgraph-workshop\.work\m02-first-agent\lgws-m02-first-agent Agent name: lgws-m02-first-agent
4. Run it locally¶
azd ai agent run creates a virtual environment, installs requirements.txt and starts the host on
http://localhost:8088. The first start takes 1–2 minutes (dependency install).
agent.run_local()
$ azd ai agent run --no-client --port 8088
Local agent ready on http://localhost:8088
'http://localhost:8088'
from workshop.azd import summarize_output
r1 = agent.local_responses("What time is it right now, and what is 42 * 17?")
print("response id:", r1["id"])
summarize_output(r1)
response id: caresp_c4a7b64f4799dd5a00LbXNK1Fub9yHKY7QKoTVo64UslScWD6T
→ tool call get_current_time({})
→ tool call calculator({"expression": "42 * 17"})
← tool result 714
← tool result 2026-09-29 07:55:55 UTC
💬 Right now it’s **2026-09-29 07:55:55 UTC**.
And **42 × 17 = 714**.
Expected output: two tool calls (get_current_time, calculator), their results, and a final message with the time and 714.
Multi-turn with previous_response_id¶
The host stores each response, so the next turn only needs the previous response's ID:
r2 = agent.local_responses("Add 100 to that result.", previous_response_id=r1["id"])
summarize_output(r2)
assert "814" in str(r2["output"])
→ tool call calculator({"expression": "714 + 100"})
← tool result 814
💬 **814**
agent.stop_local()
5. Deploy to Foundry¶
azd deploy zips src/first-agent, uploads it, lets Foundry build the dependencies remotely, creates a
new agent version and waits until it is active (~2 minutes).
agent.deploy()
$ azd deploy --no-prompt
kaging code) lgws-m02-first-agent: Publishing <project>: Deploying [9s] <project>: Done [9s] lgws-m02-first-agent: Deploying [16s] lgws-m02-first-agent: Deploying (Loading deployment environment) [43s] lgws-m02-first-agent: Deploying (Loading Foundry project) [43s] lgws-m02-first-agent: Deploying (Validating service dependencies) [43s] lgws-m02-first-agent: Deploying (Loading code package artifact) [43s] lgws-m02-first-agent: Deploying (Deploying hosted agent (code deploy)) [43s] lgws-m02-first-agent: Deploying (Checking existing agent) [43s] lgws-m02-first-agent: Deploying (Preparing code agent configuration) [43s] lgws-m02-first-agent: Deploying (Updating existing agent from code package) [52s] lgws-m02-first-agent: Deploying (Registering agent environment variables) [1m0s] lgws-m02-first-agent: Done [1m3s] For information on invoking the agent, see https://aka.ms/azd-agents-invoke Set up an evaluation suite to measure quality and impact in one step with azd ai agent eval generate Next: azd ai agent show lgws-m02-first-agent verify it's running azd ai agent invoke lgws-m02-first-agent '<payload>' test the deployment SUCCESS: Your application was deployed to Azure in 1 minute 3 seconds. You can view the resources created under the resource group <resource-group> in Azure Portal: https://portal.azure.com/#@/resource/subscriptions/<subscription-id>/resourceGroups/<resource-group>/overview
"\x1b[97;1mDeploying services (azd deploy)\x1b[0;22m\n <project>: Packaging\n lgws-m02-first-agent: Packaging\n <project>: Publishing\n lgws-m02-first-agent: Packaging (Packaging code)\n lgws-m02-first-agent: Publishing\n <project>: Deploying [9s]\n <project>: Done [9s]\n lgws-m02-first-agent: Deploying [16s]\n lgws-m02-first-agent: Deploying (Loading deployment environment) [43s]\n lgws-m02-first-agent: Deploying (Loading Foundry project) [43s]\n lgws-m02-first-agent: Deploying (Validating service dependencies) [43s]\n lgws-m02-first-agent: Deploying (Loading code package artifact) [43s]\n lgws-m02-first-agent: Deploying (Deploying hosted agent (code deploy)) [43s]\n lgws-m02-first-agent: Deploying (Checking existing agent) [43s]\n lgws-m02-first-agent: Deploying (Preparing code agent configuration) [43s]\n lgws-m02-first-agent: Deploying (Updating existing agent from code package) [52s]\n lgws-m02-first-agent: Deploying (Registering agent environment variables) [1m0s]\n lgws-m02-first-agent: Done [1m3s]\n For information on invoking the agent, see \x1b[96mhttps://aka.ms/azd-agents-invoke\x1b[0m\nSet up an evaluation suite to measure quality and impact in one step with \x1b[94mazd ai agent eval generate\x1b[0m\nNext:\n \x1b[94mazd ai agent show lgws-m02-first-agent\x1b[0m\n verify it's running\n \x1b[94mazd ai agent invoke lgws-m02-first-agent '<payload>'\x1b[0m\n test the deployment\n\x1b[32m\nSUCCESS: Your application was deployed to Azure in 1 minute 3 seconds.\x1b[0m\nYou can view the resources created under the resource group <resource-group> in Azure Portal:\n\x1b[96mhttps://portal.azure.com/#@/resource/subscriptions/<subscription-id>/resourceGroups/<resource-group>/overview\x1b[0m"
from workshop.agents import ask
h1 = ask(agent.agent_name, "What is 1234 * 5678? Use your calculator.")
h2 = ask(agent.agent_name, "Now divide that by 2.", previous_response_id=h1.id)
print("\nFinal:", h2.output_text)
[lgws-m02-first-agent] caresp_0650b41c2bc7596a00mR7cHNfSSKx1cF57jStBZ3dujs1FFO0x
→ tool call calculator({"expression": "1234 * 5678"})
← tool result 7006652
💬 7006652
[lgws-m02-first-agent] caresp_0650b41c2bc7596a00fWzvFCLJyhly8AWHNfFEdcY6wujlWi3G
→ tool call calculator({"expression": "7006652 / 2"})
← tool result 3503326.0
💬 3503326
Final: 3503326
Expected output: 7006652 then 3503326. The first call after a deploy can take ~10 s (cold start of the session sandbox).
With azd¶
agent.invoke("Say hello to the workshop in five words.")
$ azd ai agent invoke lgws-m02-first-agent Say hello to the workshop in five words.
Agent: lgws-m02-first-agent (remote)
Message: "Say hello to the workshop in five words."
Session: 9fa5aa07f708ee99f1bffeffb10b6b641bd6833c8c573cafe4397d7d5823741
Conversation: conv_0a7535ff92825e2f00ZDTeto4iRBLTya8UafE9jEkJxS2oAGKR
Trace ID: ad60bddf2b17198d26876dc8bab185f9
Response: caresp_0a7535ff92825e2f00ILp68tCXzCoTgPHa3ApJjouSF1giePpx
[lgws-m02-first-agent] Hello workshop, great to see you!
Client elapsed: 11.063s
Platform latency (resume): response headers 8538 ms
preprocess 204 ms | infra 1089 ms | readiness 6482 ms | container 763 ms
'Agent: lgws-m02-first-agent (remote)\nMessage: "Say hello to the workshop in five words."\nSession: 9fa5aa07f708ee99f1bffeffb10b6b641bd6833c8c573cafe4397d7d5823741\nConversation: conv_0a7535ff92825e2f00ZDTeto4iRBLTya8UafE9jEkJxS2oAGKR\nTrace ID: ad60bddf2b17198d26876dc8bab185f9\nResponse: caresp_0a7535ff92825e2f00ILp68tCXzCoTgPHa3ApJjouSF1giePpx\n[lgws-m02-first-agent] Hello workshop, great to see you!\n\x1b[32m\nClient elapsed: 11.063s\n\x1b[0mPlatform latency (resume): response headers 8538 ms\n preprocess 204 ms | infra 1089 ms | readiness 6482 ms | container 763 ms'
In the portal¶
Open ai.azure.com → your project → Agents. Find lgws-m02-first-agent,
open the Playground, and chat with it. The Traces tab shows each model and tool span.
🧪 Your turn¶
- Add a third tool
convert_currency(amount: float, rate: float) -> strtoagents/m02-first-agent/src/first-agent/main.py. - Re-run the init cell (it syncs your edited source), then
agent.run_local()and test it. agent.deploy()again — notice a new version of the same agent is created.
Recap¶
| Step | Command |
|---|---|
| Scaffold against a project | azd ai agent init -m azure.yaml --project-id … --model-deployment … |
| Run locally | azd ai agent run → POST http://localhost:8088/responses |
| Deploy | azd deploy |
| Invoke | azd ai agent invoke … or project.get_openai_client(agent_name=…).responses.create(...) |
➡️ Next: M3 · Tools & function calling — build the ReAct loop yourself with StateGraph.