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
@@ -1,25 +1,13 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
|
||||
from agent_framework import (
|
||||
BaseChatClient,
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
pass # type: ignore
|
||||
else:
|
||||
pass # type: ignore[import]
|
||||
|
||||
|
||||
def test_chat_client_type(chat_client: ChatClientProtocol):
|
||||
assert isinstance(chat_client, ChatClientProtocol)
|
||||
@@ -51,113 +39,3 @@ async def test_base_client_get_response(chat_client_base: ChatClientProtocol):
|
||||
async def test_base_client_get_streaming_response(chat_client_base: ChatClientProtocol):
|
||||
async for update in chat_client_base.get_streaming_response(ChatMessage(role="user", text="Hello")):
|
||||
assert update.text == "update - Hello" or update.text == "another update"
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
assert exec_counter == 1
|
||||
assert len(response.messages) == 3
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
assert response.messages[0].contents[0].name == "test_function"
|
||||
assert response.messages[0].contents[0].arguments == '{"arg1": "value1"}'
|
||||
assert response.messages[0].contents[0].call_id == "1"
|
||||
assert response.messages[1].role == Role.TOOL
|
||||
assert isinstance(response.messages[1].contents[0], FunctionResultContent)
|
||||
assert response.messages[1].contents[0].call_id == "1"
|
||||
assert response.messages[1].contents[0].result == "Processed value1"
|
||||
assert response.messages[2].role == Role.ASSISTANT
|
||||
assert response.messages[2].text == "done"
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling_resets(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="2", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
assert exec_counter == 2
|
||||
assert len(response.messages) == 5
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
assert response.messages[1].role == Role.TOOL
|
||||
assert response.messages[2].role == Role.ASSISTANT
|
||||
assert response.messages[3].role == Role.TOOL
|
||||
assert response.messages[4].role == Role.ASSISTANT
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(response.messages[1].contents[0], FunctionResultContent)
|
||||
assert isinstance(response.messages[2].contents[0], FunctionCallContent)
|
||||
assert isinstance(response.messages[3].contents[0], FunctionResultContent)
|
||||
|
||||
|
||||
async def test_base_client_with_streaming_function_calling(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1":')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='"value1"}')],
|
||||
role="assistant",
|
||||
),
|
||||
],
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[TextContent(text="Processed value1")],
|
||||
role="assistant",
|
||||
)
|
||||
],
|
||||
]
|
||||
updates = []
|
||||
async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[ai_func]):
|
||||
updates.append(update)
|
||||
assert len(updates) == 4 # two updates with the function call, the function result and the final text
|
||||
assert updates[0].contents[0].call_id == "1"
|
||||
assert updates[1].contents[0].call_id == "1"
|
||||
assert updates[2].contents[0].call_id == "1"
|
||||
assert updates[3].text == "Processed value1"
|
||||
assert exec_counter == 1
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework import (
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
assert exec_counter == 1
|
||||
assert len(response.messages) == 3
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
assert response.messages[0].contents[0].name == "test_function"
|
||||
assert response.messages[0].contents[0].arguments == '{"arg1": "value1"}'
|
||||
assert response.messages[0].contents[0].call_id == "1"
|
||||
assert response.messages[1].role == Role.TOOL
|
||||
assert isinstance(response.messages[1].contents[0], FunctionResultContent)
|
||||
assert response.messages[1].contents[0].call_id == "1"
|
||||
assert response.messages[1].contents[0].result == "Processed value1"
|
||||
assert response.messages[2].role == Role.ASSISTANT
|
||||
assert response.messages[2].text == "done"
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling_resets(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="2", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
assert exec_counter == 2
|
||||
assert len(response.messages) == 5
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
assert response.messages[1].role == Role.TOOL
|
||||
assert response.messages[2].role == Role.ASSISTANT
|
||||
assert response.messages[3].role == Role.TOOL
|
||||
assert response.messages[4].role == Role.ASSISTANT
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(response.messages[1].contents[0], FunctionResultContent)
|
||||
assert isinstance(response.messages[2].contents[0], FunctionCallContent)
|
||||
assert isinstance(response.messages[3].contents[0], FunctionResultContent)
|
||||
|
||||
|
||||
async def test_base_client_with_streaming_function_calling(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1":')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='"value1"}')],
|
||||
role="assistant",
|
||||
),
|
||||
],
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[TextContent(text="Processed value1")],
|
||||
role="assistant",
|
||||
)
|
||||
],
|
||||
]
|
||||
updates = []
|
||||
async for update in chat_client_base.get_streaming_response("hello", tool_choice="auto", tools=[ai_func]):
|
||||
updates.append(update)
|
||||
assert len(updates) == 4 # two updates with the function call, the function result and the final text
|
||||
assert updates[0].contents[0].call_id == "1"
|
||||
assert updates[1].contents[0].call_id == "1"
|
||||
assert updates[2].contents[0].call_id == "1"
|
||||
assert updates[3].text == "Processed value1"
|
||||
assert exec_counter == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"approval_required,num_functions",
|
||||
[
|
||||
pytest.param(False, 1, id="single function without approval"),
|
||||
pytest.param(True, 1, id="single function with approval"),
|
||||
pytest.param("mixed", 2, id="two functions with mixed approval"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"thread_type",
|
||||
[
|
||||
pytest.param(None, id="no thread"),
|
||||
pytest.param("local", id="local thread"),
|
||||
pytest.param("service", id="service thread"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("streaming", [False, True], ids=["non-streaming", "streaming"])
|
||||
async def test_function_invocation_scenarios(
|
||||
chat_client_base: ChatClientProtocol,
|
||||
streaming: bool,
|
||||
thread_type: str | None,
|
||||
approval_required: bool | str,
|
||||
num_functions: int,
|
||||
):
|
||||
"""Comprehensive test for function invocation scenarios.
|
||||
|
||||
This test covers:
|
||||
- Single function without approval: 3 messages (call, result, final)
|
||||
- Single function with approval: 2 messages (call, approval request)
|
||||
- Two functions with mixed approval: varies based on approval flow
|
||||
- All scenarios tested with both streaming and non-streaming
|
||||
- Thread scenarios: no thread, local thread (in-memory), and service thread (conversation_id)
|
||||
"""
|
||||
exec_counter = 0
|
||||
|
||||
# Setup thread based on parameters
|
||||
conversation_id = None
|
||||
if thread_type == "service":
|
||||
# Simulate a service-side thread with conversation_id
|
||||
conversation_id = "test-thread-123"
|
||||
|
||||
@ai_function(name="no_approval_func")
|
||||
def func_no_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
@ai_function(name="approval_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Approved {arg1}"
|
||||
|
||||
# Setup tools and responses based on the scenario
|
||||
if num_functions == 1:
|
||||
tools = [func_with_approval if approval_required else func_no_approval]
|
||||
function_name = "approval_func" if approval_required else "no_approval_func"
|
||||
|
||||
# Single function call content
|
||||
func_call = FunctionCallContent(call_id="1", name=function_name, arguments='{"arg1": "value1"}')
|
||||
completion = ChatMessage(role="assistant", text="done")
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(messages=ChatMessage(role="assistant", contents=[func_call]))
|
||||
] + ([] if approval_required else [ChatResponse(messages=completion)])
|
||||
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name=function_name, arguments='{"arg1":')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="1", name=function_name, arguments='"value1"}')],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
] + ([] if approval_required else [[ChatResponseUpdate(contents=[TextContent(text="done")], role="assistant")]])
|
||||
|
||||
else: # num_functions == 2
|
||||
tools = [func_no_approval, func_with_approval]
|
||||
|
||||
# Two function calls content
|
||||
func_calls = [
|
||||
FunctionCallContent(call_id="1", name="no_approval_func", arguments='{"arg1": "value1"}'),
|
||||
FunctionCallContent(call_id="2", name="approval_func", arguments='{"arg1": "value2"}'),
|
||||
]
|
||||
|
||||
chat_client_base.run_responses = [ChatResponse(messages=ChatMessage(role="assistant", contents=func_calls))]
|
||||
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(contents=[func_calls[0]], role="assistant"),
|
||||
ChatResponseUpdate(contents=[func_calls[1]], role="assistant"),
|
||||
]
|
||||
]
|
||||
|
||||
# Execute the test
|
||||
chat_options = ChatOptions(tool_choice="auto", tools=tools)
|
||||
if thread_type == "service":
|
||||
# For service threads, we need to pass conversation_id via ChatOptions
|
||||
chat_options.store = True
|
||||
chat_options.conversation_id = conversation_id
|
||||
|
||||
if not streaming:
|
||||
response = await chat_client_base.get_response("hello", chat_options=chat_options)
|
||||
messages = response.messages
|
||||
else:
|
||||
updates = []
|
||||
async for update in chat_client_base.get_streaming_response("hello", chat_options=chat_options):
|
||||
updates.append(update)
|
||||
messages = updates
|
||||
|
||||
# Service threads have different message management behavior (server-side storage)
|
||||
# so we skip detailed message assertions for those scenarios
|
||||
if thread_type == "service":
|
||||
# Just verify the function was executed or not based on approval
|
||||
if not approval_required or approval_required == "mixed":
|
||||
# For service threads, the execution counter check is still valid
|
||||
pass
|
||||
return
|
||||
|
||||
# Verify based on scenario (for no thread and local thread cases)
|
||||
if num_functions == 1:
|
||||
if approval_required:
|
||||
# Single function with approval: call + approval request
|
||||
if not streaming:
|
||||
assert len(messages) == 2
|
||||
assert isinstance(messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[1].contents[0], FunctionApprovalRequestContent)
|
||||
assert messages[1].contents[0].function_call.name == "approval_func"
|
||||
assert exec_counter == 0 # Function not executed yet
|
||||
else:
|
||||
# Streaming: 2 function call chunks + 1 approval request
|
||||
assert len(messages) == 3
|
||||
assert isinstance(messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[1].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[2].contents[0], FunctionApprovalRequestContent)
|
||||
assert messages[2].contents[0].function_call.name == "approval_func"
|
||||
assert exec_counter == 0 # Function not executed yet
|
||||
else:
|
||||
# Single function without approval: call + result + final
|
||||
if not streaming:
|
||||
assert len(messages) == 3
|
||||
assert isinstance(messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[1].contents[0], FunctionResultContent)
|
||||
assert messages[1].contents[0].result == "Processed value1"
|
||||
assert messages[2].role == Role.ASSISTANT
|
||||
assert messages[2].text == "done"
|
||||
assert exec_counter == 1
|
||||
else:
|
||||
# Streaming has: 2 function call updates + 1 result update + 1 final update
|
||||
assert len(messages) == 4
|
||||
assert isinstance(messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[1].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[2].contents[0], FunctionResultContent)
|
||||
assert messages[3].text == "done"
|
||||
assert exec_counter == 1
|
||||
else: # num_functions == 2
|
||||
# Two functions with mixed approval
|
||||
if not streaming:
|
||||
# Mixed: first message has both calls, second has approval requests for both
|
||||
# (because when one requires approval, all are batched for approval)
|
||||
assert len(messages) == 2
|
||||
assert len(messages[0].contents) == 2 # Both function calls
|
||||
assert isinstance(messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[0].contents[1], FunctionCallContent)
|
||||
# Both should result in approval requests
|
||||
assert len(messages[1].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in messages[1].contents)
|
||||
assert exec_counter == 0 # Neither function executed yet
|
||||
else:
|
||||
# Streaming: 2 function call updates + 1 approval request with 2 contents
|
||||
assert len(messages) == 3
|
||||
assert isinstance(messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(messages[1].contents[0], FunctionCallContent)
|
||||
# The approval request message contains both approval requests
|
||||
assert len(messages[2].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in messages[2].contents)
|
||||
assert exec_counter == 0 # Neither function executed yet
|
||||
|
||||
|
||||
async def test_rejected_approval(chat_client_base: ChatClientProtocol):
|
||||
"""Test that rejecting an approval alongside an approved one is handled correctly."""
|
||||
from agent_framework import FunctionApprovalResponseContent
|
||||
|
||||
exec_counter_approved = 0
|
||||
exec_counter_rejected = 0
|
||||
|
||||
@ai_function(name="approved_func", approval_mode="always_require")
|
||||
def func_approved(arg1: str) -> str:
|
||||
nonlocal exec_counter_approved
|
||||
exec_counter_approved += 1
|
||||
return f"Approved {arg1}"
|
||||
|
||||
@ai_function(name="rejected_func", approval_mode="always_require")
|
||||
def func_rejected(arg1: str) -> str:
|
||||
nonlocal exec_counter_rejected
|
||||
exec_counter_rejected += 1
|
||||
return f"Rejected {arg1}"
|
||||
|
||||
# Setup: two function calls that require approval
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="1", name="approved_func", arguments='{"arg1": "value1"}'),
|
||||
FunctionCallContent(call_id="2", name="rejected_func", arguments='{"arg1": "value2"}'),
|
||||
],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
]
|
||||
|
||||
# Get the response with approval requests
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[func_approved, func_rejected])
|
||||
assert len(response.messages) == 2
|
||||
assert len(response.messages[1].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in response.messages[1].contents)
|
||||
|
||||
# Approve one and reject the other
|
||||
approval_req_1 = response.messages[1].contents[0]
|
||||
approval_req_2 = response.messages[1].contents[1]
|
||||
|
||||
approved_response = FunctionApprovalResponseContent(
|
||||
id=approval_req_1.id,
|
||||
function_call=approval_req_1.function_call,
|
||||
approved=True,
|
||||
)
|
||||
rejected_response = FunctionApprovalResponseContent(
|
||||
id=approval_req_2.id,
|
||||
function_call=approval_req_2.function_call,
|
||||
approved=False,
|
||||
)
|
||||
|
||||
# Continue conversation with one approved and one rejected
|
||||
all_messages = response.messages + [ChatMessage(role="user", contents=[approved_response, rejected_response])]
|
||||
|
||||
# Call get_response which will process the approvals
|
||||
await chat_client_base.get_response(all_messages, tool_choice="auto", tools=[func_approved, func_rejected])
|
||||
|
||||
# Verify the approval/rejection was processed correctly
|
||||
# Find the results in the input messages (modified in-place)
|
||||
approved_result = None
|
||||
rejected_result = None
|
||||
for msg in all_messages:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if content.call_id == "1":
|
||||
approved_result = content
|
||||
elif content.call_id == "2":
|
||||
rejected_result = content
|
||||
|
||||
# The approved function should have been executed and have a result
|
||||
assert approved_result is not None, "Should have found result for approved function"
|
||||
assert approved_result.result == "Approved value1"
|
||||
assert exec_counter_approved == 1
|
||||
|
||||
# The rejected function should have a "not approved" result and NOT have been executed
|
||||
assert rejected_result is not None, "Should have found result for rejected function"
|
||||
assert rejected_result.result == "Error: Tool call invocation was rejected by user."
|
||||
assert exec_counter_rejected == 0
|
||||
|
||||
|
||||
async def test_max_iterations_limit(chat_client_base: ChatClientProtocol):
|
||||
"""Test that MAX_ITERATIONS in additional_properties limits function call loops."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
# Set up multiple function call responses to create a loop
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="test_function", arguments='{"arg1": "value1"}')],
|
||||
)
|
||||
),
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="2", name="test_function", arguments='{"arg1": "value2"}')],
|
||||
)
|
||||
),
|
||||
# Failsafe response when tool_choice is set to "none"
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="giving up on tools")),
|
||||
]
|
||||
|
||||
# Set max_iterations to 1 in additional_properties
|
||||
chat_client_base.additional_properties = {"max_iterations": 1}
|
||||
|
||||
response = await chat_client_base.get_response("hello", tool_choice="auto", tools=[ai_func])
|
||||
|
||||
# With max_iterations=1, we should:
|
||||
# 1. Execute first function call (exec_counter=1)
|
||||
# 2. Try to make second call but hit iteration limit
|
||||
# 3. Fall back to asking for a plain answer with tool_choice="none"
|
||||
assert exec_counter == 1 # Only first function executed
|
||||
assert response.messages[-1].text == "I broke out of the function invocation loop..." # Failsafe response
|
||||
@@ -616,3 +616,507 @@ def test_hosted_mcp_tool_with_dict_of_allowed_tools():
|
||||
url="https://mcp.example",
|
||||
allowed_tools={"toolA": "Tool A", "toolC": "Tool C"},
|
||||
)
|
||||
|
||||
|
||||
# region Approval Flow Tests
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_chat_client():
|
||||
"""Create a mock chat client for testing approval flows."""
|
||||
from agent_framework import ChatMessage, ChatResponse, ChatResponseUpdate
|
||||
|
||||
class MockChatClient:
|
||||
def __init__(self):
|
||||
self.call_count = 0
|
||||
self.responses = []
|
||||
|
||||
async def get_response(self, messages, **kwargs):
|
||||
"""Mock get_response that returns predefined responses."""
|
||||
if self.call_count < len(self.responses):
|
||||
response = self.responses[self.call_count]
|
||||
self.call_count += 1
|
||||
return response
|
||||
# Default response
|
||||
return ChatResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=["Default response"])],
|
||||
)
|
||||
|
||||
async def get_streaming_response(self, messages, **kwargs):
|
||||
"""Mock get_streaming_response that yields predefined updates."""
|
||||
if self.call_count < len(self.responses):
|
||||
response = self.responses[self.call_count]
|
||||
self.call_count += 1
|
||||
# Yield updates from the response
|
||||
for msg in response.messages:
|
||||
for content in msg.contents:
|
||||
yield ChatResponseUpdate(contents=[content], role=msg.role)
|
||||
else:
|
||||
# Default response
|
||||
yield ChatResponseUpdate(contents=["Default response"], role="assistant")
|
||||
|
||||
return MockChatClient()
|
||||
|
||||
|
||||
@ai_function(name="no_approval_tool", description="Tool that doesn't require approval")
|
||||
def no_approval_tool(x: int) -> int:
|
||||
"""A tool that doesn't require approval."""
|
||||
return x * 2
|
||||
|
||||
|
||||
@ai_function(
|
||||
name="requires_approval_tool",
|
||||
description="Tool that requires approval",
|
||||
approval_mode="always_require",
|
||||
)
|
||||
def requires_approval_tool(x: int) -> int:
|
||||
"""A tool that requires approval."""
|
||||
return x * 3
|
||||
|
||||
|
||||
async def test_non_streaming_single_function_no_approval():
|
||||
"""Test non-streaming handler with single function call that doesn't require approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
# Create mock client
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Create responses: first with function call, second with final answer
|
||||
initial_response = ChatResponse(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
)
|
||||
]
|
||||
)
|
||||
final_response = ChatResponse(messages=[ChatMessage(role="assistant", contents=["The result is 10"])])
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response, final_response]
|
||||
|
||||
async def mock_get_response(self, messages, **kwargs):
|
||||
result = responses[call_count[0]]
|
||||
call_count[0] += 1
|
||||
return result
|
||||
|
||||
# Wrap the function
|
||||
wrapped = _handle_function_calls_response(mock_get_response)
|
||||
|
||||
# Execute
|
||||
result = await wrapped(mock_client, messages=[], tools=[no_approval_tool])
|
||||
|
||||
# Verify: should have 3 messages: function call, function result, final answer
|
||||
assert len(result.messages) == 3
|
||||
assert isinstance(result.messages[0].contents[0], FunctionCallContent)
|
||||
from agent_framework import FunctionResultContent
|
||||
|
||||
assert isinstance(result.messages[1].contents[0], FunctionResultContent)
|
||||
assert result.messages[1].contents[0].result == 10 # 5 * 2
|
||||
assert result.messages[2].contents[0] == "The result is 10"
|
||||
|
||||
|
||||
async def test_non_streaming_single_function_requires_approval():
|
||||
"""Test non-streaming handler with single function call that requires approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with function call
|
||||
initial_response = ChatResponse(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response]
|
||||
|
||||
async def mock_get_response(self, messages, **kwargs):
|
||||
result = responses[call_count[0]]
|
||||
call_count[0] += 1
|
||||
return result
|
||||
|
||||
wrapped = _handle_function_calls_response(mock_get_response)
|
||||
|
||||
# Execute
|
||||
result = await wrapped(mock_client, messages=[], tools=[requires_approval_tool])
|
||||
|
||||
# Verify: should return 2 messages - function call and approval request
|
||||
from agent_framework import FunctionApprovalRequestContent
|
||||
|
||||
assert len(result.messages) == 2
|
||||
assert isinstance(result.messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(result.messages[1].contents[0], FunctionApprovalRequestContent)
|
||||
assert result.messages[1].contents[0].function_call.name == "requires_approval_tool"
|
||||
|
||||
|
||||
async def test_non_streaming_two_functions_both_no_approval():
|
||||
"""Test non-streaming handler with two function calls, neither requiring approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with two function calls to the same tool
|
||||
initial_response = ChatResponse(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
FunctionCallContent(call_id="call_2", name="no_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
final_response = ChatResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=["Both tools executed successfully"])]
|
||||
)
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response, final_response]
|
||||
|
||||
async def mock_get_response(self, messages, **kwargs):
|
||||
result = responses[call_count[0]]
|
||||
call_count[0] += 1
|
||||
return result
|
||||
|
||||
wrapped = _handle_function_calls_response(mock_get_response)
|
||||
|
||||
# Execute
|
||||
result = await wrapped(mock_client, messages=[], tools=[no_approval_tool])
|
||||
|
||||
# Verify: should have function calls, results, and final answer
|
||||
from agent_framework import FunctionResultContent
|
||||
|
||||
assert len(result.messages) == 3
|
||||
# First message has both function calls
|
||||
assert len(result.messages[0].contents) == 2
|
||||
# Second message has both results
|
||||
assert len(result.messages[1].contents) == 2
|
||||
assert all(isinstance(c, FunctionResultContent) for c in result.messages[1].contents)
|
||||
assert result.messages[1].contents[0].result == 10 # 5 * 2
|
||||
assert result.messages[1].contents[1].result == 6 # 3 * 2
|
||||
|
||||
|
||||
async def test_non_streaming_two_functions_both_require_approval():
|
||||
"""Test non-streaming handler with two function calls, both requiring approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with two function calls to the same tool
|
||||
initial_response = ChatResponse(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}'),
|
||||
FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response]
|
||||
|
||||
async def mock_get_response(self, messages, **kwargs):
|
||||
result = responses[call_count[0]]
|
||||
call_count[0] += 1
|
||||
return result
|
||||
|
||||
wrapped = _handle_function_calls_response(mock_get_response)
|
||||
|
||||
# Execute
|
||||
result = await wrapped(mock_client, messages=[], tools=[requires_approval_tool])
|
||||
|
||||
# Verify: should return 2 messages - function calls and approval requests
|
||||
from agent_framework import FunctionApprovalRequestContent
|
||||
|
||||
assert len(result.messages) == 2
|
||||
assert len(result.messages[0].contents) == 2 # Both function calls
|
||||
assert all(isinstance(c, FunctionCallContent) for c in result.messages[0].contents)
|
||||
assert len(result.messages[1].contents) == 2 # Both approval requests
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in result.messages[1].contents)
|
||||
assert result.messages[1].contents[0].function_call.name == "requires_approval_tool"
|
||||
assert result.messages[1].contents[1].function_call.name == "requires_approval_tool"
|
||||
|
||||
|
||||
async def test_non_streaming_two_functions_mixed_approval():
|
||||
"""Test non-streaming handler with two function calls, one requiring approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with two function calls
|
||||
initial_response = ChatResponse(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response]
|
||||
|
||||
async def mock_get_response(self, messages, **kwargs):
|
||||
result = responses[call_count[0]]
|
||||
call_count[0] += 1
|
||||
return result
|
||||
|
||||
wrapped = _handle_function_calls_response(mock_get_response)
|
||||
|
||||
# Execute
|
||||
result = await wrapped(mock_client, messages=[], tools=[no_approval_tool, requires_approval_tool])
|
||||
|
||||
# Verify: should return approval requests for both (when one needs approval, all are sent for approval)
|
||||
from agent_framework import FunctionApprovalRequestContent
|
||||
|
||||
assert len(result.messages) == 2
|
||||
assert len(result.messages[0].contents) == 2 # Both function calls
|
||||
assert len(result.messages[1].contents) == 2 # Both approval requests
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in result.messages[1].contents)
|
||||
|
||||
|
||||
async def test_streaming_single_function_no_approval():
|
||||
"""Test streaming handler with single function call that doesn't require approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with function call, then final response after function execution
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
)
|
||||
]
|
||||
final_updates = [ChatResponseUpdate(contents=["The result is 10"], role="assistant")]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates, final_updates]
|
||||
|
||||
async def mock_get_streaming_response(self, messages, **kwargs):
|
||||
updates = updates_list[call_count[0]]
|
||||
call_count[0] += 1
|
||||
for update in updates:
|
||||
yield update
|
||||
|
||||
wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response)
|
||||
|
||||
# Execute and collect updates
|
||||
updates = []
|
||||
async for update in wrapped(mock_client, messages=[], tools=[no_approval_tool]):
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should have function call update, tool result update (injected), and final update
|
||||
from agent_framework import FunctionResultContent, Role
|
||||
|
||||
assert len(updates) >= 3
|
||||
# First update is the function call
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
# Second update should be the tool result (injected by the wrapper)
|
||||
assert updates[1].role == Role.TOOL
|
||||
assert isinstance(updates[1].contents[0], FunctionResultContent)
|
||||
assert updates[1].contents[0].result == 10 # 5 * 2
|
||||
# Last update is the final message
|
||||
assert updates[-1].contents[0] == "The result is 10"
|
||||
|
||||
|
||||
async def test_streaming_single_function_requires_approval():
|
||||
"""Test streaming handler with single function call that requires approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with function call
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
)
|
||||
]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates]
|
||||
|
||||
async def mock_get_streaming_response(self, messages, **kwargs):
|
||||
updates = updates_list[call_count[0]]
|
||||
call_count[0] += 1
|
||||
for update in updates:
|
||||
yield update
|
||||
|
||||
wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response)
|
||||
|
||||
# Execute and collect updates
|
||||
updates = []
|
||||
async for update in wrapped(mock_client, messages=[], tools=[requires_approval_tool]):
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should yield function call and then approval request
|
||||
from agent_framework import FunctionApprovalRequestContent, Role
|
||||
|
||||
assert len(updates) == 2
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert updates[1].role == Role.TOOL
|
||||
assert isinstance(updates[1].contents[0], FunctionApprovalRequestContent)
|
||||
|
||||
|
||||
async def test_streaming_two_functions_both_no_approval():
|
||||
"""Test streaming handler with two function calls, neither requiring approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with two function calls to the same tool
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_2", name="no_approval_tool", arguments='{"x": 3}')],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
final_updates = [ChatResponseUpdate(contents=["Both tools executed successfully"], role="assistant")]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates, final_updates]
|
||||
|
||||
async def mock_get_streaming_response(self, messages, **kwargs):
|
||||
updates = updates_list[call_count[0]]
|
||||
call_count[0] += 1
|
||||
for update in updates:
|
||||
yield update
|
||||
|
||||
wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response)
|
||||
|
||||
# Execute and collect updates
|
||||
updates = []
|
||||
async for update in wrapped(mock_client, messages=[], tools=[no_approval_tool]):
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should have both function calls, one tool result update with both results, and final message
|
||||
from agent_framework import FunctionResultContent, Role
|
||||
|
||||
assert len(updates) >= 3
|
||||
# First two updates are function calls
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(updates[1].contents[0], FunctionCallContent)
|
||||
# Should have a tool result update with both results
|
||||
tool_updates = [u for u in updates if u.role == Role.TOOL]
|
||||
assert len(tool_updates) == 1
|
||||
assert len(tool_updates[0].contents) == 2
|
||||
assert all(isinstance(c, FunctionResultContent) for c in tool_updates[0].contents)
|
||||
|
||||
|
||||
async def test_streaming_two_functions_both_require_approval():
|
||||
"""Test streaming handler with two function calls, both requiring approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with two function calls to the same tool
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}')],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates]
|
||||
|
||||
async def mock_get_streaming_response(self, messages, **kwargs):
|
||||
updates = updates_list[call_count[0]]
|
||||
call_count[0] += 1
|
||||
for update in updates:
|
||||
yield update
|
||||
|
||||
wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response)
|
||||
|
||||
# Execute and collect updates
|
||||
updates = []
|
||||
async for update in wrapped(mock_client, messages=[], tools=[requires_approval_tool]):
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should yield both function calls and then approval requests
|
||||
from agent_framework import FunctionApprovalRequestContent, Role
|
||||
|
||||
assert len(updates) == 3
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(updates[1].contents[0], FunctionCallContent)
|
||||
# Tool update with both approval requests
|
||||
assert updates[2].role == Role.TOOL
|
||||
assert len(updates[2].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in updates[2].contents)
|
||||
|
||||
|
||||
async def test_streaming_two_functions_mixed_approval():
|
||||
"""Test streaming handler with two function calls, one requiring approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
|
||||
# Initial response with two function calls
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}')],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates]
|
||||
|
||||
async def mock_get_streaming_response(self, messages, **kwargs):
|
||||
updates = updates_list[call_count[0]]
|
||||
call_count[0] += 1
|
||||
for update in updates:
|
||||
yield update
|
||||
|
||||
wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response)
|
||||
|
||||
# Execute and collect updates
|
||||
updates = []
|
||||
async for update in wrapped(mock_client, messages=[], tools=[no_approval_tool, requires_approval_tool]):
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should yield both function calls and then approval requests (when one needs approval, all wait)
|
||||
from agent_framework import FunctionApprovalRequestContent, Role
|
||||
|
||||
assert len(updates) == 3
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(updates[1].contents[0], FunctionCallContent)
|
||||
# Tool update with both approval requests
|
||||
assert updates[2].role == Role.TOOL
|
||||
assert len(updates[2].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in updates[2].contents)
|
||||
|
||||
@@ -22,11 +22,11 @@ from agent_framework import (
|
||||
TextContent,
|
||||
ToolProtocol,
|
||||
ai_function,
|
||||
prepare_function_call_results,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.openai._exceptions import OpenAIContentFilterException
|
||||
from agent_framework.openai._shared import prepare_function_call_results
|
||||
|
||||
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
|
||||
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
|
||||
|
||||
@@ -782,7 +782,7 @@ def test_create_streaming_response_content_with_mcp_approval_request() -> None:
|
||||
|
||||
@pytest.mark.parametrize("enable_otel", [False], indirect=True)
|
||||
@pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True)
|
||||
def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
|
||||
async def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
|
||||
"""End-to-end mocked test:
|
||||
model issues an mcp_approval_request, user approves, client sends mcp_approval_response.
|
||||
"""
|
||||
@@ -824,7 +824,7 @@ def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
|
||||
# Patch the create call to return the two mocked responses in sequence
|
||||
with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create:
|
||||
# First call: get the approval request
|
||||
response = asyncio.run(client.get_response(messages=[ChatMessage(role="user", text="Trigger approval")]))
|
||||
response = await client.get_response(messages=[ChatMessage(role="user", text="Trigger approval")])
|
||||
assert isinstance(response.messages[0].contents[0], FunctionApprovalRequestContent)
|
||||
req = response.messages[0].contents[0]
|
||||
assert req.id == "approval-1"
|
||||
@@ -832,7 +832,7 @@ def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
|
||||
# Build a user approval and send it (include required function_call)
|
||||
approval = FunctionApprovalResponseContent(approved=True, id=req.id, function_call=req.function_call)
|
||||
approval_message = ChatMessage(role="user", contents=[approval])
|
||||
_ = asyncio.run(client.get_response(messages=[approval_message]))
|
||||
_ = await client.get_response(messages=[approval_message])
|
||||
|
||||
# Ensure two calls were made and the second includes the mcp_approval_response
|
||||
assert mock_create.call_count == 2
|
||||
|
||||
Reference in New Issue
Block a user