chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

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,152 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import AsyncIterator, Awaitable, Callable
from typing import Any, Generic, Protocol, TypeVar
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
from semantic_kernel.contents import ChatMessageContent
DEFAULT_MAX_ATTEMPTS = 3
DEFAULT_BACKOFF_SECONDS = 1
class ChatResponseProtocol(Protocol):
"""Represents a single response item returned by the agent."""
@property
def message(self) -> ChatMessageContent: ...
@property
def thread(self) -> AgentThread | None: ...
class ChatAgentProtocol(Protocol):
"""Protocol describing the common agent interface used by the tests."""
async def get_response(
self, messages: str | list[str] | None, thread: object | None = None
) -> ChatResponseProtocol: ...
def invoke(
self, messages: str | list[str] | None, thread: object | None = None
) -> AsyncIterator[ChatResponseProtocol]: ...
def invoke_stream(
self, messages: str | list[str] | None, thread: object | None = None
) -> AsyncIterator[ChatResponseProtocol]: ...
TAgent = TypeVar("TAgent", bound=ChatAgentProtocol)
async def run_with_retry(
coro: Callable[..., Awaitable[Any]],
*args,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
"""
Execute an async callable with retry/backoff logic.
Args:
coro: The async function to call
args: Positional args to pass to the function
attempts: How many times to attempt before giving up
backoff_seconds: The initial backoff in seconds, doubled after each failure
kwargs: Keyword args to pass to the function
Returns:
Whatever the async function returns
Raises:
Exception: If the function fails after the specified number of attempts
"""
delay = backoff_seconds
for attempt in range(1, attempts + 1):
try:
return await coro(*args, **kwargs)
except Exception:
if attempt == attempts:
raise
await asyncio.sleep(delay)
delay *= 2
raise RuntimeError("Unexpected error: run_with_retry exit.")
class AgentTestBase(Generic[TAgent]):
"""Common test base that wraps all agent invocation patterns with retry logic.
Each integration test can inherit from this or use its methods directly.
"""
async def get_response_with_retry(
self,
agent: Agent,
messages: str | list[str] | None,
thread: Any | None = None,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
) -> AgentResponseItem[ChatMessageContent]:
"""Wraps agent.get_response(...) in run_with_retry."""
return await run_with_retry(
agent.get_response, messages=messages, thread=thread, attempts=attempts, backoff_seconds=backoff_seconds
)
async def get_invoke_with_retry(
self,
agent: Any,
messages: str | list[str] | None,
thread: Any | None = None,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
) -> list[AgentResponseItem[ChatMessageContent]]:
"""Wraps agent.invoke(...) in run_with_retry.
Collects generator results in a list before returning them.
"""
return await run_with_retry(
self._collect_from_invoke,
agent,
messages,
thread=thread,
attempts=attempts,
backoff_seconds=backoff_seconds,
)
async def get_invoke_stream_with_retry(
self,
agent: Any,
messages: str | list[str] | None,
thread: Any | None = None,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
) -> list[AgentResponseItem[ChatMessageContent]]:
"""Wraps agent.invoke_stream(...) in run_with_retry.
Collects streaming results in a list before returning them."""
return await run_with_retry(
self._collect_from_invoke_stream,
agent,
messages,
thread=thread,
attempts=attempts,
backoff_seconds=backoff_seconds,
)
async def _collect_from_invoke(
self, agent: Agent, messages: str | list[str] | None, thread: Any | None = None
) -> list[AgentResponseItem[ChatMessageContent]]:
results: list[AgentResponseItem[ChatMessageContent]] = []
async for response in agent.invoke(messages=messages, thread=thread):
results.append(response)
return results
async def _collect_from_invoke_stream(
self, agent: Agent, messages: str | list[str] | None, thread: Any | None = None
) -> list[AgentResponseItem[ChatMessageContent]]:
results: list[AgentResponseItem[ChatMessageContent]] = []
async for response in agent.invoke_stream(messages=messages, thread=thread):
results.append(response)
return results
@@ -0,0 +1,278 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from azure.ai.agents.models import CodeInterpreterTool, FileInfo, FileSearchTool
from azure.identity.aio import AzureCliCredential
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class TestAzureAIAgentIntegration:
@pytest.fixture
async def azureai_agent(self, request):
ai_agent_settings = AzureAIAgentSettings()
async with (
AzureCliCredential() as creds,
AzureAIAgent.create_client(credential=creds) as client,
):
tools, tool_resources, plugins = [], {}, []
params = getattr(request, "param", {})
if params.get("enable_code_interpreter"):
ci_tool = CodeInterpreterTool()
tools.extend(ci_tool.definitions)
tool_resources.update(ci_tool.resources)
if params.get("enable_file_search"):
pdf_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
"resources",
"employees.pdf",
)
file: FileInfo = await client.agents.files.upload_and_poll(
file_path=pdf_file_path, purpose="assistants"
)
vector_store = await client.agents.vector_stores.create_and_poll(
file_ids=[file.id], name="my_vectorstore"
)
fs_tool = FileSearchTool(vector_store_ids=[vector_store.id])
tools.extend(fs_tool.definitions)
tool_resources.update(fs_tool.resources)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
agent_definition = await client.agents.create_agent(
model=ai_agent_settings.model_deployment_name,
tools=tools,
tool_resources=tool_resources,
name="SKPythonIntegrationTestAgent",
instructions="You are a helpful assistant that help users with their questions.",
)
azureai_agent = AzureAIAgent(
client=client,
definition=agent_definition,
plugins=plugins,
)
yield azureai_agent # yield agent for test method to use
# cleanup
await azureai_agent.client.agents.delete_agent(azureai_agent.id)
async def test_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(azureai_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
assert "thread_id" in response.message.metadata
assert "run_id" in response.message.metadata
async def test_get_response_with_thread(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
azureai_agent, messages=user_message, thread=thread
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
async def test_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
async def test_invoke_with_thread(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages=user_message, thread=thread)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
async def test_invoke_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(azureai_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_invoke_stream_with_thread(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
azureai_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = """
Using Python, sum the number of animals for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
response = await agent_test_base.get_response_with_retry(azureai_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = """
Using Python, sum the number of animals for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_invoke_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter streaming."""
input_text = """
Using Python, sum the number of animals for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_stream_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_file_search": True}], indirect=True)
async def test_file_search_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
response = await agent_test_base.get_response_with_retry(azureai_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize("azureai_agent", [{"enable_file_search": True}], indirect=True)
async def test_file_search_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize("azureai_agent", [{"enable_file_search": True}], indirect=True)
async def test_file_search_invoke_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter streaming."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_stream_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize("azureai_agent", [{"enable_kernel_function": True}], indirect=True)
async def test_function_calling_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
azureai_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize("azureai_agent", [{"enable_kernel_function": True}], indirect=True)
async def test_function_calling_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
azureai_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize("azureai_agent", [{"enable_kernel_function": True}], indirect=True)
async def test_function_calling_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
azureai_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
import pytest
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
class WeatherPlugin:
"""Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
@pytest.fixture
def kernel_with_dummy_function() -> Kernel:
kernel = Kernel()
kernel.add_plugin(WeatherPlugin(), plugin_name="weather")
return kernel
@@ -0,0 +1,141 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
import pytest
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent
from semantic_kernel.contents.binary_content import BinaryContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
class TestBedrockAgentIntegration:
@pytest.fixture(autouse=True)
async def setup_and_teardown(self, request):
"""Setup and teardown for the test.
This is run for each test function, i.e. each test function will have its own instance of the agent.
"""
try:
self.bedrock_agent = await BedrockAgent.create_and_prepare_agent(
f"semantic-kernel-integration-test-agent-{uuid.uuid4()}",
"You are a helpful assistant that help users with their questions.",
)
if hasattr(request, "param"):
if "enable_code_interpreter" in request.param:
await self.bedrock_agent.create_code_interpreter_action_group()
if "kernel" in request.param:
self.bedrock_agent.kernel = request.getfixturevalue(request.param.get("kernel"))
if "enable_kernel_function" in request.param:
await self.bedrock_agent.create_kernel_function_action_group()
except Exception as e:
pytest.fail("Failed to create agent")
raise e
# Yield control to the test
yield
# Clean up
try:
await self.bedrock_agent.delete_agent()
except Exception as e:
pytest.fail(f"Failed to delete agent: {e}")
raise e
@pytest.mark.asyncio
async def test_invoke(self):
"""Test invoke of the agent."""
async for response in self.bedrock_agent.invoke(messages="Hello"):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.asyncio
async def test_invoke_stream(self):
"""Test invoke stream of the agent."""
async for response in self.bedrock_agent.invoke_stream(messages="Hello"):
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.asyncio
@pytest.mark.parametrize("setup_and_teardown", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter(self):
"""Test code interpreter."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
binary_item: BinaryContent | None = None
async for response in self.bedrock_agent.invoke(messages=input_text):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
if not binary_item:
binary_item = next((item for item in response.message.items if isinstance(item, BinaryContent)), None)
assert binary_item
@pytest.mark.asyncio
@pytest.mark.parametrize("setup_and_teardown", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_stream(self):
"""Test code interpreter streaming."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
binary_item: BinaryContent | None = None
async for response in self.bedrock_agent.invoke_stream(messages=input_text):
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
binary_item = next((item for item in response.message.items if isinstance(item, BinaryContent)), None)
assert binary_item
@pytest.mark.asyncio
@pytest.mark.parametrize(
"setup_and_teardown",
[
{
"enable_kernel_function": True,
"kernel": "kernel_with_dummy_function",
},
],
indirect=True,
)
async def test_function_calling(self):
"""Test function calling."""
async for response in self.bedrock_agent.invoke(
messages="What is the weather in Seattle?",
):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.asyncio
@pytest.mark.parametrize(
"setup_and_teardown",
[
{
"enable_kernel_function": True,
"kernel": "kernel_with_dummy_function",
},
],
indirect=True,
)
async def test_function_calling_stream(self):
"""Test function calling streaming."""
full_message: str = ""
async for response in self.bedrock_agent.invoke_stream(
messages="What is the weather in Seattle?",
):
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
@@ -0,0 +1,272 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""A sample Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class TestChatCompletionAgentIntegration:
@pytest.fixture(params=["azure", "openai"])
async def chat_completion_agent(self, request):
raw_param = request.param
if isinstance(raw_param, str):
agent_service, params = raw_param, {}
elif isinstance(raw_param, tuple) and len(raw_param) == 2:
agent_service, params = raw_param
else:
raise ValueError(f"Unsupported param format: {raw_param}")
plugins = []
service = (
AzureChatCompletion(credential=AzureCliCredential()) if agent_service == "azure" else OpenAIChatCompletion()
)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
agent = ChatCompletionAgent(
service=service,
name="SKPythonIntegrationTestChatCompletionAgent",
instructions="You are a helpful assistant that help users with their questions.",
plugins=plugins,
)
yield agent # yield agent for test method to use
# region Simple 'Hello' messages tests
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(chat_completion_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response_with_thread(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
chat_completion_agent,
messages=user_message,
thread=thread,
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
responses = await agent_test_base.get_invoke_with_retry(chat_completion_agent, messages="Hello")
assert len(responses) > 0
usage: CompletionUsage = CompletionUsage()
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
if response.metadata.get("usage"):
usage += response.metadata["usage"]
assert usage.prompt_tokens > 0
assert usage.completion_tokens > 0
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_with_thread(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(
chat_completion_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(chat_completion_agent, messages="Hello")
assert len(responses) > 0
usage: CompletionUsage = CompletionUsage()
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
if response.metadata.get("usage"):
usage += response.metadata["usage"]
assert usage.prompt_tokens > 0
assert usage.completion_tokens > 0
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream_with_thread(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
chat_completion_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
# endregion
# region Function calling tests
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["chat_completion_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_get_response(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
chat_completion_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["chat_completion_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_invoke(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
chat_completion_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["chat_completion_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_stream(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
chat_completion_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
# endregion
# region Image Content tests
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
pytest.param(
("openai", {"enable_kernel_function": True}),
marks=pytest.mark.xfail(reason="OpenAI service raise error for downloading image from URL"),
),
],
indirect=["chat_completion_agent"],
ids=["azure-image-content-streaming", "openai-image-content-streaming"],
)
async def test_image_content_stream(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling streaming."""
IMAGE_URI = (
"https://raw.githubusercontent.com/microsoft/semantic-kernel/main/python/tests/assets/sample_image.jpg"
)
image_content_remote = ImageContent(uri=IMAGE_URI)
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
chat_completion_agent,
messages=ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="What is in this image?"), image_content_remote],
),
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert full_message is not None
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from tests.integration.agents.agent_test_base import AgentTestBase, ChatAgentProtocol
@pytest.fixture
def agent_test_base() -> AgentTestBase[ChatAgentProtocol]:
"""Provides a single AgentTestBase instance that all tests can use.
Typed as a Generic over any ChatAgentProtocol.
"""
return AgentTestBase()
@@ -0,0 +1,417 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.agents import AzureAssistantAgent, OpenAIAssistantAgent
from semantic_kernel.connectors.ai.open_ai import AzureOpenAISettings, OpenAISettings
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""A sample Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class TestOpenAIAssistantAgentIntegration:
@pytest.fixture(params=["azure", "openai"])
async def assistant_agent(self, request):
raw_param = request.param
if isinstance(raw_param, str):
agent_type, params = raw_param, {}
elif isinstance(raw_param, tuple) and len(raw_param) == 2:
agent_type, params = raw_param
else:
raise ValueError(f"Unsupported param format: {raw_param}")
tools, tool_resources, plugins = [], {}, []
if agent_type == "azure":
client = AzureAssistantAgent.create_client(credential=AzureCliCredential())
model = AzureOpenAISettings().chat_deployment_name
AgentClass = AzureAssistantAgent
else: # agent_type == "openai"
client = OpenAIAssistantAgent.create_client()
model = OpenAISettings().chat_model_id
AgentClass = OpenAIAssistantAgent
if params.get("enable_code_interpreter"):
code_interpreter_tool, code_interpreter_tool_resources = (
AzureAssistantAgent.configure_code_interpreter_tool()
if agent_type == "azure"
else OpenAIAssistantAgent.configure_code_interpreter_tool()
)
tools.extend(code_interpreter_tool)
tool_resources.update(code_interpreter_tool_resources)
if params.get("enable_file_search"):
pdf_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "employees.pdf"
)
with open(pdf_file_path, "rb") as file:
file = await client.files.create(file=file, purpose="assistants")
vector_store = await client.vector_stores.create(
name="assistant_file_search_int_tests",
file_ids=[file.id],
)
file_search_tool, file_search_tool_resources = (
AzureAssistantAgent.configure_file_search_tool(vector_store.id)
if agent_type == "azure"
else OpenAIAssistantAgent.configure_file_search_tool(vector_store.id)
)
tools.extend(file_search_tool)
tool_resources.update(file_search_tool_resources)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
definition = await client.beta.assistants.create(
model=model,
tools=tools,
tool_resources=tool_resources,
name="SKPythonIntegrationTestAssistantAgent",
instructions="You are a helpful assistant that help users with their questions.",
)
agent = AgentClass(
client=client,
definition=definition,
plugins=plugins,
)
yield agent # yield agent for test method to use
# cleanup
await client.beta.assistants.delete(agent.id)
# region Simple 'Hello' messages tests
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(assistant_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
assert "thread_id" in response.message.metadata
assert "run_id" in response.message.metadata
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response_with_thread(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
assistant_agent, messages=user_message, thread=thread
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
responses = await agent_test_base.get_invoke_with_retry(assistant_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_with_thread(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(
assistant_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(assistant_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream_with_thread(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
assistant_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
# endregion
# region Code interpreter tests
@pytest.mark.parametrize(
"assistant_agent",
[
pytest.param(
("azure", {"enable_code_interpreter": True}), marks=pytest.mark.xfail(reason="Service outage")
),
pytest.param(
("openai", {"enable_code_interpreter": True}),
),
],
indirect=["assistant_agent"],
ids=["azure-code-interpreter", "openai-code-interpreter"],
)
async def test_code_interpreter_get_response(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
response = await agent_test_base.get_response_with_retry(assistant_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize(
"assistant_agent",
[
pytest.param(
("azure", {"enable_code_interpreter": True}), marks=pytest.mark.xfail(reason="Service outage")
),
pytest.param(
("openai", {"enable_code_interpreter": True}),
),
],
indirect=["assistant_agent"],
ids=["azure-code-interpreter", "openai-code-interpreter"],
)
async def test_code_interpreter_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize(
"assistant_agent",
[
pytest.param(
("azure", {"enable_code_interpreter": True}), marks=pytest.mark.xfail(reason="Service outage")
),
pytest.param(
("openai", {"enable_code_interpreter": True}),
),
],
indirect=["assistant_agent"],
ids=["azure-code-interpreter", "openai-code-interpreter"],
)
async def test_code_interpreter_invoke_stream(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter streaming."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_stream_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
# endregion
# region File search tests
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["assistant_agent"],
ids=["azure-file-search", "openai-file-search"],
)
async def test_file_search_get_response(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
response = await agent_test_base.get_response_with_retry(assistant_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["assistant_agent"],
ids=["azure-file-search", "openai-file-search"],
)
async def test_file_search_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["assistant_agent"],
ids=["azure-file-search", "openai-file-search"],
)
async def test_file_search_invoke_stream(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter streaming."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_stream_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
# endregion
# region Function calling tests
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["assistant_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_get_response(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
assistant_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["assistant_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
assistant_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["assistant_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_stream(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
assistant_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
# endregion
@@ -0,0 +1,420 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from azure.identity import AzureCliCredential
from pydantic import BaseModel
from semantic_kernel.agents import AzureResponsesAgent, OpenAIResponsesAgent
from semantic_kernel.connectors.ai.open_ai import AzureOpenAISettings, OpenAISettings
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""A sample Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class Step(BaseModel):
explanation: str
output: str
class Reasoning(BaseModel):
steps: list[Step]
final_answer: str
class TestOpenAIResponsesAgentIntegration:
@pytest.fixture(params=["azure", "openai"])
async def responses_agent(self, request):
raw_param = request.param
if isinstance(raw_param, str):
agent_type, params = raw_param, {}
elif isinstance(raw_param, tuple) and len(raw_param) == 2:
agent_type, params = raw_param
else:
raise ValueError(f"Unsupported param format: {raw_param}")
tools, plugins, text = [], [], None
if agent_type == "azure":
client = AzureResponsesAgent.create_client(credential=AzureCliCredential())
model = AzureOpenAISettings().chat_deployment_name
AgentClass = AzureResponsesAgent
else: # agent_type == "openai"
client = OpenAIResponsesAgent.create_client()
model = OpenAISettings().chat_model_id
AgentClass = OpenAIResponsesAgent
if params.get("enable_web_search"):
web_search_tool = OpenAIResponsesAgent.configure_web_search_tool()
tools.append(web_search_tool)
if params.get("enable_structured_outputs"):
text = OpenAIResponsesAgent.configure_response_format(Reasoning)
if params.get("enable_file_search"):
pdf_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "employees.pdf"
)
with open(pdf_file_path, "rb") as file:
file = await client.files.create(file=file, purpose="assistants")
vector_store = await client.vector_stores.create(
name="responses_file_search_int_tests",
file_ids=[file.id],
)
file_search_tool = (
AzureResponsesAgent.configure_file_search_tool(vector_store.id)
if agent_type == "azure"
else OpenAIResponsesAgent.configure_file_search_tool(vector_store.id)
)
tools.append(file_search_tool)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
agent = AgentClass(
ai_model_id=model,
client=client,
name="SKPythonIntegrationTestResponsesAgent",
instructions="You are a helpful agent that help users with their questions.",
plugins=plugins,
tools=tools,
text=text,
)
yield agent # yield agent for test method to use
# region Simple 'Hello' messages tests
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(responses_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
assert "thread_id" in response.message.metadata
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response_with_thread(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
responses_agent, messages=user_message, thread=thread
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
async for response in responses_agent.invoke(messages="Hello"):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_with_thread(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(
responses_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(responses_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream_with_thread(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
responses_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
# endregion
# region Web Search tests
@pytest.mark.parametrize(
"responses_agent",
[
# Azure OpenAI Responses API doesn't yet support the web search tool
("openai", {"enable_web_search": True}),
],
indirect=["responses_agent"],
ids=["openai-web-search-get-response"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when using the web search tool.")
async def test_web_search_get_response(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Find articles about the latest AI trends."
response = await responses_agent.get_response(messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
# endregion
# region File search tests
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["responses_agent"],
ids=["azure-file-search-get-response", "openai-file-search-get-response"],
)
@pytest.mark.xfail(reason="The Responses API is unstable and is throwing 500s.")
async def test_file_search_get_response(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
response = await agent_test_base.get_response_with_retry(responses_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["responses_agent"],
ids=["azure-file-search-invoke", "openai-file-search-invoke"],
)
@pytest.mark.xfail(reason="The Responses API is unstable and is throwing 500s.")
async def test_file_search_invoke(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_with_retry(responses_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["responses_agent"],
ids=["azure-file-search-invoke-stream", "openai-file-search-invoke-stream"],
)
@pytest.mark.xfail(reason="The Responses API is unstable and is throwing 500s.")
async def test_file_search_invoke_stream(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter streaming."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_stream_with_retry(responses_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
# endregion
# region Function calling tests
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["responses_agent"],
ids=["azure-function-calling-get-response", "openai-function-calling-get-response"],
)
async def test_function_calling_get_response(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
responses_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["responses_agent"],
ids=["azure-function-calling-invoke", "openai-function-calling-invoke"],
)
async def test_function_calling_invoke(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
responses_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["responses_agent"],
ids=["azure-function-calling-invoke-stream", "openai-function-calling-invoke-stream"],
)
async def test_function_calling_stream(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
responses_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
# endregion
# region Structured Outputs
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_structured_outputs": True}),
("openai", {"enable_structured_outputs": True}),
],
indirect=["responses_agent"],
ids=["azure-structured-outputs-get-response", "openai-structured-outputs-get-response"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when configuring structured outputs.")
async def test_structured_outputs_get_response(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test structured outputs get response."""
response = await agent_test_base.get_response_with_retry(
responses_agent,
messages="how can I solve 8x + 7y = -23, and 4x=12?",
)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert Reasoning.model_validate_json(response.message.content)
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_structured_outputs": True}),
("openai", {"enable_structured_outputs": True}),
],
indirect=["responses_agent"],
ids=["azure-structured-outputs-invoke", "openai-structured-outputs-invoke"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when configuring structured outputs.")
async def test_structured_outputs_invoke(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test structured outputs invoke."""
responses = await agent_test_base.get_invoke_with_retry(
responses_agent,
messages="how can I solve 8x + 7y = -23, and 4x=12?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert Reasoning.model_validate_json(response.message.content)
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_structured_outputs": True}),
("openai", {"enable_structured_outputs": True}),
],
indirect=["responses_agent"],
ids=["azure-structured-outputs-invoke-stream", "openai-structured-outputs-invoke-stream"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when configuring structured outputs.")
async def test_structured_outputs_stream(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test structured outputs streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
responses_agent,
messages="how can I solve 8x + 7y = -23, and 4x=12?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert Reasoning.model_validate_json(full_message)
# endregion
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.audio_to_text_client_base import AudioToTextClientBase
from semantic_kernel.connectors.ai.open_ai import AzureAudioToText, OpenAIAudioToText
from tests.utils import is_service_setup_for_testing
# There is only the whisper model available on Azure OpenAI for audio to text. And that model is
# only available in the North Switzerland region. Therefore, the endpoint is different than the one
# we use for other services.
azure_setup = is_service_setup_for_testing(["AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT"])
class AudioToTextTestBase:
"""Base class for testing audio-to-text services."""
@pytest.fixture(scope="module")
def services(self) -> dict[str, AudioToTextClientBase]:
"""Return audio-to-text services."""
return {
"openai": OpenAIAudioToText(),
"azure_openai": AzureAudioToText(
endpoint=os.environ["AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT"], credential=AzureCliCredential()
)
if azure_setup
else None,
}
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from semantic_kernel.connectors.ai.audio_to_text_client_base import AudioToTextClientBase
from semantic_kernel.contents import AudioContent
from tests.integration.audio_to_text.audio_to_text_test_base import AudioToTextTestBase, azure_setup
pytestmark = pytest.mark.parametrize(
"service_id, audio_content, expected_text",
[
pytest.param(
"openai",
AudioContent.from_audio_file(os.path.join(os.path.dirname(__file__), "../../", "assets/sample_audio.mp3")),
["hi", "how", "are", "you", "doing"],
id="openai",
),
pytest.param(
"azure_openai",
AudioContent.from_audio_file(os.path.join(os.path.dirname(__file__), "../../", "assets/sample_audio.mp3")),
["hi", "how", "are", "you", "doing"],
marks=pytest.mark.skipif(not azure_setup, reason="Azure Audio to Text not setup."),
id="azure_openai",
),
],
)
class TestAudioToText(AudioToTextTestBase):
"""Test audio-to-text services."""
async def test_audio_to_text(
self,
services: dict[str, AudioToTextClientBase],
service_id: str,
audio_content: AudioContent,
expected_text: list[str],
) -> None:
"""Test audio-to-text services.
Args:
services: Audio-to-text services.
service_id: Service ID.
audio_content: Audio content.
expected_text: Expected text, list of words.
"""
service = services[service_id]
if not service:
pytest.mark.xfail("Azure Audio to Text not setup.")
result = await service.get_text_content(audio_content)
for word in expected_text:
assert word in result.text.lower(), (
f"Expected word '{word}' not found in result text: {result.text.lower()}"
)
@@ -0,0 +1,240 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import sys
from typing import Annotated
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from azure.ai.inference.aio import ChatCompletionsClient
from azure.identity import AzureCliCredential
from openai import AsyncAzureOpenAI
from semantic_kernel.connectors.ai.anthropic import AnthropicChatCompletion, AnthropicChatPromptExecutionSettings
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceChatCompletion,
AzureAIInferenceChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock import BedrockChatCompletion, BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.google.google_ai import GoogleAIChatCompletion, GoogleAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.mistral_ai import MistralAIChatCompletion, MistralAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama import OllamaChatCompletion, OllamaChatPromptExecutionSettings
from semantic_kernel.connectors.ai.onnx import OnnxGenAIChatCompletion, OnnxGenAIPromptExecutionSettings, ONNXTemplate
from semantic_kernel.connectors.ai.open_ai import (
AzureChatCompletion,
AzureChatPromptExecutionSettings,
AzureOpenAISettings,
OpenAIChatCompletion,
OpenAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.core_plugins.math_plugin import MathPlugin
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing
# Make sure all services are setup for before running the tests
# The following exceptions apply:
# 1. OpenAI and Azure OpenAI services are always setup for testing.
azure_openai_setup: bool = True
# 2. Bedrock services don't use API keys and model providers are tested individually,
# so no environment variables are required.
mistral_ai_setup: bool = is_service_setup_for_testing(
["MISTRALAI_API_KEY", "MISTRALAI_CHAT_MODEL_ID"], raise_if_not_set=False
) # We don't have a MistralAI deployment
# There is no single model in Ollama that supports both image and tool call in chat completion
# We are splitting the Ollama test into three services: chat, image, and tool call. The chat model
# can be any model that supports chat completion. Also, Ollama is only available on Linux runners in our pipeline.
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID"])
ollama_image_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID_IMAGE"])
ollama_tool_call_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID_TOOL_CALL"])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing([
"GOOGLE_AI_CLOUD_PROJECT_ID",
"GOOGLE_AI_GEMINI_MODEL_ID",
"GOOGLE_AI_CLOUD_REGION",
])
onnx_setup: bool = is_service_setup_for_testing(
["ONNX_GEN_AI_CHAT_MODEL_FOLDER"], raise_if_not_set=False
) # Tests are optional for ONNX
anthropic_setup: bool = is_service_setup_for_testing(["ANTHROPIC_API_KEY", "ANTHROPIC_CHAT_MODEL_ID"])
# A mock plugin that contains a function that returns a complex object.
class PersonDetails(KernelBaseModel):
id: str
name: str
age: int
class PersonSearchPlugin:
@kernel_function(name="SearchPerson", description="Search details of a person given their id.")
def search_person(
self, person_id: Annotated[str, "The person ID to search"]
) -> Annotated[PersonDetails, "The details of the person"]:
return PersonDetails(id=person_id, name="John Doe", age=42)
class ChatCompletionTestBase(CompletionTestBase):
"""Base class for testing completion services."""
@override
@pytest.fixture(
scope="function"
) # This needs to be scoped to function to avoid resources getting cleaned up after each test
def services(self) -> dict[str, tuple[ServiceType | None, type[PromptExecutionSettings] | None]]:
azure_openai_setup = True
credential = AzureCliCredential()
azure_openai_settings = AzureOpenAISettings()
endpoint = str(azure_openai_settings.endpoint)
deployment_name = azure_openai_settings.chat_deployment_name
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
if not ad_token:
azure_openai_setup = False
api_version = azure_openai_settings.api_version
azure_custom_client = None
azure_ai_inference_client = None
if azure_openai_setup:
azure_custom_client = AzureChatCompletion(
async_client=AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
azure_ad_token=ad_token,
api_version=api_version,
default_headers={"Test-User-X-ID": "test"},
),
)
assert deployment_name
azure_ai_inference_client = AzureAIInferenceChatCompletion(
ai_model_id=deployment_name,
client=ChatCompletionsClient(
endpoint=f"{endpoint.strip('/')}/openai/deployments/{deployment_name}",
credential=credential, # type: ignore
credential_scopes=["https://cognitiveservices.azure.com/.default"],
),
)
return {
"openai": (OpenAIChatCompletion(), OpenAIChatPromptExecutionSettings),
"azure": (
AzureChatCompletion(credential=credential) if azure_openai_setup else None,
AzureChatPromptExecutionSettings,
),
"azure_custom_client": (azure_custom_client, AzureChatPromptExecutionSettings),
"azure_ai_inference": (azure_ai_inference_client, AzureAIInferenceChatPromptExecutionSettings),
"anthropic": (AnthropicChatCompletion() if anthropic_setup else None, AnthropicChatPromptExecutionSettings),
"mistral_ai": (
MistralAIChatCompletion() if mistral_ai_setup else None,
MistralAIChatPromptExecutionSettings,
),
"ollama": (OllamaChatCompletion() if ollama_setup else None, OllamaChatPromptExecutionSettings),
"ollama_image": (
OllamaChatCompletion(ai_model_id=os.environ["OLLAMA_CHAT_MODEL_ID_IMAGE"])
if ollama_image_setup
else None,
OllamaChatPromptExecutionSettings,
),
"ollama_tool_call": (
OllamaChatCompletion(ai_model_id=os.environ["OLLAMA_CHAT_MODEL_ID_TOOL_CALL"])
if ollama_tool_call_setup
else None,
OllamaChatPromptExecutionSettings,
),
"google_ai": (GoogleAIChatCompletion() if google_ai_setup else None, GoogleAIChatPromptExecutionSettings),
"vertex_ai": (
GoogleAIChatCompletion(use_vertexai=True) if vertex_ai_setup else None,
GoogleAIChatPromptExecutionSettings,
),
"onnx_gen_ai": (
OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3V) if onnx_setup else None,
OnnxGenAIPromptExecutionSettings,
),
"bedrock_amazon_nova": (
self._try_create_bedrock_chat_completion_client("amazon.nova-lite-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_ai21labs": (
self._try_create_bedrock_chat_completion_client("ai21.jamba-1-5-mini-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_anthropic_claude": (
self._try_create_bedrock_chat_completion_client("anthropic.claude-3-sonnet-20240229-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_cohere_command": (
self._try_create_bedrock_chat_completion_client("cohere.command-r-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_meta_llama": (
self._try_create_bedrock_chat_completion_client("meta.llama3-70b-instruct-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_mistralai": (
self._try_create_bedrock_chat_completion_client("mistral.mistral-small-2402-v1:0"),
BedrockChatPromptExecutionSettings,
),
}
def setup(self, kernel: Kernel):
"""Setup the kernel with the completion service and function."""
kernel.add_plugin(MathPlugin(), plugin_name="math")
kernel.add_plugin(PersonSearchPlugin(), plugin_name="search")
async def get_chat_completion_response(
self,
kernel: Kernel,
service: ServiceType,
execution_settings: PromptExecutionSettings,
chat_history: ChatHistory,
stream: bool,
) -> ChatMessageContent | StreamingChatMessageContent | None:
"""Get response from the service
Args:
kernel (Kernel): Kernel instance.
service (ChatCompletionClientBase): Chat completion service.
execution_settings (PromptExecutionSettings): Execution settings.
input (str): Input string.
stream (bool): Stream flag.
"""
assert isinstance(service, ChatCompletionClientBase)
if not stream:
return await service.get_chat_message_content(
chat_history,
execution_settings,
kernel=kernel,
)
parts: list[StreamingChatMessageContent] = [
part
async for part in service.get_streaming_chat_message_content(
chat_history,
execution_settings,
kernel=kernel,
)
if part
]
if parts:
return sum(parts[1:], parts[0])
raise AssertionError("No response")
def _try_create_bedrock_chat_completion_client(self, model_id: str) -> BedrockChatCompletion | None:
try:
return BedrockChatCompletion(model_id=model_id)
except Exception as ex:
from conftest import logger
logger.warning(ex)
# Returning None so that the test that uses this service will be skipped
return None
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.kernel import Kernel
ServiceType = ChatCompletionClientBase | TextCompletionClientBase
class CompletionTestBase:
"""Base class for testing completion services."""
def services(self) -> dict[str, tuple["ServiceType", type[PromptExecutionSettings]]]:
"""Return completion services."""
raise NotImplementedError
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str | ChatMessageContent | list[ChatMessageContent]],
kwargs: dict[str, Any],
) -> None:
"""Test completion service (Non-streaming).
Args:
kernel (Kernel): Kernel instance.
service_id (str): Service name.
services (dict[str, tuple[ServiceType, type[PromptExecutionSettings]]]): Completion services.
execution_settings_kwargs (dict[str, Any]): Execution settings keyword arguments.
inputs (list[str]): List of input strings.
kwargs (dict[str, Any]): Keyword arguments.
"""
raise NotImplementedError
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str | ChatMessageContent | list[ChatMessageContent]],
kwargs: dict[str, Any],
):
"""Test completion service (Streaming).
Args:
kernel (Kernel): Kernel instance.
service_id (str): Service name.
services (dict[str, tuple[ServiceType, type[PromptExecutionSettings]]]): Completion services.
execution_settings_kwargs (dict[str, Any]): Execution settings keyword arguments.
inputs (list[str]): List of input strings.
kwargs (dict[str, Any]): Keyword arguments.
"""
raise NotImplementedError
def evaluate(self, test_target: Any, **kwargs):
"""Evaluate the response.
Args:
test_target (Any): Test target.
kwargs (dict[str, Any]): Keyword arguments.
"""
raise NotImplementedError
@@ -0,0 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from semantic_kernel.utils.logging import setup_logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
setup_logging()
@@ -0,0 +1,158 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import time
from random import randint
import numpy as np
import pytest
import pytest_asyncio
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
ApiKeyAuthentication,
AzureAISearchDataSource,
AzureAISearchDataSourceParameters,
DataSourceFieldsMapping,
ExtraBody,
)
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.memory.memory_record import MemoryRecord
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
try:
from semantic_kernel.connectors.memory_stores.azure_cognitive_search.azure_cognitive_search_memory_store import (
AzureCognitiveSearchMemoryStore,
)
azure_ai_search_installed = True
except ImportError:
azure_ai_search_installed = False
if os.environ.get("AZURE_COGNITIVE_SEARCH_ENDPOINT") and os.environ.get("AZURE_COGNITIVE_SEARCH_ADMIN_KEY"):
azure_ai_search_settings = True
else:
azure_ai_search_settings = False
pytestmark = pytest.mark.skipif(
not (azure_ai_search_installed and azure_ai_search_settings),
reason="Azure AI Search is not installed",
)
@pytest_asyncio.fixture
async def create_memory_store():
# Create an index and populate it with some data
collection = f"int-tests-chat-extensions-{randint(1000, 9999)}"
memory_store = AzureCognitiveSearchMemoryStore(vector_size=4)
await memory_store.create_collection(collection)
time.sleep(1)
try:
assert await memory_store.does_collection_exist(collection)
rec = MemoryRecord(
is_reference=False,
external_source_name=None,
id=None,
description="Emily and David's story.",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. \
Bonded by their love for the natural world and shared curiosity, they uncovered a \
groundbreaking phenomenon in glaciology that could potentially reshape our understanding \
of climate change.",
additional_metadata=None,
embedding=np.array([0.2, 0.1, 0.2, 0.7]),
)
await memory_store.upsert(collection, rec)
time.sleep(1)
return collection, memory_store
except Exception as e:
await memory_store.delete_collection(collection)
raise e
@pytest_asyncio.fixture
async def create_with_data_chat_function(kernel: Kernel, create_memory_store):
collection, memory_store = create_memory_store
try:
# Load Azure OpenAI with data settings
search_endpoint = os.getenv("AZURE_COGNITIVE_SEARCH_ENDPOINT")
search_api_key = os.getenv("AZURE_COGNITIVE_SEARCH_ADMIN_KEY")
extra = ExtraBody(
data_sources=[
AzureAISearchDataSource(
parameters=AzureAISearchDataSourceParameters(
index_name=collection,
endpoint=search_endpoint,
authentication=ApiKeyAuthentication(key=search_api_key),
query_type="simple",
fields_mapping=DataSourceFieldsMapping(
title_field="Description",
content_fields=["Text"],
),
top_n_documents=1,
),
)
]
)
chat_service = AzureChatCompletion(service_id="chat-gpt-extensions", credential=AzureCliCredential())
kernel.add_service(chat_service)
prompt = "{{$chat_history}}{{$input}}"
exec_settings = PromptExecutionSettings(
service_id="chat-gpt-extensions",
extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8, "extra_body": extra},
)
prompt_template_config = PromptTemplateConfig(
template=prompt, description="Chat", execution_settings=exec_settings
)
# Create the semantic function
kernel.add_function(function_name="chat", plugin_name="plugin", prompt_template_config=prompt_template_config)
chat_function = kernel.get_function("plugin", "chat")
return chat_function, kernel, collection, memory_store
except Exception as e:
await memory_store.delete_collection(collection)
raise e
@pytestmark
async def test_azure_e2e_chat_completion_with_extensions(create_with_data_chat_function):
# Create an index and populate it with some data
chat_function, kernel, collection, memory_store = create_with_data_chat_function
chat_history = ChatHistory()
chat_history.add_user_message("A story about Emily and David...")
arguments = KernelArguments(input="who are Emily and David?", chat_history=chat_history)
# TODO: get streaming working for this test
use_streaming = False
try:
result: StreamingChatMessageContent = None
if use_streaming:
async for message in kernel.invoke_stream(chat_function, arguments):
result = message[0] if not result else result + message[0]
print(message, end="")
print(f"Answer using input string: '{result}'")
for item in result.items:
if isinstance(item, FunctionResultContent):
print(f"Content: {item.result}")
assert "two passionate scientists" in item.result
else:
result = await kernel.invoke(chat_function, arguments)
print(f"Answer using input string: '{result}'")
await memory_store.delete_collection(collection)
except Exception as e:
await memory_store.delete_collection(collection)
raise e
@@ -0,0 +1,347 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import sys
from functools import partial
from typing import Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents import ChatHistory, ChatMessageContent, TextContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from tests.integration.completions.chat_completion_test_base import (
ChatCompletionTestBase,
ollama_image_setup,
onnx_setup,
vertex_ai_setup,
)
from tests.integration.completions.completion_test_base import ServiceType
from tests.utils import retry
# Use the repo's own sample image via raw GitHub URL for URI-based tests.
# Previously this pointed to a 17.5 MB Wikimedia image that got blocked by
# Wikimedia's User-Agent policy (Phabricator T400119), causing Azure's
# server-side image fetcher to fail with HTTP 403.
IMAGE_TEST_URL = "https://raw.githubusercontent.com/microsoft/semantic-kernel/main/python/tests/assets/sample_image.jpg"
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, inputs, kwargs",
[
pytest.param(
"openai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent(uri=IMAGE_TEST_URL),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="openai_image_input_uri",
),
pytest.param(
"openai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="openai_image_input_file",
),
pytest.param(
"azure",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent(uri=IMAGE_TEST_URL),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_image_input_uri",
),
pytest.param(
"azure",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_image_input_file",
),
pytest.param(
"onnx_gen_ai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
marks=(
pytest.mark.skipif(not onnx_setup, reason="Need a Onnx Model setup"),
pytest.mark.onnx,
),
id="onnx_gen_ai_image_input_file",
),
pytest.param(
"azure_ai_inference",
{
"max_tokens": 256,
},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent(uri=IMAGE_TEST_URL),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_ai_inference_image_input_uri",
),
pytest.param(
"azure_ai_inference",
{
"max_tokens": 256,
},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_ai_inference_image_input_file",
),
pytest.param(
"google_ai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="Where was it made? Make a guess if you are not sure.")],
),
],
{},
marks=[
pytest.mark.skip(reason="Skipping due to occasional throttling from Google AI."),
# pytest.mark.skipif(not google_ai_setup, reason="Google AI Environment Variables not set"),
],
id="google_ai_image_input_file",
),
pytest.param(
"vertex_ai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="Where was it made? Make a guess if you are not sure.")],
),
],
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI Environment Variables not set"),
id="vertex_ai_image_input_file",
),
pytest.param(
"ollama_image",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="Where was it made? Make a guess if you are not sure.")],
),
],
{},
marks=(
pytest.mark.skipif(not ollama_image_setup, reason="Ollama Environment Variables not set"),
pytest.mark.ollama,
),
id="ollama_image_input_file",
),
pytest.param(
"bedrock_anthropic_claude",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_anthropic_claude_image_input_file",
),
],
)
class TestChatCompletionWithImageInputTextOutput(ChatCompletionTestBase):
"""Test chat completion with image input and text output."""
@override
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
False,
)
@override
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
True,
)
@override
def evaluate(self, test_target: Any, **kwargs):
inputs = kwargs.get("inputs")
assert isinstance(inputs, list)
assert len(test_target) == len(inputs) * 2
for i in range(len(inputs)):
message = test_target[i * 2 + 1]
assert message.items, "No items in message"
assert len(message.items) == 1, "Unexpected number of items in message"
assert isinstance(message.items[0], TextContent), "Unexpected message item type"
assert message.items[0].text, "Empty message text"
async def _test_helper(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
stream: bool,
):
self.setup(kernel)
service, settings_type = services[service_id]
if service is None:
pytest.skip(f"Service {service_id} not set up")
history = ChatHistory()
for message in inputs:
history.add_message(message)
cmc: ChatMessageContent | None = await retry(
partial(
self.get_chat_completion_response,
kernel=kernel,
service=service,
execution_settings=settings_type(**execution_settings_kwargs),
chat_history=history,
stream=stream,
),
retries=5,
name="image_input",
)
if cmc:
history.add_message(cmc)
self.evaluate(history.messages, inputs=inputs)
@@ -0,0 +1,346 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from functools import partial
from typing import Any
import pytest
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai import PromptExecutionSettings
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent, TextContent
from semantic_kernel.kernel_pydantic import KernelBaseModel
from tests.integration.completions.chat_completion_test_base import (
ChatCompletionTestBase,
anthropic_setup,
mistral_ai_setup,
ollama_setup,
onnx_setup,
vertex_ai_setup,
)
from tests.integration.completions.completion_test_base import ServiceType
from tests.utils import retry
class Step(KernelBaseModel):
explanation: str
output: str
class Reasoning(KernelBaseModel):
steps: list[Step]
final_answer: str
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, inputs, kwargs",
[
# region OpenAI
pytest.param(
"openai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="openai_text_input",
),
pytest.param(
"openai",
{"response_format": Reasoning},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="openai_json_schema_response_format",
),
# endregion
# region Azure
pytest.param(
"azure",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="azure_text_input",
),
pytest.param(
"azure_custom_client",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="azure_custom_client",
),
# endregion
# region Azure AI Inference
pytest.param(
"azure_ai_inference",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="azure_ai_inference_text_input",
),
# endregion
# region Anthropic
pytest.param(
"anthropic",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skipif(not anthropic_setup, reason="Anthropic Environment Variables not set"),
id="anthropic_text_input",
),
# endregion
# region Mistral AI
pytest.param(
"mistral_ai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skipif(not mistral_ai_setup, reason="Mistral AI Environment Variables not set"),
id="mistral_ai_text_input",
),
# endregion
# region Ollama
pytest.param(
"ollama",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=(
pytest.mark.skipif(not ollama_setup, reason="Need local Ollama setup"),
pytest.mark.ollama,
),
id="ollama_text_input",
),
# endregion
# region Onnx Gen AI
pytest.param(
"onnx_gen_ai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=(
pytest.mark.skipif(not onnx_setup, reason="Need a Onnx Model setup"),
pytest.mark.onnx,
),
id="onnx_gen_ai",
),
# endregion
# region Google AI
pytest.param(
"google_ai",
{"top_p": 0.9, "temperature": 0.7},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=[
pytest.mark.skip(reason="Skipping due to occasional throttling from Google AI."),
# pytest.mark.skipif(not google_ai_setup, reason="Need Google AI setup"),
],
id="google_ai_text_input",
),
# endregion
# region Vertex AI
pytest.param(
"vertex_ai",
{"top_p": 0.9, "temperature": 0.7},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI Environment Variables not set"),
id="vertex_ai_text_input",
),
# endregion
# region Bedrock
pytest.param(
"bedrock_amazon_nova",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="bedrock_amazon_nova_text_input",
),
pytest.param(
"bedrock_ai21labs",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_ai21labs_text_input",
),
pytest.param(
"bedrock_anthropic_claude",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_anthropic_claude_text_input",
),
pytest.param(
"bedrock_cohere_command",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_cohere_command_text_input",
),
pytest.param(
"bedrock_meta_llama",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_meta_llama_text_input",
),
pytest.param(
"bedrock_mistralai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_mistralai_text_input",
),
# endregion
],
)
class TestChatCompletion(ChatCompletionTestBase):
"""Test Chat Completions.
This only tests if the services can return text completions given text inputs.
"""
@override
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
False,
)
@override
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
True,
)
@override
def evaluate(self, test_target: Any, **kwargs):
inputs = kwargs.get("inputs")
assert isinstance(inputs, list)
assert len(test_target) == len(inputs) * 2
for i in range(len(inputs)):
message = test_target[i * 2 + 1]
assert message.items, "No items in message"
assert len(message.items) == 1, "Unexpected number of items in message"
assert isinstance(message.items[0], TextContent), "Unexpected message item type"
assert message.items[0].text, "Empty message text"
async def _test_helper(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
stream: bool,
):
self.setup(kernel)
service, settings_type = services[service_id]
if service is None:
pytest.skip(f"Service {service_id} not set up")
history = ChatHistory()
for message in inputs:
history.add_message(message)
cmc: ChatMessageContent | None = await retry(
partial(
self.get_chat_completion_response,
kernel=kernel,
service=service,
execution_settings=settings_type(**execution_settings_kwargs),
chat_history=history,
stream=stream,
),
retries=5,
name="get_chat_completion_response",
)
if cmc:
history.add_message(cmc)
self.evaluate(history.messages, inputs=inputs)
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
import semantic_kernel.connectors.ai.open_ai as sk_oai
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.core_plugins.conversation_summary_plugin import ConversationSummaryPlugin
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
from tests.utils import retry
CHAT_TRANSCRIPT = """John: Hello, how are you?
Jane: I'm fine, thanks. How are you?
John: I'm doing well, writing some example code.
Jane: That's great! I'm writing some example code too.
John: What are you writing?
Jane: I'm writing a chatbot.
John: That's cool. I'm writing a chatbot too.
Jane: What language are you writing it in?
John: I'm writing it in C#.
Jane: I'm writing it in Python.
John: That's cool. I need to learn Python.
Jane: I need to learn C#.
John: Can I try out your chatbot?
Jane: Sure, here's the link.
John: Thanks!
Jane: You're welcome.
Jane: Look at this poem my chatbot wrote:
Jane: Roses are red
Jane: Violets are blue
Jane: I'm writing a chatbot
Jane: What about you?
John: That's cool. Let me see if mine will write a poem, too.
John: Here's a poem my chatbot wrote:
John: The singularity of the universe is a mystery.
Jane: You might want to try using a different model.
John: I'm using the GPT-2 model. That makes sense.
John: Here is a new poem after updating the model.
John: The universe is a mystery.
John: The universe is a mystery.
John: The universe is a mystery.
Jane: Sure, what's the problem?
John: Thanks for the help!
Jane: I'm now writing a bot to summarize conversations.
Jane: I have some bad news, we're only half way there.
John: Maybe there is a large piece of text we can use to generate a long conversation.
Jane: That's a good idea. Let me see if I can find one. Maybe Lorem Ipsum?
John: Yeah, that's a good idea."""
async def test_azure_summarize_conversation_using_plugin(kernel):
service_id = "text_completion"
execution_settings = PromptExecutionSettings(
service_id=service_id, max_tokens=ConversationSummaryPlugin._max_tokens, temperature=0.1, top_p=0.5
)
prompt_template_config = PromptTemplateConfig(
description="Given a section of a conversation transcript, summarize the part of the conversation.",
execution_settings={service_id: execution_settings},
)
kernel.add_service(sk_oai.OpenAIChatCompletion(service_id=service_id))
conversationSummaryPlugin = kernel.add_plugin(
ConversationSummaryPlugin(prompt_template_config), "conversationSummary"
)
arguments = KernelArguments(input=CHAT_TRANSCRIPT)
summary = await retry(
lambda: kernel.invoke(conversationSummaryPlugin["SummarizeConversation"], arguments), retries=5
)
output = str(summary).strip().lower()
print(output)
assert "john" in output and "jane" in output
assert len(output) < len(CHAT_TRANSCRIPT)
@@ -0,0 +1,345 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from functools import partial
from importlib import util
from typing import Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.bedrock import BedrockTextCompletion, BedrockTextPromptExecutionSettings
from semantic_kernel.connectors.ai.google.google_ai import GoogleAITextCompletion, GoogleAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.hugging_face import HuggingFacePromptExecutionSettings, HuggingFaceTextCompletion
from semantic_kernel.connectors.ai.ollama import OllamaTextCompletion, OllamaTextPromptExecutionSettings
from semantic_kernel.connectors.ai.onnx import OnnxGenAIPromptExecutionSettings, OnnxGenAITextCompletion
from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion, OpenAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents import StreamingTextContent, TextContent
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing, is_test_running_on_supported_platforms, retry
hugging_face_setup = util.find_spec("torch") is not None
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_TEXT_MODEL_ID"]) and is_test_running_on_supported_platforms([
"Linux"
])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing([
"GOOGLE_AI_CLOUD_PROJECT_ID",
"GOOGLE_AI_GEMINI_MODEL_ID",
"GOOGLE_AI_CLOUD_REGION",
])
onnx_setup: bool = is_service_setup_for_testing(
["ONNX_GEN_AI_TEXT_MODEL_FOLDER"], raise_if_not_set=False
) # Tests are optional for ONNX
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, inputs, kwargs",
[
pytest.param(
"openai",
{},
["Repeat the word Hello once"],
{},
id="openai_text_completion",
),
pytest.param(
"hf_t2t",
{},
["translate English to Dutch: Hello"],
{},
id="huggingface_text_completion_translation",
),
pytest.param(
"hf_summ",
{},
[
"""Summarize: Whales are fully aquatic, open-ocean animals:
they can feed, mate, give birth, suckle and raise their young at sea.
Whales range in size from the 2.6 metres (8.5 ft) and 135 kilograms (298 lb)
dwarf sperm whale to the 29.9 metres (98 ft) and 190 tonnes (210 short tons) blue whale,
which is the largest known animal that has ever lived. The sperm whale is the largest
toothed predator on Earth. Several whale species exhibit sexual dimorphism,
in that the females are larger than males."""
],
{},
id="huggingface_text_completion_summarization",
),
pytest.param(
"hf_gen",
{},
["Hello, I like sleeping and "],
{},
id="huggingface_text_completion_generation",
),
pytest.param(
"ollama",
{},
["Repeat the word Hello once"],
{},
marks=(
pytest.mark.skip(
reason="Need local Ollama setup" if not ollama_setup else "Ollama responses are not always correct."
),
pytest.mark.ollama,
),
id="ollama_text_completion",
),
pytest.param(
"google_ai",
{},
["Repeat the word Hello once"],
{},
marks=[
pytest.mark.skip(reason="Skipping due to occasional throttling from Google AI."),
# pytest.mark.skipif(not google_ai_setup, reason="Need Google AI setup"),
],
id="google_ai_text_completion",
),
pytest.param(
"vertex_ai",
{},
["Repeat the word Hello once"],
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Need VertexAI setup"),
id="vertex_ai_text_completion",
),
pytest.param(
"onnx_gen_ai",
{},
["<|user|>Repeat the word Hello<|end|><|assistant|>"],
{},
marks=(
pytest.mark.skipif(not onnx_setup, reason="Need a Onnx Model setup"),
pytest.mark.onnx,
),
id="onnx_gen_ai_text_completion",
),
pytest.param(
"bedrock_anthropic_claude",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_anthropic_claude_text_completion",
),
pytest.param(
"bedrock_cohere_command",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_cohere_command_text_completion",
),
pytest.param(
"bedrock_ai21labs",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_ai21labs_text_completion",
),
pytest.param(
"bedrock_meta_llama",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_meta_llama_text_completion",
),
pytest.param(
"bedrock_mistralai",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_mistralai_text_completion",
),
],
)
class TestTextCompletion(CompletionTestBase):
"""Test class for text completion"""
@override
@pytest.fixture(scope="class")
def services(self) -> dict[str, tuple[ServiceType | None, type[PromptExecutionSettings] | None]]:
"""Get the services to be tested"""
return {
"openai": (OpenAITextCompletion(), OpenAITextPromptExecutionSettings),
"ollama": (OllamaTextCompletion() if ollama_setup else None, OllamaTextPromptExecutionSettings),
"google_ai": (GoogleAITextCompletion() if google_ai_setup else None, GoogleAITextPromptExecutionSettings),
"vertex_ai": (
GoogleAITextCompletion(use_vertexai=True) if vertex_ai_setup else None,
GoogleAITextPromptExecutionSettings,
),
"hf_t2t": (
HuggingFaceTextCompletion(
service_id="patrickvonplaten/t5-tiny-random",
ai_model_id="patrickvonplaten/t5-tiny-random",
task="text2text-generation",
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"hf_summ": (
HuggingFaceTextCompletion(
service_id="Falconsai/text_summarization",
ai_model_id="Falconsai/text_summarization",
task="summarization",
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"hf_gen": (
HuggingFaceTextCompletion(
service_id="HuggingFaceM4/tiny-random-LlamaForCausalLM",
ai_model_id="HuggingFaceM4/tiny-random-LlamaForCausalLM",
task="text-generation",
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"onnx_gen_ai": (
OnnxGenAITextCompletion() if onnx_setup else None,
OnnxGenAIPromptExecutionSettings,
),
# Amazon Bedrock supports models from multiple providers but requests to and responses from the models are
# inconsistent. So we need to test each model separately.
"bedrock_anthropic_claude": (
self._try_create_bedrock_text_completion_client("anthropic.claude-v2"),
BedrockTextPromptExecutionSettings,
),
"bedrock_cohere_command": (
self._try_create_bedrock_text_completion_client("cohere.command-text-v14"),
BedrockTextPromptExecutionSettings,
),
"bedrock_ai21labs": (
self._try_create_bedrock_text_completion_client("ai21.j2-mid-v1"),
BedrockTextPromptExecutionSettings,
),
"bedrock_meta_llama": (
self._try_create_bedrock_text_completion_client("meta.llama3-70b-instruct-v1:0"),
BedrockTextPromptExecutionSettings,
),
"bedrock_mistralai": (
self._try_create_bedrock_text_completion_client("mistral.mistral-7b-instruct-v0:2"),
BedrockTextPromptExecutionSettings,
),
}
async def get_text_completion_response(
self,
service: ServiceType,
execution_settings: PromptExecutionSettings,
prompt: str,
stream: bool,
) -> Any:
"""Get response from the service
Args:
kernel (Kernel): Kernel instance.
service (ChatCompletionClientBase): Chat completion service.
execution_settings (PromptExecutionSettings): Execution settings.
prompt (str): Input string.
stream (bool): Stream flag.
"""
assert isinstance(service, TextCompletionClientBase)
if stream:
response = service.get_streaming_text_content(
prompt=prompt,
settings=execution_settings,
)
parts: list[StreamingTextContent] = [part async for part in response if part is not None]
if parts:
return sum(parts[1:], parts[0])
raise AssertionError("No response")
return await service.get_text_content(
prompt=prompt,
settings=execution_settings,
)
return response
@override
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str],
kwargs: dict[str, Any],
) -> None:
await self._test_helper(service_id, services, execution_settings_kwargs, inputs, False)
@override
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str],
kwargs: dict[str, Any],
):
if "streaming" in kwargs and not kwargs["streaming"]:
pytest.skip("Skipping streaming test")
await self._test_helper(service_id, services, execution_settings_kwargs, inputs, True)
@override
def evaluate(self, test_target: Any, **kwargs):
print(test_target)
if isinstance(test_target, TextContent):
# Test is considered successful if the test_target is not empty
assert test_target.text, "Error: Empty test target"
return
raise AssertionError(f"Unexpected output: {test_target}, type: {type(test_target)}")
async def _test_helper(
self,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str],
stream: bool,
):
service, settings_type = services[service_id]
if not service:
pytest.skip(f"Setup not ready for {service_id if service_id else 'None'}")
for test_input in inputs:
response = await retry(
partial(
self.get_text_completion_response,
service=service,
execution_settings=settings_type(**execution_settings_kwargs),
prompt=test_input,
stream=stream,
),
retries=5,
name="text completions",
)
self.evaluate(response)
def _try_create_bedrock_text_completion_client(self, model_id: str) -> BedrockTextCompletion | None:
try:
return BedrockTextCompletion(model_id=model_id)
except Exception as ex:
from conftest import logger
logger.warning(ex)
# Returning None so that the test that uses this service will be skipped
return None
@@ -0,0 +1,197 @@
{
"openapi": "3.0.1",
"info": {
"title": "Light Bulb API",
"version": "v1"
},
"servers": [
{
"url": "https://127.0.0.1"
}
],
"paths": {
"/Lights/{id}": {
"get": {
"operationId": "GetLightById",
"tags": [
"Lights"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"style": "simple",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
},
"put": {
"operationId": "PutLightById",
"tags": [
"Lights"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"style": "simple",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeStateRequest"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ChangeStateRequest"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ChangeStateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Success"
}
}
},
"delete": {
"operationId": "DeleteLightById",
"tags": [
"Lights"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"style": "simple",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/Lights": {
"get": {
"operationId": "GetLights",
"tags": [
"Lights"
],
"parameters": [
{
"name": "roomId",
"in": "query",
"style": "form",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"operationId": "CreateLights",
"tags": [
"Lights"
],
"parameters": [
{
"name": "roomId",
"in": "query",
"style": "form",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "lightName",
"in": "query",
"style": "form",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
}
},
"components": {
"schemas": {
"ChangeStateRequest": {
"type": "object",
"properties": {
"isOn": {
"type": "boolean",
"description": "Specifies whether the light is turned on or off."
},
"hexColor": {
"type": "string",
"description": "The hex color code for the light.",
"nullable": true
},
"brightness": {
"enum": [
"Low",
"Medium",
"High"
],
"type": "string",
"description": "The brightness level of the light."
},
"fadeDurationInMilliseconds": {
"type": "integer",
"description": "Duration for the light to fade to the new state, in milliseconds.",
"format": "int32"
},
"scheduledTime": {
"type": "string",
"description": "The time at which the change should occur.",
"format": "date-time"
}
},
"additionalProperties": false,
"description": "Represents a request to change the state of the light."
}
}
}
}
@@ -0,0 +1,10 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,18 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
},
{
"content": "Sure! The time in Seattle is currently 3:00 PM.",
"role": "assistant"
},
{
"content": "What about New York?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,7 @@
name: getTimes
description: Gets the time in various cities.
template: |
<message role="user">Can you help me tell the time in Seattle right now?</message>
<message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message>
<message role="user">What about New York?</message>
template_format: handlebars
@@ -0,0 +1,7 @@
name: getTimes
description: Gets the time in various cities.
template: |
<message role="user">Can you help me tell the time in Seattle right now?</message>
<message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message>
<message role="user">What about New York?</message>
template_format: jinja2
@@ -0,0 +1,10 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,14 @@
{
"messages": [
{
"content": "The current time is Sun, 04 Jun 1989 12:11:13 GMT",
"role": "system"
},
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,10 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,9 @@
name: getTimeInCity
description: Gets the time in a specified city.
template: |
<message role="user">Can you help me tell the time in {{$city}} right now?</message>
template_format: semantic-kernel
input_variables:
- name: city
description: City for which time is desired
default: Seattle
@@ -0,0 +1,5 @@
name: getSeattleTime
description: Gets the time in Seattle.
template: |
<message role="user">Can you help me tell the time in Seattle right now?</message>
template_format: semantic-kernel
@@ -0,0 +1,834 @@
# Copyright (c) Microsoft. All rights reserved.
import contextlib
import datetime
import json
import logging
import os
from collections.abc import AsyncGenerator
from typing import Literal
import httpx
import pytest
import pytest_asyncio
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAISettings
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
logger = logging.getLogger(__name__)
OPENAI_MODEL_ID = "gpt-4.1-nano"
# region Test Prompts
simple_prompt = "Can you help me tell the time in Seattle right now?"
sk_simple_prompt = "Can you help me tell the time in {{$city}} right now?"
hb_simple_prompt = "Can you help me tell the time in {{city}} right now?"
j2_simple_prompt = "Can you help me tell the time in {{city}} right now?"
sk_prompt = '<message role="system">The current time is {{Time.Now}}</message><message role="user">Can you help me tell the time in {{$city}} right now?</message>' # noqa: E501
hb_prompt = '<message role="system">The current time is {{Time-Now}}</message><message role="user">Can you help me tell the time in {{city}} right now?</message>' # noqa: E501
j2_prompt = '<message role="system">The current time is {{Time_Now()}}</message><message role="user">Can you help me tell the time in {{city}} right now?</message>' # noqa: E501
# endregion
# region Custom Logging Class
class LoggingTransport(httpx.AsyncBaseTransport):
def __init__(self, inner=None):
self.inner = inner or httpx.AsyncHTTPTransport()
self.request_headers = {}
self.request_content = None
self.response_headers = {}
self.response_content = None
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
self.request_headers = dict(request.headers)
self.request_content = request.content.decode("utf-8") if request.content else None
logger.info(f"Request URL: {request.url}")
logger.info(f"Request Headers: {self.request_headers}")
logger.info(f"Request Content: {self.request_content}")
response = await self.inner.handle_async_request(request)
raw_response_bytes = await response.aread()
self.response_headers = dict(response.headers)
self.response_content = raw_response_bytes.decode(response.encoding or "utf-8", errors="replace")
logger.info(f"Response Headers: {self.response_headers}")
logger.info(f"Response Content: {self.response_content}")
headers_without_encoding = {k: v for k, v in response.headers.items() if k.lower() != "content-encoding"}
return httpx.Response(
status_code=response.status_code,
headers=headers_without_encoding,
content=raw_response_bytes,
request=request,
extensions=response.extensions,
)
class LoggingAsyncClient(httpx.AsyncClient):
def __init__(self, *args, **kwargs):
transport = kwargs.pop("transport", None)
self.logging_transport = LoggingTransport(transport or httpx.AsyncHTTPTransport())
super().__init__(*args, **kwargs, transport=self.logging_transport)
@property
def request_headers(self):
return self.logging_transport.request_headers
@property
def request_content(self):
return self.logging_transport.request_content
@property
def response_headers(self):
return self.logging_transport.response_headers
@property
def response_content(self):
return self.logging_transport.response_content
# endregion
# region Test Helper Methods
@pytest_asyncio.fixture
async def async_clients() -> AsyncGenerator[tuple[AsyncOpenAI, LoggingAsyncClient], None]:
openai_settings = OpenAISettings()
logging_async_client = LoggingAsyncClient()
async with AsyncOpenAI(
api_key=openai_settings.api_key.get_secret_value(), http_client=logging_async_client
) as client:
yield client, logging_async_client
async def run_prompt(
kernel: Kernel,
is_inline: bool = False,
is_streaming: bool = False,
template_format: Literal[
"semantic-kernel",
"handlebars",
"jinja2",
] = None,
prompt: str = None,
arguments: KernelArguments = None,
prompt_template_config: PromptTemplateConfig = None,
):
if is_inline:
if is_streaming:
try:
async for _ in kernel.invoke_prompt_stream(
function_name="func_test_stream",
plugin_name="plugin_test",
prompt=prompt,
arguments=arguments,
template_format=template_format,
prompt_template_config=prompt_template_config,
):
pass
except NotImplementedError:
pass
else:
await kernel.invoke_prompt(
function_name="func_test",
plugin_name="plugin_test_stream",
prompt=prompt,
arguments=arguments,
template_format=template_format,
prompt_template_config=prompt_template_config,
)
else:
function = KernelFunctionFromPrompt(
function_name="test_func",
plugin_name="test_plugin",
prompt=prompt,
template_format=template_format,
prompt_template_config=prompt_template_config,
)
await run_function(kernel, is_streaming, function=function, arguments=arguments)
async def run_function(
kernel: Kernel,
is_streaming: bool = False,
function: KernelFunction | None = None,
arguments: KernelArguments | None = None,
):
if is_streaming:
try:
async for _ in kernel.invoke_stream(function=function, arguments=arguments):
pass
except NotImplementedError:
pass
else:
await kernel.invoke(function=function, arguments=arguments)
class City:
def __init__(self, name):
self.name = name
# endregion
# region Test Prompt With Chat Roles
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(
True,
False,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_inline_non_streaming",
),
pytest.param(
True,
True,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_inline_streaming",
),
pytest.param(
False,
False,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_non_inline_non_streaming",
),
pytest.param(
False,
True,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_non_inline_streaming",
),
pytest.param(
False,
False,
"handlebars",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="hb_non_inline_non_streaming",
),
pytest.param(
False,
True,
"handlebars",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="hb_non_inline_streaming",
),
pytest.param(
False,
False,
"jinja2",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="j2_non_inline_non_streaming",
),
pytest.param(
False,
True,
"jinja2",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="j2_non_inline_streaming",
),
],
)
async def test_prompt_with_chat_roles(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="test",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
template_format=template_format,
prompt=prompt,
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_chat_roles_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Prompt With Complex Objects
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(
False,
False,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_non_inline_non_streaming",
),
pytest.param(
False,
True,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_non_inline_streaming",
),
pytest.param(
False,
False,
"jinja2",
"Can you help me tell the time in {{city.name}} right now?",
id="j2_non_inline_non_streaming",
),
pytest.param(
False,
True,
"jinja2",
"Can you help me tell the time in {{city.name}} right now?",
id="j2_non_inline_streaming",
),
pytest.param(
True,
False,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_inline_non_streaming",
),
pytest.param(
True,
True,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_inline_streaming",
),
pytest.param(
True,
False,
"jinja2",
"Can you help me tell the time in {{city.name}} right now?",
id="j2_inline_non_streaming",
),
pytest.param(
True, True, "jinja2", "Can you help me tell the time in {{city.name}} right now?", id="j2_inline_streaming"
),
],
)
async def test_prompt_with_complex_objects(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
prompt=prompt,
template_format=template_format,
prompt_template_config=PromptTemplateConfig(
template=prompt, template_format=template_format, allow_dangerously_set_content=True
),
arguments=KernelArguments(city=City("Seattle")),
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_complex_objects_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Prompt With Helper Functions
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(True, False, "semantic-kernel", sk_prompt, id="sk_inline_non_streaming"),
pytest.param(True, True, "semantic-kernel", sk_prompt, id="sk_inline_streaming"),
pytest.param(False, False, "semantic-kernel", sk_prompt, id="sk_non_inline_non_streaming"),
pytest.param(False, True, "semantic-kernel", sk_prompt, id="sk_non_inline_streaming"),
pytest.param(
False,
False,
"handlebars",
hb_prompt,
id="hb_non_inline_non_streaming",
marks=pytest.mark.xfail(reason="Throws intermittent APIConnectionError errors"),
),
pytest.param(False, True, "handlebars", hb_prompt, id="hb_non_inline_streaming"),
pytest.param(False, False, "jinja2", j2_prompt, id="j2_non_inline_non_streaming"),
pytest.param(False, True, "jinja2", j2_prompt, id="j2_non_inline_streaming"),
],
)
async def test_prompt_with_helper_functions(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
func = KernelFunctionFromMethod(
method=kernel_function(
lambda: datetime.datetime(1989, 6, 4, 12, 11, 13, tzinfo=datetime.timezone.utc).strftime(
"%a, %d %b %Y %H:%M:%S GMT"
),
name="Now",
),
plugin_name="Time",
)
kernel.add_function(plugin_name="Time", function=func)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
prompt=prompt,
template_format=template_format,
prompt_template_config=PromptTemplateConfig(
template=prompt, template_format=template_format, allow_dangerously_set_content=True
),
arguments=KernelArguments(city="Seattle"),
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_helper_functions_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Prompt With Simple Variable
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(True, False, "semantic-kernel", sk_simple_prompt, id="sk_inline_non_streaming"),
pytest.param(True, True, "semantic-kernel", sk_simple_prompt, id="sk_inline_streaming"),
pytest.param(False, False, "semantic-kernel", sk_simple_prompt, id="sk_non_inline_non_streaming"),
pytest.param(False, True, "semantic-kernel", sk_simple_prompt, id="sk_non_inline_streaming"),
pytest.param(False, False, "handlebars", hb_simple_prompt, id="hb_non_inline_non_streaming"),
pytest.param(False, True, "handlebars", hb_simple_prompt, id="hb_non_inline_streaming"),
pytest.param(False, False, "jinja2", j2_simple_prompt, id="j2_non_inline_non_streaming"),
pytest.param(False, True, "jinja2", j2_simple_prompt, id="j2_non_inline_streaming"),
],
)
async def test_prompt_with_simple_variable(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
template_format=template_format,
prompt=prompt,
arguments=KernelArguments(city="Seattle"),
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_simple_variable_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Simple Prompt
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(True, False, "semantic-kernel", simple_prompt, id="sk_inline_non_streaming"),
pytest.param(True, True, "semantic-kernel", simple_prompt, id="sk_inline_streaming"),
pytest.param(False, False, "semantic-kernel", simple_prompt, id="sk_non_inline_non_streaming"),
pytest.param(False, True, "semantic-kernel", simple_prompt, id="sk_non_inline_streaming"),
pytest.param(False, False, "handlebars", simple_prompt, id="hb_non_inline_non_streaming"),
pytest.param(False, True, "handlebars", simple_prompt, id="hb_non_inline_streaming"),
pytest.param(False, False, "jinja2", simple_prompt, id="j2_non_inline_non_streaming"),
pytest.param(False, True, "jinja2", simple_prompt, id="j2_non_inline_streaming"),
],
)
async def test_simple_prompt(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
template_format=template_format,
prompt=prompt,
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_simple_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test YAML Prompts
@pytest.mark.parametrize(
"is_streaming, prompt_path, expected_result_path",
[
pytest.param(
False, "simple_prompt_test.yaml", "prompt_simple_expected.json", id="simple_prompt_test_non_streaming"
),
pytest.param(True, "simple_prompt_test.yaml", "prompt_simple_expected.json", id="simple_prompt_test_streaming"),
pytest.param(
False,
"prompt_with_chat_roles_test_hb.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_hb_non_streaming",
),
pytest.param(
True,
"prompt_with_chat_roles_test_hb.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_hb_streaming",
),
pytest.param(
False,
"prompt_with_chat_roles_test_j2.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_j2_non_streaming",
),
pytest.param(
True,
"prompt_with_chat_roles_test_j2.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_j2_streaming",
),
pytest.param(
False,
"prompt_with_simple_variable_test.yaml",
"prompt_with_simple_variable_expected.json",
id="prompt_with_simple_variable_non_streaming",
),
pytest.param(
True,
"prompt_with_simple_variable_test.yaml",
"prompt_with_simple_variable_expected.json",
id="prompt_with_simple_variable_streaming",
),
],
)
async def test_yaml_prompt(
is_streaming,
prompt_path,
expected_result_path,
kernel: Kernel,
async_clients: tuple[AsyncOpenAI, LoggingAsyncClient],
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel.add_service(ai_service)
prompt_dir = os.path.join(os.path.dirname(__file__), "data", f"{prompt_path}")
with open(prompt_dir) as f:
prompt_str = f.read()
function = KernelFunctionFromPrompt.from_yaml(yaml_str=prompt_str, plugin_name="yaml_plugin")
await run_function(kernel=kernel, is_streaming=is_streaming, function=function)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", f"{expected_result_path}")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test OpenAPI Plugin Load
async def setup_openapi_function_call(kernel: Kernel, function_name, arguments):
from semantic_kernel.connectors.openapi_plugin import OpenAPIFunctionExecutionParameters
openapi_spec_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", "light_bulb_api.json")
request_details = None
async def mock_request(request: httpx.Request):
nonlocal request_details
if request.method in ["POST", "PUT"]:
request_body = None
if request.content:
request_body = request.content.decode()
elif request.stream:
try:
stream_content = await request.stream.read()
if stream_content:
request_body = stream_content.decode()
except Exception:
request_body = None
request_details = {
"method": request.method,
"url": str(request.url),
"body": request_body,
"headers": dict(request.headers),
}
else:
request_details = {"method": request.method, "url": str(request.url), "params": dict(request.url.params)}
transport = httpx.MockTransport(mock_request)
async with httpx.AsyncClient(transport=transport) as client:
plugin = kernel.add_plugin_from_openapi(
plugin_name="LightControl",
openapi_document_path=openapi_spec_file,
execution_settings=OpenAPIFunctionExecutionParameters(
http_client=client,
server_url_validation_allowed_base_urls=["https://127.0.0.1"],
),
)
assert plugin is not None
with contextlib.suppress(Exception):
# It is expected that the API call will fail, ignore
await run_function(kernel=kernel, is_streaming=False, function=plugin[function_name], arguments=arguments)
return request_details
async def test_openapi_get_lights(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="GetLights", arguments=KernelArguments(roomId=1)
)
assert request_content is not None
assert request_content.get("method") == "GET"
assert request_content.get("url") == "https://127.0.0.1/Lights?roomId=1"
assert request_content.get("params") == {"roomId": "1"}
async def test_openapi_get_light_by_id(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="GetLightById", arguments=KernelArguments(id=1)
)
assert request_content is not None
assert request_content.get("method") == "GET"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
async def test_openapi_delete_light_by_id(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="DeleteLightById", arguments=KernelArguments(id=1)
)
assert request_content is not None
assert request_content.get("method") == "DELETE"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
async def test_openapi_create_lights(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="CreateLights", arguments=KernelArguments(roomId=1, lightName="disco")
)
assert request_content is not None
assert request_content.get("method") == "POST"
assert request_content.get("url") == "https://127.0.0.1/Lights?roomId=1&lightName=disco"
async def test_openapi_put_light_by_id(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="PutLightById", arguments=KernelArguments(id=1, hexColor="11EE11")
)
assert request_content is not None
assert request_content.get("method") == "PUT"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
assert request_content.get("body") == '{"hexColor":"11EE11"}'
# endregion
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import pytest
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from tests.integration.embeddings.test_embedding_service_base import (
EmbeddingServiceTestBase,
google_ai_setup,
mistral_ai_setup,
ollama_setup,
vertex_ai_setup,
)
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, output_dimensionality",
[
pytest.param(
"openai",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="openai",
),
pytest.param(
"azure",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="azure",
),
pytest.param(
"azure_custom_client",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="azure_custom_client",
),
pytest.param(
"azure_ai_inference",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="azure_ai_inference",
),
pytest.param(
"mistral_ai",
{},
1024,
marks=pytest.mark.skipif(not mistral_ai_setup, reason="Mistral AI environment variables not set"),
id="mistral_ai",
),
pytest.param(
"hugging_face",
{},
384,
id="hugging_face",
),
pytest.param(
"ollama",
{},
768,
marks=(
pytest.mark.skipif(not ollama_setup, reason="Ollama not setup"),
pytest.mark.ollama,
),
id="ollama",
),
pytest.param(
"google_ai",
{"output_dimensionality": 10},
10,
marks=pytest.mark.skipif(not google_ai_setup, reason="Google AI environment variables not set"),
id="google_ai",
),
pytest.param(
"vertex_ai",
{"output_dimensionality": 10},
10,
marks=(
pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
pytest.mark.timeout(300), # Vertex AI may take longer time
),
id="vertex_ai",
),
pytest.param(
"bedrock_amazon_titan-v1",
{},
1536, # This model doesn't support custom output dimensionality
id="bedrock_amazon_titan-v1",
),
pytest.param(
"bedrock_amazon_titan-v2",
{"extension_data": {"dimensions": 256}},
256,
id="bedrock_amazon_titan-v2",
),
pytest.param(
"bedrock_cohere",
{},
1024,
id="bedrock_cohere",
),
],
)
class TestEmbeddingService(EmbeddingServiceTestBase):
"""Test embedding service with memory.
This tests if the embedding service can be used with the semantic memory.
"""
async def test_embedding_service(
self,
service_id,
services: dict[str, tuple[EmbeddingGeneratorBase, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
output_dimensionality: int,
):
embedding_generator, settings_type = services[service_id]
embeddings = await embedding_generator.generate_embeddings(
texts=["Hello, world!", "Hello, universe!"],
settings=settings_type(**execution_settings_kwargs),
)
assert embeddings.shape == (2, output_dimensionality)
@@ -0,0 +1,129 @@
# Copyright (c) Microsoft. All rights reserved.
from importlib import util
import pytest
from azure.ai.inference.aio import EmbeddingsClient
from azure.identity import AzureCliCredential
from openai import AsyncAzureOpenAI
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceEmbeddingPromptExecutionSettings,
AzureAIInferenceTextEmbedding,
)
from semantic_kernel.connectors.ai.bedrock import BedrockEmbeddingPromptExecutionSettings, BedrockTextEmbedding
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.google.google_ai import (
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAITextEmbedding,
)
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextEmbedding
from semantic_kernel.connectors.ai.mistral_ai import MistralAITextEmbedding
from semantic_kernel.connectors.ai.ollama import OllamaEmbeddingPromptExecutionSettings, OllamaTextEmbedding
from semantic_kernel.connectors.ai.open_ai import (
AzureOpenAISettings,
AzureTextEmbedding,
OpenAIEmbeddingPromptExecutionSettings,
OpenAITextEmbedding,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
from tests.utils import is_service_setup_for_testing
hugging_face_setup = util.find_spec("torch") is not None
# Make sure all services are setup for before running the tests
# The following exceptions apply:
# 1. OpenAI and Azure OpenAI services are always setup for testing.
azure_openai_setup = True
# 2. The current Hugging Face service don't require any environment variables.
# 3. Bedrock services don't use API keys and model providers are tested individually,
# so no environment variables are required.
mistral_ai_setup: bool = is_service_setup_for_testing(
["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"], raise_if_not_set=False
) # We don't have a MistralAI deployment
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_EMBEDDING_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing([
"GOOGLE_AI_CLOUD_PROJECT_ID",
"GOOGLE_AI_EMBEDDING_MODEL_ID",
"GOOGLE_AI_CLOUD_REGION",
])
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_EMBEDDING_MODEL_ID"])
# When testing Bedrock, after logging into AWS CLI this has been set, so we can use it to check if the service is setup
bedrock_setup: bool = is_service_setup_for_testing(["AWS_DEFAULT_REGION"], raise_if_not_set=False)
class EmbeddingServiceTestBase:
@pytest.fixture(scope="class")
def services(self) -> dict[str, tuple[EmbeddingGeneratorBase | None, type[PromptExecutionSettings]]]:
azure_openai_setup = True
credential = AzureCliCredential()
azure_openai_settings = AzureOpenAISettings()
endpoint = str(azure_openai_settings.endpoint)
deployment_name = azure_openai_settings.embedding_deployment_name
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
if not ad_token:
azure_openai_setup = False
api_version = azure_openai_settings.api_version
azure_custom_client = None
azure_ai_inference_client = None
if azure_openai_setup:
azure_custom_client = AzureTextEmbedding(
async_client=AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
azure_ad_token=ad_token,
api_version=api_version,
default_headers={"Test-User-X-ID": "test"},
),
credential=credential,
)
azure_ai_inference_client = AzureAIInferenceTextEmbedding(
ai_model_id=deployment_name,
client=EmbeddingsClient(
endpoint=f"{endpoint.strip('/')}/openai/deployments/{deployment_name}",
credential=credential,
credential_scopes=["https://cognitiveservices.azure.com/.default"],
),
)
return {
"openai": (OpenAITextEmbedding(), OpenAIEmbeddingPromptExecutionSettings),
"azure": (
AzureTextEmbedding(credential=credential) if azure_openai_setup else None,
OpenAIEmbeddingPromptExecutionSettings,
),
"azure_custom_client": (azure_custom_client, OpenAIEmbeddingPromptExecutionSettings),
"azure_ai_inference": (azure_ai_inference_client, AzureAIInferenceEmbeddingPromptExecutionSettings),
"mistral_ai": (
MistralAITextEmbedding() if mistral_ai_setup else None,
PromptExecutionSettings,
),
"hugging_face": (
HuggingFaceTextEmbedding(ai_model_id="sentence-transformers/all-MiniLM-L6-v2")
if hugging_face_setup
else None,
PromptExecutionSettings,
),
"ollama": (OllamaTextEmbedding() if ollama_setup else None, OllamaEmbeddingPromptExecutionSettings),
"google_ai": (
GoogleAITextEmbedding() if google_ai_setup else None,
GoogleAIEmbeddingPromptExecutionSettings,
),
"vertex_ai": (
GoogleAITextEmbedding(use_vertexai=True) if vertex_ai_setup else None,
GoogleAIEmbeddingPromptExecutionSettings,
),
"bedrock_amazon_titan-v1": (
BedrockTextEmbedding(model_id="amazon.titan-embed-text-v1") if bedrock_setup else None,
BedrockEmbeddingPromptExecutionSettings,
),
"bedrock_amazon_titan-v2": (
BedrockTextEmbedding(model_id="amazon.titan-embed-text-v2:0") if bedrock_setup else None,
BedrockEmbeddingPromptExecutionSettings,
),
"bedrock_cohere": (
BedrockTextEmbedding(model_id="cohere.embed-english-v3") if bedrock_setup else None,
BedrockEmbeddingPromptExecutionSettings,
),
}
@@ -0,0 +1,169 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import pytest
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory
from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore
from tests.integration.embeddings.test_embedding_service_base import (
EmbeddingServiceTestBase,
google_ai_setup,
mistral_ai_setup,
ollama_setup,
vertex_ai_setup,
)
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs",
[
pytest.param(
"openai",
{},
id="openai",
),
pytest.param(
"azure",
{},
id="azure",
),
pytest.param(
"azure_custom_client",
{},
id="azure_custom_client",
),
pytest.param(
"azure_ai_inference",
{},
id="azure_ai_inference",
),
pytest.param(
"mistral_ai",
{},
marks=pytest.mark.skipif(not mistral_ai_setup, reason="Mistral AI environment variables not set"),
id="mistral_ai",
),
pytest.param(
"hugging_face",
{},
id="hugging_face",
),
pytest.param(
"ollama",
{},
marks=(
pytest.mark.skipif(not ollama_setup, reason="Ollama environment variables not set"),
pytest.mark.ollama,
),
id="ollama",
),
pytest.param(
"google_ai",
{},
marks=pytest.mark.skipif(not google_ai_setup, reason="Google AI environment variables not set"),
id="google_ai",
),
pytest.param(
"vertex_ai",
{},
marks=(
pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
pytest.mark.timeout(300), # Vertex AI may take longer time
),
id="vertex_ai",
),
pytest.param(
"bedrock_amazon_titan-v1",
{},
id="bedrock_amazon_titan-v1",
),
pytest.param(
"bedrock_amazon_titan-v2",
{},
marks=pytest.mark.skip(reason="This is known to fail to get the correct answer for 'What are birds?'"),
id="bedrock_amazon_titan-v2",
),
pytest.param(
"bedrock_cohere",
{},
id="bedrock_cohere",
),
],
)
class TestEmbeddingServiceWithMemory(EmbeddingServiceTestBase):
"""Test embedding service with memory.
This tests if the embedding service can be used with the semantic memory.
"""
async def test_embedding_service(
self,
service_id,
services: dict[str, tuple[EmbeddingGeneratorBase, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
):
embedding_generator, settings_type = services[service_id]
if embedding_generator is None:
pytest.skip(f"Service {service_id} not set up")
memory = SemanticTextMemory(
storage=VolatileMemoryStore(),
embeddings_generator=embedding_generator,
)
# Add some documents to the semantic memory
embeddings_kwargs = {"settings": settings_type(**execution_settings_kwargs)}
await memory.save_information(
"test",
id="info1",
text="Sharks are fish.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info2",
text="Whales are mammals.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info3",
text="Penguins are birds.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info4",
text="Dolphins are mammals.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info5",
text="Flies are insects.",
embeddings_kwargs=embeddings_kwargs,
)
# Search for documents
query = "What are mammals?"
result = await memory.search("test", query, limit=2, min_relevance_score=0.0)
assert "mammals." in result[0].text
assert "mammals." in result[1].text
query = "What are fish?"
result = await memory.search("test", query, limit=1, min_relevance_score=0.0)
assert result[0].text == "Sharks are fish."
query = "What are insects?"
result = await memory.search("test", query, limit=1, min_relevance_score=0.0)
assert result[0].text == "Flies are insects."
query = "What are birds?"
result = await memory.search("test", query, limit=1, min_relevance_score=0.0)
assert result[0].text == "Penguins are birds."
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class EmailPluginFake:
@kernel_function(
description="Given an email address and message body, send an email",
name="SendEmail",
)
def send_email(self, input: str) -> str:
return f"Sent email to: . Body: {input}"
@kernel_function(
description="Lookup an email address for a person given a name",
name="GetEmailAddress",
)
def get_email_address(self, input: str) -> str:
if input == "":
return "johndoe1234@example.com"
return f"{input}@example.com"
@kernel_function(description="Write a short poem for an e-mail", name="WritePoem")
def write_poem(self, input: str) -> str:
return f"Roses are red, violets are blue, {input} is hard, so is this test."
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
# TODO: this fake plugin is temporal usage.
# C# supports import plugin from samples dir by using test helper and python should do the same
# `semantic-kernel/dotnet/src/IntegrationTests/TestHelpers.cs`
class FunPluginFake:
@kernel_function(
description="Write a joke",
name="WriteJoke",
)
def write_joke(self) -> str:
return "WriteJoke"
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
# TODO: this fake plugin is temporal usage.
# C# supports import plugin from samples dir by using test helper and python should do the same
# `semantic-kernel/dotnet/src/IntegrationTests/TestHelpers.cs`
class SummarizePluginFake:
@kernel_function(
description="Summarize",
name="Summarize",
)
def translate(self) -> str:
return "Summarize"
@@ -0,0 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions import kernel_function
# TODO: this fake plugin is temporal usage.
# C# supports import plugin from samples dir by using test helper and python should do the same
# `semantic-kernel/dotnet/src/IntegrationTests/TestHelpers.cs`
class WriterPluginFake:
@kernel_function(
description="Translate",
name="Translate",
)
def translate(self, language: str) -> str:
return f"Translate: {language}"
@kernel_function(name="NovelOutline")
def write_novel_outline(
self,
input: Annotated[str, "The input of the function"],
name: Annotated[str, "The name of the function"] = "endMarker",
description: Annotated[str, "The marker to use to end each chapter"] = "Write an outline for a novel.",
default_value: Annotated[str, "The default value used for the function"] = "<!--===ENDPART===-->",
) -> str:
return f"Novel outline: {input}"
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
from semantic_kernel.kernel import Kernel
def test_kernel_deep_copy_fail_with_services():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
with pytest.raises(TypeError):
# This will fail because OpenAIChatCompletion is not serializable, more specifically,
# the client is not serializable
kernel.model_copy(deep=True)
async def test_kernel_clone():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
kernel_clone = kernel.clone()
assert kernel_clone is not None
assert kernel_clone.services is not None and len(kernel_clone.services) > 0
function_result = await kernel.invoke_prompt("Hello World")
assert function_result is not None
assert function_result.value is not None
assert len(str(function_result)) > 0
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import TYPE_CHECKING
from semantic_kernel.connectors.mcp import MCPStdioPlugin
from semantic_kernel.functions.kernel_arguments import KernelArguments
if TYPE_CHECKING:
from semantic_kernel import Kernel
async def test_from_mcp(kernel: "Kernel"):
mcp_server_path = os.path.join(
os.path.dirname(__file__), "../../assets/test_plugins", "TestMCPPlugin", "mcp_server.py"
)
async with MCPStdioPlugin(
name="TestMCPPlugin",
command="python",
args=[mcp_server_path],
) as plugin:
assert plugin is not None
assert plugin.name == "TestMCPPlugin"
loaded_plugin = kernel.add_plugin(plugin)
assert loaded_plugin is not None
assert loaded_plugin.name == "TestMCPPlugin"
assert len(loaded_plugin.functions) == 2
result = await loaded_plugin.functions["echo_tool"].invoke(kernel, arguments=KernelArguments(message="test"))
assert "Tool echo: test" in result.value[0].text
result = await loaded_plugin.functions["echo_prompt"].invoke(kernel, arguments=KernelArguments(message="test"))
assert "test" in result.value[0].content
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft. All rights reserved.
from dataclasses import field
from typing import Annotated, Any
from uuid import uuid4
from pydantic import BaseModel
from pytest import fixture
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
@fixture
def data_record() -> dict[str, Any]:
return {
"id": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"description": "This is a test record",
"product_type": "test",
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
}
@fixture
def record_type() -> type:
@vectorstoremodel
class TestDataModelType(BaseModel):
vector: Annotated[
list[float] | None,
VectorStoreField(
"vector",
index_kind="flat",
dimensions=5,
distance_function="cosine_similarity",
type="float",
),
] = None
id: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
product_type: Annotated[str, VectorStoreField("data")] = "N/A"
description: Annotated[
str, VectorStoreField("data", has_embedding=True, embedding_property_name="vector", type="str")
] = "N/A"
return TestDataModelType
@fixture
def data_record_with_key_as_key_field() -> dict[str, Any]:
return {
"key": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"description": "This is a test record",
"product_type": "test",
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
}
@fixture
def record_type_with_key_as_key_field() -> type:
@vectorstoremodel
class TestDataModelType(BaseModel):
vector: Annotated[
list[float] | None,
VectorStoreField(
"vector",
index_kind="flat",
dimensions=5,
distance_function="cosine_similarity",
type="float",
),
] = None
key: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
product_type: Annotated[str, VectorStoreField("data")] = "N/A"
description: Annotated[
str, VectorStoreField("data", has_embedding=True, embedding_property_name="vector", type="str")
] = "N/A"
return TestDataModelType
@@ -0,0 +1,231 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import platform
from collections.abc import Callable
from typing import Any
import pytest
from azure.cosmos.aio import CosmosClient
from azure.cosmos.partition_key import PartitionKey
from semantic_kernel.connectors.azure_cosmos_db import CosmosNoSqlCompositeKey, CosmosNoSqlStore
from semantic_kernel.data.vector import VectorStore
from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorException
from tests.integration.memory.vector_store_test_base import VectorStoreTestBase
@pytest.mark.skipif(
platform.system() != "Windows",
reason="The Azure Cosmos DB Emulator is only available on Windows.",
)
class TestCosmosDBNoSQL(VectorStoreTestBase):
"""Test Cosmos DB NoSQL store functionality."""
async def test_list_collection_names(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
):
"""Test list collection names."""
async with stores["azure_cosmos_db_no_sql"]() as store:
assert await store.list_collection_names() == []
collection_name = "list_collection_names"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
await collection.ensure_collection_exists()
collection_names = await store.list_collection_names()
assert collection_name in collection_names
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
collection_names = await store.list_collection_names()
assert collection_name not in collection_names
async def test_collection_not_created(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test get without collection."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "collection_not_created"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
assert await collection.collection_exists() is False
with pytest.raises(
MemoryConnectorException, match="The collection does not exist yet. Create the collection first."
):
await collection.upsert(record_type(**data_record))
with pytest.raises(
MemoryConnectorException, match="The collection does not exist yet. Create the collection first."
):
await collection.get(data_record["id"])
with pytest.raises(MemoryConnectorException):
await collection.delete(data_record["id"])
with pytest.raises(MemoryConnectorException, match="Container could not be deleted."):
await collection.ensure_collection_deleted()
async def test_custom_partition_key(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test custom partition key."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "custom_partition_key"
collection = store.get_collection(
collection_name=collection_name,
record_type=record_type,
partition_key=PartitionKey(path="/product_type"),
)
composite_key = CosmosNoSqlCompositeKey(key=data_record["id"], partition_key=data_record["product_type"])
# Upsert
await collection.ensure_collection_exists()
await collection.upsert(record_type(**data_record))
# Verify
record = await collection.get(composite_key)
assert record is not None
assert isinstance(record, record_type)
# Remove
await collection.delete(composite_key)
record = await collection.get(composite_key)
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_get_include_vector(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test get with include_vector."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "get_include_vector"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
# Upsert
await collection.ensure_collection_exists()
await collection.upsert(record_type(**data_record))
# Verify
record = await collection.get(data_record["id"], include_vectors=True)
assert record is not None
assert isinstance(record, record_type)
assert record.vector == data_record["vector"]
# Remove
await collection.delete(data_record["id"])
record = await collection.get(data_record["id"])
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_get_not_include_vector(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test get with include_vector."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "get_not_include_vector"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
# Upsert
await collection.ensure_collection_exists()
await collection.upsert(record_type(**data_record))
# Verify
record = await collection.get(data_record["id"], include_vectors=False)
assert record is not None
assert isinstance(record, record_type)
assert record.vector is None
# Remove
await collection.delete(data_record["id"])
record = await collection.get(data_record["id"])
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_collection_with_key_as_key_field(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type_with_key_as_key_field: type,
data_record_with_key_as_key_field: dict[str, Any],
):
"""Test collection with key as key field."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "collection_with_key_as_key_field"
collection = store.get_collection(
collection_name=collection_name, record_type=record_type_with_key_as_key_field
)
# Upsert
await collection.ensure_collection_exists()
result = await collection.upsert(record_type_with_key_as_key_field(**data_record_with_key_as_key_field))
assert data_record_with_key_as_key_field["key"] == result
# Verify
record = await collection.get(data_record_with_key_as_key_field["key"])
assert record is not None
assert isinstance(record, record_type_with_key_as_key_field)
assert record.key == data_record_with_key_as_key_field["key"]
# Remove
await collection.delete(data_record_with_key_as_key_field["key"])
record = await collection.get(data_record_with_key_as_key_field["key"])
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_custom_client(
self,
record_type: type,
):
"""Test list collection names."""
url = os.environ.get("AZURE_COSMOS_DB_NO_SQL_URL")
key = os.environ.get("AZURE_COSMOS_DB_NO_SQL_KEY")
async with (
CosmosClient(url, key) as custom_client,
CosmosNoSqlStore(
database_name="test_database",
cosmos_client=custom_client,
create_database=True,
) as store,
):
assert await store.list_collection_names() == []
collection_name = "list_collection_names"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
await collection.ensure_collection_exists()
collection_names = await store.list_collection_names()
assert collection_name in collection_names
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
collection_names = await store.list_collection_names()
assert collection_name not in collection_names
@@ -0,0 +1,145 @@
# Copyright (c) Microsoft. All rights reserved.
import numpy as np
RAW_RECORD_LIST = {
"id": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"content": "test content",
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
}
RAW_RECORD_ARRAY = {
"id": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"content": "test content",
"vector": np.array([0.1, 0.2, 0.3, 0.4, 0.5]),
}
# PANDAS_RECORD_DEFINITION = VectorStoreRecordDefinition(
# fields={
# "vector": VectorStoreRecordVectorField(
# name="vector",
# index_kind="hnsw",
# dimensions=5,
# distance_function="cosine_similarity",
# property_type="float",
# ),
# "id": VectorStoreRecordKeyField(name="id"),
# "content": VectorStoreRecordDataField(
# name="content", has_embedding=True, embedding_property_name="vector", property_type="str"
# ),
# },
# container_mode=True,
# to_dict=lambda x: x.to_dict(orient="records"),
# from_dict=lambda x, **_: pd.DataFrame(x),
# )
# A Pandas record definition with flat index kind
# PANDAS_RECORD_DEFINITION_FLAT = VectorStoreRecordDefinition(
# fields={
# "vector": VectorStoreRecordVectorField(
# name="vector",
# index_kind="flat",
# dimensions=5,
# distance_function="cosine_similarity",
# property_type="float",
# ),
# "id": VectorStoreRecordKeyField(name="id"),
# "content": VectorStoreRecordDataField(
# name="content", has_embedding=True, embedding_property_name="vector", property_type="str"
# ),
# },
# container_mode=True,
# to_dict=lambda x: x.to_dict(orient="records"),
# from_dict=lambda x, **_: pd.DataFrame(x),
# )
# @vectorstoremodel
# @dataclass
# class TestDataModelArray(distance_function: str):
# """A data model where the vector is a numpy array."""
# vector: Annotated[
# np.ndarray | None,
# VectorStoreRecordVectorField(
# index_kind="hnsw",
# dimensions=5,
# distance_function=distance_function,
# property_type="float",
# serialize_function=np.ndarray.tolist,
# deserialize_function=np.array,
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
# @vectorstoremodel
# @dataclass
# class TestDataModelArrayFlat(distance_function:str):
# """A data model where the vector is a numpy array and the index kind is IndexKind.Flat."""
# vector: Annotated[
# np.ndarray | None,
# VectorStoreRecordVectorField(
# index_kind="flat",
# dimensions=5,
# distance_function=distance_function,
# property_type="float",
# serialize_function=np.ndarray.tolist,
# deserialize_function=np.array,
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
# @vectorstoremodel
# @dataclass
# class TestDataModelList(distance_function: str):
# """A data model where the vector is a list."""
# vector: Annotated[
# list[float] | None,
# VectorStoreRecordVectorField(
# index_kind="hnsw",
# dimensions=5,
# distance_function=distance_function,
# property_type="float",
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
# @vectorstoremodel
# @dataclass
# class TestDataModelListFlat:
# """A data model where the vector is a list and the index kind is IndexKind.Flat."""
# vector: Annotated[
# list[float] | None,
# VectorStoreRecordVectorField(
# index_kind="flat",
# dimensions=5,
# distance_function="cosine_similarity",
# property_type="float",
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
@@ -0,0 +1,259 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from typing import Annotated, Any
import pandas as pd
import pytest
import pytest_asyncio
from pydantic import BaseModel
from semantic_kernel.connectors.postgres import PostgresCollection, PostgresSettings, PostgresStore
from semantic_kernel.data.vector import (
DistanceFunction,
IndexKind,
VectorStoreCollectionDefinition,
VectorStoreField,
vectorstoremodel,
)
from semantic_kernel.exceptions.memory_connector_exceptions import (
MemoryConnectorConnectionException,
MemoryConnectorInitializationError,
)
try:
import psycopg # noqa: F401
import psycopg_pool # noqa: F401
psycopg_pool_installed = True
except ImportError:
psycopg_pool_installed = False
pg_settings: PostgresSettings = PostgresSettings()
try:
connection_params_present = any(pg_settings.get_connection_args().values())
except MemoryConnectorInitializationError:
connection_params_present = False
pytestmark = pytest.mark.skipif(
not (psycopg_pool_installed or connection_params_present),
reason="psycopg_pool is not installed" if not psycopg_pool_installed else "No connection parameters provided",
)
@vectorstoremodel
class SimpleDataModel(BaseModel):
id: Annotated[int, VectorStoreField("key")]
embedding: Annotated[
list[float] | str | None,
VectorStoreField(
"vector",
index_kind=IndexKind.HNSW,
dimensions=3,
distance_function=DistanceFunction.COSINE_SIMILARITY,
),
] = None
data: Annotated[
dict[str, Any],
VectorStoreField("data", type="JSONB"),
]
def model_post_init(self, context: Any) -> None:
if self.embedding is None:
self.embedding = self.data
def DataModelPandas(record) -> tuple:
definition = VectorStoreCollectionDefinition(
fields=[
VectorStoreField(
"vector",
name="embedding",
index_kind="hnsw",
dimensions=3,
distance_function="cosine_similarity",
type="float",
),
VectorStoreField("key", name="id", type="int"),
VectorStoreField("data", name="data", type="dict"),
],
container_mode=True,
to_dict=lambda x: x.to_dict(orient="records"),
from_dict=lambda x, **_: pd.DataFrame(x),
)
df = pd.DataFrame([record])
return definition, df
@pytest_asyncio.fixture
async def vector_store() -> AsyncGenerator[PostgresStore, None]:
try:
async with await pg_settings.create_connection_pool() as pool:
yield PostgresStore(connection_pool=pool)
except MemoryConnectorConnectionException:
pytest.skip("Postgres connection not available")
yield None
return
@asynccontextmanager
async def create_simple_collection(
vector_store: PostgresStore,
) -> AsyncGenerator[PostgresCollection[int, SimpleDataModel], None]:
"""Returns a collection with a unique name that is deleted after the context.
This can be moved to use a fixture with scope=function and loop_scope=session
after upgrade to pytest-asyncio 0.24. With the current version, the fixture
would both cache and use the event loop of the declared scope.
"""
suffix = str(uuid.uuid4()).replace("-", "")[:8]
collection_id = f"test_collection_{suffix}"
collection = vector_store.get_collection(collection_name=collection_id, record_type=SimpleDataModel)
assert isinstance(collection, PostgresCollection)
await collection.ensure_collection_exists()
try:
yield collection
finally:
await collection.ensure_collection_deleted()
def test_create_store(vector_store):
assert vector_store is not None
assert vector_store.connection_pool is not None
async def test_ensure_collection_exists_exists_and_delete(vector_store: PostgresStore):
suffix = str(uuid.uuid4()).replace("-", "")[:8]
collection = vector_store.get_collection(collection_name=f"test_collection_{suffix}", record_type=SimpleDataModel)
does_exist_1 = await collection.collection_exists()
assert does_exist_1 is False
await collection.ensure_collection_exists()
does_exist_2 = await collection.collection_exists()
assert does_exist_2 is True
await collection.ensure_collection_deleted()
does_exist_3 = await collection.collection_exists()
assert does_exist_3 is False
async def test_list_collection_names(vector_store):
async with create_simple_collection(vector_store) as simple_collection:
simple_collection_id = simple_collection.collection_name
result = await vector_store.list_collection_names()
assert simple_collection_id in result
async def test_upsert_get_and_delete(vector_store: PostgresStore):
record = SimpleDataModel(id=1, embedding=[1.1, 2.2, 3.3], data={"key": "value"})
async with create_simple_collection(vector_store) as simple_collection:
result_before_upsert = await simple_collection.get(1)
assert result_before_upsert is None
await simple_collection.upsert(record)
result = await simple_collection.get(1)
assert result is not None
assert result.id == record.id
assert result.embedding == record.embedding
assert result.data == record.data
# Check that the table has an index
connection_pool = simple_collection.connection_pool
async with connection_pool.connection() as conn, conn.cursor() as cur:
await cur.execute(
"SELECT indexname FROM pg_indexes WHERE tablename = %s", (simple_collection.collection_name,)
)
rows = await cur.fetchall()
index_names = [index[0] for index in rows]
assert any("embedding_idx" in index_name for index_name in index_names)
await simple_collection.delete(1)
result_after_delete = await simple_collection.get(1)
assert result_after_delete is None
async def test_upsert_get_and_delete_pandas(vector_store):
record = SimpleDataModel(id=1, embedding=[1.1, 2.2, 3.3], data={"key": "value"})
definition, df = DataModelPandas(record.model_dump())
suffix = str(uuid.uuid4()).replace("-", "")[:8]
collection = vector_store.get_collection(
collection_name=f"test_collection_{suffix}",
record_type=pd.DataFrame,
definition=definition,
)
await collection.ensure_collection_exists()
try:
result_before_upsert = await collection.get(1)
assert result_before_upsert is None
await collection.upsert(df)
result: pd.DataFrame = await collection.get(1)
assert result is not None
row = result.iloc[0]
assert row.id == record.id
assert row.embedding == record.embedding
assert row.data == record.data
await collection.delete(1)
result_after_delete = await collection.get(1)
assert result_after_delete is None
finally:
await collection.ensure_collection_deleted()
async def test_upsert_get_and_delete_multiple(vector_store: PostgresStore):
async with create_simple_collection(vector_store) as simple_collection:
record1 = SimpleDataModel(id=1, embedding=[1.1, 2.2, 3.3], data={"key": "value"})
record2 = SimpleDataModel(id=2, embedding=[4.4, 5.5, 6.6], data={"key": "value"})
result_before_upsert = await simple_collection.get([1, 2])
assert result_before_upsert is None
await simple_collection.upsert([record1, record2])
# Test get for the two existing keys and one non-existing key;
# this should return only the two existing records.
result = await simple_collection.get([1, 2, 3])
assert result is not None
assert isinstance(result, Sequence)
assert len(result) == 2
assert result[0] is not None
assert result[0].id == record1.id
assert result[0].embedding == record1.embedding
assert result[0].data == record1.data
assert result[1] is not None
assert result[1].id == record2.id
assert result[1].embedding == record2.embedding
assert result[1].data == record2.data
await simple_collection.delete([1, 2])
result_after_delete = await simple_collection.get([1, 2])
assert result_after_delete is None
async def test_search(vector_store: PostgresStore):
async with create_simple_collection(vector_store) as simple_collection:
records = [
SimpleDataModel(id=1, embedding=[1.0, 0.0, 0.0], data={"key": "value1"}),
SimpleDataModel(id=2, embedding=[0.8, 0.2, 0.0], data={"key": "value2"}),
SimpleDataModel(id=3, embedding=[0.6, 0.0, 0.4], data={"key": "value3"}),
SimpleDataModel(id=4, embedding=[1.0, 1.0, 0.0], data={"key": "value4"}),
SimpleDataModel(id=5, embedding=[0.0, 1.0, 1.0], data={"key": "value5"}),
SimpleDataModel(id=6, embedding=[1.0, 0.0, 1.0], data={"key": "value6"}),
]
await simple_collection.upsert(records)
try:
search_results = await simple_collection.search(vector=[1.0, 0.0, 0.0], top=3, include_total_count=True)
assert search_results is not None
assert search_results.total_count == 3
assert {result.record.id async for result in search_results.results} == {1, 2, 3}
finally:
await simple_collection.delete([r.id for r in records])
@@ -0,0 +1,363 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import platform
from collections.abc import Callable
from typing import Any
import pandas as pd
import pytest
from semantic_kernel.connectors.redis import RedisCollectionTypes
from semantic_kernel.data.vector import VectorStore
from semantic_kernel.exceptions import MemoryConnectorConnectionException
from tests.integration.memory.data_records import RAW_RECORD_LIST
from tests.integration.memory.vector_store_test_base import VectorStoreTestBase
logger: logging.Logger = logging.getLogger(__name__)
class TestVectorStore(VectorStoreTestBase):
"""Test vector store functionality.
This only tests if the vector stores can upsert, get, and delete records.
"""
@pytest.mark.parametrize(
[
"store_id",
"collection_name",
"collection_options",
"record_type",
"definition",
"distance_function",
"index_kind",
"vector_property_type",
"dimensions",
"record",
],
[
# region Redis
pytest.param(
"redis",
"redis_json_list_data_model",
{"collection_type": RedisCollectionTypes.JSON},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_json_list_data_model",
),
pytest.param(
"redis",
"redis_json_pandas_data_model",
{"collection_type": RedisCollectionTypes.JSON},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_json_pandas_data_model",
),
pytest.param(
"redis",
"redis_hashset_list_data_model",
{"collection_type": RedisCollectionTypes.HASHSET},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_hashset_list_data_model",
),
pytest.param(
"redis",
"redis_hashset_pandas_data_model",
{"collection_type": RedisCollectionTypes.HASHSET},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_hashset_pandas_data_model",
),
# endregion
# region Azure AI Search
pytest.param(
"azure_ai_search",
"azure_ai_search_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="azure_ai_search_list_data_model",
),
pytest.param(
"azure_ai_search",
"azure_ai_search_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="azure_ai_search_pandas_data_model",
),
# endregion
# region Qdrant
pytest.param(
"qdrant",
"qdrant_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_list_data_model",
),
pytest.param(
"qdrant",
"qdrant_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_pandas_data_model",
),
pytest.param(
"qdrant_in_memory",
"qdrant_in_memory_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_in_memory_list_data_model",
),
pytest.param(
"qdrant_in_memory",
"qdrant_in_memory_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_in_memory_pandas_data_model",
),
pytest.param(
"qdrant",
"qdrant_grpc_list_data_model",
{"prefer_grpc": True},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_grpc_list_data_model",
),
pytest.param(
"qdrant",
"qdrant_grpc_pandas_data_model",
{"prefer_grpc": True},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_grpc_pandas_data_model",
),
# endregion
# region Weaviate
pytest.param(
"weaviate_local",
"weaviate_local_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Linux",
reason="The Weaviate docker image is only available on Linux"
" but some GitHubs job runs in a Windows container.",
),
id="weaviate_local_list_data_model",
),
pytest.param(
"weaviate_local",
"weaviate_local_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Linux",
reason="The Weaviate docker image is only available on Linux"
" but some GitHubs job runs in a Windows container.",
),
id="weaviate_local_pandas_data_model",
),
# endregion
# region Azure Cosmos DB
pytest.param(
"azure_cosmos_db_no_sql",
"azure_cosmos_db_no_sql_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
"flat",
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Windows",
reason="The Azure Cosmos DB Emulator is only available on Windows.",
),
id="azure_cosmos_db_no_sql_list_data_model",
),
pytest.param(
"azure_cosmos_db_no_sql",
"azure_cosmos_db_no_sql_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
"flat",
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Windows",
reason="The Azure Cosmos DB Emulator is only available on Windows.",
),
id="azure_cosmos_db_no_sql_pandas_data_model",
),
# endregion
# region Chroma
pytest.param(
"chroma",
"chroma_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="chroma_list_data_model",
),
pytest.param(
"chroma",
"chroma_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="chroma_pandas_data_model",
),
# endregion
],
)
# region test function
async def test_vector_store(
self,
stores: dict[str, Callable[[], VectorStore]],
store_id: str,
collection_name: str,
collection_options: dict[str, Any],
record_type: str | type,
definition: str | None,
distance_function,
index_kind,
vector_property_type,
dimensions,
record: dict[str, Any],
request,
):
"""Test vector store functionality."""
if isinstance(record_type, str):
record_type = request.getfixturevalue(record_type)
if definition is not None:
definition = request.getfixturevalue(definition)
try:
async with (
stores[store_id]() as vector_store,
vector_store.get_collection(
record_type=record_type,
definition=definition,
collection_name=collection_name,
**collection_options,
) as collection,
):
try:
await collection.ensure_collection_deleted()
except Exception as exc:
logger.warning(f"Failed to delete collection: {exc}")
try:
await collection.ensure_collection_exists()
except Exception as exc:
pytest.fail(f"Failed to create collection: {exc}")
# Upsert record
await collection.upsert(record_type([record]) if record_type is pd.DataFrame else record_type(**record))
# Get record
result = await collection.get(record["id"])
assert result is not None
# Delete record
await collection.delete(record["id"])
# Get record again, expect None
result = await collection.get(record["id"])
assert result is None
try:
await collection.ensure_collection_deleted()
except Exception as exc:
pytest.fail(f"Failed to delete collection: {exc}")
except MemoryConnectorConnectionException as exc:
pytest.xfail(f"Failed to connect to store: {exc}")
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Callable
import pytest
from semantic_kernel.data.vector import VectorStore
def get_redis_store():
from semantic_kernel.connectors.redis import RedisStore
return RedisStore()
def get_azure_ai_search_store():
from semantic_kernel.connectors.azure_ai_search import AzureAISearchStore
return AzureAISearchStore()
def get_qdrant_store():
from semantic_kernel.connectors.qdrant import QdrantStore
return QdrantStore()
def get_qdrant_store_in_memory():
from semantic_kernel.connectors.qdrant import QdrantStore
return QdrantStore(location=":memory:")
def get_weaviate_store():
from semantic_kernel.connectors.weaviate import WeaviateStore
return WeaviateStore(local_host="localhost")
def get_azure_cosmos_db_no_sql_store():
from semantic_kernel.connectors.azure_cosmos_db import CosmosNoSqlStore
return CosmosNoSqlStore(database_name="test_database", create_database=True)
def get_chroma_store():
from semantic_kernel.connectors.chroma import ChromaStore
return ChromaStore()
class VectorStoreTestBase:
@pytest.fixture
def stores(self) -> dict[str, Callable[[], VectorStore]]:
"""Return a dictionary of vector stores to test."""
return {
"redis": get_redis_store,
"azure_ai_search": get_azure_ai_search_store,
"qdrant": get_qdrant_store,
"qdrant_in_memory": get_qdrant_store_in_memory,
"weaviate_local": get_weaviate_store,
"azure_cosmos_db_no_sql": get_azure_cosmos_db_no_sql_store,
"chroma": get_chroma_store,
}
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase
from semantic_kernel.contents import AudioContent
from tests.integration.text_to_audio.text_to_audio_test_base import TextToAudioTestBase, azure_setup
pytestmark = pytest.mark.parametrize(
"service_id, text",
[
pytest.param(
"openai",
"Hello World!",
id="openai",
),
pytest.param(
"azure_openai",
"Hello World!",
marks=pytest.mark.skipif(not azure_setup, reason="Azure Audio to Text not setup."),
id="azure_openai",
),
],
)
class TestTextToAudio(TextToAudioTestBase):
"""Test text-to-audio services."""
async def test_audio_to_text(
self,
services: dict[str, TextToAudioClientBase],
service_id: str,
text: str,
) -> None:
"""Test text-to-audio services.
Args:
services: text-to-audio services.
service_id: Service ID.
text: Text content.
"""
service = services[service_id]
result = await service.get_audio_content(text)
assert isinstance(result, AudioContent)
assert result.data is not None
@@ -0,0 +1,30 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai import AzureTextToAudio, OpenAITextToAudio
from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase
from tests.utils import is_service_setup_for_testing
# TTS model on Azure model is not available in regions at which we have chat completion models.
# Therefore, we need to use a different endpoint for testing.
azure_setup = is_service_setup_for_testing(["AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT"])
class TextToAudioTestBase:
"""Base class for testing text-to-audio services."""
@pytest.fixture(scope="module")
def services(self) -> dict[str, TextToAudioClientBase]:
"""Return text-to-audio services."""
return {
"openai": OpenAITextToAudio(),
"azure_openai": AzureTextToAudio(
endpoint=os.environ["AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT"], credential=AzureCliCredential()
)
if azure_setup
else None,
}
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
from tests.integration.text_to_image.text_to_image_test_base import TextToImageTestBase
pytestmark = pytest.mark.parametrize(
"service_id, prompt",
[
pytest.param(
"openai",
"A cute tuxedo cat driving a race car.",
id="openai",
),
pytest.param(
"azure_openai",
"A cute tuxedo cat driving a race car.",
id="azure_openai",
marks=[
pytest.mark.xfail(
reason="Temporary failure due to Internal Server Error (500) from Azure OpenAI.",
),
],
),
],
)
class TestTextToImage(TextToImageTestBase):
"""Test text-to-image services."""
async def test_text_to_image(
self,
services: dict[str, TextToImageClientBase],
service_id: str,
prompt: str,
):
service = services[service_id]
image_url = await service.generate_image(prompt, 1024, 1024)
assert image_url
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai.services.azure_text_to_image import AzureTextToImage
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_to_image import OpenAITextToImage
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
class TextToImageTestBase:
"""Base class for testing text-to-image services."""
@pytest.fixture(scope="module")
def services(self) -> dict[str, TextToImageClientBase]:
"""Return text-to-image services."""
return {
"openai": OpenAITextToImage(),
"azure_openai": AzureTextToImage(credential=AzureCliCredential()),
}