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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
# Tools
Samples that show how to define, configure, and control function tools for an
agent — from basic declarations to approvals, invocation limits, session
injection, and dynamic (progressive) tool exposure.
## Function tools
| File | Demonstrates |
|------|--------------|
| [`function_tool_with_explicit_schema.py`](function_tool_with_explicit_schema.py) | Defining a tool with an explicit JSON schema. |
| [`function_tool_declaration_only.py`](function_tool_declaration_only.py) | A declaration-only tool (schema without a local implementation). |
| [`function_tool_with_kwargs.py`](function_tool_with_kwargs.py) | Passing extra keyword arguments into a tool. |
| [`function_tool_from_dict_with_dependency_injection.py`](function_tool_from_dict_with_dependency_injection.py) | Dependency injection into a tool defined from a dict. |
| [`function_tool_with_session_injection.py`](function_tool_with_session_injection.py) | Injecting the session into a tool. |
| [`tool_in_class.py`](tool_in_class.py) | Using a method on a class as a tool. |
| [`agent_as_tool_with_session_propagation.py`](agent_as_tool_with_session_propagation.py) | Exposing an agent as a tool with session propagation. |
## Approvals & invocation control
| File | Demonstrates |
|------|--------------|
| [`function_tool_with_approval.py`](function_tool_with_approval.py) | Requiring human approval before a tool runs. |
| [`function_tool_with_approval_and_sessions.py`](function_tool_with_approval_and_sessions.py) | Tool approvals combined with sessions. |
| [`tool_approval_middleware.py`](tool_approval_middleware.py) | Session-backed approval coordination, mixed-batch approvals, and "always approve" rules. |
| [`function_invocation_configuration.py`](function_invocation_configuration.py) | Configuring function-invocation settings (e.g. max iterations). |
| [`control_total_tool_executions.py`](control_total_tool_executions.py) | All the ways to cap how many times tools run. |
| [`function_tool_with_max_invocations.py`](function_tool_with_max_invocations.py) | Limiting the number of invocations per tool. |
| [`function_tool_with_max_exceptions.py`](function_tool_with_max_exceptions.py) | Limiting the number of exceptions a tool may raise. |
| [`function_tool_recover_from_failures.py`](function_tool_recover_from_failures.py) | Returning errors so the agent can recover from tool failures. |
## Progressive tool exposure (dynamic loading)
| File | Demonstrates |
|------|--------------|
| [`dynamic_tool_exposure.py`](dynamic_tool_exposure.py) | A "loader" tool that adds more tools at runtime via `FunctionInvocationContext`. |
Frontloading a model with hundreds of tools hurts tool-selection accuracy,
bloats context, and raises cost. Instead, start with a small set of loader
tools and let the model pull in more on demand. Inside a tool, the injected
`ctx: FunctionInvocationContext` exposes a live `ctx.tools` list plus
`ctx.add_tools(...)` / `ctx.remove_tools(...)` helpers. Tools added or removed
take effect on the **next iteration** of the function-calling loop.
> [!NOTE]
> Progressive tool exposure applies to the standard function-calling loop. It
> does **not** apply to CodeAct providers (`agent-framework-monty`,
> `agent-framework-hyperlight`). In CodeAct the model only sees a single
> `execute_code` tool, and host tools are exposed *inside the sandbox* as typed
> Python functions rather than as model tool-schemas. Host tools there are
> invoked without a `FunctionInvocationContext`, so `ctx.add_tools()` is not
> available; the helpers fail fast with a clear `RuntimeError` instead of
> silently doing nothing. To change a CodeAct agent's tool set, use the
> provider's own `add_tools` / `remove_tool` / `clear_tools` methods (applied
> between runs). The recommended provider-driven path for Monty and Hyperlight
> is shown in [`../context_providers/code_act/`](../context_providers/code_act/)
> ([`code_act.py`](../context_providers/code_act/code_act.py) for Hyperlight,
> [`monty_code_act.py`](../context_providers/code_act/monty_code_act.py) for
> Monty).
## Local shell & code interpreters
| Path | Demonstrates |
|------|--------------|
| [`local_shell_with_allowlist.py`](local_shell_with_allowlist.py) | `LocalShellTool` restricted by a strict command allow-list. |
| [`local_shell_with_environment_provider.py`](local_shell_with_environment_provider.py) | `LocalShellTool` wired with a `ShellEnvironmentProvider`. |
| [`local_code_interpreter/`](local_code_interpreter/) | Hyperlight-backed sandboxed code interpreter (standalone tool — *extra* pattern). |
| [`monty_code_interpreter/`](monty_code_interpreter/) | Monty-backed sandboxed code interpreter (standalone tool — *extra* pattern). |
> [!TIP]
> The `local_code_interpreter/` and `monty_code_interpreter/` samples show the
> standalone-tool wiring and are provided as *extra* reference. For most
> Monty/Hyperlight use cases the **recommended** path is the provider-driven
> CodeAct setup in
> [`../context_providers/code_act/`](../context_providers/code_act/), which adds
> dynamic tool / capability management.
@@ -0,0 +1,110 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import Awaitable, Callable
from agent_framework import Agent, AgentContext, AgentSession, FunctionInvocationContext, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
load_dotenv()
"""
Agent-as-Tool: Session Propagation Example
Demonstrates how to share an AgentSession between a coordinator agent and a
sub-agent invoked as a tool using ``propagate_session=True``.
When session propagation is enabled, both agents share the same session object,
including session_id and the mutable state dict. This allows correlated
conversation tracking and shared state across the agent hierarchy.
"""
async def log_session(
context: AgentContext,
call_next: Callable[[], Awaitable[None]],
) -> None:
"""Agent middleware that logs the session received by each agent."""
session: AgentSession | None = context.session
if not session:
print("No session found.")
await call_next()
return
agent_name = context.agent.name or "unknown"
print(
f" [{agent_name}] session_id={session.session_id}, "
f"service_session_id={session.service_session_id} state={session.state}"
)
await call_next()
@tool(description="Use this tool to store the findings so that other agents can reason over them.")
def store_findings(findings: str, ctx: FunctionInvocationContext) -> None:
if ctx.session is None:
return
current_findings = ctx.session.state.get("findings")
if current_findings is None:
ctx.session.state["findings"] = findings
else:
ctx.session.state["findings"] = f"{current_findings}\n{findings}"
@tool(description="Use this tool to gather the current findings from other agents.")
def recall_findings(ctx: FunctionInvocationContext) -> str:
if ctx.session is None:
return "No session available"
current_findings = ctx.session.state.get("findings")
if current_findings is None:
return "Nothing yet"
return current_findings
async def main() -> None:
print("=== Agent-as-Tool: Session Propagation ===\n")
client = OpenAIChatClient()
research_agent = Agent(
client=client,
name="ResearchAgent",
instructions="You are a research assistant. Provide concise answers and store your findings.",
middleware=[log_session],
tools=[store_findings, recall_findings],
)
research_tool = research_agent.as_tool(
name="research",
description="Research a topic and store your findings.",
arg_name="query",
arg_description="The research query",
propagate_session=True,
)
coordinator = Agent(
client=client,
name="CoordinatorAgent",
instructions=(
"You coordinate research. Use the 'research' tool to start research "
"and then use the recall findings tool to gather up everything."
),
tools=[research_tool, store_findings, recall_findings],
middleware=[log_session],
)
session = coordinator.create_session()
session.state["findings"] = None
print(f"Session ID: {session.session_id}")
print(f"Session state before run: {session.state}\n")
query = "What are the latest developments in quantum computing and in AI?"
print(f"User: {query}\n")
result = await coordinator.run(query, session=session)
print(f"\nCoordinator: {result}\n")
print(f"Session state after run: {session.state}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,354 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
This sample demonstrates all the ways to control how many times tools are
executed during an agent run. There are three complementary mechanisms:
1. ``max_iterations`` (on the chat client) — caps the number of **LLM
roundtrips**. Each roundtrip may invoke one or more tools in parallel.
2. ``max_function_calls`` (on the chat client) — caps the **total number of
individual function invocations** across all iterations within a single
request. This is the primary knob for cost control. If the tool is called multiple
times in one iteration, those will execute, after that it will stop working. For example,
if max_invocations is 3 and the tool is called 5 times in a single iteration,
these will complete, but any subsequent calls to the tool (in the same or future iterations)
will raise a ToolException.
3. ``max_invocations`` (on a tool) — caps the **lifetime invocation count**
of a specific tool instance. The counter is never automatically reset,
so it accumulates across requests when tools are singletons.
Because ``max_invocations`` is tracked on the ``FunctionTool`` *instance*,
wrapping the same callable with ``@tool`` multiple times creates independent
counters. This lets you give different agents different invocation budgets
for the same underlying function.
Choose the right mechanism for your scenario:
• Prevent runaway LLM loops → ``max_iterations``
• Best-effort cap on tool execution cost per request → ``max_function_calls``
(checked between iterations; a single batch of parallel calls may overshoot)
• Best-effort limit a specific expensive tool globally → ``max_invocations``
• Per-agent limits on shared tools → wrap the callable separately per agent
"""
# --- Tool definitions ---
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see function_tool_with_approval.py.
@tool(approval_mode="never_require")
def search_web(query: Annotated[str, "The search query to look up."]) -> str:
"""Search the web for information."""
return f"Results for '{query}': [page1, page2, page3]"
@tool(approval_mode="never_require")
def get_weather(city: Annotated[str, "The city to get the weather for."]) -> str:
"""Get the current weather for a city."""
return f"Weather in {city}: Sunny, 22°C"
@tool(approval_mode="never_require", max_invocations=2)
def call_expensive_api(
prompt: Annotated[str, "The prompt to send to the expensive API."],
) -> str:
"""Call a very expensive external API. Limited to 2 calls ever."""
return f"Expensive result for '{prompt}'"
# --- Scenario 1: max_iterations (limit LLM roundtrips) ---
async def scenario_max_iterations():
"""Demonstrate max_iterations: limits how many times we loop back to the LLM.
Each iteration may invoke one or more tools in parallel, so this does NOT
directly limit the total number of function executions.
"""
print("=" * 60)
print("Scenario 1: max_iterations — limit LLM roundtrips")
print("=" * 60)
client = OpenAIChatClient()
# 1. Set max_iterations to 3 — the tool loop will run at most 3 roundtrips
# to the model before forcing a text response.
client.function_invocation_configuration["max_iterations"] = 3
print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}")
agent = Agent(
client=client,
name="ResearchAgent",
instructions=(
"You are a research assistant. Use the search_web tool to answer "
"the user's question. Search for multiple aspects of the topic."
),
tools=[search_web, get_weather],
)
response = await agent.run("Tell me about the weather in Paris, London, and Tokyo.")
print(f" Response: {response.text[:200]}...")
print()
# --- Scenario 2: max_function_calls (limit total tool executions per request) ---
async def scenario_max_function_calls():
"""Demonstrate max_function_calls: caps total individual tool invocations.
Unlike max_iterations, this counts every individual function execution —
even when several tools run in parallel within a single iteration.
"""
print("=" * 60)
print("Scenario 2: max_function_calls — limit total tool executions")
print("=" * 60)
client = OpenAIChatClient()
# 1. Allow many iterations but cap total function calls to 4.
# If the model requests 3 parallel searches per iteration, after 2
# iterations (6 calls) the limit is hit and the loop stops.
client.function_invocation_configuration["max_iterations"] = 20
client.function_invocation_configuration["max_function_calls"] = 4
print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}")
print(f" max_function_calls = {client.function_invocation_configuration['max_function_calls']}")
agent = Agent(
client=client,
name="ResearchAgent",
instructions=(
"You are a research assistant. Use the search_web and get_weather "
"tools to answer the user's question comprehensively."
),
tools=[search_web, get_weather],
)
response = await agent.run(
"Search for the weather in Paris, London, Tokyo, New York, and Sydney, and also search for best travel tips."
)
print(f" Response: {response.text[:200]}...")
print()
# --- Scenario 3: max_invocations (lifetime limit on a specific tool) ---
async def scenario_max_invocations():
"""Demonstrate max_invocations: caps how many times a specific tool instance
can be called across ALL requests.
Note: this counter lives on the tool instance, so for module-level tools
it accumulates globally. Use tool.invocation_count to inspect or reset.
"""
print("=" * 60)
print("Scenario 3: max_invocations — lifetime cap on a tool")
print("=" * 60)
agent = Agent(
client=OpenAIChatClient(),
name="APIAgent",
instructions="Use call_expensive_api when asked to analyze something.",
tools=[call_expensive_api],
)
session = agent.create_session()
# 1. First call — succeeds (invocation_count: 0 → 1)
print(f" Before call 1: invocation_count = {call_expensive_api.invocation_count}")
response = await agent.run("Analyze the market trends for AI.", session=session)
print(f" After call 1: invocation_count = {call_expensive_api.invocation_count}")
print(f" Response: {response.text[:150]}...")
# 2. Second call — succeeds (invocation_count: 1 → 2)
response = await agent.run("Analyze the market trends for cloud computing.", session=session)
print(f" After call 2: invocation_count = {call_expensive_api.invocation_count}")
print(f" Response: {response.text[:150]}...")
# 3. Third call — tool refuses (max_invocations=2 reached)
response = await agent.run("Analyze the market trends for quantum computing.", session=session)
print(f" After call 3: invocation_count = {call_expensive_api.invocation_count}")
print(f" Response: {response.text[:150]}...")
# 4. Reset the counter to allow more calls
print()
print(" Resetting invocation_count to 0...")
call_expensive_api.invocation_count = 0
print(f" invocation_count = {call_expensive_api.invocation_count}")
print()
# --- Scenario 4: Per-agent limits via separate tool wrappers ---
async def scenario_per_agent_tool_limits():
"""Demonstrate per-agent max_invocations using separate tool wrappers.
Because max_invocations is tracked on the FunctionTool *instance*, you can
wrap the same callable with ``@tool`` multiple times to get independent
counters for different agents. This is useful when two agents share the
same underlying function but should have different invocation budgets.
"""
print("=" * 60)
print("Scenario 4: Per-agent limits via separate tool wrappers")
print("=" * 60)
# The underlying callable — a plain function, no decorator.
def _do_lookup(query: Annotated[str, "Search query."]) -> str:
"""Look up information."""
return f"Lookup result for '{query}'"
# Wrap it twice with different limits. Each wrapper is a separate
# FunctionTool instance with its own invocation_count.
agent_a_lookup = tool(name="lookup", approval_mode="never_require", max_invocations=2)(_do_lookup)
agent_b_lookup = tool(name="lookup", approval_mode="never_require", max_invocations=5)(_do_lookup)
client = OpenAIChatClient()
agent_a = Agent(
client=client,
name="AgentA",
instructions="Use the lookup tool to answer questions.",
tools=[agent_a_lookup],
)
agent_b = Agent(
client=client,
name="AgentB",
instructions="Use the lookup tool to answer questions.",
tools=[agent_b_lookup],
)
print(f" agent_a_lookup.max_invocations = {agent_a_lookup.max_invocations}")
print(f" agent_b_lookup.max_invocations = {agent_b_lookup.max_invocations}")
# Agent A uses its budget
session_a = agent_a.create_session()
await agent_a.run("Look up AI trends", session=session_a)
await agent_a.run("Look up cloud trends", session=session_a)
# Agent B's counter is independent — still at 0
session_b = agent_b.create_session()
await agent_b.run("Look up quantum computing", session=session_b)
print(
f" agent_a_lookup.invocation_count = {agent_a_lookup.invocation_count} (limit {agent_a_lookup.max_invocations})"
)
print(
f" agent_b_lookup.invocation_count = {agent_b_lookup.invocation_count} (limit {agent_b_lookup.max_invocations})"
)
print(" → Agent A hit its limit; Agent B used 1 of 5.")
print()
# --- Scenario 5: Combining all three mechanisms ---
async def scenario_combined():
"""Demonstrate using all three mechanisms together for defense in depth."""
print("=" * 60)
print("Scenario 5: Combined — all mechanisms together")
print("=" * 60)
client = OpenAIChatClient()
# 1. Configure the client with both iteration and function call limits.
client.function_invocation_configuration["max_iterations"] = 5 # max 5 LLM roundtrips
client.function_invocation_configuration["max_function_calls"] = 8 # max 8 total tool calls
print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}")
print(f" max_function_calls = {client.function_invocation_configuration['max_function_calls']}")
# 2. Use a tool with a lifetime invocation limit.
@tool(approval_mode="never_require", max_invocations=3)
def premium_lookup(topic: Annotated[str, "Topic to look up."]) -> str:
"""Look up premium data (max 3 calls ever)."""
return f"Premium data for '{topic}'"
print(f" premium_lookup.max_invocations = {premium_lookup.max_invocations}")
agent = Agent(
client=client,
name="MultiToolAgent",
instructions="Use all available tools to answer comprehensively.",
tools=[search_web, get_weather, premium_lookup],
)
# 3. Run a query that could trigger many tool calls.
response = await agent.run(
"Research the weather and tourism info for Paris, London, Tokyo, "
"New York, and Sydney. Use premium_lookup for the top 3 cities."
)
print(f" Response: {response.text[:200]}...")
print(f" premium_lookup.invocation_count = {premium_lookup.invocation_count}")
print()
# --- Entry point ---
async def main():
await scenario_max_iterations()
await scenario_max_function_calls()
await scenario_max_invocations()
await scenario_per_agent_tool_limits()
await scenario_combined()
"""
Sample output:
============================================================
Scenario 1: max_iterations — limit LLM roundtrips
============================================================
max_iterations = 3
Response: The weather in Paris is sunny at 22°C, London is sunny at 22°C, and Tokyo is sunny at 22°C...
============================================================
Scenario 2: max_function_calls — limit total tool executions
============================================================
max_iterations = 20
max_function_calls = 4
Response: Based on my research, Paris is sunny at 22°C, London is sunny at 22°C...
============================================================
Scenario 3: max_invocations — lifetime cap on a tool
============================================================
Before call 1: invocation_count = 0
After call 1: invocation_count = 1
Response: Based on the analysis, the AI market is showing strong growth trends...
After call 2: invocation_count = 2
Response: The cloud computing market continues to expand with key trends in...
After call 3: invocation_count = 2
Response: I'm unable to use the analysis tool right now as it has reached its limit...
Resetting invocation_count to 0...
invocation_count = 0
============================================================
Scenario 4: Per-agent limits via separate tool wrappers
============================================================
agent_a_lookup.max_invocations = 2
agent_b_lookup.max_invocations = 5
agent_a_lookup.invocation_count = 2 (limit 2)
agent_b_lookup.invocation_count = 1 (limit 5)
→ Agent A hit its limit; Agent B used 1 of 5.
============================================================
Scenario 5: Combined — all mechanisms together
============================================================
max_iterations = 5
max_function_calls = 8
premium_lookup.max_invocations = 3
Response: Here's a comprehensive overview of the weather and tourism for the cities...
premium_lookup.invocation_count = 3
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, FunctionInvocationContext, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
Dynamic Tool Exposure (Progressive Tool Loading) Example
This example demonstrates "progressive tool exposure": a tool that adds more tools to
the agent at runtime, in the same run, via ``FunctionInvocationContext``.
Frontloading a model with hundreds of tools hurts tool-selection accuracy, bloats
context, and raises cost. Instead, you can start with a small set of "loader" tools and
let the model pull in additional tools on demand. Tools added with ``ctx.add_tools(...)``
(or removed with ``ctx.remove_tools(...)``) become available to the model on the next
iteration of the function-calling loop.
"""
# These math tools are not registered on the agent up front. They are added on demand by
# the ``load_math_tools`` tool below, and only then become callable by the model.
@tool(approval_mode="never_require")
def factorial(n: Annotated[int, Field(description="A non-negative integer.")]) -> str:
"""Compute the factorial of n."""
if n < 0:
return "Error: n must be a non-negative integer."
result = 1
for value in range(2, n + 1):
result *= value
return f"{n}! = {result}"
@tool(approval_mode="never_require")
def fibonacci(n: Annotated[int, Field(description="The 0-based index in the Fibonacci sequence.")]) -> str:
"""Compute the n-th Fibonacci number."""
if n < 0:
return "Error: n must be a non-negative integer."
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return f"fib({n}) = {a}"
# The only tool the agent starts with. When called, it exposes the math tools above so the
# model can use them on the next turn. Note the ``ctx`` parameter is injected by the
# framework and is not visible to the model.
@tool(approval_mode="never_require")
def load_math_tools(ctx: FunctionInvocationContext) -> str:
"""Load additional math tools (factorial, fibonacci) so they can be used."""
ctx.add_tools([factorial, fibonacci])
return "Loaded math tools: factorial, fibonacci. You can now call them."
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(),
name="MathAgent",
instructions=(
"You are a math assistant. If you need math capabilities that are not yet "
"available, call load_math_tools first, then use the newly available tools."
),
tools=[load_math_tools],
)
# The agent starts with only ``load_math_tools``. To answer the question it must first
# load the math tools, then call ``factorial`` on the next iteration.
print(f"Agent: {await agent.run('What is 5 factorial?')}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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_sessions.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 = OpenAIChatClient()
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 = Agent(client=client, 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,80 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent, FunctionTool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 = Agent(
client=OpenAIChatClient(),
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,75 @@
# 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 Agent, FunctionTool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
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 = Agent(
client=OpenAIChatClient(),
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,112 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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_sessions.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 = Agent(
client=OpenAIChatClient(),
name="ToolAgent",
instructions="Use the provided tools.",
tools=[greet, safe_divide],
)
session = agent.create_session()
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool raises exception")
response = await agent.run("Divide 10 by 0", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call greet('Bob') - conversation can keep going.")
response = await agent.run("Greet Bob", session=session)
print(f"Response: {response.text}")
print("=" * 60)
# TODO: Use history providers to replay the conversation
# print("Replay the conversation:")
# for idx, msg in enumerate(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,166 @@
# 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 OpenAIChatClient
from dotenv import load_dotenv
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.
"""
# Load environment variables from .env file
load_dotenv()
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_sessions.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:
if user_input_needed.function_call is None:
continue
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:
if user_input_needed.function_call is None:
continue
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=OpenAIChatClient(),
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,109 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, Message, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Tool Approvals with Sessions
This sample demonstrates using tool approvals with sessions.
With sessions, you don't need to manually pass previous messages -
the session 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 sessions."""
print("=== Tool Approval with Session ===\n")
agent = Agent(
client=FoundryChatClient(credential=AzureCliCredential()),
name="CalendarAgent",
instructions="You are a helpful calendar assistant.",
tools=[add_to_calendar],
)
session = agent.create_session()
# 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, session=session)
# Check for approval requests
if result.user_input_requests:
for request in result.user_input_requests:
if request.function_call is None:
continue
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]), session=session)
print(f"Agent: {result}\n")
async def rejection_example() -> None:
"""Example showing rejection with sessions."""
print("=== Tool Rejection with Session ===\n")
agent = Agent(
client=FoundryChatClient(credential=AzureCliCredential()),
name="CalendarAgent",
instructions="You are a helpful calendar assistant.",
tools=[add_to_calendar],
)
session = agent.create_session()
query = "Add a team meeting on December 20th"
print(f"User: {query}")
result = await agent.run(query, session=session)
if result.user_input_requests:
for request in result.user_input_requests:
if request.function_call is None:
continue
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]), session=session)
print(f"Agent: {result}\n")
async def main() -> None:
await approval_example()
await rejection_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,86 @@
# 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 Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
from pydantic import BaseModel, Field
# Load environment variables from .env file
load_dotenv()
# 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 = Agent(
client=OpenAIChatClient(),
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,63 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, FunctionInvocationContext, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
AI Function with kwargs Example
This example demonstrates how to inject runtime context into an AI function
from the agent's run method, without exposing it 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. The injected context parameter can be typed as
``FunctionInvocationContext`` as shown here, or left untyped as ``ctx`` when you
prefer a lighter-weight sample setup.
"""
# Define the function tool with explicit invocation context.
# The context parameter can also be declared as an untyped ``ctx`` parameter.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
ctx: FunctionInvocationContext,
) -> str:
"""Get the weather for a given location."""
# Extract the injected argument from the explicit context
user_id = ctx.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 = Agent(
client=OpenAIChatClient(),
name="WeatherAgent",
instructions="You are a helpful weather assistant.",
tools=[get_weather],
)
# Pass the runtime context explicitly when running the agent.
response = await agent.run(
"What is the weather like in Amsterdam?",
function_invocation_kwargs={"user_id": "user_123"},
)
print(f"Agent: {response.text}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,187 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 = Agent(
client=OpenAIChatClient(),
name="ToolAgent",
instructions="Use the provided tools.",
tools=[safe_divide],
)
session = agent.create_session()
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool raises exception")
response = await agent.run("Divide 10 by 0", session=session)
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", session=session)
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}")
# TODO: Use history providers to replay the conversation
# print("Replay the conversation:")
# for idx, msg in enumerate(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. 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
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,90 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 = Agent(
client=OpenAIChatClient(),
name="ToolAgent",
instructions="Use the provided tools.",
tools=[unicorn_function],
)
session = agent.create_session()
print("=" * 60)
print("Step 1: Call unicorn_function")
response = await agent.run("Call 5 unicorns!", session=session)
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.", session=session)
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}")
# TODO: Use history providers to replay the conversation
# print("Replay the conversation:")
# for idx, msg in enumerate(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
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,57 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, AgentSession, FunctionInvocationContext, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
AI Function with Session Injection Example
This example demonstrates accessing the agent session inside a tool function
via ``FunctionInvocationContext.session``. The session is automatically
available when the agent is invoked with a session.
"""
# Define the function tool with explicit invocation context.
# The context parameter can also be declared as an untyped parameter with the name: ``ctx``.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
ctx: FunctionInvocationContext,
) -> str:
"""Get the weather for a given location."""
session = ctx.session
if session and isinstance(session, AgentSession) and session.service_session_id:
print(f"Session ID: {session.service_session_id}.")
return f"The weather in {location} is cloudy."
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(),
name="WeatherAgent",
instructions="You are a helpful weather assistant.",
tools=[get_weather],
default_options={"store": True},
)
# Create a session
session = agent.create_session()
# Run the agent with the session; tools receive it via ctx.session.
print(f"Agent: {await agent.run('What is the weather in London?', session=session)}")
print(f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}")
print(f"Agent: {await agent.run('What cities did I ask about?', session=session)}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,37 @@
# Hyperlight local code interpreter
Demonstrates the standalone [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
`HyperlightExecuteCodeTool` — a sandboxed local code interpreter that the agent
can invoke directly. Two patterns are shown:
| File | Pattern |
|------|---------|
| [`local_code_interpreter.py`](local_code_interpreter.py) | **Standalone tool**`HyperlightExecuteCodeTool` is added to the agent tool list and self-describes its sandbox tools, so no extra agent instructions are needed. Best for quick prototyping. |
| [`local_code_interpreter_manual_wiring.py`](local_code_interpreter_manual_wiring.py) | **Manual static wiring** — sandbox tools and CodeAct instructions are built once and passed to the `Agent` constructor alongside a direct-only tool (`send_email`). Best when the tool set is fixed for the agent's lifetime. |
For the recommended provider-driven pattern (with dynamic tool / capability
management), see
[`../../context_providers/code_act/`](../../context_providers/code_act/).
## Installation
```bash
pip install agent-framework agent-framework-hyperlight --pre
```
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
> at runtime when it tries to create the sandbox.
## Prerequisites
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
- A deployed model (`FOUNDRY_MODEL`)
- Azure CLI authenticated (`az login`)
## Run
```bash
python local_code_interpreter.py
python local_code_interpreter_manual_wiring.py
```
@@ -0,0 +1,109 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
from typing import Annotated, Any, Literal
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightExecuteCodeTool
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates the standalone Hyperlight execute_code tool.
The sample adds `HyperlightExecuteCodeTool` directly to the agent. The tool's
own description advertises `call_tool(...)`, the registered sandbox tools, and
the current capability configuration, so no extra CodeAct-specific agent
instructions are required.
"""
load_dotenv()
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation used by sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch simulated records from a named table."""
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
async def main() -> None:
"""Run the standalone execute_code sample."""
# 1. Create the packaged execute_code tool and register sandbox tools on it.
execute_code = HyperlightExecuteCodeTool(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 2. Create the client and the agent.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="HyperlightExecuteCodeToolAgent",
instructions="You are a helpful assistant.",
tools=execute_code,
)
# 3. Run one request through the direct-tool surface.
print("=" * 60)
print("Hyperlight execute_code tool sample")
print("=" * 60)
query = (
"Fetch all users, find admins, multiply 6*7, and print the users, admins, "
"and multiplication result. Use one execute_code call."
)
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
"""
Sample output (shape only):
============================================================
Hyperlight execute_code tool sample
============================================================
User: Fetch all users, find admins, multiply 6*7, ...
Agent: ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,132 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
from typing import Annotated, Any, Literal
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightExecuteCodeTool
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates manual static wiring of CodeAct without a provider.
Instead of using `HyperlightCodeActProvider` with `context_providers=`, this
sample creates a `HyperlightExecuteCodeTool` directly, extracts its CodeAct
instructions once, and passes both to the `Agent` constructor at build time.
This avoids the per-run provider lifecycle (`before_run` / `after_run`) and is
well-suited when the tool registry, file mounts, and network allow-list are
fixed for the agent's lifetime. The tradeoff is that dynamic tool or capability
changes between runs are not supported — any mutations to the tool would not
update the agent's instructions automatically.
"""
load_dotenv()
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation used by sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch simulated records from a named table."""
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
@tool(approval_mode="never_require")
def send_email(
to: Annotated[str, "Recipient email address."],
subject: Annotated[str, "Email subject line."],
body: Annotated[str, "Email body text."],
) -> str:
"""Simulate sending an email (direct-only tool, not available inside the sandbox)."""
return f"Email sent to {to}: {subject}"
async def main() -> None:
"""Run the manual static-wiring sample."""
# 1. Create the execute_code tool and register sandbox tools on it.
execute_code = HyperlightExecuteCodeTool(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 2. Build CodeAct instructions once. Setting tools_visible_to_model=False
# tells the instructions builder that sandbox tools are not in the agent's
# direct tool list, so the model must use call_tool(...) inside execute_code.
codeact_instructions = execute_code.build_instructions(tools_visible_to_model=False)
# 3. Create the client and the agent with everything wired at construction time.
# - send_email is a direct-only tool (not available inside the sandbox).
# - execute_code carries sandbox tools (compute, fetch_data) via call_tool.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="ManualWiringAgent",
instructions=f"You are a helpful assistant.\n\n{codeact_instructions}",
tools=[send_email, execute_code],
)
# 4. Run a request that exercises both the sandbox and the direct tool.
print("=" * 60)
print("Manual static-wiring CodeAct sample")
print("=" * 60)
query = (
"Fetch all users, find admins, multiply 6*7, and print the users, admins, "
"and multiplication result. Use one execute_code call. "
"Then send an email to admin@example.com summarising the results."
)
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
"""
Sample output (shape only):
============================================================
Manual static-wiring CodeAct sample
============================================================
User: Fetch all users, find admins, multiply 6*7, ...
Agent: ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from agent_framework_tools.shell import LocalShellTool, ShellPolicy
from dotenv import load_dotenv
"""
LocalShellTool with a strict allow-list (no approval loop).
Every command must match one of the allow-list regexes and the deny-list
still wins. Approval is disabled because the allow-list is doing the
gating; this is the safest fully-automatic configuration of
``LocalShellTool``.
"""
load_dotenv()
async def main() -> None:
client = OpenAIChatClient(model="gpt-5.4-nano")
shell = LocalShellTool(
mode="stateless",
approval_mode="never_require",
acknowledge_unsafe=True,
policy=ShellPolicy(
allowlist=[
r"^ls(\s|$)",
r"^pwd$",
r"^cat\s[^|;&]+$",
r"^git\s+(status|log|diff)(\s|$)",
r"^python\s+--version$",
],
),
timeout=10,
)
agent = Agent(
client=client,
instructions=(
"You can run a narrow set of read-only shell commands (ls, pwd, cat, "
"git status/log/diff, python --version). Anything else will be rejected."
),
tools=[client.get_shell_tool(func=shell.as_function())],
)
query = "Summarise the current directory and print the Python version."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,103 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from agent_framework_tools.shell import (
LocalShellTool,
ShellEnvironmentProvider,
ShellEnvironmentProviderOptions,
)
from dotenv import load_dotenv
"""
LocalShellTool wired with a ShellEnvironmentProvider context provider.
The provider probes the underlying shell once per provider lifetime and
injects an instructions block describing the shell family, OS, working
directory, and a configurable list of CLI tools. This helps the model
emit commands in the correct idiom (e.g. PowerShell vs bash) and avoids
asking it to use tools that are not installed.
Two phases are demonstrated:
* **Stateless** mode — each ``run`` call spawns a fresh shell, so
``cd`` does not carry across calls.
* **Persistent** mode — a single long-lived shell process backs every
call, so ``cd`` and exported environment variables persist.
Approval gating is disabled so the demo runs unattended. Real
applications should keep approval on, or use ``DockerShellTool``.
"""
load_dotenv()
def _print_snapshot(label: str, provider: ShellEnvironmentProvider) -> None:
snapshot = provider.current_snapshot
if snapshot is None:
print(f"[{label}] no snapshot captured")
return
print(f"\n[{label}] snapshot:")
print(f" family = {snapshot.family.value}")
print(f" os = {snapshot.os_description}")
print(f" shell_version = {snapshot.shell_version}")
print(f" working_directory = {snapshot.working_directory}")
for tool, version in snapshot.tool_versions.items():
print(f" {tool:<17} = {version}")
async def _ask(agent: Agent, query: str) -> None:
print(f"\nUser: {query}")
result = await agent.run(query)
if result.text:
print(f"Agent: {result.text}")
async def main() -> None:
client = OpenAIChatClient(model="gpt-5.4-nano")
options = ShellEnvironmentProviderOptions(
probe_tools=("git", "python", "uv", "node"),
)
print("=== stateless mode ===")
async with LocalShellTool(
mode="stateless",
approval_mode="never_require",
acknowledge_unsafe=True,
) as shell:
provider = ShellEnvironmentProvider(shell, options)
agent = Agent(
client=client,
instructions="Use the shell tool to answer the user's question.",
tools=[client.get_shell_tool(func=shell.as_function())],
context_providers=[provider],
)
await _ask(agent, "Show me the current working directory.")
await _ask(agent, "Now `cd ..` then show the working directory again.")
await _ask(agent, "Show the working directory once more — did `cd` persist?")
_print_snapshot("stateless", provider)
print("\n=== persistent mode ===")
async with LocalShellTool(
mode="persistent",
confine_workdir=False,
approval_mode="never_require",
acknowledge_unsafe=True,
) as shell:
provider = ShellEnvironmentProvider(shell, options)
agent = Agent(
client=client,
instructions="Use the shell tool to answer the user's question.",
tools=[client.get_shell_tool(func=shell.as_function())],
context_providers=[provider],
)
await _ask(agent, "Show me the current working directory.")
await _ask(agent, "Now `cd ..` then show the working directory again.")
await _ask(agent, "Show the working directory once more — did `cd` persist?")
_print_snapshot("persistent", provider)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,40 @@
# Monty local code interpreter
Demonstrates the standalone [Monty](https://github.com/pydantic/monty)
`MontyExecuteCodeTool` — a sandboxed local code interpreter that the agent can
invoke directly. Two patterns are shown:
| File | Pattern |
|------|---------|
| [`monty_code_interpreter.py`](monty_code_interpreter.py) | **Standalone tool**`MontyExecuteCodeTool` is added to the agent tool list and self-describes its sandbox tools, so no extra agent instructions are needed. Best for quick prototyping. |
| [`monty_code_interpreter_manual_wiring.py`](monty_code_interpreter_manual_wiring.py) | **Manual static wiring** — sandbox tools and CodeAct instructions are built once and passed to the `Agent` constructor alongside a direct-only tool (`send_email`). Best when the tool set is fixed for the agent's lifetime. |
For the recommended provider-driven pattern (with dynamic tool / capability
management), see
[`../../context_providers/code_act/`](../../context_providers/code_act/).
## Installation
```bash
pip install agent-framework agent-framework-monty --pre
```
> `agent-framework-monty` is an alpha package and is not yet part of
> `agent-framework[all]`. The `--pre` flag is required.
>
> Monty is cross-platform and has no hypervisor/WASM backend dependency.
> Inside the sandbox, OS / filesystem / network calls are blocked
> (`PermissionError`); registered host tools retain full Python access.
## Prerequisites
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
- A deployed model (`FOUNDRY_MODEL`)
- Azure CLI authenticated (`az login`)
## Run
```bash
python monty_code_interpreter.py
python monty_code_interpreter_manual_wiring.py
```
@@ -0,0 +1,114 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
from typing import Annotated, Any, Literal
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_monty import MontyExecuteCodeTool
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates the standalone Monty execute_code tool.
The sample adds `MontyExecuteCodeTool` directly to the agent. The tool's own
description advertises the registered sandbox tools (as typed async functions
and via `call_tool(...)`) plus the Monty DSL, so no extra CodeAct-specific
agent instructions are required.
Note: `agent-framework-monty` is an alpha package and is not yet part of
`agent-framework[all]`. Install it explicitly with:
pip install agent-framework agent-framework-monty --pre
"""
load_dotenv()
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation used by sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch simulated records from a named table."""
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
async def main() -> None:
"""Run the standalone Monty execute_code sample."""
# 1. Create the packaged execute_code tool and register sandbox tools on it.
execute_code = MontyExecuteCodeTool(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 2. Create the client and the agent.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="MontyExecuteCodeToolAgent",
instructions="You are a helpful assistant.",
tools=execute_code,
)
# 3. Run one request through the direct-tool surface.
print("=" * 60)
print("Monty execute_code tool sample")
print("=" * 60)
query = (
"Fetch all users, find admins, multiply 6*7, and print the users, admins, "
"and multiplication result. Use one execute_code call."
)
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
"""
Sample output (shape only):
============================================================
Monty execute_code tool sample
============================================================
User: Fetch all users, find admins, multiply 6*7, ...
Agent: ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,136 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
from typing import Annotated, Any, Literal
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_monty import MontyExecuteCodeTool
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""This sample demonstrates manual static wiring of Monty CodeAct without a provider.
Instead of using `MontyCodeActProvider` with `context_providers=`, this sample
creates a `MontyExecuteCodeTool` directly, extracts its CodeAct instructions
once, and passes both to the `Agent` constructor at build time.
This avoids the per-run provider lifecycle (`before_run` / `after_run`) and is
well-suited when the tool registry is fixed for the agent's lifetime. The
tradeoff is that dynamic tool changes between runs are not supported - any
mutations to the tool would not update the agent's instructions automatically.
Note: `agent-framework-monty` is an alpha package and is not yet part of
`agent-framework[all]`. Install it explicitly with:
pip install agent-framework agent-framework-monty --pre
"""
load_dotenv()
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation used by sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch simulated records from a named table."""
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
@tool(approval_mode="never_require")
def send_email(
to: Annotated[str, "Recipient email address."],
subject: Annotated[str, "Email subject line."],
body: Annotated[str, "Email body text."],
) -> str:
"""Simulate sending an email (direct-only tool, not available inside the sandbox)."""
return f"Email sent to {to}: {subject}"
async def main() -> None:
"""Run the manual static-wiring Monty sample."""
# 1. Create the execute_code tool and register sandbox tools on it.
execute_code = MontyExecuteCodeTool(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 2. Build CodeAct instructions once. Setting tools_visible_to_model=False
# tells the instructions builder that sandbox tools are not in the agent's
# direct tool list, so the model must call them inside execute_code.
codeact_instructions = execute_code.build_instructions(tools_visible_to_model=False)
# 3. Create the client and the agent with everything wired at construction time.
# - send_email is a direct-only tool (not available inside the sandbox).
# - execute_code carries sandbox tools (compute, fetch_data) for Monty.
agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="MontyManualWiringAgent",
instructions=f"You are a helpful assistant.\n\n{codeact_instructions}",
tools=[send_email, execute_code],
)
# 4. Run a request that exercises both the sandbox and the direct tool.
print("=" * 60)
print("Manual static-wiring Monty CodeAct sample")
print("=" * 60)
query = (
"Fetch all users, find admins, multiply 6*7, and print the users, admins, "
"and multiplication result. Use one execute_code call. "
"Then send an email to admin@example.com summarising the results."
)
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
"""
Sample output (shape only):
============================================================
Manual static-wiring Monty CodeAct sample
============================================================
User: Fetch all users, find admins, multiply 6*7, ...
Agent: ...
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,191 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import (
Agent,
AgentResponse,
AgentSession,
Content,
Message,
ToolApprovalMiddleware,
create_always_approve_tool_response,
create_always_approve_tool_with_arguments_response,
tool,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
This sample demonstrates how a host application can decide which approval
requests may run now, which must be rejected, and which can be remembered for
future runs.
The model may not request every tool on every run. The important part is the
approval mechanism:
1. Tools that are safe to run immediately use ``approval_mode="never_require"``.
2. Sensitive tools use ``approval_mode="always_require"``.
3. ``ToolApprovalMiddleware`` coordinates approval prompts and standing rules.
4. The host turns user policy into ``function_approval_response`` content:
- approve for this request only;
- reject for this request;
- approve and remember the tool for future requests;
- approve and remember the tool only when called again with the same arguments.
5. Heuristic auto-approval rules can approve low-risk function calls before
the user is prompted.
"""
# Load environment variables from .env file
load_dotenv()
@tool(approval_mode="never_require")
def lookup_ticket(ticket_id: Annotated[str, "Support ticket id, for example T-123"]) -> str:
"""Look up a support ticket. This read-only tool runs without approval."""
return f"Ticket {ticket_id}: customer confirmed the issue can be closed."
@tool(approval_mode="always_require")
def close_ticket(
ticket_id: Annotated[str, "Support ticket id, for example T-123"],
resolution: Annotated[str, "Short resolution text"],
) -> str:
"""Close a support ticket."""
return f"Ticket {ticket_id} closed with resolution: {resolution}"
@tool(approval_mode="always_require")
def notify_customer(
ticket_id: Annotated[str, "Support ticket id, for example T-123"],
message: Annotated[str, "Message to send to the customer"],
) -> str:
"""Notify the customer about a ticket update."""
return f"Customer notified for {ticket_id}: {message}"
@tool(approval_mode="always_require")
def add_internal_note(
ticket_id: Annotated[str, "Support ticket id, for example T-123"],
note: Annotated[str, "Internal note text"],
) -> str:
"""Add an internal note to a support ticket."""
return f"Internal note added to {ticket_id}: {note}"
@tool(approval_mode="always_require")
def delete_attachment(
ticket_id: Annotated[str, "Support ticket id, for example T-123"],
attachment_name: Annotated[str, "Attachment file name"],
) -> str:
"""Delete an attachment from a support ticket."""
return f"Deleted {attachment_name} from ticket {ticket_id}."
def auto_approve_low_risk_notes(function_call: Content) -> bool:
"""Heuristic rule: auto-approve short internal notes for the target ticket."""
if function_call.name != "add_internal_note":
return False
arguments = function_call.parse_arguments() or {}
note = str(arguments.get("note", ""))
return arguments.get("ticket_id") == "T-123" and len(note) <= 120
def approval_response_for_user_policy(request: Content) -> Content:
"""Convert user/host policy into an approval response for one tool request."""
function_call = request.function_call
if function_call is None or function_call.name is None:
return request.to_function_approval_response(approved=False)
tool_name = function_call.name
print(f"Approval requested: {tool_name}({function_call.arguments})")
if tool_name in {"close_ticket"}:
print(f"Decision: approve and remember future {tool_name} calls with these exact arguments")
return create_always_approve_tool_with_arguments_response(request)
if tool_name in {"notify_customer"}:
print(f"Decision: approve and remember all future {tool_name} calls")
return create_always_approve_tool_response(request)
if tool_name in {"delete_attachment"}:
print(f"Decision: reject {tool_name} for this run")
return request.to_function_approval_response(approved=False)
print(f"Decision: reject {tool_name}; no policy allowed it")
return request.to_function_approval_response(approved=False)
async def resolve_approval_requests(agent: Agent, response: AgentResponse, session: AgentSession) -> AgentResponse:
"""Resolve approval prompts until the agent returns a regular answer."""
result = response
while result.user_input_requests:
approval_responses = [approval_response_for_user_policy(request) for request in result.user_input_requests]
result = await agent.run(Message(role="user", contents=approval_responses), session=session)
return result
async def main() -> None:
"""Run the tool approval middleware sample."""
# 1. Create a regular chat client.
client = FoundryChatClient(credential=AzureCliCredential())
# 2. Create an agent with sensitive tools and opt-in ToolApprovalMiddleware.
agent = Agent(
client=client,
name="SupportAgent",
instructions=(
"You are a support agent. Use tools when useful. "
"Look up ticket T-123, close it if the customer confirmed, notify the customer, "
"add a short internal note, and do not delete attachments unless the tool is approved."
),
tools=[lookup_ticket, close_ticket, notify_customer, add_internal_note, delete_attachment],
middleware=[ToolApprovalMiddleware(auto_approval_rules=[auto_approve_low_risk_notes])],
)
session = agent.create_session()
# 3. Ask for work that may trigger a mixed batch of safe and sensitive tool calls.
query = (
"Please process ticket T-123: check the ticket, close it as resolved, "
"notify the customer, add a short internal note, and remove debug.log if it is attached."
)
print(f"User: {query}")
result = await agent.run(query, session=session)
# 4. Convert approval requests into approve/reject/always-approve responses.
result = await resolve_approval_requests(agent, result, session)
print(f"Agent: {result.text}")
# 5. Later runs can use remembered approval rules:
# - notify_customer: all future calls to the tool.
# - close_ticket: only future calls with the same arguments.
# - add_internal_note: low-risk matching calls are auto-approved by the heuristic callback.
follow_up = "Send the customer a short follow-up for ticket T-123."
print(f"\nUser: {follow_up}")
result = await agent.run(follow_up, session=session)
result = await resolve_approval_requests(agent, result, session)
print(f"Agent: {result.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
User: Please process ticket T-123: check the ticket, close it as resolved,
notify the customer, add a short internal note, and remove debug.log if it is attached.
Approval requested: close_ticket({"ticket_id": "T-123", "resolution": "resolved"})
Decision: approve and remember future close_ticket calls with these exact arguments
Approval requested: notify_customer({"ticket_id": "T-123", "message": "Your ticket has been resolved."})
Decision: approve and remember all future notify_customer calls
Approval requested: delete_attachment({"ticket_id": "T-123", "attachment_name": "debug.log"})
Decision: reject delete_attachment for this run
Agent: Ticket T-123 was closed, the customer was notified, and a short internal note was added.
I did not delete debug.log.
User: Send the customer a short follow-up for ticket T-123.
Agent: The customer was sent a short follow-up for ticket T-123.
"""
@@ -0,0 +1,103 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 = Agent(
client=OpenAIChatClient(),
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
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())