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,105 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from openai import AsyncOpenAI
from openai.types.beta.assistant import Assistant
from openai.types.beta.threads.file_citation_annotation import FileCitation, FileCitationAnnotation
from openai.types.beta.threads.file_path_annotation import FilePath, FilePathAnnotation
from openai.types.beta.threads.image_file import ImageFile
from openai.types.beta.threads.image_file_content_block import ImageFileContentBlock
from openai.types.beta.threads.text import Text
from openai.types.beta.threads.text_content_block import TextContentBlock
@pytest.fixture
def mock_thread():
class MockThread:
id = "test_thread_id"
return MockThread()
@pytest.fixture
def mock_thread_messages():
class MockMessage:
def __init__(self, id, role, content, assistant_id=None):
self.id = id
self.role = role
self.content = content
self.assistant_id = assistant_id
return [
MockMessage(
id="test_message_id_1",
role="user",
content=[
TextContentBlock(
type="text",
text=Text(
value="Hello",
annotations=[
FilePathAnnotation(
type="file_path",
file_path=FilePath(file_id="test_file_id"),
end_index=5,
start_index=0,
text="Hello",
),
FileCitationAnnotation(
type="file_citation",
file_citation=FileCitation(file_id="test_file_id"),
text="Hello",
start_index=0,
end_index=5,
),
],
),
)
],
),
MockMessage(
id="test_message_id_2",
role="assistant",
content=[
ImageFileContentBlock(type="image_file", image_file=ImageFile(file_id="test_file_id", detail="auto"))
],
assistant_id="assistant_1",
),
]
@pytest.fixture
def openai_client(assistant_definition, mock_thread, mock_thread_messages) -> AsyncMock:
async def mock_list_messages(*args, **kwargs) -> Any:
return MagicMock(data=mock_thread_messages)
async def mock_retrieve_assistant(*args, **kwargs) -> Any:
asst = AsyncMock(spec=Assistant)
asst.name = "test-assistant"
return asst
client = AsyncMock(spec=AsyncOpenAI)
client.beta = MagicMock()
client.beta.assistants = MagicMock()
client.beta.assistants.create = AsyncMock(return_value=assistant_definition)
client.beta.assistants.retrieve = AsyncMock(side_effect=mock_retrieve_assistant)
client.beta.threads = MagicMock()
client.beta.threads.create = AsyncMock(return_value=mock_thread)
client.beta.threads.messages = MagicMock()
client.beta.threads.messages.list = AsyncMock(side_effect=mock_list_messages)
return client
@pytest.fixture
def assistant_definition() -> AsyncMock:
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
return definition
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,468 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai import AsyncOpenAI
from openai.types.beta.assistant import Assistant
from openai.types.beta.threads.file_citation_annotation import FileCitation, FileCitationAnnotation
from openai.types.beta.threads.file_path_annotation import FilePath, FilePathAnnotation
from openai.types.beta.threads.image_file import ImageFile
from openai.types.beta.threads.image_file_content_block import ImageFileContentBlock
from openai.types.beta.threads.text import Text
from openai.types.beta.threads.text_content_block import TextContentBlock
from pydantic import BaseModel, ValidationError
from semantic_kernel.agents import AgentRegistry
from semantic_kernel.agents.open_ai.azure_assistant_agent import AzureAssistantAgent
from semantic_kernel.agents.open_ai.openai_assistant_agent import AssistantAgentThread
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
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_azure_openai_client_and_definition():
client = AsyncMock(spec=AsyncOpenAI)
client.beta = MagicMock()
client.beta.assistants = MagicMock()
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "DeclarativeAgent"
definition.description = "desc"
definition.instructions = "test agent"
definition.tools = []
definition.model = "gpt-4o"
definition.temperature = 1.0
definition.top_p = 1.0
definition.metadata = {}
client.beta.assistants.create = AsyncMock(return_value=definition)
return client, definition
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]
@pytest.fixture
def mock_thread_messages():
class MockMessage:
def __init__(self, id, role, content, assistant_id=None):
self.id = id
self.role = role
self.content = content
self.assistant_id = assistant_id
return [
MockMessage(
id="test_message_id_1",
role="user",
content=[
TextContentBlock(
type="text",
text=Text(
value="Hello",
annotations=[
FilePathAnnotation(
type="file_path",
file_path=FilePath(file_id="test_file_id"),
end_index=5,
start_index=0,
text="Hello",
),
FileCitationAnnotation(
type="file_citation",
file_citation=FileCitation(file_id="test_file_id"),
text="Hello",
start_index=0,
end_index=5,
),
],
),
)
],
),
MockMessage(
id="test_message_id_2",
role="assistant",
content=[
ImageFileContentBlock(type="image_file", image_file=ImageFile(file_id="test_file_id", detail="auto"))
],
assistant_id="assistant_1",
),
]
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")
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
agent = AzureAssistantAgent(
client=client,
definition=definition,
arguments=KernelArguments(test="test"),
kernel=AsyncMock(spec=Kernel),
plugins=[SamplePlugin(), kernel_plugin],
polling_options=AsyncMock(spec=RunPollingOptions),
prompt_template_config=sample_prompt_template_config, # type: ignore
other_arg="test", # type: ignore
)
assert agent.id == "agent123"
assert agent.name == "agentName"
assert agent.description == "desc"
def test_azure_open_ai_settings_create_throws(azure_openai_unit_test_env):
with patch(
"semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings.AzureOpenAISettings.__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 Azure OpenAI settings."):
_, _ = AzureAssistantAgent.setup_resources(api_key="test_api_key")
def test_open_ai_assistant_with_code_interpreter_tool():
tools, resources = AzureAssistantAgent.configure_code_interpreter_tool(file_ids=["file_id"])
assert tools is not None
assert resources is not None
def test_open_ai_assistant_with_file_search_tool():
tools, resources = AzureAssistantAgent.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 = AzureAssistantAgent.configure_response_format(model)
assert response_format is not None
if json_schema_expected:
assert response_format["json_schema"] is not None # type: ignore
def test_configure_response_format_unexpected_type():
with pytest.raises(AgentInitializationException) as exc_info:
AzureAssistantAgent.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:
AzureAssistantAgent.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:
AzureAssistantAgent.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_open_ai_assistant_agent_invoke(arguments, include_args):
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
definition.tools = []
definition.model = "gpt-4o"
definition.response_format = {"type": "json_object"}
definition.temperature = 0.1
definition.top_p = 0.9
definition.metadata = {}
agent = AzureAssistantAgent(client=client, definition=definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
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.assistant_thread_actions.AssistantThreadActions.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_open_ai_assistant_agent_invoke_stream(arguments, include_args):
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
agent = AzureAssistantAgent(client=client, definition=definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
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.assistant_thread_actions.AssistantThreadActions.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_open_ai_assistant_agent_get_channel_keys():
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
agent = AzureAssistantAgent(client=client, definition=definition)
keys = list(agent.get_channel_keys())
assert len(keys) >= 3
@pytest.fixture
def mock_thread():
class MockThread:
id = "test_thread_id"
return MockThread()
async def test_open_ai_assistant_agent_create_channel(mock_thread):
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
agent = AzureAssistantAgent(client=client, definition=definition)
client.beta = MagicMock()
client.beta.assistants = MagicMock()
client.beta.assistants.create = AsyncMock(return_value=definition)
client.beta.threads = MagicMock()
client.beta.threads.create = AsyncMock(return_value=mock_thread)
ch = await agent.create_channel()
assert isinstance(ch, OpenAIAssistantChannel)
assert ch.thread_id == "test_thread_id"
def test_create_openai_client(azure_openai_unit_test_env):
client, model = AzureAssistantAgent.setup_resources(api_key="test_api_key", default_headers={"user_agent": "test"})
assert client is not None
assert client.api_key == "test_api_key"
assert model is not None
def test_create_azure_openai_client(azure_openai_unit_test_env):
client, model = AzureAssistantAgent.setup_resources(
api_key="test_api_key", endpoint="https://test_endpoint.com", default_headers={"user_agent": "test"}
)
assert model is not None
assert client is not None
assert client.api_key == "test_api_key"
assert str(client.base_url) == "https://test_endpoint.com/openai/"
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT"]], indirect=True)
async def test_retrieve_agent_missing_endpoint_throws(kernel, azure_openai_unit_test_env):
with pytest.raises(AgentInitializationException, match="Please provide an Azure OpenAI endpoint"):
_, _ = AzureAssistantAgent.setup_resources(
env_file_path="./", api_key="test_api_key", default_headers={"user_agent": "test"}
)
@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True)
async def test_retrieve_agent_missing_chat_deployment_name_throws(kernel, azure_openai_unit_test_env):
with pytest.raises(AgentInitializationException, match="Please provide an Azure OpenAI deployment name"):
_, _ = AzureAssistantAgent.setup_resources(
env_file_path="./",
api_key="test_api_key",
endpoint="https://test_endpoint.com",
default_headers={"user_agent": "test"},
)
async def test_azure_assistant_agent_from_yaml_minimal(
azure_openai_unit_test_env, mock_azure_openai_client_and_definition
):
spec = """
type: azure_assistant
name: MinimalAgent
model:
id: ${AzureOpenAI:ChatModelId}
connection:
api_key: ${AzureOpenAI:ApiKey}
endpoint: ${AzureOpenAI:Endpoint}
"""
client, definition = mock_azure_openai_client_and_definition
definition.name = "MinimalAgent"
agent = await AgentRegistry.create_from_yaml(spec, client=client)
assert isinstance(agent, AzureAssistantAgent)
assert agent.name == "MinimalAgent"
assert agent.definition.model == "gpt-4o"
async def test_azure_assistant_agent_with_tools(azure_openai_unit_test_env, mock_azure_openai_client_and_definition):
spec = """
type: azure_assistant
name: CodeAgent
description: Uses code interpreter.
model:
id: ${AzureOpenAI:ChatModelId}
connection:
api_key: ${AzureOpenAI:ApiKey}
endpoint: ${AzureOpenAI:Endpoint}
tools:
- type: code_interpreter
options:
file_ids:
- ${AzureOpenAI:FileId1}
"""
client, definition = mock_azure_openai_client_and_definition
definition.name = "CodeAgent"
definition.tools = [{"type": "code_interpreter", "options": {"file_ids": ["file-123"]}}]
agent = await AgentRegistry.create_from_yaml(spec, client=client, extras={"AzureOpenAI:FileId1": "file-123"})
assert agent.name == "CodeAgent"
assert any(t["type"] == "code_interpreter" for t in agent.definition.tools)
async def test_azure_assistant_agent_with_inputs_outputs_template(
azure_openai_unit_test_env, mock_azure_openai_client_and_definition
):
spec = """
type: azure_assistant
name: StoryAgent
model:
id: ${AzureOpenAI:ChatModelId}
connection:
api_key: ${AzureOpenAI: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, definition = mock_azure_openai_client_and_definition
definition.name = "StoryAgent"
agent: AzureAssistantAgent = 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_azure_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_azure_assistant_agent_from_yaml_missing_required_fields():
spec = """
type: azure_assistant
"""
with pytest.raises(AgentInitializationException):
await AgentRegistry.create_from_yaml(spec)
async def test_agent_from_file_success(tmp_path, azure_openai_unit_test_env, mock_azure_openai_client_and_definition):
file_path = tmp_path / "spec.yaml"
file_path.write_text(
"""
type: azure_assistant
name: DeclarativeAgent
model:
id: ${AzureOpenAI:ChatModelId}
connection:
api_key: ${AzureOpenAI:ApiKey}
""",
encoding="utf-8",
)
client, _ = mock_azure_openai_client_and_definition
agent = await AgentRegistry.create_from_file(str(file_path), client=client)
assert agent.name == "DeclarativeAgent"
assert isinstance(agent, AzureAssistantAgent)
async def test_azure_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,334 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai import AsyncOpenAI
from openai.types.beta.assistant import Assistant, ToolResources, ToolResourcesCodeInterpreter, ToolResourcesFileSearch
from openai.types.beta.threads.file_citation_annotation import FileCitation, FileCitationAnnotation
from openai.types.beta.threads.file_path_annotation import FilePath, FilePathAnnotation
from openai.types.beta.threads.image_file import ImageFile
from openai.types.beta.threads.image_file_content_block import ImageFileContentBlock
from openai.types.beta.threads.text import Text
from openai.types.beta.threads.text_content_block import TextContentBlock
from semantic_kernel.agents.chat_completion.chat_completion_agent import ChatCompletionAgent
from semantic_kernel.agents.open_ai.openai_assistant_agent import OpenAIAssistantAgent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
@pytest.fixture
def mock_thread_messages():
class MockMessage:
def __init__(self, role, content, assistant_id=None):
self.role = role
self.content = content
self.assistant_id = assistant_id
return [
MockMessage(
role="user",
content=[
TextContentBlock(
type="text",
text=Text(
value="Hello",
annotations=[
FilePathAnnotation(
type="file_path",
file_path=FilePath(file_id="test_file_id"),
end_index=5,
start_index=0,
text="Hello",
),
FileCitationAnnotation(
type="file_citation",
file_citation=FileCitation(file_id="test_file_id"),
text="Hello",
start_index=0,
end_index=5,
),
],
),
)
],
),
MockMessage(
role="assistant",
content=[
ImageFileContentBlock(type="image_file", image_file=ImageFile(file_id="test_file_id", detail="auto"))
],
assistant_id="assistant_1",
),
]
@pytest.fixture
def mock_assistant():
return Assistant(
created_at=123456789,
object="assistant",
metadata={
"__run_options": json.dumps({
"max_completion_tokens": 100,
"max_prompt_tokens": 50,
"parallel_tool_calls_enabled": True,
"truncation_message_count": 10,
})
},
model="test_model",
description="test_description",
id="test_id",
instructions="test_instructions",
name="test_name",
tools=[{"type": "code_interpreter"}, {"type": "file_search"}], # type: ignore
temperature=0.7,
top_p=0.9,
response_format={"type": "json_object"}, # type: ignore
tool_resources=ToolResources(
code_interpreter=ToolResourcesCodeInterpreter(file_ids=["file1", "file2"]),
file_search=ToolResourcesFileSearch(vector_store_ids=["vector_store1"]),
),
)
async def test_receive_messages():
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = MagicMock(spec=AsyncOpenAI)
client.beta = AsyncMock()
thread_id = "test_thread"
channel = OpenAIAssistantChannel(client=client, thread_id=thread_id)
history = [
MagicMock(spec=ChatMessageContent, role=AuthorRole.USER, items=[TextContent(text="test")]) for _ in range(3)
]
with patch("semantic_kernel.agents.open_ai.assistant_content_generation.create_chat_message"):
await channel.receive(history) # type: ignore
async def test_invoke_agent():
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
agent = OpenAIAssistantAgent(
client=client,
definition=definition,
arguments=KernelArguments(test="test"),
kernel=AsyncMock(spec=Kernel),
)
channel = OpenAIAssistantChannel(client=client, thread_id="test_thread_id")
async def mock_invoke_internal(*args, **kwargs):
for _ in range(3):
yield True, MagicMock(spec=ChatMessageContent)
results = []
with patch(
"semantic_kernel.agents.channels.open_ai_assistant_channel.AssistantThreadActions.invoke",
side_effect=mock_invoke_internal,
):
async for is_visible, message in channel.invoke(agent):
results.append((is_visible, message))
assert len(results) == 3
for is_visible, message in results:
assert is_visible is True
assert isinstance(message, ChatMessageContent)
async def test_invoke_agent_invalid_instance_throws():
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = MagicMock(spec=AsyncOpenAI)
thread_id = "test_thread"
agent = MagicMock(spec=ChatCompletionAgent)
agent._is_deleted = False
channel = OpenAIAssistantChannel(client=client, thread_id=thread_id)
with pytest.raises(AgentChatException, match=f"Agent is not of the expected type {type(OpenAIAssistantAgent)}."):
async for _, _ in channel.invoke(agent):
pass
async def test_invoke_streaming_agent():
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = AsyncMock(spec=AsyncOpenAI)
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
agent = OpenAIAssistantAgent(
client=client,
definition=definition,
arguments=KernelArguments(test="test"),
kernel=AsyncMock(spec=Kernel),
)
channel = OpenAIAssistantChannel(client=client, thread_id="test_thread_id")
results = []
async def mock_invoke_internal(*args, **kwargs):
for _ in range(3):
msg = MagicMock(spec=ChatMessageContent)
yield msg
results.append(msg)
with patch(
"semantic_kernel.agents.channels.open_ai_assistant_channel.AssistantThreadActions.invoke_stream",
side_effect=mock_invoke_internal,
):
async for message in channel.invoke_stream(agent, results):
assert message is not None
assert len(results) == 3
for message in results:
assert isinstance(message, ChatMessageContent)
async def test_invoke_streaming_agent_invalid_instance_throws():
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = MagicMock(spec=AsyncOpenAI)
thread_id = "test_thread"
agent = MagicMock(spec=ChatCompletionAgent)
agent._is_deleted = False
channel = OpenAIAssistantChannel(client=client, thread_id=thread_id)
with pytest.raises(AgentChatException, match=f"Agent is not of the expected type {type(OpenAIAssistantAgent)}."):
async for _ in channel.invoke_stream(agent, []):
pass
async def test_invoke_agent_wrong_type():
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
client = MagicMock(spec=AsyncOpenAI)
thread_id = "test_thread"
agent = MagicMock()
channel = OpenAIAssistantChannel(client=client, thread_id=thread_id)
with pytest.raises(AgentChatException, match="Agent is not of the expected type"):
async for _ in channel.invoke(agent):
pass
async def test_get_history(mock_thread_messages, mock_assistant, openai_unit_test_env):
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
async def mock_list_messages(*args, **kwargs) -> Any:
return MagicMock(data=mock_thread_messages)
async def mock_retrieve_assistant(*args, **kwargs) -> Any:
return mock_assistant
mock_client = MagicMock(spec=AsyncOpenAI)
mock_client.beta = MagicMock()
mock_client.beta.threads = MagicMock()
mock_client.beta.threads.messages = MagicMock()
mock_client.beta.threads.messages.list = AsyncMock(side_effect=mock_list_messages)
mock_client.beta.assistants = MagicMock()
mock_client.beta.assistants.retrieve = AsyncMock(side_effect=mock_retrieve_assistant)
thread_id = "test_thread"
channel = OpenAIAssistantChannel(client=mock_client, thread_id=thread_id)
results = []
async for content in channel.get_history():
results.append(content)
assert len(results) == 2
mock_client.beta.threads.messages.list.assert_awaited_once_with(thread_id=thread_id, limit=100, order="desc")
async def test_reset_channel(mock_thread_messages, mock_assistant, openai_unit_test_env):
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
async def mock_list_messages(*args, **kwargs) -> Any:
return MagicMock(data=mock_thread_messages)
async def mock_retrieve_assistant(*args, **kwargs) -> Any:
return mock_assistant
mock_client = MagicMock(spec=AsyncOpenAI)
mock_client.beta = MagicMock()
mock_client.beta.threads = MagicMock()
mock_client.beta.threads.messages = MagicMock()
mock_client.beta.threads.messages.list = AsyncMock(side_effect=mock_list_messages)
mock_client.beta.assistants = MagicMock()
mock_client.beta.assistants.retrieve = AsyncMock(side_effect=mock_retrieve_assistant)
mock_client.beta.threads.delete = AsyncMock()
thread_id = "test_thread"
channel = OpenAIAssistantChannel(client=mock_client, thread_id=thread_id)
results = []
async for content in channel.get_history():
results.append(content)
assert len(results) == 2
mock_client.beta.threads.messages.list.assert_awaited_once_with(thread_id=thread_id, limit=100, order="desc")
await channel.reset()
assert channel.thread_id is not None
async def test_reset_channel_error_throws_exception(mock_thread_messages, mock_assistant, openai_unit_test_env):
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
async def mock_list_messages(*args, **kwargs) -> Any:
return MagicMock(data=mock_thread_messages)
async def mock_retrieve_assistant(*args, **kwargs) -> Any:
return mock_assistant
mock_client = MagicMock(spec=AsyncOpenAI)
mock_client.beta = MagicMock()
mock_client.beta.threads = MagicMock()
mock_client.beta.threads.messages = MagicMock()
mock_client.beta.threads.messages.list = AsyncMock(side_effect=mock_list_messages)
mock_client.beta.assistants = MagicMock()
mock_client.beta.assistants.retrieve = AsyncMock(side_effect=mock_retrieve_assistant)
mock_client.beta.threads.delete = AsyncMock(side_effect=Exception("Test error"))
thread_id = "test_thread"
channel = OpenAIAssistantChannel(client=mock_client, thread_id=thread_id)
results = []
async for content in channel.get_history():
results.append(content)
assert len(results) == 2
mock_client.beta.threads.messages.list.assert_awaited_once_with(thread_id=thread_id, limit=100, order="desc")
with pytest.raises(Exception, match="Test error"):
await channel.reset()
async def test_channel_receive_fcc_skipped(openai_unit_test_env):
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[FunctionCallContent(function_name="test_function")])
client = MagicMock(spec=AsyncOpenAI)
channel = OpenAIAssistantChannel(client=client, thread_id="test_thread")
await channel.receive([message])
@@ -0,0 +1,579 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai import AsyncOpenAI
from openai.types.beta.assistant import Assistant
from pydantic import BaseModel, ValidationError
from semantic_kernel.agents import AgentRegistry, AgentResponseItem, OpenAIAssistantAgent
from semantic_kernel.agents.open_ai.openai_assistant_agent import AssistantAgentThread
from semantic_kernel.agents.open_ai.run_polling_options import RunPollingOptions
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
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_and_definition():
client = AsyncMock(spec=AsyncOpenAI)
client.beta = MagicMock()
client.beta.assistants = MagicMock()
definition = AsyncMock(spec=Assistant)
definition.id = "agent123"
definition.name = "DeclarativeAgent"
definition.description = "desc"
definition.instructions = "test agent"
definition.tools = []
definition.model = "gpt-4o"
definition.temperature = 1.0
definition.top_p = 1.0
definition.metadata = {}
client.beta.assistants.create = AsyncMock(return_value=definition)
return client, definition
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(openai_client, assistant_definition):
sample_prompt_template_config = PromptTemplateConfig(
template="template",
)
kernel_plugin = KernelPlugin(name="expected_plugin_name", description="expected_plugin_description")
agent = OpenAIAssistantAgent(
client=AsyncMock(spec=AsyncOpenAI),
definition=assistant_definition,
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."):
_, _ = OpenAIAssistantAgent.setup_resources(api_key="test_api_key")
def test_open_ai_assistant_with_code_interpreter_tool():
tools, resources = OpenAIAssistantAgent.configure_code_interpreter_tool(file_ids=["file_id"])
assert tools is not None
assert resources is not None
def test_open_ai_assistant_with_file_search_tool():
tools, resources = OpenAIAssistantAgent.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 = OpenAIAssistantAgent.configure_response_format(model)
assert response_format is not None
if json_schema_expected:
assert response_format["json_schema"] is not None # type: ignore
def test_configure_response_format_unexpected_type():
with pytest.raises(AgentInitializationException) as exc_info:
OpenAIAssistantAgent.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:
OpenAIAssistantAgent.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:
OpenAIAssistantAgent.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_open_ai_assistant_agent_get_response(arguments, include_args, openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
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.assistant_thread_actions.AssistantThreadActions.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_open_ai_assistant_agent_get_response_exception(
arguments, include_args, openai_client, assistant_definition
):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
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.assistant_thread_actions.AssistantThreadActions.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_open_ai_assistant_agent_invoke(arguments, include_args, openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
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.assistant_thread_actions.AssistantThreadActions.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
async def test_open_ai_assistant_agent_invoke_message_ordering(openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
tool_call_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
content="",
items=[FunctionCallContent(name="ToolA", arguments="{}")],
)
tool_result_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
content="",
items=[FunctionResultContent(name="ToolA", result="$9.99", id="func_1")],
)
assistant_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="Here is your answer.")
emitted_callback_messages = []
yielded_messages = []
async def on_intermediate_message(msg: ChatMessageContent):
emitted_callback_messages.append(msg)
async def fake_invoke(*args, **kwargs):
yield False, tool_call_msg
yield False, tool_result_msg
yield True, assistant_msg
with patch(
"semantic_kernel.agents.open_ai.assistant_thread_actions.AssistantThreadActions.invoke",
side_effect=fake_invoke,
):
async for item in agent.invoke(
messages="test", thread=mock_thread, on_intermediate_message=on_intermediate_message
):
yielded_messages.append(item)
assert emitted_callback_messages == [tool_call_msg, tool_result_msg]
assert yielded_messages == [AgentResponseItem(message=assistant_msg, thread=mock_thread)]
@pytest.mark.parametrize(
"arguments, include_args",
[
pytest.param({"extra_args": "extra_args"}, True),
pytest.param(None, False),
],
)
async def test_open_ai_assistant_agent_invoke_stream(arguments, include_args, openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
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.assistant_thread_actions.AssistantThreadActions.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
@pytest.mark.parametrize(
"arguments, include_args",
[
pytest.param({"extra_args": "extra_args"}, True),
pytest.param(None, False),
],
)
async def test_open_ai_assistant_agent_invoke_stream_with_on_new_message_callback(
arguments, include_args, openai_client, assistant_definition
):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
mock_thread = AsyncMock(spec=AssistantAgentThread)
results = []
final_chat_history = ChatHistory()
async def handle_stream_completion(message: ChatMessageContent) -> None:
final_chat_history.add_message(message)
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT, content="", items=[FunctionCallContent(name="ToolA", arguments="{}")]
)
streamed_msg = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="fake content", choice_index=0)
async def fake_invoke(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield streamed_msg
kwargs = arguments if include_args else {}
with patch(
"semantic_kernel.agents.open_ai.assistant_thread_actions.AssistantThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for item in agent.invoke_stream(
messages="test", thread=mock_thread, on_intermediate_message=handle_stream_completion, **kwargs
):
results.append(item)
assert len(results) == 1
assert results[0].message.content == "fake content"
assert len(final_chat_history.messages) == 1
assert final_chat_history.messages[0] == tool_msg
def test_open_ai_assistant_agent_get_channel_keys(openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
keys = list(agent.get_channel_keys())
assert len(keys) >= 3
async def test_open_ai_assistant_agent_create_channel(openai_client, assistant_definition):
from semantic_kernel.agents.channels.open_ai_assistant_channel import OpenAIAssistantChannel
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
ch = await agent.create_channel()
assert isinstance(ch, OpenAIAssistantChannel)
assert ch.thread_id == "test_thread_id"
def test_create_openai_client(openai_unit_test_env):
client, model = OpenAIAssistantAgent.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."):
_, _ = OpenAIAssistantAgent.setup_resources(env_file_path="./", default_headers={"user_agent": "test"})
@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_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 model ID is required."):
_, _ = OpenAIAssistantAgent.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_and_definition):
spec = """
type: openai_assistant
name: MinimalAgent
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
"""
client, definition = mock_openai_client_and_definition
definition.name = "MinimalAgent"
agent = await AgentRegistry.create_from_yaml(spec, client=client)
assert isinstance(agent, OpenAIAssistantAgent)
assert agent.name == "MinimalAgent"
assert agent.definition.model == "gpt-4o"
async def test_openai_assistant_agent_with_tools(openai_unit_test_env, mock_openai_client_and_definition):
spec = """
type: openai_assistant
name: CodeAgent
description: Uses code interpreter.
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
tools:
- type: code_interpreter
options:
file_ids:
- ${OpenAI:FileId1}
"""
client, definition = mock_openai_client_and_definition
definition.name = "CodeAgent"
definition.tools = [{"type": "code_interpreter", "options": {"file_ids": ["file-123"]}}]
agent = await AgentRegistry.create_from_yaml(spec, client=client, extras={"OpenAI:FileId1": "file-123"})
assert agent.name == "CodeAgent"
assert any(t["type"] == "code_interpreter" for t in agent.definition.tools)
async def test_openai_assistant_agent_with_inputs_outputs_template(
openai_unit_test_env, mock_openai_client_and_definition
):
spec = """
type: openai_assistant
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, definition = mock_openai_client_and_definition
definition.name = "StoryAgent"
agent: OpenAIAssistantAgent = 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_assistant
"""
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_and_definition):
file_path = tmp_path / "spec.yaml"
file_path.write_text(
"""
type: openai_assistant
name: DeclarativeAgent
model:
id: ${OpenAI:ChatModelId}
connection:
api_key: ${OpenAI:ApiKey}
""",
encoding="utf-8",
)
client, _ = mock_openai_client_and_definition
agent = await AgentRegistry.create_from_file(str(file_path), client=client)
assert agent.name == "DeclarativeAgent"
assert isinstance(agent, OpenAIAssistantAgent)
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)
async def test_openai_assistant_agent_get_response_passes_function_choice_behavior(openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
thread = AsyncMock(spec=AssistantAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.open_ai.assistant_thread_actions.AssistantThreadActions.invoke",
side_effect=fake_invoke,
):
await agent.get_response(messages="message", thread=thread, function_choice_behavior=fcb)
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_openai_assistant_agent_invoke_passes_function_choice_behavior(openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
thread = AsyncMock(spec=AssistantAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.open_ai.assistant_thread_actions.AssistantThreadActions.invoke",
side_effect=fake_invoke,
):
async for _ in agent.invoke(messages="message", thread=thread, function_choice_behavior=fcb):
pass
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_openai_assistant_agent_invoke_stream_passes_function_choice_behavior(
openai_client, assistant_definition
):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
thread = AsyncMock(spec=AssistantAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.open_ai.assistant_thread_actions.AssistantThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for _ in agent.invoke_stream(messages="message", thread=thread, function_choice_behavior=fcb):
pass
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_openai_assistant_agent_get_response_no_fcb_passes_none(openai_client, assistant_definition):
agent = OpenAIAssistantAgent(client=openai_client, definition=assistant_definition)
thread = AsyncMock(spec=AssistantAgentThread)
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.open_ai.assistant_thread_actions.AssistantThreadActions.invoke",
side_effect=fake_invoke,
):
await agent.get_response(messages="message", thread=thread)
assert captured_kwargs.get("function_choice_behavior") is None