mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Introducing AI Function approval (#1131)
* support for local function approval * small fix * fix mypy * added bigger test scenario's for function calling and approvals * updated lock * updated return message for rejection * fix test * updated function result content handling
This commit is contained in:
committed by
GitHub
Unverified
parent
01f438d710
commit
fd819c6c02
@@ -0,0 +1,102 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randrange
|
||||
from typing import TYPE_CHECKING, Annotated, Any
|
||||
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import AgentProtocol
|
||||
|
||||
"""
|
||||
Demonstration of a tool with approvals.
|
||||
|
||||
This sample demonstrates using AI functions with user approval workflows.
|
||||
It shows how to handle function call approvals without using threads.
|
||||
"""
|
||||
|
||||
conditions = ["sunny", "cloudy", "raining", "snowing", "clear"]
|
||||
|
||||
|
||||
@ai_function
|
||||
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
|
||||
@ai_function(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: "AgentProtocol"):
|
||||
"""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.
|
||||
"""
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
result = await agent.run(query)
|
||||
while len(result.user_input_requests) > 0:
|
||||
# Start with the original query
|
||||
new_inputs: list[Any] = [query]
|
||||
|
||||
for user_input_needed in result.user_input_requests:
|
||||
print(
|
||||
f"\nUser Input Request for function from {agent.name}:"
|
||||
f"\n Function: {user_input_needed.function_call.name}"
|
||||
f"\n Arguments: {user_input_needed.function_call.arguments}"
|
||||
)
|
||||
|
||||
# Add the assistant message with the approval request
|
||||
new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed]))
|
||||
|
||||
# Get user approval
|
||||
user_approval = input("\nApprove function call? (y/n): ")
|
||||
|
||||
# Add the user's approval response
|
||||
new_inputs.append(
|
||||
ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")])
|
||||
)
|
||||
|
||||
# Run again with all the context
|
||||
result = await agent.run(new_inputs)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def run_weather_agent_with_approval() -> None:
|
||||
"""Example showing AI function with approval requirement."""
|
||||
print("\n=== Weather Agent WITH Approval Required ===\n")
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
name="WeatherAgent",
|
||||
instructions=("You are a helpful weather assistant. Use the get_weather tool to provide weather information."),
|
||||
tools=[get_weather, get_weather_detail],
|
||||
) as agent:
|
||||
query2 = "Can you give me an update of the weather in LA and Portland and detailed weather for Seattle?"
|
||||
print(f"User: {query2}")
|
||||
result2 = await handle_approvals(query2, agent)
|
||||
print(f"\n{agent.name}: {result2}\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Demonstration of a tool with approvals ===\n")
|
||||
|
||||
await run_weather_agent_with_approval()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import FunctionCallContent, FunctionResultContent
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
"""
|
||||
Tool exceptions handled by returning the error for the agent to recover from.
|
||||
|
||||
Shows how a tool that throws an exception creates gracefull recovery and can keep going.
|
||||
The LLM decides whether to retry the call or to respond with something else, based on the exception.
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
def safe_divide(
|
||||
a: Annotated[int, "Numerator"],
|
||||
b: Annotated[int, "Denominator"],
|
||||
) -> str:
|
||||
"""Divide two numbers can be used with 0 as denominator."""
|
||||
try:
|
||||
result = a / b # Will raise ZeroDivisionError
|
||||
except ZeroDivisionError as exc:
|
||||
print(f" Tool failed: with error: {exc}")
|
||||
raise
|
||||
|
||||
return f"{a} / {b} = {result}"
|
||||
|
||||
|
||||
async def main():
|
||||
# tools = Tools()
|
||||
agent = OpenAIResponsesClient().create_agent(
|
||||
name="ToolAgent",
|
||||
instructions="Use the provided tools.",
|
||||
tools=[greet, safe_divide],
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
print("=" * 60)
|
||||
print("Step 1: Call divide(10, 0) - tool raises exception")
|
||||
response = await agent.run("Divide 10 by 0", thread=thread)
|
||||
print(f"Response: {response.text}")
|
||||
print("=" * 60)
|
||||
print("Step 2: Call greet('Bob') - conversation can keep going.")
|
||||
response = await agent.run("Greet Bob", thread=thread)
|
||||
print(f"Response: {response.text}")
|
||||
print("=" * 60)
|
||||
print("Replay the conversation:")
|
||||
assert thread.message_store
|
||||
assert thread.message_store.list_messages
|
||||
for idx, msg in enumerate(await thread.message_store.list_messages()):
|
||||
if msg.text:
|
||||
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
print(
|
||||
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
|
||||
)
|
||||
if isinstance(content, FunctionResultContent):
|
||||
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())
|
||||
Reference in New Issue
Block a user