Python: restructure: Python samples into progressive 01-05 layout (#3862)

* restructure: Python samples into progressive 01-05 layout

- 01-get-started/: 6 numbered steps (hello agent → hosting)
- 02-agents/: all agent concept samples (tools, middleware, providers, etc.)
- 03-workflows/: ALL existing workflow samples preserved as-is
- 04-hosting/: azure-functions, durabletask, a2a
- 05-end-to-end/: demos, evaluation, hosted agents
- Old files moved to _to_delete/ for review
- Added AGENTS.md with structure documentation
- autogen-migration/ and semantic-kernel-migration/ preserved at root

* fix: switch to AzureOpenAI Foundry, fix CI failures

- Switch all 01-get-started samples to AzureOpenAIResponsesClient with
  Azure AI Foundry project endpoint (AZURE_AI_PROJECT_ENDPOINT +
  AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + AzureCliCredential)
- Add _to_delete/ and 05-end-to-end/ to pyrightconfig.samples.json excludes
- Fix test paths in packages/ that referenced old getting_started/ dirs:
  durabletask conftest + streaming test, azurefunctions conftest,
  devui conftest + capture_messages + openai_sdk_integration
- Fix workflow_as_agent_human_in_the_loop.py import (sibling import)
- Update hosting READMEs and tool comment paths
- Replace root README.md with new structure overview
- Update AGENTS.md to document Azure OpenAI Foundry as default provider

* cleanup: remove _to_delete folder, copy resource files to active dirs

All files in _to_delete/ were either:
- Exact duplicates of files in the new structure (240 files)
- Same file with only comment path updates (100 files)
- One import-fix diff (workflow_as_agent_human_in_the_loop.py)
- One superseded minimal_sample.py

Resource files (sample.pdf, countries.json, employees.pdf, weather.json)
copied to 02-agents/sample_assets/ and 02-agents/resources/ since active
samples reference them.

* fix: address PR review comments, centralize resources, remove root duplicates

- Fix type annotation in 04_memory.py (string union -> proper types)
- Fix old sample paths in observability files
- Fix grammar/spelling in observability samples
- Move sample_assets/ and resources/ to shared/ folder
- Remove 8 duplicate observability files from 02-agents root
- Update resource path references in multimodal_input and provider samples

* fix: update broken links from old getting_started paths to new structure

- Update relative paths in READMEs: getting_started/ → 01-get-started/,
  02-agents/, 03-workflows/, 04-hosting/, 05-end-to-end/
- Fix absolute GitHub URLs in package READMEs
- Fix broken link in ollama package README

* fix: convert absolute GitHub URLs to relative paths for link checker

Absolute URLs to python/samples/ on main branch 404 until PR merges.
Converted to relative paths that linkspector can verify locally.

* fix: update link for handoff sample moved to orchestrations/

* fix: update chatkit-integration README path from demos/ to 05-end-to-end/

* fix: update broken links in orchestrations README to match flat directory structure
This commit is contained in:
Eduard van Valkenburg
2026-02-12 18:36:36 +01:00
committed by GitHub
Unverified
parent 69dcfe31ee
commit a2856d3b92
536 changed files with 3816 additions and 1632 deletions
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
"""
This sample demonstrates how to configure function invocation settings
for an client and use a simple tool as a tool in an agent.
This behavior is the same for all chat client types.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def add(
x: Annotated[int, "First number"],
y: Annotated[int, "Second number"],
) -> str:
return f"{x} + {y} = {x + y}"
async def main():
client = OpenAIResponsesClient()
client.function_invocation_configuration["include_detailed_errors"] = True
client.function_invocation_configuration["max_iterations"] = 40
print(f"Function invocation configured as: \n{client.function_invocation_configuration}")
agent = client.as_agent(name="ToolAgent", instructions="Use the provided tools.", tools=add)
print("=" * 60)
print("Call add(239847293, 29834)")
query = "Add 239847293 and 29834"
response = await agent.run(query)
print(f"Response: {response.text}")
"""
Expected Output:
============================================================
Function invocation configured as:
{
"type": "function_invocation_configuration",
"enabled": true,
"max_iterations": 40,
"max_consecutive_errors_per_request": 3,
"terminate_on_unknown_calls": false,
"additional_tools": [],
"include_detailed_errors": true
}
============================================================
Call add(239847293, 29834)
Response: 239,877,127
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import FunctionTool
from agent_framework.openai import OpenAIResponsesClient
"""
Example of how to create a function that only consists of a declaration without an implementation.
This is useful when you want the agent to use tools that are defined elsewhere or when you want
to test the agent's ability to reason about tool usage without executing them.
The only difference is that you provide a FunctionTool without a function.
If you need a input_model, you can still provide that as well.
"""
async def main():
function_declaration = FunctionTool(
name="get_current_time",
description="Get the current time in ISO 8601 format.",
)
agent = OpenAIResponsesClient().as_agent(
name="DeclarationOnlyToolAgent",
instructions="You are a helpful agent that uses tools.",
tools=function_declaration,
)
query = "What is the current time?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Result: {result.to_json(indent=2)}\n")
"""
Expected result:
User: What is the current time?
Result: {
"type": "agent_response",
"messages": [
{
"type": "chat_message",
"role": {
"type": "role",
"value": "assistant"
},
"contents": [
{
"type": "function_call",
"call_id": "call_0flN9rfGLK8LhORy4uMDiRSC",
"name": "get_current_time",
"arguments": "{}",
"fc_id": "fc_0fd5f269955c589f016904c46584348195b84a8736e61248de"
}
],
"author_name": "DeclarationOnlyToolAgent",
"additional_properties": {}
}
],
"response_id": "resp_0fd5f269955c589f016904c462d5cc819599d28384ba067edc",
"created_at": "2025-10-31T15:14:58.000000Z",
"usage_details": {
"type": "usage_details",
"input_token_count": 63,
"output_token_count": 145,
"total_token_count": 208,
"openai.reasoning_tokens": 128
},
"additional_properties": {}
}
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,68 @@
# Copyright (c) Microsoft. All rights reserved.
# type: ignore
"""
Local Tool with Dependency Injection Example
This example demonstrates how to create a FunctionTool using the agent framework's
dependency injection system. Instead of providing the function at initialization time,
the actual callable function is injected during deserialization from a dictionary definition.
Note:
The serialization and deserialization feature used in this example is currently
in active development. The API may change in future versions as we continue
to improve and extend its functionality. Please refer to the latest documentation
for any updates to the dependency injection patterns.
Usage:
Run this script to see how a FunctionTool can be created from a dictionary
definition with the function injected at runtime. The agent will use this tool
to perform arithmetic operations.
"""
import asyncio
from agent_framework import FunctionTool
from agent_framework.openai import OpenAIResponsesClient
definition = {
"type": "function_tool",
"name": "add_numbers",
"description": "Add two numbers together.",
"input_model": {
"properties": {
"a": {"description": "The first number", "type": "integer"},
"b": {"description": "The second number", "type": "integer"},
},
"required": ["a", "b"],
"title": "func_input",
"type": "object",
},
}
async def main() -> None:
"""Main function demonstrating creating a tool with an injected function."""
def func(a, b) -> int:
"""Add two numbers together."""
return a + b
# Create the FunctionTool using dependency injection
# The 'definition' dictionary contains the serialized tool configuration,
# while the actual function implementation is provided via dependencies.
#
# Dependency structure: {"function_tool": {"name:add_numbers": {"func": func}}}
# - "function_tool": matches the tool type identifier
# - "name:add_numbers": instance-specific injection targeting tools with name="add_numbers"
# - "func": the parameter name that will receive the injected function
tool = FunctionTool.from_dict(definition, dependencies={"function_tool": {"name:add_numbers": {"func": func}}})
agent = OpenAIResponsesClient().as_agent(
name="FunctionToolAgent", instructions="You are a helpful assistant.", tools=tool
)
response = await agent.run("What is 5 + 3?")
print(f"Response: {response.text}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,106 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
"""
Tool exceptions handled by returning the error for the agent to recover from.
Shows how a tool that throws an exception creates gracefull recovery and can keep going.
The LLM decides whether to retry the call or to respond with something else, based on the exception.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def greet(name: Annotated[str, "Name to greet"]) -> str:
"""Greet someone."""
return f"Hello, {name}!"
# we trick the AI into calling this function with 0 as denominator to trigger the exception
@tool(approval_mode="never_require")
def safe_divide(
a: Annotated[int, "Numerator"],
b: Annotated[int, "Denominator"],
) -> str:
"""Divide two numbers can be used with 0 as denominator."""
try:
result = a / b # Will raise ZeroDivisionError
except ZeroDivisionError as exc:
print(f" Tool failed: with error: {exc}")
raise
return f"{a} / {b} = {result}"
async def main():
# tools = Tools()
agent = OpenAIResponsesClient().as_agent(
name="ToolAgent",
instructions="Use the provided tools.",
tools=[greet, safe_divide],
)
thread = agent.get_new_thread()
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool raises exception")
response = await agent.run("Divide 10 by 0", thread=thread)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call greet('Bob') - conversation can keep going.")
response = await agent.run("Greet Bob", thread=thread)
print(f"Response: {response.text}")
print("=" * 60)
print("Replay the conversation:")
assert thread.message_store
assert thread.message_store.list_messages
for idx, msg in enumerate(await thread.message_store.list_messages()):
if msg.text:
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
for content in msg.contents:
if content.type == "function_call":
print(
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
)
if content.type == "function_result":
print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
"""
Expected Output:
============================================================
Step 1: Call divide(10, 0) - tool raises exception
Tool failed: with error: division by zero
Response: Division by zero is undefined in standard arithmetic, so 10 ÷ 0 has no meaning.
If youre curious about limits: as x approaches 0 from the positive side, 10/x tends to +∞; from the negative side,
10/x tends to -∞.
If you want a finite result, try dividing by a nonzero number, e.g., 10 ÷ 2 = 5 or 10 ÷ 0.1 = 100. Want me to compute
something else?
============================================================
Step 2: Call greet('Bob') - conversation can keep going.
Response: Hello, Bob!
============================================================
Replay the conversation:
1 user: Divide 10 by 0
2 ToolAgent: calling function: safe_divide with arguments: {"a":10,"b":0}
3 tool: division by zero
4 ToolAgent: Division by zero is undefined in standard arithmetic, so 10 ÷ 0 has no meaning.
If youre curious about limits: as x approaches 0 from the positive side, 10/x tends to +∞; from the negative side,
10/x tends to -∞.
If you want a finite result, try dividing by a nonzero number, e.g., 10 ÷ 2 = 5 or 10 ÷ 0.1 = 100. Want me to compute
something else?
5 user: Greet Bob
6 ToolAgent: calling function: greet with arguments: {"name":"Bob"}
7 tool: Hello, Bob!
8 ToolAgent: Hello, Bob!
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,156 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randrange
from typing import TYPE_CHECKING, Annotated, Any
from agent_framework import Agent, AgentResponse, Message, tool
from agent_framework.openai import OpenAIResponsesClient
if TYPE_CHECKING:
from agent_framework import SupportsAgentRun
"""
Demonstration of a tool with approvals.
This sample demonstrates using AI functions with user approval workflows.
It shows how to handle function call approvals without using threads.
"""
conditions = ["sunny", "cloudy", "raining", "snowing", "clear"]
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(location: Annotated[str, "The city and state, e.g. San Francisco, CA"]) -> str:
"""Get the current weather for a given location."""
# Simulate weather data
return f"The weather in {location} is {conditions[randrange(0, len(conditions))]} and {randrange(-10, 30)}°C."
# Define a simple weather tool that requires approval
@tool(approval_mode="always_require")
def get_weather_detail(location: Annotated[str, "The city and state, e.g. San Francisco, CA"]) -> str:
"""Get the current weather for a given location."""
# Simulate weather data
return (
f"The weather in {location} is {conditions[randrange(0, len(conditions))]} and {randrange(-10, 30)}°C, "
"with a humidity of 88%. "
f"Tomorrow will be {conditions[randrange(0, len(conditions))]} with a high of {randrange(-10, 30)}°C."
)
async def handle_approvals(query: str, agent: "SupportsAgentRun") -> AgentResponse:
"""Handle function call approvals.
When we don't have a thread, we need to ensure we include the original query,
the approval request, and the approval response in each iteration.
"""
result = await agent.run(query)
while len(result.user_input_requests) > 0:
# Start with the original query
new_inputs: list[Any] = [query]
for user_input_needed in result.user_input_requests:
print(
f"\nUser Input Request for function from {agent.name}:"
f"\n Function: {user_input_needed.function_call.name}"
f"\n Arguments: {user_input_needed.function_call.arguments}"
)
# Add the assistant message with the approval request
new_inputs.append(Message("assistant", [user_input_needed]))
# Get user approval
user_approval = await asyncio.to_thread(input, "\nApprove function call? (y/n): ")
# Add the user's approval response
new_inputs.append(
Message("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")])
)
# Run again with all the context
result = await agent.run(new_inputs)
return result
async def handle_approvals_streaming(query: str, agent: "SupportsAgentRun") -> None:
"""Handle function call approvals with streaming responses.
When we don't have a thread, we need to ensure we include the original query,
the approval request, and the approval response in each iteration.
"""
current_input: str | list[Any] = query
has_user_input_requests = True
while has_user_input_requests:
has_user_input_requests = False
user_input_requests: list[Any] = []
# Stream the response
async for chunk in agent.run(current_input, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
# Collect user input requests from the stream
if chunk.user_input_requests:
user_input_requests.extend(chunk.user_input_requests)
if user_input_requests:
has_user_input_requests = True
# Start with the original query
new_inputs: list[Any] = [query]
for user_input_needed in user_input_requests:
print(
f"\n\nUser Input Request for function from {agent.name}:"
f"\n Function: {user_input_needed.function_call.name}"
f"\n Arguments: {user_input_needed.function_call.arguments}"
)
# Add the assistant message with the approval request
new_inputs.append(Message("assistant", [user_input_needed]))
# Get user approval
user_approval = await asyncio.to_thread(input, "\nApprove function call? (y/n): ")
# Add the user's approval response
new_inputs.append(
Message("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")])
)
# Update input with all the context for next iteration
current_input = new_inputs
async def run_weather_agent_with_approval(stream: bool) -> None:
"""Example showing AI function with approval requirement."""
print(f"\n=== Weather Agent with Approval Required ({'Streaming' if stream else 'Non-Streaming'}) ===\n")
async with Agent(
client=OpenAIResponsesClient(),
name="WeatherAgent",
instructions=("You are a helpful weather assistant. Use the get_weather tool to provide weather information."),
tools=[get_weather, get_weather_detail],
) as agent:
query = "Can you give me an update of the weather in LA and Portland and detailed weather for Seattle?"
print(f"User: {query}")
if stream:
print(f"\n{agent.name}: ", end="", flush=True)
await handle_approvals_streaming(query, agent)
print()
else:
result = await handle_approvals(query, agent)
print(f"\n{agent.name}: {result}\n")
async def main() -> None:
print("=== Demonstration of a tool with approvals ===\n")
await run_weather_agent_with_approval(stream=False)
await run_weather_agent_with_approval(stream=True)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,102 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, Message, tool
from agent_framework.azure import AzureOpenAIChatClient
"""
Tool Approvals with Threads
This sample demonstrates using tool approvals with threads.
With threads, you don't need to manually pass previous messages -
the thread stores and retrieves them automatically.
"""
@tool(approval_mode="always_require")
def add_to_calendar(
event_name: Annotated[str, "Name of the event"], date: Annotated[str, "Date of the event"]
) -> str:
"""Add an event to the calendar (requires approval)."""
print(f">>> EXECUTING: add_to_calendar(event_name='{event_name}', date='{date}')")
return f"Added '{event_name}' to calendar on {date}"
async def approval_example() -> None:
"""Example showing approval with threads."""
print("=== Tool Approval with Thread ===\n")
agent = Agent(
client=AzureOpenAIChatClient(),
name="CalendarAgent",
instructions="You are a helpful calendar assistant.",
tools=[add_to_calendar],
)
thread = agent.get_new_thread()
# Step 1: Agent requests to call the tool
query = "Add a dentist appointment on March 15th"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
# Check for approval requests
if result.user_input_requests:
for request in result.user_input_requests:
print("\nApproval needed:")
print(f" Function: {request.function_call.name}")
print(f" Arguments: {request.function_call.arguments}")
# User approves (in real app, this would be user input)
approved = True # Change to False to see rejection
print(f" Decision: {'Approved' if approved else 'Rejected'}")
# Step 2: Send approval response
approval_response = request.to_function_approval_response(approved=approved)
result = await agent.run(Message("user", [approval_response]), thread=thread)
print(f"Agent: {result}\n")
async def rejection_example() -> None:
"""Example showing rejection with threads."""
print("=== Tool Rejection with Thread ===\n")
agent = Agent(
client=AzureOpenAIChatClient(),
name="CalendarAgent",
instructions="You are a helpful calendar assistant.",
tools=[add_to_calendar],
)
thread = agent.get_new_thread()
query = "Add a team meeting on December 20th"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
if result.user_input_requests:
for request in result.user_input_requests:
print("\nApproval needed:")
print(f" Function: {request.function_call.name}")
print(f" Arguments: {request.function_call.arguments}")
# User rejects
print(" Decision: Rejected")
# Send rejection response
rejection_response = request.to_function_approval_response(approved=False)
result = await agent.run(Message("user", [rejection_response]), thread=thread)
print(f"Agent: {result}\n")
async def main() -> None:
await approval_example()
await rejection_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,81 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Function Tool with Explicit Schema Example
This example demonstrates how to provide an explicit schema to the @tool decorator
using the `schema` parameter, bypassing the automatic inference from the function
signature. This is useful when you want full control over the tool's parameter
schema that the AI model sees, or when the function signature does not accurately
represent the desired schema.
Two approaches are shown:
1. Using a Pydantic BaseModel subclass as the schema
2. Using a raw JSON schema dictionary as the schema
"""
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
from pydantic import BaseModel, Field
# Approach 1: Pydantic model as explicit schema
class WeatherInput(BaseModel):
"""Input schema for the weather tool."""
location: Annotated[str, Field(description="The city name to get weather for")]
unit: Annotated[str, Field(description="Temperature unit: celsius or fahrenheit")] = "celsius"
@tool(
name="get_weather",
description="Get the current weather for a given location.",
schema=WeatherInput,
approval_mode="never_require",
)
def get_weather(location: str, unit: str = "celsius") -> str:
"""Get the current weather for a location."""
return f"The weather in {location} is 22 degrees {unit}."
# Approach 2: JSON schema dictionary as explicit schema
get_current_time_schema = {
"type": "object",
"properties": {
"timezone": {"type": "string", "description": "The timezone to get the current time for", "default": "UTC"},
},
}
@tool(
name="get_current_time",
description="Get the current time in a given timezone.",
schema=get_current_time_schema,
approval_mode="never_require",
)
def get_current_time(timezone: str = "UTC") -> str:
"""Get the current time."""
from datetime import datetime
from zoneinfo import ZoneInfo
return f"The current time in {timezone} is {datetime.now(ZoneInfo(timezone)).isoformat()}"
async def main():
agent = OpenAIResponsesClient().as_agent(
name="AssistantAgent",
instructions="You are a helpful assistant. Use the available tools to answer questions.",
tools=[get_weather, get_current_time],
)
query = "What is the weather in Seattle and what time is it?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Result: {result.text}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,54 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated, Any
from agent_framework import tool
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
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
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().as_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,188 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
"""
Some tools are very expensive to run, so you may want to limit the number of times
it tries to call them and fails. This sample shows a tool that can only raise exceptions a
limited number of times.
"""
# we trick the AI into calling this function with 0 as denominator to trigger the exception
@tool(max_invocation_exceptions=1)
def safe_divide(
a: Annotated[int, "Numerator"],
b: Annotated[int, "Denominator"],
) -> str:
"""Divide two numbers can be used with 0 as denominator."""
try:
result = a / b # Will raise ZeroDivisionError
except ZeroDivisionError as exc:
print(f" Tool failed with error: {exc}")
raise
return f"{a} / {b} = {result}"
async def main():
# tools = Tools()
agent = OpenAIResponsesClient().as_agent(
name="ToolAgent",
instructions="Use the provided tools.",
tools=[safe_divide],
)
thread = agent.get_new_thread()
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool raises exception")
response = await agent.run("Divide 10 by 0", thread=thread)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call divide(100, 0) - will refuse to execute due to max_invocation_exceptions")
response = await agent.run("Divide 100 by 0", thread=thread)
print(f"Response: {response.text}")
print("=" * 60)
print(f"Number of tool calls attempted: {safe_divide.invocation_count}")
print(f"Number of tool calls failed: {safe_divide.invocation_exception_count}")
print("Replay the conversation:")
assert thread.message_store
assert thread.message_store.list_messages
for idx, msg in enumerate(await thread.message_store.list_messages()):
if msg.text:
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
for content in msg.contents:
if content.type == "function_call":
print(
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
)
if content.type == "function_result":
print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
"""
Expected Output:
============================================================
Step 1: Call divide(10, 0) - tool raises exception
Tool failed with error: division by zero
[2025-10-31 15:39:53 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
Function failed. Error: division by zero
Response: Division by zero is undefined in standard arithmetic. There is no finite value for 10 ÷ 0.
If you want alternatives:
- A valid example: 10 ÷ 2 = 5.
- To handle safely in code, you can check the denominator first (e.g., in Python: if b == 0:
handle error else: compute a/b).
- If youre curious about limits: as x → 0+, 10/x → +∞; as x → 0, 10/x → −∞; there is no finite limit.
Would you like me to show a safe division snippet in a specific language, or compute something else?
============================================================
Step 2: Call divide(100, 0) - will refuse to execute due to max_invocations
[2025-10-31 15:40:09 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
Function failed. Error: Function 'safe_divide' has reached its maximum exception limit, you tried to use this
tool too many times and it kept failing.
Response: Division by zero is undefined in standard arithmetic, so 100 ÷ 0 has no finite value.
If youre coding and want safe handling, here are quick patterns in a few languages:
- Python
def safe_divide(a, b):
if b == 0:
return None # or raise an exception
return a / b
safe_divide(100, 0) # -> None
- JavaScript
function safeDivide(a, b) {
if (b === 0) return undefined; // or throw
return a / b;
}
safeDivide(100, 0) // -> undefined
- Java
public static Double safeDivide(double a, double b) {
if (b == 0.0) throw new ArithmeticException("Divide by zero");
return a / b;
}
safeDivide(100, 0) // -> exception
- C/C++
double safeDivide(double a, double b) {
if (b == 0.0) return std::numeric_limits<double>::infinity(); // or handle error
return a / b;
}
Note: In many languages, dividing by zero with floating-point numbers yields Infinity (or -Infinity) or NaN,
but integer division typically raises an error.
Would you like a snippet in a specific language or to see a math explanation (limits) for what happens as the
divisor approaches zero?
============================================================
Number of tool calls attempted: 1
Number of tool calls failed: 1
Replay the conversation:
1 user: Divide 10 by 0
2 ToolAgent: calling function: safe_divide with arguments: {"a":10,"b":0}
3 tool: division by zero
4 ToolAgent: Division by zero is undefined in standard arithmetic. There is no finite value for 10 ÷ 0.
If you want alternatives:
- A valid example: 10 ÷ 2 = 5.
- To handle safely in code, you can check the denominator first (e.g., in Python: if b == 0:
handle error else: compute a/b).
- If youre curious about limits: as x → 0+, 10/x → +∞; as x → 0, 10/x → −∞; there is no finite limit.
Would you like me to show a safe division snippet in a specific language, or compute something else?
5 user: Divide 100 by 0
6 ToolAgent: calling function: safe_divide with arguments: {"a":100,"b":0}
7 tool: Function 'safe_divide' has reached its maximum exception limit, you tried to use this tool too many times
and it kept failing.
8 ToolAgent: Division by zero is undefined in standard arithmetic, so 100 ÷ 0 has no finite value.
If youre coding and want safe handling, here are quick patterns in a few languages:
- Python
def safe_divide(a, b):
if b == 0:
return None # or raise an exception
return a / b
safe_divide(100, 0) # -> None
- JavaScript
function safeDivide(a, b) {
if (b === 0) return undefined; // or throw
return a / b;
}
safeDivide(100, 0) // -> undefined
- Java
public static Double safeDivide(double a, double b) {
if (b == 0.0) throw new ArithmeticException("Divide by zero");
return a / b;
}
safeDivide(100, 0) // -> exception
- C/C++
double safeDivide(double a, double b) {
if (b == 0.0) return std::numeric_limits<double>::infinity(); // or handle error
return a / b;
}
Note: In many languages, dividing by zero with floating-point numbers yields Infinity (or -Infinity) or NaN,
but integer division typically raises an error.
Would you like a snippet in a specific language or to see a math explanation (limits) for what happens as the
divisor approaches zero?
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
"""
For tools you can specify if there is a maximum number of invocations allowed.
This sample shows a tool that can only be invoked once.
"""
@tool(max_invocations=1)
def unicorn_function(times: Annotated[int, "The number of unicorns to return."]) -> str:
"""This function returns precious unicorns!"""
return f"{'🦄' * times}"
async def main():
# tools = Tools()
agent = OpenAIResponsesClient().as_agent(
name="ToolAgent",
instructions="Use the provided tools.",
tools=[unicorn_function],
)
thread = agent.get_new_thread()
print("=" * 60)
print("Step 1: Call unicorn_function")
response = await agent.run("Call 5 unicorns!", thread=thread)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call unicorn_function again - will refuse to execute due to max_invocations")
response = await agent.run("Call 10 unicorns and use the function to do it.", thread=thread)
print(f"Response: {response.text}")
print("=" * 60)
print(f"Number of tool calls attempted: {unicorn_function.invocation_count}")
print(f"Number of tool calls failed: {unicorn_function.invocation_exception_count}")
print("Replay the conversation:")
assert thread.message_store
assert thread.message_store.list_messages
for idx, msg in enumerate(await thread.message_store.list_messages()):
if msg.text:
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
for content in msg.contents:
if content.type == "function_call":
print(
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
)
if content.type == "function_result":
print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
"""
Expected Output:
============================================================
Step 1: Call unicorn_function
Response: Five unicorns summoned: 🦄🦄🦄🦄🦄✨
============================================================
Step 2: Call unicorn_function again - will refuse to execute due to max_invocations
[2025-10-31 15:54:40 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
Function failed. Error: Function 'unicorn_function' has reached its maximum invocation limit,
you can no longer use this tool.
Response: The unicorn function has reached its maximum invocation limit. I cant call it again right now.
Here are 10 unicorns manually: 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄
Would you like me to try again later, or generate something else?
============================================================
Number of tool calls attempted: 1
Number of tool calls failed: 0
Replay the conversation:
1 user: Call 5 unicorns!
2 ToolAgent: calling function: unicorn_function with arguments: {"times":5}
3 tool: 🦄🦄🦄🦄🦄✨
4 ToolAgent: Five unicorns summoned: 🦄🦄🦄🦄🦄✨
5 user: Call 10 unicorns and use the function to do it.
6 ToolAgent: calling function: unicorn_function with arguments: {"times":10}
7 tool: Function 'unicorn_function' has reached its maximum invocation limit, you can no longer use this tool.
8 ToolAgent: The unicorn function has reached its maximum invocation limit. I cant call it again right now.
Here are 10 unicorns manually: 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄 🦄
Would you like me to try again later, or generate something else?
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated, Any
from agent_framework import AgentThread, tool
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
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
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().as_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())
@@ -0,0 +1,100 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
"""
This sample demonstrates using tool within a class,
showing how to manage state within the class that affects tool behavior.
And how to use tool-decorated methods as tools in an agent in order to adjust the behavior of a tool.
"""
class MyFunctionClass:
def __init__(self, safe: bool = False) -> None:
"""Simple class with two tools: divide and add.
The safe parameter controls whether divide raises on division by zero or returns `infinity` for divide by zero.
"""
self.safe = safe
def divide(
self,
a: Annotated[int, "Numerator"],
b: Annotated[int, "Denominator"],
) -> str:
"""Divide two numbers, safe to use also with 0 as denominator."""
result = "" if b == 0 and self.safe else a / b
return f"{a} / {b} = {result}"
def add(
self,
x: Annotated[int, "First number"],
y: Annotated[int, "Second number"],
) -> str:
return f"{x} + {y} = {x + y}"
async def main():
# Creating my function class with safe division enabled
tools = MyFunctionClass(safe=True)
# Applying the tool decorator to one of the methods of the class
add_function = tool(description="Add two numbers.")(tools.add)
agent = OpenAIResponsesClient().as_agent(
name="ToolAgent",
instructions="Use the provided tools.",
)
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool returns infinity")
query = "Divide 10 by 0"
response = await agent.run(
query,
tools=[add_function, tools.divide],
)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call set safe to False and call again")
# Disabling safe mode to allow exceptions
tools.safe = False
response = await agent.run(query, tools=[add_function, tools.divide])
print(f"Response: {response.text}")
print("=" * 60)
"""
Expected Output:
============================================================
Step 1: Call divide(10, 0) - tool returns infinity
Response: Division by zero is undefined in standard arithmetic. There is no real number that equals 10 divided by 0.
- If you look at limits: as x → 0+ (denominator approaches 0 from the positive side), 10/x → +∞; as x → 0, 10/x → −∞.
- Some calculators may display "infinity" or give an error, but that's not a real number.
If you want a numeric surrogate, you can use a small nonzero denominator, e.g., 10/0.001 = 10000. Would you like to
see more on limits or handle it with a tiny epsilon?
============================================================
Step 2: Call set safe to False and call again
[2025-10-31 16:17:44 - /Users/edvan/Work/agent-framework/python/packages/core/agent_framework/_tools.py:718 - ERROR]
Function failed. Error: division by zero
Response: Division by zero is undefined in standard arithmetic. There is no number y such that 0 × y = 10.
If youre looking at limits:
- as x → 0+, 10/x → +∞
- as x → 0, 10/x → −∞
So the limit does not exist.
In programming, dividing by zero usually raises an error or results in special values (e.g., NaN or ∞) depending
on the language.
If you want, tell me what youd like to do instead (e.g., compute 10 divided by 2, or handle division by zero safely
in code), and I can help with examples.
============================================================
"""
if __name__ == "__main__":
asyncio.run(main())