chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,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 you’re 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 you’re 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 you’re 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 you’re 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 you’re 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 you’re 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 can’t 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 can’t 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())
|
||||
+132
@@ -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())
|
||||
+136
@@ -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 you’re 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 you’d 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())
|
||||
Reference in New Issue
Block a user