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
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:
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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—I’ll remember: you prefer dark roast coffee, and you’re 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: - You’re 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())
|
||||
+117
@@ -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 January–March 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.
|
||||
+159
@@ -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)
|
||||
+50
@@ -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
|
||||
|
@@ -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 full‑text 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 full‑text 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 full‑text 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>
|
||||
"""
|
||||
+163
@@ -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>
|
||||
"""
|
||||
+185
@@ -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
|
||||
@@ -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
|
||||
*~
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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
Reference in New Issue
Block a user