Skip to content

Commit 22ff5f8

Browse files
authored
Python: Fix responses agent kernel args bug (#13056)
### Motivation and Context The `arguments: KernelArguments` are not getting handled properly for the Responses Agent invoke. Use the provided `arguments` instead of trying to get them from the kwargs. <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description - Closes #13053 - Adds unit test coverage. <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent d7db35d commit 22ff5f8

File tree

2 files changed

+135
-7
lines changed

2 files changed

+135
-7
lines changed

python/semantic_kernel/agents/open_ai/responses_agent_thread_actions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ async def invoke(
256256
kernel.invoke_function_call(
257257
function_call=function_call,
258258
chat_history=override_history,
259-
arguments=kwargs.get("arguments"),
259+
arguments=arguments,
260260
execution_settings=None,
261261
function_call_count=fc_count,
262262
request_index=request_index,
@@ -561,7 +561,7 @@ async def invoke_stream(
561561
kernel.invoke_function_call(
562562
function_call=function_call,
563563
chat_history=override_history,
564-
arguments=kwargs.get("arguments"),
564+
arguments=arguments,
565565
is_streaming=True,
566566
execution_settings=None,
567567
function_call_count=fc_count,

python/tests/unit/agents/openai_responses/test_openai_responses_thread_actions.py

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
2020
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
2121
from semantic_kernel.contents.utils.author_role import AuthorRole
22+
from semantic_kernel.functions import KernelArguments
2223

2324

2425
@pytest.fixture
@@ -67,7 +68,6 @@ def mock_thread():
6768
return thread
6869

6970

70-
@pytest.mark.asyncio
7171
async def test_invoke_no_function_calls(mock_agent, mock_response, mock_chat_history, mock_thread):
7272
async def mock_get_response(*args, **kwargs):
7373
return mock_response
@@ -89,7 +89,6 @@ async def mock_get_response(*args, **kwargs):
8989
assert final_msg.role == AuthorRole.ASSISTANT
9090

9191

92-
@pytest.mark.asyncio
9392
async def test_invoke_raises_on_failed_response(mock_agent, mock_chat_history, mock_thread):
9493
mock_failed_response = MagicMock(spec=Response)
9594
mock_failed_response.status = "failed"
@@ -115,7 +114,6 @@ async def mock_get_response(*args, **kwargs):
115114
pass
116115

117116

118-
@pytest.mark.asyncio
119117
async def test_invoke_reaches_maximum_attempts(mock_agent, mock_chat_history, mock_thread):
120118
call_counter = 0
121119

@@ -173,7 +171,6 @@ async def mock_get_response(*args, **kwargs):
173171
assert messages is not None
174172

175173

176-
@pytest.mark.asyncio
177174
async def test_invoke_with_function_calls(mock_agent, mock_chat_history, mock_thread):
178175
initial_response = MagicMock(spec=Response)
179176
initial_response.status = "completed"
@@ -227,6 +224,138 @@ async def mock_get_response(*args, **kwargs):
227224
assert len(messages) == 3, f"Expected exactly 3 messages, got {len(messages)}"
228225

229226

227+
async def test_invoke_passes_kernel_arguments_to_kernel(mock_agent, mock_chat_history, mock_thread):
228+
# Prepare a response that triggers a function call
229+
initial_response = MagicMock(spec=Response)
230+
initial_response.status = "completed"
231+
initial_response.id = "fake-response-id"
232+
initial_response.output = [
233+
ResponseFunctionToolCall(
234+
id="tool_call_id",
235+
call_id="call_id",
236+
name="test_function",
237+
arguments='{"some_arg": 123}',
238+
type="function_call",
239+
)
240+
]
241+
initial_response.error = None
242+
initial_response.incomplete_details = None
243+
initial_response.created_at = 123456
244+
initial_response.usage = None
245+
initial_response.role = "assistant"
246+
247+
final_response = MagicMock(spec=Response)
248+
final_response.status = "completed"
249+
final_response.id = "fake-final-response-id"
250+
final_response.output = []
251+
final_response.error = None
252+
final_response.incomplete_details = None
253+
final_response.created_at = 123456
254+
final_response.usage = None
255+
final_response.role = "assistant"
256+
257+
responses = [initial_response, final_response]
258+
259+
async def mock_invoke_fc(*args, **kwargs):
260+
# Assert that KernelArguments were forwarded
261+
assert isinstance(kwargs.get("arguments"), KernelArguments)
262+
assert kwargs["arguments"].get("foo") == "bar"
263+
return MagicMock(terminate=False)
264+
265+
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_fc)
266+
267+
async def mock_get_response(*args, **kwargs):
268+
return responses.pop(0)
269+
270+
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
271+
args = KernelArguments(foo="bar")
272+
# Run invoke and ensure no assertion fails inside mock_invoke_fc
273+
collected = []
274+
async for _, msg in ResponsesAgentThreadActions.invoke(
275+
agent=mock_agent,
276+
chat_history=mock_chat_history,
277+
thread=mock_thread,
278+
store_enabled=True,
279+
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=1),
280+
arguments=args,
281+
):
282+
collected.append(msg)
283+
assert len(collected) >= 2
284+
285+
286+
async def test_invoke_stream_passes_kernel_arguments_to_kernel(mock_agent, mock_chat_history, mock_thread):
287+
class MockStream(AsyncStream[ResponseStreamEvent]):
288+
def __init__(self, events):
289+
self._events = events
290+
291+
async def __aenter__(self):
292+
return self
293+
294+
async def __aexit__(self, exc_type, exc_val, exc_tb):
295+
pass
296+
297+
def __aiter__(self):
298+
return self
299+
300+
async def __anext__(self):
301+
if not self._events:
302+
raise StopAsyncIteration
303+
return self._events.pop(0)
304+
305+
# Event that includes a function call
306+
mock_tool_call_event = ResponseOutputItemAddedEvent(
307+
item=ResponseFunctionToolCall(
308+
id="fake-tool-call-id",
309+
call_id="fake-call-id",
310+
name="test_function",
311+
arguments='{"arg": 123}',
312+
type="function_call",
313+
),
314+
output_index=0,
315+
type="response.output_item.added",
316+
sequence_number=0,
317+
)
318+
319+
mock_stream_event_end = ResponseOutputItemDoneEvent(
320+
item=ResponseOutputMessage(
321+
role="assistant",
322+
status="completed",
323+
id="fake-item-id",
324+
content=[ResponseOutputText(text="Final message after tool call", type="output_text", annotations=[])],
325+
type="message",
326+
),
327+
output_index=0,
328+
sequence_number=0,
329+
type="response.output_item.done",
330+
)
331+
332+
async def mock_get_response(*args, **kwargs):
333+
return MockStream([mock_tool_call_event, mock_stream_event_end])
334+
335+
async def mock_invoke_function_call(*args, **kwargs):
336+
assert isinstance(kwargs.get("arguments"), KernelArguments)
337+
assert kwargs["arguments"].get("foo") == "bar"
338+
return MagicMock(terminate=False)
339+
340+
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_function_call)
341+
342+
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
343+
args = KernelArguments(foo="bar")
344+
collected_stream_messages = []
345+
async for _ in ResponsesAgentThreadActions.invoke_stream(
346+
agent=mock_agent,
347+
chat_history=mock_chat_history,
348+
thread=mock_thread,
349+
store_enabled=True,
350+
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=1),
351+
output_messages=collected_stream_messages,
352+
arguments=args,
353+
):
354+
pass
355+
# If assertions passed in mock, arguments were forwarded
356+
assert len(collected_stream_messages) >= 1
357+
358+
230359
async def test_invoke_stream_no_function_calls(mock_agent, mock_chat_history, mock_thread):
231360
class MockStream(AsyncStream[ResponseStreamEvent]):
232361
def __init__(self, events):
@@ -294,7 +423,6 @@ async def mock_get_response(*args, **kwargs):
294423
assert collected_stream_messages[0].role == AuthorRole.ASSISTANT
295424

296425

297-
@pytest.mark.asyncio
298426
async def test_invoke_stream_with_tool_calls(mock_agent, mock_chat_history, mock_thread):
299427
class MockStream(AsyncStream[ResponseStreamEvent]):
300428
def __init__(self, events):

0 commit comments

Comments
 (0)