chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user