chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
+272
@@ -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()
|
||||
+417
@@ -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
|
||||
+420
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
+347
@@ -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
|
||||
+10
@@ -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"
|
||||
}
|
||||
+14
@@ -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"
|
||||
}
|
||||
+10
@@ -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()),
|
||||
}
|
||||
Reference in New Issue
Block a user