chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
"""
Hello Agent — Simplest possible agent
This sample creates a minimal agent using FoundryChatClient via an
Azure AI Foundry project endpoint, and runs it in both non-streaming and streaming modes.
There are XML tags in all of the get started samples, those are used to display the same code in the docs repo.
"""
async def main() -> None:
# <create_agent>
client = FoundryChatClient(
project_endpoint="https://your-project.services.ai.azure.com",
model="gpt-4o",
credential=AzureCliCredential(),
)
agent = Agent(
client=client,
name="HelloAgent",
instructions="You are a friendly assistant. Keep your answers brief.",
)
# </create_agent>
# <run_agent>
# Non-streaming: get the complete response at once
result = await agent.run("What is the capital of France?")
print(f"Agent: {result}")
# </run_agent>
# <run_agent_streaming>
# Streaming: receive tokens as they are generated
print("Agent (streaming): ", end="", flush=True)
async for chunk in agent.run("Tell me a one-sentence fun fact.", stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print()
# </run_agent_streaming>
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from pydantic import Field
"""
Add Tools — Give your agent a function tool
This sample shows how to define a function tool with the @tool decorator
and wire it into an agent so the model can call it.
"""
# <define_tool>
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production for user confirmation before tool execution.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
# </define_tool>
async def main() -> None:
client = FoundryChatClient(
project_endpoint="https://your-project.services.ai.azure.com",
model="gpt-4o",
credential=AzureCliCredential(),
)
# <create_agent_with_tools>
agent = Agent(
client=client,
name="WeatherAgent",
instructions="You are a helpful weather agent. Use the get_weather tool to answer questions.",
tools=[get_weather],
)
# </create_agent_with_tools>
# <run_agent>
result = await agent.run("What's the weather like in Seattle?")
print(f"Agent: {result}")
# </run_agent>
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,47 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
"""
Multi-Turn Conversations — Use AgentSession to maintain context
This sample shows how to keep conversation history across multiple calls
by reusing the same session object.
"""
async def main() -> None:
# <create_agent>
client = FoundryChatClient(
project_endpoint="https://your-project.services.ai.azure.com",
model="gpt-4o",
credential=AzureCliCredential(),
)
agent = Agent(
client=client,
name="ConversationAgent",
instructions="You are a friendly assistant. Keep your answers brief.",
)
# </create_agent>
# <multi_turn>
# Create a session to maintain conversation history
session = agent.create_session()
# First turn
result = await agent.run("My name is Alice and I love hiking.", session=session)
print(f"Agent: {result}\n")
# Second turn — the agent should remember the user's name and hobby
result = await agent.run("What do you remember about me?", session=session)
print(f"Agent: {result}")
# </multi_turn>
if __name__ == "__main__":
asyncio.run(main())
+105
View File
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
from agent_framework import Agent, AgentSession, ContextProvider, SessionContext
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
"""
Agent Memory with Context Providers and Session State
Context providers inject dynamic context into each agent call. This sample
shows a provider that stores the user's name in session state and personalizes
responses — the name persists across turns via the session.
"""
# <context_provider>
class UserMemoryProvider(ContextProvider):
"""A context provider that remembers user info in session state."""
DEFAULT_SOURCE_ID = "user_memory"
def __init__(self):
super().__init__(self.DEFAULT_SOURCE_ID)
async def before_run(
self,
*,
agent: Any,
session: AgentSession | None,
context: SessionContext,
state: dict[str, Any],
) -> None:
"""Inject personalization instructions based on stored user info."""
user_name = state.get("user_name")
if user_name:
context.extend_instructions(
self.source_id,
f"The user's name is {user_name}. Always address them by name.",
)
else:
context.extend_instructions(
self.source_id,
"You don't know the user's name yet. Ask for it politely.",
)
async def after_run(
self,
*,
agent: Any,
session: AgentSession | None,
context: SessionContext,
state: dict[str, Any],
) -> None:
"""Extract and store user info in session state after each call."""
for msg in context.input_messages:
text = msg.text if hasattr(msg, "text") else ""
if isinstance(text, str) and "my name is" in text.lower():
state["user_name"] = text.lower().split("my name is")[-1].strip().split()[0].capitalize()
# </context_provider>
async def main() -> None:
# <create_agent>
client = FoundryChatClient(
project_endpoint="https://your-project.services.ai.azure.com",
model="gpt-4o",
credential=AzureCliCredential(),
)
agent = Agent(
client=client,
name="MemoryAgent",
instructions="You are a friendly assistant.",
context_providers=[UserMemoryProvider()],
)
# </create_agent>
# <run_with_memory>
session = agent.create_session()
# The provider doesn't know the user yet — it will ask for a name
result = await agent.run("Hello! What's the square root of 9?", session=session)
print(f"Agent: {result}\n")
# Now provide the name — the provider stores it in session state
result = await agent.run("My name is Alice", session=session)
print(f"Agent: {result}\n")
# Subsequent calls are personalized — name persists via session state
result = await agent.run("What is 2 + 2?", session=session)
print(f"Agent: {result}\n")
# Inspect session state to see what the provider stored
provider_state = session.state.get("user_memory", {})
print(f"[Session State] Stored user name: {provider_state.get('user_name')}")
# </run_with_memory>
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Functional Workflow with Agents — Call agents inside @workflow
This sample shows how to call agents inside a functional workflow.
Agent calls are just regular async function calls — no special wrappers needed.
"""
import asyncio
from agent_framework import Agent, workflow
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file (e.g., FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL)
load_dotenv()
# <create_agents>
client = FoundryChatClient(credential=AzureCliCredential())
writer = Agent(
name="WriterAgent",
instructions="Write a short poem (4 lines max) about the given topic.",
client=client,
)
reviewer = Agent(
name="ReviewerAgent",
instructions="Review the given poem in one sentence. Is it good?",
client=client,
)
# </create_agents>
# <create_workflow>
@workflow
async def poem_workflow(topic: str) -> str:
"""Write a poem, then review it."""
poem = (await writer.run(f"Write a poem about: {topic}")).text
review = (await reviewer.run(f"Review this poem: {poem}")).text
return f"Poem:\n{poem}\n\nReview: {review}"
# </create_workflow>
async def main() -> None:
result = await poem_workflow.run("a cat learning to code")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Functional Workflow Basics — Orchestrate async functions with @workflow
The functional API lets you write workflows as plain Python async functions.
No graph concepts, no edges, no executor classes — just call functions
and use native control flow (if/else, loops, asyncio.gather).
This sample builds a minimal pipeline with two steps:
1. Convert text to uppercase
2. Reverse the text
No external services are required.
"""
import asyncio
from agent_framework import workflow
# Plain async functions — no decorators needed
async def to_upper_case(text: str) -> str:
"""Convert input to uppercase."""
return text.upper()
async def reverse_text(text: str) -> str:
"""Reverse the string."""
return text[::-1]
# <create_workflow>
@workflow
async def text_workflow(text: str) -> str:
"""Uppercase the text, then reverse it."""
upper = await to_upper_case(text)
return await reverse_text(upper)
# </create_workflow>
async def main() -> None:
# <run_workflow>
result = await text_workflow.run("hello world")
print(f"Output: {result.get_outputs()}")
print(f"Final state: {result.get_final_state()}")
# </run_workflow>
"""
Expected output:
Output: ['DLROW OLLEH']
Final state: WorkflowRunState.IDLE
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,73 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
executor,
handler,
)
from typing_extensions import Never
"""
First Graph Workflow — Chain executors with edges
The graph API gives you full control over execution topology: edges,
fan-out/fan-in, switch/case, and superstep-based checkpointing.
This sample builds a minimal graph workflow with two steps:
1. Convert text to uppercase (class-based executor)
2. Reverse the text (function-based executor)
No external services are required.
"""
# <create_workflow>
# Step 1: A class-based executor that converts text to uppercase
class UpperCase(Executor):
def __init__(self, id: str):
super().__init__(id=id)
@handler
async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None:
"""Convert input to uppercase and forward to the next node."""
await ctx.send_message(text.upper())
# Step 2: A function-based executor that reverses the string and yields output
@executor(id="reverse_text")
async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None:
"""Reverse the string and yield the final workflow output."""
await ctx.yield_output(text[::-1])
def create_workflow():
"""Build the workflow: UpperCase → reverse_text."""
upper = UpperCase(id="upper_case")
return WorkflowBuilder(start_executor=upper).add_edge(upper, reverse_text).build()
# </create_workflow>
async def main() -> None:
# <run_workflow>
workflow = create_workflow()
events = await workflow.run("hello world")
print(f"Output: {events.get_outputs()}")
print(f"Final state: {events.get_final_state()}")
# </run_workflow>
"""
Expected output:
Output: ['DLROW OLLEH']
Final state: WorkflowRunState.IDLE
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: E305
# fmt: off
from typing import Any
from agent_framework import Agent
from agent_framework.azure import AgentFunctionApp
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
"""Host your agent with Azure Functions.
This sample shows the Python hosting pattern used in docs:
- Create an agent with `FoundryChatClient`
- Register it with `AgentFunctionApp`
- Run with Azure Functions Core Tools (`func start`)
Prerequisites:
pip install agent-framework-azurefunctions --pre
"""
# <create_agent>
def _create_agent() -> Any:
"""Create a hosted agent backed by Azure OpenAI."""
return Agent(
client=FoundryChatClient(
project_endpoint="https://your-project.services.ai.azure.com",
model="gpt-4o",
credential=AzureCliCredential(),
),
name="HostedAgent",
instructions="You are a helpful assistant hosted in Azure Functions.",
)
# </create_agent>
# <host_agent>
app = AgentFunctionApp(agents=[_create_agent()], enable_health_check=True, max_poll_retries=50)
# </host_agent>
if __name__ == "__main__":
print("Start the Functions host with: func start")
print("Then call: POST /api/agents/HostedAgent/run")
+38
View File
@@ -0,0 +1,38 @@
# Get Started with Agent Framework for Python
This folder contains a progressive set of samples that introduce the core
concepts of **Agent Framework** one step at a time.
## Prerequisites
```bash
pip install agent-framework
```
Set the required environment variables:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project-endpoint"
export FOUNDRY_MODEL="gpt-4o" # optional, defaults to gpt-4o
```
## Samples
| # | File | What you'll learn |
|---|------|-------------------|
| 1 | [01_hello_agent.py](01_hello_agent.py) | Create your first agent and run it (streaming and non-streaming). |
| 2 | [02_add_tools.py](02_add_tools.py) | Define a function tool with `@tool` and attach it to an agent. |
| 3 | [03_multi_turn.py](03_multi_turn.py) | Keep conversation history across turns with `AgentSession`. |
| 4 | [04_memory.py](04_memory.py) | Add dynamic context with a custom `ContextProvider`. |
| 5 | [05_functional_workflow_with_agents.py](05_functional_workflow_with_agents.py) | Call agents inside a functional workflow. |
| 6 | [06_functional_workflow_basics.py](06_functional_workflow_basics.py) | Write a workflow as a plain async function. |
| 7 | [07_first_graph_workflow.py](07_first_graph_workflow.py) | Chain executors into a graph workflow with edges. |
| 8 | [08_host_your_agent.py](08_host_your_agent.py) | Host a single agent with Azure Functions. |
Run any sample with:
```bash
python 01_hello_agent.py
```
These samples use Azure Foundry models with the Responses API. To switch providers, just replace the client, see [all providers](../02-agents/providers/README.md)
+54
View File
@@ -0,0 +1,54 @@
# A2A Client Samples
These samples demonstrate how to **consume** remote A2A-compliant agents using the Agent Framework's `A2AAgent` class.
For hosting your own agents as A2A servers, see [`samples/04-hosting/a2a/`](../../04-hosting/a2a/).
## Samples
| Sample | Concept |
|--------|---------|
| [`agent_with_a2a.py`](agent_with_a2a.py) | Basic consumption — non-streaming and streaming |
| [`a2a_agent_as_function_tools.py`](a2a_agent_as_function_tools.py) | Expose A2A skills as function tools for a host agent |
| [`a2a_polling.py`](a2a_polling.py) | Poll a long-running task with continuation tokens |
| [`a2a_stream_reconnection.py`](a2a_stream_reconnection.py) | Resume an interrupted stream via continuation token |
| [`a2a_protocol_selection.py`](a2a_protocol_selection.py) | Configure preferred protocol bindings (JSONRPC, GRPC, HTTP+JSON) |
## Prerequisites
- A running A2A-compliant agent server (see `samples/04-hosting/a2a/` to start one)
- Set `A2A_AGENT_HOST` environment variable to the server URL
- For `a2a_agent_as_function_tools.py`: also set `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL`
## Running
```bash
cd python/samples/02-agents/a2a
# Start an A2A server in another terminal first:
# cd python/samples/04-hosting/a2a && uv run python a2a_server.py
export A2A_AGENT_HOST="http://localhost:5001/"
uv run python agent_with_a2a.py
```
## Key APIs
```python
from agent_framework.a2a import A2AAgent
# Connect to a remote agent
async with A2AAgent(url="http://localhost:5001/", agent_card=card) as agent:
# Non-streaming
response = await agent.run("Hello")
# Streaming
stream = agent.run("Hello", stream=True)
async for update in stream:
print(update.text)
# Background + polling
response = await agent.run("Long task", background=True)
while response.continuation_token:
response = await agent.poll_task(response.continuation_token)
```
@@ -0,0 +1,152 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import re
import httpx
from a2a.client import A2ACardResolver
from agent_framework import Agent
from agent_framework.a2a import A2AAgent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
A2A Agent Skills as Function Tools
This sample demonstrates how to represent an A2A agent's skills as individual
function tools and register them with a host agent. Each skill advertised in the
remote agent's AgentCard becomes a separate tool that the host agent can invoke.
Key concepts demonstrated:
- Resolving an AgentCard from a remote A2A endpoint
- Converting each skill into a FunctionTool via as_tool()
- Registering those tools with a host agent
- Having the host agent autonomously select and invoke A2A skills
Prerequisites:
- Set A2A_AGENT_HOST to the URL of a running A2A server
- Set FOUNDRY_PROJECT_ENDPOINT to your Azure AI Foundry project endpoint
- Set FOUNDRY_MODEL to the model deployment name (e.g. gpt-4o)
To run this sample:
cd python/samples/02-agents/a2a
uv run python a2a_agent_as_function_tools.py
"""
async def main() -> None:
"""Discover A2A agent skills and register them as tools on a host agent."""
# 1. Read environment configuration.
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
if not project_endpoint or not model:
raise ValueError("FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL must be set")
print(f"Connecting to A2A agent at: {a2a_agent_host}")
# 2. Resolve the remote agent card to discover its skills.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
print(f"Found agent: {agent_card.name} ({len(agent_card.skills)} skill(s))")
for skill in agent_card.skills:
print(f" - {skill.name}: {skill.description}")
# 3. Create the A2AAgent that wraps the remote endpoint.
async with A2AAgent(
name=agent_card.name,
description=agent_card.description,
agent_card=agent_card,
url=a2a_agent_host,
) as a2a_agent:
# 4. Convert each A2A skill into a FunctionTool.
# Skill names may contain spaces or special characters, so we
# sanitize them into valid tool identifiers before passing to as_tool().
skill_tools = [
a2a_agent.as_tool(
name=re.sub(r"[^0-9A-Za-z]+", "_", skill.name),
description=skill.description or "",
)
for skill in agent_card.skills
]
# 5. Create the host agent with the skill tools.
credential = AzureCliCredential()
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
)
host_agent = Agent(
client=client,
name="assistant",
instructions="You are a helpful assistant. Use your tools to answer questions.",
tools=skill_tools,
)
# 6. Run the host agent — it will select and invoke the appropriate A2A skill tools.
query = "Show me all invoices for Contoso"
print(f"\nUser: {query}\n")
response = await host_agent.run(query)
print(f"Agent: {response}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Connecting to A2A agent at: http://localhost:5000/
Found agent: InvoiceAgent (1 skill(s))
- InvoiceQuery: Handles requests relating to invoices.
User: Show me all invoices for Contoso
Agent: Here are the invoices for Contoso:
1. **Invoice ID:** INV789
- **Date:** 2026-02-15
- **Products:**
- T-Shirts: 150 units @ $10.00 = $1,500.00
- Hats: 200 units @ $15.00 = $3,000.00
- Glasses: 300 units @ $5.00 = $1,500.00
- **Total:** $6,000.00
2. **Invoice ID:** INV333
- **Date:** 2026-03-14
- **Products:**
- T-Shirts: 400 units @ $11.00 = $4,400.00
- Hats: 600 units @ $15.00 = $9,000.00
- Glasses: 700 units @ $5.00 = $3,500.00
- **Total:** $16,900.00
3. **Invoice ID:** INV666
- **Date:** 2026-02-06
- **Products:**
- T-Shirts: 2,500 units @ $8.00 = $20,000.00
- Hats: 1,200 units @ $10.00 = $12,000.00
- Glasses: 1,000 units @ $6.00 = $6,000.00
- **Total:** $38,000.00
4. **Invoice ID:** INV999
- **Date:** 2026-03-19
- **Products:**
- T-Shirts: 1,400 units @ $10.50 = $14,700.00
- Hats: 1,100 units @ $9.00 = $9,900.00
- Glasses: 950 units @ $12.00 = $11,400.00
- **Total:** $36,000.00
If you need more details or a specific invoice, please let me know!
"""
@@ -0,0 +1,96 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import httpx
from a2a.client import A2ACardResolver
from agent_framework.a2a import A2AAgent
from dotenv import load_dotenv
load_dotenv()
"""
A2A Polling for Task Completion
This sample demonstrates how to poll a long-running A2A task for completion
using continuation tokens. When `background=True`, the agent returns immediately
with a continuation token that you can use to check progress later.
Key concepts demonstrated:
- Starting a background A2A task with `background=True`
- Receiving a continuation token for in-progress tasks
- Polling with `poll_task()` until the task reaches a terminal state
This is the A2A equivalent of the .NET A2AAgent_PollingForTaskCompletion sample.
Prerequisites:
- Set A2A_AGENT_HOST to the URL of a running A2A server
To run this sample:
cd python/samples/02-agents/a2a
uv run python a2a_polling.py
"""
async def main() -> None:
"""Demonstrates polling a long-running A2A task for completion."""
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
# 1. Resolve agent card and create agent.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
async with A2AAgent(
name=agent_card.name,
agent_card=agent_card,
url=a2a_agent_host,
) as agent:
# 2. Start a background task — the agent returns immediately.
print("Starting background task...")
response = await agent.run(
"Write a detailed research report on quantum computing advances in 2025",
background=True,
)
# 3. Check if we got a continuation token (task still in progress).
if response.continuation_token is None:
# Task completed immediately — no polling needed.
print("Task completed immediately:")
print(f" {response.text}")
return
# 4. Poll until the task completes.
token = response.continuation_token
poll_count = 0
while token is not None:
poll_count += 1
print(f" Poll #{poll_count} — task still in progress, waiting 2s...")
await asyncio.sleep(2)
response = await agent.poll_task(token) # type: ignore[arg-type]
token = response.continuation_token
# 5. Task is done — print the final response.
print(f"\nTask completed after {poll_count} poll(s):")
print(f" {response.text[:200]}...")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Starting background task...
Poll #1 — task still in progress, waiting 2s...
Poll #2 — task still in progress, waiting 2s...
Poll #3 — task still in progress, waiting 2s...
Task completed after 3 poll(s):
Quantum computing has seen remarkable progress in 2025, with breakthroughs in...
"""
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import httpx
from a2a.client import A2ACardResolver
from agent_framework.a2a import A2AAgent
from dotenv import load_dotenv
load_dotenv()
"""
A2A Protocol Selection
This sample demonstrates how to configure which protocol binding the A2A client
uses when connecting to a remote agent. The A2A specification defines three
standard bindings: JSONRPC, GRPC, and HTTP+JSON. Agents declare their supported
bindings in their AgentCard, and clients can express a preference.
Key concepts demonstrated:
- Configuring `supported_protocol_bindings` on A2AAgent
- The client selects a binding that matches the remote agent's capabilities
- Fallback behavior when preferred binding is unavailable
This is the A2A equivalent of the .NET A2AAgent_ProtocolSelection sample.
Prerequisites:
- Set A2A_AGENT_HOST to the URL of a running A2A server
To run this sample:
cd python/samples/02-agents/a2a
uv run python a2a_protocol_selection.py
"""
async def main() -> None:
"""Demonstrates configuring A2A protocol binding preferences."""
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
# 1. Resolve agent card to see what bindings are available.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
print(f"Agent: {agent_card.name}")
print("Supported interfaces:")
for interface in agent_card.supported_interfaces:
print(f" - {interface.protocol_binding} @ {interface.url}")
# 2. Create agent with explicit protocol binding preference.
# The list is ordered by preference — the SDK will select the first
# binding that matches a supported interface on the agent card.
#
# This matters when a server exposes multiple interfaces (e.g. JSONRPC
# on / and HTTP+JSON on /api/). If only one binding is available, the
# client uses it regardless of your preference list.
async with A2AAgent(
name=agent_card.name,
agent_card=agent_card,
url=a2a_agent_host,
supported_protocol_bindings=["HTTP+JSON", "JSONRPC"],
) as agent:
print("\nConfigured bindings: ['HTTP+JSON', 'JSONRPC']")
response = await agent.run("Tell me a short joke")
print(f"Response: {response.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Agent: PolicyAgent
Supported interfaces:
- JSONRPC @ http://localhost:5001/
Configured bindings: ['HTTP+JSON', 'JSONRPC']
Response: Here's a short joke for you...
"""
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
import httpx
from a2a.client import A2ACardResolver
from agent_framework.a2a import A2AAgent
from agent_framework_a2a import A2AContinuationToken
from dotenv import load_dotenv
load_dotenv()
"""
A2A Stream Reconnection
This sample demonstrates how to reconnect to an interrupted A2A stream
using a continuation token. When streaming a long-running task, you can
capture the continuation token from any update and use it to resume the
stream later if the connection is lost.
Key concepts demonstrated:
- Streaming an A2A response with `stream=True`
- Capturing continuation tokens from in-progress updates
- Simulating a stream interruption (break)
- Resuming the stream with `run(continuation_token=..., stream=True)`
This is the A2A equivalent of the .NET A2AAgent_StreamReconnection sample.
Prerequisites:
- Set A2A_AGENT_HOST to the URL of a running A2A server
To run this sample:
cd python/samples/02-agents/a2a
uv run python a2a_stream_reconnection.py
"""
async def main() -> None:
"""Demonstrates reconnecting to an interrupted A2A stream."""
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
# 1. Resolve agent card and create agent.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
async with A2AAgent(
name=agent_card.name,
agent_card=agent_card,
url=a2a_agent_host,
) as agent:
# 2. Start a streaming background task.
print("Starting streaming task...")
stream = agent.run(
"Write a long essay about the history of artificial intelligence",
stream=True,
background=True,
)
# 3. Read a few updates, capture the continuation token, then "disconnect".
saved_token = None
update_count = 0
async for update in stream:
update_count += 1
if update.continuation_token:
saved_token = update.continuation_token
for content in update.contents:
if content.text:
print(content.text, end="", flush=True)
# Simulate a disconnect after receiving 3 updates.
if update_count >= 3:
print("\n\n--- Connection interrupted! ---\n")
break
if saved_token is None:
print("No continuation token received — task may have completed before interruption.")
return
# 4. Reconnect using the saved continuation token.
# background=True is required so that in-progress task updates
# surface continuation tokens (matching the A2AAgent contract).
print("Reconnecting with continuation token...")
resumed_stream = agent.run(
continuation_token=cast(A2AContinuationToken, saved_token),
stream=True,
background=True,
)
# 5. Continue receiving updates from where we left off.
async for update in resumed_stream:
update_count += 1
for content in update.contents:
if content.text:
print(content.text, end="", flush=True)
print() # newline after streaming completes
response = await resumed_stream.get_final_response()
print(f"\nStream completed. Total updates: {update_count}")
print(f"Final response: {len(response.messages)} message(s)")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Starting streaming task...
Policy:
--- Connection interrupted! ---
Reconnecting with continuation token (task_id=task-abc123)...
Short Shipment Dispute Handling Policy V2.1
Summary: "For short shipments reported by customers, first verify internal..."
Stream completed. Total updates: 106
Final response: 103 message(s)
"""
@@ -0,0 +1,109 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import httpx
from a2a.client import A2ACardResolver
from agent_framework.a2a import A2AAgent
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Agent2Agent (A2A) Protocol Integration Sample
This sample demonstrates how to connect to and communicate with external agents using
the A2A protocol. A2A is a standardized communication protocol that enables interoperability
between different agent systems, allowing agents built with different frameworks and
technologies to communicate seamlessly.
By default the A2AAgent waits for the remote agent to finish before returning (background=False).
This means long-running A2A tasks are handled transparently — the caller simply awaits the result.
For advanced scenarios where you need to poll or resubscribe to in-progress tasks, see the
a2a_polling and a2a_stream_reconnection samples in this folder.
For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/
Key concepts demonstrated:
- Discovering A2A-compliant agents using AgentCard resolution
- Creating A2AAgent instances to wrap external A2A endpoints
- Non-streaming request/response
- Streaming responses to receive incremental updates via SSE
To run this sample:
1. Set the A2A_AGENT_HOST environment variable to point to an A2A-compliant agent endpoint
Example: export A2A_AGENT_HOST="https://your-a2a-agent.example.com"
2. Ensure the target agent exposes its AgentCard at /.well-known/agent.json
3. Run: uv run python agent_with_a2a.py
Visit the README.md for more details on setting up and running A2A agents.
"""
async def main():
"""Demonstrates connecting to and communicating with an A2A-compliant agent."""
# 1. Get A2A agent host from environment.
a2a_agent_host = os.getenv("A2A_AGENT_HOST")
if not a2a_agent_host:
raise ValueError("A2A_AGENT_HOST environment variable is not set")
print(f"Connecting to A2A agent at: {a2a_agent_host}")
# 2. Resolve the agent card to discover capabilities.
async with httpx.AsyncClient(timeout=60.0) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host)
agent_card = await resolver.get_agent_card()
print(f"Found agent: {agent_card.name} - {agent_card.description}")
# 3. Create A2A agent instance.
async with A2AAgent(
name=agent_card.name,
description=agent_card.description,
agent_card=agent_card,
url=a2a_agent_host,
) as agent:
# 4. Simple request/response — the agent waits for completion internally.
# Even if the remote agent takes a while, background=False (the default)
# means the call blocks until a terminal state is reached.
print("\n--- Non-streaming response ---")
response = await agent.run("What are your capabilities?")
print(f"Agent Response:\n {response.text}")
# 5. Stream a response — the natural model for A2A.
# Updates arrive as Server-Sent Events, letting you observe
# progress in real time as the remote agent works.
print("\n--- Streaming response ---")
stream = agent.run("Tell me about yourself", stream=True)
async for update in stream:
for content in update.contents:
if content.text:
print(content.text, end="", flush=True)
print() # newline after streaming completes
response = await stream.get_final_response()
print(f"\nFinal response:\n {response.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Connecting to A2A agent at: http://localhost:5001/
Found agent: MyAgent - A helpful AI assistant
--- Non-streaming response ---
Agent Response:
I can help with code generation, analysis, and general Q&A.
--- Streaming response ---
I am an AI assistant built to help with various tasks.
Final response:
I am an AI assistant built to help with various tasks.
"""
+255
View File
@@ -0,0 +1,255 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "agent-framework-foundry",
# "tenacity",
# ]
# ///
# Run with any PEP 723 compatible runner, e.g.:
# uv run samples/02-agents/auto_retry.py
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from collections.abc import Awaitable, Callable
from typing import Any, TypeVar, cast
from agent_framework import Agent, ChatContext, ChatMiddleware, SupportsChatGetResponse, chat_middleware
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from openai import RateLimitError
from tenacity import (
AsyncRetrying,
before_sleep_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
# Load environment variables from .env file
load_dotenv()
"""
Auto-Retry Rate Limiting Sample
Every model inference API enforces rate limits, so production agents need retry logic
to handle 429 responses gracefully. This sample shows two ways to add automatic retry
using the `tenacity` library, keeping your application code free of boilerplate.
Approach 1 Class decorator
Apply a class decorator to any client type implementing
SupportsChatGetResponse. The decorator patches get_response() with retry
behavior. Non-streaming responses are retried; streaming is returned as-is
(streaming retry requires more delicate handling).
Approach 2 Chat middleware
Register middleware on the agent that catches RateLimitError raised inside
call_next() and retries the entire request pipeline. Two styles are shown:
a) Class-based middleware (ChatMiddleware subclass)
b) Function-based middleware (@chat_middleware decorator)
Both approaches use the same tenacity primitives:
- stop_after_attempt cap the total number of tries
- wait_exponential exponential back-off between retries
- retry_if_exception_type(RateLimitError) only retry on 429 errors
- before_sleep_log log each retry attempt at WARNING level
"""
logger = logging.getLogger(__name__)
RETRY_ATTEMPTS = 3
# =============================================================================
# Approach 1: Class decorator
# =============================================================================
ChatClientT = TypeVar("ChatClientT", bound=SupportsChatGetResponse[Any])
def with_rate_limit_retry(*, retry_attempts: int = RETRY_ATTEMPTS) -> Callable[[type[ChatClientT]], type[ChatClientT]]:
"""Class decorator that adds non-streaming retry behavior to get_response()."""
def decorator(client_cls: type[ChatClientT]) -> type[ChatClientT]:
original_get_response = client_cls.get_response
def get_response_with_retry(self, *args, **kwargs): # type: ignore[no-untyped-def]
stream = kwargs.get("stream", False)
if stream:
# Streaming retry is more complex; fall back to the original behaviour.
return original_get_response(self, *args, **kwargs)
async def _with_retry():
async for attempt in AsyncRetrying(
stop=stop_after_attempt(retry_attempts),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(RateLimitError),
reraise=True,
before_sleep=before_sleep_log(logger, logging.WARNING),
):
with attempt:
return await original_get_response(self, *args, **kwargs)
return None
return _with_retry()
client_cls.get_response = cast(Any, get_response_with_retry)
return client_cls
return decorator
@with_rate_limit_retry()
class RetryingFoundryChatClient(FoundryChatClient):
"""Azure OpenAI Chat client with class-decorator-based retry behavior."""
# =============================================================================
# Approach 2a: Class-based chat middleware
# =============================================================================
class RateLimitRetryMiddleware(ChatMiddleware):
"""Chat middleware that retries a single model-call pipeline on rate limit errors.
Register this middleware on an agent (or at the run level) to automatically
retry any chat-model call that raises RateLimitError. In tool-loop scenarios,
the middleware applies independently to each inner model call.
"""
def __init__(self, *, max_attempts: int = RETRY_ATTEMPTS) -> None:
"""Initialize with the maximum number of retry attempts."""
self.max_attempts = max_attempts
async def process(
self,
context: ChatContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Retry call_next() on rate limit errors with exponential back-off."""
async for attempt in AsyncRetrying(
stop=stop_after_attempt(self.max_attempts),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(RateLimitError),
reraise=True,
before_sleep=before_sleep_log(logger, logging.WARNING),
):
with attempt:
await call_next()
# =============================================================================
# Approach 2b: Function-based chat middleware
# =============================================================================
@chat_middleware
async def rate_limit_retry_middleware(
context: ChatContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Function-based chat middleware that retries on rate limit errors.
Wrap call_next() with a tenacity @retry decorator so any RateLimitError
raised during a single model call triggers an automatic retry with exponential
back-off. In tool-loop scenarios, the middleware applies independently to
each inner model call.
"""
@retry(
stop=stop_after_attempt(RETRY_ATTEMPTS),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(RateLimitError),
reraise=True,
before_sleep=before_sleep_log(logger, logging.WARNING),
)
async def _call_next_with_retry() -> None:
await call_next()
await _call_next_with_retry()
# =============================================================================
# Demo
# =============================================================================
async def class_decorator_example() -> None:
"""Demonstrate Approach 1: class decorator on a chat client type."""
print("\n" + "=" * 60)
print("Approach 1: Class decorator (applied to client type)")
print("=" * 60)
# For authentication, run `az login` command in terminal or replace
# AzureCliCredential with your preferred authentication option.
agent = Agent(
client=RetryingFoundryChatClient(credential=AzureCliCredential()),
instructions="You are a helpful assistant.",
)
query = "Say hello!"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
async def class_based_middleware_example() -> None:
"""Demonstrate Approach 2a: class-based chat middleware."""
print("\n" + "=" * 60)
print("Approach 2a: Class-based chat middleware")
print("=" * 60)
# For authentication, run `az login` command in terminal or replace
# AzureCliCredential with your preferred authentication option.
agent = Agent(
client=FoundryChatClient(credential=AzureCliCredential()),
instructions="You are a helpful assistant.",
middleware=[RateLimitRetryMiddleware(max_attempts=3)],
)
query = "Say hello!"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
async def function_based_middleware_example() -> None:
"""Demonstrate Approach 2b: function-based chat middleware."""
print("\n" + "=" * 60)
print("Approach 2b: Function-based chat middleware")
print("=" * 60)
# For authentication, run `az login` command in terminal or replace
# AzureCliCredential with your preferred authentication option.
agent = Agent(
client=FoundryChatClient(credential=AzureCliCredential()),
instructions="You are a helpful assistant.",
middleware=[rate_limit_retry_middleware],
)
query = "Say hello!"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
async def main() -> None:
"""Run all auto-retry examples."""
print("=== Auto-Retry Rate Limiting Sample ===")
print(
"Demonstrates two approaches for automatic retry on rate limit (429) errors.\n"
"Set AZURE_OPENAI_ENDPOINT and FOUNDRY_MODEL (and optionally\n"
"AZURE_OPENAI_API_KEY) before running, or populate a .env file."
)
await class_decorator_example()
await class_based_middleware_example()
await function_based_middleware_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,144 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import cast
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions, OpenAIContinuationToken
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""Background Responses Sample.
This sample demonstrates long-running agent operations using the OpenAI
Responses API ``background`` option. Two patterns are shown:
1. **Non-streaming polling** start a background run, then poll with the
``continuation_token`` until the operation completes.
2. **Streaming with resumption** start a background streaming run, simulate
an interruption, and resume from the last ``continuation_token``.
Prerequisites:
- Set the ``OPENAI_API_KEY`` environment variable.
- A model that benefits from background execution (e.g. ``o3``).
"""
# 1. Create the agent with an OpenAI Responses client.
agent = Agent(
name="researcher",
instructions="You are a helpful research assistant. Be concise.",
client=OpenAIChatClient(model="o3"),
)
async def non_streaming_polling() -> None:
"""Demonstrate non-streaming background run with polling."""
print("=== Non-Streaming Polling ===\n")
session = agent.create_session()
# 2. Start a background run — returns immediately.
response = await agent.run(
messages="Briefly explain the theory of relativity in two sentences.",
session=session,
options=OpenAIChatOptions(background=True),
)
print(f"Initial status: continuation_token={'set' if response.continuation_token else 'None'}")
# 3. Poll until the operation completes.
poll_count = 0
while response.continuation_token is not None:
poll_count += 1
await asyncio.sleep(2)
response = await agent.run(
session=session,
options=OpenAIChatOptions(continuation_token=cast(OpenAIContinuationToken, response.continuation_token)),
)
print(f" Poll {poll_count}: continuation_token={'set' if response.continuation_token else 'None'}")
# 4. Done — print the final result.
print(f"\nResult ({poll_count} poll(s)):\n{response.text}\n")
async def streaming_with_resumption() -> None:
"""Demonstrate streaming background run with simulated interruption and resumption."""
print("=== Streaming with Resumption ===\n")
session = agent.create_session()
# 2. Start a streaming background run.
last_token = None
stream = agent.run(
messages="Briefly list three benefits of exercise.",
stream=True,
session=session,
options=OpenAIChatOptions(background=True),
)
# 3. Read some chunks, then simulate an interruption.
chunk_count = 0
print("First stream (before interruption):")
async for update in stream:
last_token = update.continuation_token
if update.text:
print(update.text, end="", flush=True)
chunk_count += 1
if chunk_count >= 3:
print("\n [simulated interruption]")
break
# 4. Resume from the last continuation token.
if last_token is not None:
print("Resumed stream:")
stream = agent.run(
stream=True,
session=session,
options=OpenAIChatOptions(continuation_token=cast(OpenAIContinuationToken, last_token)),
)
async for update in stream:
if update.text:
print(update.text, end="", flush=True)
print("\n")
async def main() -> None:
await non_streaming_polling()
await streaming_with_resumption()
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== Non-Streaming Polling ===
Initial status: continuation_token=set
Poll 1: continuation_token=set
Poll 2: continuation_token=None
Result (2 poll(s)):
The theory of relativity, developed by Albert Einstein, consists of special
relativity (1905), which shows that the laws of physics are the same for all
non-accelerating observers and that the speed of light is constant, and general
relativity (1915), which describes gravity as the curvature of spacetime caused
by mass and energy.
=== Streaming with Resumption ===
First stream (before interruption):
Here are three
[simulated interruption]
Resumed stream:
key benefits of regular exercise:
1. **Improved cardiovascular health** ...
2. **Better mental health** ...
3. **Stronger muscles and bones** ...
"""
@@ -0,0 +1,78 @@
# Chat Client Examples
This folder contains examples for direct chat client usage patterns.
## Examples
| File | Description |
|------|-------------|
| [`built_in_chat_clients.py`](built_in_chat_clients.py) | Consolidated sample for built-in chat clients. Uses `get_client()` to create the selected client and pass it to `main()`. |
| [`chat_response_cancellation.py`](chat_response_cancellation.py) | Demonstrates how to cancel chat responses during streaming, showing proper cancellation handling and cleanup. |
| [`custom_chat_client.py`](custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `Agent` using the `as_agent()` method. |
| [`require_per_service_call_history_persistence.py`](require_per_service_call_history_persistence.py) | Compares two otherwise identical `FoundryChatClient` agents with `store=False`; the only difference is whether `require_per_service_call_history_persistence` is enabled, and only the run without it stores the synthesized tool result when middleware terminates the loop early. |
## Selecting a built-in client
`built_in_chat_clients.py` starts with:
```python
asyncio.run(main("openai_responses"))
```
Change the argument to pick a client:
- `openai_responses`
- `openai_chat_completion`
- `anthropic`
- `ollama`
- `bedrock`
- `azure_openai_responses`
- `azure_openai_chat_completion`
- `foundry_chat`
Example:
```bash
uv run samples/02-agents/chat_client/built_in_chat_clients.py
```
The `require_per_service_call_history_persistence.py` sample uses `FoundryChatClient`, so set the usual Foundry settings first and sign in with the Azure CLI:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://<your-project>.services.ai.azure.com/api/projects/<project-name>"
export FOUNDRY_MODEL="<your-model-deployment-name>"
az login
uv run samples/02-agents/chat_client/require_per_service_call_history_persistence.py
```
## Environment Variables
Depending on the selected client, set the appropriate environment variables:
**For Azure OpenAI clients (`azure_openai_responses` and `azure_openai_chat_completion`):**
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint
- `AZURE_OPENAI_MODEL`: The Azure OpenAI deployment used by the sample
- `AZURE_OPENAI_API_VERSION` (optional): Azure OpenAI API version override
- `AZURE_OPENAI_API_KEY` (optional): Azure OpenAI API key if you are not using `AzureCliCredential`
**For Foundry client (`foundry_chat`):**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: The Foundry deployment used by the sample
**For OpenAI clients:**
- `OPENAI_API_KEY`: Your OpenAI API key
- `OPENAI_CHAT_COMPLETION_MODEL`: The OpenAI model for `openai_chat_completion`
- `OPENAI_CHAT_MODEL`: The OpenAI model for `openai_responses`
**For Anthropic client (`anthropic`):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key
- `ANTHROPIC_CHAT_MODEL`: The Anthropic model to use (for example, `claude-sonnet-4-5`)
**For Ollama client (`ollama`):**
- `OLLAMA_HOST`: Ollama server URL (defaults to `http://localhost:11434` if unset)
- `OLLAMA_MODEL`: Ollama model name (for example, `mistral`, `qwen2.5:8b`)
**For Bedrock client (`bedrock`):**
- `BEDROCK_CHAT_MODEL`: Bedrock model ID (for example, `anthropic.claude-3-5-sonnet-20240620-v1:0`)
- `BEDROCK_REGION`: AWS region (defaults to `us-east-1` if unset)
- AWS credentials via standard environment variables (for example, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
@@ -0,0 +1,112 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from random import randint
from typing import Annotated, Any, Literal
from agent_framework import Message, SupportsChatGetResponse, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIChatClient, OpenAIChatCompletionClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
Built-in Chat Clients Example
This sample demonstrates how to run the same prompt flow against different built-in
chat clients using a single `get_client` factory.
Select one of these client names:
- openai_chat
- openai_chat_completion
- anthropic
- ollama
- bedrock
- azure_openai_chat
- azure_openai_chat_completion
- foundry_chat
"""
ClientName = Literal[
"openai_chat",
"openai_chat_completion",
"anthropic",
"ollama",
"bedrock",
"azure_openai_chat",
"azure_openai_chat_completion",
"foundry_chat",
]
# NOTE: approval_mode="never_require" is for sample brevity.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]:
"""Create a built-in chat client from a name."""
from agent_framework.amazon import BedrockChatClient
from agent_framework.anthropic import AnthropicClient
from agent_framework.ollama import OllamaChatClient
if client_name == "openai_chat":
return OpenAIChatClient()
if client_name == "openai_chat_completion":
return OpenAIChatCompletionClient()
if client_name == "anthropic":
return AnthropicClient()
if client_name == "ollama":
return OllamaChatClient()
if client_name == "bedrock":
return BedrockChatClient()
if client_name == "azure_openai_chat":
return OpenAIChatClient(credential=AzureCliCredential())
if client_name == "azure_openai_chat_completion":
return OpenAIChatCompletionClient(credential=AzureCliCredential())
if client_name == "foundry_chat":
return FoundryChatClient(credential=AzureCliCredential())
raise ValueError(f"Unsupported client name: {client_name}")
async def main(client_name: ClientName = "openai_chat") -> None:
"""Run a basic prompt using a selected built-in client."""
client = get_client(client_name)
message = Message("user", contents=["What's the weather in Amsterdam and in Paris?"])
stream = os.getenv("STREAM", "false").lower() == "true"
print(f"Client: {client_name}")
print(f"User: {message.text}")
if stream:
response_stream = client.get_response([message], stream=True, options={"tools": get_weather})
print("Assistant: ", end="")
async for chunk in response_stream:
if chunk.text:
print(chunk.text, end="")
print("")
else:
print(f"Assistant: {await client.get_response([message], stream=False, options={'tools': get_weather})}")
if __name__ == "__main__":
asyncio.run(main("openai_chat"))
"""
Sample output:
User: What's the weather in Amsterdam and in Paris?
Assistant: The weather in Amsterdam is sunny with a high of 25°C.
...and in Paris it is cloudy with a high of 19°C.
"""
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Message
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Chat Response Cancellation Example
Demonstrates proper cancellation of streaming chat responses during execution.
Shows asyncio task cancellation and resource cleanup techniques.
"""
async def main() -> None:
"""
Demonstrates cancelling a chat request after 1 second.
Creates a task for the chat request, waits briefly, then cancels it to show proper cleanup.
Configuration:
- FOUNDRY_PROJECT_ENDPOINT: Azure AI Foundry project endpoint URL
- FOUNDRY_MODEL: Model deployment name (e.g. gpt-4o)
- Authentication: Run `az login` to authenticate via AzureCliCredential
"""
client = FoundryChatClient(credential=AzureCliCredential())
async def get_story_response() -> None:
await client.get_response(messages=[Message(role="user", contents=["Tell me a fantasy story."])])
try:
task = asyncio.create_task(get_story_response())
await asyncio.sleep(1)
task.cancel()
await task
except asyncio.CancelledError:
print("Request was cancelled")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,213 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import random
import sys
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from typing import Any, ClassVar, TypeAlias, TypedDict
from agent_framework import (
Agent,
BaseChatClient,
ChatMiddlewareLayer,
ChatResponse,
ChatResponseUpdate,
Content,
FunctionInvocationLayer,
InMemoryHistoryProvider,
Message,
ResponseStream,
)
from agent_framework.observability import ChatTelemetryLayer
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
"""
Custom Chat Client Implementation Example
This sample demonstrates implementing a custom chat client and optionally composing
middleware, telemetry, and function invocation layers explicitly. The recommended
layer order is `FunctionInvocationLayer -> ChatMiddlewareLayer -> ChatTelemetryLayer`
so chat middleware runs within each tool-loop iteration while telemetry records
per-call spans without middleware latency.
"""
class EchoingChatClientOptions(TypedDict, total=False):
"""Custom options for EchoingChatClient."""
uppercase: bool
suffix: str
stream_delay_seconds: float
OptionsT: TypeAlias = EchoingChatClientOptions
class EchoingChatClient(BaseChatClient[OptionsT]):
"""A custom chat client that echoes messages back with modifications.
This demonstrates how to implement a custom chat client by extending BaseChatClient
and implementing the required _inner_get_response() method.
"""
OTEL_PROVIDER_NAME: ClassVar[str] = "EchoingChatClient"
def __init__(self, *, prefix: str = "Echo:", **kwargs: Any) -> None:
"""Initialize the EchoingChatClient.
Args:
prefix: Prefix to add to echoed messages.
**kwargs: Additional keyword arguments passed to BaseChatClient.
"""
super().__init__(**kwargs)
self.prefix = prefix
@override
def _inner_get_response(
self,
*,
messages: Sequence[Message],
stream: bool = False,
options: Mapping[str, Any],
**kwargs: Any,
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
"""Echo back the user's message with a prefix."""
if not messages:
response_text = "No messages to echo!"
else:
# Echo the last user message
last_user_message = None
for message in reversed(messages):
if message.role == "user":
last_user_message = message
break
if last_user_message and last_user_message.text:
response_text = f"{self.prefix} {last_user_message.text}"
else:
response_text = f"{self.prefix} [No text message found]"
if options.get("uppercase"):
response_text = response_text.upper()
if suffix := options.get("suffix"):
response_text = f"{response_text} {suffix}"
stream_delay_seconds = float(options.get("stream_delay_seconds", 0.05))
response_message = Message(role="assistant", contents=[response_text])
response = ChatResponse(
messages=[response_message],
model="echo-model-v1",
response_id=f"echo-resp-{random.randint(1000, 9999)}",
)
if not stream:
async def _get_response() -> ChatResponse:
return response
return _get_response()
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
response_text_local = response_message.text or ""
for char in response_text_local:
yield ChatResponseUpdate(
contents=[Content.from_text(char)],
role="assistant",
response_id=f"echo-stream-resp-{random.randint(1000, 9999)}",
model="echo-model-v1",
)
await asyncio.sleep(stream_delay_seconds)
return ResponseStream(_stream(), finalizer=lambda updates: response)
class EchoingChatClientWithLayers( # type: ignore[misc]
FunctionInvocationLayer[OptionsT],
ChatMiddlewareLayer[OptionsT],
ChatTelemetryLayer[OptionsT],
EchoingChatClient,
):
"""Echoing chat client that explicitly composes middleware, telemetry, and function layers."""
OTEL_PROVIDER_NAME: ClassVar[str] = "EchoingChatClientWithLayers"
async def main() -> None:
"""Demonstrates how to implement and use a custom chat client with Agent."""
print("=== Custom Chat Client Example ===\n")
# Create the custom chat client
print("--- EchoingChatClient Example ---")
echo_client = EchoingChatClientWithLayers(prefix="🔊 Echo:")
# Use the chat client directly
print("Using chat client directly:")
direct_response = await echo_client.get_response(
[Message(role="user", contents=["Hello, custom chat client!"])],
options={
"uppercase": True,
"suffix": "(CUSTOM OPTIONS)",
"stream_delay_seconds": 0.02,
},
)
print(f"Direct response: {direct_response.messages[0].text}")
# Create an agent using the custom chat client
echo_agent = Agent(
client=echo_client,
name="EchoAgent",
instructions="You are a helpful assistant that echoes back what users say.",
)
print(f"\nAgent Name: {echo_agent.name}")
# Test non-streaming with agent
query = "This is a test message"
print(f"\nUser: {query}")
result = await echo_agent.run(query)
print(f"Agent: {result.messages[0].text}")
# Test streaming with agent
query2 = "Stream this message back to me"
print(f"\nUser: {query2}")
print("Agent: ", end="", flush=True)
async for chunk in echo_agent.run(query2, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print()
# Example: Using with sessions and conversation history
print("\n--- Using Custom Chat Client with Session ---")
session = echo_agent.create_session()
# Multiple messages in conversation
messages = [
"Hello, I'm starting a conversation",
"How are you doing?",
"Thanks for chatting!",
]
for msg in messages:
result = await echo_agent.run(msg, session=session)
print(f"User: {msg}")
print(f"Agent: {result.messages[0].text}\n")
# Check conversation history
memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
session_messages = memory_state.get("messages", [])
if session_messages:
print(f"Session contains {len(session_messages)} messages")
else:
print("Session has no messages stored")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,194 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from typing import Annotated
from agent_framework import (
Agent,
FunctionInvocationContext,
FunctionMiddleware,
InMemoryHistoryProvider,
Message,
MiddlewareTermination,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
"""
Compare Foundry agents with and without per-service-call chat history persistence.
This sample runs two otherwise identical Foundry agents with ``store=False`` so
history stays local for both runs.
The sample adds a function middleware that raises ``MiddlewareTermination``
immediately after the tool runs, so the request stops before a second model
call.
That early termination is the important difference:
- Without per-service-call chat history persistence, the synthesized tool result is
still written to local history.
- With ``require_per_service_call_history_persistence=True``, that synthesized tool result is
not written to local history.
The per-service-call persistence case matches service-side storage behavior. When a terminated
request never sends the tool result back to the service, that result also never
becomes part of the service-managed history.
"""
# Load environment variables from .env file
load_dotenv()
def lookup_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Return a deterministic weather result for the requested location."""
return f"The weather in {location} is sunny."
class TerminateAfterToolMiddleware(FunctionMiddleware):
"""Stop the tool loop after the first tool finishes."""
async def process(
self,
context: FunctionInvocationContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Run the tool, then terminate the loop with that tool result."""
await call_next()
raise MiddlewareTermination(result=context.result)
def _describe_message(message: Message) -> str:
"""Render one stored message in a compact, readable format."""
parts: list[str] = []
for content in message.contents:
if content.type == "text" and content.text:
parts.append(content.text)
elif content.type == "function_call":
parts.append(f"function_call -> {content.name}({content.arguments})")
elif content.type == "function_result":
parts.append(f"function_result -> {content.result}")
else:
parts.append(content.type)
return f"{message.role}: {' | '.join(parts)}"
def _includes_tool_result(messages: list[Message]) -> bool:
"""Return whether any stored message contains a tool result."""
return any(content.type == "function_result" for message in messages for content in message.contents)
async def main() -> None:
"""Run both comparison scenarios."""
print("=== require_per_service_call_history_persistence when middleware terminates the tool loop ===\n")
# 1. Create one Foundry chat client that both agents will share.
client = FoundryChatClient(credential=AzureCliCredential())
query = "What is the weather in Seattle, and should I bring sunglasses?"
# 2. Create and run the agent without per-service-call persistence.
agent_without_persistence = Agent(
client=client,
instructions=(
"You are a weather assistant. Call lookup_weather exactly once before answering "
"any weather question, then summarize the tool result in one short paragraph."
),
tools=[lookup_weather],
context_providers=[InMemoryHistoryProvider()],
middleware=[TerminateAfterToolMiddleware()],
default_options={"tool_choice": "required", "store": False},
)
session_without_persistence = agent_without_persistence.create_session()
await agent_without_persistence.run(
query,
session=session_without_persistence,
)
stored_messages_without_persistence = session_without_persistence.state[InMemoryHistoryProvider.DEFAULT_SOURCE_ID][
"messages"
]
print("=== Without per-service-call persistence ===")
print("Loop terminated immediately after the tool finished.")
print(f"Stored synthesized tool result: {_includes_tool_result(stored_messages_without_persistence)}")
print("Stored history:")
for index, message in enumerate(stored_messages_without_persistence, start=1):
print(f" {index}. {_describe_message(message)}")
print()
# 3. Create and run the agent with per-service-call persistence enabled.
agent_with_persistence = Agent(
client=client,
instructions=(
"You are a weather assistant. Call lookup_weather exactly once before answering "
"any weather question, then summarize the tool result in one short paragraph."
),
tools=[lookup_weather],
context_providers=[InMemoryHistoryProvider()],
middleware=[TerminateAfterToolMiddleware()],
require_per_service_call_history_persistence=True,
default_options={"tool_choice": "required", "store": False},
)
session_with_persistence = agent_with_persistence.create_session()
await agent_with_persistence.run(
query,
session=session_with_persistence,
)
stored_messages_with_persistence = session_with_persistence.state[InMemoryHistoryProvider.DEFAULT_SOURCE_ID][
"messages"
]
print("=== With per-service-call persistence ===")
print("Loop terminated immediately after the tool finished.")
print(f"Stored synthesized tool result: {_includes_tool_result(stored_messages_with_persistence)}")
print("Stored history:")
for index, message in enumerate(stored_messages_with_persistence, start=1):
print(f" {index}. {_describe_message(message)}")
print()
# 4. Summarize the effect of the flag.
print(
"Both runs used FoundryChatClient with store=False and terminated right after the tool. "
"Without per-service-call persistence, local history still stored the synthesized tool result. "
"With per-service-call persistence, local history stopped at the assistant function-call message instead, "
"which matches service-side storage because the terminated tool result is never sent back to the service."
)
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== require_per_service_call_history_persistence when middleware terminates the tool loop ===
=== Without per-service-call persistence ===
Loop terminated immediately after the tool finished.
Stored synthesized tool result: True
Stored history:
1. user: What is the weather in Seattle, and should I bring sunglasses?
2. assistant: function_call -> lookup_weather({"location":"Seattle"})
3. tool: function_result -> The weather in Seattle is sunny.
=== With per-service-call persistence ===
Loop terminated immediately after the tool finished.
Stored synthesized tool result: False
Stored history:
1. user: What is the weather in Seattle, and should I bring sunglasses?
2. assistant: function_call -> lookup_weather({"location":"Seattle"})
Both runs used FoundryChatClient with store=False and terminated right after
the tool. Without per-service-call persistence, local history still stored the
synthesized tool result. With per-service-call persistence, local history
stopped at the assistant function-call message instead, which matches
service-side storage because the terminated tool result is never sent back to
the service.
"""
@@ -0,0 +1,36 @@
# Context Compaction Samples
This folder demonstrates context compaction patterns introduced by ADR-0019.
## Files
- `basics.py` — builds a local message list and applies each built-in strategy one at a time.
- `summarization.py` — runs `SummarizationStrategy` directly with a real summarizing chat client.
- `advanced.py` — composes multiple strategies with `TokenBudgetComposedStrategy`, including a real summarizer and tool-call groups.
- `agent_client_overrides.py` — shows client defaults, agent-level overrides, and per-run compaction overrides.
- `custom.py` — defines a custom strategy implementing the `CompactionStrategy` protocol.
- `tiktoken_tokenizer.py` — shows a `TokenizerProtocol` implementation backed by `tiktoken`.
- `compaction_provider.py` — uses `CompactionProvider` with an agent and `InMemoryHistoryProvider`.
Run samples with:
```bash
uv run samples/02-agents/compaction/basics.py
uv run samples/02-agents/compaction/summarization.py # requires OPENAI_API_KEY
uv run samples/02-agents/compaction/advanced.py # requires OPENAI_API_KEY
uv run samples/02-agents/compaction/agent_client_overrides.py
uv run samples/02-agents/compaction/custom.py
uv run samples/02-agents/compaction/tiktoken_tokenizer.py
uv run samples/02-agents/compaction/compaction_provider.py # requires OPENAI_API_KEY
```
## Security Considerations
Most compaction strategies in this folder (`TruncationStrategy`, `SlidingWindowStrategy`,
`SelectiveToolCallCompactionStrategy`, `ToolResultCompactionStrategy`) only remove or reorder
existing messages and carry no additional risk. `SummarizationStrategy` is the exception: it
calls out to an LLM to produce replacement summary content that permanently becomes part of
chat history. A compromised or malicious summarization service could return a summary
containing unsafe instructions, creating a persistent indirect-prompt-injection vector. Using
`SummarizationStrategy` is optional and requires explicit configuration — only point its
chat client at a summarization service you trust as much as the primary model.
@@ -0,0 +1,211 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any, cast
from agent_framework import (
GROUP_ANNOTATION_KEY,
GROUP_TOKEN_COUNT_KEY,
SUMMARY_OF_MESSAGE_IDS_KEY,
CharacterEstimatorTokenizer,
Content,
Message,
SelectiveToolCallCompactionStrategy,
SlidingWindowStrategy,
SummarizationStrategy,
TokenBudgetComposedStrategy,
annotate_message_groups,
apply_compaction,
included_token_count,
)
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
load_dotenv()
"""This sample demonstrates composed in-run compaction under a token budget.
A long, tool-using conversation is compacted with a single
``TokenBudgetComposedStrategy`` that runs three strategies in order until the
included-token count fits the budget:
1. ``SelectiveToolCallCompactionStrategy`` — drop older tool-call groups
(assistant ``function_call`` + ``tool`` result messages) that are expensive
and rarely needed verbatim once acted upon.
2. ``SummarizationStrategy`` — use a *real* chat client to summarize the oldest
remaining turns into a single linked summary message.
3. ``SlidingWindowStrategy`` — as a final guard, keep only the most recent
groups if the budget is still exceeded.
Key components:
- TokenBudgetComposedStrategy with ordered, escalating strategies
- A real OpenAIChatClient used as the summarizer (not a stub)
- Tool-call groups in the history so tool-call compaction is meaningful
- Token accounting before/after via a TokenizerProtocol
Run with:
uv run samples/02-agents/compaction/advanced.py # requires OPENAI_API_KEY
"""
def _build_long_history() -> list[Message]:
"""Build a long, tool-using migration conversation to create token pressure."""
history: list[Message] = [
Message(role="system", contents=["You are a migration copilot that plans and executes database migrations."]),
]
# A few verbose planning turns to build up token pressure.
for i in range(1, 5):
history.append(
Message(
role="user",
contents=[f"Iteration {i}: capture migration requirements, constraints, and edge cases in detail."],
)
)
history.append(
Message(
role="assistant",
contents=[
(
f"Iteration {i}: produced a detailed plan covering dependencies, rollback guidance, data "
"backfill, and a full testing matrix. This response is intentionally verbose to add pressure."
)
],
)
)
# A tool-call group: the assistant inspects the schema via a tool.
history.append(
Message(
role="assistant",
contents=[Content.from_function_call(call_id="call_1", name="inspect_schema", arguments='{"db":"legacy"}')],
)
)
history.append(
Message(
role="tool",
contents=[Content.from_function_result(call_id="call_1", result="tables: users, orders, invoices, events")],
)
)
history.append(Message(role="assistant", contents=["Schema inspection found four core tables to migrate."]))
# The most recent turn — this should survive compaction verbatim.
history.append(Message(role="user", contents=["What is the safest order to migrate these tables?"]))
history.append(
Message(
role="assistant",
contents=["Migrate reference tables (users) first, then orders, then invoices, and events last."],
)
)
return history
def _annotation(message: Message) -> dict[str, Any] | None:
annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
return cast("dict[str, Any]", annotation) if isinstance(annotation, dict) else None
def _token_count(message: Message) -> int | None:
annotation = _annotation(message)
return annotation.get(GROUP_TOKEN_COUNT_KEY) if annotation else None
def _relation(message: Message) -> str:
"""Describe how a projected message relates to the original messages."""
annotation = _annotation(message)
if annotation is None:
return ""
summarizes = annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY)
if summarizes:
return f" <- summary of {summarizes}"
return ""
async def main() -> None:
# 1. Build synthetic history representing long-running, tool-using growth.
messages = _build_long_history()
# 2. Configure tokenizer and measure token count before compaction.
tokenizer = CharacterEstimatorTokenizer()
annotate_message_groups(messages, tokenizer=tokenizer)
budget_before = included_token_count(messages)
print("Before compaction message set:")
for msg in messages:
text_preview = msg.text[:80] if msg.text else "<non-text>"
print(f"- [{msg.role}] {text_preview} ({msg.message_id}, {_token_count(msg)} tokens)")
print()
# 3. Create a real summarizer client. SummarizationStrategy only requires a
# SupportsChatGetResponse-compatible client.
summarizer = OpenAIChatClient(model="gpt-4o-mini")
# 4. Configure the composed strategy stack. Strategies run in order and the
# composed strategy stops as soon as the included-token budget is met.
# The budget is set high enough that the generated summary fits within it:
# a tighter budget would trip the composed fallback, which excludes the
# oldest group first (the summary) once the included set exceeds the
# budget. SlidingWindowStrategy remains as a recency safety net for longer
# histories; for this sample summarization alone reaches budget, so the
# window does not need to fire.
composed = TokenBudgetComposedStrategy(
token_budget=400,
tokenizer=tokenizer,
strategies=[
SelectiveToolCallCompactionStrategy(keep_last_tool_call_groups=0),
SummarizationStrategy(client=summarizer, target_count=3, threshold=2),
SlidingWindowStrategy(keep_last_groups=4),
],
)
# 5. Apply compaction and inspect the budget result.
projected = await apply_compaction(messages, strategy=composed, tokenizer=tokenizer)
budget_after = included_token_count(messages)
print(f"Projected messages after compaction: {len(projected)}")
print(f"Included token count before compaction: {budget_before}")
print(f"Included token count after compaction: {budget_after}")
print("Projected roles:", [m.role for m in projected])
print("Projected messages with token counts:")
for msg in projected:
text_preview = msg.text[:80] if msg.text else "<non-text>"
print(f"- [{msg.role}] {text_preview} ({msg.message_id}, {_token_count(msg)} tokens){_relation(msg)}")
# 6. Surface the model-generated summary, if summarization fired.
for msg in messages:
annotation = _annotation(msg)
if annotation and annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY):
print("\nGenerated summary:")
print(f" {msg.text}")
print(f" summarizes: {annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY)}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output (summary text and token counts vary because the summary is generated by the model):
Before compaction message set:
- [system] You are a migration copilot that plans and executes database migrations. (msg_0, 46 tokens)
- [user] Iteration 1: capture migration requirements, constraints, and edge cases in deta (msg_1, 48 tokens)
- [assistant] Iteration 1: produced a detailed plan covering dependencies, rollback guidance, (msg_2, 73 tokens)
...
- [user] What is the safest order to migrate these tables? (msg_12, 40 tokens)
- [assistant] Migrate reference tables (users) first, then orders, then invoices, and events l (msg_13, 50 tokens)
Projected messages after compaction: 5
Included token count before compaction: 757
Included token count after compaction: 274
Projected roles: ['system', 'assistant', 'assistant', 'user', 'assistant']
Projected messages with token counts:
- [system] You are a migration copilot that plans and executes database migrations. (msg_0, 46 tokens)
- [assistant] Across four planning turns the user and assistant... (summary_14, 96 tokens) <- summary of [msg_1..8]
- [assistant] Schema inspection found four core tables to migrate. (msg_11, 42 tokens)
- [user] What is the safest order to migrate these tables? (msg_12, 40 tokens)
- [assistant] Migrate reference tables (users) first, then orders, then invoices, and events l (msg_13, 50 tokens)
Generated summary:
Across four planning turns the user and assistant defined the migration requirements...
summarizes: ['msg_1', 'msg_2', 'msg_3', 'msg_4', 'msg_5', 'msg_6', 'msg_7', 'msg_8']
"""
@@ -0,0 +1,144 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Mapping, Sequence
from typing import Any
from agent_framework import (
GROUP_ANNOTATION_KEY,
GROUP_TOKEN_COUNT_KEY,
Agent,
BaseChatClient,
ChatResponse,
Message,
SlidingWindowStrategy,
TruncationStrategy,
)
"""This sample demonstrates client defaults, agent overrides, and run-level overrides for in-run compaction.
Key components:
- A shared client with default `compaction_strategy` and `tokenizer`
- An agent-level override that takes precedence over the shared client defaults
- A run-level override passed through `agent.run(...)`
"""
class FixedTokenizer:
"""Simple tokenizer used to make token annotations easy to inspect."""
def __init__(self, token_count: int) -> None:
self._token_count = token_count
def count_tokens(self, text: str) -> int:
return self._token_count
class InspectingChatClient(BaseChatClient[Any]):
"""Chat client that records the messages it receives after compaction."""
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.last_messages: list[Message] = []
def _inner_get_response(
self,
*,
messages: Sequence[Message],
stream: bool,
options: Mapping[str, Any],
**kwargs: Any,
) -> Awaitable[ChatResponse]:
if stream:
raise ValueError("This sample only demonstrates non-streaming responses.")
self.last_messages = list(messages)
async def _get_response() -> ChatResponse:
return ChatResponse(messages=[Message(role="assistant", contents=["done"])])
return _get_response()
def _build_messages() -> list[Message]:
return [
Message(role="user", contents=["Collect the deployment requirements."]),
Message(role="assistant", contents=["I will gather the constraints first."]),
Message(role="user", contents=["Summarize the rollout risks."]),
Message(role="assistant", contents=["The main risks are drift, downtime, and rollback gaps."]),
]
def _token_count(message: Message) -> int | None:
group_annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
if not isinstance(group_annotation, dict):
return None
value = group_annotation.get(GROUP_TOKEN_COUNT_KEY)
return value if isinstance(value, int) else None
def _print_model_input(title: str, client: InspectingChatClient) -> None:
print(f"\n{title}")
print(f"Model receives {len(client.last_messages)} message(s):")
for message in client.last_messages:
print(f"- [{message.role}] {message.text} ({_token_count(message)} tokens)")
async def main() -> None:
# 1. Create one shared client with default compaction settings.
shared_client = InspectingChatClient(
compaction_strategy=TruncationStrategy(max_n=3, compact_to=2),
tokenizer=FixedTokenizer(7),
)
# 2. Create one agent that relies on the client defaults.
client_default_agent = Agent(client=shared_client, name="ClientDefaultAgent")
# 3. Create another agent that overrides the shared client's defaults.
agent_override = Agent(
client=shared_client,
name="AgentOverrideAgent",
compaction_strategy=SlidingWindowStrategy(keep_last_groups=3),
tokenizer=FixedTokenizer(11),
)
# 4. Run the first agent; the client defaults are applied.
await client_default_agent.run(_build_messages())
_print_model_input("1. Client default compaction", shared_client)
# 5. Run the second agent; the agent-level override wins over the client defaults.
await agent_override.run(_build_messages())
_print_model_input("2. Agent-level override", shared_client)
# 6. Override both settings for a single run; the per-run values win over both.
await agent_override.run(
_build_messages(),
compaction_strategy=TruncationStrategy(max_n=2, compact_to=1),
tokenizer=FixedTokenizer(23),
)
_print_model_input("3. Per-run override", shared_client)
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
1. Client default compaction
Model receives 2 message(s):
- [user] Summarize the rollout risks. (7 tokens)
- [assistant] The main risks are drift, downtime, and rollback gaps. (7 tokens)
2. Agent-level override
Model receives 3 message(s):
- [assistant] I will gather the constraints first. (11 tokens)
- [user] Summarize the rollout risks. (11 tokens)
- [assistant] The main risks are drift, downtime, and rollback gaps. (11 tokens)
3. Per-run override
Model receives 1 message(s):
- [assistant] The main risks are drift, downtime, and rollback gaps. (23 tokens)
"""
@@ -0,0 +1,241 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
from agent_framework import (
CharacterEstimatorTokenizer,
ChatResponse,
Content,
Message,
SelectiveToolCallCompactionStrategy,
SlidingWindowStrategy,
SummarizationStrategy,
TokenBudgetComposedStrategy,
ToolResultCompactionStrategy,
TruncationStrategy,
apply_compaction,
)
"""This sample demonstrates selecting one compaction strategy at a time.
How to use this sample:
- Keep one ``selected_strategy`` block active in ``main``.
- Comment the active block and uncomment one of the alternatives to switch strategies.
- Run again to compare behavior against the same "before" message list shown once.
"""
SUMMARY_OF_MESSAGE_IDS_KEY = "_summary_of_message_ids"
SUMMARIZED_BY_SUMMARY_ID_KEY = "_summarized_by_summary_id"
# Keep optional strategy classes imported for quick uncomment/switch in main().
AVAILABLE_STRATEGY_TYPES = (
TruncationStrategy,
CharacterEstimatorTokenizer,
SlidingWindowStrategy,
SelectiveToolCallCompactionStrategy,
ToolResultCompactionStrategy,
SummarizationStrategy,
TokenBudgetComposedStrategy,
)
class LocalSummaryClient:
"""Simple local summarizer compatible with SupportsChatGetResponse."""
async def get_response(
self,
messages: list[Message],
*,
stream: bool = False,
options: dict[str, Any] | None = None,
**kwargs: Any,
) -> ChatResponse:
return ChatResponse(messages=[Message(role="assistant", contents=[f"Summary for {len(messages)} messages."])])
async def main() -> None:
# 1. Build one baseline history and print it once.
messages = [
Message(role="system", contents=["You are a helpful assistant."]),
Message(role="user", contents=["Plan a data migration."]),
Message(role="assistant", contents=["I will gather requirements."]),
Message(
role="assistant",
contents=[
Content.from_function_call(
call_id="call_1",
name="list_tables",
arguments='{"db":"legacy"}',
)
],
),
Message(
role="tool",
contents=[
Content.from_function_result(
call_id="call_1",
result="users, orders, events",
)
],
),
Message(role="assistant", contents=["I found three core tables."]),
Message(role="user", contents=["Estimate effort and risks."]),
Message(role="assistant", contents=["Primary risk is schema drift."]),
]
print("\n--- Before compaction ---")
print(f"Message count: {len(messages)}")
for index, message in enumerate(messages, start=1):
message_text = message.text or ", ".join(content.type for content in message.contents)
print(f"{index:02d}. [{message.role}] {message_text}")
# 2. Select exactly one strategy (default shown below).
# Truncate when included history exceeds 5 messages, then keep 4.
# System remains anchored, so the oldest non-system messages are removed first.
# selected_strategy_name = "TruncationStrategy"
# selected_strategy = TruncationStrategy(max_n=5, compact_to=4, preserve_system=True)
# Keep the most recent 4 non-system groups and preserve the system anchor.
# A group represents a user turn (and related assistant/tool follow-up).
# selected_strategy_name = "SlidingWindowStrategy"
# selected_strategy = SlidingWindowStrategy(keep_last_groups=4, preserve_system=True)
# This means all tool-call groups are removed (assistant function_call message
# plus matching tool result messages). In this example, setting to 0 removes
# the single assistant+tool pair.
selected_strategy_name = "SelectiveToolCallCompactionStrategy"
selected_strategy = SelectiveToolCallCompactionStrategy(keep_last_tool_call_groups=0)
# Collapse older tool-call groups into short "[Tool results: tool_name]" summaries
# while keeping the most recent group verbatim. Unlike SelectiveToolCallCompactionStrategy
# which fully excludes groups, this preserves a readable trace of tool usage.
# selected_strategy_name = "ToolResultCompactionStrategy"
# selected_strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=0)
# Summarize older messages so only recent context remains, and attach summary
# trace metadata linking summary -> originals and originals -> summary.
# summary_client = LocalSummaryClient()
# selected_strategy_name = "SummarizationStrategy"
# selected_strategy = SummarizationStrategy(
# client=summary_client, target_count=3, threshold=2
# )
# tokenizer = CharacterEstimatorTokenizer()
# selected_strategy_name = "TokenBudgetComposedStrategy"
# selected_strategy = TokenBudgetComposedStrategy(
# token_budget=150,
# tokenizer=tokenizer,
# strategies=[
# SelectiveToolCallCompactionStrategy(keep_last_tool_call_groups=0),
# SlidingWindowStrategy(keep_last_groups=2),
# ],
# )
# 3. Apply the selected strategy and print projected output.
projected = await apply_compaction(messages, strategy=selected_strategy)
print(f"\n--- After compaction ({selected_strategy_name}) ---")
print(f"Message count: {len(projected)}")
for index, message in enumerate(projected, start=1):
message_text = message.text or ", ".join(content.type for content in message.contents)
print(f"{index:02d}. [{message.role}] {message_text}")
summaries = []
summarized = []
for message in messages:
group_annotation = message.additional_properties.get("_group")
if not isinstance(group_annotation, dict):
continue
if group_annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY):
summaries.append(message)
if group_annotation.get(SUMMARIZED_BY_SUMMARY_ID_KEY):
summarized.append(message)
if summaries or summarized:
print("Summary trace metadata present:")
for message in summaries:
group_annotation = message.additional_properties.get("_group")
summarized_ids = (
group_annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY) if isinstance(group_annotation, dict) else None
)
print(f" summary_id={message.message_id} summarizes={summarized_ids}")
for message in summarized:
group_annotation = message.additional_properties.get("_group")
summarized_by = (
group_annotation.get(SUMMARIZED_BY_SUMMARY_ID_KEY) if isinstance(group_annotation, dict) else None
)
print(f" original_id={message.message_id} summarized_by={summarized_by}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output (always present):
--- Before compaction ---
Message count: 8
01. [system] You are a helpful assistant.
02. [user] Plan a data migration.
03. [assistant] I will gather requirements.
04. [assistant] function_call
05. [tool] function_result
06. [assistant] I found three core tables.
07. [user] Estimate effort and risks.
08. [assistant] Primary risk is schema drift.
"""
"""
Sample output (varies based on selected strategy):
--- After compaction (TruncationStrategy) ---
Message count: 4
01. [system] You are a helpful assistant.
02. [assistant] I found three core tables.
03. [user] Estimate effort and risks.
04. [assistant] Primary risk is schema drift.
--- After compaction (SlidingWindowStrategy) ---
Message count: 6
01. [system] You are a helpful assistant.
02. [assistant] function_call
03. [tool] function_result
04. [assistant] I found three core tables.
05. [user] Estimate effort and risks.
06. [assistant] Primary risk is schema drift.
--- After compaction (SelectiveToolCallCompactionStrategy) ---
Message count: 6
01. [system] You are a helpful assistant.
02. [user] Plan a data migration.
03. [assistant] I will gather requirements.
04. [assistant] I found three core tables.
05. [user] Estimate effort and risks.
06. [assistant] Primary risk is schema drift.
--- After compaction (ToolResultCompactionStrategy) ---
Message count: 7
01. [system] You are a helpful assistant.
02. [assistant] [Tool results: list_tables]
03. [user] Plan a data migration.
04. [assistant] I will gather requirements.
05. [assistant] I found three core tables.
06. [user] Estimate effort and risks.
07. [assistant] Primary risk is schema drift.
--- After compaction (SummarizationStrategy) ---
Message count: 5
01. [system] You are a helpful assistant.
02. [assistant] Summary for 2 messages.
03. [assistant] I found three core tables.
04. [user] Estimate effort and risks.
05. [assistant] Primary risk is schema drift.
Summary trace metadata present:
summary_id=summary_8 summarizes=['msg_1', 'msg_2', 'msg_3', 'msg_4']
original_id=msg_1 summarized_by=summary_8
original_id=msg_2 summarized_by=summary_8
original_id=msg_3 summarized_by=summary_8
original_id=msg_4 summarized_by=summary_8
--- After compaction (TokenBudgetComposedStrategy) ---
Message count: 3
01. [system] You are a helpful assistant.
02. [user] Estimate effort and risks.
03. [assistant] Primary risk is schema drift.
"""
@@ -0,0 +1,249 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import Sequence
from typing import Any
from agent_framework import (
Agent,
ChatContext,
CompactionProvider,
InMemoryHistoryProvider,
Message,
SlidingWindowStrategy,
ToolResultCompactionStrategy,
chat_middleware,
tool,
)
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
load_dotenv()
"""
CompactionProvider with Agent Example
Demonstrates ``CompactionProvider`` as part of a real agent's context-provider
pipeline alongside ``InMemoryHistoryProvider``.
The compaction provider uses two separate strategies:
- ``before_strategy``: Applied to the loaded history before the model sees it.
Here a ``SlidingWindowStrategy`` keeps only the last 3 message groups, so
older turns get dropped as the conversation grows.
- ``after_strategy``: Applied to the stored history after each turn.
Here a ``ToolResultCompactionStrategy`` collapses all but the most recent
tool-call group into short ``[Tool results: ...]`` summaries.
A chat middleware logs the messages the model actually receives (after context
providers and compaction have run) so you can see the effect of compaction.
This sample intentionally is too aggressive in excluding content, because you can see
that the last turn actually does not have the full context any longer and is therefore
only comparing the results from Paris and Tokyo and not from London.
Run with:
uv run samples/02-agents/compaction/compaction_provider.py
"""
@tool(approval_mode="never_require")
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
weather_data = {
"London": "cloudy, 12°C",
"Paris": "sunny, 18°C",
"Tokyo": "rainy, 22°C",
}
return weather_data.get(city, f"No data for {city}")
@chat_middleware
async def log_model_input(context: ChatContext, call_next: Any) -> None:
"""Chat middleware that logs the messages sent to the model (after compaction)."""
msgs: Sequence[Message] = context.messages
print(f"\n Model receives {len(msgs)} messages:")
for i, m in enumerate(msgs, 1):
text = m.text or ", ".join(c.type for c in m.contents)
print(f" {i:02d}. [{m.role}] {text[:70]}")
await call_next()
async def main() -> None:
client = OpenAIChatClient(model="gpt-4o-mini")
# History provider loads/stores conversation messages in session.state.
# skip_excluded=True means get_messages() will omit messages that were
# marked as excluded by the CompactionProvider's after_strategy.
history = InMemoryHistoryProvider(skip_excluded=True)
compaction = CompactionProvider(
# BEFORE each turn: SlidingWindow drops older message groups from
# the loaded context so the model's input stays bounded. With
# keep_last_groups=3, only the 3 most recent non-system groups are
# sent to the model — older turns are not shown to the model.
before_strategy=SlidingWindowStrategy(keep_last_groups=3, preserve_system=True),
# AFTER each turn: ToolResultCompaction marks older tool-call groups
# (assistant function_call + tool result messages) as excluded and
# inserts a short "[Tool results: ...]" summary. The original messages
# stay in storage with _excluded=True; skip_excluded on the history
# provider ensures they won't be loaded on the next turn.
after_strategy=ToolResultCompactionStrategy(keep_last_tool_call_groups=1),
history_source_id=history.source_id,
)
# Provider order matters:
# before_run: history loads → compaction trims (forward order)
# after_run: compaction marks exclusions → history stores (reverse order)
agent = Agent(
client=client,
name="WeatherAssistant",
instructions="You are a helpful weather assistant. Use the get_weather tool when asked about weather.",
tools=[get_weather],
context_providers=[history, compaction],
middleware=[log_model_input],
)
session = agent.create_session()
queries = [
"What is the weather in London?",
"How about Paris?",
"And Tokyo?",
"Which city is the warmest?",
]
for turn, query in enumerate(queries, 1):
print(f"\n{'=' * 60}")
print(f"Turn {turn} — User: {query}")
# ── What is in the persistent store right now? ──
# This shows ALL messages the history provider has accumulated,
# including any that were marked as excluded by the after_strategy
# on the previous turn. Messages marked ✗ are excluded and won't
# be loaded because skip_excluded=True on the history provider.
stored = session.state.get(history.source_id, {}).get("messages", [])
if stored:
excluded_count = sum(1 for m in stored if m.additional_properties.get("_excluded", False))
print(f"\n Stored history: {len(stored)} messages ({excluded_count} excluded)")
for i, m in enumerate(stored, 1):
text = m.text or ", ".join(c.type for c in m.contents)
excluded = m.additional_properties.get("_excluded", False)
reason = m.additional_properties.get("_exclude_reason", "")
if excluded:
marker = f" ✗ ({reason})"
elif (m.text or "").startswith("[Tool results:"):
marker = " ← summary"
else:
marker = ""
print(f" {i:02d}. [{m.role}]{marker} {text[:65]}")
# ── What the model actually sees ──
# The chat middleware fires AFTER the full context pipeline:
# 1. InMemoryHistoryProvider loads non-excluded stored messages
# 2. CompactionProvider.before_strategy (SlidingWindow) drops
# older groups so only the last 3 non-system groups survive
# 3. The agent prepends instructions and appends the new user input
# So this list is shorter than what's in storage.
result = await agent.run(query, session=session)
# ── What happens after the turn ──
# The agent's after_run pipeline runs in reverse provider order:
# 1. CompactionProvider.after_strategy (ToolResultCompaction) marks
# older tool-call groups as excluded in the stored messages —
# their assistant+tool messages get ✗ and a summary is inserted
# 2. InMemoryHistoryProvider appends the new input + response
# On the NEXT turn, skip_excluded=True means the ✗ messages won't load.
print(f"\n Agent: {result.text}")
print(f"\n{'=' * 60}")
print("Done.")
"""
Example output:
============================================================
Turn 1 — User: What is the weather in London?
Model receives 1 messages:
01. [user] What is the weather in London?
Agent: The weather in London is cloudy with a temperature of 12°C.
============================================================
Turn 2 — User: How about Paris?
Stored history: 4 messages (0 excluded)
01. [user] What is the weather in London?
02. [assistant] function_call
03. [tool] function_result
04. [assistant] The weather in London is cloudy with a temperature of 12°C.
Model receives 5 messages:
01. [user] What is the weather in London?
02. [assistant] function_call
03. [tool] function_result
04. [assistant] The weather in London is cloudy with a temperature of 12°C.
05. [user] How about Paris?
Agent: The weather in Paris is sunny with a temperature of 18°C.
============================================================
Turn 3 — User: And Tokyo?
Stored history: 8 messages (0 excluded)
01. [user] What is the weather in London?
02. [assistant] function_call
03. [tool] function_result
04. [assistant] The weather in London is cloudy with a temperature of 12°C.
05. [user] How about Paris?
06. [assistant] function_call
07. [tool] function_result
08. [assistant] The weather in Paris is sunny with a temperature of 18°C.
Model receives 5 messages:
01. [assistant] The weather in London is cloudy with a temperature of 12°C.
02. [assistant] function_call
03. [tool] function_result
04. [assistant] The weather in Paris is sunny with a temperature of 18°C.
05. [user] And Tokyo?
Agent: The weather in Tokyo is rainy with a temperature of 22°C.
============================================================
Turn 4 — User: Which city is the warmest?
Stored history: 13 messages (3 excluded)
01. [user] What is the weather in London?
02. [assistant] ← summary [Tool results: get_weather: cloudy, 12°C]
03. [assistant] ✗ (tool_result_compaction) function_call
04. [tool] ✗ (tool_result_compaction) function_result
05. [assistant] The weather in London is cloudy with a temperature of 12°C.
06. [user] ✗ (tool_result_compaction) How about Paris?
07. [assistant] function_call
08. [tool] function_result
09. [assistant] The weather in Paris is sunny with a temperature of 18°C.
10. [user] And Tokyo?
11. [assistant] function_call
12. [tool] function_result
13. [assistant] The weather in Tokyo is rainy with a temperature of 22°C.
Model receives 8 messages:
01. [assistant] function_call
02. [tool] function_result
03. [assistant] The weather in Paris is sunny with a temperature of 18°C.
04. [user] And Tokyo?
05. [assistant] function_call
06. [tool] function_result
07. [assistant] The weather in Tokyo is rainy with a temperature of 22°C.
08. [user] Which city is the warmest?
Agent: Tokyo is the warmest city with a temperature of 22°C, compared to Paris, which is at 18°C.
============================================================
Done.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import (
Message,
annotate_message_groups,
apply_compaction,
included_messages,
)
"""This sample demonstrates authoring a custom compaction strategy.
The custom strategy keeps system messages and the most recent user turn while
excluding older non-system groups.
"""
EXCLUDED_KEY = "_excluded"
GROUP_ANNOTATION_KEY = "_group"
class KeepLastUserTurnStrategy:
async def __call__(self, messages: list[Message]) -> bool:
group_ids = annotate_message_groups(messages)
group_kinds: dict[str, str] = {}
for message in messages:
group_annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
group_id = group_annotation.get("id") if isinstance(group_annotation, dict) else None
kind = group_annotation.get("kind") if isinstance(group_annotation, dict) else None
if isinstance(group_id, str) and isinstance(kind, str) and group_id not in group_kinds:
group_kinds[group_id] = kind
user_group_ids = [group_id for group_id in group_ids if group_kinds.get(group_id) == "user"]
if not user_group_ids:
return False
keep_user_group_id = user_group_ids[-1]
changed = False
for message in messages:
group_annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
group_id = group_annotation.get("id") if isinstance(group_annotation, dict) else None
if message.role == "system":
continue
if group_id == keep_user_group_id:
continue
if message.additional_properties.get(EXCLUDED_KEY) is not True:
changed = True
message.additional_properties[EXCLUDED_KEY] = True
return changed
def _messages() -> list[Message]:
return [
Message(role="system", contents=["You are concise."]),
Message(role="user", contents=["first request"]),
Message(role="assistant", contents=["first response"]),
Message(role="user", contents=["second request"]),
Message(role="assistant", contents=["second response"]),
]
async def main() -> None:
# 1. Build a short conversation.
messages = _messages()
print(f"Number of messages before compaction: {len(messages)}")
# 2. Apply custom strategy.
await apply_compaction(messages, strategy=KeepLastUserTurnStrategy())
# 3. Print projected messages.
projected = included_messages(messages)
print(f"Number of messages after compaction: {len(projected)}")
for msg in projected:
print(f"[{msg.role}] {msg.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Number of messages before compaction: 5
Number of messages after compaction: 2
[system] You are concise.
[user] second request
"""
@@ -0,0 +1,159 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any, cast
from agent_framework import (
GROUP_ANNOTATION_KEY,
SUMMARIZED_BY_SUMMARY_ID_KEY,
SUMMARY_OF_MESSAGE_IDS_KEY,
Message,
SummarizationStrategy,
apply_compaction,
)
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
load_dotenv()
"""This sample demonstrates the SummarizationStrategy directly.
Unlike SlidingWindow/Truncation strategies that simply drop older groups,
``SummarizationStrategy`` calls a real chat client to *summarize* the oldest
message groups, replaces them with a single linked summary message, and keeps
the most recent turns verbatim. This preserves long-range context (decisions,
goals, unresolved items) while bounding the prompt size.
Key components:
- SummarizationStrategy with a real OpenAIChatClient summarizer
- ``apply_compaction`` to run the strategy over a message list
- Bidirectional summary trace metadata (summary -> originals, original -> summary)
Run with:
uv run samples/02-agents/compaction/summarization.py # requires OPENAI_API_KEY
"""
def _annotation(message: Message) -> dict[str, Any] | None:
annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
return cast("dict[str, Any]", annotation) if isinstance(annotation, dict) else None
def _build_history() -> list[Message]:
"""Build a multi-turn conversation long enough to trigger summarization."""
return [
Message(role="system", contents=["You are a project planning assistant."]),
Message(role="user", contents=["We are migrating a monolith to microservices. Where do we start?"]),
Message(
role="assistant",
contents=["Start by mapping bounded contexts and identifying the highest-churn modules to extract first."],
),
Message(role="user", contents=["The billing module changes most often. What are the risks of extracting it?"]),
Message(
role="assistant",
contents=["Main risks: distributed transactions, invoices-table ownership, and latency on hot paths."],
),
Message(role="user", contents=["How should we handle the shared invoices table?"]),
Message(
role="assistant",
contents=["Use the strangler-fig pattern: dual-write during transition, then make billing the owner."],
),
Message(role="user", contents=["What is the most recent decision we made?"]),
Message(role="assistant", contents=["We decided to extract billing first using the strangler-fig pattern."]),
]
def _print_messages(label: str, messages: list[Message]) -> None:
print(f"\n--- {label} ---")
print(f"Message count: {len(messages)}")
for index, message in enumerate(messages, start=1):
text = message.text or ", ".join(content.type for content in message.contents)
print(f"{index:02d}. [{message.role}] {text[:90]}")
async def main() -> None:
# 1. Create a real summarizing client. SummarizationStrategy only requires a
# SupportsChatGetResponse-compatible client, so any chat client works.
summarizer = OpenAIChatClient(model="gpt-4o-mini")
# 2. Build a conversation and show it before compaction.
messages = _build_history()
_print_messages("Before compaction", messages)
# 3. Configure the strategy. It triggers once the included non-system message
# count exceeds ``target_count + threshold`` (here 4 + 2 = 6), summarizing
# the oldest groups down toward ``target_count`` while keeping recent turns.
strategy = SummarizationStrategy(
client=summarizer,
target_count=4,
threshold=2,
)
# 4. Apply the strategy. The oldest groups are summarized into a single
# assistant message; the projected list is what the model would receive.
projected = await apply_compaction(messages, strategy=strategy)
_print_messages("After compaction (SummarizationStrategy)", projected)
# 5. Inspect the generated summary and its bidirectional trace metadata.
print("\n--- Summary trace ---")
for message in messages:
annotation = _annotation(message)
if annotation is None:
continue
summarizes = annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY)
if summarizes:
print(f"Generated summary ({message.message_id}):")
print(f" {message.text}")
print(f" summarizes original ids: {summarizes}")
summarized_by: dict[str | None, Any] = {}
for message in messages:
annotation = _annotation(message)
if annotation is None:
continue
summary_id = annotation.get(SUMMARIZED_BY_SUMMARY_ID_KEY)
if summary_id:
summarized_by[message.message_id] = summary_id
if summarized_by:
print("Originals replaced by the summary:")
for original_id, summary_id in summarized_by.items():
print(f" {original_id} -> {summary_id}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output (summary text varies because it is generated by the model):
--- Before compaction ---
Message count: 9
01. [system] You are a project planning assistant.
02. [user] We are migrating a monolith to microservices. Where do we start?
03. [assistant] Start by mapping bounded contexts and identifying the highest-churn modules to ex
04. [user] The billing module changes most often. What are the risks of extracting it?
05. [assistant] Main risks: distributed transactions, data ownership of the invoices table, and lat
06. [user] How should we handle the shared invoices table?
07. [assistant] Use the strangler-fig pattern: dual-write during transition, then make billing the
08. [user] What is the most recent decision we made?
09. [assistant] We decided to extract billing first using the strangler-fig pattern.
--- After compaction (SummarizationStrategy) ---
Message count: 6
01. [system] You are a project planning assistant.
02. [assistant] The user is migrating a monolith to microservices and decided to extract the billin
03. [user] How should we handle the shared invoices table?
04. [assistant] Use the strangler-fig pattern: dual-write during transition, then make billing the
05. [user] What is the most recent decision we made?
06. [assistant] We decided to extract billing first using the strangler-fig pattern.
--- Summary trace ---
Generated summary (summary_9):
The user is migrating a monolith to microservices and decided to extract the billing module first...
summarizes original ids: ['msg_1', 'msg_2', 'msg_3', 'msg_4', 'msg_5']
Originals replaced by the summary:
msg_1 -> summary_9
msg_2 -> summary_9
msg_3 -> summary_9
msg_4 -> summary_9
msg_5 -> summary_9
"""
@@ -0,0 +1,124 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "agent-framework-core",
# "tiktoken",
# ]
# ///
# Run with: uv run samples/02-agents/compaction/tiktoken_tokenizer.py
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
import tiktoken # type: ignore
from agent_framework import (
Message,
TokenizerProtocol,
TruncationStrategy,
annotate_message_groups,
apply_compaction,
included_token_count,
)
"""This sample demonstrates a custom TokenizerProtocol implementation with tiktoken.
Key components:
- `TiktokenTokenizer` backed by `tiktoken`
- Token-based `TruncationStrategy` (`max_n` / `compact_to`)
- Inspecting projected roles and remaining included token count
"""
class TiktokenTokenizer(TokenizerProtocol):
"""TokenizerProtocol implementation backed by tiktoken's o200k_base (gpt-4.1 and up default) encoding."""
def __init__(self, *, encoding_name: str = "o200k_base", model: str | None = None) -> None:
if model is not None:
self._encoding = tiktoken.encoding_for_model(model)
else:
self._encoding: Any = tiktoken.get_encoding(encoding_name)
def count_tokens(self, text: str) -> int:
return len(self._encoding.encode(text))
def _build_messages() -> list[Message]:
return [
Message(role="system", contents=["You are a migration assistant."]),
Message(
role="user",
contents=["List all migration risks and include detailed mitigations for each risk category."],
),
Message(
role="assistant",
contents=[
(
"Primary risks include schema drift, missing foreign key constraints, "
"and data quality regressions. Mitigations include staged validation, "
"shadow writes, and replay-based verification."
)
],
),
Message(
role="user",
contents=[("Now provide a detailed checklist with owners, rollback gates, and validation criteria.")],
),
Message(
role="assistant",
contents=[
(
"Checklist: baseline snapshots, migration dry-run, production "
"canary, progressive deployment, automated integrity checks, and "
"post-migration reconciliation."
)
],
),
]
async def main() -> None:
# 1. Create a tokenizer implementation that uses tiktoken.
tokenizer = TiktokenTokenizer()
# 2. Configure token-based truncation.
strategy = TruncationStrategy(
max_n=250,
compact_to=150,
tokenizer=tokenizer,
preserve_system=True,
)
# 3. Build conversation and measure token count before compaction.
messages = _build_messages()
annotate_message_groups(messages, tokenizer=tokenizer)
token_count_before = included_token_count(messages)
# 4. Apply compaction and measure token count after compaction.
projected = await apply_compaction(messages, strategy=strategy, tokenizer=tokenizer)
token_count_after = included_token_count(messages)
# 5. Print before/after token counts and projected conversation.
print(f"Projected messages: {len(projected)}")
print(f"Included token count before compaction: {token_count_before}")
print(f"Included token count after compaction: {token_count_after}")
print("Projected roles:", [message.role for message in projected])
for message in projected:
token_count = message.additional_properties.get("_group", {}).get("token_count")
print(f"- [{message.role}] {message.text} ({token_count} tokens)")
if __name__ == "__main__":
asyncio.run(main())
"""
Projected messages: 3
Included token count before compaction: 263
Included token count after compaction: 149
Projected roles: ['system', 'user', 'assistant']
- [system] You are a migration assistant. (40 tokens)
- [user] Now provide a detailed checklist with owners, rollback gates, and validation criteria. (49 tokens)
- [assistant] Checklist: baseline snapshots, migration dry-run, production canary,
progressive deployment, automated integrity checks, and post-migration reconciliation. (60 tokens)
"""
@@ -0,0 +1,34 @@
# Context Provider Samples
These samples demonstrate how to use context providers to enrich agent conversations with external knowledge — from custom logic to Azure AI Search (RAG) and memory services.
## Samples
| File / Folder | Description |
|---------------|-------------|
| [`simple_context_provider.py`](simple_context_provider.py) | Implement a custom context provider by extending `ContextProvider` to extract and inject structured user information across turns. |
| [`azure_ai_foundry_memory.py`](azure_ai_foundry_memory.py) | Use `FoundryMemoryProvider` to add semantic memory — automatically retrieves, searches, and stores memories via Azure AI Foundry. |
| [`file_access_data_processing/`](file_access_data_processing/) | Use `FileAccessProvider` with `FileSystemAgentFileStore` to give an agent read/write/search access to a folder of CSV data files. See its own [README](file_access_data_processing/README.md). |
| [`azure_ai_search/`](azure_ai_search/) | Retrieval Augmented Generation (RAG) with Azure AI Search in semantic and agentic modes. See its own [README](azure_ai_search/README.md). |
| [`mem0/`](mem0/) | Memory-powered context using the Mem0 integration (open-source and managed). See its own [README](mem0/README.md). |
| [`redis/`](redis/) | Redis-backed context providers for conversation memory and sessions. See its own [README](redis/README.md). |
## Prerequisites
**For `simple_context_provider.py`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: Model deployment name
- Azure CLI authentication (`az login`)
**For `azure_ai_foundry_memory.py`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: Chat/responses model deployment name
- `AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME`: Embedding model deployment name (e.g., `text-embedding-ada-002`)
- Azure CLI authentication (`az login`)
**For `file_access_data_processing/`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: Chat model deployment name
- Azure CLI authentication (`az login`)
See each subfolder's README for provider-specific prerequisites.
@@ -0,0 +1,173 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from datetime import datetime, timezone
from agent_framework import Agent, InMemoryHistoryProvider
from agent_framework.foundry import FoundryChatClient, FoundryMemoryProvider
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
MemoryStoreDefaultDefinition,
MemoryStoreDefaultOptions,
)
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
"""
Azure AI Agent with Foundry Memory Context Provider Example
This sample demonstrates using the FoundryMemoryProvider as a context provider
to add semantic memory capabilities to your agents. The provider automatically:
1. Retrieves static (user profile) memories on first run
2. Searches for contextual memories based on conversation
3. Updates the memory store with new conversation messages
The sample creates a temporary memory store with user profile enabled (and chat summary
disabled), scopes memories to a specific user ID ("user_123"), and sets update_delay=0
so memories are stored immediately (in production, use a delay to batch updates and
reduce costs). Conversation history is intentionally not stored (neither service-side
via ``store=False`` nor client-side via ``load_messages=False`` on the history provider),
so that follow-up responses demonstrate the agent relying solely on Foundry Memory
rather than chat history. The memory store is deleted at the end of the run.
Prerequisites:
1. Set FOUNDRY_PROJECT_ENDPOINT environment variable
2. Set FOUNDRY_MODEL for the chat/responses model
3. Set AZURE_OPENAI_EMBEDDING_MODEL for the embedding model
4. Deploy both a chat model (e.g. gpt-4) and an embedding model (e.g. text-embedding-3-small)
"""
load_dotenv()
async def main() -> None:
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=endpoint, credential=credential) as project_client,
):
# Generate a unique memory store name to avoid conflicts
memory_store_name = f"agent_framework_memory_{datetime.now(timezone.utc).strftime('%Y%m%d')}"
# Specify memory store options
options = MemoryStoreDefaultOptions(
chat_summary_enabled=False,
user_profile_enabled=True,
user_profile_details="Avoid irrelevant or sensitive data, such as age, financials, precise location, and credentials",
)
memory_store_definition = MemoryStoreDefaultDefinition(
chat_model=os.environ["FOUNDRY_MODEL"],
embedding_model=os.environ["AZURE_OPENAI_EMBEDDING_MODEL"],
options=options,
)
print(f"Creating memory store '{memory_store_name}'...")
try:
# Create a memory store
memory_store = await project_client.beta.memory_stores.create(
name=memory_store_name,
description="Memory store for Agent Framework with FoundryMemoryProvider",
definition=memory_store_definition,
)
except Exception as e:
print(f"Failed to create memory store: {e}")
return
print(f"Created memory store: {memory_store.name} ({memory_store.id})")
print(f"Description: {memory_store.description}\n")
print("==========================================")
# Create the chat client
client = FoundryChatClient(project_client=project_client)
# Create the Foundry Memory context provider
memory_provider = FoundryMemoryProvider(
project_client=project_client,
memory_store_name=memory_store.name,
scope="user_123", # Scope memories to a specific user, if not set, the session_id
# will be used as scope, which means memories are only shared within the same session
update_delay=0, # Do not wait to update memories after each interaction (for demo purposes)
# In production, consider setting a delay to batch updates and reduce costs
)
# Create an agent with the memory context provider
async with Agent(
name="MemoryAgent",
client=client,
instructions="""You are a helpful assistant that remembers past conversations.
The memories from previous interactions are automatically provided to you.""",
context_providers=[memory_provider, InMemoryHistoryProvider(load_messages=False)],
default_options={"store": False},
) as agent:
try:
# note that we will use the service side storage, nor load messsages from the history provider,
# but we include it to demonstrate that it can be used alongside the Foundry provider for other use cases.
session = agent.create_session()
# First interaction - establish some preferences
print("=== First conversation ===")
query1 = "I prefer dark roast coffee and I'm allergic to nuts"
print(f"User: {query1}")
result1 = await agent.run(query1, session=session)
print(f"Agent: {result1}\n")
# Wait for memories to be processed
print("Waiting for memories to be stored...")
await asyncio.sleep(8)
# Second interaction - test memory recall
print("=== Second conversation ===")
query2 = "Can you recommend a coffee and snack for me?"
print(f"User: {query2}")
result2 = await agent.run(query2, session=session)
print(f"Agent: {result2}\n")
# Third interaction - continue the conversation
print("=== Third conversation ===")
query3 = "What do you remember about my preferences?"
print(f"User: {query3}")
result3 = await agent.run(query3, session=session)
print(f"Agent: {result3}\n")
print(f"Stored memories from: {memory_store.name} ({memory_store.id})")
res = await project_client.beta.memory_stores.search_memories(name=memory_store.name, scope="user_123")
for memory in res.memories:
print(f"Memory: {memory.memory_item.content}")
except Exception as e:
print(f"An error occurred: {e}")
finally:
await project_client.beta.memory_stores.delete(memory_store_name)
print("==========================================")
print("Memory store deleted")
if __name__ == "__main__":
asyncio.run(main())
"""
Example output:
Creating memory store 'agent_framework_memory_20260223'...
Created memory store: agent_framework_memory_20260223 (memstore_57c1f95bb4040c6d00RVOP71Q8tS23opIc4G4ZE8DuALiBFx44)
Description: Memory store for Agent Framework with FoundryMemoryProvider
==========================================
=== First conversation ===
User: I prefer dark roast coffee and I'm allergic to nuts
Agent: Got it—Ill remember: you prefer dark roast coffee, and youre allergic to nuts.
Waiting for memories to be stored...
=== Second conversation ===
User: Can you recommend a coffee and snack for me?
Agent: For coffee: **dark roast drip or Americano** (choose a **dark roast** like French/Italian roast). If you like it smoother, try a **dark-roast cold brew**.
For a snack (nut-free): **Greek yogurt with berries**, or a **cheese stick + whole-grain crackers**. If you want something sweet: **dark chocolate (check “may contain nuts” warnings)**.
=== Third conversation ===
User: What do you remember about my preferences?
Agent: - Youre allergic to nuts.
- You prefer dark roast coffee.
Stored memories from: agent_framework_memory_20260223 (memstore_57c1f95bb4040c6d00RVOP71Q8tS23opIc4G4ZE8DuALiBFx44)
Memory: The user is allergic to nuts.
Memory: The user prefers dark roast coffee.
==========================================
Memory store deleted
"""
@@ -0,0 +1,284 @@
# Azure AI Search Context Provider Examples
Azure AI Search context provider enables Retrieval Augmented Generation (RAG) with your agents by retrieving relevant documents from Azure AI Search indexes. It supports two search modes optimized for different use cases.
This folder contains examples demonstrating how to use the Azure AI Search context provider with the Agent Framework.
## Examples
| File | Description |
|------|-------------|
| [`search_context_agentic.py`](search_context_agentic.py) | **Agentic mode** (recommended for most scenarios): Uses Knowledge Bases in Azure AI Search for query planning and multi-hop reasoning. Provides more accurate results through intelligent retrieval with automatic query reformulation. Slightly slower with more token consumption for query planning. [Learn more](https://learn.microsoft.com/azure/search/agentic-retrieval-overview) |
| [`search_context_semantic.py`](search_context_semantic.py) | **Semantic mode** (fast queries): Fast hybrid search combining vector and keyword search with semantic ranking. Returns raw search results as context. Best for scenarios where speed is critical and simple retrieval is sufficient. |
## Installation
```bash
pip install agent-framework-azure-ai-search agent-framework-foundry
```
## Prerequisites
### Required Resources
1. **Azure AI Search service** with a search index containing your documents
- [Create Azure AI Search service](https://learn.microsoft.com/azure/search/search-create-service-portal)
- [Create and populate a search index](https://learn.microsoft.com/azure/search/search-what-is-an-index)
2. **Azure AI Foundry project** with a model deployment
- [Create Azure AI Foundry project](https://learn.microsoft.com/azure/ai-studio/how-to/create-projects)
- Deploy a model (e.g., GPT-4o)
3. **For Agentic mode only**: Azure OpenAI resource for Knowledge Base model calls
- [Create Azure OpenAI resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource)
- Note: This is separate from your Azure AI Foundry project endpoint
### Authentication
Both examples support two authentication methods:
- **API Key**: Set `AZURE_SEARCH_API_KEY` environment variable
- **Entra ID (Managed Identity)**: Uses `DefaultAzureCredential` when API key is not provided
Run `az login` if using Entra ID authentication.
### API versions (stable vs preview)
The provider auto-detects which build of `azure-search-documents` is installed — nothing to
configure in code:
- **Stable / GA** — `pip install azure-search-documents` (`>=12.0.0`) → api-version `2026-04-01`.
- **Preview** — `pip install --pre azure-search-documents` (e.g. `12.1.0b1`) → api-version `2026-05-01-preview`.
The installed build picks its own api-version, so newer releases work without code changes.
Agentic `knowledge_base_output_mode="answer_synthesis"` and `retrieval_reasoning_effort` of
`"low"`/`"medium"` ship **only** in the preview build. On a stable build the provider uses
extractive output with minimal reasoning effort and raises an actionable error if a preview-only
option is requested. To enable them, just install the preview build (`pip install --pre
azure-search-documents`) — no code change.
## Configuration
### Environment Variables
**Common (both modes):**
- `AZURE_SEARCH_ENDPOINT`: Your Azure AI Search endpoint (e.g., `https://myservice.search.windows.net`)
- `AZURE_SEARCH_INDEX_NAME`: Name of your search index
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: Model deployment name (e.g., `gpt-4o`, defaults to `gpt-4o`)
- `AZURE_SEARCH_API_KEY`: _(Optional)_ Your search API key - if not provided, uses DefaultAzureCredential
**Agentic mode only:**
- `AZURE_SEARCH_KNOWLEDGE_BASE_NAME`: Name of your Knowledge Base in Azure AI Search
- `AZURE_OPENAI_RESOURCE_URL`: Your Azure OpenAI resource URL (e.g., `https://myresource.openai.azure.com`)
- **Important**: This is different from `FOUNDRY_PROJECT_ENDPOINT` - Knowledge Base needs the OpenAI endpoint for model calls
### Example .env file
**For Semantic Mode:**
```env
AZURE_SEARCH_ENDPOINT=https://myservice.search.windows.net
AZURE_SEARCH_INDEX_NAME=my-index
FOUNDRY_PROJECT_ENDPOINT=https://<resource-name>.services.ai.azure.com/api/projects/<project-name>
FOUNDRY_MODEL=gpt-4o
# Optional - omit to use Entra ID
AZURE_SEARCH_API_KEY=your-search-key
```
**For Agentic Mode (add these to semantic mode variables):**
```env
AZURE_SEARCH_KNOWLEDGE_BASE_NAME=my-knowledge-base
AZURE_OPENAI_RESOURCE_URL=https://myresource.openai.azure.com
```
## Search Modes Comparison
| Feature | Semantic Mode | Agentic Mode |
|---------|--------------|--------------|
| **Speed** | Fast | Slower (query planning overhead) |
| **Token Usage** | Lower | Higher (query reformulation) |
| **Retrieval Strategy** | Hybrid search + semantic ranking | Multi-hop reasoning with Knowledge Base |
| **Query Handling** | Direct search | Automatic query reformulation |
| **Best For** | Simple queries, speed-critical apps | Complex queries, multi-document reasoning |
| **Additional Setup** | None | Requires Knowledge Base + OpenAI resource |
### When to Use Semantic Mode
- **Simple queries** where direct keyword/vector search is sufficient
- **Speed is critical** and you need low latency
- **Straightforward retrieval** from single documents
- **Lower token costs** are important
### When to Use Agentic Mode
- **Complex queries** requiring multi-hop reasoning
- **Cross-document analysis** where information spans multiple sources
- **Ambiguous queries** that benefit from automatic reformulation
- **Higher accuracy** is more important than speed
- You need **intelligent query planning** and document synthesis
## How the Examples Work
### Semantic Mode Flow
1. User query is sent to Azure AI Search
2. Hybrid search (vector + keyword) retrieves relevant documents
3. Semantic ranking reorders results for relevance
4. Top-k documents are returned as context
5. Agent generates response using retrieved context
### Agentic Mode Flow
1. User query is sent to the Knowledge Base
2. Knowledge Base plans the retrieval strategy
3. Multiple search queries may be executed (multi-hop)
4. Retrieved information is synthesized
5. Enhanced context is provided to the agent
6. Agent generates response with comprehensive context
## Code Example
### Semantic Mode
```python
from agent_framework import Agent
from agent_framework.azure import AzureAISearchContextProvider
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import DefaultAzureCredential
# Create search provider with semantic mode (default)
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key, # Or use credential for Entra ID
mode="semantic", # Default mode
top_k=3, # Number of documents to retrieve
)
# Create agent with search context
async with FoundryChatClient(
project_endpoint=project_endpoint,
model=model_deployment,
credential=DefaultAzureCredential(),
) as client:
async with Agent(
client=client,
context_providers=[search_provider],
) as agent:
response = await agent.run("What information is in the knowledge base?")
```
### Agentic Mode
```python
from agent_framework.azure import AzureAISearchContextProvider
# Create search provider with agentic mode
search_provider = AzureAISearchContextProvider(
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key,
mode="agentic", # Enable agentic retrieval
knowledge_base_name=knowledge_base_name,
azure_openai_resource_url=azure_openai_resource_url,
top_k=5,
)
# Use with agent (same as semantic mode)
async with Agent(
client=client,
model=model_deployment,
context_providers=[search_provider],
) as agent:
response = await agent.run("Analyze and compare topics across documents")
```
## Running the Examples
1. **Set up environment variables** (see Configuration section above)
2. **Ensure you have an Azure AI Search index** with documents:
```bash
# Verify your index exists
curl -X GET "https://myservice.search.windows.net/indexes/my-index?api-version=2024-07-01" \
-H "api-key: YOUR_API_KEY"
```
3. **For agentic mode**: Create a Knowledge Base in Azure AI Search
- [Knowledge Base documentation](https://learn.microsoft.com/azure/search/knowledge-store-create-portal)
4. **Run the examples**:
```bash
# Semantic mode (fast, simple)
python azure_ai_with_search_context_semantic.py
# Agentic mode (intelligent, complex)
python azure_ai_with_search_context_agentic.py
```
## Key Parameters
### Common Parameters
- `endpoint`: Azure AI Search service endpoint
- `index_name`: Name of the search index
- `api_key`: API key for authentication (optional, can use credential instead)
- `credential`: Azure credential for Entra ID auth (e.g., `DefaultAzureCredential()`)
- `mode`: Search mode - `"semantic"` (default) or `"agentic"`
- `top_k`: Number of documents to retrieve (default: 3 for semantic, 5 for agentic)
### Semantic Mode Parameters
- `semantic_configuration`: Name of semantic configuration in your index (optional)
- `query_type`: Query type - `"semantic"` for semantic search (default)
### Agentic Mode Parameters
- `knowledge_base_name`: Name of your Knowledge Base (required)
- `azure_openai_resource_url`: Azure OpenAI resource URL (required)
- `max_search_queries`: Maximum number of search queries to generate (default: 3)
## Troubleshooting
### Common Issues
1. **Authentication errors**
- Ensure `AZURE_SEARCH_API_KEY` is set, or run `az login` for Entra ID auth
- Verify your credentials have search permissions
2. **Index not found**
- Verify `AZURE_SEARCH_INDEX_NAME` matches your index name exactly
- Check that the index exists and contains documents
3. **Agentic mode errors**
- Ensure `AZURE_SEARCH_KNOWLEDGE_BASE_NAME` is correctly configured
- Verify `AZURE_OPENAI_RESOURCE_URL` points to your Azure OpenAI resource (not AI Foundry endpoint)
- Check that your OpenAI resource has the necessary model deployments
4. **No results returned**
- Verify your index has documents with vector embeddings (for semantic/hybrid search)
- Check that your queries match the content in your index
- Try increasing `top_k` parameter
5. **Slow responses in agentic mode**
- This is expected - agentic mode trades speed for accuracy
- Reduce `max_search_queries` if needed
- Consider semantic mode for speed-critical applications
## Performance Tips
- **Use semantic mode** as the default for most scenarios - it's fast and effective
- **Switch to agentic mode** when you need multi-hop reasoning or complex queries
- **Adjust `top_k`** based on your needs - higher values provide more context but increase token usage
- **Enable semantic configuration** in your index for better semantic ranking
- **Use Entra ID authentication** in production for better security
## Additional Resources
- [Azure AI Search Documentation](https://learn.microsoft.com/azure/search/)
- [Azure AI Foundry Documentation](https://learn.microsoft.com/azure/ai-studio/)
- [RAG with Azure AI Search](https://learn.microsoft.com/azure/search/retrieval-augmented-generation-overview)
- [Semantic Search in Azure AI Search](https://learn.microsoft.com/azure/search/semantic-search-overview)
- [Knowledge Bases in Azure AI Search](https://learn.microsoft.com/azure/search/knowledge-store-concept-intro)
- [Agentic Retrieval in Azure AI Search](https://learn.microsoft.com/azure/search/agentic-retrieval-overview)
@@ -0,0 +1,150 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import AzureAISearchContextProvider
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample demonstrates how to use Azure AI Search with agentic mode for RAG
(Retrieval Augmented Generation) with Azure AI agents.
**Agentic mode** is recommended for most scenarios:
- Uses Knowledge Bases in Azure AI Search for query planning
- Performs multi-hop reasoning across documents
- Provides more accurate results through intelligent retrieval
- Slightly slower with more token consumption for query planning
- See: https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720
For simple queries where speed is critical, use semantic mode instead (see azure_ai_with_search_context_semantic.py).
Prerequisites:
1. An Azure AI Search service
2. An Azure AI Foundry project with a model deployment
3. Either an existing Knowledge Base OR a search index (to auto-create a KB)
Environment variables:
- AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint
- AZURE_SEARCH_API_KEY: (Optional) API key - if not provided, uses AzureCliCredential
- FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- FOUNDRY_MODEL: Your model deployment name (e.g., "gpt-4o")
For using an existing Knowledge Base (recommended):
- AZURE_SEARCH_KNOWLEDGE_BASE_NAME: Your Knowledge Base name
For auto-creating a Knowledge Base from an index:
- AZURE_SEARCH_INDEX_NAME: Your search index name
- AZURE_OPENAI_RESOURCE_URL: Azure OpenAI resource URL (e.g., "https://myresource.openai.azure.com")
"""
# Sample queries to demonstrate agentic RAG
USER_INPUTS = [
"What information is available in the knowledge base?",
"Analyze and compare the main topics from different documents",
"What connections can you find across different sections?",
]
async def main() -> None:
"""Main function demonstrating Azure AI Search agentic mode."""
# Get configuration from environment
search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"]
search_key = os.environ.get("AZURE_SEARCH_API_KEY")
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
model_deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o")
# Agentic mode requires exactly ONE of: knowledge_base_name OR index_name
# Option 1: Use existing Knowledge Base (recommended)
knowledge_base_name = os.environ.get("AZURE_SEARCH_KNOWLEDGE_BASE_NAME")
# Option 2: Auto-create KB from index (requires azure_openai_resource_url)
index_name = os.environ.get("AZURE_SEARCH_INDEX_NAME")
azure_openai_resource_url = os.environ.get("AZURE_OPENAI_RESOURCE_URL")
# Create Azure AI Search context provider with agentic mode (recommended for accuracy)
print("Using AGENTIC mode (Knowledge Bases with query planning, recommended)\n")
print("This mode is slightly slower but provides more accurate results.\n")
# Configure based on whether using existing KB or auto-creating from index
if knowledge_base_name:
# Use existing Knowledge Base - simplest approach
search_provider = AzureAISearchContextProvider(
source_id="search_provider",
endpoint=search_endpoint,
api_key=search_key,
credential=AzureCliCredential() if not search_key else None,
mode="agentic",
knowledge_base_name=knowledge_base_name,
# Optional: Configure retrieval behavior. "answer_synthesis" output mode and
# "medium"/"low" reasoning effort require the preview build of azure-search-documents
# (`pip install --pre azure-search-documents`); the provider auto-detects the build.
knowledge_base_output_mode="extractive_data", # or "answer_synthesis" (preview build only)
retrieval_reasoning_effort="minimal", # or "medium", "low" (preview build only)
)
else:
# Auto-create Knowledge Base from index
if not index_name:
raise ValueError("Set AZURE_SEARCH_KNOWLEDGE_BASE_NAME or AZURE_SEARCH_INDEX_NAME")
if not azure_openai_resource_url:
raise ValueError("AZURE_OPENAI_RESOURCE_URL required when using index_name")
search_provider = AzureAISearchContextProvider(
source_id="search_provider",
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key,
credential=AzureCliCredential() if not search_key else None,
mode="agentic",
azure_openai_resource_url=azure_openai_resource_url,
model=model_deployment,
# Optional: Configure retrieval behavior. "answer_synthesis" output mode and
# "medium"/"low" reasoning effort require the preview build of azure-search-documents
# (`pip install --pre azure-search-documents`); the provider auto-detects the build.
knowledge_base_output_mode="extractive_data", # or "answer_synthesis" (preview build only)
retrieval_reasoning_effort="minimal", # or "medium", "low" (preview build only)
top_k=3,
)
# Create agent with search context provider
async with (
search_provider,
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model_deployment,
credential=AzureCliCredential(),
),
name="SearchAgent",
instructions=(
"You are a helpful assistant with advanced reasoning capabilities. "
"Use the provided context from the knowledge base to answer complex "
"questions that may require synthesizing information from multiple sources."
),
context_providers=[search_provider],
) as agent,
):
print("=== Azure AI Agent with Search Context (Agentic Mode) ===\n")
for user_input in USER_INPUTS:
print(f"User: {user_input}")
print("Agent: ", end="", flush=True)
# Stream response
async for chunk in agent.run(user_input, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
for content in chunk.contents:
if content.annotations:
print(f"\n[Sources: {content.annotations}]", end="", flush=True)
print("\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,117 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import AzureAISearchContextProvider
from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIEmbeddingClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample demonstrates how to use Azure AI Search with semantic mode for RAG
(Retrieval Augmented Generation) with Azure AI agents.
**Semantic mode** is the recommended default mode:
- Fast hybrid search combining vector and keyword search
- Uses semantic ranking for improved relevance
- Returns raw search results as context
- Best for most RAG use cases
Prerequisites:
1. An Azure AI Search service with a search index
2. An Azure AI Foundry project with a model deployment
3. Set the following environment variables:
- AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint
- AZURE_SEARCH_API_KEY: (Optional) Your search API key - if not provided, uses AzureCliCredential for Entra ID
- AZURE_SEARCH_INDEX_NAME: Your search index name
- FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- FOUNDRY_MODEL: Your model deployment name (e.g., "gpt-4o")
- AZURE_OPENAI_EMBEDDING_MODEL: (Optional) Your Azure OpenAI embedding deployment for hybrid search
- AZURE_OPENAI_ENDPOINT: (Optional) Your Azure OpenAI resource URL, required if using Azure OpenAI embeddings
"""
# Sample queries to demonstrate RAG
USER_INPUTS = [
"What information is available in the knowledge base?",
"Summarize the main topics from the documents",
"Find specific details about the content",
]
async def main() -> None:
"""Main function demonstrating Azure AI Search semantic mode."""
credential = AzureCliCredential()
# Get configuration from environment
search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"]
search_key = os.environ.get("AZURE_SEARCH_API_KEY")
index_name = os.environ["AZURE_SEARCH_INDEX_NAME"]
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
model_deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o")
openai_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
embedding_deployment = os.environ.get("AZURE_OPENAI_EMBEDDING_MODEL")
embedding_client = None
if openai_endpoint and embedding_deployment:
embedding_client = OpenAIEmbeddingClient(
azure_endpoint=openai_endpoint,
model=embedding_deployment,
credential=credential,
)
# Create Azure AI Search context provider with semantic mode (recommended, fast)
print("Using SEMANTIC mode (hybrid search + semantic ranking, fast)\n")
search_provider = AzureAISearchContextProvider(
source_id="search_provider",
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key, # Use api_key for API key auth, or credential for managed identity
credential=credential if not search_key else None,
mode="semantic", # Default mode
top_k=3, # Retrieve top 3 most relevant documents
embedding_function=embedding_client, # Provide embedding function for hybrid search
vector_field_name="DescriptionVector"
if embedding_client
else None, # Set vector field for hybrid search if using embeddings
)
# Create agent with search context provider
async with (
search_provider,
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model_deployment,
credential=credential,
),
name="SearchAgent",
instructions=(
"You are a helpful assistant. Use the provided context from the "
"knowledge base to answer questions accurately."
),
context_providers=[search_provider],
) as agent,
):
print("=== Azure AI Agent with Search Context (Semantic Mode) ===\n")
for user_input in USER_INPUTS:
print(f"User: {user_input}")
print("Agent: ", end="", flush=True)
# Stream response
async for chunk in agent.run(user_input, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,43 @@
# CodeAct context providers
Demonstrates the provider-owned CodeAct flow with two backends:
| File | Backend | Notes |
|------|---------|-------|
| [`code_act.py`](code_act.py) | [Hyperlight](https://github.com/hyperlight-dev/hyperlight) WASM sandbox via `HyperlightCodeActProvider` | Hardened sandbox with WASM isolation; sandbox tools called via `call_tool(...)`. |
| [`monty_code_act.py`](monty_code_act.py) | [Monty](https://github.com/pydantic/monty) Rust-based Python interpreter via `MontyCodeActProvider` (alpha) | Cross-platform pure interpreter; sandbox tools can be called as typed async functions (`await compute(...)`) or via `call_tool(...)`. |
Both providers inject an `execute_code` tool into the agent and keep the
registered sandbox tools (`compute`, `fetch_data`) hidden from the model — the
model invokes them from inside the sandbox.
## Installation
```bash
pip install agent-framework agent-framework-hyperlight --pre # Hyperlight sample
pip install agent-framework agent-framework-monty --pre # Monty sample
```
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
> at runtime when it tries to create the sandbox.
>
> Monty is cross-platform and has no hypervisor/WASM backend dependency, but it
> interprets a Python subset (e.g. `os`/network/subprocess access is blocked).
> `agent-framework-monty` is an alpha package and is not yet part of
> `agent-framework[all]`; install it explicitly with `--pre`.
## Prerequisites
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
- A deployed model (`FOUNDRY_MODEL`)
- Azure CLI authenticated (`az login`)
## Run
```bash
python code_act.py # Hyperlight
python monty_code_act.py # Monty
```
See the source files for the full annotated examples.
@@ -0,0 +1,187 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import logging
import os
from collections.abc import Awaitable, Callable
from typing import Annotated, Any, Literal
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightCodeActProvider
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates the provider-owned Hyperlight CodeAct flow.
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
registers them only with `HyperlightCodeActProvider`. The model therefore sees a
single `execute_code` tool and must call the provider-owned tools from inside
the sandbox with `call_tool(...)`.
"""
load_dotenv()
_CYAN = "\033[36m"
_YELLOW = "\033[33m"
_GREEN = "\033[32m"
_DIM = "\033[2m"
_RESET = "\033[0m"
class _ColoredFormatter(logging.Formatter):
"""Dim logger output so it does not compete with sample prints."""
def format(self, record: logging.LogRecord) -> str:
return f"{_DIM}{super().format(record)}{_RESET}"
logging.basicConfig(level=logging.WARNING)
logging.getLogger().handlers[0].setFormatter(
_ColoredFormatter("[%(asctime)s] %(levelname)s: %(message)s"),
)
@function_middleware
async def log_function_calls(
context: FunctionInvocationContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Log tool calls, including readable execute_code blocks."""
import time
function_name = context.function.name
arguments = context.arguments if isinstance(context.arguments, dict) else {}
if function_name == "execute_code" and "code" in arguments:
print(f"\n{_YELLOW}{'' * 60}")
print("▶ execute_code")
print(f"{'' * 60}{_RESET}")
print(arguments["code"])
print(f"{_YELLOW}{'' * 60}{_RESET}")
else:
pairs = ", ".join(f"{name}={value!r}" for name, value in arguments.items())
print(f"\n{_YELLOW}{function_name}({pairs}){_RESET}")
start = time.perf_counter()
await call_next()
elapsed = time.perf_counter() - start
result = context.result
if function_name == "execute_code" and isinstance(result, list):
for output in result:
if output.type == "text" and output.text:
print(f"{_GREEN}stdout:\n{output.text}{_RESET}")
elif output.type == "error" and output.error_details:
print(f"{_YELLOW}stderr:\n{output.error_details}{_RESET}")
else:
print(f"{_YELLOW}{function_name}{result!r}{_RESET}")
print(f"{_DIM} ({elapsed:.4f}s){_RESET}")
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation for sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
async def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch records from a named table."""
await asyncio.sleep(0.5)
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
async def main() -> None:
"""Run the provider-owned Hyperlight CodeAct sample."""
# 1. Create the Hyperlight-backed provider and register sandbox tools on it.
codeact = HyperlightCodeActProvider(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 2. Create the client and the agent.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="HyperlightCodeActProviderAgent",
instructions="You are a helpful assistant.",
context_providers=[codeact],
middleware=[log_function_calls],
)
# 3. Run a request that should use execute_code plus provider-owned tools.
query = (
"Fetch all users, find admins, multiply 7*(3*2), and print the users, "
"admins, and multiplication result. Use execute_code and call_tool(...) "
"inside the sandbox."
)
print(f"{_CYAN}{'=' * 60}")
print("Hyperlight CodeAct provider sample")
print(f"{'=' * 60}{_RESET}")
print(f"{_CYAN}User: {query}{_RESET}")
result = await agent.run(query)
print(f"{_CYAN}Agent: {result.text}{_RESET}")
"""
Sample output (shape only):
============================================================
Hyperlight CodeAct provider sample
============================================================
User: Fetch all users, find admins, multiply 7*(3*2), ...
────────────────────────────────────────────────────────────
▶ execute_code
────────────────────────────────────────────────────────────
users = call_tool("fetch_data", table="users")
admins = [user for user in users if user["role"] == "admin"]
result = call_tool("compute", operation="multiply", a=7, b=6)
print("Users:", users)
print("Admins:", admins)
print("7 * 6 =", result)
────────────────────────────────────────────────────────────
stdout:
Users: [...]
Admins: [...]
7 * 6 = 42.0
(0.0xxx s)
Agent: ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,201 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import logging
import os
from collections.abc import Awaitable, Callable
from typing import Annotated, Any, Literal
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_monty import MontyCodeActProvider
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates the provider-owned Monty CodeAct flow.
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
registers them only with `MontyCodeActProvider`. The model therefore sees a
single `execute_code` tool and calls the provider-owned tools from inside the
sandbox - either as typed async functions (`await compute(...)`) or via the
generic `call_tool(...)` fallback.
`MontyCodeActProvider` uses [pydantic-monty](https://github.com/pydantic/monty),
a Rust-based Python interpreter, so it runs cross-platform with no
hypervisor/WASM backend dependency.
Note: `agent-framework-monty` is an alpha package and is not yet part of
`agent-framework[all]`. Install it explicitly with:
pip install agent-framework agent-framework-monty --pre
It is imported as `agent_framework_monty` (no lazy-loading namespace yet).
"""
load_dotenv()
_CYAN = "\033[36m"
_YELLOW = "\033[33m"
_GREEN = "\033[32m"
_DIM = "\033[2m"
_RESET = "\033[0m"
class _ColoredFormatter(logging.Formatter):
"""Dim logger output so it does not compete with sample prints."""
def format(self, record: logging.LogRecord) -> str:
return f"{_DIM}{super().format(record)}{_RESET}"
logging.basicConfig(level=logging.WARNING)
logging.getLogger().handlers[0].setFormatter(
_ColoredFormatter("[%(asctime)s] %(levelname)s: %(message)s"),
)
@function_middleware
async def log_function_calls(
context: FunctionInvocationContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Log tool calls, including readable execute_code blocks."""
import time
function_name = context.function.name
arguments = context.arguments if isinstance(context.arguments, dict) else {}
if function_name == "execute_code" and "code" in arguments:
print(f"\n{_YELLOW}{'' * 60}")
print("▶ execute_code")
print(f"{'' * 60}{_RESET}")
print(arguments["code"])
print(f"{_YELLOW}{'' * 60}{_RESET}")
else:
pairs = ", ".join(f"{name}={value!r}" for name, value in arguments.items())
print(f"\n{_YELLOW}{function_name}({pairs}){_RESET}")
start = time.perf_counter()
await call_next()
elapsed = time.perf_counter() - start
result = context.result
if function_name == "execute_code" and isinstance(result, list):
for output in result:
if output.type == "text" and output.text:
print(f"{_GREEN}stdout:\n{output.text}{_RESET}")
elif output.type == "error" and output.error_details:
print(f"{_YELLOW}stderr:\n{output.error_details}{_RESET}")
else:
print(f"{_YELLOW}{function_name}{result!r}{_RESET}")
print(f"{_DIM} ({elapsed:.4f}s){_RESET}")
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation for sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
async def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch records from a named table."""
await asyncio.sleep(0.5)
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
async def main() -> None:
"""Run the provider-owned Monty CodeAct sample."""
# 1. Create the Monty-backed provider and register sandbox tools on it.
codeact = MontyCodeActProvider(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 2. Create the client and the agent.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="MontyCodeActProviderAgent",
instructions="You are a helpful assistant.",
context_providers=[codeact],
middleware=[log_function_calls],
)
# 3. Run a request that should use execute_code plus provider-owned tools.
query = (
"Fetch all users, find admins, multiply 7*(3*2), and print the users, "
"admins, and multiplication result. Use a single execute_code call. "
"You may call the registered tools directly as typed async functions "
"(`await compute(operation='multiply', a=7, b=6)`) or via "
"`call_tool('compute', ...)`."
)
print(f"{_CYAN}{'=' * 60}")
print("Monty CodeAct provider sample")
print(f"{'=' * 60}{_RESET}")
print(f"{_CYAN}User: {query}{_RESET}")
result = await agent.run(query)
print(f"{_CYAN}Agent: {result.text}{_RESET}")
"""
Sample output (shape only):
============================================================
Monty CodeAct provider sample
============================================================
User: Fetch all users, find admins, multiply 7*(3*2), ...
────────────────────────────────────────────────────────────
▶ execute_code
────────────────────────────────────────────────────────────
users = await fetch_data(table="users")
admins = [u for u in users if u["role"] == "admin"]
result = await compute(operation="multiply", a=7, b=6)
print("Users:", users)
print("Admins:", admins)
print("7 * 6 =", result)
────────────────────────────────────────────────────────────
stdout:
Users: [...]
Admins: [...]
7 * 6 = 42.0
(0.5xxx s)
Agent: ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,62 @@
# File Access Data Processing
This sample demonstrates how to give an `Agent` access to a folder of data files
by attaching `FileAccessProvider` (backed by `FileSystemAgentFileStore`) as a
context provider.
The agent is given a `working/` folder containing `sales.csv` — ~50 rows of
sales transaction data — and is driven through a short scripted conversation
that exercises every tool the provider exposes:
| Step | Prompt | Tool(s) used |
|---|---|---|
| 1 | "What files do you have access to?" | `file_access_ls` |
| 2 | "Read sales.csv and summarize…" | `file_access_read` |
| 3 | "Calculate the total revenue per region…" | (uses previously read data) |
| 4 | "Save a markdown report named `region_totals.md`…" | `file_access_write` |
| 5 | "List the files again so I can confirm…" | `file_access_ls` |
After the run, the sample prints the final contents of `working/` so the
written file is easy to spot.
## Prerequisites
| Variable | Description |
|---|---|
| `FOUNDRY_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint. |
| `FOUNDRY_MODEL` | Chat model deployment name (e.g. `gpt-4o`). |
Run `az login` before executing the sample so `AzureCliCredential` can
authenticate.
## Running the sample
From `python/`:
```bash
uv run --package agent-framework-core python samples/02-agents/context_providers/file_access_data_processing/data_processing.py
```
Or directly:
```bash
python samples/02-agents/context_providers/file_access_data_processing/data_processing.py
```
## Sample data
`working/sales.csv` contains JanuaryMarch 2025 sales transactions with these
columns:
| Column | Description |
|---|---|
| `date` | Transaction date (YYYY-MM-DD) |
| `product` | Product name |
| `category` | Product category (Electronics, Furniture, Stationery) |
| `quantity` | Units sold |
| `unit_price` | Price per unit |
| `region` | Sales region (North, South, West) |
| `salesperson` | Name of the salesperson |
The sample writes `region_totals.md` into the same folder. Delete it between
runs if you want a clean state.
@@ -0,0 +1,159 @@
# Copyright (c) Microsoft. All rights reserved.
"""Sample: use ``FileAccessProvider`` to give an agent access to a folder of CSV data files.
This sample demonstrates how to attach :class:`FileAccessProvider` (backed by
:class:`FileSystemAgentFileStore`) to an ``Agent`` so the model can read input
data, perform analysis, and write summary output back to the same folder via
the ``file_access_*`` tools.
The file-access tools all require approval (``approval_mode="always_require"``),
so a base ``Agent`` installs :class:`ToolApprovalMiddleware` to drive the
approval handshake. Because this sample is non-interactive, it auto-approves
every file-access tool via
:meth:`FileAccessProvider.all_tools_auto_approval_rule`.
The sibling ``working/`` folder contains ``sales.csv`` — ~50 rows of sales
transactions (date, product, category, quantity, unit_price, region,
salesperson). The agent is asked, in a single session, to: list available
files, inspect the data, compute regional totals, and save a markdown summary.
Prerequisites:
- ``FOUNDRY_PROJECT_ENDPOINT``: Your Azure AI Foundry project endpoint.
- ``FOUNDRY_MODEL``: Chat model deployment name.
- Run ``az login`` before executing the sample.
"""
import asyncio
import os
from pathlib import Path
from agent_framework import Agent, FileAccessProvider, FileSystemAgentFileStore, ToolApprovalMiddleware
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load python/.env (python-dotenv walks up from this file by default). Pass
# override=True so values from .env take precedence over any pre-existing OS
# environment variables — without this, OS-level values silently win.
load_dotenv(override=True)
INSTRUCTIONS = """
You are a data analyst assistant. You have access to a folder of data files via
the file_access_* tools.
## Getting started
- Start by listing available files with file_access_ls to see what data
is available. Files may be organized into subdirectories — use
file_access_ls to discover folders and explore the tree level
by level.
- Read the files to understand their structure and contents.
## Working with data
- When asked to analyze data, read the relevant files first, then perform the
analysis.
- Show your analysis clearly with tables, summaries, and key insights.
- When calculations are needed, work through them step by step and show your
reasoning.
## Writing output
- When asked to produce output files (e.g., reports, summaries, filtered data),
use file_access_write to write them.
- Use appropriate file formats: CSV for tabular data, Markdown for reports.
- Confirm what you wrote and where.
## Important
- Never modify or delete the original input data files unless explicitly asked
to do so.
- If asked about data you haven't read yet, read it first before answering.
- Always explain your reasoning between tool calls so the user can follow along.
"""
PROMPTS = [
"What files do you have access to?",
"Read sales.csv and summarize what columns it contains and how many rows it has.",
"Calculate the total revenue (quantity * unit_price) per region and show the result as a table.",
(
"Save a markdown report named region_totals.md that contains the regional totals "
"and a one-paragraph summary of which region performed best."
),
"List the files again so I can confirm region_totals.md was created.",
]
async def main() -> None:
# 1. Resolve the working directory bundled alongside this script.
working_dir = Path(__file__).parent / "working"
# 2. Build the chat client.
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# 3. Wire up the file access provider against a file-system-backed store
# rooted at the sample's working/ folder. The provider injects its
# default instructions plus exposes six file_access_* tools to the
# agent for the duration of each run.
file_access = FileAccessProvider(store=FileSystemAgentFileStore(working_dir))
# 4. Create the agent and attach the provider. The file-access tools all
# require approval (approval_mode="always_require"). Developers can
# present these to the user for approval, or like in this case, auto-approve
# them via FileAccessProvider.all_tools_auto_approval_rule. Note that
# to use tool approval rules, the agent must have ToolApprovalMiddleware
# in its middleware stack.
async with Agent(
client=client,
name="DataAnalyst",
description="A data analyst assistant that reads, analyzes, and processes data files.",
instructions=INSTRUCTIONS,
context_providers=[file_access],
middleware=[ToolApprovalMiddleware(auto_approval_rules=[FileAccessProvider.all_tools_auto_approval_rule])],
) as agent:
# 5. Run all prompts inside one session so the conversation remains
# coherent across turns.
session = agent.create_session()
for prompt in PROMPTS:
print(f"\nUser: {prompt}")
response = await agent.run(prompt, session=session)
print(f"Assistant: {response}")
# 6. Show the final folder contents so the side effects of the run are
# visible to the reader.
print("\nFinal contents of working/:")
for path in sorted(working_dir.iterdir()):
print(f" - {path.name} ({path.stat().st_size} bytes)")
if __name__ == "__main__":
asyncio.run(main())
# Sample output (truncated):
#
# User: What files do you have access to?
# Assistant: I can see one file in the working directory: sales.csv.
#
# User: Read sales.csv and summarize what columns it contains and how many rows it has.
# Assistant: sales.csv has 50 data rows and 7 columns: date, product, category,
# quantity, unit_price, region, salesperson.
#
# User: Calculate the total revenue (quantity * unit_price) per region and show the result as a table.
# Assistant:
# | Region | Total Revenue |
# |--------|---------------|
# | North | $X,XXX.XX |
# | South | $X,XXX.XX |
# | West | $X,XXX.XX |
#
# User: Save a markdown report named region_totals.md ...
# Assistant: I wrote region_totals.md to the working folder.
#
# User: List the files again so I can confirm region_totals.md was created.
# Assistant: The working folder now contains: region_totals.md, sales.csv.
#
# Final contents of working/:
# - region_totals.md (NNN bytes)
# - sales.csv (3175 bytes)
@@ -0,0 +1,50 @@
date,product,category,quantity,unit_price,region,salesperson
2025-01-03,Laptop Pro 15,Electronics,2,1299.99,North,Alice
2025-01-05,Ergonomic Chair,Furniture,5,349.50,South,Bob
2025-01-07,Wireless Mouse,Electronics,12,24.99,North,Alice
2025-01-08,Standing Desk,Furniture,1,599.00,West,Carol
2025-01-10,USB-C Hub,Electronics,8,45.99,North,David
2025-01-12,Monitor 27in,Electronics,3,429.00,South,Bob
2025-01-14,Desk Lamp,Furniture,6,79.95,West,Carol
2025-01-15,Keyboard Mech,Electronics,4,149.99,North,Alice
2025-01-17,Filing Cabinet,Furniture,2,189.00,South,David
2025-01-20,Webcam HD,Electronics,10,89.99,West,Bob
2025-01-22,Laptop Pro 15,Electronics,1,1299.99,South,Carol
2025-01-24,Ergonomic Chair,Furniture,3,349.50,North,Alice
2025-01-25,Notebook Pack,Stationery,20,12.99,South,David
2025-01-27,Wireless Mouse,Electronics,15,24.99,West,Carol
2025-01-28,Whiteboard,Stationery,4,129.00,North,Bob
2025-01-30,Standing Desk,Furniture,2,599.00,South,Alice
2025-02-02,USB-C Hub,Electronics,6,45.99,West,David
2025-02-04,Monitor 27in,Electronics,2,429.00,North,Carol
2025-02-05,Desk Lamp,Furniture,8,79.95,South,Bob
2025-02-07,Keyboard Mech,Electronics,5,149.99,West,Alice
2025-02-09,Filing Cabinet,Furniture,1,189.00,North,David
2025-02-11,Webcam HD,Electronics,7,89.99,South,Carol
2025-02-13,Laptop Pro 15,Electronics,3,1299.99,West,Bob
2025-02-15,Notebook Pack,Stationery,30,12.99,North,Alice
2025-02-17,Ergonomic Chair,Furniture,4,349.50,South,David
2025-02-19,Wireless Mouse,Electronics,20,24.99,North,Carol
2025-02-20,Whiteboard,Stationery,2,129.00,West,Bob
2025-02-22,Standing Desk,Furniture,1,599.00,North,Alice
2025-02-24,USB-C Hub,Electronics,10,45.99,South,David
2025-02-26,Monitor 27in,Electronics,4,429.00,West,Carol
2025-02-28,Desk Lamp,Furniture,3,79.95,North,Bob
2025-03-02,Keyboard Mech,Electronics,6,149.99,South,Alice
2025-03-04,Filing Cabinet,Furniture,3,189.00,West,David
2025-03-06,Webcam HD,Electronics,9,89.99,North,Carol
2025-03-08,Laptop Pro 15,Electronics,2,1299.99,South,Bob
2025-03-10,Notebook Pack,Stationery,25,12.99,West,Alice
2025-03-12,Ergonomic Chair,Furniture,6,349.50,North,David
2025-03-14,Wireless Mouse,Electronics,18,24.99,South,Carol
2025-03-15,Whiteboard,Stationery,5,129.00,North,Bob
2025-03-17,Standing Desk,Furniture,3,599.00,West,Alice
2025-03-19,USB-C Hub,Electronics,7,45.99,North,David
2025-03-21,Monitor 27in,Electronics,5,429.00,South,Carol
2025-03-23,Desk Lamp,Furniture,4,79.95,West,Bob
2025-03-25,Keyboard Mech,Electronics,3,149.99,North,Alice
2025-03-27,Filing Cabinet,Furniture,2,189.00,South,David
2025-03-28,Webcam HD,Electronics,11,89.99,West,Carol
2025-03-29,Laptop Pro 15,Electronics,1,1299.99,North,Bob
2025-03-30,Notebook Pack,Stationery,15,12.99,South,Alice
2025-03-31,Ergonomic Chair,Furniture,2,349.50,West,David
1 date product category quantity unit_price region salesperson
2 2025-01-03 Laptop Pro 15 Electronics 2 1299.99 North Alice
3 2025-01-05 Ergonomic Chair Furniture 5 349.50 South Bob
4 2025-01-07 Wireless Mouse Electronics 12 24.99 North Alice
5 2025-01-08 Standing Desk Furniture 1 599.00 West Carol
6 2025-01-10 USB-C Hub Electronics 8 45.99 North David
7 2025-01-12 Monitor 27in Electronics 3 429.00 South Bob
8 2025-01-14 Desk Lamp Furniture 6 79.95 West Carol
9 2025-01-15 Keyboard Mech Electronics 4 149.99 North Alice
10 2025-01-17 Filing Cabinet Furniture 2 189.00 South David
11 2025-01-20 Webcam HD Electronics 10 89.99 West Bob
12 2025-01-22 Laptop Pro 15 Electronics 1 1299.99 South Carol
13 2025-01-24 Ergonomic Chair Furniture 3 349.50 North Alice
14 2025-01-25 Notebook Pack Stationery 20 12.99 South David
15 2025-01-27 Wireless Mouse Electronics 15 24.99 West Carol
16 2025-01-28 Whiteboard Stationery 4 129.00 North Bob
17 2025-01-30 Standing Desk Furniture 2 599.00 South Alice
18 2025-02-02 USB-C Hub Electronics 6 45.99 West David
19 2025-02-04 Monitor 27in Electronics 2 429.00 North Carol
20 2025-02-05 Desk Lamp Furniture 8 79.95 South Bob
21 2025-02-07 Keyboard Mech Electronics 5 149.99 West Alice
22 2025-02-09 Filing Cabinet Furniture 1 189.00 North David
23 2025-02-11 Webcam HD Electronics 7 89.99 South Carol
24 2025-02-13 Laptop Pro 15 Electronics 3 1299.99 West Bob
25 2025-02-15 Notebook Pack Stationery 30 12.99 North Alice
26 2025-02-17 Ergonomic Chair Furniture 4 349.50 South David
27 2025-02-19 Wireless Mouse Electronics 20 24.99 North Carol
28 2025-02-20 Whiteboard Stationery 2 129.00 West Bob
29 2025-02-22 Standing Desk Furniture 1 599.00 North Alice
30 2025-02-24 USB-C Hub Electronics 10 45.99 South David
31 2025-02-26 Monitor 27in Electronics 4 429.00 West Carol
32 2025-02-28 Desk Lamp Furniture 3 79.95 North Bob
33 2025-03-02 Keyboard Mech Electronics 6 149.99 South Alice
34 2025-03-04 Filing Cabinet Furniture 3 189.00 West David
35 2025-03-06 Webcam HD Electronics 9 89.99 North Carol
36 2025-03-08 Laptop Pro 15 Electronics 2 1299.99 South Bob
37 2025-03-10 Notebook Pack Stationery 25 12.99 West Alice
38 2025-03-12 Ergonomic Chair Furniture 6 349.50 North David
39 2025-03-14 Wireless Mouse Electronics 18 24.99 South Carol
40 2025-03-15 Whiteboard Stationery 5 129.00 North Bob
41 2025-03-17 Standing Desk Furniture 3 599.00 West Alice
42 2025-03-19 USB-C Hub Electronics 7 45.99 North David
43 2025-03-21 Monitor 27in Electronics 5 429.00 South Carol
44 2025-03-23 Desk Lamp Furniture 4 79.95 West Bob
45 2025-03-25 Keyboard Mech Electronics 3 149.99 North Alice
46 2025-03-27 Filing Cabinet Furniture 2 189.00 South David
47 2025-03-28 Webcam HD Electronics 11 89.99 West Carol
48 2025-03-29 Laptop Pro 15 Electronics 1 1299.99 North Bob
49 2025-03-30 Notebook Pack Stationery 15 12.99 South Alice
50 2025-03-31 Ergonomic Chair Furniture 2 349.50 West David
@@ -0,0 +1,47 @@
# Mem0 Context Provider Examples
[Mem0](https://mem0.ai/) is a self-improving memory layer for Large Language Models that enables applications to have long-term memory capabilities. The Agent Framework's Mem0 context provider integrates with Mem0's API to provide persistent memory across conversation sessions.
This folder contains examples demonstrating how to use the Mem0 context provider with the Agent Framework for persistent memory and context management across conversations.
## Examples
| File | Description |
|------|-------------|
| [`mem0_basic.py`](mem0_basic.py) | Basic example of using Mem0 context provider to store and retrieve user preferences across different conversation threads. |
| [`mem0_sessions.py`](mem0_sessions.py) | Example demonstrating different memory scoping strategies with Mem0. Covers user-scoped memory (memories shared across all sessions for the same user), agent-scoped memory (memories isolated per agent), and multiple agents with different memory configurations for personal vs. work contexts. |
| [`mem0_oss.py`](mem0_oss.py) | Example of using the Mem0 Open Source self-hosted version as the context provider. Demonstrates setup and configuration for local deployment. |
## Prerequisites
### Required Resources
1. [Mem0 API Key](https://app.mem0.ai/) - Sign up for a Mem0 account and get your API key - _or_ self-host [Mem0 Open Source](https://docs.mem0.ai/open-source/overview)
2. Azure AI project endpoint (used in these examples)
3. Azure CLI authentication (run `az login`)
## Configuration
### Environment Variables
Set the following environment variables:
**For Mem0 Platform:**
- `MEM0_API_KEY`: Your Mem0 API key (alternatively, pass it as `api_key` parameter to `Mem0Provider`). Not required if you are self-hosting [Mem0 Open Source](https://docs.mem0.ai/open-source/overview)
**For Mem0 Open Source:**
- `OPENAI_API_KEY`: Your OpenAI API key (used by Mem0 OSS for embedding generation and automatic memory extraction)
**For Azure AI:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI project endpoint
- `FOUNDRY_MODEL`: The name of your model deployment
## Key Concepts
### Memory Scoping
The Mem0 context provider supports scoping via identifiers:
- **User scope** (`user_id`): Associate memories with a specific user, shared across all sessions
- **Agent scope** (`agent_id`): Isolate memories per agent persona
- **Application scope** (`application_id`): Associate memories with an application context
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import uuid
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.mem0 import Mem0ContextProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def retrieve_company_report(company_code: str, detailed: bool) -> str:
if company_code != "CNTS":
raise ValueError("Company code not found")
if not detailed:
return "CNTS is a company that specializes in technology."
return (
"CNTS is a company that specializes in technology. "
"It had a revenue of $10 million in 2022. It has 100 employees."
)
async def main() -> None:
"""Example of memory usage with Mem0 context provider."""
print("=== Mem0 Context Provider Example ===")
# Each record in Mem0 should be associated with agent_id or user_id or application_id.
# In this example, we associate Mem0 records with user_id.
user_id = str(uuid.uuid4())
# For Azure authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
# For Mem0 authentication, set Mem0 API key via "api_key" parameter or MEM0_API_KEY environment variable.
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="FriendlyAssistant",
instructions="You are a friendly assistant.",
tools=retrieve_company_report,
context_providers=[Mem0ContextProvider(source_id="mem0", user_id=user_id)],
) as agent,
):
# First ask the agent to retrieve a company report with no previous context.
# The agent will not be able to invoke the tool, since it doesn't know
# the company code or the report format, so it should ask for clarification.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Now tell the agent the company code and the report format that you want to use
# and it should be able to invoke the tool and return the report.
query = "I always work with CNTS and I always want a detailed report format. Please remember and retrieve it."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Mem0 processes and indexes memories asynchronously.
# Wait for memories to be indexed before querying in a new thread.
# In production, consider implementing retry logic or using Mem0's
# eventual consistency handling instead of a fixed delay.
print("Waiting for memories to be processed...")
await asyncio.sleep(15) # Empirically determined delay for Mem0 indexing
print("\nRequest within a new session:")
# Create a new session for the agent.
# The new session has no context of the previous conversation.
session = agent.create_session()
# Since we have the mem0 component in the session, the agent should be able to
# retrieve the company report without asking for clarification, as it will
# be able to remember the user preferences from Mem0 component.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query, session=session)
print(f"Agent: {result}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import uuid
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.mem0 import Mem0ContextProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
from mem0 import AsyncMemory
# Load environment variables from .env file
load_dotenv()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def retrieve_company_report(company_code: str, detailed: bool) -> str:
if company_code != "CNTS":
raise ValueError("Company code not found")
if not detailed:
return "CNTS is a company that specializes in technology."
return (
"CNTS is a company that specializes in technology. "
"It had a revenue of $10 million in 2022. It has 100 employees."
)
async def main() -> None:
"""Example of memory usage with local Mem0 OSS context provider."""
print("=== Mem0 Context Provider Example ===")
# Each record in Mem0 should be associated with agent_id or user_id or application_id.
# In this example, we associate Mem0 records with user_id.
user_id = str(uuid.uuid4())
# For Azure authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
# By default, local Mem0 authenticates to your OpenAI using the OPENAI_API_KEY environment variable.
# See the Mem0 documentation for other LLM providers and authentication options.
local_mem0_client = AsyncMemory()
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="FriendlyAssistant",
instructions="You are a friendly assistant.",
tools=retrieve_company_report,
context_providers=[Mem0ContextProvider(source_id="mem0", user_id=user_id, mem0_client=local_mem0_client)],
) as agent,
):
# First ask the agent to retrieve a company report with no previous context.
# The agent will not be able to invoke the tool, since it doesn't know
# the company code or the report format, so it should ask for clarification.
query = "Please retrieve my company report"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Now tell the agent the company code and the report format that you want to use
# and it should be able to invoke the tool and return the report.
query = "I always work with CNTS and I always want a detailed report format. Please remember and retrieve it."
print("\nRequest within a new session:")
# Create a new session for the agent.
# The new session has no context of the previous conversation.
session = agent.create_session()
# Since we have the mem0 component in the session, the agent should be able to
# retrieve the company report without asking for clarification, as it will
# be able to remember the user preferences from Mem0 component.
result = await agent.run(query, session=session)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,170 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.mem0 import Mem0ContextProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_user_preferences(user_id: str) -> str:
"""Mock function to get user preferences."""
preferences = {
"user123": "Prefers concise responses and technical details",
"user456": "Likes detailed explanations with examples",
}
return preferences.get(user_id, "No specific preferences found")
async def example_user_scoped_memory() -> None:
"""Example 1: User-scoped memory (memories shared across all sessions for the same user)."""
print("1. User-Scoped Memory Example:")
print("-" * 40)
user_id = "user123"
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="UserMemoryAssistant",
instructions="You are an assistant that remembers user preferences across conversations.",
tools=get_user_preferences,
context_providers=[
Mem0ContextProvider(
source_id="mem0",
user_id=user_id,
)
],
) as user_agent,
):
# Store some preferences
query = "Remember that I prefer technical responses with code examples when discussing programming."
print(f"User: {query}")
result = await user_agent.run(query)
print(f"Agent: {result}\n")
# Create a new session - memories should still be accessible via user_id scoping
new_session = user_agent.create_session()
query = "What do you know about my preferences?"
print(f"User (new session): {query}")
result = await user_agent.run(query, session=new_session)
print(f"Agent: {result}\n")
async def example_agent_scoped_memory() -> None:
"""Example 2: Agent-scoped memory (memories isolated per agent_id).
Note: Use different agent_id values to isolate memories between different
agent personas, even when the user_id is the same.
"""
print("2. Agent-Scoped Memory Example:")
print("-" * 40)
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="ScopedMemoryAssistant",
instructions="You are an assistant with agent-scoped memory.",
tools=get_user_preferences,
context_providers=[
Mem0ContextProvider(
source_id="mem0",
agent_id="scoped_assistant",
)
],
) as scoped_agent,
):
query = (
"Remember that I'm working on a Python project about data analysis "
"and I prefer using pandas and matplotlib."
)
print(f"User: {query}")
result = await scoped_agent.run(query)
print(f"Agent: {result}\n")
new_session = scoped_agent.create_session()
query = "What do you know about my current project and preferences?"
print(f"User (new session): {query}")
result = await scoped_agent.run(query, session=new_session)
print(f"Agent: {result}\n")
async def example_multiple_agents() -> None:
"""Example 3: Multiple agents with different memory configurations."""
print("3. Multiple Agents with Different Memory Configurations:")
print("-" * 40)
agent_id_1 = "agent_personal"
agent_id_2 = "agent_work"
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="PersonalAssistant",
instructions="You are a personal assistant that helps with personal tasks.",
context_providers=[
Mem0ContextProvider(
source_id="mem0",
agent_id=agent_id_1,
)
],
) as personal_agent,
Agent(
client=FoundryChatClient(credential=credential),
name="WorkAssistant",
instructions="You are a work assistant that helps with professional tasks.",
context_providers=[
Mem0ContextProvider(
source_id="mem0",
agent_id=agent_id_2,
)
],
) as work_agent,
):
# Store personal information
query = "Remember that I like to exercise at 6 AM and prefer outdoor activities."
print(f"User to Personal Agent: {query}")
result = await personal_agent.run(query)
print(f"Personal Agent: {result}\n")
# Store work information
query = "Remember that I have team meetings every Tuesday at 2 PM."
print(f"User to Work Agent: {query}")
result = await work_agent.run(query)
print(f"Work Agent: {result}\n")
# Test memory isolation
query = "What do you know about my schedule?"
print(f"User to Personal Agent: {query}")
result = await personal_agent.run(query)
print(f"Personal Agent: {result}\n")
print(f"User to Work Agent: {query}")
result = await work_agent.run(query)
print(f"Work Agent: {result}\n")
async def main() -> None:
"""Run all Mem0 memory management examples."""
print("=== Mem0 Memory Management Example ===\n")
await example_user_scoped_memory()
await example_agent_scoped_memory()
await example_multiple_agents()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,19 @@
# Neo4j Context Providers
Neo4j offers two context providers for the Agent Framework, each serving a different purpose:
| | [Neo4j Memory](../neo4j_memory/README.md) | [Neo4j GraphRAG](../../../05-end-to-end/neo4j_graphrag/README.md) |
|---|---|---|
| **What it does** | Read-write memory — stores conversations, builds knowledge graphs, learns from interactions | Read-only retrieval from a pre-existing knowledge base with optional graph traversal |
| **Data source** | Agent interactions (grows over time) | Pre-loaded documents and indexes |
| **Python package** | [`neo4j-agent-memory`](https://pypi.org/project/neo4j-agent-memory/) | [`agent-framework-neo4j`](https://pypi.org/project/agent-framework-neo4j/) |
| **Database setup** | Empty — creates its own schema | Requires pre-indexed documents with vector or fulltext indexes |
| **Example use case** | "Remember my preferences", "What did we discuss last time?" | "Search our documents", "What risks does Acme Corp face?" |
## Which should I use?
**Use [Neo4j Memory](../neo4j_memory/README.md)** when your agent needs to remember things across sessions — user preferences, past conversations, extracted entities, and reasoning traces. The memory provider writes to the database on every interaction, building a knowledge graph that grows over time.
**Use [Neo4j GraphRAG](../../../05-end-to-end/neo4j_graphrag/README.md)** when your agent needs to search an existing knowledge base — documents, articles, product catalogs — and optionally enrich results by traversing graph relationships. The GraphRAG provider is read-only and does not modify your data.
You can use both together: GraphRAG for domain knowledge retrieval, Memory for personalization and learning.
@@ -0,0 +1,9 @@
# Neo4j Memory Context Provider
[Neo4j Agent Memory](https://github.com/neo4j-labs/agent-memory) is a graph-native memory system for AI agents that stores conversations, builds knowledge graphs from interactions, and lets agents learn from their own reasoning — all backed by Neo4j.
For full documentation, installation instructions, code examples, and configuration details, see the [Neo4j Memory integration guide on Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/neo4j-memory).
For a runnable example, see the [retail assistant sample](https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant).
For help choosing between the Memory and GraphRAG providers, see the [Neo4j Context Providers overview](../neo4j/README.md).
@@ -0,0 +1,123 @@
# Redis Context Provider Examples
The Redis context provider enables persistent, searchable memory for your agents using Redis (RediSearch). It supports fulltext search and optional hybrid search with vector embeddings, letting agents remember and retrieve user context across sessions and threads.
This folder contains an example demonstrating how to use the Redis context provider with the Agent Framework.
## Examples
| File | Description |
|------|-------------|
| [`azure_redis_conversation.py`](azure_redis_conversation.py) | Demonstrates conversation persistence with RedisHistoryProvider and Azure Redis with Azure AD (Entra ID) authentication using credential provider. |
| [`redis_basics.py`](redis_basics.py) | Shows standalone provider usage and agent integration. Demonstrates writing messages to Redis, retrieving context via fulltext or hybrid vector search, and persisting preferences across threads. Also includes a simple tool example whose outputs are remembered. |
| [`redis_conversation.py`](redis_conversation.py) | Simple example showing conversation persistence with RedisContextProvider using traditional connection string authentication. |
| [`redis_sessions.py`](redis_sessions.py) | Demonstrates memory scoping strategies. Includes: (1) global memory scope with `application_id`, `agent_id`, and `user_id` shared across operations; (2) hybrid vector search using a custom OpenAI vectorizer for richer context retrieval; and (3) multiple agents with isolated memory via different `agent_id` values. |
## Prerequisites
### Required resources
1. A running Redis with RediSearch (Redis Stack or a managed service)
2. Python environment with Agent Framework Redis extra installed
3. Azure AI Foundry project endpoint and Azure OpenAI Responses deployment
4. Optional: OpenAI API key if using vector embeddings
### Install the package
```bash
pip install "agent-framework-redis"
```
## Running Redis
Pick one option:
### Option A: Docker (local Redis Stack)
```bash
docker run --name redis -p 6379:6379 -d redis:8.0.3
```
### Option B: Redis Cloud
Create a free database and get the connection URL at `https://redis.io/cloud/`.
### Option C: Azure Managed Redis
See quickstart: `https://learn.microsoft.com/azure/redis/quickstart-create-managed-redis`
## Configuration
### Environment variables
- `FOUNDRY_PROJECT_ENDPOINT` (required): Azure AI Foundry project endpoint for `FoundryChatClient`
- `FOUNDRY_MODEL` (required): Foundry model deployment name
- `OPENAI_API_KEY` (optional): Required only if you set `vectorizer_choice="openai"` to enable hybrid search.
### Provider configuration highlights
The provider supports both fulltext only and hybrid vector search:
- Set `vectorizer_choice` to `"openai"` or `"hf"` to enable embeddings and hybrid search.
- When using a vectorizer, also set `vector_field_name` (e.g., `"vector"`).
- Partition fields for scoping memory: `application_id`, `agent_id`, `user_id`.
- Index management: `index_name`, `overwrite_redis_index`, `drop_redis_index`.
## What the example does
`redis_basics.py` walks through three scenarios:
1. Standalone provider usage: adds messages and retrieves context via `invoking`.
2. Agent integration: teaches the agent a preference and verifies it is remembered across turns.
3. Agent + tool: calls a sample tool (flight search) and then asks the agent to recall details remembered from the tool output.
It uses `FoundryChatClient` for chat and, in some steps, optional OpenAI embeddings for hybrid search.
## How to run
1) Start Redis (see options above). For local default, ensure it's reachable at `redis://localhost:6379`.
2) Set Azure Foundry/OpenAI responses environment variables:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://<resource>.services.ai.azure.com/api/projects/<project>"
export FOUNDRY_MODEL="<deployment-name>"
```
3) (Optional) Set your OpenAI key if using embeddings:
```bash
export OPENAI_API_KEY="<your key>"
```
4) Run the example:
```bash
python redis_basics.py
```
You should see the agent responses and, when using embeddings, context retrieved from Redis. The example includes commented debug helpers you can print, such as index info or all stored docs.
## Key concepts
### Memory scoping
- Global scope: set `application_id`, `agent_id`, or `user_id` on the provider to filter memory.
- Agent isolation: use different `agent_id` values to keep memories separated for different agent personas.
### Hybrid vector search (optional)
- Enable by setting `vectorizer_choice` to `"openai"` (requires `OPENAI_API_KEY`) or `"hf"` (offline model).
- Provide `vector_field_name` (e.g., `"vector"`); other vector settings have sensible defaults.
### Index lifecycle controls
- `overwrite_redis_index` and `drop_redis_index` help recreate indexes during iteration.
## Troubleshooting
- Ensure at least one of `application_id`, `agent_id`, or `user_id` is set; the provider requires a scope.
- Verify `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL` are set for the chat client.
- If using embeddings, verify `OPENAI_API_KEY` is set and reachable.
- Make sure Redis exposes RediSearch (Redis Stack image or managed service with search enabled).
@@ -0,0 +1,140 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure Managed Redis History Provider with Azure AD Authentication
This example demonstrates how to use Azure Managed Redis with Azure AD authentication
to persist conversation history using RedisHistoryProvider.
Key concepts:
- RedisHistoryProvider = durable storage (where messages are persisted)
- AgentSession = conversation identity (which conversation the messages belong to)
Requirements:
- Azure Managed Redis instance with Azure AD authentication enabled
- Azure credentials configured (az login or managed identity)
- agent-framework-redis: pip install agent-framework-redis
- azure-identity: pip install azure-identity
Environment Variables:
- AZURE_REDIS_HOST: Your Azure Managed Redis host (e.g., myredis.redis.cache.windows.net)
- FOUNDRY_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- FOUNDRY_MODEL: Azure OpenAI Responses deployment name
- AZURE_USER_OBJECT_ID: Your Azure AD User Object ID for authentication
"""
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.redis import RedisHistoryProvider
from azure.identity import AzureCliCredential
from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential
from dotenv import load_dotenv
from redis.credentials import CredentialProvider
load_dotenv()
class AzureCredentialProvider(CredentialProvider):
"""Credential provider for Azure AD authentication with Redis Enterprise."""
def __init__(self, azure_credential: AsyncAzureCliCredential, user_object_id: str):
self.azure_credential = azure_credential
self.user_object_id = user_object_id
async def get_credentials_async(self) -> tuple[str] | tuple[str, str]:
"""Get Azure AD token for Redis authentication.
Returns (username, token) where username is the Azure user's Object ID.
"""
token = await self.azure_credential.get_token("https://redis.azure.com/.default")
return (self.user_object_id, token.token)
async def main() -> None:
redis_host = os.environ.get("AZURE_REDIS_HOST")
if not redis_host:
print("ERROR: Set AZURE_REDIS_HOST environment variable")
return
# For Azure Redis with Entra ID, username must be your Object ID
user_object_id = os.environ.get("AZURE_USER_OBJECT_ID")
if not user_object_id:
print("ERROR: Set AZURE_USER_OBJECT_ID environment variable")
print("Get your Object ID from the Azure Portal")
return
# 1. Create Azure CLI credential provider (uses 'az login' credentials)
azure_credential = AsyncAzureCliCredential()
credential_provider = AzureCredentialProvider(azure_credential, user_object_id)
# 2. Create Azure Redis history provider (the durable storage backend)
history_provider = RedisHistoryProvider(
source_id="redis_memory",
credential_provider=credential_provider,
host=redis_host,
port=10000,
ssl=True,
key_prefix="chat_messages",
max_messages=100,
)
# 3. Create chat client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# 4. Create agent with Azure Redis history provider
agent = Agent(
client=client,
name="AzureRedisAssistant",
instructions="You are a helpful assistant.",
context_providers=[history_provider],
)
# 5. Create a session to provide conversation identity.
# The session ID is used as the Redis key — all runs sharing the same session
# will read/write the same conversation history in Redis.
session = agent.create_session()
# 6. Conversation — each run passes the same session for continuity
query = "Remember that I enjoy gumbo"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
# Ask the agent to recall the stored preference; it should retrieve from memory
query = "What do I enjoy?"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "What did I say to you just now?"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "Remember that I have a meeting at 3pm tomorrow"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "Tulips are red"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "What was the first thing I said to you this conversation?"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
# Cleanup
await azure_credential.close()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,281 @@
# Copyright (c) Microsoft. All rights reserved.
"""Redis Context Provider: Basic usage and agent integration
This example demonstrates how to use the Redis context provider to persist and
retrieve conversational memory for agents. It covers three progressively more
realistic scenarios:
1) Standalone provider usage ("basic cache")
- Write messages to Redis and retrieve relevant context using full-text or
hybrid vector search.
2) Agent + provider
- Connect the provider to an agent so the agent can store user preferences
and recall them across turns.
3) Agent + provider + tool memory
- Expose a simple tool to the agent, then verify that details from the tool
outputs are captured and retrievable as part of the agent's memory.
Requirements:
- A Redis instance with RediSearch enabled (e.g., Redis Stack)
- agent-framework with the Redis extra installed: pip install "agent-framework-redis"
- Optionally an OpenAI API key if enabling embeddings for hybrid search
Run:
python redis_basics.py
"""
import asyncio
import os
from agent_framework import Agent, Message, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
# Load environment variables from .env file
load_dotenv()
# Default Redis URL for local Redis Stack.
# Override via the REDIS_URL environment variable for remote or authenticated instances.
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def search_flights(origin_airport_code: str, destination_airport_code: str, detailed: bool = False) -> str:
"""Simulated flight-search tool to demonstrate tool memory.
The agent can call this function, and the returned details can be stored
by the Redis context provider. We later ask the agent to recall facts from
these tool results to verify memory is working as expected.
"""
# Minimal static catalog used to simulate a tool's structured output
flights = {
("JFK", "LAX"): {
"airline": "SkyJet",
"duration": "6h 15m",
"price": 325,
"cabin": "Economy",
"baggage": "1 checked bag",
},
("SFO", "SEA"): {
"airline": "Pacific Air",
"duration": "2h 5m",
"price": 129,
"cabin": "Economy",
"baggage": "Carry-on only",
},
("LHR", "DXB"): {
"airline": "EuroWings",
"duration": "6h 50m",
"price": 499,
"cabin": "Business",
"baggage": "2 bags included",
},
}
route = (origin_airport_code.upper(), destination_airport_code.upper())
if route not in flights:
return f"No flights found between {origin_airport_code} and {destination_airport_code}"
flight = flights[route]
if not detailed:
return f"Flights available from {origin_airport_code} to {destination_airport_code}."
return (
f"{flight['airline']} operates flights from {origin_airport_code} to {destination_airport_code}. "
f"Duration: {flight['duration']}. "
f"Price: ${flight['price']}. "
f"Cabin: {flight['cabin']}. "
f"Baggage policy: {flight['baggage']}."
)
def create_chat_client() -> FoundryChatClient:
"""Create a FoundryChatClient using a Foundry project endpoint."""
return FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
async def main() -> None:
"""Walk through provider-only, agent integration, and tool-memory scenarios.
Helpful debugging (uncomment when iterating):
- print(await provider.redis_index.info())
- print(await provider.search_all())
"""
print("1. Standalone provider usage:")
print("-" * 40)
# Create a provider with partition scope and OpenAI embeddings
# Please set OPENAI_API_KEY to use the OpenAI vectorizer.
# For chat responses, also set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL.
# We attach an embedding vectorizer so the provider can perform hybrid (text + vector)
# retrieval. If you prefer text-only retrieval, instantiate RedisContextProvider without the
# 'vectorizer' and vector_* parameters.
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url=REDIS_URL),
)
# The provider manages persistence and retrieval. application_id/agent_id/user_id
# scope data for multi-tenant separation; thread_id (set later) narrows to a
# specific conversation.
provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_basics",
application_id="matrix_of_kermits",
agent_id="agent_kermit",
user_id="kermit",
redis_vectorizer=vectorizer,
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
)
# Build sample chat messages to persist to Redis
messages = [
Message("user", ["runA CONVO: User Message"]),
Message("assistant", ["runA CONVO: Assistant Message"]),
Message("system", ["runA CONVO: System Message"]),
]
# Use the provider's before_run/after_run API to store and retrieve messages.
# In practice, the agent handles this automatically; this shows the low-level API.
from typing import cast
from agent_framework import AgentSession, SessionContext, SupportsAgentRun
session = AgentSession(session_id="runA")
context = SessionContext(input_messages=messages)
state = session.state
# Store messages via after_run
await provider.after_run(agent=cast(SupportsAgentRun, None), session=session, context=context, state=state)
# Retrieve relevant memories via before_run
query_context = SessionContext(input_messages=[Message("system", ["B: Assistant Message"])])
await provider.before_run(agent=cast(SupportsAgentRun, None), session=session, context=query_context, state=state)
# Inspect retrieved memories that would be injected into instructions
# (Debug-only output so you can verify retrieval works as expected.)
print("Before Run Result:")
print(query_context)
# Drop / delete the provider index in Redis
await provider.redis_index.delete()
# --- Agent + provider: teach and recall a preference ---
print("\n2. Agent + provider: teach and recall a preference")
print("-" * 40)
# Fresh provider for the agent demo (recreates index)
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url=REDIS_URL),
)
# Recreate a clean index so the next scenario starts fresh
provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_basics_2",
prefix="context_2",
application_id="matrix_of_kermits",
agent_id="agent_kermit",
user_id="kermit",
redis_vectorizer=vectorizer,
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
)
# Create chat client for the agent
client = create_chat_client()
# Create agent wired to the Redis context provider. The provider automatically
# persists conversational details and surfaces relevant context on each turn.
agent = Agent(
client=client,
name="MemoryEnhancedAssistant",
instructions=(
"You are a helpful assistant. Personalize replies using provided context. "
"Before answering, always check for stored context"
),
tools=[],
context_providers=[provider],
)
# Teach a user preference; the agent writes this to the provider's memory
query = "Remember that I enjoy glugenflorgle"
result = await agent.run(query)
print("User: ", query)
print("Agent: ", result)
# Ask the agent to recall the stored preference; it should retrieve from memory
query = "What do I enjoy?"
result = await agent.run(query)
print("User: ", query)
print("Agent: ", result)
# Drop / delete the provider index in Redis
await provider.redis_index.delete()
# --- Agent + provider + tool: store and recall tool-derived context ---
print("\n3. Agent + provider + tool: store and recall tool-derived context")
print("-" * 40)
# Text-only provider (full-text search only). Omits vectorizer and related params.
provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_basics_3",
prefix="context_3",
application_id="matrix_of_kermits",
agent_id="agent_kermit",
user_id="kermit",
)
# Create agent exposing the flight search tool. Tool outputs are captured by the
# provider and become retrievable context for later turns.
client = create_chat_client()
agent = Agent(
client=client,
name="MemoryEnhancedAssistant",
instructions=(
"You are a helpful assistant. Personalize replies using provided context. "
"Before answering, always check for stored context"
),
tools=search_flights,
context_providers=[provider],
)
# Invoke the tool; outputs become part of memory/context
query = "Are there any flights from new york city (jfk) to la? Give me details"
result = await agent.run(query)
print("User: ", query)
print("Agent: ", result)
# Verify the agent can recall tool-derived context
query = "Which flight did I ask about?"
result = await agent.run(query)
print("User: ", query)
print("Agent: ", result)
# Drop / delete the provider index in Redis
await provider.redis_index.delete()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,124 @@
# Copyright (c) Microsoft. All rights reserved.
"""Redis Context Provider: Basic usage and agent integration
This example demonstrates how to use the Redis context provider to persist
conversational details. Pass it as a constructor argument to create_agent.
Note: For session history persistence, see RedisHistoryProvider in the
conversations/redis_history_provider.py sample. RedisContextProvider is for
AI context (RAG, memories), while RedisHistoryProvider stores message history.
Requirements:
- A Redis instance with RediSearch enabled (e.g., Redis Stack)
- agent-framework with the Redis extra installed: pip install "agent-framework-redis"
- Optionally an OpenAI API key if enabling embeddings for hybrid search
Run:
python redis_conversation.py
"""
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
load_dotenv()
# Default Redis URL for local Redis Stack.
# Override via the REDIS_URL environment variable for remote or authenticated instances.
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
async def main() -> None:
"""Walk through provider and chat message store usage.
Helpful debugging (uncomment when iterating):
- print(await provider.redis_index.info())
- print(await provider.search_all())
"""
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url=REDIS_URL),
)
provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_conversation",
prefix="redis_conversation",
application_id="matrix_of_kermits",
agent_id="agent_kermit",
user_id="kermit",
redis_vectorizer=vectorizer,
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
)
# Create chat client for the agent
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create agent wired to the Redis context provider. The provider automatically
# persists conversational details and surfaces relevant context on each turn.
agent = Agent(
client=client,
name="MemoryEnhancedAssistant",
instructions=(
"You are a helpful assistant. Personalize replies using provided context. "
"Before answering, always check for stored context"
),
tools=[],
context_providers=[provider],
)
# Create a session to manage conversation state
session = agent.create_session()
# Teach a user preference; the agent writes this to the provider's memory
query = "Remember that I enjoy gumbo"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
# Ask the agent to recall the stored preference; it should retrieve from memory
query = "What do I enjoy?"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "What did I say to you just now?"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "Remember that I have a meeting at 3pm tomorro"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "Tulips are red"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
query = "What was the first thing I said to you this conversation?"
result = await agent.run(query, session=session)
print("User: ", query)
print("Agent: ", result)
# Drop / delete the provider index in Redis
await provider.redis_index.delete()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,257 @@
# Copyright (c) Microsoft. All rights reserved.
"""Redis Context Provider: Memory scoping examples
This sample demonstrates how conversational memory can be scoped when using the
Redis context provider. It covers three scenarios:
1) Global memory scope
- Use application_id, agent_id, and user_id to share memories across
all operations/sessions.
2) Hybrid vector search
- Use a custom OpenAI vectorizer with the provider for hybrid vector search.
Demonstrates combining full-text and semantic search for richer context
retrieval.
3) Multiple agents with isolated memory
- Use different agent_id values to keep memories separated for different
agent personas, even when the user_id is the same.
Requirements:
- A Redis instance with RediSearch enabled (e.g., Redis Stack)
- agent-framework with the Redis extra installed: pip install "agent-framework-redis"
- Optionally an OpenAI API key for the chat client in this demo
Run:
python redis_sessions.py
"""
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
# Load environment variables from .env file
load_dotenv()
# Default Redis URL for local Redis Stack.
# Override via the REDIS_URL environment variable for remote or authenticated instances.
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
# Please set OPENAI_API_KEY to use the OpenAI vectorizer.
# For chat responses, also set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL.
def create_chat_client() -> FoundryChatClient:
"""Create a FoundryChatClient using a Foundry project endpoint."""
return FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
async def example_global_memory_scope() -> None:
"""Example 1: Global memory scope (memories shared across all operations)."""
print("1. Global Memory Scope Example:")
print("-" * 40)
client = create_chat_client()
provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_threads_global",
application_id="threads_demo_app",
agent_id="threads_demo_agent",
user_id="threads_demo_user",
)
agent = Agent(
client=client,
name="GlobalMemoryAssistant",
instructions=(
"You are a helpful assistant. Personalize replies using provided context. "
"Before answering, always check for stored context containing information"
),
tools=[],
context_providers=[provider],
)
# Store a preference in the global scope
query = "Remember that I prefer technical responses with code examples when discussing programming."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Create a new session - memories should still be accessible due to global scope
new_session = agent.create_session()
query = "What technical responses do I prefer?"
print(f"User (new session): {query}")
result = await agent.run(query, session=new_session)
print(f"Agent: {result}\n")
# Clean up the Redis index
await provider.redis_index.delete()
async def example_hybrid_vector_search() -> None:
"""Example 2: Hybrid vector search with custom vectorizer.
Demonstrates using a custom OpenAI vectorizer for hybrid vector search,
combining full-text and semantic search for richer context retrieval.
"""
print("2. Hybrid Vector Search Example:")
print("-" * 40)
client = create_chat_client()
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url=REDIS_URL),
)
provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_threads_dynamic",
application_id="threads_demo_app",
agent_id="threads_demo_agent",
user_id="threads_demo_user",
redis_vectorizer=vectorizer,
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
)
agent = Agent(
client=client,
name="HybridSearchAssistant",
instructions="You are an assistant with hybrid vector search for richer context retrieval.",
context_providers=[provider],
)
# Store some information
query = "Remember that for this conversation, I'm working on a Python project about data analysis."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Test memory retrieval via hybrid search
query = "What project am I working on?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Store more information
query = "Also remember that I prefer using pandas and matplotlib for this project."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Test comprehensive memory retrieval
query = "What do you know about my current project and preferences?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
# Clean up the Redis index
await provider.redis_index.delete()
async def example_multiple_agents() -> None:
"""Example 3: Multiple agents with different memory configurations (isolated via agent_id) but within 1 index."""
print("3. Multiple Agents with Different Memory Configurations:")
print("-" * 40)
client = create_chat_client()
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
api_config={"api_key": os.getenv("OPENAI_API_KEY")},
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url=REDIS_URL),
)
personal_provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_threads_agents",
application_id="threads_demo_app",
agent_id="agent_personal",
user_id="threads_demo_user",
redis_vectorizer=vectorizer,
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
)
personal_agent = Agent(
client=client,
name="PersonalAssistant",
instructions="You are a personal assistant that helps with personal tasks.",
context_providers=[personal_provider],
)
work_provider = RedisContextProvider(
source_id="redis_context",
redis_url=REDIS_URL,
index_name="redis_threads_agents",
application_id="threads_demo_app",
agent_id="agent_work",
user_id="threads_demo_user",
redis_vectorizer=vectorizer,
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
)
work_agent = Agent(
client=client,
name="WorkAssistant",
instructions="You are a work assistant that helps with professional tasks.",
context_providers=[work_provider],
)
# Store personal information
query = "Remember that I like to exercise at 6 AM and prefer outdoor activities."
print(f"User to Personal Agent: {query}")
result = await personal_agent.run(query)
print(f"Personal Agent: {result}\n")
# Store work information
query = "Remember that I have team meetings every Tuesday at 2 PM."
print(f"User to Work Agent: {query}")
result = await work_agent.run(query)
print(f"Work Agent: {result}\n")
# Test memory isolation
query = "What do you know about my schedule?"
print(f"User to Personal Agent: {query}")
result = await personal_agent.run(query)
print(f"Personal Agent: {result}\n")
print(f"User to Work Agent: {query}")
result = await work_agent.run(query)
print(f"Work Agent: {result}\n")
# Clean up the Redis index (shared)
await work_provider.redis_index.delete()
async def main() -> None:
print("=== Redis Memory Scoping Examples ===\n")
await example_global_memory_scope()
await example_hybrid_vector_search()
await example_multiple_agents()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,128 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from contextlib import suppress
from typing import Any
from agent_framework import Agent, AgentSession, ContextProvider, SessionContext, SupportsChatGetResponse
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel
# Load environment variables from .env file
load_dotenv()
class UserInfo(BaseModel):
name: str | None = None
age: int | None = None
class UserInfoMemory(ContextProvider):
DEFAULT_SOURCE_ID = "user_info_memory"
def __init__(self, source_id: str = DEFAULT_SOURCE_ID, *, client: SupportsChatGetResponse, **kwargs: Any):
"""Create the memory.
If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
"""
super().__init__(source_id)
self._chat_client = client
async def after_run(
self,
*,
agent: Any,
session: AgentSession | None,
context: SessionContext,
state: dict[str, Any],
) -> None:
"""Extract user information from messages after each agent call."""
# ensure you get all the messages you want to parse from, including the input in this case.
request_messages = context.get_messages(include_input=True, include_response=True)
# Check if we need to extract user info from user messages
user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role == "user"] # type: ignore
if (state["user_info"].name is None or state["user_info"].age is None) and user_messages:
with suppress(Exception):
# Use the chat client to extract structured information
result = await self._chat_client.get_response(
messages=request_messages, # type: ignore
options={
"instructions": "Extract the user's name and age from the message if present. "
"If not present return nulls.",
"response_format": UserInfo,
},
)
# Update user info with extracted data
with suppress(Exception):
extracted = result.value
user_info = state["user_info"]
if not isinstance(extracted, UserInfo) or not isinstance(user_info, UserInfo):
return
if user_info.name is None and extracted.name:
user_info.name = extracted.name
if user_info.age is None and extracted.age:
user_info.age = extracted.age
async def before_run(
self,
*,
agent: Any,
session: AgentSession | None,
context: SessionContext,
state: dict[str, Any],
) -> None:
"""Provide user information context before each agent call."""
state.setdefault("user_info", UserInfo())
context.extend_instructions(
self.source_id,
"Ask the user for their name and politely decline to answer any questions until they provide it."
if state["user_info"].name is None
else f"The user's name is {state['user_info'].name}.",
)
context.extend_instructions(
self.source_id,
"Ask the user for their age and politely decline to answer any questions until they provide it."
if state["user_info"].age is None
else f"The user's age is {state['user_info'].age}.",
)
async def main():
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
context_name = UserInfoMemory.DEFAULT_SOURCE_ID
# Create the memory provider
memory_provider = UserInfoMemory(context_name, client=client)
# Create the agent with memory
async with Agent(
client=client,
instructions="You are a friendly assistant. Always address the user by their name.",
context_providers=[memory_provider],
) as agent:
# Create a new session for the conversation
session = agent.create_session()
for msg in ["Hello, what is the square root of 9?", "My name is Ruaidhrí", "I am 20 years old"]:
print(f"User: {msg}")
print(f"Assistant: {await agent.run(msg, session=session)}")
# Access the memory component and inspect the memories
print()
print(f"MEMORY - User Name: {session.state[context_name]['user_info'].name}")
print(f"MEMORY - User Age: {session.state[context_name]['user_info'].age}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,56 @@
# Conversation & Session Management Samples
These samples demonstrate different approaches to managing conversation history and session state in Agent Framework.
## Samples
| File | Description |
|------|-------------|
| [`suspend_resume_session.py`](suspend_resume_session.py) | Suspend and resume conversation sessions, comparing service-managed sessions (Azure AI Foundry) with in-memory sessions (OpenAI). |
| [`custom_history_provider.py`](custom_history_provider.py) | Implement a custom history provider by extending `HistoryProvider`, enabling conversation persistence in your preferred storage backend. |
| [`file_history_provider.py`](file_history_provider.py) | Use the experimental `FileHistoryProvider` with `FoundryChatClient` and a function tool so the local JSON Lines file shows the full tool-calling loop. |
| [`file_history_provider_conversation_persistence.py`](file_history_provider_conversation_persistence.py) | Persist a tool-driven weather conversation with `FileHistoryProvider`, inspect the stored JSONL records, and continue with another city. |
| [`cosmos_history_provider.py`](cosmos_history_provider.py) | Use Azure Cosmos DB as a history provider for durable conversation storage with `CosmosHistoryProvider`. |
| [`cosmos_history_provider_conversation_persistence.py`](cosmos_history_provider_conversation_persistence.py) | Persist and resume conversations across application restarts using `CosmosHistoryProvider` — serialize session state, restore it, and continue with full Cosmos DB history. |
| [`cosmos_history_provider_messages.py`](cosmos_history_provider_messages.py) | Direct message history operations — retrieve stored messages as a transcript, clear session history, and verify data deletion. |
| [`cosmos_history_provider_sessions.py`](cosmos_history_provider_sessions.py) | Multi-session and multi-tenant management — per-tenant session isolation, `list_sessions()` to enumerate, switch between sessions, and resume specific conversations. |
| [`redis_history_provider.py`](redis_history_provider.py) | Use Redis as a history provider for persistent conversation history storage across sessions. |
## Prerequisites
**For `suspend_resume_session.py`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint (service-managed session)
- `FOUNDRY_MODEL`: The Foundry model deployment name
- `OPENAI_API_KEY`: Your OpenAI API key (in-memory session)
- Azure CLI authentication (`az login`)
**For `custom_history_provider.py`:**
- `OPENAI_API_KEY`: Your OpenAI API key
**For `file_history_provider.py`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: The Foundry model deployment name
- Azure CLI authentication (`az login`)
- The sample writes plaintext JSONL conversation logs to disk; use a trusted
local directory and avoid treating the history files as secure secret storage
**For `file_history_provider_conversation_persistence.py`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: The Foundry model deployment name
- Azure CLI authentication (`az login`)
- The sample writes plaintext JSONL conversation logs to disk; use a trusted
local directory and avoid treating the history files as secure secret storage
**For Cosmos DB samples (`cosmos_history_provider*.py`):**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: The Foundry model deployment name
- `AZURE_COSMOS_ENDPOINT`: Your Azure Cosmos DB account endpoint
- `AZURE_COSMOS_DATABASE_NAME`: The database that stores conversation history
- `AZURE_COSMOS_CONTAINER_NAME`: The container that stores conversation history
- Either `AZURE_COSMOS_KEY` or Azure CLI authentication (`az login`)
**For `redis_history_provider.py`:**
- `OPENAI_API_KEY`: Your OpenAI API key
- A running Redis server — default URL is `redis://localhost:6379`
- Override via the `REDIS_URL` environment variable for remote or authenticated instances
- Quickstart with Docker: `docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest`
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import CosmosHistoryProvider
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file.
load_dotenv()
"""
This sample demonstrates CosmosHistoryProvider as an agent history provider.
Key components:
- FoundryChatClient configured with an Azure AI project endpoint
- CosmosHistoryProvider configured for Cosmos DB-backed message history
- Provider-configured container name with session_id as partition key
Environment variables:
FOUNDRY_PROJECT_ENDPOINT
FOUNDRY_MODEL
AZURE_COSMOS_ENDPOINT
AZURE_COSMOS_DATABASE_NAME
AZURE_COSMOS_CONTAINER_NAME
Optional:
AZURE_COSMOS_KEY
"""
async def main() -> None:
"""Run the Cosmos history provider sample with an Agent."""
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
cosmos_key = os.getenv("AZURE_COSMOS_KEY")
if (
not project_endpoint
or not model
or not cosmos_endpoint
or not cosmos_database_name
or not cosmos_container_name
):
print(
"Please set FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL, "
"AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_DATABASE_NAME, and AZURE_COSMOS_CONTAINER_NAME."
)
return
# 1. Create an Azure credential and a CosmosHistoryProvider for agent context
async with (
AzureCliCredential() as credential,
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
# 2. Create an agent that uses Cosmos for persisted conversation history.
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
),
name="CosmosHistoryAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
# 3. Create a session (session_id is used as the partition key).
session = agent.create_session()
# 4. Run a multi-turn conversation; history is persisted by CosmosHistoryProvider.
response1 = await agent.run("My name is Ada and I enjoy distributed systems.", session=session)
print(f"Assistant: {response1.text}")
response2 = await agent.run("What do you remember about me?", session=session)
print(f"Assistant: {response2.text}")
print(f"Container: {history_provider.container_name}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Assistant: Nice to meet you, Ada! Distributed systems are a fascinating area.
Assistant: You told me your name is Ada and that you enjoy distributed systems.
Container: <AZURE_COSMOS_CONTAINER_NAME>
"""
@@ -0,0 +1,163 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
import asyncio
import os
from agent_framework import Agent, AgentSession
from agent_framework.foundry import FoundryChatClient
from agent_framework_azure_cosmos import CosmosHistoryProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file.
load_dotenv()
"""
This sample demonstrates persisting and resuming conversations across application
restarts using CosmosHistoryProvider as the persistent backend.
Key components:
- Phase 1: Run a conversation and serialize the session with session.to_dict()
- Phase 2: Simulate an app restart — create new provider and agent instances,
restore the session with AgentSession.from_dict(), and continue the conversation
- Cosmos DB reloads the full message history, so the agent remembers everything
Environment variables:
FOUNDRY_PROJECT_ENDPOINT
FOUNDRY_MODEL
AZURE_COSMOS_ENDPOINT
AZURE_COSMOS_DATABASE_NAME
AZURE_COSMOS_CONTAINER_NAME
Optional:
AZURE_COSMOS_KEY
"""
async def main() -> None:
"""Run the conversation persistence sample."""
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
cosmos_key = os.getenv("AZURE_COSMOS_KEY")
if (
not project_endpoint
or not model
or not cosmos_endpoint
or not cosmos_database_name
or not cosmos_container_name
):
print(
"Please set FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL, "
"AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_DATABASE_NAME, and AZURE_COSMOS_CONTAINER_NAME."
)
return
# ── Phase 1: Initial conversation ──
print("=== Phase 1: Initial conversation ===\n")
async with (
AzureCliCredential() as credential,
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
),
name="PersistentAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
session = agent.create_session()
response1 = await agent.run("My name is Ada. I'm building a distributed database in Rust.", session=session)
print("User: My name is Ada. I'm building a distributed database in Rust.")
print(f"Assistant: {response1.text}\n")
response2 = await agent.run("The hardest part is the consensus algorithm.", session=session)
print("User: The hardest part is the consensus algorithm.")
print(f"Assistant: {response2.text}\n")
serialized_session = session.to_dict()
print(f"Session serialized. Session ID: {session.session_id}")
# ── Phase 2: Simulate app restart ──
print("\n=== Phase 2: Resuming after 'restart' ===\n")
async with (
AzureCliCredential() as credential,
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
),
name="PersistentAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
restored_session = AgentSession.from_dict(serialized_session)
print(f"Session restored. Session ID: {restored_session.session_id}\n")
response3 = await agent.run("What was I working on and what was the challenge?", session=restored_session)
print("User: What was I working on and what was the challenge?")
print(f"Assistant: {response3.text}\n")
messages = await history_provider.get_messages(restored_session.session_id)
print(f"Messages stored in Cosmos DB: {len(messages)}")
for i, msg in enumerate(messages, 1):
print(f" {i}. [{msg.role}] {msg.text[:80]}...")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== Phase 1: Initial conversation ===
User: My name is Ada. I'm building a distributed database in Rust.
Assistant: That sounds like a great project, Ada! Rust is an excellent choice for ...
User: The hardest part is the consensus algorithm.
Assistant: Consensus algorithms can be tricky! Are you looking at Raft, Paxos, or ...
Session serialized. Session ID: <session-uuid>
=== Phase 2: Resuming after 'restart' ===
Session restored. Session ID: <session-uuid>
User: What was I working on and what was the challenge?
Assistant: You told me you're building a distributed database in Rust and that the hardest
part is the consensus algorithm.
Messages stored in Cosmos DB: 6
1. [user] My name is Ada. I'm building a distributed database in Rust....
2. [assistant] That sounds like a great project, Ada! Rust is an excellent ch...
3. [user] The hardest part is the consensus algorithm....
4. [assistant] Consensus algorithms can be tricky! Are you looking at Raft, Pa...
5. [user] What was I working on and what was the challenge?...
6. [assistant] You told me you're building a distributed database in Rust and ...
"""
@@ -0,0 +1,157 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework_azure_cosmos import CosmosHistoryProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file.
load_dotenv()
"""
This sample demonstrates direct message history operations using
CosmosHistoryProvider — retrieving, displaying, and clearing stored messages.
Key components:
- get_messages(session_id): Retrieve all stored messages as a chat transcript
- clear(session_id): Delete all messages for a session (e.g., GDPR compliance)
- Verifying that history is empty after clearing
- Running a new conversation in the same session after clearing
Environment variables:
FOUNDRY_PROJECT_ENDPOINT
FOUNDRY_MODEL
AZURE_COSMOS_ENDPOINT
AZURE_COSMOS_DATABASE_NAME
AZURE_COSMOS_CONTAINER_NAME
Optional:
AZURE_COSMOS_KEY
"""
async def main() -> None:
"""Run the messages history sample."""
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
cosmos_key = os.getenv("AZURE_COSMOS_KEY")
if (
not project_endpoint
or not model
or not cosmos_endpoint
or not cosmos_database_name
or not cosmos_container_name
):
print(
"Please set FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL, "
"AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_DATABASE_NAME, and AZURE_COSMOS_CONTAINER_NAME."
)
return
async with (
AzureCliCredential() as credential,
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
),
name="HistoryAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
session = agent.create_session()
session_id = session.session_id
# 1. Have a multi-turn conversation.
print("=== Building a conversation ===\n")
queries = [
"Hi! My favorite programming language is Python.",
"I also enjoy hiking in the mountains on weekends.",
"What do you know about me so far?",
]
for query in queries:
response = await agent.run(query, session=session)
print(f"User: {query}")
print(f"Assistant: {response.text}\n")
# 2. Retrieve and display the full message history as a transcript.
print("=== Chat transcript from Cosmos DB ===\n")
messages = await history_provider.get_messages(session_id)
print(f"Total messages stored: {len(messages)}\n")
for i, msg in enumerate(messages, 1):
print(f" {i}. [{msg.role}] {msg.text[:100]}")
# 3. Clear the session history.
print("\n=== Clearing session history ===\n")
await history_provider.clear(session_id)
print(f"Cleared all messages for session: {session_id}")
# 4. Verify history is empty.
remaining = await history_provider.get_messages(session_id)
print(f"Messages after clear: {len(remaining)}")
# 5. Start a fresh conversation in the same session — agent has no memory.
print("\n=== Fresh conversation (same session, no memory) ===\n")
response = await agent.run("What do you know about me?", session=session)
print("User: What do you know about me?")
print(f"Assistant: {response.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== Building a conversation ===
User: Hi! My favorite programming language is Python.
Assistant: That's great! Python is a wonderful language. What do you like most about it?
User: I also enjoy hiking in the mountains on weekends.
Assistant: Hiking sounds lovely! Do you have a favorite trail or mountain range?
User: What do you know about me so far?
Assistant: You love Python as your favorite programming language and enjoy hiking in the mountains on weekends.
=== Chat transcript from Cosmos DB ===
Total messages stored: 6
1. [user] Hi! My favorite programming language is Python.
2. [assistant] That's great! Python is a wonderful language. What do you like most about it?
3. [user] I also enjoy hiking in the mountains on weekends.
4. [assistant] Hiking sounds lovely! Do you have a favorite trail or mountain range?
5. [user] What do you know about me so far?
6. [assistant] You love Python as your favorite programming language and enjoy hiking ...
=== Clearing session history ===
Cleared all messages for session: <session-uuid>
Messages after clear: 0
=== Fresh conversation (same session, no memory) ===
User: What do you know about me?
Assistant: I don't have any information about you yet. Feel free to share anything you'd like!
"""
@@ -0,0 +1,193 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework_azure_cosmos import CosmosHistoryProvider
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file.
load_dotenv()
"""
This sample demonstrates multi-session and multi-tenant management using
CosmosHistoryProvider. Each tenant (user) gets isolated conversation sessions
stored in the same Cosmos DB container, partitioned by session_id.
Key components:
- Per-tenant session isolation using prefixed session IDs
- list_sessions(): Enumerate all stored sessions across tenants
- Switching between sessions for different users
- Resuming a specific user's session — verifying data isolation
Environment variables:
FOUNDRY_PROJECT_ENDPOINT
FOUNDRY_MODEL
AZURE_COSMOS_ENDPOINT
AZURE_COSMOS_DATABASE_NAME
AZURE_COSMOS_CONTAINER_NAME
Optional:
AZURE_COSMOS_KEY
"""
async def main() -> None:
"""Run the session management sample."""
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
cosmos_key = os.getenv("AZURE_COSMOS_KEY")
if (
not project_endpoint
or not model
or not cosmos_endpoint
or not cosmos_database_name
or not cosmos_container_name
):
print(
"Please set FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_MODEL, "
"AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_DATABASE_NAME, and AZURE_COSMOS_CONTAINER_NAME."
)
return
async with (
AzureCliCredential() as credential,
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
),
name="MultiTenantAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
# 1. Tenant "alice" starts a conversation about travel.
print("=== Tenant: Alice — Travel conversation ===\n")
alice_session = agent.create_session(session_id="tenant-alice-session-1")
response = await agent.run("Hi! I'm planning a trip to Italy. I love Renaissance art.", session=alice_session)
print("Alice: I'm planning a trip to Italy. I love Renaissance art.")
print(f"Assistant: {response.text}\n")
response = await agent.run("Which museums should I visit in Florence?", session=alice_session)
print("Alice: Which museums should I visit in Florence?")
print(f"Assistant: {response.text}\n")
# 2. Tenant "bob" starts a separate conversation about cooking.
print("=== Tenant: Bob — Cooking conversation ===\n")
bob_session = agent.create_session(session_id="tenant-bob-session-1")
response = await agent.run("Hey! I'm learning to cook Thai food. I just made pad thai.", session=bob_session)
print("Bob: I'm learning to cook Thai food. I just made pad thai.")
print(f"Assistant: {response.text}\n")
response = await agent.run("What Thai dish should I try next?", session=bob_session)
print("Bob: What Thai dish should I try next?")
print(f"Assistant: {response.text}\n")
# 3. List all sessions stored in Cosmos DB.
print("=== Listing all sessions ===\n")
sessions = await history_provider.list_sessions()
print(f"Found {len(sessions)} session(s):")
for sid in sessions:
print(f" - {sid}")
# 4. Resume Alice's session — verify she gets her travel context back.
print("\n=== Resuming Alice's session ===\n")
alice_resumed = agent.create_session(session_id="tenant-alice-session-1")
response = await agent.run("What were we discussing?", session=alice_resumed)
print("Alice: What were we discussing?")
print(f"Assistant: {response.text}\n")
# 5. Resume Bob's session — verify he gets his cooking context back.
print("=== Resuming Bob's session ===\n")
bob_resumed = agent.create_session(session_id="tenant-bob-session-1")
response = await agent.run("What was the last dish I mentioned?", session=bob_resumed)
print("Bob: What was the last dish I mentioned?")
print(f"Assistant: {response.text}\n")
# 6. Show per-session message counts.
print("=== Per-session message counts ===\n")
alice_messages = await history_provider.get_messages("tenant-alice-session-1")
bob_messages = await history_provider.get_messages("tenant-bob-session-1")
print(f"Alice's session: {len(alice_messages)} messages")
print(f"Bob's session: {len(bob_messages)} messages")
# 7. Clean up: clear both sessions.
print("\n=== Cleaning up ===\n")
await history_provider.clear("tenant-alice-session-1")
await history_provider.clear("tenant-bob-session-1")
print("Cleared Alice's and Bob's sessions.")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
=== Tenant: Alice — Travel conversation ===
Alice: I'm planning a trip to Italy. I love Renaissance art.
Assistant: Italy is a dream for Renaissance art lovers! Florence, Rome, and Venice ...
Alice: Which museums should I visit in Florence?
Assistant: In Florence, the Uffizi Gallery is a must — it has Botticelli's Birth of Venus ...
=== Tenant: Bob — Cooking conversation ===
Bob: I'm learning to cook Thai food. I just made pad thai.
Assistant: Pad thai is a great start! How did it turn out?
Bob: What Thai dish should I try next?
Assistant: I'd suggest trying green curry or tom yum soup — both are classic Thai dishes ...
=== Listing all sessions ===
Found 2 session(s):
- tenant-alice-session-1
- tenant-bob-session-1
=== Resuming Alice's session ===
Alice: What were we discussing?
Assistant: We were discussing your trip to Italy and your love for Renaissance art ...
=== Resuming Bob's session ===
Bob: What was the last dish I mentioned?
Assistant: You mentioned pad thai — it was the dish you just made!
=== Per-session message counts ===
Alice's session: 6 messages
Bob's session: 6 messages
=== Cleaning up ===
Cleared Alice's and Bob's sessions.
"""
@@ -0,0 +1,90 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import Sequence
from typing import Any
from agent_framework import Agent, AgentSession, HistoryProvider, Message
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Custom History Provider Example
This sample demonstrates how to implement and use a custom history provider
for session management, allowing you to persist conversation history in your
preferred storage solution (database, file system, etc.).
"""
class CustomHistoryProvider(HistoryProvider):
"""Implementation of custom history provider.
In real applications, this can be an implementation of relational database or vector store."""
def __init__(self) -> None:
super().__init__("custom-history")
self._storage: dict[str, list[Message]] = {}
async def get_messages(
self, session_id: str | None, *, state: dict[str, Any] | None = None, **kwargs: Any
) -> list[Message]:
key = session_id or "default"
return list(self._storage.get(key, []))
async def save_messages(
self,
session_id: str | None,
messages: Sequence[Message],
*,
state: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
key = session_id or "default"
if key not in self._storage:
self._storage[key] = []
self._storage[key].extend(messages)
async def main() -> None:
"""Demonstrates how to use 3rd party or custom history provider for sessions."""
print("=== Session with 3rd party or custom history provider ===")
# OpenAI Chat Client is used as an example here,
# other chat clients can be used as well.
agent = Agent(
client=OpenAIChatClient(),
name="CustomBot",
instructions="You are a helpful assistant that remembers our conversation.",
# Use custom history provider.
# If not provided, the default in-memory provider will be used.
context_providers=[CustomHistoryProvider()],
)
# Start a new session for the agent conversation.
session = agent.create_session()
# Respond to user input.
query = "Hello! My name is Alice and I love pizza."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, session=session)}\n")
# Serialize the session state, so it can be stored for later use.
serialized_session = session.to_dict()
# The session can now be saved to a database, file, or any other storage mechanism and loaded again later.
print(f"Serialized session: {serialized_session}\n")
# Deserialize the session state after loading from storage.
resumed_session = AgentSession.from_dict(serialized_session)
# Respond to user input.
query = "What do you remember about me?"
print(f"User: {query}")
print(f"Agent: {await agent.run(query, session=resumed_session)}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,157 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import Annotated
# Uncomment this filter to suppress the experimental FileHistoryProvider warning
# before running the sample.
# import warnings # isort: skip
# warnings.filterwarnings("ignore", message=r"\[FILE_HISTORY\].*", category=FutureWarning)
from agent_framework import Agent, FileHistoryProvider, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
try:
import orjson # pyright: ignore[reportMissingImports]
except ImportError:
orjson = None
# Load environment variables from .env file.
load_dotenv()
"""
File History Provider
This sample demonstrates how to use the experimental `FileHistoryProvider` with
`FoundryChatClient` and a function tool so the persisted JSON Lines file shows
the tool-calling loop as well as the regular chat turns.
Environment variables:
FOUNDRY_PROJECT_ENDPOINT: Azure AI Foundry project endpoint.
FOUNDRY_MODEL: Foundry model deployment name.
Key components:
- `FileHistoryProvider`: Stores one message JSON object per line in a local
`.jsonl` file for each session.
- `lookup_weather`: A function tool that makes the persisted file show the
assistant function call and tool result lines.
- `json.dumps(..., indent=2)`: Pretty-prints selected records in the sample
output while keeping the on-disk JSONL file compact and valid.
- `USE_TEMP_DIRECTORY`: Toggle between a temporary directory and a persistent
`sessions/` folder next to this sample file.
Security posture:
- The history files are plaintext JSONL on disk, so use a trusted storage
directory and treat the files as conversation logs, not as secure secret
storage.
- Path safety checks protect the filename derived from the session id, but they
do not redact message contents or encrypt the file.
"""
USE_TEMP_DIRECTORY = False
"""When True, store JSONL files in a temporary directory for this run only."""
LOCAL_SESSIONS_DIRECTORY_NAME = "sessions"
"""Folder name used when persisting history next to this sample file."""
@tool(approval_mode="never_require")
def lookup_weather(
location: Annotated[str, Field(description="The city to look up weather for.")],
) -> str:
"""Return a deterministic weather report for a city."""
weather_reports = {
"Seattle": "Seattle is rainy with a high of 13C.",
"Amsterdam": "Amsterdam is cloudy with a high of 16C.",
}
return weather_reports.get(location, f"{location} is sunny with a high of 20C.")
@contextmanager
def _resolve_storage_directory() -> Iterator[Path]:
"""Yield the configured storage directory for the sample run."""
if USE_TEMP_DIRECTORY:
with tempfile.TemporaryDirectory(prefix="af-file-history-") as temp_directory:
yield Path(temp_directory)
return
storage_directory = Path(__file__).resolve().parent / LOCAL_SESSIONS_DIRECTORY_NAME
storage_directory.mkdir(parents=True, exist_ok=True)
yield storage_directory
async def main() -> None:
"""Run the file history provider sample."""
with _resolve_storage_directory() as storage_directory:
print(f"Using temporary directory: {USE_TEMP_DIRECTORY}")
print(f"Storage directory: {storage_directory}\n")
# 2. Create the agent with a tool so the JSONL file includes tool-calling messages.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.getenv("FOUNDRY_PROJECT_ENDPOINT"),
model=os.getenv("FOUNDRY_MODEL"),
credential=AzureCliCredential(),
),
name="FileHistoryAgent",
instructions=(
"You are a helpful assistant, use the lookup_weather tool for weather questions and "
"answer with the tool result in one sentence."
),
tools=[lookup_weather],
# if orjson is available, use it for faster JSON serialization in the FileHistoryProvider,
# otherwise fall back to the default json module.
context_providers=[
FileHistoryProvider(
storage_directory,
dumps=orjson.dumps if orjson else None,
loads=orjson.loads if orjson else None,
)
],
default_options={"store": False},
)
# 3. Let Agent create the default UUID session id for this conversation.
session = agent.create_session()
# 4. Ask a question that triggers the weather tool.
print("=== Run with tool calling ===")
query = "Use the lookup_weather tool for Seattle and tell me the weather."
response = await agent.run(query, session=session)
print(f"User: {query}")
print(f"Assistant: {response.text}\n")
# 5. Ask a follow-up question that triggers the weather tool as well
print("=== Follow-up question ===")
query = "And what about Amsterdam?"
response = await agent.run(query, session=session)
print(f"User: {query}")
print(f"Assistant: {response.text}\n")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Using temporary directory: False
Storage directory: /path/to/samples/02-agents/conversations/sessions
=== Run with tool calling ===
User: Use the lookup_weather tool for Seattle and tell me the weather.
Assistant: <model response varies>
=== Follow-up question ===
User: And what about Amsterdam?
Assistant: <model response varies>
"""
@@ -0,0 +1,185 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
from __future__ import annotations
import asyncio
import json
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import Annotated
# Uncomment this filter to suppress the experimental FileHistoryProvider warning
# before running the sample.
# import warnings # isort: skip
# warnings.filterwarnings("ignore", message=r"\[FILE_HISTORY\].*", category=FutureWarning)
from agent_framework import Agent, FileHistoryProvider, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
try:
import orjson # pyright: ignore[reportMissingImports]
except ImportError:
orjson = None
load_dotenv()
"""
File History Provider Conversation Persistence
This sample demonstrates persisting a tool-driven conversation with the
experimental `FileHistoryProvider`, reading the stored JSONL file back from
disk, and then continuing the same conversation with another city.
Environment variables:
FOUNDRY_PROJECT_ENDPOINT: Azure AI Foundry project endpoint.
FOUNDRY_MODEL: Foundry model deployment name.
Key components:
- `FileHistoryProvider`: Stores one message JSON object per line in a local
`.jsonl` file for each session.
- `get_weather`: A function tool that makes the persisted file show the
assistant function call and tool result records.
- `json.dumps(..., indent=2)`: Pretty-prints a few persisted JSONL records
while keeping the on-disk file compact and valid.
- `load_dotenv()`: Loads `.env` values up front so the sample can stay focused
on history persistence instead of manual environment variable plumbing.
- Optional `orjson`: Uses `orjson.dumps` / `orjson.loads` automatically when
available, otherwise falls back to the standard library `json` module.
Security posture:
- The history file is plaintext JSONL on disk, so use a trusted storage
directory and treat it as conversation logging, not as secure secret storage.
- Path safety checks protect the filename derived from the session id, but they
do not redact message contents or encrypt the file.
"""
USE_TEMP_DIRECTORY = False
"""When True, store JSONL files in a temporary directory for this run only."""
LOCAL_SESSIONS_DIRECTORY_NAME = "sessions"
"""Folder name used when persisting history next to this sample file."""
@tool(approval_mode="never_require")
def get_weather(
city: Annotated[str, Field(description="The city to get the weather for.")],
) -> str:
"""Return a deterministic weather report for a city."""
weather_reports = {
"Seattle": "Seattle is rainy with a high of 13C.",
"Amsterdam": "Amsterdam is cloudy with a high of 16C.",
}
return weather_reports.get(city, f"{city} is sunny with a high of 20C.")
@contextmanager
def _resolve_storage_directory() -> Iterator[Path]:
"""Yield the configured storage directory for the sample run."""
if USE_TEMP_DIRECTORY:
with tempfile.TemporaryDirectory(prefix="af-file-history-resume-") as temp_directory:
yield Path(temp_directory)
return
storage_directory = Path(__file__).resolve().parent / LOCAL_SESSIONS_DIRECTORY_NAME
storage_directory.mkdir(parents=True, exist_ok=True)
yield storage_directory
async def main() -> None:
"""Run the file history provider conversation persistence sample."""
with _resolve_storage_directory() as storage_directory:
print(f"Using temporary directory: {USE_TEMP_DIRECTORY}")
print(f"Storage directory: {storage_directory}\n")
# 1. Create the client, history provider, and tool-enabled agent.
agent = Agent(
client=FoundryChatClient(
credential=AzureCliCredential(),
),
name="WeatherHistoryAgent",
instructions=(
"You are a helpful assistant. Use the get_weather tool for weather questions "
"and answer in one sentence using the tool result."
),
tools=[get_weather],
context_providers=[
FileHistoryProvider(
storage_directory,
dumps=orjson.dumps if orjson else None,
loads=orjson.loads if orjson else None,
)
],
default_options={"store": False},
)
# 2. Ask about the first city so the JSONL file is created on disk.
session = agent.create_session()
history_file = storage_directory / f"{session.session_id}.jsonl"
print("=== First weather question ===\n")
first_query = "Use the get_weather tool and tell me the weather in Seattle."
first_response = await agent.run(first_query, session=session)
print(f"User: {first_query}")
print(f"Assistant: {first_response.text}\n")
# 3. Read the stored JSONL records back from disk and pretty-print a few of them.
raw_lines = (await asyncio.to_thread(history_file.read_text, encoding="utf-8")).splitlines()
print(f"Stored message lines after first question: {len(raw_lines)}")
print(f"History file: {history_file}\n")
print("=== JSONL preview from disk ===\n")
for index, line in enumerate(raw_lines[:4], start=1):
print(f"Record {index}:")
print(json.dumps(json.loads(line), indent=2))
print()
# 4. Continue the same persisted conversation with another city.
print("=== Second weather question ===\n")
second_query = "Now use the get_weather tool for Amsterdam."
second_response = await agent.run(second_query, session=session)
print(f"User: {second_query}")
print(f"Assistant: {second_response.text}\n")
updated_lines = (await asyncio.to_thread(history_file.read_text, encoding="utf-8")).splitlines()
print(f"Stored message lines after second question: {len(updated_lines)}")
print(f"History file: {history_file}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Using temporary directory: False
Storage directory: /path/to/samples/02-agents/conversations/sessions
=== First weather question ===
User: Use the get_weather tool and tell me the weather in Seattle.
Assistant: <model response varies>
Stored message lines after first question: 4
History file: /path/to/samples/02-agents/conversations/sessions/<session-uuid>.jsonl
=== JSONL preview from disk ===
Record 1:
{
"type": "message",
"role": "user",
...
}
=== Second weather question ===
User: Now use the get_weather tool for Amsterdam.
Assistant: <model response varies>
Stored message lines after second question: 8
History file: /path/to/samples/02-agents/conversations/sessions/<session-uuid>.jsonl
"""
@@ -0,0 +1,270 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from uuid import uuid4
from agent_framework import Agent, AgentSession
from agent_framework.openai import OpenAIChatClient
from agent_framework.redis import RedisHistoryProvider
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Redis History Provider Session Example
This sample demonstrates how to use Redis as a history provider for session
management, enabling persistent conversation history storage across sessions
with Redis as the backend data store.
"""
# Default Redis URL for local Redis Stack.
# Override via the REDIS_URL environment variable for remote or authenticated instances.
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
async def example_manual_memory_store() -> None:
"""Basic example of using Redis history provider."""
print("=== Basic Redis History Provider Example ===")
# Create Redis history provider
redis_provider = RedisHistoryProvider(
source_id="redis_basic_chat",
redis_url=REDIS_URL,
)
# Create agent with Redis history provider
agent = Agent(
client=OpenAIChatClient(),
name="RedisBot",
instructions="You are a helpful assistant that remembers our conversation using Redis.",
context_providers=[redis_provider],
)
# Create session
session = agent.create_session()
# Have a conversation
print("\n--- Starting conversation ---")
query1 = "Hello! My name is Alice and I love pizza."
print(f"User: {query1}")
response1 = await agent.run(query1, session=session)
print(f"Agent: {response1.text}")
query2 = "What do you remember about me?"
print(f"User: {query2}")
response2 = await agent.run(query2, session=session)
print(f"Agent: {response2.text}")
print("Done\n")
async def example_user_session_management() -> None:
"""Example of managing user sessions with Redis."""
print("=== User Session Management Example ===")
user_id = "alice_123"
session_id = f"session_{uuid4()}"
# Create Redis history provider for specific user session
redis_provider = RedisHistoryProvider(
source_id=f"redis_{user_id}",
redis_url=REDIS_URL,
max_messages=10, # Keep only last 10 messages
)
# Create agent with history provider
agent = Agent(
client=OpenAIChatClient(),
name="SessionBot",
instructions="You are a helpful assistant. Keep track of user preferences.",
context_providers=[redis_provider],
)
# Start conversation
session = agent.create_session(session_id=session_id)
print(f"Started session for user {user_id}")
# Simulate conversation
queries = [
"Hi, I'm Alice and I prefer vegetarian food.",
"What restaurants would you recommend?",
"I also love Italian cuisine.",
"Can you remember my food preferences?",
]
for i, query in enumerate(queries, 1):
print(f"\n--- Message {i} ---")
print(f"User: {query}")
response = await agent.run(query, session=session)
print(f"Agent: {response.text}")
print("Done\n")
async def example_conversation_persistence() -> None:
"""Example of conversation persistence across application restarts."""
print("=== Conversation Persistence Example ===")
# Phase 1: Start conversation
print("--- Phase 1: Starting conversation ---")
redis_provider = RedisHistoryProvider(
source_id="redis_persistent_chat",
redis_url=REDIS_URL,
)
agent = Agent(
client=OpenAIChatClient(),
name="PersistentBot",
instructions="You are a helpful assistant. Remember our conversation history.",
context_providers=[redis_provider],
)
session = agent.create_session()
# Start conversation
query1 = "Hello! I'm working on a Python project about machine learning."
print(f"User: {query1}")
response1 = await agent.run(query1, session=session)
print(f"Agent: {response1.text}")
query2 = "I'm specifically interested in neural networks."
print(f"User: {query2}")
response2 = await agent.run(query2, session=session)
print(f"Agent: {response2.text}")
# Serialize session state
serialized = session.to_dict()
# Phase 2: Resume conversation (simulating app restart)
print("\n--- Phase 2: Resuming conversation (after 'restart') ---")
restored_session = AgentSession.from_dict(serialized)
# Continue conversation - agent should remember context
query3 = "What was I working on before?"
print(f"User: {query3}")
response3 = await agent.run(query3, session=restored_session)
print(f"Agent: {response3.text}")
query4 = "Can you suggest some Python libraries for neural networks?"
print(f"User: {query4}")
response4 = await agent.run(query4, session=restored_session)
print(f"Agent: {response4.text}")
print("Done\n")
async def example_session_serialization() -> None:
"""Example of session state serialization and deserialization."""
print("=== Session Serialization Example ===")
redis_provider = RedisHistoryProvider(
source_id="redis_serialization_chat",
redis_url=REDIS_URL,
)
agent = Agent(
client=OpenAIChatClient(),
name="SerializationBot",
instructions="You are a helpful assistant.",
context_providers=[redis_provider],
)
session = agent.create_session()
# Have initial conversation
print("--- Initial conversation ---")
query1 = "Hello! I'm testing serialization."
print(f"User: {query1}")
response1 = await agent.run(query1, session=session)
print(f"Agent: {response1.text}")
# Serialize session state
serialized = session.to_dict()
print(f"\nSerialized session state: {serialized}")
# Deserialize session state (simulating loading from database/file)
print("\n--- Deserializing session state ---")
restored_session = AgentSession.from_dict(serialized)
# Continue conversation with restored session
query2 = "Do you remember what I said about testing?"
print(f"User: {query2}")
response2 = await agent.run(query2, session=restored_session)
print(f"Agent: {response2.text}")
print("Done\n")
async def example_message_limits() -> None:
"""Example of automatic message trimming with limits."""
print("=== Message Limits Example ===")
# Create provider with small message limit
redis_provider = RedisHistoryProvider(
source_id="redis_limited_chat",
redis_url=REDIS_URL,
max_messages=3, # Keep only 3 most recent messages
)
agent = Agent(
client=OpenAIChatClient(),
name="LimitBot",
instructions="You are a helpful assistant with limited memory.",
context_providers=[redis_provider],
)
session = agent.create_session()
# Send multiple messages to test trimming
messages = [
"Message 1: Hello!",
"Message 2: How are you?",
"Message 3: What's the weather?",
"Message 4: Tell me a joke.",
"Message 5: This should trigger trimming.",
]
for i, query in enumerate(messages, 1):
print(f"\n--- Sending message {i} ---")
print(f"User: {query}")
response = await agent.run(query, session=session)
print(f"Agent: {response.text}")
print("Done\n")
async def main() -> None:
"""Run all Redis history provider examples."""
print("Redis History Provider Examples")
print("=" * 50)
print("Prerequisites:")
print("- Redis server running (set REDIS_URL env var or default localhost:6379)")
print("- OPENAI_API_KEY environment variable set")
print("=" * 50)
# Check prerequisites
if not os.getenv("OPENAI_API_KEY"):
print("ERROR: OPENAI_API_KEY environment variable not set")
return
try:
# Run all examples
await example_manual_memory_store()
await example_user_session_management()
await example_conversation_persistence()
await example_session_serialization()
await example_message_limits()
print("All examples completed successfully!")
except Exception as e:
print(f"Error running examples: {e}")
raise
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,101 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent, AgentSession
from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIChatCompletionClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Session Suspend and Resume Example
This sample demonstrates how to suspend and resume conversation sessions, comparing
service-managed sessions (Azure AI) with in-memory sessions (OpenAI) for persistent
conversation state across sessions.
"""
async def suspend_resume_service_managed_session() -> None:
"""Demonstrates how to suspend and resume a service-managed session."""
print("=== Suspend-Resume Service-Managed Session ===")
# FoundryChatClient supports service-managed sessions.
async with (
AzureCliCredential() as credential,
Agent(
client=FoundryChatClient(credential=credential),
name="MemoryBot",
instructions="You are a helpful assistant that remembers our conversation.",
) as agent,
):
# Start a new session for the agent conversation.
session = agent.create_session()
# Respond to user input.
query = "Hello! My name is Alice and I love pizza."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, session=session)}\n")
# Serialize the session state, so it can be stored for later use.
serialized_session = session.to_dict()
# The session can now be saved to a database, file, or any other storage mechanism and loaded again later.
print(f"Serialized session: {serialized_session}\n")
# Deserialize the session state after loading from storage.
resumed_session = AgentSession.from_dict(serialized_session)
# Respond to user input.
query = "What do you remember about me?"
print(f"User: {query}")
print(f"Agent: {await agent.run(query, session=resumed_session)}\n")
async def suspend_resume_in_memory_session() -> None:
"""Demonstrates how to suspend and resume an in-memory session."""
print("=== Suspend-Resume In-Memory Session ===")
# OpenAI Chat Client is used as an example here,
# other chat clients can be used as well.
agent = Agent(
client=OpenAIChatCompletionClient(),
name="MemoryBot",
instructions="You are a helpful assistant that remembers our conversation.",
)
# Start a new session for the agent conversation.
session = agent.create_session()
# Respond to user input.
query = "Hello! My name is Alice and I love pizza."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, session=session)}\n")
# Serialize the session state, so it can be stored for later use.
serialized_session = session.to_dict()
# The session can now be saved to a database, file, or any other storage mechanism and loaded again later.
print(f"Serialized session: {serialized_session}\n")
# Deserialize the session state after loading from storage.
resumed_session = AgentSession.from_dict(serialized_session)
# Respond to user input.
query = "What do you remember about me?"
print(f"User: {query}")
print(f"Agent: {await agent.run(query, session=resumed_session)}\n")
async def main() -> None:
print("=== Suspend-Resume Session Examples ===")
await suspend_resume_service_managed_session()
await suspend_resume_in_memory_session()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,272 @@
# Declarative Agent Samples
This folder contains sample code demonstrating how to use the **Microsoft Agent Framework Declarative** package to create agents from YAML specifications. The declarative approach allows you to define your agents in a structured, configuration-driven way, separating agent behavior from implementation details.
## Installation
Install the declarative package via pip:
```bash
pip install agent-framework-declarative --pre
```
## What is Declarative Agent Framework?
The declarative package provides support for building agents based on YAML specifications. This approach offers several benefits:
- **Cross-Platform Compatibility**: Write one YAML definition and create agents in both Python and .NET - the same agent configuration works across both platforms
- **Separation of Concerns**: Define agent behavior in YAML files separate from your implementation code
- **Reusability**: Share and version agent configurations independently across projects and languages
- **Flexibility**: Easily swap between different LLM providers and configurations
- **Maintainability**: Update agent instructions and settings without modifying code
## Samples in This Folder
### 1. **Get Weather Agent** ([`get_weather_agent.py`](./get_weather_agent.py))
Demonstrates how to create an agent with custom function tools using the declarative approach.
- Uses Azure OpenAI Responses client
- Shows how to bind Python functions to the agent using the `bindings` parameter
- Loads agent configuration from `declarative-agents/agent-samples/chatclient/GetWeather.yaml`
- Implements a simple weather lookup function tool
**Key concepts**: Function binding, Azure OpenAI integration, tool usage
### 2. **Microsoft Learn Agent** ([`microsoft_learn_agent.py`](./microsoft_learn_agent.py))
Shows how to create an agent that can search and retrieve information from Microsoft Learn documentation using the Model Context Protocol (MCP).
- Uses Azure AI Foundry client with MCP server integration
- Demonstrates async context managers for proper resource cleanup
- Loads agent configuration from `declarative-agents/agent-samples/foundry/MicrosoftLearnAgent.yaml`
- Uses Azure CLI credentials for authentication
- Leverages MCP to access Microsoft documentation tools
**Requirements**: `pip install agent-framework-foundry`
**Key concepts**: Azure AI Foundry integration, MCP server usage, async patterns, resource management
### 3. **Inline YAML Agent** ([`inline_yaml.py`](./inline_yaml.py))
Shows how to create an agent using an inline YAML string rather than a file.
- Uses Azure AI Foundry v2 Client with instructions.
**Requirements**: `pip install agent-framework-foundry`
**Key concepts**: Inline YAML definition.
### 4. **Azure OpenAI Responses Agent** ([`azure_openai_responses_agent.py`](./azure_openai_responses_agent.py))
Illustrates a basic agent using Azure OpenAI with structured responses.
- Uses Azure OpenAI Responses client
- Shows how to pass credentials via `client_kwargs`
- Loads agent configuration from `declarative-agents/agent-samples/azure/AzureOpenAIResponses.yaml`
- Demonstrates accessing structured response data
**Key concepts**: Azure OpenAI integration, credential management, structured outputs
### 5. **OpenAI Responses Agent** ([`openai_agent.py`](./openai_agent.py))
Demonstrates the simplest possible agent using OpenAI directly.
- Uses OpenAI API (requires `OPENAI_API_KEY` environment variable)
- Shows minimal configuration needed for basic agent creation
- Loads agent configuration from `declarative-agents/agent-samples/openai/OpenAIResponses.yaml`
**Key concepts**: OpenAI integration, minimal setup, environment-based configuration
## Agent Samples Repository
All the YAML configuration files referenced in these samples are located in the [`declarative-agents/agent-samples`](../../../../declarative-agents/agent-samples/) folder at the repository root. This folder contains declarative agent specifications organized by provider:
- **`declarative-agents/agent-samples/azure/`** - Azure OpenAI agent configurations
- **`declarative-agents/agent-samples/chatclient/`** - Chat client agent configurations with tools
- **`declarative-agents/agent-samples/foundry/`** - Azure AI Foundry agent configurations
- **`declarative-agents/agent-samples/openai/`** - OpenAI agent configurations
**Important**: These YAML files are **platform-agnostic** and work with both Python and .NET implementations of the Agent Framework. You can use the exact same YAML definition to create agents in either language, making it easy to share agent configurations across different technology stacks.
These YAML files define:
- Agent instructions and system prompts
- Model selection and parameters
- Tool and function configurations
- Provider-specific settings
- MCP server integrations (where applicable)
## Common Patterns
### Creating an Agent from YAML String
```python
from agent_framework.declarative import AgentFactory
with open("agent.yaml", "r") as f:
yaml_str = f.read()
agent = AgentFactory().create_agent_from_yaml(yaml_str)
# response = await agent.run("Your query here")
```
### Creating an Agent from YAML Path
```python
from pathlib import Path
from agent_framework.declarative import AgentFactory
yaml_path = Path("agent.yaml")
agent = AgentFactory().create_agent_from_yaml_path(yaml_path)
# response = await agent.run("Your query here")
```
### Binding Custom Functions
```python
from pathlib import Path
from agent_framework.declarative import AgentFactory
def my_function(param: str) -> str:
return f"Result: {param}"
agent_factory = AgentFactory(bindings={"my_function": my_function})
agent = agent_factory.create_agent_from_yaml_path(Path("agent_with_tool.yaml"))
```
### Using Credentials
```python
from pathlib import Path
from agent_framework.declarative import AgentFactory
from azure.identity import AzureCliCredential
agent = AgentFactory(
client_kwargs={"credential": AzureCliCredential()}
).create_agent_from_yaml_path(Path("azure_agent.yaml"))
```
### Adding Custom Provider Mappings
```python
from pathlib import Path
from agent_framework.declarative import AgentFactory
# from my_custom_module import MyCustomChatClient
# Register a custom provider mapping
agent_factory = AgentFactory(
additional_mappings={
"MyProvider": {
"package": "my_custom_module",
"name": "MyCustomChatClient",
"model_field": "model",
}
}
)
# Now you can reference "MyProvider" in your YAML
# Example YAML snippet:
# model:
# provider: MyProvider
# id: my-model-name
agent = agent_factory.create_agent_from_yaml_path(Path("custom_provider.yaml"))
```
This allows you to extend the declarative framework with custom chat client implementations. The mapping requires:
- **package**: The Python package/module to import from
- **name**: The class name of your SupportsChatGetResponse implementation
- **model_field**: The constructor parameter name that accepts the value of the `model.id` field from the YAML
You can reference your custom provider using either `Provider.ApiType` format or just `Provider` in your YAML configuration, as long as it matches the registered mapping.
### Using PowerFx Formulas in YAML
The declarative framework supports PowerFx formulas in YAML values, enabling dynamic configuration based on environment variables and conditional logic. Prefix any value with `=` to evaluate it as a PowerFx expression.
#### Environment Variable Lookup
Access environment variables using the `Env.<variable_name>` syntax:
```yaml
model:
connection:
kind: key
apiKey: =Env.OPENAI_API_KEY
endpoint: =Env.BASE_URL & "/v1" # String concatenation with &
options:
temperature: 0.7
maxOutputTokens: =Env.MAX_TOKENS # Will be converted to appropriate type
```
#### Conditional Logic
Use PowerFx operators for conditional configuration. This is particularly useful for adjusting parameters based on which model is being used:
```yaml
model:
id: =Env.MODEL_NAME
options:
# Set max tokens based on model - using conditional logic
maxOutputTokens: =If(Env.MODEL_NAME = "gpt-5", 8000, 4000)
# Adjust temperature for different environments
temperature: =If(Env.ENVIRONMENT = "production", 0.3, 0.7)
# Use logical operators for complex conditions
seed: =If(Env.ENVIRONMENT = "production" And Env.DETERMINISTIC = "true", 42, Blank())
```
#### Supported PowerFx Features
- **String operations**: Concatenation (`&`), comparison (`=`, `<>`), substring testing (`in`, `exactin`)
- **Logical operators**: `And`, `Or`, `Not` (also `&&`, `||`, `!`)
- **Arithmetic**: Basic math operations (`+`, `-`, `*`, `/`)
- **Conditional**: `If(condition, true_value, false_value)`
- **Environment access**: `Env.<VARIABLE_NAME>`
Example with multiple features:
```yaml
instructions: =If(
Env.USE_EXPERT_MODE = "true",
"You are an expert AI assistant with advanced capabilities. " & Env.CUSTOM_INSTRUCTIONS,
"You are a helpful AI assistant."
)
model:
options:
stopSequences: =If("gpt-4" in Env.MODEL_NAME, ["END", "STOP"], ["END"])
```
**Note**: PowerFx evaluation happens when the YAML is loaded, not at runtime. Use environment variables (via `.env` file or `env_file` parameter) to make configurations flexible across environments.
## Running the Samples
Each sample can be run independently. Make sure you have the required environment variables set:
- For Azure samples: Ensure you're logged in via Azure CLI (`az login`)
- For OpenAI samples: Set `OPENAI_API_KEY` environment variable
```bash
# Run a specific sample
python get_weather_agent.py
python microsoft_learn_agent.py
python inline_yaml.py
python azure_openai_responses_agent.py
python openai_responses_agent.py
```
## Learn More
- [Agent Framework Declarative Package](../../../packages/declarative/) - Main declarative package documentation
- [Agent Samples](../../../../declarative-agents/agent-samples/) - Additional declarative agent YAML specifications
- [Agent Framework Core](../../../packages/core/) - Core agent framework documentation
## Next Steps
1. Explore the YAML files in the `declarative-agents/agent-samples` folder to understand the configuration format
2. Try modifying the samples to use different models or instructions
3. Create your own declarative agent configurations
4. Build custom function tools and bind them to your agents
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from pathlib import Path
from agent_framework.declarative import AgentFactory
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def main():
"""Create an agent from a declarative yaml specification and run it."""
# get the path
current_path = Path(__file__).parent
yaml_path = (
current_path.parent.parent.parent.parent
/ "declarative-agents"
/ "agent-samples"
/ "azure"
/ "AzureOpenAIResponses.yaml"
)
# load the yaml from the path
with yaml_path.open("r") as f:
yaml_str = f.read()
# create the agent from the yaml
agent = AgentFactory(client_kwargs={"credential": AzureCliCredential()}).create_agent_from_yaml(yaml_str)
# use the agent
response = await agent.run("Why is the sky blue, answer in Dutch?")
# Use response.value with try/except for safe parsing
try:
parsed = response.value
model_dump_json = getattr(parsed, "model_dump_json", None)
if callable(model_dump_json):
print("Agent response:", model_dump_json(indent=2))
else:
print("Agent response:", response.text)
except Exception:
print("Agent response:", response.text)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from pathlib import Path
from random import randint
from typing import Literal
from agent_framework.declarative import AgentFactory
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
def get_weather(location: str, unit: Literal["celsius", "fahrenheit"] = "celsius") -> str:
"""A simple function tool to get weather information."""
return f"The weather in {location} is {randint(-10, 30) if unit == 'celsius' else randint(30, 100)} degrees {unit}."
async def main():
"""Create an agent from a declarative yaml specification and run it."""
# get the path
current_path = Path(__file__).parent
yaml_path = (
current_path.parent.parent.parent.parent
/ "declarative-agents"
/ "agent-samples"
/ "chatclient"
/ "GetWeather.yaml"
)
# load the yaml from the path
with yaml_path.open("r") as f:
yaml_str = f.read()
# create the AgentFactory with a chat client and bindings
agent_factory = AgentFactory(
client=FoundryChatClient(credential=AzureCliCredential()),
bindings={"get_weather": get_weather},
)
# create the agent from the yaml
agent = agent_factory.create_agent_from_yaml(yaml_str)
# use the agent
response = await agent.run("What's the weather in Amsterdam, in celsius?")
print("Agent response:", response.text)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,52 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.declarative import AgentFactory
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample shows how to create an agent using an inline YAML string rather than a file.
It uses a Azure AI Client so it needs the credential to be passed into the AgentFactory.
Prerequisites:
- `pip install agent-framework-foundry agent-framework-declarative --pre`
- Set the following environment variables in a .env file or your environment:
- FOUNDRY_PROJECT_ENDPOINT
- FOUNDRY_MODEL
"""
async def main():
"""Create an agent from a declarative YAML specification and run it."""
yaml_definition = """kind: Prompt
name: DiagnosticAgent
displayName: Diagnostic Assistant
instructions: Specialized diagnostic and issue detection agent for systems with critical error protocol and automatic handoff capabilities
description: A agent that performs diagnostics on systems and can escalate issues when critical errors are detected.
model:
id: =Env.FOUNDRY_MODEL
"""
# create the agent from the yaml
async with (
AzureCliCredential() as credential,
AgentFactory(
client_kwargs={
"credential": credential,
"project_endpoint": os.environ["FOUNDRY_PROJECT_ENDPOINT"],
},
safe_mode=False,
).create_agent_from_yaml(yaml_definition) as agent,
):
response = await agent.run("What can you do for me?")
print("Agent response:", response.text)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,161 @@
# Copyright (c) Microsoft. All rights reserved.
"""
MCP Tool via YAML Declaration
This sample demonstrates how to create agents with MCP (Model Context Protocol)
tools using YAML declarations and the declarative AgentFactory.
Key Features Demonstrated:
1. Loading agent definitions from YAML using AgentFactory
2. Configuring MCP tools with different authentication methods:
- API key authentication (OpenAI.Responses provider)
- Azure AI Foundry connection references (Foundry provider)
Authentication Options:
- OpenAI.Responses: Supports inline API key auth via headers
- Foundry: Uses project-backed chat with Foundry connections for secure credential storage
(no secrets passed in API calls - connection name references pre-configured auth)
Prerequisites:
- `pip install agent-framework-openai agent-framework-declarative --pre`
- For OpenAI example: Set OPENAI_API_KEY and GITHUB_PAT environment variables
- For Azure AI example: Set up a Foundry connection in your Azure AI project
"""
import asyncio
from agent_framework.declarative import AgentFactory
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Example 1: OpenAI.Responses with API key authentication
# Uses inline API key - suitable for OpenAI provider which supports headers
YAML_OPENAI_WITH_API_KEY = """
kind: Prompt
name: GitHubAgent
displayName: GitHub Assistant
description: An agent that can interact with GitHub using the MCP protocol
instructions: |
You are a helpful assistant that can interact with GitHub.
You can search for repositories, read file contents, and check issues.
Always be clear about what operations you're performing.
model:
id: gpt-4o
provider: OpenAI.Responses # Uses OpenAI's Responses API (requires OPENAI_API_KEY env var)
tools:
- kind: mcp
name: github-mcp
description: GitHub MCP tool for repository operations
url: https://api.githubcopilot.com/mcp/
connection:
kind: key
apiKey: =Env.GITHUB_PAT # PowerFx syntax to read from environment variable
approvalMode: never
allowedTools:
- get_file_contents
- get_me
- search_repositories
- search_code
- list_issues
"""
# Example 2: Azure AI with Foundry connection reference
# No secrets in YAML - references a pre-configured Foundry connection by name
# The connection stores credentials securely in Azure AI Foundry
YAML_AZURE_AI_WITH_FOUNDRY_CONNECTION = """
kind: Prompt
name: GitHubAgent
displayName: GitHub Assistant
description: An agent that can interact with GitHub using the MCP protocol
instructions: |
You are a helpful assistant that can interact with GitHub.
You can search for repositories, read file contents, and check issues.
Always be clear about what operations you're performing.
model:
id: gpt-4o
provider: Foundry
tools:
- kind: mcp
name: github-mcp
description: GitHub MCP tool for repository operations
url: https://api.githubcopilot.com/mcp/
connection:
kind: remote
authenticationMode: oauth
name: github-mcp-oauth-connection # References a Foundry connection
approvalMode: never
allowedTools:
- get_file_contents
- get_me
- search_repositories
- search_code
- list_issues
"""
async def run_openai_example():
"""Run the OpenAI.Responses example with API key auth."""
print("=" * 60)
print("Example 1: OpenAI.Responses with API Key Authentication")
print("=" * 60)
factory = AgentFactory(
safe_mode=False, # Allow PowerFx env var resolution (=Env.VAR_NAME)
)
print("\nCreating agent from YAML definition...")
agent = factory.create_agent_from_yaml(YAML_OPENAI_WITH_API_KEY)
async with agent:
query = "What is my GitHub username?"
print(f"\nUser: {query}")
response = await agent.run(query)
print(f"\nAgent: {response.text}")
async def run_azure_ai_example():
"""Run the Azure AI example with Foundry connection.
Prerequisites:
1. Create a Foundry connection named 'github-mcp-oauth-connection' in your
Azure AI project with OAuth credentials for GitHub
2. Set PROJECT_ENDPOINT environment variable to your Azure AI project endpoint
"""
print("=" * 60)
print("Example 2: Azure AI with Foundry Connection Reference")
print("=" * 60)
from azure.identity import AzureCliCredential
factory = AgentFactory(client_kwargs={"credential": AzureCliCredential()})
print("\nCreating agent from YAML definition...")
# Use async method for provider-based agent creation
agent = await factory.create_agent_from_yaml_async(YAML_AZURE_AI_WITH_FOUNDRY_CONNECTION)
async with agent:
query = "What is my GitHub username?"
print(f"\nUser: {query}")
response = await agent.run(query)
print(f"\nAgent: {response.text}")
async def main():
"""Run the MCP tool examples."""
# Run the OpenAI example
await run_openai_example()
# Run the Azure AI example (uncomment to run)
# Requires: Foundry connection set up and PROJECT_ENDPOINT env var
# await run_azure_ai_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,50 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from pathlib import Path
from agent_framework.declarative import AgentFactory
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample demonstrates creating an agent from a declarative YAML file specification.
It uses a MCP server to connect to the Microsoft Learn content and a FoundryChatClient.
The yaml also has some chat options set, such as temperature and topP.
These options do not work with newer OpenAI models, so ensure to use a compatible model such as gpt-4o-mini.
Environment variables:
- FOUNDRY_PROJECT_ENDPOINT: The endpoint URL for the Foundry project.
- FOUNDRY_MODEL: The model ID to use for the agent, make sure it is compatible with the chat options specified in
the yaml, or remove the options.
"""
async def main():
"""Create an agent from a declarative yaml specification and run it."""
# get the path
current_path = Path(__file__).parent
yaml_path = (
current_path.parent.parent.parent.parent
/ "declarative-agents"
/ "agent-samples"
/ "foundry"
/ "MicrosoftLearnAgent.yaml"
)
# create the agent from the yaml
async with (
AzureCliCredential() as credential,
AgentFactory(client_kwargs={"credential": credential}, safe_mode=False).create_agent_from_yaml_path(
yaml_path
) as agent,
):
response = await agent.run("How do I create a storage account with private endpoint using bicep?")
print("Agent response:", response.text)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from pathlib import Path
from agent_framework.declarative import AgentFactory
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def main():
"""Create an agent from a declarative yaml specification and run it."""
# get the path
current_path = Path(__file__).parent
yaml_path = (
current_path.parent.parent.parent.parent
/ "declarative-agents"
/ "agent-samples"
/ "openai"
/ "OpenAIResponses.yaml"
)
# create the agent from the yaml
agent = AgentFactory(safe_mode=False).create_agent_from_yaml_path(yaml_path)
# use the agent
response = await agent.run("Why is the sky blue, answer in Dutch?")
# Use response.value with try/except for safe parsing
try:
parsed = response.value
print("Agent response:", parsed)
except Exception:
print("Agent response:", response.text)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,15 @@
# Shared configuration for samples/02-agents/devui
# Used by in_memory_mode.py, main.py, and as a fallback for discovered samples.
# Run `az login` before starting Azure-backed samples.
# Microsoft Foundry samples
FOUNDRY_PROJECT_ENDPOINT=https://your-project.services.ai.azure.com
FOUNDRY_MODEL=gpt-4o
# Azure OpenAI workflow sample
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_CHAT_MODEL=gpt-4o
# Optional fallback env name also supported by workflow_with_agents/workflow.py:
AZURE_OPENAI_MODEL=gpt-4o
# Optional if you need to override the default API version:
AZURE_OPENAI_API_VERSION=2024-10-21
+19
View File
@@ -0,0 +1,19 @@
# Auto-generated Dockerfiles from DevUI deployment
*/Dockerfile
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
# Environment files (may contain secrets)
.env
*.env
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
+208
View File
@@ -0,0 +1,208 @@
# DevUI Samples
This folder contains sample agents and workflows designed to work with the Agent Framework DevUI - a lightweight web interface for running and testing agents interactively.
## What is DevUI?
DevUI is a sample application that provides:
- A web interface for testing agents and workflows
- OpenAI-compatible API endpoints
- Directory-based entity discovery
- In-memory entity registration
- Sample entity gallery
> **Note**: DevUI is a sample app for development and testing. For production use, build your own custom interface using the Agent Framework SDK.
## Quick Start
### Option 1: In-Memory Mode (Programmatic Registration)
Run a single sample directly. This demonstrates how to register agents and workflows in code without using DevUI's directory discovery.
This sample uses Azure AI Foundry. Before running it:
1. Copy `.env.example` in this folder to `.env`, or export the same values in your shell
2. Set `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL`
3. Run `az login`
Then start the sample:
```bash
cd python/samples/02-agents/devui
python in_memory_mode.py
```
This opens your browser at http://localhost:8090 with two Foundry-backed agents and a simple text transformation workflow.
### Option 2: Directory Discovery with Shared Root `.env`
Run the folder-level launcher to load `samples/02-agents/devui/.env` and then start DevUI with directory discovery for this folder:
```bash
cd python/samples/02-agents/devui
python main.py
```
This starts the server at http://localhost:8080 with all discoverable agents and workflows available. The root `.env` acts as shared fallback configuration for discovered samples.
### Option 3: Directory Discovery with the `devui` CLI
If you prefer the CLI directly, you can still launch DevUI from this folder:
```bash
cd python/samples/02-agents/devui
devui .
```
DevUI discovery checks for a sample-specific `.env` first and then falls back to `.env` in `samples/02-agents/devui/`.
## Sample Structure
DevUI discovers samples from Python packages that export either `agent` or `workflow`.
Typical agent layout:
```
agent_name/
├── __init__.py # Must export: agent = ...
├── agent.py # Agent implementation
└── .env.example # Optional example environment variables
```
Typical workflow layout:
```
workflow_name/
├── __init__.py # Must export: workflow = ...
├── workflow.py # Workflow implementation
├── workflow.yaml # Optional declarative definition
└── .env.example # Optional example environment variables
```
## Available Samples
### Agents
| Sample | What it demonstrates | Required keys / auth |
| ------ | -------------------- | -------------------- |
| [**agent_weather/**](agent_weather/) | A richer Foundry-backed weather agent that shows chat middleware, function middleware, tool calling, and an approval-required tool alongside auto-approved tools. | `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL`, plus Azure CLI auth via `az login` |
| [**agent_foundry/**](agent_foundry/) | A minimal Foundry-backed weather agent with current weather and forecast tools. Use this when you want the smallest possible directory-discovered agent sample. | `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL`, plus Azure CLI auth via `az login` |
### Workflows
| Sample | What it demonstrates | Required keys / auth |
| ------ | -------------------- | -------------------- |
| [**workflow_declarative/**](workflow_declarative/) | A YAML-defined workflow loaded through `WorkflowFactory`, with nested age-based branching and no model client code. | None |
| [**workflow_with_agents/**](workflow_with_agents/) | A content review workflow that uses agents as executors and routes based on structured review output (`Writer -> Reviewer -> Editor/Publisher -> Summarizer`). | `AZURE_OPENAI_ENDPOINT`, plus `AZURE_OPENAI_CHAT_MODEL` or `AZURE_OPENAI_MODEL`; Azure CLI auth via `az login`; `AZURE_OPENAI_API_VERSION` is optional |
| [**workflow_spam/**](workflow_spam/) | A multi-step spam detection workflow with human-in-the-loop approval, branching for spam vs. legitimate messages, and a final reporting step. | None |
| [**workflow_fanout/**](workflow_fanout/) | A larger fan-out/fan-in data processing workflow with parallel validation, multiple transformations, QA, aggregation, and demo failure toggles. | None |
### Standalone Examples
| Sample | What it demonstrates | Required keys / auth |
| ------ | -------------------- | -------------------- |
| [**in_memory_mode.py**](in_memory_mode.py) | Registers multiple entities directly in Python: two Foundry-backed agents plus a simple workflow, all served from one file without directory discovery. | `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL`, plus Azure CLI auth via `az login` |
## Environment Variables
For samples that require external services:
1. Copy `.env.example` to `.env`
2. Fill in the required values
3. Run `az login` for samples that use Azure CLI authentication
Directory discovery checks `.env` files in this order:
1. The entity directory itself, for example `agent_weather/.env`
2. The root DevUI samples folder, `samples/02-agents/devui/.env`
That means the root `.env.example` can hold shared defaults for multiple samples, while a sample-specific `.env` can override those values when needed.
`in_memory_mode.py` and `main.py` both load `.env` from `samples/02-agents/devui/`, so the root `.env.example` in this folder is the right starting point for both commands.
Alternatively, set environment variables globally:
```bash
# Foundry-backed samples
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com"
export FOUNDRY_MODEL="gpt-4o"
# Azure OpenAI workflow_with_agents sample
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com"
export AZURE_OPENAI_CHAT_MODEL="gpt-4o"
export AZURE_OPENAI_MODEL="gpt-4o"
az login
```
## Using DevUI with Your Own Agents
To make your agent discoverable by DevUI:
1. Create a folder for your agent
2. Add an `__init__.py` that exports `agent` or `workflow`
3. (Optional) Add a `.env` file for environment variables
Example:
```python
# my_agent/__init__.py
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
agent = Agent(
name="MyAgent",
description="My custom agent",
client=OpenAIChatClient(),
# ... your configuration
)
```
Then run:
```bash
devui /path/to/my/agents/folder
```
## API Usage
DevUI exposes OpenAI-compatible endpoints:
```bash
curl -X POST http://localhost:8080/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "agent-framework",
"input": "What is the weather in Seattle?",
"extra_body": {"entity_id": "agent_directory_weather-agent_<uuid>"}
}'
```
List available entities:
```bash
curl http://localhost:8080/v1/entities
```
## Learn More
- [DevUI Documentation](../../../packages/devui/README.md)
- [Agent Framework Documentation](https://docs.microsoft.com/agent-framework)
- [Sample Guidelines](../../SAMPLE_GUIDELINES.md)
## Troubleshooting
**Missing credentials or settings**: Check your `.env` files, confirm the required variables for the sample you are running, and make sure `az login` has completed for Azure-authenticated samples.
**Import errors**: Make sure you've installed the devui package:
```bash
pip install agent-framework-devui --pre
```
**Port conflicts**: DevUI uses ports 8080 (directory mode) and 8090 (in-memory mode) by default. Close other services or specify a different port:
```bash
devui --port 8888
```
@@ -0,0 +1,5 @@
# Azure AI Foundry Configuration
# Make sure to run 'az login' before starting devui
FOUNDRY_PROJECT_ENDPOINT=https://your-project.services.ai.azure.com
FOUNDRY_MODEL=gpt-4o
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Weather agent sample for DevUI testing."""
from .agent import agent # ty: ignore[unresolved-import] # pyrefly: ignore
__all__ = ["agent"]
@@ -0,0 +1,88 @@
# Copyright (c) Microsoft. All rights reserved.
"""Foundry-based weather agent for Agent Framework Debug UI.
This agent uses Azure AI Foundry with Azure CLI authentication.
Make sure to run 'az login' before starting devui.
"""
import os
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 22
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
def get_forecast(
location: Annotated[str, Field(description="The location to get the forecast for.")],
days: Annotated[int, Field(description="Number of days for forecast")] = 3,
) -> str:
"""Get weather forecast for multiple days."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
forecast: list[str] = []
for day in range(1, days + 1):
condition = conditions[day % len(conditions)]
temp = 18 + day
forecast.append(f"Day {day}: {condition}, {temp}°C")
return f"Weather forecast for {location}:\n" + "\n".join(forecast)
# Agent instance following Agent Framework conventions
agent = Agent(
name="FoundryWeatherAgent",
client=FoundryChatClient(
project_endpoint=os.environ.get("FOUNDRY_PROJECT_ENDPOINT"),
model=os.environ.get("FOUNDRY_MODEL"),
credential=AzureCliCredential(),
),
instructions="""
You are a weather assistant using Azure AI Foundry models. You can provide
current weather information and forecasts for any location. Always be helpful
and provide detailed weather information when asked.
""",
tools=[get_weather, get_forecast],
)
def main():
"""Launch the Foundry weather agent in DevUI."""
import logging
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Foundry Weather Agent")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: agent_FoundryWeatherAgent")
logger.info("Note: Make sure 'az login' has been run for authentication")
# Launch server with the agent
serve(entities=[agent], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,5 @@
# Azure AI Foundry Configuration
# Make sure to run 'az login' before starting devui
FOUNDRY_PROJECT_ENDPOINT=https://your-project.services.ai.azure.com
FOUNDRY_MODEL=gpt-4o
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Weather agent sample for DevUI testing."""
from .agent import agent # ty: ignore[unresolved-import] # pyrefly: ignore
__all__ = ["agent"]
@@ -0,0 +1,188 @@
# Copyright (c) Microsoft. All rights reserved.
"""Sample weather agent for Agent Framework Debug UI."""
import logging
import os
from collections.abc import AsyncIterable, Awaitable, Callable
from typing import Annotated
from agent_framework import (
Agent,
ChatContext,
ChatResponse,
ChatResponseUpdate,
Content,
FunctionInvocationContext,
Message,
MiddlewareTermination,
ResponseStream,
chat_middleware,
function_middleware,
tool,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework_devui import register_cleanup
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
def cleanup_resources():
"""Cleanup function that runs when DevUI shuts down."""
logger.info("=" * 60)
logger.info(" Cleaning up resources...")
logger.info(" (In production, this would close credentials, sessions, etc.)")
logger.info("=" * 60)
@chat_middleware
async def security_filter_middleware(
context: ChatContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Chat middleware that blocks requests containing sensitive information."""
blocked_terms = ["password", "secret", "api_key", "token"]
# Check only the last message (most recent user input)
last_message = context.messages[-1] if context.messages else None
if last_message and last_message.role == "user" and last_message.text:
message_lower = last_message.text.lower()
for term in blocked_terms:
if term in message_lower:
error_message = (
"I cannot process requests containing sensitive information. "
"Please rephrase your question without including passwords, secrets, "
"or other sensitive data."
)
if context.stream:
# Streaming mode: wrap in ResponseStream
async def blocked_stream(msg: str = error_message) -> AsyncIterable[ChatResponseUpdate]:
yield ChatResponseUpdate(
contents=[Content.from_text(text=msg)],
role="assistant",
)
response = ChatResponse(messages=[Message(role="assistant", contents=[error_message])])
context.result = ResponseStream(blocked_stream(), finalizer=lambda _, r=response: r)
else:
# Non-streaming mode: return complete response
context.result = ChatResponse(
messages=[
Message(
role="assistant",
contents=[error_message],
)
]
)
raise MiddlewareTermination(result=context.result)
await call_next()
@function_middleware
async def atlantis_location_filter_middleware(
context: FunctionInvocationContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Function middleware that blocks weather requests for Atlantis."""
# Check if location parameter is "atlantis"
location = getattr(context.arguments, "location", None)
if location and location.lower() == "atlantis":
context.result = (
"Blocked! Hold up right there!! Tell the user that "
"'Atlantis is a special place, we must never ask about the weather there!!'"
)
raise MiddlewareTermination(result=context.result)
await call_next()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, "The location to get the weather for."],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 53
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
def get_forecast(
location: Annotated[str, "The location to get the forecast for."],
days: Annotated[int, "Number of days for forecast"] = 3,
) -> str:
"""Get weather forecast for multiple days."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
forecast: list[str] = []
for day in range(1, days + 1):
condition = conditions[0]
temp = 53
forecast.append(f"Day {day}: {condition}, {temp}°C")
return f"Weather forecast for {location}:\n" + "\n".join(forecast)
@tool(approval_mode="always_require")
def send_email(
recipient: Annotated[str, "The email address of the recipient."],
subject: Annotated[str, "The subject of the email."],
body: Annotated[str, "The body content of the email."],
) -> str:
"""Simulate sending an email."""
return f"Email sent to {recipient} with subject '{subject}'."
# Agent instance following Agent Framework conventions
agent = Agent(
name="WeatherAgent",
description="A helpful agent that provides weather information and forecasts",
instructions="""
You are a weather assistant. You can provide current weather information
and forecasts for any location. Always be helpful and provide detailed
weather information when asked.
""",
client=FoundryChatClient(
project_endpoint=os.environ.get("FOUNDRY_PROJECT_ENDPOINT"),
model=os.environ.get("FOUNDRY_MODEL"),
credential=AzureCliCredential(),
),
tools=[get_weather, get_forecast, send_email],
middleware=[security_filter_middleware, atlantis_location_filter_middleware],
)
# Register cleanup hook - demonstrates resource cleanup on shutdown
register_cleanup(agent, cleanup_resources)
def main():
"""Launch the Weather Agent in DevUI."""
import logging
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Weather Agent")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: agent_WeatherAgent")
# Launch server with the agent
serve(entities=[agent], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,135 @@
# Copyright (c) Microsoft. All rights reserved.
"""Example of using Agent Framework DevUI with in-memory entity registration.
This demonstrates the simplest way to serve agents and workflows as OpenAI-compatible API endpoints.
Includes both agents and a basic workflow to showcase different entity types.
"""
import logging
import os
from typing import Annotated
from agent_framework import (
Agent,
Executor,
WorkflowBuilder,
WorkflowContext,
handler,
tool,
)
from agent_framework.devui import serve
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
from typing_extensions import Never
# Load environment variables from .env file
load_dotenv()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, "The location to get the weather for."],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 53
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
def get_time(
timezone: Annotated[str, "The timezone to get time for."] = "UTC",
) -> str:
"""Get current time for a timezone."""
from datetime import datetime
# Simplified for example
return f"Current time in {timezone}: {datetime.now().strftime('%H:%M:%S')}"
# Basic workflow executors
class UpperCase(Executor):
"""Convert text to uppercase."""
@handler
async def to_upper(self, text: str, ctx: WorkflowContext[str]) -> None:
"""Convert input to uppercase and forward to next executor."""
result = text.upper()
await ctx.send_message(result)
class AddExclamation(Executor):
"""Add exclamation mark to text."""
@handler
async def add_exclamation(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
"""Add exclamation and yield as workflow output."""
result = f"{text}!"
await ctx.yield_output(result)
def main():
"""Main function demonstrating in-memory entity registration."""
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
# Create Azure OpenAI chat client
client = FoundryChatClient(
model=os.environ["FOUNDRY_MODEL"],
project_endpoint=os.environ.get("FOUNDRY_PROJECT_ENDPOINT"),
credential=AzureCliCredential(),
)
# Create agents
weather_assistant = Agent(
name="weather-assistant",
description="Provides weather information and time",
instructions=(
"You are a helpful weather and time assistant. Use the available tools to "
"provide accurate weather information and current time for any location."
),
client=client,
tools=[get_weather, get_time],
)
simple_agent = Agent(
name="general-assistant",
description="A simple conversational agent",
instructions="You are a helpful assistant.",
client=client,
)
# Create a basic workflow: Input -> UpperCase -> AddExclamation -> Output
upper_executor = UpperCase(id="upper_case")
exclaim_executor = AddExclamation(id="add_exclamation")
basic_workflow = (
WorkflowBuilder(
name="Text Transformer",
description="Simple 2-step workflow that converts text to uppercase and adds exclamation",
start_executor=upper_executor,
)
.add_edge(upper_executor, exclaim_executor)
.build()
)
# Collect entities for serving
entities = [weather_assistant, simple_agent, basic_workflow]
logger.info("Starting DevUI on http://localhost:8090")
logger.info("Entities available:")
logger.info(" - Agents: weather-assistant, general-assistant")
logger.info(" - Workflow: basic text transformer (uppercase + exclamation)")
# Launch server with auto-generated entity IDs
serve(entities=entities, port=8090, auto_open=True)
if __name__ == "__main__":
main()
+32
View File
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft. All rights reserved.
"""Launch DevUI with folder discovery for the samples in this directory.
This sample demonstrates:
- Loading a shared root `.env` file for the DevUI samples folder
- Starting DevUI in directory discovery mode for this folder
- Using root-level settings as fallbacks for discovered samples
"""
from pathlib import Path
from agent_framework.devui import serve
from dotenv import load_dotenv
def main() -> None:
"""Load the root .env file and launch DevUI with folder discovery."""
samples_dir = Path(__file__).resolve().parent
# 1. Load shared defaults for the samples in this folder.
load_dotenv(samples_dir / ".env")
# 2. Start DevUI and discover entities from this directory.
serve(entities_dir=str(samples_dir), auto_open=True)
if __name__ == "__main__":
main()
# Sample output:
# Starting Agent Framework DevUI on 127.0.0.1:8080
@@ -0,0 +1,3 @@
# Copyright (c) Microsoft. All rights reserved.
"""Declarative workflow sample for DevUI."""
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Run the declarative workflow sample with DevUI.
Demonstrates conditional branching based on age input using YAML-defined workflow.
"""
from pathlib import Path
from agent_framework.declarative import WorkflowFactory
from agent_framework.devui import serve
factory = WorkflowFactory()
workflow_path = Path(__file__).parent / "workflow.yaml"
workflow = factory.create_workflow_from_yaml_path(workflow_path)
def main():
"""Run the declarative workflow with DevUI."""
serve(entities=[workflow], auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,64 @@
name: conditional-workflow
description: Demonstrates conditional branching based on user input
inputs:
age:
type: integer
description: The user's age in years
actions:
- kind: SetValue
id: get_age
displayName: Get user age
path: turn.age
value: =inputs.age
- kind: If
id: check_age
displayName: Check age category
condition: =turn.age < 13
then:
- kind: SetValue
path: turn.category
value: child
- kind: SendActivity
activity:
text: "Welcome, young one! Here are some fun activities for kids."
else:
- kind: If
condition: =turn.age < 20
then:
- kind: SetValue
path: turn.category
value: teenager
- kind: SendActivity
activity:
text: "Hey there! Check out these cool things for teens."
else:
- kind: If
condition: =turn.age < 65
then:
- kind: SetValue
path: turn.category
value: adult
- kind: SendActivity
activity:
text: "Welcome! Here are our professional services."
else:
- kind: SetValue
path: turn.category
value: senior
- kind: SendActivity
activity:
text: "Welcome! Enjoy our senior member benefits."
- kind: SendActivity
id: summary
displayName: Send category summary
activity:
text: '=Concat("You have been categorized as: ", turn.category)'
- kind: SetValue
id: set_output
path: workflow.outputs.category
value: =turn.category
@@ -0,0 +1,3 @@
# Copyright (c) Microsoft. All rights reserved.
"""Fanout workflow example."""
@@ -0,0 +1,703 @@
# Copyright (c) Microsoft. All rights reserved.
"""Complex Fan-In/Fan-Out Data Processing Workflow.
This workflow demonstrates a sophisticated data processing pipeline with multiple stages:
1. Data Ingestion - Simulates loading data from multiple sources
2. Data Validation - Multiple validators run in parallel to check data quality
3. Data Transformation - Fan-out to different transformation processors
4. Quality Assurance - Multiple QA checks run in parallel
5. Data Aggregation - Fan-in to combine processed results
6. Final Processing - Generate reports and complete workflow
The workflow includes realistic delays to simulate actual processing time and
shows complex fan-in/fan-out patterns with conditional processing.
"""
import asyncio
import logging
from dataclasses import dataclass
from enum import Enum
from typing import Literal
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
handler,
)
from pydantic import BaseModel, Field
from typing_extensions import Never
class DataType(Enum):
"""Types of data being processed."""
CUSTOMER = "customer"
TRANSACTION = "transaction"
PRODUCT = "product"
ANALYTICS = "analytics"
class ValidationResult(Enum):
"""Results of data validation."""
VALID = "valid"
WARNING = "warning"
ERROR = "error"
class ProcessingRequest(BaseModel):
"""Complex input structure for data processing workflow."""
# Basic information
data_source: Literal["database", "api", "file_upload", "streaming"] = Field(
description="The source of the data to be processed", default="database"
)
data_type: Literal["customer", "transaction", "product", "analytics"] = Field(
description="Type of data being processed", default="customer"
)
processing_priority: Literal["low", "normal", "high", "critical"] = Field(
description="Processing priority level", default="normal"
)
# Processing configuration
batch_size: int = Field(description="Number of records to process in each batch", default=500, ge=100, le=10000)
quality_threshold: float = Field(
description="Minimum quality score required (0.0-1.0)", default=0.8, ge=0.0, le=1.0
)
# Validation settings
enable_schema_validation: bool = Field(description="Enable schema validation checks", default=True)
enable_security_validation: bool = Field(description="Enable security validation checks", default=True)
enable_quality_validation: bool = Field(description="Enable data quality validation checks", default=True)
# Transformation options
transformations: list[Literal["normalize", "enrich", "aggregate"]] = Field(
description="List of transformations to apply", default=["normalize", "enrich"]
)
# Optional description
description: str | None = Field(description="Optional description of the processing request", default=None)
# Test failure scenarios
force_validation_failure: bool = Field(
description="Force validation failure for testing (demo purposes)", default=False
)
force_transformation_failure: bool = Field(
description="Force transformation failure for testing (demo purposes)", default=False
)
@dataclass
class DataBatch:
"""Represents a batch of data being processed."""
batch_id: str
data_type: DataType
size: int
content: str
source: str = "unknown"
timestamp: float = 0.0
@dataclass
class ValidationReport:
"""Report from data validation."""
batch_id: str
validator_id: str
result: ValidationResult
issues_found: int
processing_time: float
details: str
@dataclass
class TransformationResult:
"""Result from data transformation."""
batch_id: str
transformer_id: str
original_size: int
processed_size: int
transformation_type: str
processing_time: float
success: bool
@dataclass
class QualityAssessment:
"""Quality assessment result."""
batch_id: str
assessor_id: str
quality_score: float
recommendations: list[str]
processing_time: float
@dataclass
class ProcessingSummary:
"""Summary of all processing stages."""
batch_id: str
total_processing_time: float
validation_reports: list[ValidationReport]
transformation_results: list[TransformationResult]
quality_assessments: list[QualityAssessment]
final_status: str
# Data Ingestion Stage
class DataIngestion(Executor):
"""Simulates ingesting data from multiple sources with delays."""
@handler
async def ingest_data(self, request: ProcessingRequest, ctx: WorkflowContext[DataBatch]) -> None:
"""Simulate data ingestion with realistic delays based on input configuration."""
# Simulate network delay based on data source
delay_map = {"database": 1.5, "api": 3.0, "file_upload": 4.0, "streaming": 1.0}
delay = delay_map.get(request.data_source, 3.0)
await asyncio.sleep(delay) # Fixed delay for demo
# Simulate data size based on priority and configuration
base_size = request.batch_size
if request.processing_priority == "critical":
size_multiplier = 1.7 # Critical priority gets the largest batches
elif request.processing_priority == "high":
size_multiplier = 1.3 # High priority gets larger batches
elif request.processing_priority == "low":
size_multiplier = 0.6 # Low priority gets smaller batches
else: # normal
size_multiplier = 1.0 # Normal priority uses base size
actual_size = int(base_size * size_multiplier)
batch = DataBatch(
batch_id=f"batch_{5555}", # Fixed batch ID for demo
data_type=DataType(request.data_type),
size=actual_size,
content=f"Processing {request.data_type} data from {request.data_source}",
source=request.data_source,
timestamp=asyncio.get_event_loop().time(),
)
# Store both batch data and original request in workflow state
ctx.set_state(f"batch_{batch.batch_id}", batch)
ctx.set_state(f"request_{batch.batch_id}", request)
await ctx.send_message(batch)
# Validation Stage (Fan-out)
class SchemaValidator(Executor):
"""Validates data schema and structure."""
@handler
async def validate_schema(self, batch: DataBatch, ctx: WorkflowContext[ValidationReport]) -> None:
"""Perform schema validation with processing delay."""
# Check if schema validation is enabled
request = ctx.get_state(f"request_{batch.batch_id}")
if not request or not request.enable_schema_validation:
return
# Simulate schema validation processing
processing_time = 2.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Simulate validation results - consider force failure flag
issues = 4 if request.force_validation_failure else 2 # Fixed issue counts
result = (
ValidationResult.VALID
if issues <= 1
else (ValidationResult.WARNING if issues <= 2 else ValidationResult.ERROR)
)
report = ValidationReport(
batch_id=batch.batch_id,
validator_id=self.id,
result=result,
issues_found=issues,
processing_time=processing_time,
details=f"Schema validation found {issues} issues in {batch.data_type.value} data from {batch.source}",
)
await ctx.send_message(report)
class DataQualityValidator(Executor):
"""Validates data quality and completeness."""
@handler
async def validate_quality(self, batch: DataBatch, ctx: WorkflowContext[ValidationReport]) -> None:
"""Perform data quality validation."""
# Check if quality validation is enabled
request = ctx.get_state(f"request_{batch.batch_id}")
if not request or not request.enable_quality_validation:
return
processing_time = 2.5 # Fixed processing time
await asyncio.sleep(processing_time)
# Quality checks are stricter for higher priority data
issues = (
2 # Fixed issue count for high priority
if request.processing_priority in ["critical", "high"]
else 3 # Fixed issue count for normal priority
)
if request.force_validation_failure:
issues = max(issues, 4) # Ensure failure
result = (
ValidationResult.VALID
if issues <= 1
else (ValidationResult.WARNING if issues <= 3 else ValidationResult.ERROR)
)
report = ValidationReport(
batch_id=batch.batch_id,
validator_id=self.id,
result=result,
issues_found=issues,
processing_time=processing_time,
details=f"Quality check found {issues} data quality issues (priority: {request.processing_priority})",
)
await ctx.send_message(report)
class SecurityValidator(Executor):
"""Validates data for security and compliance issues."""
@handler
async def validate_security(self, batch: DataBatch, ctx: WorkflowContext[ValidationReport]) -> None:
"""Perform security validation."""
# Check if security validation is enabled
request = ctx.get_state(f"request_{batch.batch_id}")
if not request or not request.enable_security_validation:
return
processing_time = 3.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Security is more stringent for customer/transaction data
issues = 1 if batch.data_type in [DataType.CUSTOMER, DataType.TRANSACTION] else 2
if request.force_validation_failure:
issues = max(issues, 1) # Force at least one security issue
# Security errors are more serious - less tolerance
result = ValidationResult.VALID if issues == 0 else ValidationResult.ERROR
report = ValidationReport(
batch_id=batch.batch_id,
validator_id=self.id,
result=result,
issues_found=issues,
processing_time=processing_time,
details=f"Security scan found {issues} security issues in {batch.data_type.value} data",
)
await ctx.send_message(report)
# Validation Aggregator (Fan-in)
class ValidationAggregator(Executor):
"""Aggregates validation results and decides on next steps."""
@handler
async def aggregate_validations(
self, reports: list[ValidationReport], ctx: WorkflowContext[DataBatch, str]
) -> None:
"""Aggregate all validation reports and make processing decision."""
if not reports:
return
batch_id = reports[0].batch_id
request = ctx.get_state(f"request_{batch_id}")
await asyncio.sleep(1) # Aggregation processing time
total_issues = sum(report.issues_found for report in reports)
has_errors = any(report.result == ValidationResult.ERROR for report in reports)
# Calculate quality score (0.0 to 1.0)
max_possible_issues = len(reports) * 5 # Assume max 5 issues per validator
quality_score = max(0.0, 1.0 - (total_issues / max_possible_issues))
# Decision logic: fail if errors OR quality below threshold
should_fail = has_errors or (quality_score < request.quality_threshold)
if should_fail:
failure_reason: list[str] = []
if has_errors:
failure_reason.append("validation errors detected")
if quality_score < request.quality_threshold:
failure_reason.append(
f"quality score {quality_score:.2f} below threshold {request.quality_threshold:.2f}"
)
reason = " and ".join(failure_reason)
await ctx.yield_output(
f"Batch {batch_id} failed validation: {reason}. "
f"Total issues: {total_issues}, Quality score: {quality_score:.2f}"
)
return
# Retrieve original batch from workflow state
batch_data = ctx.get_state(f"batch_{batch_id}")
if batch_data:
await ctx.send_message(batch_data)
else:
# Fallback: create a simplified batch
batch = DataBatch(
batch_id=batch_id,
data_type=DataType.ANALYTICS,
size=500,
content="Validated data ready for transformation",
)
await ctx.send_message(batch)
# Transformation Stage (Fan-out)
class DataNormalizer(Executor):
"""Normalizes and cleans data."""
@handler
async def normalize_data(self, batch: DataBatch, ctx: WorkflowContext[TransformationResult]) -> None:
"""Perform data normalization."""
request = ctx.get_state(f"request_{batch.batch_id}")
# Check if normalization is enabled
if not request or "normalize" not in request.transformations:
# Send a "skipped" result
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=batch.size,
transformation_type="normalization",
processing_time=0.1,
success=True, # Consider skipped as successful
)
await ctx.send_message(result)
return
processing_time = 4.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Simulate data size change during normalization
processed_size = int(batch.size * 1.0) # No size change for demo
# Consider force failure flag
success = not request.force_transformation_failure # 75% success rate simplified to always success
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=processed_size,
transformation_type="normalization",
processing_time=processing_time,
success=success,
)
await ctx.send_message(result)
class DataEnrichment(Executor):
"""Enriches data with additional information."""
@handler
async def enrich_data(self, batch: DataBatch, ctx: WorkflowContext[TransformationResult]) -> None:
"""Perform data enrichment."""
request = ctx.get_state(f"request_{batch.batch_id}")
# Check if enrichment is enabled
if not request or "enrich" not in request.transformations:
# Send a "skipped" result
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=batch.size,
transformation_type="enrichment",
processing_time=0.1,
success=True, # Consider skipped as successful
)
await ctx.send_message(result)
return
processing_time = 5.0 # Fixed processing time
await asyncio.sleep(processing_time)
processed_size = int(batch.size * 1.3) # Enrichment increases data
# Consider force failure flag
success = not request.force_transformation_failure # 67% success rate simplified to always success
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=processed_size,
transformation_type="enrichment",
processing_time=processing_time,
success=success,
)
await ctx.send_message(result)
class DataAggregator(Executor):
"""Aggregates and summarizes data."""
@handler
async def aggregate_data(self, batch: DataBatch, ctx: WorkflowContext[TransformationResult]) -> None:
"""Perform data aggregation."""
request = ctx.get_state(f"request_{batch.batch_id}")
# Check if aggregation is enabled
if not request or "aggregate" not in request.transformations:
# Send a "skipped" result
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=batch.size,
transformation_type="aggregation",
processing_time=0.1,
success=True, # Consider skipped as successful
)
await ctx.send_message(result)
return
processing_time = 2.5 # Fixed processing time
await asyncio.sleep(processing_time)
processed_size = int(batch.size * 0.5) # Aggregation reduces data
# Consider force failure flag
success = not request.force_transformation_failure # 80% success rate simplified to always success
result = TransformationResult(
batch_id=batch.batch_id,
transformer_id=self.id,
original_size=batch.size,
processed_size=processed_size,
transformation_type="aggregation",
processing_time=processing_time,
success=success,
)
await ctx.send_message(result)
# Quality Assurance Stage (Fan-out)
class PerformanceAssessor(Executor):
"""Assesses performance characteristics of processed data."""
@handler
async def assess_performance(
self, results: list[TransformationResult], ctx: WorkflowContext[QualityAssessment]
) -> None:
"""Assess performance of transformations."""
if not results:
return
batch_id = results[0].batch_id
processing_time = 2.0 # Fixed processing time
await asyncio.sleep(processing_time)
avg_processing_time = sum(r.processing_time for r in results) / len(results)
success_rate = sum(1 for r in results if r.success) / len(results)
quality_score = (success_rate * 0.7 + (1 - min(avg_processing_time / 10, 1)) * 0.3) * 100
recommendations: list[str] = []
if success_rate < 0.8:
recommendations.append("Consider improving transformation reliability")
if avg_processing_time > 5:
recommendations.append("Optimize processing performance")
if quality_score < 70:
recommendations.append("Review overall data pipeline efficiency")
assessment = QualityAssessment(
batch_id=batch_id,
assessor_id=self.id,
quality_score=quality_score,
recommendations=recommendations,
processing_time=processing_time,
)
await ctx.send_message(assessment)
class AccuracyAssessor(Executor):
"""Assesses accuracy and correctness of processed data."""
@handler
async def assess_accuracy(
self, results: list[TransformationResult], ctx: WorkflowContext[QualityAssessment]
) -> None:
"""Assess accuracy of transformations."""
if not results:
return
batch_id = results[0].batch_id
processing_time = 3.0 # Fixed processing time
await asyncio.sleep(processing_time)
# Simulate accuracy analysis
accuracy_score = 85.0 # Fixed accuracy score
recommendations: list[str] = []
if accuracy_score < 85:
recommendations.append("Review data transformation algorithms")
if accuracy_score < 80:
recommendations.append("Implement additional validation steps")
assessment = QualityAssessment(
batch_id=batch_id,
assessor_id=self.id,
quality_score=accuracy_score,
recommendations=recommendations,
processing_time=processing_time,
)
await ctx.send_message(assessment)
# Final Processing and Completion
class FinalProcessor(Executor):
"""Final processing stage that combines all results."""
@handler
async def process_final_results(
self, assessments: list[QualityAssessment], ctx: WorkflowContext[Never, str]
) -> None:
"""Generate final processing summary and complete workflow."""
if not assessments:
await ctx.yield_output("No quality assessments received")
return
batch_id = assessments[0].batch_id
# Simulate final processing delay
await asyncio.sleep(2)
# Calculate overall metrics
avg_quality_score = sum(a.quality_score for a in assessments) / len(assessments)
total_recommendations = sum(len(a.recommendations) for a in assessments)
total_processing_time = sum(a.processing_time for a in assessments)
# Determine final status
if avg_quality_score >= 85:
final_status = "EXCELLENT"
elif avg_quality_score >= 75:
final_status = "GOOD"
elif avg_quality_score >= 65:
final_status = "ACCEPTABLE"
else:
final_status = "NEEDS_IMPROVEMENT"
completion_message = (
f"Batch {batch_id} processing completed!\n"
f"📊 Overall Quality Score: {avg_quality_score:.1f}%\n"
f"⏱️ Total Processing Time: {total_processing_time:.1f}s\n"
f"💡 Total Recommendations: {total_recommendations}\n"
f"🎖️ Final Status: {final_status}"
)
await ctx.yield_output(completion_message)
# Workflow Builder Helper
class WorkflowSetupHelper:
"""Helper class to set up the complex workflow with state management."""
@staticmethod
async def store_batch_data(batch: DataBatch, ctx: WorkflowContext) -> None:
"""Store batch data in workflow state for later retrieval."""
ctx.set_state(f"batch_{batch.batch_id}", batch)
# Create the workflow instance
def create_complex_workflow():
"""Create the complex fan-in/fan-out workflow."""
# Create all executors
data_ingestion = DataIngestion(id="data_ingestion")
# Validation stage (fan-out)
schema_validator = SchemaValidator(id="schema_validator")
quality_validator = DataQualityValidator(id="quality_validator")
security_validator = SecurityValidator(id="security_validator")
validation_aggregator = ValidationAggregator(id="validation_aggregator")
# Transformation stage (fan-out)
data_normalizer = DataNormalizer(id="data_normalizer")
data_enrichment = DataEnrichment(id="data_enrichment")
data_aggregator_exec = DataAggregator(id="data_aggregator")
# Quality assurance stage (fan-out)
performance_assessor = PerformanceAssessor(id="performance_assessor")
accuracy_assessor = AccuracyAssessor(id="accuracy_assessor")
# Final processing
final_processor = FinalProcessor(id="final_processor")
# Build the workflow with complex fan-in/fan-out patterns
return (
WorkflowBuilder(
name="Data Processing Pipeline",
description="Complex workflow with parallel validation, transformation, and quality assurance stages",
start_executor=data_ingestion,
)
# Fan-out to validation stage
.add_fan_out_edges(data_ingestion, [schema_validator, quality_validator, security_validator])
# Fan-in from validation to aggregator
.add_fan_in_edges([schema_validator, quality_validator, security_validator], validation_aggregator)
# Fan-out to transformation stage
.add_fan_out_edges(validation_aggregator, [data_normalizer, data_enrichment, data_aggregator_exec])
# Fan-in to quality assurance stage (both assessors receive all transformation results)
.add_fan_in_edges([data_normalizer, data_enrichment, data_aggregator_exec], performance_assessor)
.add_fan_in_edges([data_normalizer, data_enrichment, data_aggregator_exec], accuracy_assessor)
# Fan-in to final processor
.add_fan_in_edges([performance_assessor, accuracy_assessor], final_processor)
.build()
)
# Export the workflow for DevUI discovery
workflow = create_complex_workflow()
def main():
"""Launch the fanout workflow in DevUI."""
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Complex Fan-In/Fan-Out Data Processing Workflow")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: workflow_complex_workflow")
# Launch server with the workflow
serve(entities=[workflow], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Spam detection workflow sample for DevUI testing."""
from .workflow import workflow # ty: ignore[unresolved-import] # pyrefly: ignore
__all__ = ["workflow"]
@@ -0,0 +1,440 @@
# Copyright (c) Microsoft. All rights reserved.
"""Spam Detection Workflow Sample for DevUI.
The following sample demonstrates a comprehensive 4-step workflow with multiple executors
that process, detect spam, and handle email messages. This workflow illustrates
complex branching logic with human-in-the-loop approval and realistic processing delays.
Workflow Steps:
1. Email Preprocessor - Cleans and prepares the email
2. Spam Detector - Analyzes content and determines if the message is spam (with human approval)
3a. Spam Handler - Processes spam messages (quarantine, log, remove)
3b. Message Responder - Handles legitimate messages (validate, respond)
4. Final Processor - Completes the workflow with logging and cleanup
"""
import asyncio
import logging
from dataclasses import dataclass
from typing import Literal
from agent_framework import (
Case,
Default,
Executor,
WorkflowBuilder,
WorkflowContext,
handler,
response_handler,
)
from pydantic import BaseModel, Field
from typing_extensions import Never
# Define response model with clear user guidance
class SpamDecision(BaseModel):
"""User's decision on whether the email is spam."""
decision: Literal["spam", "not spam"] = Field(
description="Enter 'spam' to mark as spam, or 'not spam' to mark as legitimate"
)
@dataclass
class EmailContent:
"""A data class to hold the processed email content."""
original_message: str
cleaned_message: str
word_count: int
has_suspicious_patterns: bool = False
@dataclass
class SpamDetectorResponse:
"""A data class to hold the spam detection results."""
email_content: EmailContent
is_spam: bool = False
confidence_score: float = 0.0
spam_reasons: list[str] | None = None
human_reviewed: bool = False
human_decision: str | None = None
ai_original_classification: bool = False
def __post_init__(self):
"""Initialize spam_reasons list if None."""
if self.spam_reasons is None:
self.spam_reasons = []
@dataclass
class SpamApprovalRequest:
"""Human-in-the-loop approval request for spam classification."""
email_message: str
detected_as_spam: bool
confidence: float
reasons: list[str]
full_email_content: EmailContent
@dataclass
class ProcessingResult:
"""A data class to hold the final processing result."""
original_message: str
action_taken: str
processing_time: float
status: str
is_spam: bool
confidence_score: float
spam_reasons: list[str]
was_human_reviewed: bool = False
human_override: str | None = None
ai_original_decision: bool = False
class EmailRequest(BaseModel):
"""Request model for email processing."""
email: str = Field(
description="The email message to be processed.",
default="Hi there, are you interested in our new urgent offer today? Click here!",
)
class EmailPreprocessor(Executor):
"""Step 1: An executor that preprocesses and cleans email content."""
@handler
async def handle_email(self, email: EmailRequest, ctx: WorkflowContext[EmailContent]) -> None:
"""Clean and preprocess the email message."""
await asyncio.sleep(1.5) # Simulate preprocessing time
# Simulate email cleaning
cleaned = email.email.strip().lower()
word_count = len(email.email.split())
# Check for suspicious patterns
suspicious_patterns = ["urgent", "limited time", "act now", "free money"]
has_suspicious = any(pattern in cleaned for pattern in suspicious_patterns)
result = EmailContent(
original_message=email.email,
cleaned_message=cleaned,
word_count=word_count,
has_suspicious_patterns=has_suspicious,
)
await ctx.send_message(result)
class SpamDetector(Executor):
"""Step 2: An executor that analyzes content and determines if a message is spam."""
def __init__(self, spam_keywords: list[str], id: str):
"""Initialize the executor with spam keywords."""
super().__init__(id=id)
self._spam_keywords = spam_keywords
@handler
async def handle_email_content(
self, email_content: EmailContent, ctx: WorkflowContext[SpamApprovalRequest]
) -> None:
"""Analyze email content and determine if the message is spam, then request human approval."""
await asyncio.sleep(2.0) # Simulate analysis and detection time
email_text = email_content.cleaned_message
# Analyze content for risk indicators
contains_links = "http" in email_text or "www" in email_text
has_attachments = "attachment" in email_text
sentiment_score = 0.5 if email_content.has_suspicious_patterns else 0.8
# Build risk indicators
risk_indicators: list[str] = []
if email_content.has_suspicious_patterns:
risk_indicators.append("suspicious_language")
if contains_links:
risk_indicators.append("contains_links")
if has_attachments:
risk_indicators.append("has_attachments")
if email_content.word_count < 10:
risk_indicators.append("too_short")
# Check for spam keywords
keyword_matches = [kw for kw in self._spam_keywords if kw in email_text]
# Calculate spam probability
spam_score = 0.0
spam_reasons: list[str] = []
if keyword_matches:
spam_score += 0.4
spam_reasons.append(f"spam_keywords: {keyword_matches}")
if email_content.has_suspicious_patterns:
spam_score += 0.3
spam_reasons.append("suspicious_patterns")
if len(risk_indicators) >= 3:
spam_score += 0.2
spam_reasons.append("high_risk_indicators")
if sentiment_score < 0.4:
spam_score += 0.1
spam_reasons.append("negative_sentiment")
is_spam = spam_score >= 0.5
# Request human approval before proceeding using new API
approval_request = SpamApprovalRequest(
email_message=email_text[:200], # First 200 chars
detected_as_spam=is_spam,
confidence=spam_score,
reasons=spam_reasons,
full_email_content=email_content,
)
await ctx.request_info(
request_data=approval_request,
response_type=SpamDecision,
)
@response_handler
async def handle_human_response(
self, original_request: SpamApprovalRequest, response: SpamDecision, ctx: WorkflowContext[SpamDetectorResponse]
) -> None:
"""Process human approval response and continue workflow."""
print(f"[SpamDetector] handle_human_response called with response: {response}")
# Get stored detection result
ai_original = original_request.detected_as_spam
confidence_score = original_request.confidence
spam_reasons = original_request.reasons
# Parse human decision from the response model
human_decision = response.decision.strip().lower()
# Determine final classification based on human input
if human_decision in ["not spam"]:
is_spam = False
elif human_decision in ["spam"]:
is_spam = True
else:
# Default to AI decision if unclear
is_spam = ai_original
result = SpamDetectorResponse(
email_content=original_request.full_email_content,
is_spam=is_spam,
confidence_score=confidence_score,
spam_reasons=spam_reasons,
human_reviewed=True,
human_decision=response.decision,
ai_original_classification=ai_original,
)
print(
f"[SpamDetector] Sending SpamDetectorResponse: is_spam={is_spam}, confidence={confidence_score}, human_reviewed=True"
)
await ctx.send_message(result)
print("[SpamDetector] Message sent successfully")
class SpamHandler(Executor):
"""Step 3a: An executor that handles spam messages with quarantine and logging."""
@handler
async def handle_spam_detection(
self,
spam_result: SpamDetectorResponse,
ctx: WorkflowContext[ProcessingResult],
) -> None:
"""Handle spam messages by quarantining and logging."""
if not spam_result.is_spam:
raise RuntimeError("Message is not spam, cannot process with spam handler.")
await asyncio.sleep(2.2) # Simulate spam handling time
result = ProcessingResult(
original_message=spam_result.email_content.original_message,
action_taken="quarantined_and_logged",
processing_time=2.2,
status="spam_handled",
is_spam=spam_result.is_spam,
confidence_score=spam_result.confidence_score,
spam_reasons=spam_result.spam_reasons or [],
was_human_reviewed=spam_result.human_reviewed,
human_override=spam_result.human_decision,
ai_original_decision=spam_result.ai_original_classification,
)
await ctx.send_message(result)
class LegitimateMessageHandler(Executor):
"""Step 3b: An executor that handles legitimate (non-spam) messages."""
@handler
async def handle_spam_detection(
self,
spam_result: SpamDetectorResponse,
ctx: WorkflowContext[ProcessingResult],
) -> None:
"""Respond to legitimate messages."""
if spam_result.is_spam:
raise RuntimeError("Message is spam, cannot respond with message responder.")
await asyncio.sleep(2.5) # Simulate response time
result = ProcessingResult(
original_message=spam_result.email_content.original_message,
action_taken="delivered_to_inbox",
processing_time=2.5,
status="message_processed",
is_spam=spam_result.is_spam,
confidence_score=spam_result.confidence_score,
spam_reasons=spam_result.spam_reasons or [],
was_human_reviewed=spam_result.human_reviewed,
human_override=spam_result.human_decision,
ai_original_decision=spam_result.ai_original_classification,
)
await ctx.send_message(result)
class FinalProcessor(Executor):
"""Step 4: An executor that completes the workflow with final logging and cleanup."""
@handler
async def handle_processing_result(
self,
result: ProcessingResult,
ctx: WorkflowContext[Never, str],
) -> None:
"""Complete the workflow with final processing and logging."""
await asyncio.sleep(1.5) # Simulate final processing time
total_time = result.processing_time + 1.5
# Build classification status with human review info
classification = "SPAM" if result.is_spam else "LEGITIMATE"
# Add human review context
review_status = ""
if result.was_human_reviewed:
if result.ai_original_decision != result.is_spam:
review_status = " (human-overridden)"
else:
review_status = " (human-verified)"
# Build appropriate message based on classification
if result.is_spam:
# For spam messages
spam_indicators = ", ".join(result.spam_reasons) if result.spam_reasons else "none detected"
if result.was_human_reviewed:
ai_status = "SPAM" if result.ai_original_decision else "LEGITIMATE"
human_decision = result.human_override if result.human_override else "unknown"
completion_message = (
f"Email classified as {classification}{review_status}.\n"
f"AI detected: {ai_status} (confidence: {result.confidence_score:.2f})\n"
f"Human reviewer: {human_decision}\n"
f"Spam indicators: {spam_indicators}\n"
f"Action: Message quarantined for review\n"
f"Processing time: {total_time:.1f}s"
)
else:
completion_message = (
f"Email classified as {classification} (confidence: {result.confidence_score:.2f}).\n"
f"Spam indicators: {spam_indicators}\n"
f"Action: Message quarantined for review\n"
f"Processing time: {total_time:.1f}s"
)
else:
# For legitimate messages
if result.was_human_reviewed:
ai_status = "SPAM" if result.ai_original_decision else "LEGITIMATE"
human_decision = result.human_override if result.human_override else "unknown"
completion_message = (
f"Email classified as {classification}{review_status}.\n"
f"AI detected: {ai_status} (confidence: {result.confidence_score:.2f})\n"
f"Human reviewer: {human_decision}\n"
f"Action: Delivered to inbox\n"
f"Processing time: {total_time:.1f}s"
)
else:
completion_message = (
f"Email classified as {classification} (confidence: {result.confidence_score:.2f}).\n"
f"Action: Delivered to inbox\n"
f"Processing time: {total_time:.1f}s"
)
await ctx.yield_output(completion_message)
# DevUI will provide checkpoint storage automatically via the new workflow API
# No need to create checkpoint storage here anymore!
# Create the workflow instance that DevUI can discover
spam_keywords = ["spam", "advertisement", "offer", "click here", "winner", "congratulations", "urgent"]
# Create all the executors for the 4-step workflow
email_preprocessor = EmailPreprocessor(id="email_preprocessor")
spam_detector = SpamDetector(spam_keywords, id="spam_detector")
spam_handler = SpamHandler(id="spam_handler")
legitimate_message_handler = LegitimateMessageHandler(id="legitimate_message_handler")
final_processor = FinalProcessor(id="final_processor")
# Build the comprehensive 4-step workflow with branching logic and HIL support
# Note: No checkpoint_storage in constructor - DevUI will pass checkpoint_storage at runtime
workflow = (
WorkflowBuilder(
name="Email Spam Detector",
description="4-step email classification workflow with human-in-the-loop spam approval",
start_executor=email_preprocessor,
)
.add_edge(email_preprocessor, spam_detector)
# HIL handled within spam_detector via @response_handler
# Continue with branching logic after human approval
# Only route SpamDetectorResponse messages (not SpamApprovalRequest)
.add_switch_case_edge_group(
spam_detector,
[
Case(condition=lambda x: isinstance(x, SpamDetectorResponse) and x.is_spam, target=spam_handler),
Default(
target=legitimate_message_handler
), # Default handles non-spam and non-SpamDetectorResponse messages
],
)
.add_edge(spam_handler, final_processor)
.add_edge(legitimate_message_handler, final_processor)
.build()
)
# Note: Workflow metadata is determined by executors and graph structure
def main():
"""Launch the spam detection workflow in DevUI."""
from agent_framework.devui import serve
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Spam Detection Workflow")
logger.info("Available at: http://localhost:8090")
logger.info("Entity ID: workflow_spam_detection")
# Launch server with the workflow
serve(entities=[workflow], port=8090, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,9 @@
# Azure OpenAI configuration for the Responses-based workflow sample
# This sample uses Azure CLI auth, so run `az login` before starting DevUI.
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_CHAT_MODEL=gpt-4o
# Optional fallback env name also supported by the client:
# AZURE_OPENAI_MODEL=gpt-4o
# Optional if you need to override the default API version:
AZURE_OPENAI_API_VERSION=2024-10-21
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Sequential Agents Workflow - Writer → Reviewer."""
from .workflow import workflow # ty: ignore[unresolved-import] # pyrefly: ignore
__all__ = ["workflow"]
@@ -0,0 +1,185 @@
# Copyright (c) Microsoft. All rights reserved.
"""Agent Workflow - Content Review with Quality Routing.
This sample demonstrates:
- Using agents directly as executors
- Conditional routing based on structured outputs
- Quality-based workflow paths with convergence
Use case: Content creation with automated review.
Writer creates content, Reviewer evaluates quality:
- High quality (score >= 80): → Publisher → Summarizer
- Low quality (score < 80): → Editor → Publisher → Summarizer
Both paths converge at Summarizer for final report.
"""
import os
from typing import Any
from agent_framework import Agent, AgentExecutorResponse, WorkflowBuilder
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel
# Load environment variables from .env file
load_dotenv()
# Define structured output for review results
class ReviewResult(BaseModel):
"""Review evaluation with scores and feedback."""
score: int # Overall quality score (0-100)
feedback: str # Concise, actionable feedback
clarity: int # Clarity score (0-100)
completeness: int # Completeness score (0-100)
accuracy: int # Accuracy score (0-100)
structure: int # Structure score (0-100)
# Condition function: route to editor if score < 80
def needs_editing(message: Any) -> bool:
"""Check if content needs editing based on review score."""
if not isinstance(message, AgentExecutorResponse):
return False
try:
review = ReviewResult.model_validate_json(message.agent_response.text)
return review.score < 80
except Exception:
return False
# Condition function: content is approved (score >= 80)
def is_approved(message: Any) -> bool:
"""Check if content is approved (high quality)."""
if not isinstance(message, AgentExecutorResponse):
return True
try:
review = ReviewResult.model_validate_json(message.agent_response.text)
return review.score >= 80
except Exception:
return True
# Create Azure OpenAI Responses chat client
client = OpenAIChatClient(
model=os.environ.get("AZURE_OPENAI_CHAT_MODEL") or os.environ.get("AZURE_OPENAI_MODEL"),
azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
api_version=os.environ.get("AZURE_OPENAI_API_VERSION"),
credential=AzureCliCredential(),
)
# Create Writer agent - generates content
writer = Agent(
client=client,
name="Writer",
instructions=(
"You are an excellent content writer. "
"Create clear, engaging content based on the user's request. "
"Focus on clarity, accuracy, and proper structure."
),
)
# Create Reviewer agent - evaluates and provides structured feedback
reviewer = Agent(
client=client,
name="Reviewer",
instructions=(
"You are an expert content reviewer. "
"Evaluate the writer's content based on:\n"
"1. Clarity - Is it easy to understand?\n"
"2. Completeness - Does it fully address the topic?\n"
"3. Accuracy - Is the information correct?\n"
"4. Structure - Is it well-organized?\n\n"
"Return a JSON object with:\n"
"- score: overall quality (0-100)\n"
"- feedback: concise, actionable feedback\n"
"- clarity, completeness, accuracy, structure: individual scores (0-100)"
),
default_options=OpenAIChatOptions[Any](response_format=ReviewResult),
)
# Create Editor agent - improves content based on feedback
editor = Agent(
client=client,
name="Editor",
instructions=(
"You are a skilled editor. "
"You will receive content along with review feedback. "
"Improve the content by addressing all the issues mentioned in the feedback. "
"Maintain the original intent while enhancing clarity, completeness, accuracy, and structure."
),
)
# Create Publisher agent - formats content for publication
publisher = Agent(
client=client,
name="Publisher",
instructions=(
"You are a publishing agent. "
"You receive either approved content or edited content. "
"Format it for publication with proper headings and structure."
),
)
# Create Summarizer agent - creates final publication report
summarizer = Agent(
client=client,
name="Summarizer",
instructions=(
"You are a summarizer agent. "
"Create a final publication report that includes:\n"
"1. A brief summary of the published content\n"
"2. The workflow path taken (direct approval or edited)\n"
"3. Key highlights and takeaways\n"
"Keep it concise and professional."
),
)
# Build workflow with branching and convergence:
# Writer → Reviewer → [branches]:
# - If score >= 80: → Publisher → Summarizer (direct approval path)
# - If score < 80: → Editor → Publisher → Summarizer (improvement path)
# Both paths converge at Summarizer for final report
workflow = (
WorkflowBuilder(
name="Content Review Workflow",
description="Multi-agent content creation workflow with quality-based routing (Writer → Reviewer → Editor/Publisher)",
start_executor=writer,
)
.add_edge(writer, reviewer)
# Branch 1: High quality (>= 80) goes directly to publisher
.add_edge(reviewer, publisher, condition=is_approved)
# Branch 2: Low quality (< 80) goes to editor first, then publisher
.add_edge(reviewer, editor, condition=needs_editing)
.add_edge(editor, publisher)
# Both paths converge: Publisher → Summarizer
.add_edge(publisher, summarizer)
.build()
)
def main():
"""Launch the branching workflow in DevUI."""
import logging
from agent_framework.devui import serve
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
logger.info("Starting Agent Workflow (Content Review with Quality Routing)")
logger.info("Available at: http://localhost:8093")
logger.info("\nThis workflow demonstrates:")
logger.info("- Conditional routing based on structured outputs")
logger.info("- Path 1 (score >= 80): Reviewer → Publisher → Summarizer")
logger.info("- Path 2 (score < 80): Reviewer → Editor → Publisher → Summarizer")
logger.info("- Both paths converge at Summarizer for final report")
serve(entities=[workflow], port=8093, auto_open=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,94 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "agent-framework-foundry",
# ]
# ///
# Run with: uv run samples/02-agents/embeddings/foundry_embeddings.py
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import pathlib
from agent_framework import Content
from agent_framework.foundry import FoundryEmbeddingClient
from dotenv import load_dotenv
load_dotenv()
"""Microsoft Foundry Image Embedding Example
This sample demonstrates how to generate image embeddings using the
Foundry embedding client with the Cohere-embed-v3-english model.
Images are passed as ``Content`` objects created with ``Content.from_data()``.
Prerequisites:
Deploy an embedding model to a Foundry-hosted inference endpoint that supports image inputs,
such as Cohere-embed-v3-english.
The details page for that model, has a target URI and a Key, which should be set in environment variables or a .env
file as follows, the target URI should append the `/models` path:
- FOUNDRY_MODELS_ENDPOINT: Your Foundry models endpoint URL, for instance:
https://<apim-instance>.azure-api.net/<foundry-instance>/models
- FOUNDRY_MODELS_API_KEY: Your API key
- FOUNDRY_EMBEDDING_MODEL: The text embedding model name
(e.g. "text-embedding-3-small")
- FOUNDRY_IMAGE_EMBEDDING_MODEL: The image embedding model name
(e.g. "Cohere-embed-v3-english")
"""
SAMPLE_IMAGE_PATH = pathlib.Path(__file__).parent.parent.parent / "shared" / "sample_assets" / "sample_image.jpg"
async def main() -> None:
"""Generate image embeddings with Foundry."""
async with FoundryEmbeddingClient() as client:
# 1. Generate an image embedding.
image_bytes = SAMPLE_IMAGE_PATH.read_bytes()
image_content = Content.from_data(data=image_bytes, media_type="image/jpeg")
result = await client.get_embeddings([image_content])
print(f"Image embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
print(f"Model: {result[0].model}")
print(f"Usage: {result.usage}")
print()
# 2. Generate image and text embeddings separately in one call.
# The client dispatches text to the text endpoint and images to the image
# endpoint, then reassembles results in the original input order.
result = await client.get_embeddings(["A half-timbered house in a forested valley", image_content])
print(f"Text embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
print(f"Image embedding dimensions: {result[1].dimensions}")
print(f"First 5 values: {result[1].vector[:5]}")
print()
# 3. Generate image embeddings with input_type option.
result = await client.get_embeddings(
[image_content],
options={"input_type": "document"},
)
print(f"Document embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output (using deployment: Cohere-embed-v3-english, which is Cohere's "embed-english-v3.0-image" model):
Image embedding dimensions: 1024
First 5 values: [0.029159546, -0.007926941, -0.0032978058, -0.0030403137, -0.012786865]
Model: embed-english-v3.0-image
Usage: {'input_token_count': 1000, 'output_token_count': 0}
Text embedding dimensions: 1536
First 5 values: [-0.019439403, 0.015791258, 0.012358093, 0.0028533707, -0.01649483]
Image embedding dimensions: 1024
First 5 values: [0.029159546, -0.007926941, -0.0032978058, -0.0030403137, -0.012786865]
Document embedding dimensions: 1024
First 5 values: [0.029159546, -0.007926941, -0.0032978058, -0.0030403137, -0.012786865]
"""
@@ -0,0 +1,66 @@
# Copyright (c) Microsoft. All rights reserved.
# Run with: uv run samples/02-agents/embeddings/openai_embeddings.py
import asyncio
import os
from agent_framework.openai import OpenAIEmbeddingClient
from dotenv import load_dotenv
"""This sample demonstrates OpenAI embedding generation with explicit constructor settings.
Prerequisites:
Set ``OPENAI_API_KEY`` in your environment or in a local ``.env`` file.
"""
load_dotenv()
async def main() -> None:
"""Generate embeddings with OpenAI."""
client = OpenAIEmbeddingClient(
model="text-embedding-3-small",
api_key=os.getenv("OPENAI_API_KEY"),
)
# 1. Generate a single embedding.
result = await client.get_embeddings(["Hello, world!"])
print(f"Single embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
print(f"Model: {result[0].model}")
print(f"Usage: {result.usage}")
print()
# 2. Generate embeddings for multiple inputs.
texts = [
"The weather is sunny today.",
"It is raining outside.",
"Machine learning is fascinating.",
]
result = await client.get_embeddings(texts)
print(f"Batch of {len(result)} embeddings, each with {result[0].dimensions} dimensions")
print(f"First embedding vector: {result[0].vector[:5]}")
print()
# 3. Generate embeddings with custom dimensions.
result = await client.get_embeddings(["Custom dimensions example"], options={"dimensions": 256})
print(f"Custom dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Single embedding dimensions: 1536
First 5 values: [0.012, -0.034, 0.056, -0.078, 0.090]
Model: text-embedding-3-small
Usage: {'prompt_tokens': 4, 'total_tokens': 4}
Batch of 3 embeddings, each with 1536 dimensions
Custom dimensions: 256
"""
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft. All rights reserved.
# Run with: uv run samples/02-agents/embeddings/azure_openai_embeddings.py
import asyncio
import os
from agent_framework.openai import OpenAIEmbeddingClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates Azure OpenAI embedding generation with ``OpenAIEmbeddingClient``.
Prerequisites:
Set the following environment variables or add them to a local ``.env`` file:
- ``AZURE_OPENAI_ENDPOINT``: Your Azure OpenAI endpoint URL
- ``AZURE_OPENAI_EMBEDDING_MODEL``: The embedding deployment name
- ``AZURE_OPENAI_API_VERSION``: Optional API version override
Sign in with ``az login`` before running the sample.
"""
load_dotenv()
async def main() -> None:
"""Generate embeddings with Azure OpenAI."""
async with AzureCliCredential() as credential:
client = OpenAIEmbeddingClient(
model=os.getenv("AZURE_OPENAI_EMBEDDING_MODEL"),
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
credential=credential,
)
# 1. Generate a single embedding.
result = await client.get_embeddings(["Hello, world!"])
print(f"Single embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
print(f"Model: {result[0].model}")
print(f"Usage: {result.usage}")
print()
# 2. Generate embeddings for multiple inputs.
texts = [
"The weather is sunny today.",
"It is raining outside.",
"Machine learning is fascinating.",
]
result = await client.get_embeddings(texts)
print(f"Batch of {len(result)} embeddings, each with {result[0].dimensions} dimensions")
print(f"First embedding vector: {result[0].vector[:5]}")
print()
# 3. Generate embeddings with custom dimensions.
result = await client.get_embeddings(["Custom dimensions example"], options={"dimensions": 256})
print(f"Custom dimensions: {result[0].dimensions}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Single embedding dimensions: 1536
First 5 values: [0.012, -0.034, 0.056, -0.078, 0.090]
Model: text-embedding-3-small
Usage: {'prompt_tokens': 4, 'total_tokens': 4}
Batch of 3 embeddings, each with 1536 dimensions
Custom dimensions: 256
"""
@@ -0,0 +1,81 @@
# Copyright (c) Microsoft. All rights reserved.
"""Evaluate an agent with local checks — no API keys needed.
Demonstrates the simplest evaluation workflow:
1. Define checks using the @evaluator decorator
2. Run evaluate_agent() which calls agent.run() under the covers
3. Assert results in CI or inspect interactively
Usage:
uv run python samples/02-agents/evaluation/evaluate_agent.py
"""
import asyncio
import os
from agent_framework import (
Agent,
LocalEvaluator,
evaluate_agent,
evaluator,
keyword_check,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
load_dotenv()
# A custom check — parameter names determine what data you receive
@evaluator
def is_helpful(response: str) -> bool:
"""Check the response isn't empty or a refusal."""
refusals = ["i can't", "i'm not able", "i don't know"]
return len(response) > 10 and not any(r in response.lower() for r in refusals)
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ.get("FOUNDRY_MODEL", "gpt-4o"),
credential=AzureCliCredential(),
)
agent = Agent(
client=client,
name="weather-assistant",
instructions="You are a helpful weather assistant.",
)
# Combine built-in and custom checks
local = LocalEvaluator(
keyword_check("weather"), # response must mention "weather"
is_helpful, # custom check
)
# evaluate_agent() calls agent.run() for each query, then evaluates
results = await evaluate_agent(
agent=agent,
queries=[
"What's the weather like in Seattle?",
"Will it rain in London tomorrow?",
"What should I wear for 30°C weather?",
],
evaluators=local,
)
for r in results:
print(f"{r.provider}: {r.passed}/{r.total} passed")
for item in r.items:
print(f" [{item.status}] Q: {(item.input_text or '')[:50]} A: {(item.output_text or '')[:50]}...")
for score in item.scores:
print(f" {'PASS' if score.passed else 'FAIL'} {score.name}")
# Use in CI: will raise EvalNotPassedError if any check fails
# results[0].raise_for_status()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,121 @@
# Copyright (c) Microsoft. All rights reserved.
"""Evaluate multimodal (image) conversations locally.
Demonstrates that the evaluation pipeline preserves image content:
1. Build EvalItems with image content in conversations
2. Use @evaluator checks that inspect multimodal content
3. Verify images flow through the eval pipeline intact
Usage:
uv run python samples/02-agents/evaluation/evaluate_multimodal.py
"""
import asyncio
import base64
from agent_framework import (
Content,
EvalItem,
LocalEvaluator,
Message,
evaluator,
)
# -- Custom evaluators that inspect multimodal content --
@evaluator
def has_image_content(conversation: list) -> bool:
"""Check that the conversation contains at least one image."""
return any(
c.type in ("data", "uri") and c.media_type and c.media_type.startswith("image/")
for m in conversation
for c in (m.contents or [])
)
@evaluator
def response_describes_image(response: str) -> bool:
"""Check that the assistant response acknowledges the image."""
image_words = {"image", "picture", "photo", "shows", "depicts", "see"}
return any(word in response.lower() for word in image_words)
@evaluator
def image_count(conversation: list) -> float:
"""Return the number of images in the conversation as a score."""
count = sum(
1
for m in conversation
for c in (m.contents or [])
if c.type in ("data", "uri") and c.media_type and c.media_type.startswith("image/")
)
return float(count)
# A tiny 1x1 red PNG for demonstration (no external dependencies needed)
_TINY_PNG = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
)
async def main() -> None:
# Build eval items with multimodal content (no agent run needed)
items = [
# Item 1: User sends an image URL with a question
EvalItem(
conversation=[
Message(
"user",
[
Content.from_text("What do you see in this image?"),
Content.from_uri(
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/300px-PNG_transparency_demonstration_1.png",
media_type="image/png",
),
],
),
Message("assistant", ["The image shows two dice on a transparent background."]),
]
),
# Item 2: User sends inline image bytes
EvalItem(
conversation=[
Message(
"user",
[
Content.from_text("Describe this picture"),
Content.from_data(data=_TINY_PNG, media_type="image/png"),
],
),
Message("assistant", ["I see a small red image — it appears to be a single pixel."]),
]
),
# Item 3: Text-only conversation (should fail has_image_content)
EvalItem(
conversation=[
Message("user", ["Tell me about cats"]),
Message("assistant", ["Cats are wonderful pets."]),
]
),
]
local = LocalEvaluator(
has_image_content,
response_describes_image,
image_count,
)
results = await local.evaluate(items)
print(f"\n{results.provider}: {results.passed}/{results.total} passed")
for item in results.items:
print(f"\n [{item.status}] Q: {(item.input_text or '')[:60]}...")
for score in item.scores:
symbol = "PASS" if score.passed else "FAIL"
print(f" {symbol} {score.name}: {score.score}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,73 @@
# Copyright (c) Microsoft. All rights reserved.
"""Evaluate an agent with expected outputs and tool call checks.
Demonstrates ground-truth comparison and tool usage evaluation:
1. Provide expected outputs alongside queries
2. Use built-in tool_calls_present for tool verification
3. Combine multiple evaluation criteria
Usage:
uv run python samples/02-agents/evaluation/evaluate_with_expected.py
"""
import asyncio
import os
from agent_framework import (
Agent,
LocalEvaluator,
evaluate_agent,
evaluator,
tool_calls_present,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
load_dotenv()
@evaluator
def response_matches_expected(response: str, expected_output: str) -> float:
"""Score based on word overlap with expected output."""
if not expected_output:
return 1.0
response_words = set(response.lower().split())
expected_words = set(expected_output.lower().split())
return len(response_words & expected_words) / max(len(expected_words), 1)
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ.get("FOUNDRY_MODEL", "gpt-4o"),
credential=AzureCliCredential(),
)
agent = Agent(
client=client,
name="math-tutor",
instructions="You are a math tutor. Answer concisely.",
)
local = LocalEvaluator(
response_matches_expected,
tool_calls_present, # verifies expected tools were called
)
results = await evaluate_agent(
agent=agent,
queries=["What is 2 + 2?", "What is the square root of 144?"],
expected_output=["4", "12"],
evaluators=local,
)
for r in results:
print(f"{r.provider}: {r.passed}/{r.total} passed")
for item in r.items:
print(f" [{item.status}] {item.input_text} -> {(item.output_text or '')[:80]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
import agent_framework
"""Feature stage introspection.
This sample demonstrates how to inspect feature lifecycle metadata on Agent
Framework APIs.
The recommended minimal setup for consumers is:
1. Read `__feature_stage__` with `getattr(...)` to see whether an API is staged
2. Read `__feature_id__` with `getattr(...)` only when that metadata is present
3. Treat missing metadata as "no explicit feature-stage annotation"
4. Do not rely on `ExperimentalFeature` or `ReleaseCandidateFeature` membership
over time, since staged features may move or be removed as they advance
This sample loops through the symbols exported from the root `agent_framework`
module and reports the ones that currently carry feature-stage metadata.
"""
def describe_api(name: str, api: Any) -> None:
"""Print optional feature-stage metadata for one API."""
feature_stage = getattr(api, "__feature_stage__", "released")
feature_id = getattr(api, "__feature_id__", None)
print(f"{name}:")
print(f" feature_stage = {feature_stage!r}")
print(f" feature_id = {feature_id!r}")
def iter_staged_root_exports() -> list[tuple[str, Any]]:
"""Return root exports that currently carry feature-stage metadata."""
staged_root_symbols: list[tuple[str, Any]] = []
for symbol_name in sorted(agent_framework.__all__):
symbol = getattr(agent_framework, symbol_name)
feature_stage = getattr(symbol, "__feature_stage__", None)
feature_id = getattr(symbol, "__feature_id__", None)
if feature_stage is None and feature_id is None:
continue
staged_root_symbols.append((symbol_name, symbol))
return staged_root_symbols
async def main() -> None:
"""Run the feature-stage introspection sample."""
print("Feature stage introspection")
print("-" * 60)
# 1. Loop through everything exported from the root module.
staged_root_symbols = iter_staged_root_exports()
# 2. Show the root exports that currently carry feature-stage metadata.
if not staged_root_symbols:
print("No root exports currently carry feature-stage metadata.")
return
print("Root exports with feature-stage metadata:")
for name, api in staged_root_symbols:
describe_api(name, api)
print()
print("Root exports without metadata currently have no explicit feature-stage metadata.")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Feature stage introspection
------------------------------------------------------------
Root exports with feature-stage metadata:
<export name>:
feature_stage = 'experimental'
feature_id = '<feature id>'
Root exports without metadata currently have no explicit feature-stage metadata.
"""
+164
View File
@@ -0,0 +1,164 @@
# Harness Agent Samples
This folder demonstrates `create_harness_agent` — a factory function that builds a
pre-configured, batteries-included agent by assembling the full agent pipeline
from a chat client.
## What is `create_harness_agent`?
`create_harness_agent` bundles the following features into a single `Agent` instance:
| Feature | Description |
|---------|-------------|
| Function invocation | Automatic tool calling loop |
| Per-service-call persistence | History persisted after every model call |
| Compaction | Context-window management (sliding window + tool result compaction) |
| TodoProvider | Todo list management for planning and tracking |
| AgentModeProvider | Plan/execute mode tracking |
| MemoryContextProvider | File-based durable memory (when `memory_store` provided) |
| SkillsProvider | File-based skill discovery and progressive loading |
| Shell tool | Shell command execution + environment probing (when `shell_executor` provided) |
| Tool approval | "Don't ask again" standing rules + heuristic auto-approval (enabled by default) |
| Looping | Re-invoke the agent until a `loop_should_continue` predicate is satisfied (when provided) |
| OpenTelemetry | Built-in observability |
Each feature can be disabled or customized via keyword arguments.
## Samples
| File | Description |
|------|-------------|
| `harness_research.py` | Interactive research assistant with web search, a plan/execute workflow, and an execute-mode loop that re-invokes the agent until every todo is complete |
| `harness_data_processing.py` | Data-processing assistant over a folder of CSV files, demonstrating file-access tools and tool approval |
| [`build_your_own_claw/`](./build_your_own_claw/README.md) | *Build your own claw* blog series — a personal finance assistant built step by step |
## Running
```bash
# Set your Foundry environment variables
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project-name"
export FOUNDRY_MODEL="your-model-deployment-name"
# Authenticate with Azure (required for AzureCliCredential)
az login
# Run a sample against the released agent-framework (PEP 723 isolated env)
uv run samples/02-agents/harness/harness_research.py
```
### Running against the local repo
To run a sample against your **local** `agent-framework` checkout (so it picks
up uncommitted changes), use the workspace environment instead of the isolated
PEP 723 env. From the `python/` directory, run the script with `uv run python`
and add the `textual` UI dependency the harness console needs:
```bash
uv run --with textual python samples/02-agents/harness/harness_research.py
uv run --with textual python samples/02-agents/harness/harness_data_processing.py
```
The workspace environment already provides the editable `agent-framework`
packages plus the samples' other dependencies (`rich`, `python-dotenv`,
`azure-identity`); only `textual` needs to be supplied with `--with`.
> Note: invoking `uv run python <script>` (with `python`) bypasses the PEP 723
> metadata and uses the workspace env; `uv run <script>` (without `python`)
> uses the isolated env with the released package.
## Key Concepts
### Minimal Setup
`create_harness_agent` requires only a chat client:
```python
from agent_framework import create_harness_agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
agent = create_harness_agent(
client=FoundryChatClient(credential=AzureCliCredential()),
)
```
### With Compaction
Provide token budget parameters to enable automatic context-window compaction:
```python
agent = create_harness_agent(
client=FoundryChatClient(credential=AzureCliCredential()),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
)
```
### Further Customization
Disable or customize any feature:
```python
agent = create_harness_agent(
client=client,
max_context_window_tokens=128_000,
max_output_tokens=16_384,
name="my-agent",
agent_instructions="Custom instructions here.",
disable_todo=True, # Skip todo management
disable_mode=True, # Skip plan/execute modes
disable_compaction=True, # Skip compaction
)
```
### Plan/Execute Workflow
The `AgentModeProvider` enables a two-phase workflow:
1. **Plan mode** — Interactive: the agent asks questions, creates todos, gets approval
2. **Execute mode** — Autonomous: the agent works through todos independently
### Shell Tool
Pass a shell executor (e.g. `LocalShellTool` from `agent-framework-tools`) to enable shell
command execution plus automatic environment probing via a `ShellEnvironmentProvider`. The
tool is only wired when the chat client supports shell tools; otherwise a warning is logged
and the shell tool/provider are skipped. The caller owns the executor's lifecycle.
```python
from agent_framework_tools.shell import LocalShellTool, ShellEnvironmentProviderOptions
async with LocalShellTool(acknowledge_unsafe=True) as shell:
agent = create_harness_agent(
client=client,
max_context_window_tokens=128_000,
max_output_tokens=16_384,
shell_executor=shell,
# Optional: customize environment probing.
shell_environment_provider_options=ShellEnvironmentProviderOptions(probe_tools=("git", "python")),
)
```
## Security Considerations
Several harness capabilities extend the agent's trust boundary to external systems the developer
configures. Each is opt-in and requires explicit configuration by the developer, who is responsible
for vetting the external service, agent, skill source, or provider before enabling it:
- **`background_agents`** (`BackgroundAgentsProvider`) — delegates work to developer-supplied agents,
which receive input from the parent and whose output is fed back into its context. A compromised
agent could exfiltrate data or inject adversarial content via indirect prompt injection. Vet all
supplied agents.
- **External skill sources** (`skills_provider` with e.g. `MCPSkillsSource`) — load skill content,
and potentially scripts, from a remote source. A compromised source could return adversarial skills
(indirect prompt injection) or exfiltrate data. Only enable sources you trust.
- **`AgentLoopMiddleware.with_judge`** — sends the request and the agent's latest response to a second,
external judge chat client on every iteration. A compromised judge could exfiltrate that data or
return manipulated feedback. Trust the judge as much as the primary model.
- **`SummarizationStrategy`** (via `before_compaction_strategy` / `after_compaction_strategy`) — calls
out to an LLM whose output permanently becomes chat history. A compromised summarization service
could inject unsafe, persistent instructions. Only use a service you trust as much as the primary
model.
- **Telemetry** — when observability is enabled, telemetry destinations are developer-configured.
Default telemetry is metadata only; enabling sensitive data additionally emits raw message content,
tool arguments, and tool results. See the [observability samples](../observability/README.md).

Some files were not shown because too many files have changed in this diff Show More