mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Rebase durable task feature branch with main (#2806)
This commit is contained in:
committed by
GitHub
Unverified
parent
a48a8dd524
commit
87a38bc7da
@@ -25,6 +25,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_azure_ai_search.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_azure_ai_search.py) | Azure AI Agent with Azure AI Search Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py) | Azure AI agent with Bing Grounding search for real-time web information |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py) | Azure AI Agent with Code Interpreter Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py) | Azure AI Agent with Code Interpreter File Generation Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_existing_agent.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_existing_agent.py) | Azure AI Agent with Existing Agent Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py) | Azure AI Agent with Existing Thread Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py) | Azure AI Agent with Explicit Settings Example |
|
||||
@@ -47,6 +48,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py`](./getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py) | Azure AI Agent with Bing Custom Search Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_browser_automation.py`](./getting_started/agents/azure_ai/azure_ai_with_browser_automation.py) | Azure AI Agent with Browser Automation Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py`](./getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py) | Azure AI Agent with Code Interpreter Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py`](./getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py) | Azure AI Agent with Code Interpreter File Generation Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_existing_agent.py`](./getting_started/agents/azure_ai/azure_ai_with_existing_agent.py) | Azure AI Agent with Existing Agent Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py`](./getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py) | Azure AI Agent with Existing Conversation Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py`](./getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py) | Azure AI Agent with Explicit Settings Example |
|
||||
@@ -100,6 +102,8 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`getting_started/agents/ollama/ollama_with_openai_chat_client.py`](./getting_started/agents/ollama/ollama_with_openai_chat_client.py) | Ollama with OpenAI Chat Client Example |
|
||||
| [`packages/ollama/getting_started/ollama_agent_basic.py`](../packages/ollama/getting_started/ollama_agent_basic.py) | (Experimental) Ollama Agent with native Ollama Chat Client |
|
||||
| [`packages/ollama/getting_started/ollama_agent_reasoning.py`](../packages/ollama/getting_started/ollama_agent_reasoning.py) | (Experimental) Ollama Reasoning Agent with native Ollama Chat Client |
|
||||
|
||||
### OpenAI
|
||||
|
||||
@@ -145,6 +149,8 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| [`getting_started/chat_client/openai_assistants_client.py`](./getting_started/chat_client/openai_assistants_client.py) | OpenAI Assistants Client Direct Usage Example |
|
||||
| [`getting_started/chat_client/openai_chat_client.py`](./getting_started/chat_client/openai_chat_client.py) | OpenAI Chat Client Direct Usage Example |
|
||||
| [`getting_started/chat_client/openai_responses_client.py`](./getting_started/chat_client/openai_responses_client.py) | OpenAI Responses Client Direct Usage Example |
|
||||
| [`packages/ollama/getting_started/ollama_chat_client.py`](../packages/ollama/getting_started/ollama_chat_client.py) | (Experimental) Ollama Chat Client with native Ollama Chat Client |
|
||||
|
||||
|
||||
## Context Providers
|
||||
|
||||
@@ -219,6 +225,8 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| [`getting_started/multimodal_input/azure_chat_multimodal.py`](./getting_started/multimodal_input/azure_chat_multimodal.py) | Azure OpenAI Chat with multimodal (image) input example |
|
||||
| [`getting_started/multimodal_input/azure_responses_multimodal.py`](./getting_started/multimodal_input/azure_responses_multimodal.py) | Azure OpenAI Responses with multimodal (image) input example |
|
||||
| [`getting_started/multimodal_input/openai_chat_multimodal.py`](./getting_started/multimodal_input/openai_chat_multimodal.py) | OpenAI Chat with multimodal (image) input example |
|
||||
| [`packages/ollama/getting_started/ollama_chat_multimodal.py`](../packages/ollama/getting_started/ollama_chat_multimodal.py) | (Experimental) Ollama Chat with multimodal native Ollama Chat Client |
|
||||
|
||||
|
||||
## Azure Functions
|
||||
|
||||
@@ -239,10 +247,10 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
| [`getting_started/observability/advanced_manual_setup_console_output.py`](./getting_started/observability/advanced_manual_setup_console_output.py) | Advanced manual observability setup with console output |
|
||||
| [`getting_started/observability/advanced_zero_code.py`](./getting_started/observability/advanced_zero_code.py) | Zero-code observability setup example |
|
||||
| [`getting_started/observability/agent_observability.py`](./getting_started/observability/agent_observability.py) | Agent observability example |
|
||||
| [`getting_started/observability/agent_with_foundry_tracing.py`](./getting_started/observability/agent_with_foundry_tracing.py) | Any chat client setup with Azure Foundry Observability |
|
||||
| [`getting_started/observability/azure_ai_agent_observability.py`](./getting_started/observability/azure_ai_agent_observability.py) | Azure AI agent observability example |
|
||||
| [`getting_started/observability/azure_ai_chat_client_with_observability.py`](./getting_started/observability/azure_ai_chat_client_with_observability.py) | Azure AI chat client with observability example |
|
||||
| [`getting_started/observability/setup_observability_with_env_var.py`](./getting_started/observability/setup_observability_with_env_var.py) | Setup observability using environment variables |
|
||||
| [`getting_started/observability/setup_observability_with_parameters.py`](./getting_started/observability/setup_observability_with_parameters.py) | Setup observability using parameters |
|
||||
| [`getting_started/observability/configure_otel_providers_with_env_var.py`](./getting_started/observability/configure_otel_providers_with_env_var.py) | Setup observability using environment variables |
|
||||
| [`getting_started/observability/configure_otel_providers_with_parameters.py`](./getting_started/observability/configure_otel_providers_with_parameters.py) | Setup observability using parameters |
|
||||
| [`getting_started/observability/workflow_observability.py`](./getting_started/observability/workflow_observability.py) | Workflow observability example |
|
||||
|
||||
## Threads
|
||||
|
||||
@@ -14,6 +14,7 @@ This folder contains examples demonstrating different ways to create and use age
|
||||
| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. |
|
||||
| [`azure_ai_with_browser_automation.py`](azure_ai_with_browser_automation.py) | Shows how to use Browser Automation with Azure AI agents to perform automated web browsing tasks and provide responses based on web interactions. Requires a Browser Automation connection configured in your Azure AI project. |
|
||||
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. |
|
||||
| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. |
|
||||
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. |
|
||||
| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentThread with an existing conversation ID. |
|
||||
| [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. |
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
CitationAnnotation,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework._agents import AgentRunResponseUpdate
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI V2 Code Interpreter File Generation Sample
|
||||
|
||||
This sample demonstrates how the V2 AzureAIClient handles file annotations
|
||||
when code interpreter generates text files. It shows both non-streaming
|
||||
and streaming approaches to verify file ID extraction.
|
||||
"""
|
||||
|
||||
QUERY = (
|
||||
"Write a simple Python script that creates a text file called 'sample.txt' containing "
|
||||
"'Hello from the code interpreter!' and save it to disk."
|
||||
)
|
||||
|
||||
|
||||
async def test_non_streaming() -> None:
|
||||
"""Test non-streaming response - should have annotations on TextContent."""
|
||||
print("=== Testing Non-Streaming Response ===")
|
||||
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIClient(credential=credential).create_agent(
|
||||
name="V2CodeInterpreterFileAgent",
|
||||
instructions="You are a helpful assistant that can write and execute Python code to create files.",
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
) as agent,
|
||||
):
|
||||
print(f"User: {QUERY}\n")
|
||||
|
||||
result = await agent.run(QUERY)
|
||||
print(f"Agent: {result.text}\n")
|
||||
|
||||
# Check for annotations in the response
|
||||
annotations_found: list[str] = []
|
||||
# AgentRunResponse has messages property, which contains ChatMessage objects
|
||||
for message in result.messages:
|
||||
for content in message.contents:
|
||||
if isinstance(content, TextContent) and content.annotations:
|
||||
for annotation in content.annotations:
|
||||
if isinstance(annotation, CitationAnnotation) and annotation.file_id:
|
||||
annotations_found.append(annotation.file_id)
|
||||
print(f"Found file annotation: file_id={annotation.file_id}")
|
||||
|
||||
if annotations_found:
|
||||
print(f"SUCCESS: Found {len(annotations_found)} file annotation(s)")
|
||||
else:
|
||||
print("WARNING: No file annotations found in non-streaming response")
|
||||
|
||||
|
||||
async def test_streaming() -> None:
|
||||
"""Test streaming response - check if file content is captured via HostedFileContent."""
|
||||
print("\n=== Testing Streaming Response ===")
|
||||
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIClient(credential=credential).create_agent(
|
||||
name="V2CodeInterpreterFileAgentStreaming",
|
||||
instructions="You are a helpful assistant that can write and execute Python code to create files.",
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
) as agent,
|
||||
):
|
||||
print(f"User: {QUERY}\n")
|
||||
annotations_found: list[str] = []
|
||||
text_chunks: list[str] = []
|
||||
file_ids_found: list[str] = []
|
||||
|
||||
async for update in agent.run_stream(QUERY):
|
||||
if isinstance(update, AgentRunResponseUpdate):
|
||||
for content in update.contents:
|
||||
if isinstance(content, TextContent):
|
||||
if content.text:
|
||||
text_chunks.append(content.text)
|
||||
if content.annotations:
|
||||
for annotation in content.annotations:
|
||||
if isinstance(annotation, CitationAnnotation) and annotation.file_id:
|
||||
annotations_found.append(annotation.file_id)
|
||||
print(f"Found streaming annotation: file_id={annotation.file_id}")
|
||||
elif isinstance(content, HostedFileContent):
|
||||
file_ids_found.append(content.file_id)
|
||||
print(f"Found streaming HostedFileContent: file_id={content.file_id}")
|
||||
|
||||
print(f"\nAgent response: {''.join(text_chunks)[:200]}...")
|
||||
|
||||
if annotations_found or file_ids_found:
|
||||
total = len(annotations_found) + len(file_ids_found)
|
||||
print(f"SUCCESS: Found {total} file reference(s) in streaming")
|
||||
else:
|
||||
print("WARNING: No file annotations found in streaming response")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("AzureAIClient Code Interpreter File Generation Test\n")
|
||||
await test_non_streaming()
|
||||
await test_streaming()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -9,6 +9,8 @@ This folder contains examples demonstrating different ways to create and use age
|
||||
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureAIAgentClient`. It automatically handles all configuration using environment variables. |
|
||||
| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to set up and use HostedWebSearchTool with custom search instances. |
|
||||
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates web search capabilities with proper source citations and comprehensive error handling. |
|
||||
| [`azure_ai_with_bing_grounding_citations.py`](azure_ai_with_bing_grounding_citations.py) | Demonstrates how to extract and display citations from Bing Grounding search responses. Shows how to collect citation annotations (title, URL, snippet) during streaming responses, enabling users to verify sources and access referenced content. |
|
||||
| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. |
|
||||
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
|
||||
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created agents. |
|
||||
| [`azure_ai_with_existing_thread.py`](azure_ai_with_existing_thread.py) | Shows how to work with a pre-existing thread by providing the thread ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created threads. |
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import ChatAgent, CitationAnnotation, HostedWebSearchTool
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
This sample demonstrates how to create an Azure AI agent that uses Bing Grounding
|
||||
search to find real-time information from the web with comprehensive citation support.
|
||||
It shows how to extract and display citations (title, URL, and snippet) from Bing
|
||||
Grounding responses, enabling users to verify sources and explore referenced content.
|
||||
|
||||
Prerequisites:
|
||||
1. A connected Grounding with Bing Search resource in your Azure AI project
|
||||
2. Set BING_CONNECTION_ID environment variable
|
||||
Example: BING_CONNECTION_ID="your-bing-connection-id"
|
||||
|
||||
To set up Bing Grounding:
|
||||
1. Go to Azure AI Foundry portal (https://ai.azure.com)
|
||||
2. Navigate to your project's "Connected resources" section
|
||||
3. Add a new connection for "Grounding with Bing Search"
|
||||
4. Copy the connection ID and set the BING_CONNECTION_ID environment variable
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main function demonstrating Azure AI agent with Bing Grounding search."""
|
||||
# 1. Create Bing Grounding search tool using HostedWebSearchTool
|
||||
# The connection ID will be automatically picked up from environment variable
|
||||
bing_search_tool = HostedWebSearchTool(
|
||||
name="Bing Grounding Search",
|
||||
description="Search the web for current information using Bing",
|
||||
)
|
||||
|
||||
# 2. Use AzureAIAgentClient as async context manager for automatic cleanup
|
||||
async with (
|
||||
AzureAIAgentClient(credential=AzureCliCredential()) as client,
|
||||
ChatAgent(
|
||||
chat_client=client,
|
||||
name="BingSearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant that can search the web for current information. "
|
||||
"Use the Bing search tool to find up-to-date information and provide accurate, "
|
||||
"well-sourced answers. Always cite your sources when possible."
|
||||
),
|
||||
tools=bing_search_tool,
|
||||
) as agent,
|
||||
):
|
||||
# 3. Demonstrate agent capabilities with web search
|
||||
print("=== Azure AI Agent with Bing Grounding Search ===\n")
|
||||
|
||||
user_input = "What is the most popular programming language?"
|
||||
print(f"User: {user_input}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
|
||||
# Stream the response and collect citations
|
||||
citations: list[CitationAnnotation] = []
|
||||
async for chunk in agent.run_stream(user_input):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
|
||||
# Collect citations from Bing Grounding responses
|
||||
for content in getattr(chunk, "contents", []):
|
||||
annotations = getattr(content, "annotations", [])
|
||||
if annotations:
|
||||
citations.extend(annotations)
|
||||
|
||||
print()
|
||||
|
||||
# Display collected citations
|
||||
if citations:
|
||||
print("\n\nCitations:")
|
||||
for i, citation in enumerate(citations, 1):
|
||||
print(f"[{i}] {citation.title}: {citation.url}")
|
||||
if citation.snippet:
|
||||
print(f" Snippet: {citation.snippet}")
|
||||
else:
|
||||
print("\nNo citations found in the response.")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunResponseUpdate, ChatAgent, HostedCodeInterpreterTool, HostedFileContent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent Code Interpreter File Generation Example
|
||||
|
||||
This sample demonstrates using HostedCodeInterpreterTool with AzureAIAgentClient
|
||||
to generate a text file and then retrieve it.
|
||||
|
||||
The test flow:
|
||||
1. Create an agent with code interpreter tool
|
||||
2. Ask the agent to generate a txt file using Python code
|
||||
3. Capture the file_id from HostedFileContent in the response
|
||||
4. Retrieve the file using the agents_client.files API
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Test file generation and retrieval with code interpreter."""
|
||||
|
||||
async with AzureCliCredential() as credential:
|
||||
client = AzureAIAgentClient(credential=credential)
|
||||
|
||||
try:
|
||||
async with ChatAgent(
|
||||
chat_client=client,
|
||||
instructions=(
|
||||
"You are a Python code execution assistant. "
|
||||
"ALWAYS use the code interpreter tool to execute Python code when asked to create files. "
|
||||
"Write actual Python code to create files, do not just describe what you would do."
|
||||
),
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
) as agent:
|
||||
# Be very explicit about wanting code execution and a download link
|
||||
query = (
|
||||
"Use the code interpreter to execute this Python code and then provide me "
|
||||
"with a download link for the generated file:\n"
|
||||
"```python\n"
|
||||
"with open('/mnt/data/sample.txt', 'w') as f:\n"
|
||||
" f.write('Hello, World! This is a test file.')\n"
|
||||
"'/mnt/data/sample.txt'\n" # Return the path so it becomes downloadable
|
||||
"```"
|
||||
)
|
||||
print(f"User: {query}\n")
|
||||
print("=" * 60)
|
||||
|
||||
# Collect file_ids from the response
|
||||
file_ids: list[str] = []
|
||||
|
||||
async for chunk in agent.run_stream(query):
|
||||
if not isinstance(chunk, AgentRunResponseUpdate):
|
||||
continue
|
||||
|
||||
for content in chunk.contents:
|
||||
if content.type == "text":
|
||||
print(content.text, end="", flush=True)
|
||||
elif content.type == "hosted_file":
|
||||
if isinstance(content, HostedFileContent):
|
||||
file_ids.append(content.file_id)
|
||||
print(f"\n[File generated: {content.file_id}]")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
# Attempt to retrieve discovered files
|
||||
if file_ids:
|
||||
print(f"\nAttempting to retrieve {len(file_ids)} file(s):")
|
||||
for file_id in file_ids:
|
||||
try:
|
||||
file_info = await client.agents_client.files.get(file_id)
|
||||
print(f" File {file_id}: Retrieved successfully")
|
||||
print(f" Filename: {file_info.filename}")
|
||||
print(f" Purpose: {file_info.purpose}")
|
||||
print(f" Bytes: {file_info.bytes}")
|
||||
except Exception as e:
|
||||
print(f" File {file_id}: FAILED to retrieve - {e}")
|
||||
else:
|
||||
print("No file IDs were captured from the response.")
|
||||
|
||||
# List all files to see if any exist
|
||||
print("\nListing all files in the agent service:")
|
||||
try:
|
||||
files_list = await client.agents_client.files.list()
|
||||
count = 0
|
||||
for file_info in files_list.data:
|
||||
count += 1
|
||||
print(f" - {file_info.id}: {file_info.filename} ({file_info.purpose})")
|
||||
if count == 0:
|
||||
print(" No files found.")
|
||||
except Exception as e:
|
||||
print(f" Failed to list files: {e}")
|
||||
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -6,7 +6,11 @@ This folder contains examples demonstrating how to use Ollama models with the Ag
|
||||
|
||||
1. **Install Ollama**: Download and install Ollama from [ollama.com](https://ollama.com/)
|
||||
2. **Start Ollama**: Ensure Ollama is running on your local machine
|
||||
3. **Pull a model**: Run `ollama pull mistral` (or any other model you prefer that supports function calling)
|
||||
3. **Pull a model**: Run `ollama pull mistral` (or any other model you prefer)
|
||||
- For function calling examples, use models that support tool calling like `mistral` or `qwen2.5`
|
||||
- For reasoning examples, use models that support reasoning like `qwen2.5:8b`
|
||||
|
||||
> **Note**: Not all models support all features. Function calling and reasoning capabilities depend on the specific model you're using.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -16,15 +20,16 @@ This folder contains examples demonstrating how to use Ollama models with the Ag
|
||||
|
||||
## Configuration
|
||||
|
||||
The examples use environment variables for configuration:
|
||||
The examples use environment variables for configuration. Set the appropriate variables based on which example you're running:
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the following environment variables before running the examples:
|
||||
### For OpenAI Client with Ollama (`ollama_with_openai_chat_client.py`)
|
||||
|
||||
- `OLLAMA_ENDPOINT`: The base URL for your Ollama server
|
||||
Set the following environment variables:
|
||||
|
||||
- `OLLAMA_ENDPOINT`: The base URL for your Ollama server with `/v1/` suffix
|
||||
- Example: `export OLLAMA_ENDPOINT="http://localhost:11434/v1/"`
|
||||
|
||||
- `OLLAMA_MODEL`: The model name to use
|
||||
- Example: `export OLLAMA_MODEL="mistral"`
|
||||
- Must be a model you have pulled with Ollama
|
||||
- Must be a model you have pulled with Ollama
|
||||
@@ -32,3 +32,9 @@ Depending on which client you're using, set the appropriate environment variable
|
||||
- `OPENAI_API_KEY`: Your OpenAI API key
|
||||
- `OPENAI_CHAT_MODEL_ID`: The OpenAI model to use for chat clients (e.g., `gpt-4o`, `gpt-4o-mini`, `gpt-3.5-turbo`)
|
||||
- `OPENAI_RESPONSES_MODEL_ID`: The OpenAI model to use for responses clients (e.g., `gpt-4o`, `gpt-4o-mini`, `gpt-3.5-turbo`)
|
||||
|
||||
**For Ollama client:**
|
||||
- `OLLAMA_HOST`: Your Ollama server URL (defaults to `http://localhost:11434` if not set)
|
||||
- `OLLAMA_CHAT_MODEL_ID`: The Ollama model to use for chat (e.g., `llama3.2`, `llama2`, `codellama`)
|
||||
|
||||
> **Note**: For Ollama, ensure you have Ollama installed and running locally with at least one model downloaded. Visit [https://ollama.com/](https://ollama.com/) for installation instructions.
|
||||
@@ -54,6 +54,13 @@ async def main() -> None:
|
||||
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(12) # Empirically determined delay for Mem0 indexing
|
||||
|
||||
print("\nRequest within a new thread:")
|
||||
# Create a new thread for the agent.
|
||||
# The new thread has no context of the previous conversation.
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,14 +1,49 @@
|
||||
APPLICATIONINSIGHTS_CONNECTION_STRING="..."
|
||||
OTLP_ENDPOINT="http://localhost:4317/"
|
||||
# Observability Configuration
|
||||
# ===========================
|
||||
|
||||
# Standard OpenTelemetry environment variables
|
||||
# See https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
|
||||
|
||||
# OTLP Endpoint (for Aspire Dashboard, Jaeger, etc.)
|
||||
# Default protocol is gRPC (port 4317), HTTP uses port 4318
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
|
||||
# Optional: Override endpoint for specific signals
|
||||
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://localhost:4317"
|
||||
# OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://localhost:4317"
|
||||
# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://localhost:4317"
|
||||
|
||||
# Optional: Specify protocol (grpc or http)
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
|
||||
|
||||
# Optional: Add headers (e.g., for authentication)
|
||||
# OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token,x-api-key=key"
|
||||
|
||||
# Optional: Service identification
|
||||
# OTEL_SERVICE_NAME="my-agent-app"
|
||||
# OTEL_SERVICE_VERSION="1.0.0"
|
||||
# OTEL_RESOURCE_ATTRIBUTES="deployment.environment=dev,host.name=localhost"
|
||||
|
||||
# Agent Framework specific settings
|
||||
# ==================================
|
||||
|
||||
# Enable sensitive data logging (prompts, responses, etc.)
|
||||
# WARNING: Only enable in dev/test environments
|
||||
ENABLE_SENSITIVE_DATA=true
|
||||
# This is not required if you run `setup_observability()` in your code
|
||||
ENABLE_OTEL=true
|
||||
|
||||
# Optional: Enable console exporters for debugging
|
||||
# ENABLE_CONSOLE_EXPORTERS=true
|
||||
|
||||
# Optional: Enable observability (automatically enabled if env vars are set or configure_otel_providers() is called)
|
||||
# ENABLE_INSTRUMENTATION=true
|
||||
|
||||
# OpenAI specific variables
|
||||
# ==========================
|
||||
OPENAI_API_KEY="..."
|
||||
OPENAI_RESPONSES_MODEL_ID="gpt-4o-2024-08-06"
|
||||
OPENAI_CHAT_MODEL_ID="gpt-4o-2024-08-06"
|
||||
|
||||
# Foundry specific variables
|
||||
# Azure AI Foundry specific variables
|
||||
# ====================================
|
||||
AZURE_AI_PROJECT_ENDPOINT="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
|
||||
|
||||
@@ -20,80 +20,162 @@ For more information, please refer to the following resources:
|
||||
|
||||
The Agent Framework Python SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of agent/model invocation and tool execution. This allows you to effectively monitor your AI application's performance and accurately track token consumption. It does so based on the Semantic Conventions for GenAI defined by OpenTelemetry, and the workflows emit their own spans to provide end-to-end visibility.
|
||||
|
||||
Next to what happens in the code when you run, we also make setting up observability as easy as possible. By calling a single function `configure_otel_providers()` from the `agent_framework.observability` module, you can enable telemetry for traces, logs, and metrics. The function automatically reads standard OpenTelemetry environment variables to configure exporters and providers, making it simple to get started.
|
||||
|
||||
### Five patterns for configuring observability
|
||||
|
||||
We've identified multiple ways to configure observability in your application, depending on your needs:
|
||||
|
||||
**1. Standard otel environment variables, configured for you**
|
||||
|
||||
The simplest approach - configure everything via environment variables:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
|
||||
# Reads OTEL_EXPORTER_OTLP_* environment variables automatically
|
||||
configure_otel_providers()
|
||||
```
|
||||
Or if you just want console exporters:
|
||||
```python
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
# Enable console exporters via environment variable
|
||||
|
||||
configure_otel_providers(enable_console_exporters=True)
|
||||
```
|
||||
This is the **recommended approach** for getting started.
|
||||
|
||||
**2. Custom Exporters**
|
||||
One level more control over the exporters that are created is to do that yourself, and then pass them to `configure_otel_providers()`. We will still create the providers for you, but you can customize the exporters as needed:
|
||||
|
||||
```python
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
|
||||
# Create custom exporters with specific configuration
|
||||
exporters = [
|
||||
OTLPSpanExporter(endpoint="http://localhost:4317", compression=Compression.Gzip),
|
||||
OTLPLogExporter(endpoint="http://localhost:4317"),
|
||||
OTLPMetricExporter(endpoint="http://localhost:4317"),
|
||||
]
|
||||
|
||||
# These will be added alongside any exporters from environment variables
|
||||
configure_otel_providers(exporters=exporters, enable_sensitive_data=True)
|
||||
```
|
||||
|
||||
**3. Third party setup**
|
||||
|
||||
A lot of third party specific otel package, have their own easy setup methods, for example Azure Monitor has `configure_azure_monitor()`. You can use those methods to setup the third party first, and then call `enable_instrumentation()` from the `agent_framework.observability` module to activate the Agent Framework telemetry code paths. In all these cases, if you already setup observability via environment variables, you don't need to call `enable_instrumentation()` as it will be enabled automatically.
|
||||
|
||||
```python
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor
|
||||
from agent_framework.observability import create_resource, enable_instrumentation
|
||||
|
||||
# Configure Azure Monitor first
|
||||
configure_azure_monitor(
|
||||
connection_string="InstrumentationKey=...",
|
||||
resource=create_resource(), # Uses OTEL_SERVICE_NAME, etc.
|
||||
enable_live_metrics=True,
|
||||
)
|
||||
|
||||
# Then activate Agent Framework's telemetry code paths
|
||||
# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars
|
||||
enable_instrumentation(enable_sensitive_data=False)
|
||||
```
|
||||
For Azure AI projects, use the `client.configure_azure_monitor()` method which wraps the calls to `configure_azure_monitor()` and `enable_instrumentation()`:
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
|
||||
async with (
|
||||
AIProjectClient(...) as project_client,
|
||||
AzureAIClient(project_client=project_client) as client,
|
||||
):
|
||||
# Automatically configures Azure Monitor with connection string from project
|
||||
await client.configure_azure_monitor(enable_live_metrics=True)
|
||||
```
|
||||
|
||||
Or with [Langfuse](https://langfuse.com/integrations/frameworks/microsoft-agent-framework):
|
||||
|
||||
```python
|
||||
# environment should be setup correctly, with langfuse urls and keys
|
||||
from agent_framework.observability import enable_instrumentation
|
||||
from langfuse import get_client
|
||||
|
||||
langfuse = get_client()
|
||||
|
||||
# Verify connection
|
||||
if langfuse.auth_check():
|
||||
print("Langfuse client is authenticated and ready!")
|
||||
else:
|
||||
print("Authentication failed. Please check your credentials and host.")
|
||||
|
||||
# Then activate Agent Framework's telemetry code paths
|
||||
# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars
|
||||
enable_instrumentation(enable_sensitive_data=False)
|
||||
```
|
||||
|
||||
**4. Manual setup**
|
||||
Of course you can also do a complete manual setup of exporters, providers, and instrumentation. Please refer to sample [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a comprehensive example of how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console. This gives you full control over which exporters and providers to use. We do have a helper function `create_resource()` in the `agent_framework.observability` module that you can use to create a resource with the appropriate service name and version based on environment variables or standard defaults for Agent Framework, this is not used in the sample.
|
||||
|
||||
**5. Auto-instrumentation (zero-code)**
|
||||
You can also use the [OpenTelemetry CLI tool](https://opentelemetry.io/docs/instrumentation/python/getting-started/#automatic-instrumentation) to automatically instrument your application without changing any code. Please refer to sample [advanced_zero_code.py](./advanced_zero_code.py) for an example of how to use the CLI tool to enable instrumentation for Agent Framework applications.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required resources
|
||||
|
||||
1. OpenAI or [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal)
|
||||
2. An [Azure AI project](https://ai.azure.com/doc/azure/ai-foundry/what-is-azure-ai-foundry)
|
||||
|
||||
### Optional resources
|
||||
|
||||
The following resources are needed if you want to send telemetry data to them:
|
||||
|
||||
1. [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-workspace-resource)
|
||||
2. [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone-for-python?tabs=flask%2Cwindows#start-the-aspire-dashboard)
|
||||
|
||||
### Dependencies
|
||||
|
||||
No additional dependencies are required to enable telemetry. The necessary packages are included as part of the `agent-framework` package. Unless you want to use a different APM vendor, in which case you will need to install the appropriate OpenTelemetry exporter package.
|
||||
As part of Agent Framework we use the following OpenTelemetry packages:
|
||||
- `opentelemetry-api`
|
||||
- `opentelemetry-sdk`
|
||||
- `opentelemetry-semantic-conventions-ai`
|
||||
|
||||
We do not install exporters by default, so you will need to add those yourself, this prevents us from installing unnecessary dependencies. For Application Insights, you will need to install `azure-monitor-opentelemetry`. For Aspire Dashboard or other OTLP compatible backends, you will need to install `opentelemetry-exporter-otlp-proto-grpc`. For HTTP protocol support, you will also need to install `opentelemetry-exporter-otlp-proto-http`.
|
||||
|
||||
And for many others, different packages are used, so refer to the documentation of the specific exporter you want to use.
|
||||
|
||||
### Environment variables
|
||||
|
||||
The following environment variables are used to turn on/off observability of the Agent Framework:
|
||||
|
||||
- ENABLE_OTEL=true
|
||||
- ENABLE_SENSITIVE_DATA=true
|
||||
- `ENABLE_INSTRUMENTATION`
|
||||
- `ENABLE_SENSITIVE_DATA`
|
||||
- `ENABLE_CONSOLE_EXPORTERS`
|
||||
|
||||
The framework will emit observability data when one of the above environment variables is set to true.
|
||||
All of these are booleans and default to `false`.
|
||||
|
||||
Finally we have `VS_CODE_EXTENSION_PORT` which you can set to a port, which can be used to setup the AI Toolkit for VS Code tracing integration. See [here](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) for more details.
|
||||
|
||||
The framework will emit observability data when the `ENABLE_INSTRUMENTATION` environment variable is set to `true`. If both are `true` then it will also emit sensitive information. When these are not set, or set to false, you can use the `enable_instrumentation()` function from the `agent_framework.observability` module to turn on instrumentation programmatically. This is useful when you want to control this via code instead of environment variables.
|
||||
|
||||
> **Note**: Sensitive information includes prompts, responses, and more, and should only be enabled in a development or test environment. It is not recommended to enable this in production environments as it may expose sensitive data.
|
||||
|
||||
### Configuring exporters and providers
|
||||
The two other variables, `ENABLE_CONSOLE_EXPORTERS` and `VS_CODE_EXTENSION_PORT`, are used to configure where the observability data is sent. Those are only activated when calling `configure_otel_providers()`.
|
||||
|
||||
Turning on observability is just the first step, you also need to configure where to send the observability data (i.e. Console, Application Insights). By default, no exporters or providers are configured.
|
||||
#### Environment variables for `configure_otel_providers()`
|
||||
|
||||
#### Setting up exporters and providers manually
|
||||
The `configure_otel_providers()` function automatically reads **standard OpenTelemetry environment variables** to configure exporters:
|
||||
|
||||
Please refer to sample [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a comprehensive example of how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console.
|
||||
**OTLP Configuration** (for Aspire Dashboard, Jaeger, etc.):
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT` - Base endpoint for all signals (e.g., `http://localhost:4317`)
|
||||
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - Traces-specific endpoint (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` - Metrics-specific endpoint (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - Logs-specific endpoint (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_PROTOCOL` - Protocol to use (`grpc` or `http`, default: `grpc`)
|
||||
- `OTEL_EXPORTER_OTLP_HEADERS` - Headers for all signals (e.g., `key1=value1,key2=value2`)
|
||||
- `OTEL_EXPORTER_OTLP_TRACES_HEADERS` - Traces-specific headers (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_METRICS_HEADERS` - Metrics-specific headers (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_LOGS_HEADERS` - Logs-specific headers (overrides base)
|
||||
|
||||
#### Setting up exporters and providers using `setup_observability()`
|
||||
**Service Identification**:
|
||||
- `OTEL_SERVICE_NAME` - Service name (default: `agent_framework`)
|
||||
- `OTEL_SERVICE_VERSION` - Service version (default: package version)
|
||||
- `OTEL_RESOURCE_ATTRIBUTES` - Additional resource attributes (e.g., `key1=value1,key2=value2`)
|
||||
|
||||
To make it easier for developers to get started, the `agent_framework.observability` module provides a `setup_observability()` function that will setup exporters and providers for traces, logs, and metrics based on environment variables. You can call this function at the start of your application to enable telemetry.
|
||||
|
||||
```python
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
setup_observability()
|
||||
```
|
||||
|
||||
Agent Framework also has an opinionated logging format, which you can setup using:
|
||||
```python
|
||||
from agent_framework import setup_logging
|
||||
|
||||
setup_logging()
|
||||
```
|
||||
|
||||
#### Environment variables for `setup_observability()`
|
||||
|
||||
The `setup_observability()` function will look for the following environment variables to determine how to setup the exporters and providers:
|
||||
|
||||
- OTLP_ENDPOINT="..."
|
||||
- APPLICATIONINSIGHTS_CONNECTION_STRING="..."
|
||||
|
||||
By providing the above environment variables, the `setup_observability()` function will automatically configure the appropriate exporters and providers for you. If no environment variables are provided, the function will not setup any exporters or providers.
|
||||
|
||||
You can also pass in a list of exporters directly to the `setup_observability()` function if you want to customize the exporters or add additional ones besides the ones configured via environment variables.
|
||||
|
||||
```python
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
exporter = OTLPSpanExporter(endpoint="another-otlp-endpoint")
|
||||
setup_observability(exporters=[exporter])
|
||||
```
|
||||
|
||||
> Using this method implicitly enables telemetry, so you do not need to set the `ENABLE_OTEL` environment variable. You can still set `ENABLE_SENSITIVE_DATA` to control whether sensitive data is included in the telemetry, or call the `setup_observability()` function with the `enable_sensitive_data` parameter set to `True`.
|
||||
> **Note**: These are standard OpenTelemetry environment variables. See the [OpenTelemetry spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for more details.
|
||||
|
||||
#### Logging
|
||||
Agent Framework has a built-in logging configuration that works well with telemetry. It sets the format to a standard format that includes timestamp, pathname, line number, and log level. You can use that by calling the `setup_logging()` function from the `agent_framework` module.
|
||||
@@ -119,65 +201,27 @@ This folder contains different samples demonstrating how to use telemetry in var
|
||||
|
||||
| Sample | Description |
|
||||
|--------|-------------|
|
||||
| [setup_observability_with_parameters.py](./setup_observability_with_parameters.py) | A simple example showing how to setup telemetry by passing in parameters to the `setup_observability()` function. This sample also uses the `setup_logging()` function to configure logging. |
|
||||
| [setup_observability_with_env_var.py](./setup_observability_with_env_var.py) | A simple example showing how to setup telemetry with the `setup_observability()` function using environment variables. |
|
||||
| [agent_observability.py](./agent_observability.py) | A simple example showing how to setup telemetry for an agentic application. |
|
||||
| [azure_ai_agent_observability.py](./azure_ai_agent_observability.py) | A simple example showing how to setup telemetry for an agentic application with an Azure AI project. |
|
||||
| [azure_ai_chat_client_with_observability.py](./azure_ai_chat_client_with_observability.py) | A simple example showing how to setup telemetry for a chat client with an Azure AI project. |
|
||||
| [workflow_observability.py](./workflow_observability.py) | A simple example showing how to setup telemetry for a workflow. |
|
||||
| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | A comprehensive example showing how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console. |
|
||||
| [advanced_zero_code.py](./advanced_zero_code.py) | A comprehensive example showing how to setup telemetry using the `opentelemetry-instrument` lib without modifying any code. |
|
||||
| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | **Recommended starting point**: Shows how to create custom exporters with specific configuration and pass them to `configure_otel_providers()`. Useful for advanced scenarios. |
|
||||
| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | Shows how to setup telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). |
|
||||
| [agent_observability.py](./agent_observability.py) | Shows telemetry collection for an agentic application with tool calls using environment variables. |
|
||||
| [agent_with_foundry_tracing.py](./agent_with_foundry_tracing.py) | Shows Azure Monitor integration with Foundry for any chat client. |
|
||||
| [azure_ai_agent_observability.py](./azure_ai_agent_observability.py) | Shows Azure Monitor integration for a AzureAIClient. |
|
||||
| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: Shows manual setup of exporters and providers with console output. Useful for understanding how observability works under the hood. |
|
||||
| [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: Shows zero-code telemetry setup using the `opentelemetry-enable_instrumentation` CLI tool. |
|
||||
| [workflow_observability.py](./workflow_observability.py) | Shows telemetry collection for a workflow with multiple executors and message passing. |
|
||||
|
||||
### Running the samples
|
||||
|
||||
1. Open a terminal and navigate to this folder: `python/samples/getting_started/observability/`. This is necessary for the `.env` file to be read correctly.
|
||||
2. Create a `.env` file if one doesn't already exist in this folder. Please refer to the [example file](./.env.example).
|
||||
> Note that `APPLICATIONINSIGHTS_CONNECTION_STRING` and `OTLP_ENDPOINT` are optional. If you don't configure them, everything will get outputted to the console.
|
||||
3. Activate your python virtual environment, and then run `python setup_observability_with_env_vars.py` or others.
|
||||
> **Note**: You can start with just `ENABLE_INSTRUMENTATION=true` and add `OTEL_EXPORTER_OTLP_ENDPOINT` or other configuration as needed. If no exporters are configured, you can set `ENABLE_CONSOLE_EXPORTERS=true` for console output.
|
||||
3. Activate your python virtual environment, and then run `python configure_otel_providers_with_env_var.py` or others.
|
||||
|
||||
> This will also print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard.
|
||||
> Each sample will print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard.
|
||||
|
||||
## Application Insights/Azure Monitor
|
||||
# Appendix
|
||||
|
||||
### Authentication
|
||||
|
||||
You can connect to your Application Insights instance using a connection string. You can also authenticate using Entra ID by passing a [TokenCredential](https://learn.microsoft.com/en-us/python/api/azure-core/azure.core.credentials.tokencredential?view=azure-python) to the `setup_observability()` function used in the samples above.
|
||||
|
||||
```python
|
||||
from azure.identity import DefaultAzureCredential
|
||||
|
||||
# The credential will be for resources specified in the environment variables and the parameters passed in.
|
||||
setup_observability(..., credential=DefaultAzureCredential())
|
||||
```
|
||||
|
||||
It is recommended to use [DefaultAzureCredential](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential?view=azure-python) for local development and [ManagedIdentityCredential](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.managedidentitycredential?view=azure-python) for production environments.
|
||||
|
||||
### Logs and traces
|
||||
|
||||
Go to your Application Insights instance, click on _Transaction search_ on the left menu. Use the operation id printed by the program to search for the logs and traces associated with the operation. Click on any of the search result to view the end-to-end transaction details. Read more [here](https://learn.microsoft.com/en-us/azure/azure-monitor/app/transaction-search-and-diagnostics?tabs=transaction-search).
|
||||
|
||||
### Metrics
|
||||
|
||||
Running the application once will only generate one set of measurements (for each metrics). Run the application a couple times to generate more sets of measurements.
|
||||
|
||||
> Note: Make sure not to run the program too frequently. Otherwise, you may get throttled.
|
||||
|
||||
Please refer to here on how to analyze metrics in [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/analyze-metrics).
|
||||
|
||||
### Adding exporters
|
||||
|
||||
You can also create exporters directly and have those added to the tracer_providers, logger_providers and metrics_providers, this is useful if you want to add a different exporter on the fly, or if you want to customize the exporter. Here is an example of how to create an OTLP exporter and add it to the observability setup:
|
||||
|
||||
```python
|
||||
from grpc import Compression
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
exporter = OTLPSpanExporter(endpoint="your-otlp-endpoint", compression=Compression.Gzip)
|
||||
setup_observability(exporters=[exporter])
|
||||
```
|
||||
|
||||
### Logs
|
||||
## Azure Monitor Queries
|
||||
|
||||
When you are in Azure Monitor and want to have a overall view of the span, use this query in the logs section:
|
||||
|
||||
@@ -212,6 +256,118 @@ Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-agent>
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
|
||||

|
||||
|
||||
## Migration Guide
|
||||
|
||||
We've done a major update to the observability API in Agent Framework Python SDK. The new API simplifies configuration by relying more on standard OpenTelemetry environment variables and have split the instrumentation from the configuration.
|
||||
|
||||
If you're updating from a previous version of the Agent Framework, here are the key changes to the observability API:
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Old Variable | New Variable | Notes |
|
||||
|-------------|--------------|-------|
|
||||
| `OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Standard OpenTelemetry env var |
|
||||
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | N/A | Use `configure_azure_monitor()` |
|
||||
| N/A | `ENABLE_CONSOLE_EXPORTERS` | New opt-in flag for console output |
|
||||
|
||||
### OTLP Configuration
|
||||
|
||||
**Before (Deprecated):**
|
||||
```python
|
||||
from agent_framework.observability import setup_observability
|
||||
# Via parameter
|
||||
setup_observability(otlp_endpoint="http://localhost:4317")
|
||||
|
||||
# Via environment variable
|
||||
# OTLP_ENDPOINT=http://localhost:4317
|
||||
setup_observability()
|
||||
```
|
||||
|
||||
**After (Current):**
|
||||
```python
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
# Via standard OTEL environment variable (recommended)
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
configure_otel_providers()
|
||||
|
||||
# Or via custom exporters
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
||||
|
||||
configure_otel_providers(exporters=[
|
||||
OTLPSpanExporter(endpoint="http://localhost:4317"),
|
||||
OTLPLogExporter(endpoint="http://localhost:4317"),
|
||||
OTLPMetricExporter(endpoint="http://localhost:4317"),
|
||||
])
|
||||
```
|
||||
|
||||
### Azure Monitor Configuration
|
||||
|
||||
**Before (Deprecated):**
|
||||
```python
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
setup_observability(
|
||||
applicationinsights_connection_string="InstrumentationKey=...",
|
||||
applicationinsights_live_metrics=True,
|
||||
)
|
||||
```
|
||||
|
||||
**After (Current):**
|
||||
```python
|
||||
# For Azure AI projects
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
|
||||
async with (
|
||||
AIProjectClient(...) as project_client,
|
||||
AzureAIClient(project_client=project_client) as client,
|
||||
):
|
||||
await client.configure_azure_monitor(enable_live_metrics=True)
|
||||
|
||||
# For non-Azure AI projects
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor
|
||||
from agent_framework.observability import create_resource, enable_instrumentation
|
||||
|
||||
configure_azure_monitor(
|
||||
connection_string="InstrumentationKey=...",
|
||||
resource=create_resource(),
|
||||
enable_live_metrics=True,
|
||||
)
|
||||
enable_instrumentation()
|
||||
```
|
||||
|
||||
### Console Output
|
||||
|
||||
**Before (Deprecated):**
|
||||
```python
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
# Console was used as automatic fallback
|
||||
setup_observability() # Would output to console if no exporters configured
|
||||
```
|
||||
|
||||
**After (Current):**
|
||||
```python
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
|
||||
# Console exporters are now opt-in
|
||||
# ENABLE_CONSOLE_EXPORTERS=true
|
||||
configure_otel_providers()
|
||||
|
||||
# Or programmatically
|
||||
configure_otel_providers(enable_console_exporters=True)
|
||||
```
|
||||
|
||||
### Benefits of New API
|
||||
|
||||
1. **Standards Compliant**: Uses standard OpenTelemetry environment variables
|
||||
2. **Simpler**: Less configuration needed, more relies on environment
|
||||
3. **Flexible**: Easy to add custom exporters alongside environment-based ones
|
||||
4. **Cleaner Separation**: Azure Monitor setup is in Azure-specific client
|
||||
5. **Better Compatibility**: Works with any OTEL-compatible tool (Jaeger, Zipkin, Prometheus, etc.)
|
||||
|
||||
## Aspire Dashboard
|
||||
|
||||
The [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) is a local telemetry viewing tool that provides an excellent experience for viewing OpenTelemetry data without requiring Azure setup.
|
||||
@@ -239,13 +395,13 @@ This will start the dashboard with:
|
||||
Make sure your `.env` file includes the OTLP endpoint:
|
||||
|
||||
```bash
|
||||
OTLP_ENDPOINT=http://localhost:4317
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
```
|
||||
|
||||
Or set it as an environment variable when running your samples:
|
||||
|
||||
```bash
|
||||
ENABLE_OTEL=true OTLP_ENDPOINT=http://localhost:4317 python 01-zero_code.py
|
||||
ENABLE_INSTRUMENTATION=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py
|
||||
```
|
||||
|
||||
### Viewing telemetry data
|
||||
@@ -253,9 +409,3 @@ ENABLE_OTEL=true OTLP_ENDPOINT=http://localhost:4317 python 01-zero_code.py
|
||||
> Make sure you have the dashboard running to receive telemetry data.
|
||||
|
||||
Once your sample finishes running, navigate to <http://localhost:18888> in a web browser to see the telemetry data. Follow the [Aspire Dashboard exploration guide](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/explore) to authenticate to the dashboard and start exploring your traces, logs, and metrics!
|
||||
|
||||
## Console output
|
||||
|
||||
You won't have to deploy an Application Insights resource or install Docker to run Aspire Dashboard if you choose to inspect telemetry data in a console. However, it is difficult to navigate through all the spans and logs produced, so **this method is only recommended when you are just getting started**.
|
||||
|
||||
Use the guides from OpenTelemetry to setup exporters for [the console](https://opentelemetry.io/docs/languages/python/getting-started/), or use [advanced_manual_setup_console_output](./advanced_manual_setup_console_output.py) as a reference, just know that there are a lot of options you can setup and this is not a comprehensive example.
|
||||
|
||||
+3
-1
@@ -5,6 +5,7 @@ import logging
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework.observability import enable_instrumentation
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
from opentelemetry.metrics import set_meter_provider
|
||||
@@ -21,7 +22,7 @@ from pydantic import Field
|
||||
|
||||
"""
|
||||
This sample shows how to manually configure to send traces, logs, and metrics to the console,
|
||||
without using the `setup_observability` helper function.
|
||||
without using the `configure_otel_providers` helper function.
|
||||
"""
|
||||
|
||||
resource = Resource.create({SERVICE_NAME: "ManualSetup"})
|
||||
@@ -114,6 +115,7 @@ async def main():
|
||||
setup_logging()
|
||||
setup_tracing()
|
||||
setup_metrics()
|
||||
enable_instrumentation()
|
||||
|
||||
await run_chat_client()
|
||||
|
||||
|
||||
@@ -19,12 +19,23 @@ This sample shows how you can configure observability of an application with zer
|
||||
It relies on the OpenTelemetry auto-instrumentation capabilities, and the observability setup
|
||||
is done via environment variables.
|
||||
|
||||
This sample requires the `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable to be set.
|
||||
Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool.
|
||||
|
||||
Run the sample with the following command:
|
||||
```
|
||||
uv run --env-file=.env opentelemetry-instrument python advanced_zero_code.py
|
||||
And setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update the endpoint below).
|
||||
|
||||
Then you can run:
|
||||
```bash
|
||||
opentelemetry-enable_instrumentation \
|
||||
--traces_exporter otlp \
|
||||
--metrics_exporter otlp \
|
||||
--service_name agent_framework \
|
||||
--exporter_otlp_endpoint http://localhost:4317 \
|
||||
python samples/getting_started/observability/advanced_zero_code.py
|
||||
```
|
||||
(or use uv run in front when you have did the install within your uv virtual environment)
|
||||
|
||||
You can also set the environment variables instead of passing them as CLI arguments.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.observability import get_tracer, setup_observability
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
@@ -27,9 +27,10 @@ async def get_weather(
|
||||
|
||||
|
||||
async def main():
|
||||
# This will enable tracing and create the necessary tracing, logging and metrics providers
|
||||
# based on environment variables. See the .env.example file for the available configuration options.
|
||||
setup_observability()
|
||||
# calling `configure_otel_providers` will *enable* tracing and create the necessary tracing, logging
|
||||
# and metrics providers based on environment variables.
|
||||
# See the .env.example file for the available configuration options.
|
||||
configure_otel_providers()
|
||||
|
||||
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
|
||||
|
||||
@@ -41,10 +42,11 @@ async def main():
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
id="weather-agent",
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
for question in questions:
|
||||
print(f"User: {question}")
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.display_name}: ", end="")
|
||||
async for update in agent.run_stream(
|
||||
question,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
import dotenv
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.observability import create_resource, enable_instrumentation, get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
This sample shows you can can setup telemetry in Microsoft Foundry for a custom agent.
|
||||
First ensure you have a Foundry workspace with Application Insights enabled.
|
||||
And use the Operate tab to Register an Agent.
|
||||
Set the OpenTelemetry agent ID to the value used below in the ChatAgent creation: `weather-agent` (or change both).
|
||||
The sample uses the Azure Monitor OpenTelemetry exporter to send traces to Application Insights.
|
||||
So ensure you have the `azure-monitor-opentelemetry` package installed.
|
||||
"""
|
||||
|
||||
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
|
||||
dotenv.load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main():
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
):
|
||||
# This will enable tracing and configure the application to send telemetry data to the
|
||||
# Application Insights instance attached to the Azure AI project.
|
||||
# This will override any existing configuration.
|
||||
try:
|
||||
conn_string = await project_client.telemetry.get_application_insights_connection_string()
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"No Application Insights connection string found for the Azure AI Project. "
|
||||
"Please ensure Application Insights is configured in your Azure AI project, "
|
||||
"or call configure_otel_providers() manually with custom exporters."
|
||||
)
|
||||
return
|
||||
configure_azure_monitor(
|
||||
connection_string=conn_string,
|
||||
enable_live_metrics=True,
|
||||
resource=create_resource(),
|
||||
enable_performance_counters=False,
|
||||
)
|
||||
# This call is not necessary if you have the environment variable ENABLE_INSTRUMENTATION=true set
|
||||
# If not or set to false, or if you want to enable or disable sensitive data collection, call this function.
|
||||
enable_instrumentation(enable_sensitive_data=True)
|
||||
print("Observability is set up. Starting Weather Agent...")
|
||||
|
||||
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
|
||||
|
||||
with get_tracer().start_as_current_span("Weather Agent Chat", kind=SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
agent = ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
id="weather-agent",
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
for question in questions:
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.display_name}: ", end="")
|
||||
async for update in agent.run_stream(
|
||||
question,
|
||||
thread=thread,
|
||||
):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -7,11 +7,9 @@ from typing import Annotated
|
||||
|
||||
import dotenv
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from agent_framework.observability import get_tracer
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
@@ -40,36 +38,16 @@ async def get_weather(
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def setup_azure_ai_observability(
|
||||
project_client: AIProjectClient, enable_sensitive_data: bool | None = None
|
||||
) -> None:
|
||||
"""Use this method to setup tracing in your Azure AI Project.
|
||||
|
||||
This will take the connection string from the AIProjectClient.
|
||||
It will override any connection string that is set in the environment variables.
|
||||
It will disable any OTLP endpoint that might have been set.
|
||||
"""
|
||||
try:
|
||||
conn_string = await project_client.telemetry.get_application_insights_connection_string()
|
||||
except ResourceNotFoundError:
|
||||
print("No Application Insights connection string found for the Azure AI Project.")
|
||||
return
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data)
|
||||
|
||||
|
||||
async def main():
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
AzureAIAgentClient(agents_client=agents_client) as client,
|
||||
AzureAIClient(project_client=project_client) as client,
|
||||
):
|
||||
# This will enable tracing and configure the application to send telemetry data to the
|
||||
# Application Insights instance attached to the Azure AI project.
|
||||
# This will override any existing configuration.
|
||||
await setup_azure_ai_observability(project_client)
|
||||
await client.configure_azure_monitor(enable_live_metrics=True)
|
||||
|
||||
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
|
||||
|
||||
@@ -81,10 +59,11 @@ async def main():
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
id="edvan-weather-agent",
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
for question in questions:
|
||||
print(f"User: {question}")
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.display_name}: ", end="")
|
||||
async for update in agent.run_stream(
|
||||
question,
|
||||
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
import dotenv
|
||||
from agent_framework import HostedCodeInterpreterTool
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework.observability import get_tracer
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
This sample, shows you can leverage the built-in telemetry in Azure AI.
|
||||
It uses the Azure AI client to setup the telemetry, this calls out to
|
||||
Azure AI for the connection string of the attached Application Insights
|
||||
instance.
|
||||
|
||||
You must add an Application Insights instance to your Azure AI project
|
||||
for this sample to work.
|
||||
"""
|
||||
|
||||
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# ANSI color codes for printing in blue and resetting after each print
|
||||
BLUE = "\x1b[34m"
|
||||
RESET = "\x1b[0m"
|
||||
|
||||
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def setup_azure_ai_observability(
|
||||
project_client: AIProjectClient, enable_sensitive_data: bool | None = None
|
||||
) -> None:
|
||||
"""Use this method to setup tracing in your Azure AI Project.
|
||||
|
||||
This will take the connection string from the AIProjectClient instance.
|
||||
It will override any connection string that is set in the environment variables.
|
||||
It will disable any OTLP endpoint that might have been set.
|
||||
"""
|
||||
try:
|
||||
conn_string = await project_client.telemetry.get_application_insights_connection_string()
|
||||
except ResourceNotFoundError:
|
||||
print("No Application Insights connection string found for the Azure AI Project.")
|
||||
return
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run an AI service.
|
||||
|
||||
This function runs an AI service and prints the output.
|
||||
Telemetry will be collected for the service execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI service execution.
|
||||
|
||||
In azure_ai you will also see specific operations happening that are called by the Azure AI implementation,
|
||||
such as `create_agent`.
|
||||
"""
|
||||
questions = [
|
||||
"What's the weather in Amsterdam and in Paris?",
|
||||
"Why is the sky blue?",
|
||||
"Tell me about AI.",
|
||||
"Can you write a python function that adds two numbers? and use it to add 8483 and 5692?",
|
||||
]
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
AzureAIAgentClient(agents_client=agents_client) as client,
|
||||
):
|
||||
# This will enable tracing and configure the application to send telemetry data to the
|
||||
# Application Insights instance attached to the Azure AI project.
|
||||
# This will override any existing configuration.
|
||||
await setup_azure_ai_observability(project_client)
|
||||
|
||||
with get_tracer().start_as_current_span(
|
||||
name="Foundry Telemetry from Agent Framework", kind=SpanKind.CLIENT
|
||||
) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
for question in questions:
|
||||
print(f"{BLUE}User: {question}{RESET}")
|
||||
print(f"{BLUE}Assistant: {RESET}", end="")
|
||||
async for chunk in client.get_streaming_response(
|
||||
question, tools=[get_weather, HostedCodeInterpreterTool()]
|
||||
):
|
||||
if str(chunk):
|
||||
print(f"{BLUE}{str(chunk)}{RESET}", end="")
|
||||
print(f"{BLUE}{RESET}")
|
||||
|
||||
print(f"{BLUE}Done{RESET}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+3
-3
@@ -7,7 +7,7 @@ from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from agent_framework import ai_function
|
||||
from agent_framework.observability import get_tracer, setup_observability
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
||||
|
||||
"""
|
||||
This sample, show how you can configure observability of an application via the
|
||||
`setup_observability` function with environment variables.
|
||||
`configure_otel_providers` function with environment variables.
|
||||
|
||||
When you run this sample with an OTLP endpoint or an Application Insights connection string,
|
||||
you should see traces, logs, and metrics in the configured backend.
|
||||
@@ -100,7 +100,7 @@ async def main(scenario: Literal["chat_client", "chat_client_stream", "ai_functi
|
||||
|
||||
# This will enable tracing and create the necessary tracing, logging and metrics providers
|
||||
# based on environment variables. See the .env.example file for the available configuration options.
|
||||
setup_observability()
|
||||
configure_otel_providers()
|
||||
|
||||
with get_tracer().start_as_current_span("Sample Scenario's", kind=trace.SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
+35
-14
@@ -7,7 +7,7 @@ from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from agent_framework import ai_function, setup_logging
|
||||
from agent_framework.observability import get_tracer, setup_observability
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
@@ -17,14 +17,14 @@ if TYPE_CHECKING:
|
||||
from agent_framework import ChatClientProtocol
|
||||
|
||||
"""
|
||||
This sample, show how you can configure observability of an application via the
|
||||
`setup_observability` function and inline parameters.
|
||||
This sample shows how you can configure observability with custom exporters passed directly
|
||||
to the `configure_otel_providers()` function.
|
||||
|
||||
When you run this sample with an OTLP endpoint or an Application Insights connection string,
|
||||
you should see traces, logs, and metrics in the configured backend.
|
||||
This approach gives you full control over exporter configuration (endpoints, headers, compression, etc.)
|
||||
and allows you to add multiple exporters programmatically.
|
||||
|
||||
If no OTLP endpoint or Application Insights connection string is configured, the sample will
|
||||
output traces, logs, and metrics to the console.
|
||||
For standard OTLP setup, it's recommended to use environment variables (see configure_otel_providers_with_env_var.py).
|
||||
Use this approach when you need custom exporter configuration beyond what environment variables provide.
|
||||
"""
|
||||
|
||||
# Define the scenarios that can be run to show the telemetry data collected by the SDK
|
||||
@@ -100,14 +100,35 @@ async def main(scenario: Literal["chat_client", "chat_client_stream", "ai_functi
|
||||
|
||||
# Setup the logging with the more complete format
|
||||
setup_logging()
|
||||
# This will enable tracing and create the necessary tracing, logging and metrics providers
|
||||
# based on the provided parameters.
|
||||
setup_observability(
|
||||
|
||||
# Create custom OTLP exporters with specific configuration
|
||||
# Note: You need to install opentelemetry-exporter-otlp-proto-grpc or -http separately
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
|
||||
# Create exporters with custom configuration
|
||||
# These will be added to any exporters configured via environment variables
|
||||
custom_exporters = [
|
||||
OTLPSpanExporter(endpoint="http://localhost:4317"),
|
||||
OTLPMetricExporter(endpoint="http://localhost:4317"),
|
||||
OTLPLogExporter(endpoint="http://localhost:4317"),
|
||||
]
|
||||
except ImportError:
|
||||
print(
|
||||
"Warning: opentelemetry-exporter-otlp-proto-grpc not installed. "
|
||||
"Install with: pip install opentelemetry-exporter-otlp-proto-grpc"
|
||||
)
|
||||
print("Continuing without custom exporters...\n")
|
||||
custom_exporters = []
|
||||
|
||||
# Setup observability with custom exporters and sensitive data enabled
|
||||
# The exporters parameter allows you to add custom exporters alongside
|
||||
# those configured via environment variables (OTEL_EXPORTER_OTLP_*)
|
||||
configure_otel_providers(
|
||||
enable_sensitive_data=True,
|
||||
# If you have set the `OTLP_ENDPOINT` environment variable and it'd different from the one below,
|
||||
# both endpoints will be used to create the OTLP exporter.
|
||||
# Same applies to the Application Insights connection string.
|
||||
otlp_endpoint=["http://localhost:4317/"],
|
||||
exporters=custom_exporters,
|
||||
)
|
||||
|
||||
with get_tracer().start_as_current_span("Sample Scenario's", kind=trace.SpanKind.CLIENT) as current_span:
|
||||
@@ -9,7 +9,7 @@ from agent_framework import (
|
||||
WorkflowOutputEvent,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.observability import get_tracer, setup_observability
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from typing_extensions import Never
|
||||
@@ -105,7 +105,7 @@ async def main():
|
||||
"""Run the telemetry sample with a simple sequential workflow."""
|
||||
# This will enable tracing and create the necessary tracing, logging and metrics providers
|
||||
# based on environment variables. See the .env.example file for the available configuration options.
|
||||
setup_observability()
|
||||
configure_otel_providers()
|
||||
|
||||
with get_tracer().start_as_current_span("Sequential Workflow Scenario", kind=SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
@@ -11,6 +11,8 @@ This folder contains examples demonstrating how to use AI functions (tools) with
|
||||
| [`ai_function_recover_from_failures.py`](ai_function_recover_from_failures.py) | Demonstrates graceful error handling when tools raise exceptions. Shows how agents receive error information and can recover from failures, deciding whether to retry or respond differently based on the exception. |
|
||||
| [`ai_function_with_approval.py`](ai_function_with_approval.py) | Shows how to implement user approval workflows for function calls without using threads. Demonstrates both streaming and non-streaming approval patterns where users can approve or reject function executions before they run. |
|
||||
| [`ai_function_with_approval_and_threads.py`](ai_function_with_approval_and_threads.py) | Demonstrates tool approval workflows using threads for automatic conversation history management. Shows how threads simplify approval workflows by automatically storing and retrieving conversation context. Includes both approval and rejection examples. |
|
||||
| [`ai_function_with_kwargs.py`](ai_function_with_kwargs.py) | Demonstrates how to inject custom arguments (context) into an AI function from the agent's run method. Useful for passing runtime information like access tokens or user IDs that the tool needs but the model shouldn't see. |
|
||||
| [`ai_function_with_thread_injection.py`](ai_function_with_thread_injection.py) | Shows how to access the current `thread` object inside an AI function via `**kwargs`. |
|
||||
| [`ai_function_with_max_exceptions.py`](ai_function_with_max_exceptions.py) | Shows how to limit the number of times a tool can fail with exceptions using `max_invocation_exceptions`. Useful for preventing expensive tools from being called repeatedly when they keep failing. |
|
||||
| [`ai_function_with_max_invocations.py`](ai_function_with_max_invocations.py) | Demonstrates limiting the total number of times a tool can be invoked using `max_invocations`. Useful for rate-limiting expensive operations or ensuring tools are only called a specific number of times per conversation. |
|
||||
| [`ai_functions_in_class.py`](ai_functions_in_class.py) | Shows how to use `ai_function` decorator with class methods to create stateful tools. Demonstrates how class state can control tool behavior dynamically, allowing you to adjust tool functionality at runtime by modifying class properties. |
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import ai_function
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
AI Function with kwargs Example
|
||||
|
||||
This example demonstrates how to inject custom keyword arguments (kwargs) into an AI function
|
||||
from the agent's run method, without exposing them to the AI model.
|
||||
|
||||
This is useful for passing runtime information like access tokens, user IDs, or
|
||||
request-specific context that the tool needs but the model shouldn't know about
|
||||
or provide.
|
||||
"""
|
||||
|
||||
|
||||
# Define the function tool with **kwargs to accept injected arguments
|
||||
@ai_function
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
# Extract the injected argument from kwargs
|
||||
user_id = kwargs.get("user_id", "unknown")
|
||||
|
||||
# Simulate using the user_id for logging or personalization
|
||||
print(f"Getting weather for user: {user_id}")
|
||||
|
||||
return f"The weather in {location} is cloudy with a high of 15°C."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = OpenAIResponsesClient().create_agent(
|
||||
name="WeatherAgent",
|
||||
instructions="You are a helpful weather assistant.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
# Pass the injected argument when running the agent
|
||||
# The 'user_id' kwarg will be passed down to the tool execution via **kwargs
|
||||
response = await agent.run("What is the weather like in Amsterdam?", user_id="user_123")
|
||||
|
||||
print(f"Agent: {response.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import AgentThread, ai_function
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
AI Function with Thread Injection Example
|
||||
|
||||
This example demonstrates the behavior when passing 'thread' to agent.run()
|
||||
and accessing that thread in AI function.
|
||||
"""
|
||||
|
||||
|
||||
# Define the function tool with **kwargs
|
||||
@ai_function
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
# Get thread object from kwargs
|
||||
thread = kwargs.get("thread")
|
||||
if thread and isinstance(thread, AgentThread):
|
||||
if thread.message_store:
|
||||
messages = await thread.message_store.list_messages()
|
||||
print(f"Thread contains {len(messages)} messages.")
|
||||
elif thread.service_thread_id:
|
||||
print(f"Thread ID: {thread.service_thread_id}.")
|
||||
|
||||
return f"The weather in {location} is cloudy."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = OpenAIChatClient().create_agent(
|
||||
name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=[get_weather]
|
||||
)
|
||||
|
||||
# Create a thread
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# Run the agent with the thread
|
||||
print(f"Agent: {await agent.run('What is the weather in London?', thread=thread)}")
|
||||
print(f"Agent: {await agent.run('What is the weather in Amsterdam?', thread=thread)}")
|
||||
print(f"Agent: {await agent.run('What cities did I ask about?', thread=thread)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -44,6 +44,7 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
| Magentic Workflow as Agent | [agents/magentic_workflow_as_agent.py](./agents/magentic_workflow_as_agent.py) | Configure Magentic orchestration with callbacks, then expose the workflow as an agent |
|
||||
| Workflow as Agent (Reflection Pattern) | [agents/workflow_as_agent_reflection_pattern.py](./agents/workflow_as_agent_reflection_pattern.py) | Wrap a workflow so it can behave like an agent (reflection pattern) |
|
||||
| Workflow as Agent + HITL | [agents/workflow_as_agent_human_in_the_loop.py](./agents/workflow_as_agent_human_in_the_loop.py) | Extend workflow-as-agent with human-in-the-loop capability |
|
||||
| Workflow as Agent with Thread | [agents/workflow_as_agent_with_thread.py](./agents/workflow_as_agent_with_thread.py) | Use AgentThread to maintain conversation history across workflow-as-agent invocations |
|
||||
| Handoff Workflow as Agent | [agents/handoff_workflow_as_agent.py](./agents/handoff_workflow_as_agent.py) | Use a HandoffBuilder workflow as an agent with HITL via FunctionCallContent/FunctionResultContent |
|
||||
|
||||
### checkpoint
|
||||
@@ -54,6 +55,7 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
| Checkpoint & HITL Resume | [checkpoint/checkpoint_with_human_in_the_loop.py](./checkpoint/checkpoint_with_human_in_the_loop.py) | Combine checkpointing with human approvals and resume pending HITL requests |
|
||||
| Checkpointed Sub-Workflow | [checkpoint/sub_workflow_checkpoint.py](./checkpoint/sub_workflow_checkpoint.py) | Save and resume a sub-workflow that pauses for human approval |
|
||||
| Handoff + Tool Approval Resume | [checkpoint/handoff_with_tool_approval_checkpoint_resume.py](./checkpoint/handoff_with_tool_approval_checkpoint_resume.py) | Handoff workflow that captures tool-call approvals in checkpoints and resumes with human decisions |
|
||||
| Workflow as Agent Checkpoint | [checkpoint/workflow_as_agent_checkpoint.py](./checkpoint/workflow_as_agent_checkpoint.py) | Enable checkpointing when using workflow.as_agent() with checkpoint_storage parameter |
|
||||
|
||||
### composition
|
||||
|
||||
@@ -84,7 +86,6 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
| ConcurrentBuilder Request Info | [human-in-the-loop/concurrent_request_info.py](./human-in-the-loop/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` on ConcurrentBuilder |
|
||||
| GroupChatBuilder Request Info | [human-in-the-loop/group_chat_request_info.py](./human-in-the-loop/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` on GroupChatBuilder |
|
||||
|
||||
|
||||
### tool-approval
|
||||
|
||||
Tool approval samples demonstrate using `@ai_function(approval_mode="always_require")` to gate sensitive tool executions with human approval. These work with the high-level builder APIs.
|
||||
@@ -110,6 +111,7 @@ For additional observability samples in Agent Framework, see the [observability
|
||||
| Concurrent Orchestration (Default Aggregator) | [orchestration/concurrent_agents.py](./orchestration/concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined ChatMessages |
|
||||
| Concurrent Orchestration (Custom Aggregator) | [orchestration/concurrent_custom_aggregator.py](./orchestration/concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM |
|
||||
| Concurrent Orchestration (Custom Agent Executors) | [orchestration/concurrent_custom_agent_executors.py](./orchestration/concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder |
|
||||
| Concurrent Orchestration (Participant Factory) | [orchestration/concurrent_participant_factory.py](./orchestration/concurrent_participant_factory.py) | Use participant factories for state isolation between workflow instances |
|
||||
| Group Chat with Agent Manager | [orchestration/group_chat_agent_manager.py](./orchestration/group_chat_agent_manager.py) | Agent-based manager using `set_manager()` to select next speaker |
|
||||
| Group Chat Philosophical Debate | [orchestration/group_chat_philosophical_debate.py](./orchestration/group_chat_philosophical_debate.py) | Agent manager moderates long-form, multi-round debate across diverse participants |
|
||||
| Group Chat with Simple Function Selector | [orchestration/group_chat_simple_selector.py](./orchestration/group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker |
|
||||
@@ -117,6 +119,7 @@ For additional observability samples in Agent Framework, see the [observability
|
||||
| Handoff (Specialist-to-Specialist) | [orchestration/handoff_specialist_to_specialist.py](./orchestration/handoff_specialist_to_specialist.py) | Multi-tier routing: specialists can hand off to other specialists using `.add_handoff()` fluent API |
|
||||
| Handoff (Return-to-Previous) | [orchestration/handoff_return_to_previous.py](./orchestration/handoff_return_to_previous.py) | Return-to-previous routing: after user input, routes back to the previous specialist instead of coordinator using `.enable_return_to_previous()` |
|
||||
| Handoff (Autonomous) | [orchestration/handoff_autonomous.py](./orchestration/handoff_autonomous.py) | Autonomous mode: specialists iterate independently until invoking a handoff tool using `.with_interaction_mode("autonomous", autonomous_turn_limit=N)` |
|
||||
| Handoff (Participant Factory) | [orchestration/handoff_participant_factory.py](./orchestration/handoff_participant_factory.py) | Use participant factories for state isolation between workflow instances |
|
||||
| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming |
|
||||
| Magentic + Human Plan Review | [orchestration/magentic_human_plan_update.py](./orchestration/magentic_human_plan_update.py) | Human reviews/updates the plan before execution |
|
||||
| Magentic + Human Stall Intervention | [orchestration/magentic_human_replan.py](./orchestration/magentic_human_replan.py) | Human intervenes when workflow stalls with `with_human_input_on_stall()` |
|
||||
@@ -146,6 +149,8 @@ to configure which agents can route to which others with a fluent, type-safe API
|
||||
| Sample | File | Concepts |
|
||||
|---|---|---|
|
||||
| Shared States | [state-management/shared_states_with_agents.py](./state-management/shared_states_with_agents.py) | Store in shared state once and later reuse across agents |
|
||||
| Workflow Kwargs (Custom Context) | [state-management/workflow_kwargs.py](./state-management/workflow_kwargs.py) | Pass custom context (data, user tokens) via kwargs to `@ai_function` tools |
|
||||
|
||||
|
||||
### visualization
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentThread, ChatAgent, ChatMessageStore, SequentialBuilder
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Thread Conversation History and Checkpointing
|
||||
|
||||
This sample demonstrates how to use AgentThread with a workflow wrapped as an agent
|
||||
to maintain conversation history across multiple invocations. When using as_agent(),
|
||||
the thread's message store history is included in each workflow run, enabling
|
||||
the workflow participants to reference prior conversation context.
|
||||
|
||||
It also demonstrates how to enable checkpointing for workflow execution state
|
||||
persistence, allowing workflows to be paused and resumed.
|
||||
|
||||
Key concepts:
|
||||
- Workflows can be wrapped as agents using workflow.as_agent()
|
||||
- AgentThread with ChatMessageStore preserves conversation history
|
||||
- Each call to agent.run() includes thread history + new message
|
||||
- Participants in the workflow see the full conversation context
|
||||
- checkpoint_storage parameter enables workflow state persistence
|
||||
|
||||
Use cases:
|
||||
- Multi-turn conversations with workflow-based orchestrations
|
||||
- Stateful workflows that need context from previous interactions
|
||||
- Building conversational agents that leverage workflow patterns
|
||||
- Long-running workflows that need pause/resume capability
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI environment variables configured for OpenAIChatClient
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create a chat client
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
# Define factory functions for workflow participants
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="assistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Answer questions based on the conversation "
|
||||
"history. If the user asks about something mentioned earlier, reference it."
|
||||
),
|
||||
)
|
||||
|
||||
def create_summarizer() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="summarizer",
|
||||
instructions=(
|
||||
"You are a summarizer. After the assistant responds, provide a brief "
|
||||
"one-sentence summary of the key point from the conversation so far."
|
||||
),
|
||||
)
|
||||
|
||||
# Build a sequential workflow: assistant -> summarizer
|
||||
workflow = SequentialBuilder().register_participants([create_assistant, create_summarizer]).build()
|
||||
|
||||
# Wrap the workflow as an agent
|
||||
agent = workflow.as_agent(name="ConversationalWorkflowAgent")
|
||||
|
||||
# Create a thread with a ChatMessageStore to maintain history
|
||||
message_store = ChatMessageStore()
|
||||
thread = AgentThread(message_store=message_store)
|
||||
|
||||
print("=" * 60)
|
||||
print("Workflow as Agent with Thread - Multi-turn Conversation")
|
||||
print("=" * 60)
|
||||
|
||||
# First turn: Introduce a topic
|
||||
query1 = "My name is Alex and I'm learning about machine learning."
|
||||
print(f"\n[Turn 1] User: {query1}")
|
||||
|
||||
response1 = await agent.run(query1, thread=thread)
|
||||
if response1.messages:
|
||||
for msg in response1.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Second turn: Reference the previous topic
|
||||
query2 = "What was my name again, and what am I learning about?"
|
||||
print(f"\n[Turn 2] User: {query2}")
|
||||
|
||||
response2 = await agent.run(query2, thread=thread)
|
||||
if response2.messages:
|
||||
for msg in response2.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Third turn: Ask a follow-up question
|
||||
query3 = "Can you suggest a good first project for me to try?"
|
||||
print(f"\n[Turn 3] User: {query3}")
|
||||
|
||||
response3 = await agent.run(query3, thread=thread)
|
||||
if response3.messages:
|
||||
for msg in response3.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Show the accumulated conversation history
|
||||
print("\n" + "=" * 60)
|
||||
print("Full Thread History")
|
||||
print("=" * 60)
|
||||
if thread.message_store:
|
||||
history = await thread.message_store.list_messages()
|
||||
for i, msg in enumerate(history, start=1):
|
||||
role = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
|
||||
speaker = msg.author_name or role
|
||||
text_preview = msg.text[:80] + "..." if len(msg.text) > 80 else msg.text
|
||||
print(f"{i:02d}. [{speaker}]: {text_preview}")
|
||||
|
||||
|
||||
async def demonstrate_thread_serialization() -> None:
|
||||
"""
|
||||
Demonstrates serializing and resuming a thread with a workflow agent.
|
||||
|
||||
This shows how conversation history can be persisted and restored,
|
||||
enabling long-running conversational workflows.
|
||||
"""
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="memory_assistant",
|
||||
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
|
||||
)
|
||||
|
||||
workflow = SequentialBuilder().register_participants([create_assistant]).build()
|
||||
agent = workflow.as_agent(name="MemoryWorkflowAgent")
|
||||
|
||||
# Create initial thread and have a conversation
|
||||
thread = AgentThread(message_store=ChatMessageStore())
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Thread Serialization Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# First interaction
|
||||
query = "Remember this: the secret code is ALPHA-7."
|
||||
print(f"\n[Session 1] User: {query}")
|
||||
response = await agent.run(query, thread=thread)
|
||||
if response.messages:
|
||||
print(f"[assistant]: {response.messages[0].text}")
|
||||
|
||||
# Serialize thread state (could be saved to database/file)
|
||||
serialized_state = await thread.serialize()
|
||||
print("\n[Serialized thread state for persistence]")
|
||||
|
||||
# Simulate a new session by creating a new thread from serialized state
|
||||
restored_thread = AgentThread(message_store=ChatMessageStore())
|
||||
await restored_thread.update_from_thread_state(serialized_state)
|
||||
|
||||
# Continue conversation with restored thread
|
||||
query = "What was the secret code I told you?"
|
||||
print(f"\n[Session 2 - Restored] User: {query}")
|
||||
response = await agent.run(query, thread=restored_thread)
|
||||
if response.messages:
|
||||
print(f"[assistant]: {response.messages[0].text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(demonstrate_thread_serialization())
|
||||
+12
-10
@@ -122,11 +122,17 @@ def _print_handoff_request(request: HandoffUserInputRequest, request_id: str) ->
|
||||
print(f"Awaiting agent: {request.awaiting_agent_id}")
|
||||
print(f"Prompt: {request.prompt}")
|
||||
|
||||
print("\nConversation so far:")
|
||||
for msg in request.conversation[-3:]:
|
||||
author = msg.author_name or msg.role.value
|
||||
snippet = msg.text[:120] + "..." if len(msg.text) > 120 else msg.text
|
||||
print(f" {author}: {snippet}")
|
||||
# Note: After checkpoint restore, conversation may be empty because it's not serialized
|
||||
# to prevent duplication (the conversation is preserved in the coordinator's state).
|
||||
# See issue #2667.
|
||||
if request.conversation:
|
||||
print("\nConversation so far:")
|
||||
for msg in request.conversation[-3:]:
|
||||
author = msg.author_name or msg.role.value
|
||||
snippet = msg.text[:120] + "..." if len(msg.text) > 120 else msg.text
|
||||
print(f" {author}: {snippet}")
|
||||
else:
|
||||
print("\n(Conversation restored from checkpoint - context preserved in workflow state)")
|
||||
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
@@ -273,11 +279,7 @@ async def resume_with_responses(
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
print("\n[Workflow Output Event - Conversation Update]")
|
||||
if (
|
||||
event.data
|
||||
and isinstance(event.data, list)
|
||||
and all(isinstance(msg, ChatMessage) for msg in event.data)
|
||||
):
|
||||
if event.data and isinstance(event.data, list) and all(isinstance(msg, ChatMessage) for msg in event.data):
|
||||
# Now safe to cast event.data to list[ChatMessage]
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
for msg in conversation[-3:]: # Show last 3 messages
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Sample: Workflow as Agent with Checkpointing
|
||||
|
||||
Purpose:
|
||||
This sample demonstrates how to use checkpointing with a workflow wrapped as an agent.
|
||||
It shows how to enable checkpoint storage when calling agent.run() or agent.run_stream(),
|
||||
allowing workflow execution state to be persisted and potentially resumed.
|
||||
|
||||
What you learn:
|
||||
- How to pass checkpoint_storage to WorkflowAgent.run() and run_stream()
|
||||
- How checkpoints are created during workflow-as-agent execution
|
||||
- How to combine thread conversation history with workflow checkpointing
|
||||
- How to resume a workflow-as-agent from a checkpoint
|
||||
|
||||
Key concepts:
|
||||
- Thread (AgentThread): Maintains conversation history across agent invocations
|
||||
- Checkpoint: Persists workflow execution state for pause/resume capability
|
||||
- These are complementary: threads track conversation, checkpoints track workflow state
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI environment variables configured for OpenAIChatClient
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
AgentThread,
|
||||
ChatAgent,
|
||||
ChatMessageStore,
|
||||
InMemoryCheckpointStorage,
|
||||
SequentialBuilder,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
|
||||
async def basic_checkpointing() -> None:
|
||||
"""Demonstrate basic checkpoint storage with workflow-as-agent."""
|
||||
print("=" * 60)
|
||||
print("Basic Checkpointing with Workflow as Agent")
|
||||
print("=" * 60)
|
||||
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="assistant",
|
||||
instructions="You are a helpful assistant. Keep responses brief.",
|
||||
)
|
||||
|
||||
def create_reviewer() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="reviewer",
|
||||
instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.",
|
||||
)
|
||||
|
||||
# Build sequential workflow with participant factories
|
||||
workflow = SequentialBuilder().register_participants([create_assistant, create_reviewer]).build()
|
||||
agent = workflow.as_agent(name="CheckpointedAgent")
|
||||
|
||||
# Create checkpoint storage
|
||||
checkpoint_storage = InMemoryCheckpointStorage()
|
||||
|
||||
# Run with checkpointing enabled
|
||||
query = "What are the benefits of renewable energy?"
|
||||
print(f"\nUser: {query}")
|
||||
|
||||
response = await agent.run(query, checkpoint_storage=checkpoint_storage)
|
||||
|
||||
for msg in response.messages:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
print(f"[{speaker}]: {msg.text}")
|
||||
|
||||
# Show checkpoints that were created
|
||||
checkpoints = await checkpoint_storage.list_checkpoints(workflow.id)
|
||||
print(f"\nCheckpoints created: {len(checkpoints)}")
|
||||
for i, cp in enumerate(checkpoints[:5], 1):
|
||||
print(f" {i}. {cp.checkpoint_id}")
|
||||
|
||||
|
||||
async def checkpointing_with_thread() -> None:
|
||||
"""Demonstrate combining thread history with checkpointing."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Checkpointing with Thread Conversation History")
|
||||
print("=" * 60)
|
||||
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="memory_assistant",
|
||||
instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.",
|
||||
)
|
||||
|
||||
workflow = SequentialBuilder().register_participants([create_assistant]).build()
|
||||
agent = workflow.as_agent(name="MemoryAgent")
|
||||
|
||||
# Create both thread (for conversation) and checkpoint storage (for workflow state)
|
||||
thread = AgentThread(message_store=ChatMessageStore())
|
||||
checkpoint_storage = InMemoryCheckpointStorage()
|
||||
|
||||
# First turn
|
||||
query1 = "My favorite color is blue. Remember that."
|
||||
print(f"\n[Turn 1] User: {query1}")
|
||||
response1 = await agent.run(query1, thread=thread, checkpoint_storage=checkpoint_storage)
|
||||
if response1.messages:
|
||||
print(f"[assistant]: {response1.messages[0].text}")
|
||||
|
||||
# Second turn - agent should remember from thread history
|
||||
query2 = "What's my favorite color?"
|
||||
print(f"\n[Turn 2] User: {query2}")
|
||||
response2 = await agent.run(query2, thread=thread, checkpoint_storage=checkpoint_storage)
|
||||
if response2.messages:
|
||||
print(f"[assistant]: {response2.messages[0].text}")
|
||||
|
||||
# Show accumulated state
|
||||
checkpoints = await checkpoint_storage.list_checkpoints(workflow.id)
|
||||
print(f"\nTotal checkpoints across both turns: {len(checkpoints)}")
|
||||
|
||||
if thread.message_store:
|
||||
history = await thread.message_store.list_messages()
|
||||
print(f"Messages in thread history: {len(history)}")
|
||||
|
||||
|
||||
async def streaming_with_checkpoints() -> None:
|
||||
"""Demonstrate streaming with checkpoint storage."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Streaming with Checkpointing")
|
||||
print("=" * 60)
|
||||
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
def create_assistant() -> ChatAgent:
|
||||
return chat_client.create_agent(
|
||||
name="streaming_assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
workflow = SequentialBuilder().register_participants([create_assistant]).build()
|
||||
agent = workflow.as_agent(name="StreamingCheckpointAgent")
|
||||
|
||||
checkpoint_storage = InMemoryCheckpointStorage()
|
||||
|
||||
query = "List three interesting facts about the ocean."
|
||||
print(f"\nUser: {query}")
|
||||
print("[assistant]: ", end="", flush=True)
|
||||
|
||||
# Stream with checkpointing
|
||||
async for update in agent.run_stream(query, checkpoint_storage=checkpoint_storage):
|
||||
if update.text:
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
print() # Newline after streaming
|
||||
|
||||
checkpoints = await checkpoint_storage.list_checkpoints(workflow.id)
|
||||
print(f"\nCheckpoints created during stream: {len(checkpoints)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(basic_checkpointing())
|
||||
asyncio.run(checkpointing_with_thread())
|
||||
asyncio.run(streaming_with_checkpoints())
|
||||
@@ -25,7 +25,7 @@ What this example shows:
|
||||
- ExecutorCompletedEvent.data contains the messages sent via ctx.send_message()
|
||||
- How to generically observe all executor I/O through workflow streaming events
|
||||
|
||||
This approach allows you to instrument any workflow for observability without
|
||||
This approach allows you to enable_instrumentation any workflow for observability without
|
||||
changing the executor implementations.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ to synthesize a concise, consolidated summary from the experts' outputs.
|
||||
The workflow completes when all participants become idle.
|
||||
|
||||
Demonstrates:
|
||||
- ConcurrentBuilder().participants([...]).with_custom_aggregator(callback)
|
||||
- ConcurrentBuilder().participants([...]).with_aggregator(callback)
|
||||
- Fan-out to agents and fan-in at an aggregator
|
||||
- Aggregation implemented via an LLM call (chat_client.get_response)
|
||||
- Workflow output yielded with the synthesized summary string
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Never
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
ConcurrentBuilder,
|
||||
Executor,
|
||||
Role,
|
||||
Workflow,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""
|
||||
Sample: Concurrent Orchestration with participant factories and Custom Aggregator
|
||||
|
||||
Build a concurrent workflow with ConcurrentBuilder that fans out one prompt to
|
||||
multiple domain agents and fans in their responses.
|
||||
|
||||
Override the default aggregator with a custom Executor class that uses
|
||||
AzureOpenAIChatClient.get_response() to synthesize a concise, consolidated summary
|
||||
from the experts' outputs.
|
||||
|
||||
All participants and the aggregator are created via factory functions that return
|
||||
their respective ChatAgent or Executor instances.
|
||||
|
||||
Using participant factories allows you to set up proper state isolation between workflow
|
||||
instances created by the same builder. This is particularly useful when you need to handle
|
||||
requests or tasks in parallel with stateful participants.
|
||||
|
||||
Demonstrates:
|
||||
- ConcurrentBuilder().register_participants([...]).with_aggregator(callback)
|
||||
- Fan-out to agents and fan-in at an aggregator
|
||||
- Aggregation implemented via an LLM call (chat_client.get_response)
|
||||
- Workflow output yielded with the synthesized summary string
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars)
|
||||
"""
|
||||
|
||||
|
||||
def create_researcher() -> ChatAgent:
|
||||
"""Factory function to create a researcher agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
|
||||
" opportunities, and risks."
|
||||
),
|
||||
name="researcher",
|
||||
)
|
||||
|
||||
|
||||
def create_marketer() -> ChatAgent:
|
||||
"""Factory function to create a marketer agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
|
||||
" aligned to the prompt."
|
||||
),
|
||||
name="marketer",
|
||||
)
|
||||
|
||||
|
||||
def create_legal() -> ChatAgent:
|
||||
"""Factory function to create a legal/compliance agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions=(
|
||||
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
|
||||
" based on the prompt."
|
||||
),
|
||||
name="legal",
|
||||
)
|
||||
|
||||
|
||||
class SummarizationExecutor(Executor):
|
||||
"""Custom aggregator executor that synthesizes expert outputs into a concise summary."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(id="summarization_executor")
|
||||
self.chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
@handler
|
||||
async def summarize_results(self, results: list[Any], ctx: WorkflowContext[Never, str]) -> None:
|
||||
expert_sections: list[str] = []
|
||||
for r in results:
|
||||
try:
|
||||
messages = getattr(r.agent_run_response, "messages", [])
|
||||
final_text = messages[-1].text if messages and hasattr(messages[-1], "text") else "(no content)"
|
||||
expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}:\n{final_text}")
|
||||
except Exception as e:
|
||||
expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}: (error: {type(e).__name__}: {e})")
|
||||
|
||||
# Ask the model to synthesize a concise summary of the experts' outputs
|
||||
system_msg = ChatMessage(
|
||||
Role.SYSTEM,
|
||||
text=(
|
||||
"You are a helpful assistant that consolidates multiple domain expert outputs "
|
||||
"into one cohesive, concise summary with clear takeaways. Keep it under 200 words."
|
||||
),
|
||||
)
|
||||
user_msg = ChatMessage(Role.USER, text="\n\n".join(expert_sections))
|
||||
|
||||
response = await self.chat_client.get_response([system_msg, user_msg])
|
||||
|
||||
await ctx.yield_output(response.messages[-1].text if response.messages else "")
|
||||
|
||||
|
||||
async def run_workflow(workflow: Workflow, query: str) -> None:
|
||||
events = await workflow.run(query)
|
||||
outputs = events.get_outputs()
|
||||
|
||||
if outputs:
|
||||
print(outputs[0]) # Get the first (and typically only) output
|
||||
else:
|
||||
raise RuntimeError("No outputs received from the workflow.")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Create a concurrent builder with participant factories and a custom aggregator
|
||||
# - register_participants([...]) accepts factory functions that return
|
||||
# AgentProtocol (agents) or Executor instances.
|
||||
# - register_aggregator(...) takes a factory function that returns an Executor instance.
|
||||
concurrent_builder = (
|
||||
ConcurrentBuilder()
|
||||
.register_participants([create_researcher, create_marketer, create_legal])
|
||||
.register_aggregator(SummarizationExecutor)
|
||||
)
|
||||
|
||||
# Build workflow_a
|
||||
workflow_a = concurrent_builder.build()
|
||||
|
||||
# Run workflow_a
|
||||
# Context is maintained across runs
|
||||
print("=== First Run on workflow_a ===")
|
||||
await run_workflow(workflow_a, "We are launching a new budget-friendly electric bike for urban commuters.")
|
||||
print("\n=== Second Run on workflow_a ===")
|
||||
await run_workflow(workflow_a, "Refine your response to focus on the California market.")
|
||||
|
||||
# Build workflow_b
|
||||
# This will create new instances of all participants and the aggregator
|
||||
# The agents will also get new threads
|
||||
workflow_b = concurrent_builder.build()
|
||||
# Run workflow_b
|
||||
# Context is not maintained across instances
|
||||
# Should not expect mentions of electric bikes in the results
|
||||
print("\n=== First Run on workflow_b ===")
|
||||
await run_workflow(workflow_b, "Refine your response to focus on the California market.")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
=== First Run on workflow_a ===
|
||||
The budget-friendly electric bike market is poised for significant growth, driven by urbanization, ...
|
||||
|
||||
=== Second Run on workflow_a ===
|
||||
Launching a budget-friendly electric bike in California presents significant opportunities, driven ...
|
||||
|
||||
=== First Run on workflow_b ===
|
||||
To successfully penetrate the California market, consider these tailored strategies focused on ...
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
HandoffBuilder,
|
||||
HostedWebSearchTool,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
)
|
||||
@@ -44,31 +45,29 @@ def create_agents(
|
||||
"""Create coordinator and specialists for autonomous iteration."""
|
||||
coordinator = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a coordinator. Route user requests to either research_agent or summary_agent. "
|
||||
"Always call exactly one handoff tool with a short routing acknowledgement. "
|
||||
"If unsure, default to research_agent. Never request information yourself. "
|
||||
"After a specialist hands off back to you, provide a concise final summary and stop."
|
||||
"You are a coordinator. You break down a user query into a research task and a summary task. "
|
||||
"Assign the two tasks to the appropriate specialists, one after the other."
|
||||
),
|
||||
name="coordinator",
|
||||
)
|
||||
|
||||
research_agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a research specialist that explores topics thoroughly. "
|
||||
"You are a research specialist that explores topics thoroughly on the Microsoft Learn Site."
|
||||
"When given a research task, break it down into multiple aspects and explore each one. "
|
||||
"Continue your research across multiple responses - don't try to finish everything in one response. "
|
||||
"After each response, think about what else needs to be explored. "
|
||||
"When you have covered the topic comprehensively (at least 3-4 different aspects), "
|
||||
"call the handoff tool to return to coordinator with your findings. "
|
||||
"Keep each individual response focused on one aspect."
|
||||
"Continue your research across multiple responses - don't try to finish everything in one "
|
||||
"response. After each response, think about what else needs to be explored. When you have "
|
||||
"covered the topic comprehensively (at least 3-4 different aspects), return control to the "
|
||||
"coordinator. Keep each individual response focused on one aspect."
|
||||
),
|
||||
name="research_agent",
|
||||
tools=[HostedWebSearchTool()],
|
||||
)
|
||||
|
||||
summary_agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You summarize research findings. Provide a concise, well-organized summary. "
|
||||
"When done, hand off to coordinator."
|
||||
"You summarize research findings. Provide a concise, well-organized summary. When done, return "
|
||||
"control to the coordinator."
|
||||
),
|
||||
name="summary_agent",
|
||||
)
|
||||
@@ -76,25 +75,29 @@ def create_agents(
|
||||
return coordinator, research_agent, summary_agent
|
||||
|
||||
|
||||
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
"""Collect all events from an async stream."""
|
||||
return [event async for event in stream]
|
||||
last_response_id: str | None = None
|
||||
|
||||
|
||||
def _print_conversation(events: list[WorkflowEvent]) -> None:
|
||||
def _display_event(event: WorkflowEvent) -> None:
|
||||
"""Print the final conversation snapshot from workflow output events."""
|
||||
for event in events:
|
||||
if isinstance(event, AgentRunUpdateEvent):
|
||||
print(event.data, flush=True, end="")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
print("\n=== Final Conversation (Autonomous with Iteration) ===")
|
||||
for message in conversation:
|
||||
speaker = message.author_name or message.role.value
|
||||
text_preview = message.text[:200] + "..." if len(message.text) > 200 else message.text
|
||||
print(f"- {speaker}: {text_preview}")
|
||||
print(f"\nTotal messages: {len(conversation)}")
|
||||
print("=====================================================")
|
||||
if isinstance(event, AgentRunUpdateEvent) and event.data:
|
||||
update: AgentRunResponseUpdate = event.data
|
||||
if not update.text:
|
||||
return
|
||||
global last_response_id
|
||||
if update.response_id != last_response_id:
|
||||
last_response_id = update.response_id
|
||||
print(f"\n- {update.author_name}: ", flush=True, end="")
|
||||
print(event.data, flush=True, end="")
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
print("\n=== Final Conversation (Autonomous with Iteration) ===")
|
||||
for message in conversation:
|
||||
speaker = message.author_name or message.role.value
|
||||
text_preview = message.text[:200] + "..." if len(message.text) > 200 else message.text
|
||||
print(f"- {speaker}: {text_preview}")
|
||||
print(f"\nTotal messages: {len(conversation)}")
|
||||
print("=====================================================")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -122,12 +125,10 @@ async def main() -> None:
|
||||
.build()
|
||||
)
|
||||
|
||||
initial_request = "Research the key benefits and challenges of renewable energy adoption."
|
||||
print("Initial request:", initial_request)
|
||||
print("\nExpecting multiple iterations from the research agent...\n")
|
||||
|
||||
events = await _drain(workflow.run_stream(initial_request))
|
||||
_print_conversation(events)
|
||||
request = "Perform a comprehensive research on Microsoft Agent Framework."
|
||||
print("Request:", request)
|
||||
async for event in workflow.run_stream(request):
|
||||
_display_event(event)
|
||||
|
||||
"""
|
||||
Expected behavior:
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from typing import Annotated
|
||||
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
|
||||
"""Sample: Autonomous handoff workflow with agent factory.
|
||||
|
||||
This sample demonstrates how to use participant factories in HandoffBuilder to create
|
||||
agents dynamically.
|
||||
|
||||
Using participant factories allows you to set up proper state isolation between workflow
|
||||
instances created by the same builder. This is particularly useful when you need to handle
|
||||
requests or tasks in parallel with stateful participants.
|
||||
|
||||
Routing Pattern:
|
||||
User -> Coordinator -> Specialist (iterates N times) -> Handoff -> Final Output
|
||||
|
||||
Prerequisites:
|
||||
- `az login` (Azure CLI authentication)
|
||||
- Environment variables for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
|
||||
|
||||
Key Concepts:
|
||||
- Participant factories: create agents via factory functions for isolation
|
||||
"""
|
||||
|
||||
|
||||
@ai_function
|
||||
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
|
||||
"""Simulated function to process a refund for a given order number."""
|
||||
return f"Refund processed successfully for order {order_number}."
|
||||
|
||||
|
||||
@ai_function
|
||||
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
|
||||
"""Simulated function to check the status of a given order number."""
|
||||
return f"Order {order_number} is currently being processed and will ship in 2 business days."
|
||||
|
||||
|
||||
@ai_function
|
||||
def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str:
|
||||
"""Simulated function to process a return for a given order number."""
|
||||
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
|
||||
|
||||
|
||||
def create_triage_agent() -> ChatAgent:
|
||||
"""Factory function to create a triage agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions=(
|
||||
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
|
||||
"based on the problem described."
|
||||
),
|
||||
name="triage_agent",
|
||||
)
|
||||
|
||||
|
||||
def create_refund_agent() -> ChatAgent:
|
||||
"""Factory function to create a refund agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions="You process refund requests.",
|
||||
name="refund_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[process_refund],
|
||||
)
|
||||
|
||||
|
||||
def create_order_status_agent() -> ChatAgent:
|
||||
"""Factory function to create an order status agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions="You handle order and shipping inquiries.",
|
||||
name="order_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[check_order_status],
|
||||
)
|
||||
|
||||
|
||||
def create_return_agent() -> ChatAgent:
|
||||
"""Factory function to create a return agent instance."""
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
|
||||
instructions="You manage product return requests.",
|
||||
name="return_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[process_return],
|
||||
)
|
||||
|
||||
|
||||
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
"""Collect all events from an async stream into a list.
|
||||
|
||||
This helper drains the workflow's event stream so we can process events
|
||||
synchronously after each workflow step completes.
|
||||
|
||||
Args:
|
||||
stream: Async iterable of WorkflowEvent
|
||||
|
||||
Returns:
|
||||
List of all events from the stream
|
||||
"""
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
"""Process workflow events and extract any pending user input requests.
|
||||
|
||||
This function inspects each event type and:
|
||||
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
|
||||
- Displays final conversation snapshots when workflow completes
|
||||
- Prints user input request prompts
|
||||
- Collects all RequestInfoEvent instances for response handling
|
||||
|
||||
Args:
|
||||
events: List of WorkflowEvent to process
|
||||
|
||||
Returns:
|
||||
List of RequestInfoEvent representing pending user input requests
|
||||
"""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
|
||||
for event in events:
|
||||
# WorkflowOutputEvent: Contains the final conversation when workflow terminates
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
if isinstance(conversation, list):
|
||||
print("\n=== Final Conversation Snapshot ===")
|
||||
for message in conversation:
|
||||
speaker = message.author_name or message.role.value
|
||||
print(f"- {speaker}: {message.text}")
|
||||
print("===================================")
|
||||
|
||||
# RequestInfoEvent: Workflow is requesting user input
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
if isinstance(event.data, HandoffUserInputRequest):
|
||||
_print_agent_responses_since_last_user_message(event.data)
|
||||
requests.append(event)
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
def _print_agent_responses_since_last_user_message(request: HandoffUserInputRequest) -> None:
|
||||
"""Display agent responses since the last user message in a handoff request.
|
||||
|
||||
The HandoffUserInputRequest contains the full conversation history so far,
|
||||
allowing the user to see what's been discussed before providing their next input.
|
||||
|
||||
Args:
|
||||
request: The user input request containing conversation and prompt
|
||||
"""
|
||||
if not request.conversation:
|
||||
raise RuntimeError("HandoffUserInputRequest missing conversation history.")
|
||||
|
||||
# Reverse iterate to collect agent responses since last user message
|
||||
agent_responses: list[ChatMessage] = []
|
||||
for message in request.conversation[::-1]:
|
||||
if message.role == Role.USER:
|
||||
break
|
||||
agent_responses.append(message)
|
||||
|
||||
# Print agent responses in original order
|
||||
agent_responses.reverse()
|
||||
for message in agent_responses:
|
||||
speaker = message.author_name or message.role.value
|
||||
print(f"- {speaker}: {message.text}")
|
||||
|
||||
|
||||
async def _run_Workflow(workflow: Workflow, user_inputs: list[str]) -> None:
|
||||
"""Run the workflow with the given user input and display events."""
|
||||
print(f"- User: {user_inputs[0]}")
|
||||
events = await _drain(workflow.run_stream(user_inputs[0]))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
# Process the request/response cycle
|
||||
# The workflow will continue requesting input until:
|
||||
# 1. The termination condition is met (4 user messages in this case), OR
|
||||
# 2. We run out of scripted responses
|
||||
while pending_requests and user_inputs[1:]:
|
||||
# Get the next scripted response
|
||||
user_response = user_inputs.pop(1)
|
||||
print(f"\n- User: {user_response}")
|
||||
|
||||
# Send response(s) to all pending requests
|
||||
# In this demo, there's typically one request per cycle, but the API supports multiple
|
||||
responses = {req.request_id: user_response for req in pending_requests}
|
||||
|
||||
# Send responses and get new events
|
||||
# We use send_responses_streaming() to get events as they occur, allowing us to
|
||||
# display agent responses in real-time and handle new requests as they arrive
|
||||
events = await _drain(workflow.send_responses_streaming(responses))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the autonomous handoff workflow with participant factories."""
|
||||
# Build the handoff workflow using participant factories
|
||||
workflow_builder = (
|
||||
HandoffBuilder(
|
||||
name="Autonomous Handoff with Participant Factories",
|
||||
participant_factories={
|
||||
"triage": create_triage_agent,
|
||||
"refund": create_refund_agent,
|
||||
"order_status": create_order_status_agent,
|
||||
"return": create_return_agent,
|
||||
},
|
||||
)
|
||||
.set_coordinator("triage")
|
||||
.with_termination_condition(
|
||||
# Custom termination: Check if the triage agent has provided a closing message.
|
||||
# This looks for the last message being from triage_agent and containing "welcome",
|
||||
# which indicates the conversation has concluded naturally.
|
||||
lambda conversation: len(conversation) > 0
|
||||
and conversation[-1].author_name == "triage_agent"
|
||||
and "welcome" in conversation[-1].text.lower()
|
||||
)
|
||||
)
|
||||
|
||||
# Scripted user responses for reproducible demo
|
||||
# In a console application, replace this with:
|
||||
# user_input = input("Your response: ")
|
||||
# or integrate with a UI/chat interface
|
||||
user_inputs = [
|
||||
"Hello, I need assistance with my recent purchase.",
|
||||
"My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.",
|
||||
"Is my return being processed?",
|
||||
"Thanks for resolving this.",
|
||||
]
|
||||
|
||||
workflow_a = workflow_builder.build()
|
||||
print("=== Running workflow_a ===")
|
||||
await _run_Workflow(workflow_a, list(user_inputs))
|
||||
|
||||
workflow_b = workflow_builder.build()
|
||||
print("=== Running workflow_b ===")
|
||||
# Only provide the last two inputs to workflow_b to demonstrate state isolation
|
||||
# The agents in this workflow have no prior context thus should not have knowledge of
|
||||
# order 1234 or previous interactions.
|
||||
await _run_Workflow(workflow_b, user_inputs[2:])
|
||||
"""
|
||||
Expected behavior:
|
||||
- workflow_a and workflow_b maintain separate states for their participants.
|
||||
- Each workflow processes its requests independently without interference.
|
||||
- workflow_a will answer the follow-up request based on its own conversation history,
|
||||
while workflow_b will provide a general answer without prior context.
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import cast
|
||||
from typing import Annotated, cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
@@ -10,10 +10,12 @@ from agent_framework import (
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
@@ -22,10 +24,10 @@ from azure.identity import AzureCliCredential
|
||||
|
||||
This sample demonstrates the basic handoff pattern where only the triage agent can
|
||||
route to specialists. Specialists cannot hand off to other specialists - after any
|
||||
specialist responds, control returns to the user for the next input.
|
||||
specialist responds, control returns to the user (via the triage agent) for the next input.
|
||||
|
||||
Routing Pattern:
|
||||
User → Triage Agent → Specialist → Back to User → Triage Agent → ...
|
||||
User → Triage Agent → Specialist → Triage Agent → User → Triage Agent → ...
|
||||
|
||||
This is the simplest handoff configuration, suitable for straightforward support
|
||||
scenarios where a triage agent dispatches to domain specialists, and each specialist
|
||||
@@ -39,12 +41,31 @@ Prerequisites:
|
||||
|
||||
Key Concepts:
|
||||
- Single-tier routing: Only triage agent has handoff capabilities
|
||||
- Auto-registered handoff tools: HandoffBuilder creates tools automatically
|
||||
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
|
||||
for each participant, allowing the coordinator to transfer control to specialists
|
||||
- Termination condition: Controls when the workflow stops requesting user input
|
||||
- Request/response cycle: Workflow requests input, user responds, cycle continues
|
||||
"""
|
||||
|
||||
|
||||
@ai_function
|
||||
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
|
||||
"""Simulated function to process a refund for a given order number."""
|
||||
return f"Refund processed successfully for order {order_number}."
|
||||
|
||||
|
||||
@ai_function
|
||||
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
|
||||
"""Simulated function to check the status of a given order number."""
|
||||
return f"Order {order_number} is currently being processed and will ship in 2 business days."
|
||||
|
||||
|
||||
@ai_function
|
||||
def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str:
|
||||
"""Simulated function to process a return for a given order number."""
|
||||
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
|
||||
|
||||
|
||||
def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent, ChatAgent]:
|
||||
"""Create and configure the triage and specialist agents.
|
||||
|
||||
@@ -54,51 +75,46 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg
|
||||
- Signaling handoff by calling one of the explicit handoff tools exposed to it
|
||||
|
||||
Specialist agents are invoked only when the triage agent explicitly hands off to them.
|
||||
After a specialist responds, control returns to the triage agent.
|
||||
After a specialist responds, control returns to the triage agent, which then prompts
|
||||
the user for their next message.
|
||||
|
||||
Returns:
|
||||
Tuple of (triage_agent, refund_agent, order_agent, support_agent)
|
||||
Tuple of (triage_agent, refund_agent, order_agent, return_agent)
|
||||
"""
|
||||
# Triage agent: Acts as the frontline dispatcher
|
||||
# NOTE: The instructions explicitly tell it to call the correct handoff tool when routing.
|
||||
# The HandoffBuilder intercepts these tool calls and routes to the matching specialist.
|
||||
triage = chat_client.create_agent(
|
||||
triage_agent = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are frontline support triage. Read the latest user message and decide whether "
|
||||
"to hand off to refund_agent, order_agent, or support_agent. Provide a brief natural-language "
|
||||
"response for the user. When delegation is required, call the matching handoff tool "
|
||||
"(`handoff_to_refund_agent`, `handoff_to_order_agent`, or `handoff_to_support_agent`)."
|
||||
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
|
||||
"based on the problem described."
|
||||
),
|
||||
name="triage_agent",
|
||||
)
|
||||
|
||||
# Refund specialist: Handles refund requests
|
||||
refund = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You handle refund workflows. Ask for any order identifiers you require and outline the refund steps."
|
||||
),
|
||||
refund_agent = chat_client.create_agent(
|
||||
instructions="You process refund requests.",
|
||||
name="refund_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[process_refund],
|
||||
)
|
||||
|
||||
# Order/shipping specialist: Resolves delivery issues
|
||||
order = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You resolve shipping and fulfillment issues. Clarify the delivery problem and describe the actions "
|
||||
"you will take to remedy it."
|
||||
),
|
||||
order_agent = chat_client.create_agent(
|
||||
instructions="You handle order and shipping inquiries.",
|
||||
name="order_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[check_order_status],
|
||||
)
|
||||
|
||||
# General support specialist: Fallback for other issues
|
||||
support = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a general support agent. Offer empathetic troubleshooting and gather missing details if the "
|
||||
"issue does not match other specialists."
|
||||
),
|
||||
name="support_agent",
|
||||
# Return specialist: Handles return requests
|
||||
return_agent = chat_client.create_agent(
|
||||
instructions="You manage product return requests.",
|
||||
name="return_agent",
|
||||
# In a real application, an agent can have multiple tools; here we keep it simple
|
||||
tools=[process_return],
|
||||
)
|
||||
|
||||
return triage, refund, order, support
|
||||
return triage_agent, refund_agent, order_agent, return_agent
|
||||
|
||||
|
||||
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
@@ -139,7 +155,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
WorkflowRunState.IDLE,
|
||||
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
|
||||
}:
|
||||
print(f"[status] {event.state.name}")
|
||||
print(f"\n[Workflow Status] {event.state.name}")
|
||||
|
||||
# WorkflowOutputEvent: Contains the final conversation when workflow terminates
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
@@ -154,14 +170,14 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
# RequestInfoEvent: Workflow is requesting user input
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
if isinstance(event.data, HandoffUserInputRequest):
|
||||
_print_handoff_request(event.data)
|
||||
_print_agent_responses_since_last_user_message(event.data)
|
||||
requests.append(event)
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
def _print_handoff_request(request: HandoffUserInputRequest) -> None:
|
||||
"""Display a user input request prompt with conversation context.
|
||||
def _print_agent_responses_since_last_user_message(request: HandoffUserInputRequest) -> None:
|
||||
"""Display agent responses since the last user message in a handoff request.
|
||||
|
||||
The HandoffUserInputRequest contains the full conversation history so far,
|
||||
allowing the user to see what's been discussed before providing their next input.
|
||||
@@ -169,11 +185,21 @@ def _print_handoff_request(request: HandoffUserInputRequest) -> None:
|
||||
Args:
|
||||
request: The user input request containing conversation and prompt
|
||||
"""
|
||||
print("\n=== User Input Requested ===")
|
||||
for message in request.conversation:
|
||||
if not request.conversation:
|
||||
raise RuntimeError("HandoffUserInputRequest missing conversation history.")
|
||||
|
||||
# Reverse iterate to collect agent responses since last user message
|
||||
agent_responses: list[ChatMessage] = []
|
||||
for message in request.conversation[::-1]:
|
||||
if message.role == Role.USER:
|
||||
break
|
||||
agent_responses.append(message)
|
||||
|
||||
# Print agent responses in original order
|
||||
agent_responses.reverse()
|
||||
for message in agent_responses:
|
||||
speaker = message.author_name or message.role.value
|
||||
print(f"- {speaker}: {message.text}")
|
||||
print("============================")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -196,20 +222,26 @@ async def main() -> None:
|
||||
triage, refund, order, support = create_agents(chat_client)
|
||||
|
||||
# Build the handoff workflow
|
||||
# - participants: All agents that can participate (triage MUST be first or explicitly set as set_coordinator)
|
||||
# - set_coordinator: The triage agent receives all user input first
|
||||
# - with_termination_condition: Custom logic to stop the request/response loop
|
||||
# Default is 10 user messages; here we terminate after 4 to match our scripted demo
|
||||
# - participants: All agents that can participate in the workflow
|
||||
# - set_coordinator: The triage agent is designated as the coordinator, which means
|
||||
# it receives all user input first and orchestrates handoffs to specialists
|
||||
# - with_termination_condition: Custom logic to stop the request/response loop.
|
||||
# Without this, the default behavior continues requesting user input until max_turns
|
||||
# is reached. Here we use a custom condition that checks if the conversation has ended
|
||||
# naturally (when triage agent says something like "you're welcome").
|
||||
workflow = (
|
||||
HandoffBuilder(
|
||||
name="customer_support_handoff",
|
||||
participants=[triage, refund, order, support],
|
||||
)
|
||||
.set_coordinator("triage_agent")
|
||||
.set_coordinator(triage)
|
||||
.with_termination_condition(
|
||||
# Terminate after 4 user messages (initial + 3 scripted responses)
|
||||
# Count only USER role messages to avoid counting agent responses
|
||||
lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 4
|
||||
# Custom termination: Check if the triage agent has provided a closing message.
|
||||
# This looks for the last message being from triage_agent and containing "welcome",
|
||||
# which indicates the conversation has concluded naturally.
|
||||
lambda conversation: len(conversation) > 0
|
||||
and conversation[-1].author_name == "triage_agent"
|
||||
and "welcome" in conversation[-1].text.lower()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
@@ -219,15 +251,16 @@ async def main() -> None:
|
||||
# user_input = input("Your response: ")
|
||||
# or integrate with a UI/chat interface
|
||||
scripted_responses = [
|
||||
"My order 1234 arrived damaged and the packaging was destroyed.",
|
||||
"Yes, I'd like a refund if that's possible.",
|
||||
"My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.",
|
||||
"Thanks for resolving this.",
|
||||
]
|
||||
|
||||
# Start the workflow with the initial user message
|
||||
# run_stream() returns an async iterator of WorkflowEvent
|
||||
print("\n[Starting workflow with initial user message...]")
|
||||
events = await _drain(workflow.run_stream("Hello, I need assistance with my recent purchase."))
|
||||
print("[Starting workflow with initial user message...]\n")
|
||||
initial_message = "Hello, I need assistance with my recent purchase."
|
||||
print(f"- User: {initial_message}")
|
||||
events = await _drain(workflow.run_stream(initial_message))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
# Process the request/response cycle
|
||||
@@ -237,13 +270,15 @@ async def main() -> None:
|
||||
while pending_requests and scripted_responses:
|
||||
# Get the next scripted response
|
||||
user_response = scripted_responses.pop(0)
|
||||
print(f"\n[User responding: {user_response}]")
|
||||
print(f"\n- User: {user_response}")
|
||||
|
||||
# Send response(s) to all pending requests
|
||||
# In this demo, there's typically one request per cycle, but the API supports multiple
|
||||
responses = {req.request_id: user_response for req in pending_requests}
|
||||
|
||||
# Send responses and get new events
|
||||
# We use send_responses_streaming() to get events as they occur, allowing us to
|
||||
# display agent responses in real-time and handle new requests as they arrive
|
||||
events = await _drain(workflow.send_responses_streaming(responses))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
@@ -252,84 +287,30 @@ async def main() -> None:
|
||||
|
||||
[Starting workflow with initial user message...]
|
||||
|
||||
=== User Input Requested ===
|
||||
- User: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: Could you please provide more details about the issue you're experiencing with your recent purchase? This will help me route you to the appropriate specialist.
|
||||
|
||||
[Workflow Status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
- User: My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.
|
||||
- triage_agent: I've directed your request to our return agent, who will assist you with returning the damaged order. Thank you for your patience!
|
||||
- return_agent: The return for your order 1234 has been successfully initiated. You will receive return instructions via email shortly. If you have any other questions or need further assistance, feel free to ask!
|
||||
|
||||
[Workflow Status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
- User: Thanks for resolving this.
|
||||
|
||||
=== Final Conversation Snapshot ===
|
||||
- user: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: I'd be happy to help you with your recent purchase. Could you please provide more details about the issue you're experiencing?
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User responding: My order 1234 arrived damaged and the packaging was destroyed.]
|
||||
|
||||
=== User Input Requested ===
|
||||
- user: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: I'd be happy to help you with your recent purchase. Could you please provide more details about the issue you're experiencing?
|
||||
- user: My order 1234 arrived damaged and the packaging was destroyed.
|
||||
- triage_agent: I'm sorry to hear that your order arrived damaged and the packaging was destroyed. I will connect you with a specialist who can assist you further with this issue.
|
||||
|
||||
Tool Call: handoff_to_support_agent (awaiting approval)
|
||||
- support_agent: I'm so sorry to hear that your order arrived in such poor condition. I'll help you get this sorted out.
|
||||
|
||||
To assist you better, could you please let me know:
|
||||
- Which item(s) from order 1234 arrived damaged?
|
||||
- Could you describe the damage, or provide photos if possible?
|
||||
- Would you prefer a replacement or a refund?
|
||||
|
||||
Once I have this information, I can help resolve this for you as quickly as possible.
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User responding: Yes, I'd like a refund if that's possible.]
|
||||
|
||||
=== User Input Requested ===
|
||||
- user: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: I'd be happy to help you with your recent purchase. Could you please provide more details about the issue you're experiencing?
|
||||
- user: My order 1234 arrived damaged and the packaging was destroyed.
|
||||
- triage_agent: I'm sorry to hear that your order arrived damaged and the packaging was destroyed. I will connect you with a specialist who can assist you further with this issue.
|
||||
|
||||
Tool Call: handoff_to_support_agent (awaiting approval)
|
||||
- support_agent: I'm so sorry to hear that your order arrived in such poor condition. I'll help you get this sorted out.
|
||||
|
||||
To assist you better, could you please let me know:
|
||||
- Which item(s) from order 1234 arrived damaged?
|
||||
- Could you describe the damage, or provide photos if possible?
|
||||
- Would you prefer a replacement or a refund?
|
||||
|
||||
Once I have this information, I can help resolve this for you as quickly as possible.
|
||||
- user: Yes, I'd like a refund if that's possible.
|
||||
- triage_agent: Thank you for letting me know you'd prefer a refund. I'll connect you with a specialist who can process your refund request.
|
||||
|
||||
Tool Call: handoff_to_refund_agent (awaiting approval)
|
||||
- refund_agent: Thank you for confirming that you'd like a refund for order 1234.
|
||||
|
||||
Here's what will happen next:
|
||||
|
||||
...
|
||||
|
||||
Tool Call: handoff_to_refund_agent (awaiting approval)
|
||||
- refund_agent: Thank you for confirming that you'd like a refund for order 1234.
|
||||
|
||||
Here's what will happen next:
|
||||
|
||||
**1. Verification:**
|
||||
I will need to verify a few more details to proceed.
|
||||
- Can you confirm the items in order 1234 that arrived damaged?
|
||||
- Do you have any photos of the damaged items/packaging? (Photos help speed up the process.)
|
||||
|
||||
**2. Refund Request Submission:**
|
||||
- Once I have the details, I will submit your refund request for review.
|
||||
|
||||
**3. Return Instructions (if needed):**
|
||||
- In some cases, we may provide instructions on how to return the damaged items.
|
||||
- You will receive a prepaid return label if necessary.
|
||||
|
||||
**4. Refund Processing:**
|
||||
- After your request is approved (and any returns are received if required), your refund will be processed.
|
||||
- Refunds usually appear on your original payment method within 5-10 business days.
|
||||
|
||||
Could you please reply with the specific item(s) damaged and, if possible, attach photos? This will help me get your refund started right away.
|
||||
- triage_agent: Could you please provide more details about the issue you're experiencing with your recent purchase? This will help me route you to the appropriate specialist.
|
||||
- user: My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.
|
||||
- triage_agent: I've directed your request to our return agent, who will assist you with returning the damaged order. Thank you for your patience!
|
||||
- return_agent: The return for your order 1234 has been successfully initiated. You will receive return instructions via email shortly. If you have any other questions or need further assistance, feel free to ask!
|
||||
- user: Thanks for resolving this.
|
||||
- triage_agent: You're welcome! If you have any more questions or need assistance in the future, feel free to reach out. Have a great day!
|
||||
===================================
|
||||
[status] IDLE
|
||||
|
||||
[Workflow Status] IDLE
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Handoff Workflow with Code Interpreter File Generation Sample
|
||||
|
||||
This sample demonstrates retrieving file IDs from code interpreter output
|
||||
in a handoff workflow context. A triage agent routes to a code specialist
|
||||
that generates a text file, and we verify the file_id is captured correctly
|
||||
from the streaming AgentRunUpdateEvent events.
|
||||
|
||||
Verifies GitHub issue #2718: files generated by code interpreter in
|
||||
HandoffBuilder workflows can be properly retrieved.
|
||||
|
||||
Toggle USE_V2_CLIENT to switch between:
|
||||
- V1: AzureAIAgentClient (azure-ai-agents SDK)
|
||||
- V2: AzureAIClient (azure-ai-projects 2.x with Responses API)
|
||||
|
||||
IMPORTANT: When using V2 AzureAIClient with HandoffBuilder, each agent must
|
||||
have its own client instance. The V2 client binds to a single server-side
|
||||
agent name, so sharing a client between agents causes routing issues.
|
||||
|
||||
Prerequisites:
|
||||
- `az login` (Azure CLI authentication)
|
||||
- V1: AZURE_AI_AGENT_PROJECT_CONNECTION_STRING
|
||||
- V2: AZURE_AI_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable, AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunUpdateEvent,
|
||||
ChatAgent,
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
RequestInfoEvent,
|
||||
TextContent,
|
||||
WorkflowEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
# Toggle between V1 (AzureAIAgentClient) and V2 (AzureAIClient)
|
||||
USE_V2_CLIENT = False
|
||||
|
||||
|
||||
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
"""Collect all events from an async stream."""
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent], list[str]]:
|
||||
"""Process workflow events and extract file IDs and pending requests.
|
||||
|
||||
Returns:
|
||||
Tuple of (pending_requests, file_ids_found)
|
||||
"""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
file_ids: list[str] = []
|
||||
|
||||
for event in events:
|
||||
if isinstance(event, WorkflowStatusEvent):
|
||||
if event.state in {WorkflowRunState.IDLE, WorkflowRunState.IDLE_WITH_PENDING_REQUESTS}:
|
||||
print(f"[status] {event.state.name}")
|
||||
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
if isinstance(event.data, HandoffUserInputRequest):
|
||||
print("\n=== Conversation So Far ===")
|
||||
for msg in event.data.conversation:
|
||||
speaker = msg.author_name or msg.role.value
|
||||
text = msg.text or ""
|
||||
txt = text[:200] + "..." if len(text) > 200 else text
|
||||
print(f"- {speaker}: {txt}")
|
||||
print("===========================\n")
|
||||
requests.append(event)
|
||||
|
||||
elif isinstance(event, AgentRunUpdateEvent):
|
||||
update = event.data
|
||||
if update is None:
|
||||
continue
|
||||
for content in update.contents:
|
||||
if isinstance(content, HostedFileContent):
|
||||
file_ids.append(content.file_id)
|
||||
print(f"[Found HostedFileContent: file_id={content.file_id}]")
|
||||
elif isinstance(content, TextContent) and content.annotations:
|
||||
for annotation in content.annotations:
|
||||
if hasattr(annotation, "file_id") and annotation.file_id:
|
||||
file_ids.append(annotation.file_id)
|
||||
print(f"[Found file annotation: file_id={annotation.file_id}]")
|
||||
|
||||
return requests, file_ids
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def create_agents_v1(credential: AzureCliCredential) -> AsyncIterator[tuple[ChatAgent, ChatAgent]]:
|
||||
"""Create agents using V1 AzureAIAgentClient."""
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
|
||||
async with AzureAIAgentClient(credential=credential) as client:
|
||||
triage = client.create_agent(
|
||||
name="triage_agent",
|
||||
instructions=(
|
||||
"You are a triage agent. Route code-related requests to the code_specialist. "
|
||||
"When the user asks to create or generate files, hand off to code_specialist "
|
||||
"by calling handoff_to_code_specialist."
|
||||
),
|
||||
)
|
||||
|
||||
code_specialist = client.create_agent(
|
||||
name="code_specialist",
|
||||
instructions=(
|
||||
"You are a Python code specialist. Use the code interpreter to execute Python code "
|
||||
"and create files when requested. Always save files to /mnt/data/ directory."
|
||||
),
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
)
|
||||
|
||||
yield triage, code_specialist
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def create_agents_v2(credential: AzureCliCredential) -> AsyncIterator[tuple[ChatAgent, ChatAgent]]:
|
||||
"""Create agents using V2 AzureAIClient.
|
||||
|
||||
Each agent needs its own client instance because the V2 client binds
|
||||
to a single server-side agent name.
|
||||
"""
|
||||
from agent_framework.azure import AzureAIClient
|
||||
|
||||
async with (
|
||||
AzureAIClient(credential=credential) as triage_client,
|
||||
AzureAIClient(credential=credential) as code_client,
|
||||
):
|
||||
triage = triage_client.create_agent(
|
||||
name="TriageAgent",
|
||||
instructions=(
|
||||
"You are a triage agent. Your ONLY job is to route requests to the appropriate specialist. "
|
||||
"For code or file creation requests, call handoff_to_CodeSpecialist immediately. "
|
||||
"Do NOT try to complete tasks yourself. Just hand off."
|
||||
),
|
||||
)
|
||||
|
||||
code_specialist = code_client.create_agent(
|
||||
name="CodeSpecialist",
|
||||
instructions=(
|
||||
"You are a Python code specialist. You have access to a code interpreter tool. "
|
||||
"Use the code interpreter to execute Python code and create files. "
|
||||
"Always save files to /mnt/data/ directory. "
|
||||
"Do NOT discuss handoffs or routing - just complete the coding task directly."
|
||||
),
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
)
|
||||
|
||||
yield triage, code_specialist
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run a simple handoff workflow with code interpreter file generation."""
|
||||
client_version = "V2 (AzureAIClient)" if USE_V2_CLIENT else "V1 (AzureAIAgentClient)"
|
||||
print(f"=== Handoff Workflow with Code Interpreter File Generation [{client_version}] ===\n")
|
||||
|
||||
async with AzureCliCredential() as credential:
|
||||
create_agents = create_agents_v2 if USE_V2_CLIENT else create_agents_v1
|
||||
|
||||
async with create_agents(credential) as (triage, code_specialist):
|
||||
workflow = (
|
||||
HandoffBuilder()
|
||||
.participants([triage, code_specialist])
|
||||
.set_coordinator(triage)
|
||||
.with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 2)
|
||||
.build()
|
||||
)
|
||||
|
||||
user_inputs = [
|
||||
"Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.",
|
||||
"exit",
|
||||
]
|
||||
input_index = 0
|
||||
all_file_ids: list[str] = []
|
||||
|
||||
print(f"User: {user_inputs[0]}")
|
||||
events = await _drain(workflow.run_stream(user_inputs[0]))
|
||||
requests, file_ids = _handle_events(events)
|
||||
all_file_ids.extend(file_ids)
|
||||
input_index += 1
|
||||
|
||||
while requests:
|
||||
request = requests[0]
|
||||
if input_index >= len(user_inputs):
|
||||
break
|
||||
user_input = user_inputs[input_index]
|
||||
print(f"\nUser: {user_input}")
|
||||
|
||||
responses = {request.request_id: user_input}
|
||||
events = await _drain(workflow.send_responses_streaming(responses))
|
||||
requests, file_ids = _handle_events(events)
|
||||
all_file_ids.extend(file_ids)
|
||||
input_index += 1
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if all_file_ids:
|
||||
print(f"SUCCESS: Found {len(all_file_ids)} file ID(s) in handoff workflow:")
|
||||
for fid in all_file_ids:
|
||||
print(f" - {fid}")
|
||||
else:
|
||||
print("WARNING: No file IDs captured from the handoff workflow.")
|
||||
print("=" * 50)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
User: Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.
|
||||
[Found HostedFileContent: file_id=assistant-JT1sA...]
|
||||
|
||||
=== Conversation So Far ===
|
||||
- user: Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.
|
||||
- triage_agent: I am handing off your request to create the text file "hello.txt" with the specified content to the code specialist. They will assist you shortly.
|
||||
- code_specialist: The file "hello.txt" has been created with the content "Hello from handoff workflow!". You can download it using the link below:
|
||||
|
||||
[hello.txt](sandbox:/mnt/data/hello.txt)
|
||||
===========================
|
||||
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
User: exit
|
||||
[status] IDLE
|
||||
|
||||
==================================================
|
||||
SUCCESS: Found 1 file ID(s) in handoff workflow:
|
||||
- assistant-JT1sA...
|
||||
==================================================
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,132 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import ChatMessage, SequentialBuilder, WorkflowOutputEvent, ai_function
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Sample: Workflow kwargs Flow to @ai_function Tools
|
||||
|
||||
This sample demonstrates how to flow custom context (skill data, user tokens, etc.)
|
||||
through any workflow pattern to @ai_function tools using the **kwargs pattern.
|
||||
|
||||
Key Concepts:
|
||||
- Pass custom context as kwargs when invoking workflow.run_stream() or workflow.run()
|
||||
- kwargs are stored in SharedState and passed to all agent invocations
|
||||
- @ai_function tools receive kwargs via **kwargs parameter
|
||||
- Works with Sequential, Concurrent, GroupChat, Handoff, and Magentic patterns
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI environment variables configured
|
||||
"""
|
||||
|
||||
|
||||
# Define tools that accept custom context via **kwargs
|
||||
@ai_function
|
||||
def get_user_data(
|
||||
query: Annotated[str, Field(description="What user data to retrieve")],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Retrieve user-specific data based on the authenticated context."""
|
||||
user_token = kwargs.get("user_token", {})
|
||||
user_name = user_token.get("user_name", "anonymous")
|
||||
access_level = user_token.get("access_level", "none")
|
||||
|
||||
print(f"\n[get_user_data] Received kwargs keys: {list(kwargs.keys())}")
|
||||
print(f"[get_user_data] User: {user_name}")
|
||||
print(f"[get_user_data] Access level: {access_level}")
|
||||
|
||||
return f"Retrieved data for user {user_name} with {access_level} access: {query}"
|
||||
|
||||
|
||||
@ai_function
|
||||
def call_api(
|
||||
endpoint_name: Annotated[str, Field(description="Name of the API endpoint to call")],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Call an API using the configured endpoints from custom_data."""
|
||||
custom_data = kwargs.get("custom_data", {})
|
||||
api_config = custom_data.get("api_config", {})
|
||||
|
||||
base_url = api_config.get("base_url", "unknown")
|
||||
endpoints = api_config.get("endpoints", {})
|
||||
|
||||
print(f"\n[call_api] Received kwargs keys: {list(kwargs.keys())}")
|
||||
print(f"[call_api] Base URL: {base_url}")
|
||||
print(f"[call_api] Available endpoints: {list(endpoints.keys())}")
|
||||
|
||||
if endpoint_name in endpoints:
|
||||
return f"Called {base_url}{endpoints[endpoint_name]} successfully"
|
||||
return f"Endpoint '{endpoint_name}' not found in configuration"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=" * 70)
|
||||
print("Workflow kwargs Flow Demo (SequentialBuilder)")
|
||||
print("=" * 70)
|
||||
|
||||
# Create chat client
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
# Create agent with tools that use kwargs
|
||||
agent = chat_client.create_agent(
|
||||
name="assistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. Use the available tools to help users. "
|
||||
"When asked about user data, use get_user_data. "
|
||||
"When asked to call an API, use call_api."
|
||||
),
|
||||
tools=[get_user_data, call_api],
|
||||
)
|
||||
|
||||
# Build a simple sequential workflow
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
# Define custom context that will flow to ai_functions via kwargs
|
||||
custom_data = {
|
||||
"api_config": {
|
||||
"base_url": "https://api.example.com",
|
||||
"endpoints": {
|
||||
"users": "/v1/users",
|
||||
"orders": "/v1/orders",
|
||||
"products": "/v1/products",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
user_token = {
|
||||
"user_name": "bob@contoso.com",
|
||||
"access_level": "admin",
|
||||
}
|
||||
|
||||
print("\nCustom Data being passed:")
|
||||
print(json.dumps(custom_data, indent=2))
|
||||
print(f"\nUser: {user_token['user_name']}")
|
||||
print("\n" + "-" * 70)
|
||||
print("Workflow Execution (watch for [tool_name] logs showing kwargs received):")
|
||||
print("-" * 70)
|
||||
|
||||
# Run workflow with kwargs - these will flow through to ai_functions
|
||||
async for event in workflow.run_stream(
|
||||
"Please get my user data and then call the users API endpoint.",
|
||||
custom_data=custom_data,
|
||||
user_token=user_token,
|
||||
):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output_data = event.data
|
||||
if isinstance(output_data, list):
|
||||
for item in output_data:
|
||||
if isinstance(item, ChatMessage) and item.text:
|
||||
print(f"\n[Final Answer]: {item.text}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Sample Complete")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user