mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Update hosting agent samples + fixes (#5485)
* Update foundry hosting samples * Add file data type support * Fix file content and add more tests * Fix README * Address comments * Fix int tests * remove temp
This commit is contained in:
committed by
GitHub
Unverified
parent
9b22ecd119
commit
88347f6494
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -1075,6 +1076,31 @@ def _convert_output_message_content(content: OutputMessageContent) -> Content:
|
||||
raise ValueError(f"Unsupported OutputMessageContent type: {content.type}")
|
||||
|
||||
|
||||
def _convert_file_data(data_uri: str, filename: str | None = None) -> Content:
|
||||
"""Convert a file_data data URI to a Content object.
|
||||
|
||||
For text/* MIME types, decodes the base64 content and returns it as text.
|
||||
For other types, returns a URI-based Content with the filename preserved.
|
||||
"""
|
||||
# Parse data URI: data:<media_type>;base64,<data>
|
||||
if data_uri.startswith("data:") and ";base64," in data_uri:
|
||||
header, encoded = data_uri.split(";base64,", 1)
|
||||
media_type = header[len("data:") :]
|
||||
if media_type.startswith("text/"):
|
||||
try:
|
||||
decoded_text = base64.b64decode(encoded).decode("utf-8")
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
logger.warning(
|
||||
"Failed to decode text/* file_data as UTF-8, falling through to URI passthrough.",
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
prefix = f"[File: {filename}]\n" if filename else ""
|
||||
return Content.from_text(f"{prefix}{decoded_text}")
|
||||
additional_properties = {"filename": filename} if filename else None
|
||||
return Content.from_uri(data_uri, additional_properties=additional_properties)
|
||||
|
||||
|
||||
def _convert_message_content(content: MessageContent) -> Content:
|
||||
"""Converts a MessageContent to a Content object.
|
||||
|
||||
@@ -1108,7 +1134,9 @@ def _convert_message_content(content: MessageContent) -> Content:
|
||||
if content.type == "input_image":
|
||||
image = cast(MessageContentInputImageContent, content)
|
||||
if image.image_url:
|
||||
return Content.from_uri(image.image_url)
|
||||
if image.image_url.startswith("data:"):
|
||||
return Content.from_uri(image.image_url)
|
||||
return Content.from_uri(image.image_url, media_type="image/*")
|
||||
if image.file_id:
|
||||
return Content.from_hosted_file(image.file_id)
|
||||
if content.type == "input_file":
|
||||
@@ -1117,6 +1145,8 @@ def _convert_message_content(content: MessageContent) -> Content:
|
||||
return Content.from_uri(file.file_url)
|
||||
if file.file_id:
|
||||
return Content.from_hosted_file(file.file_id, name=file.filename)
|
||||
if file.file_data:
|
||||
return _convert_file_data(file.file_data, file.filename)
|
||||
if content.type == "computer_screenshot":
|
||||
screenshot = cast(ComputerScreenshotContent, content)
|
||||
return Content.from_uri(screenshot.image_url)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -1507,6 +1507,121 @@ class TestMultiTurnMixedContent:
|
||||
assert messages[0].contents[1].type == "uri"
|
||||
assert messages[0].contents[1].uri == "https://example.com/doc.pdf"
|
||||
|
||||
async def test_text_and_file_data_input_single_turn(self) -> None:
|
||||
"""Agent receives a message with text and file content via inline file_data."""
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("File received")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Summarize this document"},
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_data": "data:application/pdf;base64,JVBERi0xLjQ=",
|
||||
"filename": "doc.pdf",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert len(messages[0].contents) == 2
|
||||
assert messages[0].contents[0].type == "text"
|
||||
assert messages[0].contents[0].text == "Summarize this document"
|
||||
assert messages[0].contents[1].type == "data"
|
||||
assert messages[0].contents[1].uri == "data:application/pdf;base64,JVBERi0xLjQ="
|
||||
|
||||
async def test_text_mime_file_data_decoded(self) -> None:
|
||||
"""Agent receives a text/* file_data that is base64-decoded to plain text."""
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Got it")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
|
||||
import base64
|
||||
|
||||
encoded = base64.b64encode(b"Hello, world!").decode()
|
||||
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_data": f"data:text/plain;base64,{encoded}",
|
||||
"filename": "greeting.txt",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert messages[0].contents[0].type == "text"
|
||||
assert messages[0].contents[0].text == "[File: greeting.txt]\nHello, world!"
|
||||
|
||||
async def test_text_mime_file_data_invalid_base64_falls_through(self) -> None:
|
||||
"""Invalid base64 in a text/* file_data falls through to URI passthrough."""
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Got it")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_data": "data:text/plain;base64,!!!invalid!!!",
|
||||
"filename": "bad.txt",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert messages[0].contents[0].type == "data"
|
||||
assert messages[0].contents[0].uri == "data:text/plain;base64,!!!invalid!!!"
|
||||
|
||||
async def test_mixed_text_and_image_input(self) -> None:
|
||||
"""Agent receives a single message with both text and image content."""
|
||||
agent = _make_agent(
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Integration tests for ResponsesHostServer with a real Foundry endpoint.
|
||||
|
||||
These tests exercise the full HTTP pipeline using httpx.AsyncClient with
|
||||
ASGITransport — no real server process is started. The agent talks to a real
|
||||
Foundry project endpoint so every test requires valid credentials.
|
||||
|
||||
Required environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT - The Azure AI Foundry project endpoint URL.
|
||||
FOUNDRY_MODEL - The model deployment name (e.g. gpt-4o).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.ai.agentserver.responses import InMemoryResponseProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Skip / marker helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
skip_if_foundry_hosting_integration_tests_disabled = pytest.mark.skipif(
|
||||
os.getenv("FOUNDRY_PROJECT_ENDPOINT", "") in ("", "https://test-project.services.ai.azure.com/")
|
||||
or os.getenv("FOUNDRY_MODEL", "") == "",
|
||||
reason="No real FOUNDRY_PROJECT_ENDPOINT or FOUNDRY_MODEL provided; skipping integration tests.",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server() -> ResponsesHostServer:
|
||||
"""Create a ResponsesHostServer backed by a real Foundry agent."""
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a concise assistant. Keep answers very short (one or two sentences).",
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
return ResponsesHostServer(agent, store=InMemoryResponseProvider())
|
||||
|
||||
|
||||
@tool
|
||||
async def get_weather(location: Annotated[str, "The city name"]) -> str:
|
||||
"""Get the current weather in a given location."""
|
||||
return f"The weather in {location} is 72°F and sunny."
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server_with_tools() -> ResponsesHostServer:
|
||||
"""Create a ResponsesHostServer whose agent has a tool."""
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a concise assistant. Use the provided tools when appropriate. Keep answers very short.",
|
||||
tools=[get_weather],
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
return ResponsesHostServer(agent, store=InMemoryResponseProvider())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _post_json(
|
||||
server: ResponsesHostServer,
|
||||
payload: dict[str, Any],
|
||||
) -> httpx.Response:
|
||||
"""Send a POST /responses request with a raw JSON payload."""
|
||||
transport = httpx.ASGITransport(app=server)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
return await client.post("/responses", json=payload, timeout=120)
|
||||
|
||||
|
||||
def _parse_sse_events(body: str) -> list[dict[str, Any]]:
|
||||
"""Parse SSE text into a list of event dicts with 'event' and 'data' keys."""
|
||||
events: list[dict[str, Any]] = []
|
||||
current_event: str | None = None
|
||||
current_data_lines: list[str] = []
|
||||
|
||||
for line in body.split("\n"):
|
||||
if line.startswith("event: "):
|
||||
current_event = line[len("event: ") :]
|
||||
elif line.startswith("data: "):
|
||||
current_data_lines.append(line[len("data: ") :])
|
||||
elif line.strip() == "" and current_event is not None:
|
||||
data_str = "\n".join(current_data_lines)
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
data = data_str
|
||||
events.append({"event": current_event, "data": data})
|
||||
current_event = None
|
||||
current_data_lines = []
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def _sse_event_types(events: list[dict[str, Any]]) -> list[str]:
|
||||
"""Extract event type strings from parsed SSE events."""
|
||||
return [e["event"] for e in events]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — basic text input
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBasicText:
|
||||
"""Simple text-in / text-out round trips."""
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_simple_text_non_streaming(self, server: ResponsesHostServer) -> None:
|
||||
"""Non-streaming: send a text prompt and get a completed response."""
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "Say hello in exactly three words.",
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
# There should be exactly one output item with text
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
text_parts = [c for c in output_messages[0]["content"] if c["type"] == "output_text"]
|
||||
assert len(text_parts) >= 1
|
||||
assert len(text_parts[0]["text"]) > 0
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_simple_text_streaming(self, server: ResponsesHostServer) -> None:
|
||||
"""Streaming: send a text prompt and verify SSE lifecycle events."""
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "Say hello in exactly three words.",
|
||||
"stream": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "text/event-stream" in resp.headers["content-type"]
|
||||
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[1] == "response.in_progress"
|
||||
assert types[-1] == "response.completed"
|
||||
assert "response.output_text.delta" in types
|
||||
assert "response.output_text.done" in types
|
||||
|
||||
# The done event should have accumulated text
|
||||
done_events = [e for e in events if e["event"] == "response.output_text.done"]
|
||||
assert len(done_events) >= 1
|
||||
assert len(done_events[0]["data"]["text"]) > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — structured content input
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestStructuredContentInput:
|
||||
"""Structured content arrays: text + images, text + files."""
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_text_array_input(self, server: ResponsesHostServer) -> None:
|
||||
"""Multiple input_text parts in one message."""
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "My name is Alice."},
|
||||
{"type": "input_text", "text": "What is my name?"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
# The response should mention Alice
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"]
|
||||
assert "alice" in output_text.lower()
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_input_image_url(self, server: ResponsesHostServer) -> None:
|
||||
"""Send an image via URL and ask the model about it."""
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What animal is in this image? Reply in one word."},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"].lower()
|
||||
assert "cat" in output_text
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_input_image_file_data(self, server: ResponsesHostServer) -> None:
|
||||
"""Send a local image file as inline base64 data URI."""
|
||||
image_path = Path(__file__).resolve().parent / "test_assets" / "sample_image.jpg" # noqa: ASYNC240
|
||||
image_bytes = image_path.read_bytes()
|
||||
b64 = base64.b64encode(image_bytes).decode()
|
||||
data_uri = f"data:image/jpeg;base64,{b64}"
|
||||
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What animal is in this image? Reply in one word."},
|
||||
{"type": "input_image", "image_url": data_uri},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"].lower()
|
||||
assert "cat" in output_text
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_input_file_data(self, server: ResponsesHostServer) -> None:
|
||||
"""Send a small text file as inline file_data (base64 data URI)."""
|
||||
text_content = "The capital of France is Paris."
|
||||
b64 = base64.b64encode(text_content.encode()).decode()
|
||||
data_uri = f"data:text/plain;base64,{b64}"
|
||||
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "What is the capital mentioned in the attached file?"},
|
||||
{"type": "input_file", "file_data": data_uri, "filename": "info.txt"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"].lower()
|
||||
assert "paris" in output_text
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_input_pdf_file_data(self, server: ResponsesHostServer) -> None:
|
||||
"""Send a real PDF file as inline file_data (base64 data URI)."""
|
||||
pdf_path = Path(__file__).resolve().parent / "test_assets" / "sample.pdf" # noqa: ASYNC240
|
||||
pdf_bytes = pdf_path.read_bytes()
|
||||
b64 = base64.b64encode(pdf_bytes).decode()
|
||||
data_uri = f"data:application/pdf;base64,{b64}"
|
||||
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Summarize this PDF in one sentence."},
|
||||
{"type": "input_file", "file_data": data_uri, "filename": "sample.pdf"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"]
|
||||
assert "microsoft" in output_text.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — multi-turn conversations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMultiTurn:
|
||||
"""Multi-round conversations using previous_response_id."""
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_two_turn_conversation(self, server: ResponsesHostServer) -> None:
|
||||
"""Turn 1: introduce context. Turn 2: ask about it using previous_response_id."""
|
||||
# Turn 1
|
||||
resp1 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "My favorite color is blue. Remember that.",
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp1.status_code == 200
|
||||
body1 = resp1.json()
|
||||
assert body1["status"] == "completed"
|
||||
response_id_1 = body1["id"]
|
||||
|
||||
# Turn 2 — references turn 1
|
||||
resp2 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "What is my favorite color?",
|
||||
"stream": False,
|
||||
"previous_response_id": response_id_1,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp2.status_code == 200
|
||||
body2 = resp2.json()
|
||||
assert body2["status"] == "completed"
|
||||
output_messages = [o for o in body2["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"].lower()
|
||||
assert "blue" in output_text
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_three_turn_conversation(self, server: ResponsesHostServer) -> None:
|
||||
"""Three sequential turns to verify history accumulates correctly."""
|
||||
# Turn 1
|
||||
resp1 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "I have a pet dog named Max.",
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
assert resp1.status_code == 200
|
||||
id1 = resp1.json()["id"]
|
||||
|
||||
# Turn 2
|
||||
resp2 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "I also have a cat named Luna.",
|
||||
"stream": False,
|
||||
"previous_response_id": id1,
|
||||
},
|
||||
)
|
||||
assert resp2.status_code == 200
|
||||
id2 = resp2.json()["id"]
|
||||
|
||||
# Turn 3 — should remember both pets
|
||||
resp3 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "What are my pets' names?",
|
||||
"stream": False,
|
||||
"previous_response_id": id2,
|
||||
},
|
||||
)
|
||||
assert resp3.status_code == 200
|
||||
body3 = resp3.json()
|
||||
output_messages = [o for o in body3["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"].lower()
|
||||
assert "max" in output_text
|
||||
assert "luna" in output_text
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_multi_turn_streaming(self, server: ResponsesHostServer) -> None:
|
||||
"""Multi-turn conversation with streaming on the second turn."""
|
||||
# Turn 1 — non-streaming
|
||||
resp1 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "My favorite number is 42.",
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
assert resp1.status_code == 200
|
||||
id1 = resp1.json()["id"]
|
||||
|
||||
# Turn 2 — streaming
|
||||
resp2 = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "What is my favorite number?",
|
||||
"stream": True,
|
||||
"previous_response_id": id1,
|
||||
},
|
||||
)
|
||||
assert resp2.status_code == 200
|
||||
assert "text/event-stream" in resp2.headers["content-type"]
|
||||
|
||||
events = _parse_sse_events(resp2.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[-1] == "response.completed"
|
||||
assert "response.output_text.done" in types
|
||||
|
||||
done_events = [e for e in events if e["event"] == "response.output_text.done"]
|
||||
assert "42" in done_events[0]["data"]["text"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — tool calling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestToolCalling:
|
||||
"""Tests that verify function-tool round trips through the hosting layer."""
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_tool_call_non_streaming(self, server_with_tools: ResponsesHostServer) -> None:
|
||||
"""Agent invokes a tool and returns a final answer (non-streaming)."""
|
||||
resp = await _post_json(
|
||||
server_with_tools,
|
||||
{
|
||||
"input": "What is the weather in Seattle?",
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
# The output should contain the final text referencing the weather
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
final_text = output_messages[0]["content"][0]["text"].lower()
|
||||
assert "72" in final_text or "sunny" in final_text or "seattle" in final_text
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_tool_call_streaming(self, server_with_tools: ResponsesHostServer) -> None:
|
||||
"""Agent invokes a tool and returns a final answer (streaming)."""
|
||||
resp = await _post_json(
|
||||
server_with_tools,
|
||||
{
|
||||
"input": "What is the weather in Seattle?",
|
||||
"stream": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "text/event-stream" in resp.headers["content-type"]
|
||||
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[-1] == "response.completed"
|
||||
|
||||
# Should have text output with the weather info
|
||||
done_events = [e for e in events if e["event"] == "response.output_text.done"]
|
||||
assert len(done_events) >= 1
|
||||
final_text = done_events[-1]["data"]["text"].lower()
|
||||
assert "72" in final_text or "sunny" in final_text or "seattle" in final_text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — options passthrough
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOptions:
|
||||
"""Verify chat options are passed through to the model."""
|
||||
|
||||
@pytest.mark.flaky
|
||||
@pytest.mark.integration
|
||||
@skip_if_foundry_hosting_integration_tests_disabled
|
||||
async def test_temperature_and_max_tokens(self, server: ResponsesHostServer) -> None:
|
||||
"""Set temperature and max_output_tokens and verify the response succeeds."""
|
||||
resp = await _post_json(
|
||||
server,
|
||||
{
|
||||
"input": "Say hello briefly.",
|
||||
"stream": False,
|
||||
"max_output_tokens": 50,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
output_messages = [o for o in body["output"] if o["type"] == "message"]
|
||||
assert len(output_messages) == 1
|
||||
output_text = output_messages[0]["content"][0]["text"]
|
||||
assert len(output_text) > 0
|
||||
@@ -1,12 +1,139 @@
|
||||
# Foundry Hosted Agents Samples
|
||||
# Foundry Hosted Agent Samples
|
||||
|
||||
This directory contains samples that demonstrate how to use the Agent Framework to host agents on Foundry with different capabilities and configurations. Each sample includes a README with instructions on how to set up, run, and interact with the agent.
|
||||
This directory contains samples that demonstrate how to use hosted [Agent Framework](https://github.com/microsoft/agent-framework) agents with different capabilities and configurations on Foundry using the Foundry Hosting Agent service. Each sample includes a README with instructions on how to set up, run, and interact with the agent.
|
||||
|
||||
Read more about Foundry Hosted Agents [here](https://learn.microsoft.com/en-us/azure/foundry/agents/concepts/hosted-agents).
|
||||
## Samples
|
||||
|
||||
## Environment setup
|
||||
### Responses API
|
||||
|
||||
1. Navigate to the sample directory you want to run. For example:
|
||||
| # | Sample | Description |
|
||||
|---|--------|-------------|
|
||||
| 1 | [Basic](responses/01_basic/) | A minimal agent demonstrating basic request/response interaction and multi-turn conversations using `previous_response_id`. |
|
||||
| 2 | [Tools](responses/02_tools/) | An agent with local tools (e.g., weather lookup), demonstrating how to register and invoke custom tool functions alongside the LLM. |
|
||||
| 3 | [MCP](responses/03_mcp/) | An agent connected to a remote MCP server (GitHub), demonstrating external MCP tool provider integration. |
|
||||
| 4 | [Foundry Toolbox](responses/04_foundry_toolbox/) | An agent using Azure Foundry Toolbox, demonstrating toolbox provisioning and querying available tools at runtime. |
|
||||
| 5 | [Workflows](responses/05_workflows/) | An agent with a multi-step orchestrated workflow, demonstrating chaining prompts through an orchestrated flow. |
|
||||
| 6 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. |
|
||||
|
||||
### Invocations API
|
||||
|
||||
| # | Sample | Description |
|
||||
|---|--------|-------------|
|
||||
| 1 | [Basic](invocations/01_basic/) | A minimal agent demonstrating session state management via `agent_session_id` in URL params/response headers. |
|
||||
| 2 | [Break Glass](invocations/02_break_glass/) | An agent demonstrating a "break glass" scenario where customizations of the API behaviors are needed, allowing for more direct control over how requests and responses are handled by the hosting layer. |
|
||||
|
||||
## Running the Agent Host Locally
|
||||
|
||||
### Using `azd`
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. **Azure Developer CLI (`azd`)**
|
||||
|
||||
- [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) and the AI agent extension: `azd ext install azure.ai.agents`
|
||||
- Authenticated: `azd auth login`
|
||||
|
||||
2. **Azure Subscription**
|
||||
|
||||
#### Create a new project
|
||||
|
||||
**No cloning required**. Create a new folder, point azd at the manifest on GitHub.
|
||||
|
||||
```bash
|
||||
mkdir hosted-agent-framework-agent && cd hosted-agent-framework-agent
|
||||
|
||||
# Initialize from the manifest
|
||||
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/python/samples/04-hosting/foundry-hosted-agents/responses/01_basic/agent.manifest.yaml
|
||||
```
|
||||
|
||||
Follow the instructions from `azd ai agent init` to complete the agent initialization. If you don't have an existing Foundry project and a model deployment, `azd ai agent init` will guide you through creating them.
|
||||
|
||||
#### Provision Azure Resources
|
||||
|
||||
> This step is only needed if you don't have an existing Foundry project and model deployment.
|
||||
|
||||
Run the following command to provision the necessary Azure resources:
|
||||
|
||||
```bash
|
||||
azd provision
|
||||
```
|
||||
|
||||
This will create the following Azure resources:
|
||||
|
||||
- A new resource group named `rg-[project_name]-dev`. In this guide, `[project_name]` will be `hosted-agent-framework-agent`.
|
||||
- Within the resource group, among other resources, the most important ones are:
|
||||
- A new Foundry instance
|
||||
- A new Foundry project, within which a new model deployment will be created
|
||||
- An Application Insights instance
|
||||
- A container registry, which will be used to store the container images for the hosted agent
|
||||
|
||||
#### Set Environment Variables
|
||||
|
||||
```bash
|
||||
export FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
|
||||
export AZURE_AI_MODEL_DEPLOYMENT_NAME="<your-model-deployment-name>"
|
||||
# And any other environment variables required by the sample
|
||||
```
|
||||
|
||||
Or in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
|
||||
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="<your-model-deployment-name>"
|
||||
# And any other environment variables required by the sample
|
||||
```
|
||||
|
||||
> Note: The environment variables set above are only for the current session. You will need to set them again if you open a new terminal session. if you want to set the environment variables permanently in the azd environment, you can use `azd env set <name> <value>`.
|
||||
|
||||
#### Running the Agent Host
|
||||
|
||||
```bash
|
||||
azd ai agent run
|
||||
```
|
||||
|
||||
Right now, the agent host should be running on `http://localhost:8088`
|
||||
|
||||
#### Invoking the Agent
|
||||
|
||||
Open another terminal, **navigate to the project directory**, and run the following command to invoke the agent:
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Hello!"
|
||||
```
|
||||
|
||||
Or you can in another terminal, without navigating to the project directory, run the following command to invoke the agent:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hello!"}'
|
||||
```
|
||||
|
||||
Or in PowerShell:
|
||||
|
||||
```powershell
|
||||
(Invoke-WebRequest -Uri http://localhost:8088/responses -Method POST -ContentType "application/json" -Body '{"input": "Hello!"}').Content
|
||||
```
|
||||
|
||||
### Using `python`
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. An existing Foundry project
|
||||
2. A deployed model in your Foundry project
|
||||
3. Azure CLI installed and authenticated
|
||||
4. Python 3.10 or later
|
||||
|
||||
#### Running the Agent Host with Python
|
||||
|
||||
Clone the repository containing the sample code:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/microsoft/agent-framework.git
|
||||
cd agent-framework/python/samples/04-hosting/foundry-hosted-agents/responses
|
||||
```
|
||||
|
||||
#### Environment setup
|
||||
|
||||
1. Navigate to the sample directory you want to explore. Create a virtual environment:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
@@ -32,25 +159,58 @@ Read more about Foundry Hosted Agents [here](https://learn.microsoft.com/en-us/a
|
||||
az login
|
||||
```
|
||||
|
||||
## Deploying to a Docker container
|
||||
|
||||
Navigate to the sample directory and build the Docker image:
|
||||
#### Running the Agent Host
|
||||
|
||||
```bash
|
||||
docker build -t hosted-agent-sample .
|
||||
python main.py
|
||||
```
|
||||
|
||||
Run the container, passing in the required environment variables:
|
||||
Right now, the agent host should be running on `http://localhost:8088`
|
||||
|
||||
#### Invoking the Agent
|
||||
|
||||
On another terminal, run the following command to invoke the agent:
|
||||
|
||||
```bash
|
||||
docker run -p 8088:8088 \
|
||||
-e FOUNDRY_PROJECT_ENDPOINT=<your-endpoint> \
|
||||
-e MODEL_DEPLOYMENT_NAME=<your-model> \
|
||||
hosted-agent-sample
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hello!"}'
|
||||
```
|
||||
|
||||
The server will be available at `http://localhost:8088`. You can send requests using the same `curl` command shown above.
|
||||
Or in PowerShell:
|
||||
|
||||
## Deploying to Foundry
|
||||
```powershell
|
||||
(Invoke-WebRequest -Uri http://localhost:8088/responses -Method POST -ContentType "application/json" -Body '{"input": "Hello!"}').Content
|
||||
```
|
||||
|
||||
Follow this [guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent?tabs=bash#configure-your-agent) to deploy your agent to Foundry.
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
Once you've tested locally, deploy to Microsoft Foundry.
|
||||
|
||||
### With an Existing Foundry Project
|
||||
|
||||
If you already have a Foundry project and the necessary Azure resources provisioned, you can skip the setup steps and proceed directly to deploying the agent.
|
||||
|
||||
After running `azd ai agent init -m <agent.manifest.yaml>` and following the prompts to configure your agent, you will have a project ready for deployment.
|
||||
|
||||
### Setting Up a New Foundry Project
|
||||
|
||||
Follow the steps in [Using `azd`](#using-azd) to set up the project and provision the necessary Azure resources for your Foundry deployment.
|
||||
|
||||
### Deploying the Agent
|
||||
|
||||
Once the project is setup and resources are provisioned, you can deploy the agent to Foundry by running:
|
||||
|
||||
```bash
|
||||
azd deploy
|
||||
```
|
||||
|
||||
> The Foundry hosting infrastructure will inject the following environment variables into your agent at runtime:
|
||||
>
|
||||
> - `FOUNDRY_PROJECT_ENDPOINT`: The endpoint URL for the Foundry project where the agent is deployed.
|
||||
> - `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of the model deployment in your Foundry project. This is configured during the agent initialization process with `azd ai agent init`.
|
||||
> - `APPLICATIONINSIGHTS_CONNECTION_STRING`: The connection string for Application Insights to enable telemetry for your agent.
|
||||
|
||||
This will package your agent and deploy it to the Foundry environment, making it accessible through the Foundry project endpoint. Once it's deployed, you can also access the agent through the Foundry UI.
|
||||
|
||||
For the full deployment guide, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
|
||||
|
||||
Once deployed, learn more about how to manage deployed agents in the [official management guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent).
|
||||
@@ -1,2 +1,2 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,18 +1,26 @@
|
||||
# Basic example of hosting an agent with the `invocations` API
|
||||
# What this sample demonstrates
|
||||
|
||||
## Running the server locally
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent hosted using the **Invocations protocol** with session management. Unlike the Responses protocol, the Invocations protocol does **not** provide built-in server-side conversation history — this agent maintains an in-memory session store keyed by `agent_session_id`. In production, replace it with durable storage (Redis, Cosmos DB, etc.) so history survives restarts.
|
||||
|
||||
### Environment setup
|
||||
## How It Works
|
||||
|
||||
Follow the instructions in the [Environment setup](../../README.md#environment-setup) section of the README in the parent directory to set up your environment and install dependencies.
|
||||
### Model Integration
|
||||
|
||||
Run the following command to start the server:
|
||||
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. When a request arrives, the handler looks up (or creates) a session by `session_id`, runs the agent with the user message and session context, and returns the reply. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
### Interacting with the agent
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `InvocationsHostServer`, which provisions a REST API endpoint compatible with the Azure AI Invocations protocol.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
@@ -22,7 +30,7 @@ curl -X POST http://localhost:8088/invocations -i -H "Content-Type: application/
|
||||
|
||||
The server will respond with a JSON object containing the response text. The `-i` flag in the `curl` command includes the HTTP response headers in the output, which includes the session ID that can be used for multi-turn conversations. Here is an example of the response:
|
||||
|
||||
```bash
|
||||
```
|
||||
HTTP/1.1 200
|
||||
content-length: 34
|
||||
content-type: application/json
|
||||
@@ -42,3 +50,7 @@ To have a multi-turn conversation with the agent, take the session ID from the r
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/invocations?agent_session_id=9370b9d4-cd13-4436-a57f-03b843ac0e17 -i -H "Content-Type: application/json" -d '{"message": "How are you?"}'
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
|
||||
+3
-3
@@ -15,9 +15,9 @@ template:
|
||||
- protocol: invocations
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
@@ -6,4 +6,4 @@ protocols:
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: '0.25'
|
||||
memory: '0.5Gi'
|
||||
memory: '0.5Gi'
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import InvocationsHostServer
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
@@ -15,8 +15,8 @@ load_dotenv()
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
+21
-11
@@ -1,20 +1,26 @@
|
||||
# Basic example of hosting an agent with the `invocations` API
|
||||
# What this sample demonstrates
|
||||
|
||||
This is the same as the [01_basic](../01_basic/README.md) example, but demonstrates the "break glass" scenario where you can create your own `invoke_handler` to handle specific types of invocations. This is useful when you want to override the default behavior for certain requests or add custom processing logic.
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent hosted using the **Invocations protocol** with session management. Unlike the Responses protocol, the Invocations protocol does **not** provide built-in server-side conversation history — this agent maintains an in-memory session store keyed by `agent_session_id`. In production, replace it with durable storage (Redis, Cosmos DB, etc.) so history survives restarts.
|
||||
|
||||
## Running the server locally
|
||||
## How It Works
|
||||
|
||||
### Environment setup
|
||||
### Model Integration
|
||||
|
||||
Follow the instructions in the [Environment setup](../../README.md#environment-setup) section of the README in the parent directory to set up your environment and install dependencies.
|
||||
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. When a request arrives, the handler looks up (or creates) a session by `session_id`, runs the agent with the user message and session context, and returns the reply. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
|
||||
|
||||
Run the following command to start the server:
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
### Agent Hosting
|
||||
|
||||
### Interacting with the agent
|
||||
The agent is hosted using the [Azure AI AgentServer Invocations SDK](https://pypi.org/project/azure-ai-agentserver-invocations/) (`InvocationAgentServerHost`), which provisions a REST API endpoint compatible with the Azure AI Invocations protocol.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
@@ -24,7 +30,7 @@ curl -X POST http://localhost:8088/invocations -i -H "Content-Type: application/
|
||||
|
||||
The server will respond with a JSON object containing the response text. The `-i` flag in the `curl` command includes the HTTP response headers in the output, which includes the session ID that can be used for multi-turn conversations. Here is an example of the response:
|
||||
|
||||
```bash
|
||||
```
|
||||
HTTP/1.1 200
|
||||
content-length: 34
|
||||
content-type: application/json
|
||||
@@ -44,3 +50,7 @@ To have a multi-turn conversation with the agent, take the session ID from the r
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/invocations?agent_session_id=9370b9d4-cd13-4436-a57f-03b843ac0e17 -i -H "Content-Type: application/json" -d '{"message": "How are you?"}'
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
+3
-3
@@ -15,9 +15,9 @@ template:
|
||||
- protocol: invocations
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
+1
-1
@@ -6,4 +6,4 @@ protocols:
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: '0.25'
|
||||
memory: '0.5Gi'
|
||||
memory: '0.5Gi'
|
||||
@@ -22,7 +22,7 @@ _sessions: dict[str, AgentSession] = {}
|
||||
# Create the agent
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Hosting agents with Foundry Hosting and the `invocations` API
|
||||
|
||||
This folder contains a list of samples that show how to host agents using the `invocations` API and deploy them to Foundry Hosting.
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [01_basic](./01_basic) | A basic example of hosting an agent with the `invocations` API and carrying on a multi-turn conversation. |
|
||||
| [02_break_glass](./02_break_glass) | An example of hosting an agent with the `invocations` API and a "break glass" scenario where you can create your own `invoke_handler` to handle specific types of invocations. |
|
||||
@@ -1,2 +1,2 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,31 +1,39 @@
|
||||
# Basic example of hosting an agent with the `responses` API
|
||||
# What this sample demonstrates
|
||||
|
||||
This agent only contains an instruction (personal). It's the most basic agent with an LLM and no tools.
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent hosted using the **Responses protocol**.
|
||||
|
||||
## Running the server locally
|
||||
## How It Works
|
||||
|
||||
### Environment setup
|
||||
### Model Integration
|
||||
|
||||
Follow the instructions in the [Environment setup](../../README.md#environment-setup) section of the README in the parent directory to set up your environment and install dependencies.
|
||||
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
|
||||
|
||||
Run the following command to start the server:
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "input" field to interact with the agent. For example:
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi"}'
|
||||
```
|
||||
|
||||
## Multi-turn conversation
|
||||
The server will respond with a JSON object containing the response text and a response ID. You can use this response ID to continue the conversation in subsequent requests.
|
||||
|
||||
### Multi-turn conversation
|
||||
|
||||
To have a multi-turn conversation with the agent, include the previous response id in the request body. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "How are you?", "previous_response_id": "REPLACE_WITH_PREVIOUS_RESPONSE_ID"}'
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
name: agent-framework-agent-basic
|
||||
name: agent-framework-agent-basic-responses
|
||||
description: >
|
||||
A basic Agent Framework agent hosted by Foundry.
|
||||
metadata:
|
||||
@@ -9,15 +9,15 @@ metadata:
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-basic
|
||||
name: agent-framework-agent-basic-responses
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
@@ -1,8 +1,9 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: agent-framework-agent-basic
|
||||
name: agent-framework-agent-basic-responses
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
cpu: '0.25'
|
||||
memory: '0.5Gi'
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
@@ -15,8 +15,8 @@ load_dotenv()
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,27 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API and local tools
|
||||
|
||||
This agent is equipped with a function tool and a local shell tool.
|
||||
|
||||
> We recommend deploying this sample on a local container or to Foundry Hosting because the agent has access to a local shell tool, which can run arbitrary commands on the machine.
|
||||
|
||||
## Running the server locally
|
||||
|
||||
### Environment setup
|
||||
|
||||
Follow the instructions in the [Environment setup](../../README.md#environment-setup) section of the README in the parent directory to set up your environment and install dependencies.
|
||||
|
||||
Run the following command to start the server:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "input" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What is the weather in Seattle?"}'
|
||||
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List the files in the current directory."}'
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -0,0 +1,33 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent with **locally-defined Python tools** hosted using the **Responses protocol**. It shows how to define custom tools with the `@tool` decorator and register them with the agent so the model can call them during a conversation.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Model Integration
|
||||
|
||||
The agent uses `FoundryChatClient` from the Agent Framework to create a Responses client from the project endpoint and model deployment. The agent supports both streaming (SSE events) and non-streaming (JSON) response modes.
|
||||
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What is the weather in Seattle?"}'
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
name: agent-framework-agent-with-local-tools
|
||||
name: agent-framework-agent-with-local-tools-responses
|
||||
description: >
|
||||
An Agent Framework agent with local tools hosted by Foundry.
|
||||
metadata:
|
||||
@@ -9,15 +9,15 @@ metadata:
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-with-local-tools
|
||||
name: agent-framework-agent-with-local-tools-responses
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
kind: hosted
|
||||
name: agent-framework-agent-with-local-tools
|
||||
name: agent-framework-agent-with-local-tools-responses
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
+3
-3
@@ -8,7 +8,7 @@ from typing import Annotated
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
|
||||
@@ -52,8 +52,8 @@ def run_bash(command: str) -> str:
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
TOOLBOX_NAME="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
GITHUB_PAT="..."
|
||||
@@ -0,0 +1,33 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that connects to a **remote MCP server** (GitHub) for tool discovery and hosted using the **Responses protocol**. Instead of defining tools locally, the agent discovers and invokes tools at runtime from an MCP-compatible endpoint — in this case, the GitHub Copilot MCP server. This enables dynamic tool integration without redeployment.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Model Integration
|
||||
|
||||
The agent uses `FoundryChatClient` from the Agent Framework to create an OpenAI-compatible Responses client. It registers a remote MCP tool pointing at `https://api.githubcopilot.com/mcp/`, authenticating with a GitHub Personal Access Token (PAT). When the model decides to call a tool, the framework forwards the call to the MCP server and returns the result to the model for the final reply.
|
||||
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List all the repositories I own on GitHub."}'
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
+5
-7
@@ -1,4 +1,4 @@
|
||||
name: agent-framework-agent-with-remote-mcp-tools
|
||||
name: agent-framework-agent-with-remote-mcp-tools-responses
|
||||
description: >
|
||||
An Agent Framework agent with remote MCP tools hosted by Foundry.
|
||||
metadata:
|
||||
@@ -9,19 +9,17 @@ metadata:
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-with-remote-mcp-tools
|
||||
name: agent-framework-agent-with-remote-mcp-tools-responses
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: GITHUB_PAT
|
||||
value: ${GITHUB_PAT}
|
||||
- name: TOOLBOX_NAME
|
||||
value: ${TOOLBOX_NAME}
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
@@ -0,0 +1,11 @@
|
||||
kind: hosted
|
||||
name: agent-framework-agent-with-remote-mcp-tools-responses
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
environment_variables:
|
||||
- name: GITHUB_PAT
|
||||
value: ${GITHUB_PAT}
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agent_framework import Agent, ToolTypes
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
github_pat = os.environ["GITHUB_PAT"]
|
||||
tools: list[ToolTypes] = []
|
||||
if not github_pat:
|
||||
logger.warning("GITHUB_PAT environment variable is not set. The GitHub MCP tool will not get registered.")
|
||||
else:
|
||||
tools.append(
|
||||
client.get_mcp_tool(
|
||||
name="GitHub",
|
||||
url="https://api.githubcopilot.com/mcp/",
|
||||
headers={
|
||||
"Authorization": f"Bearer {github_pat}",
|
||||
},
|
||||
approval_mode="never_require",
|
||||
)
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
tools=tools,
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(agent)
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,25 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API and a remote MCP
|
||||
|
||||
This agent is equipped with a GitHub MCP server and a Foundry Toolbox, which are both remote MCPs.
|
||||
|
||||
> Note that there are other ways to interact with Foundry toolboxes. Using it as a MCP is just one of the options.
|
||||
|
||||
## Running the server locally
|
||||
|
||||
### Environment setup
|
||||
|
||||
Follow the instructions in the [Environment setup](../../README.md#environment-setup) section of the README in the parent directory to set up your environment and install dependencies.
|
||||
|
||||
Run the following command to start the server:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "input" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List all the repositories I own on GitHub."}'
|
||||
```
|
||||
@@ -1,76 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from agent_framework import Agent, MCPStreamableHTTPTool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ToolboxAuth(httpx.Auth):
|
||||
"""httpx Auth that injects a fresh bearer token on every request."""
|
||||
|
||||
def auth_flow(self, request: httpx.Request):
|
||||
credential = AzureCliCredential()
|
||||
token = credential.get_token("https://ai.azure.com/.default").token
|
||||
request.headers["Authorization"] = f"Bearer {token}"
|
||||
yield request
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Foundry Toolbox as a MCP tool
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
toolbox_name = os.environ["TOOLBOX_NAME"]
|
||||
toolbox_endpoint = f"{project_endpoint.rstrip('/')}/toolboxes/{toolbox_name}/mcp?api-version=v1"
|
||||
http_client = httpx.AsyncClient(auth=ToolboxAuth(), headers={"Foundry-Features": "Toolboxes=V1Preview"})
|
||||
foundry_mcp_tool = MCPStreamableHTTPTool(
|
||||
name="toolbox",
|
||||
url=toolbox_endpoint,
|
||||
http_client=http_client,
|
||||
load_prompts=False,
|
||||
)
|
||||
|
||||
# GitHub MCP server
|
||||
github_pat = os.environ["GITHUB_PAT"]
|
||||
if not github_pat:
|
||||
raise ValueError(
|
||||
"GITHUB_PAT environment variable must be set. Create a token at https://github.com/settings/tokens"
|
||||
)
|
||||
|
||||
github_mcp_tool = client.get_mcp_tool(
|
||||
name="GitHub",
|
||||
url="https://api.githubcopilot.com/mcp/",
|
||||
headers={
|
||||
"Authorization": f"Bearer {github_pat}",
|
||||
},
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
tools=[foundry_mcp_tool, github_mcp_tool],
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(agent)
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
TOOLBOX_NAME="..."
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that uses **Foundry Toolbox** for tool discovery and hosted using the **Responses protocol**. Foundry Toolbox is a managed tool registry in Microsoft Foundry that lets you define tools centrally and share them across agents.
|
||||
|
||||
## Creating a Foundry Toolbox
|
||||
|
||||
You can create a Foundry Toolbox by code. Refer to this sample for an example: [Foundry Toolbox CRUD Sample](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolboxes_crud.py).
|
||||
|
||||
You can also create a Foundry Toolbox in the Foundry portal. Read more about it [in the Foundry toolbox documentation](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox).
|
||||
|
||||
> If you set up a project with this sample and provision the resources using `azd provision`, a Foundry Toolbox will be created with the specified tools in [`agent.manifest.yaml`](agent.manifest.yaml).
|
||||
|
||||
## How It Works
|
||||
|
||||
### Model Integration
|
||||
|
||||
The agent uses `FoundryChatClient` from the Agent Framework to create an OpenAI-compatible Responses client. It loads a named Foundry Toolbox via `client.get_toolbox(name)` — the toolbox is a server-side bundle of tool configurations (e.g., `code_interpreter`, `web_search`) defined in the Foundry portal or by `azd provision`. Omitting `version` resolves the toolbox's current default version at runtime.
|
||||
|
||||
The sample then narrows the toolbox to a subset of tool types via `select_toolbox_tools(toolbox, include_types=[...])` before handing it to the agent. This demonstrates how one toolbox can be reused across agents that each expose only the tools they need — here, the agent only sees `code_interpreter` even though the toolbox also includes `web_search`.
|
||||
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What tools do you have?"}'
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
name: agent-framework-agent-with-foundry-toolbox-responses
|
||||
description: >
|
||||
An Agent Framework agent with Foundry Toolbox integration.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-with-foundry-toolbox-responses
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: TOOLBOX_NAME
|
||||
value: "agent-tools"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
- kind: toolbox
|
||||
name: agent-tools
|
||||
tools:
|
||||
- type: web_search
|
||||
name: web_search
|
||||
- type: code_interpreter
|
||||
name: code_interpreter
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
kind: hosted
|
||||
name: agent-framework-agent-with-remote-mcp-tools
|
||||
name: agent-framework-agent-with-foundry-toolbox-responses
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
# Load the named toolbox from the Foundry project. Omitting `version`
|
||||
# resolves the toolbox's current default version at runtime.
|
||||
toolbox = await client.get_toolbox(os.environ["TOOLBOX_NAME"])
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
tools=toolbox,
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(agent)
|
||||
await server.run_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
@@ -1,2 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,23 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API and a workflow
|
||||
|
||||
This sample demonstrates how to host a workflow using the `responses` API.
|
||||
|
||||
## Running the server locally
|
||||
|
||||
### Environment setup
|
||||
|
||||
Follow the instructions in the [Environment setup](../../README.md#environment-setup) section of the README in the parent directory to set up your environment and install dependencies.
|
||||
|
||||
Run the following command to start the server:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "input" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive."}'
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
@@ -0,0 +1,2 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . user_agent/
|
||||
WORKDIR /app/user_agent
|
||||
|
||||
RUN if [ -f requirements.txt ]; then \
|
||||
pip install -r requirements.txt; \
|
||||
else \
|
||||
echo "No requirements.txt found"; \
|
||||
fi
|
||||
|
||||
EXPOSE 8088
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
@@ -0,0 +1,43 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) workflow demonstrating **multi-agent chaining** and hosted using the **Responses protocol**. It shows how to use the Agent Framework's `WorkflowBuilder` to compose a pipeline of specialized agents — a slogan writer, a legal reviewer, and a formatter — that process a request sequentially. Each agent receives only the output of the previous agent, and only the final formatted result is returned to the caller.
|
||||
|
||||
> The workflow will be used as an agent. Read more about Agent Framework workflows in the [Agent Framework documentation](https://learn.microsoft.com/en-us/agent-framework/workflows/) and workflow as an agent in the [Workflow as an Agent documentation](https://learn.microsoft.com/en-us/agent-framework/workflows/as-agents?pivots=programming-language-python).
|
||||
|
||||
> This sample requires a more advanced model because the model needs to continue the conversation from an assistant message. Not all models perform well in this scenario. Tested with OpenAI's model `gpt-5.4`.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Model Integration
|
||||
|
||||
The agent creates three specialized `Agent` instances sharing the same `FoundryChatClient`: a **writer** that generates slogans, a **legal reviewer** that ensures compliance, and a **formatter** that styles the output. Each agent is wrapped in an `AgentExecutor` with `context_mode="last_agent"` so it only sees the previous agent's output. The `WorkflowBuilder` wires them into a linear pipeline and limits the output to the formatter's result.
|
||||
|
||||
See [main.py](main.py) for the full implementation.
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The workflow is exposed as a single agent via `.as_agent()` and hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
## Running the Agent Host
|
||||
|
||||
Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
> Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent.
|
||||
|
||||
Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive."}'
|
||||
```
|
||||
|
||||
Invoke with `azd`:
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Create a slogan for a new electric SUV that is affordable and fun to drive."
|
||||
```
|
||||
|
||||
## Deploying the Agent to Foundry
|
||||
|
||||
To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory.
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
name: agent-framework-workflows
|
||||
name: agent-framework-workflows-responses
|
||||
description: >
|
||||
An Agent Framework workflow hosted by Foundry.
|
||||
metadata:
|
||||
@@ -9,15 +9,15 @@ metadata:
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-workflows
|
||||
name: agent-framework-workflows-responses
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
id: gpt-5.4
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
kind: hosted
|
||||
name: agent-framework-workflows
|
||||
name: agent-framework-workflows-responses
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
+3
-3
@@ -5,7 +5,7 @@ import os
|
||||
from agent_framework import Agent, AgentExecutor, WorkflowBuilder
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
@@ -15,8 +15,8 @@ load_dotenv()
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=DefaultAzureCredential(),
|
||||
)
|
||||
|
||||
writer_agent = Agent(
|
||||
@@ -1,11 +0,0 @@
|
||||
# Hosting agents with Foundry Hosting and the `responses` API
|
||||
|
||||
This folder contains a list of samples that show how to host agents using the `responses` API and deploy them to Foundry Hosting.
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [01_basic](./01_basic) | A basic example of hosting an agent with the `responses` API and carrying on a multi-turn conversation. |
|
||||
| [02_local_tools](./02_local_tools) | An example of hosting an agent with the `responses` API and local tools including a function tool and a local shell tool. |
|
||||
| [03_remote_mcp](./03_remote_mcp) | An example of hosting an agent with the `responses` API and remote MCPs, including a GitHub MCP server and a Foundry Toolbox. |
|
||||
| [04_workflows](./04_workflows) | An example of hosting a workflow with the `responses` API. |
|
||||
| [using_deployed_agent.py](./using_deployed_agent.py) | Connect to the deployed basic Foundry agent with `FoundryAgent`, `allow_preview=True`, and version `v2`. |
|
||||
Reference in New Issue
Block a user