chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,366 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from openai import AsyncOpenAI
from pydantic import BaseModel, ValidationError
from semantic_kernel.agents import AgentRegistry
from semantic_kernel.agents.open_ai.openai_responses_agent import OpenAIResponsesAgent, ResponsesAgentThread
from semantic_kernel.agents.open_ai.run_polling_options import RunPollingOptions
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
@pytest.fixture
def mock_openai_client():
return AsyncMock(spec=AsyncOpenAI)
class SamplePlugin:
@kernel_function
def test_plugin(self, *args, **kwargs):
pass
class ResponseModelPydantic(BaseModel):
response: str
items: list[str]
class ResponseModelNonPydantic:
response: str
items: list[str]
async def test_open_ai_assistant_agent_init():
sample_prompt_template_config = PromptTemplateConfig(
template="template",
)
kernel_plugin = KernelPlugin(name="expected_plugin_name", description="expected_plugin_description")
agent = OpenAIResponsesAgent(
ai_model_id="model_id",
id="agent123",
name="agentName",
description="desc",
client=AsyncMock(spec=AsyncOpenAI),
arguments=KernelArguments(test="test"),
kernel=AsyncMock(spec=Kernel),
plugins=[SamplePlugin(), kernel_plugin],
polling_options=AsyncMock(spec=RunPollingOptions),
prompt_template_config=sample_prompt_template_config,
other_arg="test",
)
assert agent.id == "agent123"
assert agent.name == "agentName"
assert agent.description == "desc"
def test_open_ai_settings_create_throws(openai_unit_test_env):
with patch(
"semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings.OpenAISettings.__init__"
) as mock_create:
mock_create.side_effect = ValidationError.from_exception_data("test", line_errors=[], input_type="python")
with pytest.raises(AgentInitializationException, match="Failed to create OpenAI settings."):
_, _ = OpenAIResponsesAgent.setup_resources(api_key="test_api_key")
def test_open_ai_assistant_with_file_search_tool():
tools, resources = OpenAIResponsesAgent.configure_file_search_tool(vector_store_ids=["vector_store_id"])
assert tools is not None
assert resources is not None
@pytest.mark.parametrize(
"model, json_schema_expected",
[
pytest.param(ResponseModelPydantic, True),
pytest.param(ResponseModelNonPydantic, True),
pytest.param({"type": "json_object"}, False),
pytest.param({"type": "json_schema", "json_schema": {"schema": {}}}, False),
],
)
def test_configure_response_format(model, json_schema_expected):
response_format = OpenAIResponsesAgent.configure_response_format(model)
assert response_format is not None
if json_schema_expected:
assert response_format["format"]["schema"] is not None # type: ignore
def test_configure_response_format_unexpected_type():
with pytest.raises(AgentInitializationException) as exc_info:
OpenAIResponsesAgent.configure_response_format({"type": "invalid_type"})
assert "Encountered unexpected response_format type" in str(exc_info.value)
def test_configure_response_format_json_schema_invalid_schema():
with pytest.raises(AgentInitializationException) as exc_info:
OpenAIResponsesAgent.configure_response_format({"type": "json_schema", "json_schema": "not_a_dict"})
assert "If response_format has type 'json_schema'" in str(exc_info.value)
def test_configure_response_format_invalid_input_type():
with pytest.raises(AgentInitializationException) as exc_info:
OpenAIResponsesAgent.configure_response_format(3) # type: ignore
assert "response_format must be a dictionary" in str(exc_info.value)
@pytest.mark.parametrize(
"arguments, include_args",
[
pytest.param({"extra_args": "extra_args"}, True),
pytest.param(None, False),
],
)
async def test_openai_responses_agent_get_response(arguments, include_args):
agent = OpenAIResponsesAgent(client=AsyncMock(spec=AsyncOpenAI), ai_model_id="model_id")
mock_thread = AsyncMock(spec=ResponsesAgentThread)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
kwargs = None
if include_args:
kwargs = arguments
with patch(
"semantic_kernel.agents.open_ai.responses_agent_thread_actions.ResponsesAgentThreadActions.invoke",
side_effect=fake_invoke,
):
response = await agent.get_response(messages="test", thread=mock_thread, **(kwargs or {}))
assert response is not None
assert response.message.content == "content"
assert response.thread is not None
@pytest.mark.parametrize(
"arguments, include_args",
[
pytest.param({"extra_args": "extra_args"}, True),
pytest.param(None, False),
],
)
async def test_openai_responses_agent_get_response_exception(arguments, include_args):
agent = OpenAIResponsesAgent(client=AsyncMock(spec=AsyncOpenAI), ai_model_id="model_id")
mock_thread = AsyncMock(spec=ResponsesAgentThread)
async def fake_invoke(*args, **kwargs):
yield False, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
kwargs = None
if include_args:
kwargs = arguments
with (
patch(
"semantic_kernel.agents.open_ai.responses_agent_thread_actions.ResponsesAgentThreadActions.invoke",
side_effect=fake_invoke,
),
pytest.raises(AgentInvokeException),
):
await agent.get_response(messages="test", thread=mock_thread, **(kwargs or {}))
@pytest.mark.parametrize(
"arguments, include_args",
[
pytest.param({"extra_args": "extra_args"}, True),
pytest.param(None, False),
],
)
async def test_openai_responses_agent_invoke(arguments, include_args):
agent = OpenAIResponsesAgent(client=AsyncMock(spec=AsyncOpenAI), ai_model_id="model_id")
mock_thread = AsyncMock(spec=ResponsesAgentThread)
results = []
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
kwargs = None
if include_args:
kwargs = arguments
with patch(
"semantic_kernel.agents.open_ai.responses_agent_thread_actions.ResponsesAgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for item in agent.invoke(messages="test", thread=mock_thread, **(kwargs or {})):
results.append(item)
assert len(results) == 1
@pytest.mark.parametrize(
"arguments, include_args",
[
pytest.param({"extra_args": "extra_args"}, True),
pytest.param(None, False),
],
)
async def test_openai_responses_agent_invoke_stream(arguments, include_args):
agent = OpenAIResponsesAgent(client=AsyncMock(spec=AsyncOpenAI), ai_model_id="model_id")
mock_thread = AsyncMock(spec=ResponsesAgentThread)
results = []
async def fake_invoke(*args, **kwargs):
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
kwargs = None
if include_args:
kwargs = arguments
with patch(
"semantic_kernel.agents.open_ai.responses_agent_thread_actions.ResponsesAgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for item in agent.invoke_stream(messages="test", thread=mock_thread, **(kwargs or {})):
results.append(item)
assert len(results) == 1
def test_create_openai_client(openai_unit_test_env):
client, model = OpenAIResponsesAgent.setup_resources(env_file_path="./", default_headers={"user_agent": "test"})
assert client is not None
assert client.api_key == "test_api_key"
assert model is not None
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
async def test_open_ai_agent_missing_api_key_throws(kernel, openai_unit_test_env):
with pytest.raises(AgentInitializationException, match="The OpenAI API key is required."):
_, _ = OpenAIResponsesAgent.setup_resources(env_file_path="./", default_headers={"user_agent": "test"})
@pytest.mark.parametrize("exclude_list", [["OPENAI_RESPONSES_MODEL_ID"]], indirect=True)
async def test_open_ai_agent_missing_chat_deployment_name_throws(kernel, openai_unit_test_env):
with pytest.raises(AgentInitializationException, match="The OpenAI Responses model ID is required."):
_, _ = OpenAIResponsesAgent.setup_resources(
env_file_path="./",
api_key="test_api_key",
default_headers={"user_agent": "test"},
)
async def test_openai_assistant_agent_from_yaml_minimal(openai_unit_test_env, mock_openai_client):
spec = """
type: openai_responses
name: MinimalAgent
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
"""
client = mock_openai_client
agent: OpenAIResponsesAgent = await AgentRegistry.create_from_yaml(spec, client=client)
assert isinstance(agent, OpenAIResponsesAgent)
assert agent.name == "MinimalAgent"
assert agent.ai_model_id == openai_unit_test_env.get("OPENAI_RESPONSES_MODEL_ID")
async def test_openai_assistant_agent_with_tools(openai_unit_test_env, mock_openai_client):
spec = """
type: openai_responses
name: FileSearchAgent
description: Uses file search.
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
tools:
- type: file_search
description: File search for document retrieval.
options:
vector_store_ids:
- ${OpenAI:VectorStoreId}
"""
client = mock_openai_client
agent: OpenAIResponsesAgent = await AgentRegistry.create_from_yaml(
spec, client=client, extras={"OpenAI:VectorStoreId": "vector-store-123"}
)
assert agent.name == "FileSearchAgent"
assert any(t["type"] == "file_search" for t in agent.tools)
async def test_openai_assistant_agent_with_inputs_outputs_template(openai_unit_test_env, mock_openai_client):
spec = """
type: openai_responses
name: StoryAgent
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
inputs:
topic:
description: The story topic.
required: true
default: AI
length:
description: The length of story.
required: true
default: 2
outputs:
output1:
description: The story.
template:
format: semantic-kernel
"""
client = mock_openai_client
agent: OpenAIResponsesAgent = await AgentRegistry.create_from_yaml(spec, client=client)
assert agent.name == "StoryAgent"
assert agent.prompt_template.prompt_template_config.template_format == "semantic-kernel"
async def test_openai_assistant_agent_from_dict_missing_type():
data = {"name": "NoType"}
with pytest.raises(AgentInitializationException, match="Missing 'type'"):
await AgentRegistry.create_from_dict(data)
async def test_openai_assistant_agent_from_yaml_missing_required_fields():
spec = """
type: openai_responses
"""
with pytest.raises(AgentInitializationException):
await AgentRegistry.create_from_yaml(spec)
async def test_agent_from_file_success(tmp_path, openai_unit_test_env, mock_openai_client):
file_path = tmp_path / "spec.yaml"
file_path.write_text(
"""
type: openai_responses
name: DeclarativeAgent
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
""",
encoding="utf-8",
)
client = mock_openai_client
agent: OpenAIResponsesAgent = await AgentRegistry.create_from_file(str(file_path), client=client)
assert agent.name == "DeclarativeAgent"
assert isinstance(agent, OpenAIResponsesAgent)
async def test_openai_assistant_agent_from_yaml_invalid_type():
spec = """
type: not_registered
name: ShouldFail
"""
with pytest.raises(AgentInitializationException, match="not registered"):
await AgentRegistry.create_from_yaml(spec)
@@ -0,0 +1,568 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai._streaming import AsyncStream
from openai.types.responses import ResponseFunctionToolCall
from openai.types.responses.response import Response
from openai.types.responses.response_output_item_added_event import ResponseOutputItemAddedEvent
from openai.types.responses.response_output_item_done_event import ResponseOutputItemDoneEvent
from openai.types.responses.response_output_message import ResponseOutputMessage
from openai.types.responses.response_output_text import ResponseOutputText
from openai.types.responses.response_stream_event import ResponseStreamEvent
from openai.types.responses.response_text_delta_event import Logprob, ResponseTextDeltaEvent
from semantic_kernel.agents.open_ai.openai_responses_agent import OpenAIResponsesAgent
from semantic_kernel.agents.open_ai.responses_agent_thread_actions import ResponsesAgentThreadActions
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.functions import KernelArguments
@pytest.fixture
def mock_agent():
agent = AsyncMock(spec=OpenAIResponsesAgent)
agent.ai_model_id = "test-model-id"
agent.name = "test-agent"
agent.polling_options = MagicMock()
agent.polling_options.default_polling_interval.total_seconds.return_value = 0.0001
agent.tools = []
agent.text = "auto"
agent.temperature = 0.7
agent.top_p = 1.0
agent.metadata = {}
agent.format_instructions = AsyncMock(return_value="base instructions")
agent.kernel = MagicMock()
agent.polling_options.run_polling_timeout.total_seconds.return_value = 5
agent.polling_options.default_polling_interval.total_seconds.return_value = 1
return agent
@pytest.fixture
def mock_response():
response = MagicMock(spec=Response)
response.status = "completed"
response.output = []
response.id = "fake-response-id"
response.error = None
response.incomplete_details = None
response.created_at = 10303039393
response.usage = None
return response
@pytest.fixture
def mock_chat_history():
history = MagicMock()
history.messages = [ChatMessageContent(role=AuthorRole.USER, content="Hello")]
return history
@pytest.fixture
def mock_thread():
thread = MagicMock()
thread._chat_history.messages = []
return thread
async def test_invoke_no_function_calls(mock_agent, mock_response, mock_chat_history, mock_thread):
async def mock_get_response(*args, **kwargs):
return mock_response
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
results = []
async for is_visible, msg in ResponsesAgentThreadActions.invoke(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(),
):
results.append((is_visible, msg))
assert len(results) == 1
is_visible, final_msg = results[0]
assert is_visible is True
assert final_msg.role == AuthorRole.ASSISTANT
async def test_invoke_raises_on_failed_response(mock_agent, mock_chat_history, mock_thread):
mock_failed_response = MagicMock(spec=Response)
mock_failed_response.status = "failed"
mock_failed_response.error = MagicMock()
mock_failed_response.error.message = "some error"
mock_failed_response.incomplete_details = None
mock_failed_response.id = "fake-failed-response-id"
async def mock_get_response(*args, **kwargs):
return mock_failed_response
with (
patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response),
pytest.raises(Exception, match="Run failed with status: `failed`"),
):
async for _ in ResponsesAgentThreadActions.invoke(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(),
):
pass
async def test_invoke_reaches_maximum_attempts(mock_agent, mock_chat_history, mock_thread):
call_counter = 0
response_with_tool_call = MagicMock(spec=Response)
response_with_tool_call.status = "completed"
response_with_tool_call.id = "fake-response-id"
response_with_tool_call.output = [
ResponseFunctionToolCall(
id="tool_call_id",
call_id="call_id",
name="test_function",
arguments='{"some_arg": 123}',
type="function_call",
)
]
response_with_tool_call.error = None
response_with_tool_call.incomplete_details = None
response_with_tool_call.created_at = 123456
response_with_tool_call.usage = None
response_with_tool_call.role = "assistant"
final_response = MagicMock(spec=Response)
final_response.status = "completed"
final_response.id = "fake-final-response-id"
final_response.output = []
final_response.error = None
final_response.incomplete_details = None
final_response.created_at = 123456
final_response.usage = None
final_response.role = "assistant"
async def mock_invoke_fc(*args, **kwargs):
return MagicMock(terminate=False)
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_fc)
async def mock_get_response(*args, **kwargs):
nonlocal call_counter
if call_counter < 3:
call_counter += 1
return response_with_tool_call
return final_response
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
messages = []
async for _, msg in ResponsesAgentThreadActions.invoke(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=3),
):
messages.append(msg)
assert messages is not None
async def test_invoke_with_function_calls(mock_agent, mock_chat_history, mock_thread):
initial_response = MagicMock(spec=Response)
initial_response.status = "completed"
initial_response.id = "fake-response-id"
initial_response.output = [
ResponseFunctionToolCall(
id="tool_call_id",
call_id="call_id",
name="test_function",
arguments='{"some_arg": 123}',
type="function_call",
)
]
initial_response.error = None
initial_response.incomplete_details = None
initial_response.created_at = 123456
initial_response.usage = None
initial_response.role = "assistant"
final_response = MagicMock(spec=Response)
final_response.status = "completed"
final_response.id = "fake-final-response-id"
final_response.output = []
final_response.error = None
final_response.incomplete_details = None
final_response.created_at = 123456
final_response.usage = None
final_response.role = "assistant"
responses = [initial_response, final_response]
async def mock_invoke_fc(*args, **kwargs):
return MagicMock(terminate=False)
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_fc)
async def mock_get_response(*args, **kwargs):
return responses.pop(0)
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
messages = []
async for is_visible, msg in ResponsesAgentThreadActions.invoke(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=1),
):
messages.append(msg)
assert len(messages) == 3, f"Expected exactly 3 messages, got {len(messages)}"
async def test_invoke_passes_kernel_arguments_to_kernel(mock_agent, mock_chat_history, mock_thread):
# Prepare a response that triggers a function call
initial_response = MagicMock(spec=Response)
initial_response.status = "completed"
initial_response.id = "fake-response-id"
initial_response.output = [
ResponseFunctionToolCall(
id="tool_call_id",
call_id="call_id",
name="test_function",
arguments='{"some_arg": 123}',
type="function_call",
)
]
initial_response.error = None
initial_response.incomplete_details = None
initial_response.created_at = 123456
initial_response.usage = None
initial_response.role = "assistant"
final_response = MagicMock(spec=Response)
final_response.status = "completed"
final_response.id = "fake-final-response-id"
final_response.output = []
final_response.error = None
final_response.incomplete_details = None
final_response.created_at = 123456
final_response.usage = None
final_response.role = "assistant"
responses = [initial_response, final_response]
async def mock_invoke_fc(*args, **kwargs):
# Assert that KernelArguments were forwarded
assert isinstance(kwargs.get("arguments"), KernelArguments)
assert kwargs["arguments"].get("foo") == "bar"
return MagicMock(terminate=False)
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_fc)
async def mock_get_response(*args, **kwargs):
return responses.pop(0)
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
args = KernelArguments(foo="bar")
# Run invoke and ensure no assertion fails inside mock_invoke_fc
collected = []
async for _, msg in ResponsesAgentThreadActions.invoke(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=1),
arguments=args,
):
collected.append(msg)
assert len(collected) >= 2
async def test_invoke_stream_passes_kernel_arguments_to_kernel(mock_agent, mock_chat_history, mock_thread):
class MockStream(AsyncStream[ResponseStreamEvent]):
def __init__(self, events):
self._events = events
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
def __aiter__(self):
return self
async def __anext__(self):
if not self._events:
raise StopAsyncIteration
return self._events.pop(0)
# Event that includes a function call
mock_tool_call_event = ResponseOutputItemAddedEvent(
item=ResponseFunctionToolCall(
id="fake-tool-call-id",
call_id="fake-call-id",
name="test_function",
arguments='{"arg": 123}',
type="function_call",
),
output_index=0,
type="response.output_item.added",
sequence_number=0,
)
mock_stream_event_end = ResponseOutputItemDoneEvent(
item=ResponseOutputMessage(
role="assistant",
status="completed",
id="fake-item-id",
content=[ResponseOutputText(text="Final message after tool call", type="output_text", annotations=[])],
type="message",
),
output_index=0,
sequence_number=0,
type="response.output_item.done",
)
async def mock_get_response(*args, **kwargs):
return MockStream([mock_tool_call_event, mock_stream_event_end])
async def mock_invoke_function_call(*args, **kwargs):
assert isinstance(kwargs.get("arguments"), KernelArguments)
assert kwargs["arguments"].get("foo") == "bar"
return MagicMock(terminate=False)
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_function_call)
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
args = KernelArguments(foo="bar")
collected_stream_messages = []
async for _ in ResponsesAgentThreadActions.invoke_stream(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=1),
output_messages=collected_stream_messages,
arguments=args,
):
pass
# If assertions passed in mock, arguments were forwarded
assert len(collected_stream_messages) >= 1
async def test_invoke_stream_no_function_calls(mock_agent, mock_chat_history, mock_thread):
class MockStream(AsyncStream[ResponseStreamEvent]):
def __init__(self, events):
self._events = events
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
def __aiter__(self):
return self
async def __anext__(self):
if not self._events:
raise StopAsyncIteration
return self._events.pop(0)
mock_stream_event = ResponseTextDeltaEvent(
delta="Test partial content",
content_index=0,
item_id="fake-item-id",
logprobs=[Logprob(token="test_token", logprob=0.3)],
output_index=0,
type="response.output_text.delta",
sequence_number=0,
)
mock_stream_event_end = ResponseOutputItemDoneEvent(
item=ResponseOutputMessage(
role="assistant",
status="completed",
id="fake-item-id",
content=[ResponseOutputText(text="Test partial content", type="output_text", annotations=[])],
type="message",
),
output_index=0,
sequence_number=0,
type="response.output_item.done",
)
async def mock_get_response(*args, **kwargs):
return MockStream([mock_stream_event, mock_stream_event_end])
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
collected_stream_messages = []
received_text = ""
async for streaming_msg in ResponsesAgentThreadActions.invoke_stream(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=False,
function_choice_behavior=MagicMock(),
output_messages=collected_stream_messages,
):
assert isinstance(streaming_msg, StreamingChatMessageContent)
for item in streaming_msg.items:
if isinstance(item, StreamingTextContent):
received_text += item.text
assert "Test partial content" in received_text, "Expected streamed partial content."
assert len(collected_stream_messages) == 1, "Expected exactly one final message."
assert collected_stream_messages[0].role == AuthorRole.ASSISTANT
async def test_invoke_stream_with_tool_calls(mock_agent, mock_chat_history, mock_thread):
class MockStream(AsyncStream[ResponseStreamEvent]):
def __init__(self, events):
self._events = events
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
def __aiter__(self):
return self
async def __anext__(self):
if not self._events:
raise StopAsyncIteration
return self._events.pop(0)
mock_tool_call_event = ResponseOutputItemAddedEvent(
item=ResponseFunctionToolCall(
id="fake-tool-call-id",
call_id="fake-call-id",
name="test_function",
arguments='{"arg": 123}',
type="function_call",
),
output_index=0,
type="response.output_item.added",
sequence_number=0,
)
mock_stream_event_end = ResponseOutputItemDoneEvent(
item=ResponseOutputMessage(
role="assistant",
status="completed",
id="fake-item-id",
content=[ResponseOutputText(text="Final message after tool call", type="output_text", annotations=[])],
type="message",
),
output_index=0,
sequence_number=0,
type="response.output_item.done",
)
async def mock_get_response(*args, **kwargs):
return MockStream([mock_tool_call_event, mock_stream_event_end])
async def mock_invoke_function_call(*args, **kwargs):
return MagicMock(terminate=False)
mock_agent.kernel.invoke_function_call = MagicMock(side_effect=mock_invoke_function_call)
with patch.object(ResponsesAgentThreadActions, "_get_response", new=mock_get_response):
collected_stream_messages = []
received_text = ""
async for streaming_msg in ResponsesAgentThreadActions.invoke_stream(
agent=mock_agent,
chat_history=mock_chat_history,
thread=mock_thread,
store_enabled=True,
function_choice_behavior=MagicMock(maximum_auto_invoke_attempts=1),
output_messages=collected_stream_messages,
):
assert isinstance(streaming_msg, StreamingChatMessageContent)
for item in streaming_msg.items:
if isinstance(item, StreamingTextContent):
received_text += item.text
assert len(collected_stream_messages) == 2, "Expected exactly two final messages after tool call."
assert collected_stream_messages[0].role == AuthorRole.ASSISTANT
def test_get_tools(mock_agent, kernel, custom_plugin_class):
kernel.add_plugin(custom_plugin_class)
fcb = FunctionChoiceBehavior()
tools = ResponsesAgentThreadActions._get_tools(
agent=mock_agent,
kernel=kernel,
function_choice_behavior=fcb,
)
assert len(tools) == len(mock_agent.tools) + len(kernel.get_full_list_of_function_metadata())
def test_prepare_chat_history_multiple_images_no_duplication():
"""Test that multiple images in a message don't get duplicated in the request."""
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.text_content import TextContent
# Create a chat history with a message containing text and multiple images
chat_history = ChatHistory()
message_items = [
TextContent(text="How many pictures do you get?"),
ImageContent(uri="https://example.com/image1.jpg"),
ImageContent(uri="https://example.com/image2.jpg"),
ImageContent(uri="https://example.com/image3.jpg"),
ImageContent(uri="https://example.com/image4.jpg"),
]
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
message = ChatMessageContent(role=AuthorRole.USER, items=message_items)
chat_history.add_message(message)
# Call the method that was causing duplication
result = ResponsesAgentThreadActions._prepare_chat_history_for_request(chat_history, True)
# Verify we have exactly one message in the result
assert len(result) == 1, f"Expected 1 message, got {len(result)}"
# Get the content from the message
message_content = result[0]["content"]
# Count text and image items
text_items = [item for item in message_content if item["type"] == "input_text"]
image_items = [item for item in message_content if item["type"] == "input_image"]
# Verify counts
assert len(text_items) == 1, f"Expected 1 text item, got {len(text_items)}"
assert len(image_items) == 4, f"Expected 4 image items, got {len(image_items)}"
# Verify the text content
assert text_items[0]["text"] == "How many pictures do you get?"
# Verify the image URLs are correct and not duplicated
expected_urls = [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg",
"https://example.com/image4.jpg",
]
actual_urls = [item["image_url"] for item in image_items]
assert actual_urls == expected_urls, f"Expected {expected_urls}, got {actual_urls}"
# Verify total content items equals expected (1 text + 4 images = 5)
assert len(message_content) == 5, f"Expected 5 total content items, got {len(message_content)}"