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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

@@ -0,0 +1,26 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class TestNativeEchoBotPlugin:
"""Description: Test Native Plugin for testing purposes"""
@kernel_function(
description="Echo for input text",
name="echoAsync",
)
async def echo(self, text: Annotated[str, "The text to echo"]) -> str:
"""Echo for input text
Example:
"hello world" => "hello world"
Args:
text -- The text to echo
Returns:
input text
"""
return text
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class TestNativeEchoBotPlugin:
"""Description: Test Native Plugin for testing purposes"""
def __init__(self, static_input: str | None = None):
self.static_input = static_input or ""
@kernel_function(
description="Echo for input text with static",
name="echo",
)
def echo(self, text: Annotated[str, "The text to echo"]) -> str:
"""Echo for input text with a static input
Example:
"hello world" => "hello world"
Args:
text -- The text to echo
Returns:
input text
"""
return self.static_input + text
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
@kernel_function(
description="Echo for input text",
name="echoAsync",
)
async def echo(text: Annotated[str, "The text to echo"]) -> str:
"""Echo for input text
Example:
"hello world" => "hello world"
Args:
text -- The text to echo
Returns:
input text
"""
return text
@@ -0,0 +1,12 @@
name: TestFunction
template_format: semantic-kernel
template: |
{{$input}}
description: A test function from a yaml file.
execution_settings:
default:
temperature: 0.6
max_tokens: 123
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 2.0
@@ -0,0 +1,12 @@
name: TestFunctionHandlebars
template_format: handlebars
template: |
{{#each chat_history}}{{#message role=role}}{{~content~}}{{/message}}{{/each}}
description: A test function from a yaml file.
execution_settings:
default:
temperature: 0.6
max_tokens: 123
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 2.0
@@ -0,0 +1,12 @@
name: TestFunctionJinja2
template_format: jinja2
template: |
Repeat {% for item in chat_history %}{{ message(item) }}{% endfor %}
description: A test function from a yaml file.
execution_settings:
default:
temperature: 0.6
max_tokens: 123
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 2.0
@@ -0,0 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.prompt()
def echo_prompt(message: str) -> str:
"""Create an echo prompt"""
return f"Please process this message: {message}"
if __name__ == "__main__":
# Initialize and run the server
mcp.run(transport="stdio")
@@ -0,0 +1,13 @@
{
"schema": 1,
"description": "Test Description",
"execution_settings": {
"default": {
"max_tokens": 123,
"temperature": 0.0,
"top_p": 1.0,
"presence_penalty": 0.0,
"frequency_penalty": 2.0
}
}
}
@@ -0,0 +1,5 @@
{{$input}}
==
Test prompt.
==
@@ -0,0 +1,26 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class TestNativeEchoBotPlugin:
"""Description: Test Native Plugin for testing purposes"""
@kernel_function(
description="Echo for input text",
name="echoAsync",
)
async def echo(self, text: Annotated[str, "The text to echo"]) -> str:
"""Echo for input text
Example:
"hello world" => "hello world"
Args:
text -- The text to echo
Returns:
input text
"""
return text
@@ -0,0 +1,12 @@
name: TestFunctionYaml
template_format: semantic-kernel
template: |
{{$input}}
description: A test function from a yaml file.
execution_settings:
default:
temperature: 0.6
max_tokens: 123
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 2.0
@@ -0,0 +1,20 @@
{
"schema_version": "v1",
"name_for_model": "AzureKeyVault",
"name_for_human": "AzureKeyVault",
"description_for_model": "An Azure Key Vault plugin for interacting with secrets.",
"description_for_human": "An Azure Key Vault plugin for interacting with secrets.",
"auth": {
"type": "oauth",
"scope": "https://vault.azure.net/.default",
"authorization_url": "https://login.microsoftonline.com/e80e3e25-bb8d-4b4d-ab3f-b91669dd8ae4/oauth2/v2.0/token",
"authorization_content_type": "application/x-www-form-urlencoded"
},
"api": {
"type": "openapi",
"url": "file:///./tests/assets/test_plugins/TestPlugin/TestOpenAPIPlugin/akv-openapi.yaml"
},
"logo_url": "",
"contact_email": "",
"legal_info_url": ""
}
@@ -0,0 +1,133 @@
openapi: 3.1.0
info:
title: Azure Key Vault [Sample]
description: "A sample connector for the Azure Key Vault service. This connector is built for the Azure Key Vault REST API. You can see the details of the API here: https://docs.microsoft.com/rest/api/keyvault/."
version: "1.0"
servers:
- url: https://my-key-vault.vault.azure.net/
paths:
/secrets/{secret-name}:
get:
summary: Get secret
description: "Get a specified secret from a given key vault. For details, see: https://learn.microsoft.com/en-us/rest/api/keyvault/secrets/get-secret/get-secret."
operationId: GetSecret
parameters:
- name: secret-name
in: path
required: true
schema:
type: string
- name: api-version
in: query
required: true
schema:
type: string
default: "7.0"
x-ms-visibility: internal
responses:
'200':
description: default
content:
application/json:
schema:
type: object
properties:
attributes:
type: object
properties:
created:
type: integer
format: int32
description: created
enabled:
type: boolean
description: enabled
recoverylevel:
type: string
description: recoverylevel
updated:
type: integer
format: int32
description: updated
id:
type: string
description: id
value:
type: string
format: byte
description: value
put:
summary: Create or update secret value
description: "Sets a secret in a specified key vault. This operation adds a secret to the Azure Key Vault. If the named secret already exists, Azure Key Vault creates a new version of that secret. This operation requires the secrets/set permission. For details, see: https://learn.microsoft.com/en-us/rest/api/keyvault/secrets/set-secret/set-secret."
operationId: SetSecret
parameters:
- name: secret-name
in: path
required: true
schema:
type: string
- name: api-version
in: query
required: true
schema:
type: string
default: "7.0"
x-ms-visibility: internal
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
attributes:
type: object
properties:
enabled:
type: boolean
description: Determines whether the object is enabled.
value:
type: string
description: The value of the secret.
required:
- value
responses:
'200':
description: default
content:
application/json:
schema:
type: object
properties:
attributes:
type: object
properties:
created:
type: integer
format: int32
description: created
enabled:
type: boolean
description: enabled
recoverylevel:
type: string
description: recoverylevel
updated:
type: integer
format: int32
description: updated
id:
type: string
description: id
value:
type: string
description: value
components:
securitySchemes:
oauth2_auth:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://login.windows.net/common/oauth2/authorize
tokenUrl: https://login.windows.net/common/oauth2/token
scopes: {}
@@ -0,0 +1,13 @@
{
"schema": 1,
"description": "Test Description",
"execution_settings": {
"default": {
"max_tokens": 123,
"temperature": 0.0,
"top_p": 1.0,
"presence_penalty": 0.0,
"frequency_penalty": 2.0
}
}
}
@@ -0,0 +1,5 @@
{{$input}}
==
Test prompt.
==
@@ -0,0 +1,13 @@
{
"schema": 1,
"description": "Test Description",
"execution_settings": {
"default": {
"max_tokens": 123,
"temperature": 0.0,
"top_p": 1.0,
"presence_penalty": 0.0,
"frequency_penalty": 2.0
}
}
}
@@ -0,0 +1,14 @@
{
"schema": 1,
"description": "Test Description",
"template_format": "handlebars",
"execution_settings": {
"default": {
"max_tokens": 123,
"temperature": 0.0,
"top_p": 1.0,
"presence_penalty": 0.0,
"frequency_penalty": 2.0
}
}
}
@@ -0,0 +1,5 @@
{{input}}
==
Test prompt.
==
@@ -0,0 +1,5 @@
{{$input}}
==
Test prompt.
==
@@ -0,0 +1,8 @@
type: chat_completion_agent
name: FunctionCallingAgent
description: This agent uses the provided functions to answer questions about the menu.
instructions: Use the provided functions to answer questions about the menu.
model:
id: "gpt-4.1"
options:
temperature: 0.4
+459
View File
@@ -0,0 +1,459 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Annotated, Any
from unittest.mock import MagicMock
from uuid import uuid4
from pydantic import BaseModel
from pytest import fixture
from semantic_kernel.agents import Agent, DeclarativeSpecMixin, register_agent_type
from semantic_kernel.data.vector import VectorStoreCollectionDefinition, VectorStoreField, vectorstoremodel
if TYPE_CHECKING:
from semantic_kernel import Kernel
from semantic_kernel.contents import ChatHistory
from semantic_kernel.filters import FunctionInvocationContext
from semantic_kernel.functions import KernelFunction
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
def pytest_configure(config):
logging.basicConfig(level=logging.ERROR)
logging.getLogger("tests.utils").setLevel(logging.INFO)
logging.getLogger("openai").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("semantic_kernel").setLevel(logging.INFO)
# region: Kernel fixtures
@fixture(scope="function")
def kernel() -> "Kernel":
from semantic_kernel import Kernel
return Kernel()
@fixture(scope="session")
def service() -> "AIServiceClientBase":
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
return AIServiceClientBase(service_id="service", ai_model_id="ai_model_id")
@fixture(scope="session")
def default_service() -> "AIServiceClientBase":
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
return AIServiceClientBase(service_id="default", ai_model_id="ai_model_id")
@fixture(scope="function")
def kernel_with_service(kernel: "Kernel", service: "AIServiceClientBase") -> "Kernel":
kernel.add_service(service)
return kernel
@fixture(scope="function")
def kernel_with_default_service(kernel: "Kernel", default_service: "AIServiceClientBase") -> "Kernel":
kernel.add_service(default_service)
return kernel
@fixture(scope="session")
def not_decorated_native_function() -> Callable:
def not_decorated_native_function(arg1: str) -> str:
return "test"
return not_decorated_native_function
@fixture(scope="session")
def decorated_native_function() -> Callable:
from semantic_kernel.functions.kernel_function_decorator import kernel_function
@kernel_function(name="getLightStatus")
def decorated_native_function(arg1: str) -> str:
return "test"
return decorated_native_function
@fixture(scope="session")
def custom_plugin_class():
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class CustomPlugin:
@kernel_function(name="getLightStatus")
def decorated_native_function(self) -> str:
return "test"
return CustomPlugin
@fixture(scope="session")
def experimental_plugin_class():
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class ExperimentalPlugin:
@kernel_function(name="getLightStatus")
def decorated_native_function(self) -> str:
return "test"
return ExperimentalPlugin
@fixture(scope="session")
def auto_function_invocation_filter() -> Callable:
"""A filter that will be called for each function call in the response."""
from semantic_kernel.filters import AutoFunctionInvocationContext
async def auto_function_invocation_filter(context: AutoFunctionInvocationContext, next):
await next(context)
context.terminate = True
return auto_function_invocation_filter
@fixture(scope="session")
def create_mock_function() -> Callable:
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
async def stream_func(*args, **kwargs):
yield [StreamingTextContent(choice_index=0, text="test", metadata={})]
def create_mock_function(name: str, value: str = "test") -> "KernelFunction":
kernel_function_metadata = KernelFunctionMetadata(
name=name,
plugin_name="TestPlugin",
description="test description",
parameters=[],
is_prompt=True,
is_asynchronous=True,
)
class CustomKernelFunction(KernelFunction):
call_count: int = 0
async def _invoke_internal_stream(
self,
context: "FunctionInvocationContext",
) -> None:
self.call_count += 1
context.result = FunctionResult(
function=kernel_function_metadata,
value=stream_func(),
)
async def _invoke_internal(self, context: "FunctionInvocationContext"):
self.call_count += 1
context.result = FunctionResult(function=kernel_function_metadata, value=value, metadata={})
return CustomKernelFunction(metadata=kernel_function_metadata)
return create_mock_function
@fixture(scope="function")
def get_tool_call_mock():
from semantic_kernel.contents.function_call_content import FunctionCallContent
tool_call_mock = MagicMock(spec=FunctionCallContent)
tool_call_mock.split_name_dict.return_value = {"arg_name": "arg_value"}
tool_call_mock.to_kernel_arguments.return_value = {"arg_name": "arg_value"}
tool_call_mock.name = "test-function"
tool_call_mock.function_name = "function"
tool_call_mock.plugin_name = "test"
tool_call_mock.arguments = {"arg_name": "arg_value"}
tool_call_mock.ai_model_id = None
tool_call_mock.metadata = {}
tool_call_mock.index = 0
tool_call_mock.parse_arguments.return_value = {"arg_name": "arg_value"}
tool_call_mock.id = "test_id"
return tool_call_mock
@fixture(scope="function")
def chat_history() -> "ChatHistory":
from semantic_kernel.contents.chat_history import ChatHistory
return ChatHistory()
@fixture(scope="function")
def prompt() -> str:
return "test prompt"
# region: Connector Settings fixtures
@fixture
def exclude_list(request):
"""Fixture that returns a list of environment variables to exclude."""
return request.param if hasattr(request, "param") else []
@fixture
def override_env_param_dict(request):
"""Fixture that returns a dict of environment variables to override."""
return request.param if hasattr(request, "param") else {}
# These two fixtures are used for multiple things, also non-connector tests
@fixture()
def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for AzureOpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment",
"AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment",
"AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME": "test_text_to_image_deployment",
"AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME": "test_audio_to_text_deployment",
"AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME": "test_text_to_audio_deployment",
"AZURE_OPENAI_REALTIME_DEPLOYMENT_NAME": "test_realtime_deployment",
"AZURE_OPENAI_API_KEY": "test_api_key",
"AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com",
"AZURE_OPENAI_API_VERSION": "2023-03-15-preview",
"AZURE_OPENAI_BASE_URL": "https://test_text_deployment.test-base-url.com",
"AZURE_OPENAI_TOKEN_ENDPOINT": "https://test-token-endpoint.com",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@fixture()
def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for OpenAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"OPENAI_API_KEY": "test_api_key",
"OPENAI_ORG_ID": "test_org_id",
"OPENAI_RESPONSES_MODEL_ID": "test_responses_model_id",
"OPENAI_CHAT_MODEL_ID": "test_chat_model_id",
"OPENAI_TEXT_MODEL_ID": "test_text_model_id",
"OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id",
"OPENAI_TEXT_TO_IMAGE_MODEL_ID": "test_text_to_image_model_id",
"OPENAI_AUDIO_TO_TEXT_MODEL_ID": "test_audio_to_text_model_id",
"OPENAI_TEXT_TO_AUDIO_MODEL_ID": "test_text_to_audio_model_id",
"OPENAI_REALTIME_MODEL_ID": "test_realtime_model_id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
# region: Data Model Fixtures
# some of these fixtures are used in both unit and integration tests
@fixture
def index_kind(request) -> str:
if hasattr(request, "param"):
return request.param
return "hnsw"
@fixture
def distance_function(request) -> str:
if hasattr(request, "param"):
return request.param
return "cosine_similarity"
@fixture
def vector_property_type(request) -> str:
if hasattr(request, "param"):
return request.param
return "float"
@fixture
def dimensions(request) -> int:
if hasattr(request, "param"):
return request.param
return 5
@fixture
def dataclass_vector_data_model(
index_kind: str, distance_function: str, vector_property_type: str, dimensions: int
) -> object:
@vectorstoremodel
@dataclass
class MyDataModel:
vector: Annotated[
list[float] | str | None,
VectorStoreField(
"vector",
index_kind=index_kind,
dimensions=dimensions,
distance_function=distance_function,
type=vector_property_type,
),
] = None
id: Annotated[str, VectorStoreField("key", type="str")] = field(default_factory=lambda: str(uuid4()))
content: Annotated[str, VectorStoreField("data", type="str")] = "content1"
return MyDataModel
@fixture
def definition(
index_kind: str, distance_function: str, vector_property_type: str, dimensions: int
) -> VectorStoreCollectionDefinition:
return VectorStoreCollectionDefinition(
fields=[
VectorStoreField("key", name="id", type="str"),
VectorStoreField("data", name="content", type="str", is_full_text_indexed=True),
VectorStoreField(
"vector",
name="vector",
dimensions=dimensions,
index_kind=index_kind,
distance_function=distance_function,
type=vector_property_type,
),
]
)
@fixture
def definition_pandas(index_kind: str, distance_function: str, vector_property_type: str, dimensions: int) -> object:
import pandas as pd
return VectorStoreCollectionDefinition(
fields=[
VectorStoreField(
"vector",
name="vector",
index_kind=index_kind,
dimensions=dimensions,
distance_function=distance_function,
type=vector_property_type,
),
VectorStoreField("key", name="id"),
VectorStoreField("data", name="content", type="str"),
],
container_mode=True,
to_dict=lambda x: x.to_dict(orient="records"),
from_dict=lambda x, **_: pd.DataFrame(x),
)
@fixture
def record_type(index_kind: str, distance_function: str, vector_property_type: str, dimensions: int) -> object:
@vectorstoremodel
class DataModelClass(BaseModel):
content: Annotated[str, VectorStoreField("data")]
vector: Annotated[
list[float] | str | None,
VectorStoreField(
"vector",
type=vector_property_type,
dimensions=dimensions,
index_kind=index_kind,
distance_function=distance_function,
),
] = None
id: Annotated[str, VectorStoreField("key")]
def model_post_init(self, context: Any) -> None:
if self.vector is None:
self.vector = self.content
return DataModelClass
@fixture
def record_type_with_key_as_key_field(
index_kind: str, distance_function: str, vector_property_type: str, dimensions: int
) -> object:
"""Data model type with key as key field."""
@vectorstoremodel
class DataModelClass(BaseModel):
content: Annotated[str, VectorStoreField("data")]
vector: Annotated[
str | list[float] | None,
VectorStoreField(
"vector",
index_kind=index_kind,
distance_function=distance_function,
type=vector_property_type,
dimensions=dimensions,
),
]
key: Annotated[str, VectorStoreField("key")]
return DataModelClass
# region Declarative Spec
@register_agent_type("test_agent")
class TestAgent(DeclarativeSpecMixin, Agent):
@classmethod
def resolve_placeholders(cls, yaml_str, settings=None, extras=None):
return yaml_str
@classmethod
async def _from_dict(cls, data, **kwargs):
return cls(
name=data.get("name"),
description=data.get("description"),
instructions=data.get("instructions"),
kernel=data.get("kernel"),
)
async def get_response(self, messages, instructions_override=None):
return "test response"
async def invoke(self, messages, **kwargs):
return "invoke result"
async def invoke_stream(self, messages, **kwargs):
yield "stream result"
@fixture(scope="session")
def test_agent_cls():
return TestAgent
# endregion
@@ -0,0 +1,152 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import AsyncIterator, Awaitable, Callable
from typing import Any, Generic, Protocol, TypeVar
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
from semantic_kernel.contents import ChatMessageContent
DEFAULT_MAX_ATTEMPTS = 3
DEFAULT_BACKOFF_SECONDS = 1
class ChatResponseProtocol(Protocol):
"""Represents a single response item returned by the agent."""
@property
def message(self) -> ChatMessageContent: ...
@property
def thread(self) -> AgentThread | None: ...
class ChatAgentProtocol(Protocol):
"""Protocol describing the common agent interface used by the tests."""
async def get_response(
self, messages: str | list[str] | None, thread: object | None = None
) -> ChatResponseProtocol: ...
def invoke(
self, messages: str | list[str] | None, thread: object | None = None
) -> AsyncIterator[ChatResponseProtocol]: ...
def invoke_stream(
self, messages: str | list[str] | None, thread: object | None = None
) -> AsyncIterator[ChatResponseProtocol]: ...
TAgent = TypeVar("TAgent", bound=ChatAgentProtocol)
async def run_with_retry(
coro: Callable[..., Awaitable[Any]],
*args,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
"""
Execute an async callable with retry/backoff logic.
Args:
coro: The async function to call
args: Positional args to pass to the function
attempts: How many times to attempt before giving up
backoff_seconds: The initial backoff in seconds, doubled after each failure
kwargs: Keyword args to pass to the function
Returns:
Whatever the async function returns
Raises:
Exception: If the function fails after the specified number of attempts
"""
delay = backoff_seconds
for attempt in range(1, attempts + 1):
try:
return await coro(*args, **kwargs)
except Exception:
if attempt == attempts:
raise
await asyncio.sleep(delay)
delay *= 2
raise RuntimeError("Unexpected error: run_with_retry exit.")
class AgentTestBase(Generic[TAgent]):
"""Common test base that wraps all agent invocation patterns with retry logic.
Each integration test can inherit from this or use its methods directly.
"""
async def get_response_with_retry(
self,
agent: Agent,
messages: str | list[str] | None,
thread: Any | None = None,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
) -> AgentResponseItem[ChatMessageContent]:
"""Wraps agent.get_response(...) in run_with_retry."""
return await run_with_retry(
agent.get_response, messages=messages, thread=thread, attempts=attempts, backoff_seconds=backoff_seconds
)
async def get_invoke_with_retry(
self,
agent: Any,
messages: str | list[str] | None,
thread: Any | None = None,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
) -> list[AgentResponseItem[ChatMessageContent]]:
"""Wraps agent.invoke(...) in run_with_retry.
Collects generator results in a list before returning them.
"""
return await run_with_retry(
self._collect_from_invoke,
agent,
messages,
thread=thread,
attempts=attempts,
backoff_seconds=backoff_seconds,
)
async def get_invoke_stream_with_retry(
self,
agent: Any,
messages: str | list[str] | None,
thread: Any | None = None,
attempts: int = DEFAULT_MAX_ATTEMPTS,
backoff_seconds: float = DEFAULT_BACKOFF_SECONDS,
) -> list[AgentResponseItem[ChatMessageContent]]:
"""Wraps agent.invoke_stream(...) in run_with_retry.
Collects streaming results in a list before returning them."""
return await run_with_retry(
self._collect_from_invoke_stream,
agent,
messages,
thread=thread,
attempts=attempts,
backoff_seconds=backoff_seconds,
)
async def _collect_from_invoke(
self, agent: Agent, messages: str | list[str] | None, thread: Any | None = None
) -> list[AgentResponseItem[ChatMessageContent]]:
results: list[AgentResponseItem[ChatMessageContent]] = []
async for response in agent.invoke(messages=messages, thread=thread):
results.append(response)
return results
async def _collect_from_invoke_stream(
self, agent: Agent, messages: str | list[str] | None, thread: Any | None = None
) -> list[AgentResponseItem[ChatMessageContent]]:
results: list[AgentResponseItem[ChatMessageContent]] = []
async for response in agent.invoke_stream(messages=messages, thread=thread):
results.append(response)
return results
@@ -0,0 +1,278 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from azure.ai.agents.models import CodeInterpreterTool, FileInfo, FileSearchTool
from azure.identity.aio import AzureCliCredential
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class TestAzureAIAgentIntegration:
@pytest.fixture
async def azureai_agent(self, request):
ai_agent_settings = AzureAIAgentSettings()
async with (
AzureCliCredential() as creds,
AzureAIAgent.create_client(credential=creds) as client,
):
tools, tool_resources, plugins = [], {}, []
params = getattr(request, "param", {})
if params.get("enable_code_interpreter"):
ci_tool = CodeInterpreterTool()
tools.extend(ci_tool.definitions)
tool_resources.update(ci_tool.resources)
if params.get("enable_file_search"):
pdf_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
"resources",
"employees.pdf",
)
file: FileInfo = await client.agents.files.upload_and_poll(
file_path=pdf_file_path, purpose="assistants"
)
vector_store = await client.agents.vector_stores.create_and_poll(
file_ids=[file.id], name="my_vectorstore"
)
fs_tool = FileSearchTool(vector_store_ids=[vector_store.id])
tools.extend(fs_tool.definitions)
tool_resources.update(fs_tool.resources)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
agent_definition = await client.agents.create_agent(
model=ai_agent_settings.model_deployment_name,
tools=tools,
tool_resources=tool_resources,
name="SKPythonIntegrationTestAgent",
instructions="You are a helpful assistant that help users with their questions.",
)
azureai_agent = AzureAIAgent(
client=client,
definition=agent_definition,
plugins=plugins,
)
yield azureai_agent # yield agent for test method to use
# cleanup
await azureai_agent.client.agents.delete_agent(azureai_agent.id)
async def test_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(azureai_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
assert "thread_id" in response.message.metadata
assert "run_id" in response.message.metadata
async def test_get_response_with_thread(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
azureai_agent, messages=user_message, thread=thread
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
async def test_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
async def test_invoke_with_thread(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages=user_message, thread=thread)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
async def test_invoke_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(azureai_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_invoke_stream_with_thread(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
azureai_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = """
Using Python, sum the number of animals for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
response = await agent_test_base.get_response_with_retry(azureai_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = """
Using Python, sum the number of animals for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_invoke_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter streaming."""
input_text = """
Using Python, sum the number of animals for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_stream_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("azureai_agent", [{"enable_file_search": True}], indirect=True)
async def test_file_search_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
response = await agent_test_base.get_response_with_retry(azureai_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize("azureai_agent", [{"enable_file_search": True}], indirect=True)
async def test_file_search_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize("azureai_agent", [{"enable_file_search": True}], indirect=True)
async def test_file_search_invoke_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test code interpreter streaming."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_stream_with_retry(azureai_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize("azureai_agent", [{"enable_kernel_function": True}], indirect=True)
async def test_function_calling_get_response(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
azureai_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize("azureai_agent", [{"enable_kernel_function": True}], indirect=True)
async def test_function_calling_invoke(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
azureai_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize("azureai_agent", [{"enable_kernel_function": True}], indirect=True)
async def test_function_calling_stream(self, azureai_agent: AzureAIAgent, agent_test_base: AgentTestBase):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
azureai_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
import pytest
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
class WeatherPlugin:
"""Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
@pytest.fixture
def kernel_with_dummy_function() -> Kernel:
kernel = Kernel()
kernel.add_plugin(WeatherPlugin(), plugin_name="weather")
return kernel
@@ -0,0 +1,141 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
import pytest
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent
from semantic_kernel.contents.binary_content import BinaryContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
class TestBedrockAgentIntegration:
@pytest.fixture(autouse=True)
async def setup_and_teardown(self, request):
"""Setup and teardown for the test.
This is run for each test function, i.e. each test function will have its own instance of the agent.
"""
try:
self.bedrock_agent = await BedrockAgent.create_and_prepare_agent(
f"semantic-kernel-integration-test-agent-{uuid.uuid4()}",
"You are a helpful assistant that help users with their questions.",
)
if hasattr(request, "param"):
if "enable_code_interpreter" in request.param:
await self.bedrock_agent.create_code_interpreter_action_group()
if "kernel" in request.param:
self.bedrock_agent.kernel = request.getfixturevalue(request.param.get("kernel"))
if "enable_kernel_function" in request.param:
await self.bedrock_agent.create_kernel_function_action_group()
except Exception as e:
pytest.fail("Failed to create agent")
raise e
# Yield control to the test
yield
# Clean up
try:
await self.bedrock_agent.delete_agent()
except Exception as e:
pytest.fail(f"Failed to delete agent: {e}")
raise e
@pytest.mark.asyncio
async def test_invoke(self):
"""Test invoke of the agent."""
async for response in self.bedrock_agent.invoke(messages="Hello"):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.asyncio
async def test_invoke_stream(self):
"""Test invoke stream of the agent."""
async for response in self.bedrock_agent.invoke_stream(messages="Hello"):
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.asyncio
@pytest.mark.parametrize("setup_and_teardown", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter(self):
"""Test code interpreter."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
binary_item: BinaryContent | None = None
async for response in self.bedrock_agent.invoke(messages=input_text):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
if not binary_item:
binary_item = next((item for item in response.message.items if isinstance(item, BinaryContent)), None)
assert binary_item
@pytest.mark.asyncio
@pytest.mark.parametrize("setup_and_teardown", [{"enable_code_interpreter": True}], indirect=True)
async def test_code_interpreter_stream(self):
"""Test code interpreter streaming."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
binary_item: BinaryContent | None = None
async for response in self.bedrock_agent.invoke_stream(messages=input_text):
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
binary_item = next((item for item in response.message.items if isinstance(item, BinaryContent)), None)
assert binary_item
@pytest.mark.asyncio
@pytest.mark.parametrize(
"setup_and_teardown",
[
{
"enable_kernel_function": True,
"kernel": "kernel_with_dummy_function",
},
],
indirect=True,
)
async def test_function_calling(self):
"""Test function calling."""
async for response in self.bedrock_agent.invoke(
messages="What is the weather in Seattle?",
):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.asyncio
@pytest.mark.parametrize(
"setup_and_teardown",
[
{
"enable_kernel_function": True,
"kernel": "kernel_with_dummy_function",
},
],
indirect=True,
)
async def test_function_calling_stream(self):
"""Test function calling streaming."""
full_message: str = ""
async for response in self.bedrock_agent.invoke_stream(
messages="What is the weather in Seattle?",
):
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
@@ -0,0 +1,272 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""A sample Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class TestChatCompletionAgentIntegration:
@pytest.fixture(params=["azure", "openai"])
async def chat_completion_agent(self, request):
raw_param = request.param
if isinstance(raw_param, str):
agent_service, params = raw_param, {}
elif isinstance(raw_param, tuple) and len(raw_param) == 2:
agent_service, params = raw_param
else:
raise ValueError(f"Unsupported param format: {raw_param}")
plugins = []
service = (
AzureChatCompletion(credential=AzureCliCredential()) if agent_service == "azure" else OpenAIChatCompletion()
)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
agent = ChatCompletionAgent(
service=service,
name="SKPythonIntegrationTestChatCompletionAgent",
instructions="You are a helpful assistant that help users with their questions.",
plugins=plugins,
)
yield agent # yield agent for test method to use
# region Simple 'Hello' messages tests
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(chat_completion_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response_with_thread(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
chat_completion_agent,
messages=user_message,
thread=thread,
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
responses = await agent_test_base.get_invoke_with_retry(chat_completion_agent, messages="Hello")
assert len(responses) > 0
usage: CompletionUsage = CompletionUsage()
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
if response.metadata.get("usage"):
usage += response.metadata["usage"]
assert usage.prompt_tokens > 0
assert usage.completion_tokens > 0
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_with_thread(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(
chat_completion_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream(self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(chat_completion_agent, messages="Hello")
assert len(responses) > 0
usage: CompletionUsage = CompletionUsage()
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
if response.metadata.get("usage"):
usage += response.metadata["usage"]
assert usage.prompt_tokens > 0
assert usage.completion_tokens > 0
@pytest.mark.parametrize("chat_completion_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream_with_thread(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
chat_completion_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
# endregion
# region Function calling tests
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["chat_completion_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_get_response(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
chat_completion_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["chat_completion_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_invoke(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
chat_completion_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["chat_completion_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_stream(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
chat_completion_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
# endregion
# region Image Content tests
@pytest.mark.parametrize(
"chat_completion_agent",
[
("azure", {"enable_kernel_function": True}),
pytest.param(
("openai", {"enable_kernel_function": True}),
marks=pytest.mark.xfail(reason="OpenAI service raise error for downloading image from URL"),
),
],
indirect=["chat_completion_agent"],
ids=["azure-image-content-streaming", "openai-image-content-streaming"],
)
async def test_image_content_stream(
self, chat_completion_agent: ChatCompletionAgent, agent_test_base: AgentTestBase
):
"""Test function calling streaming."""
IMAGE_URI = (
"https://raw.githubusercontent.com/microsoft/semantic-kernel/main/python/tests/assets/sample_image.jpg"
)
image_content_remote = ImageContent(uri=IMAGE_URI)
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
chat_completion_agent,
messages=ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="What is in this image?"), image_content_remote],
),
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert full_message is not None
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from tests.integration.agents.agent_test_base import AgentTestBase, ChatAgentProtocol
@pytest.fixture
def agent_test_base() -> AgentTestBase[ChatAgentProtocol]:
"""Provides a single AgentTestBase instance that all tests can use.
Typed as a Generic over any ChatAgentProtocol.
"""
return AgentTestBase()
@@ -0,0 +1,417 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.agents import AzureAssistantAgent, OpenAIAssistantAgent
from semantic_kernel.connectors.ai.open_ai import AzureOpenAISettings, OpenAISettings
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""A sample Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class TestOpenAIAssistantAgentIntegration:
@pytest.fixture(params=["azure", "openai"])
async def assistant_agent(self, request):
raw_param = request.param
if isinstance(raw_param, str):
agent_type, params = raw_param, {}
elif isinstance(raw_param, tuple) and len(raw_param) == 2:
agent_type, params = raw_param
else:
raise ValueError(f"Unsupported param format: {raw_param}")
tools, tool_resources, plugins = [], {}, []
if agent_type == "azure":
client = AzureAssistantAgent.create_client(credential=AzureCliCredential())
model = AzureOpenAISettings().chat_deployment_name
AgentClass = AzureAssistantAgent
else: # agent_type == "openai"
client = OpenAIAssistantAgent.create_client()
model = OpenAISettings().chat_model_id
AgentClass = OpenAIAssistantAgent
if params.get("enable_code_interpreter"):
code_interpreter_tool, code_interpreter_tool_resources = (
AzureAssistantAgent.configure_code_interpreter_tool()
if agent_type == "azure"
else OpenAIAssistantAgent.configure_code_interpreter_tool()
)
tools.extend(code_interpreter_tool)
tool_resources.update(code_interpreter_tool_resources)
if params.get("enable_file_search"):
pdf_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "employees.pdf"
)
with open(pdf_file_path, "rb") as file:
file = await client.files.create(file=file, purpose="assistants")
vector_store = await client.vector_stores.create(
name="assistant_file_search_int_tests",
file_ids=[file.id],
)
file_search_tool, file_search_tool_resources = (
AzureAssistantAgent.configure_file_search_tool(vector_store.id)
if agent_type == "azure"
else OpenAIAssistantAgent.configure_file_search_tool(vector_store.id)
)
tools.extend(file_search_tool)
tool_resources.update(file_search_tool_resources)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
definition = await client.beta.assistants.create(
model=model,
tools=tools,
tool_resources=tool_resources,
name="SKPythonIntegrationTestAssistantAgent",
instructions="You are a helpful assistant that help users with their questions.",
)
agent = AgentClass(
client=client,
definition=definition,
plugins=plugins,
)
yield agent # yield agent for test method to use
# cleanup
await client.beta.assistants.delete(agent.id)
# region Simple 'Hello' messages tests
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(assistant_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
assert "thread_id" in response.message.metadata
assert "run_id" in response.message.metadata
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response_with_thread(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
assistant_agent, messages=user_message, thread=thread
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
responses = await agent_test_base.get_invoke_with_retry(assistant_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_with_thread(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(
assistant_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(assistant_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("assistant_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream_with_thread(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
assistant_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
# endregion
# region Code interpreter tests
@pytest.mark.parametrize(
"assistant_agent",
[
pytest.param(
("azure", {"enable_code_interpreter": True}), marks=pytest.mark.xfail(reason="Service outage")
),
pytest.param(
("openai", {"enable_code_interpreter": True}),
),
],
indirect=["assistant_agent"],
ids=["azure-code-interpreter", "openai-code-interpreter"],
)
async def test_code_interpreter_get_response(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
response = await agent_test_base.get_response_with_retry(assistant_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize(
"assistant_agent",
[
pytest.param(
("azure", {"enable_code_interpreter": True}), marks=pytest.mark.xfail(reason="Service outage")
),
pytest.param(
("openai", {"enable_code_interpreter": True}),
),
],
indirect=["assistant_agent"],
ids=["azure-code-interpreter", "openai-code-interpreter"],
)
async def test_code_interpreter_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize(
"assistant_agent",
[
pytest.param(
("azure", {"enable_code_interpreter": True}), marks=pytest.mark.xfail(reason="Service outage")
),
pytest.param(
("openai", {"enable_code_interpreter": True}),
),
],
indirect=["assistant_agent"],
ids=["azure-code-interpreter", "openai-code-interpreter"],
)
async def test_code_interpreter_invoke_stream(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter streaming."""
input_text = """
Create a bar chart for the following data:
Panda 5
Tiger 8
Lion 3
Monkey 6
Dolphin 2
"""
responses = await agent_test_base.get_invoke_stream_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
# endregion
# region File search tests
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["assistant_agent"],
ids=["azure-file-search", "openai-file-search"],
)
async def test_file_search_get_response(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
response = await agent_test_base.get_response_with_retry(assistant_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["assistant_agent"],
ids=["azure-file-search", "openai-file-search"],
)
async def test_file_search_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["assistant_agent"],
ids=["azure-file-search", "openai-file-search"],
)
async def test_file_search_invoke_stream(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter streaming."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_stream_with_retry(assistant_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
# endregion
# region Function calling tests
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["assistant_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_get_response(
self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
assistant_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["assistant_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_invoke(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
assistant_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"assistant_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["assistant_agent"],
ids=["azure-function-calling", "openai-function-calling"],
)
async def test_function_calling_stream(self, assistant_agent: OpenAIAssistantAgent, agent_test_base: AgentTestBase):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
assistant_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
# endregion
@@ -0,0 +1,420 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import Annotated
import pytest
from azure.identity import AzureCliCredential
from pydantic import BaseModel
from semantic_kernel.agents import AzureResponsesAgent, OpenAIResponsesAgent
from semantic_kernel.connectors.ai.open_ai import AzureOpenAISettings, OpenAISettings
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.functions import kernel_function
from tests.integration.agents.agent_test_base import AgentTestBase
class WeatherPlugin:
"""A sample Mock weather plugin."""
@kernel_function(description="Get real-time weather information.")
def current_weather(self, location: Annotated[str, "The location to get the weather"]) -> str:
"""Returns the current weather."""
return f"The weather in {location} is sunny."
class Step(BaseModel):
explanation: str
output: str
class Reasoning(BaseModel):
steps: list[Step]
final_answer: str
class TestOpenAIResponsesAgentIntegration:
@pytest.fixture(params=["azure", "openai"])
async def responses_agent(self, request):
raw_param = request.param
if isinstance(raw_param, str):
agent_type, params = raw_param, {}
elif isinstance(raw_param, tuple) and len(raw_param) == 2:
agent_type, params = raw_param
else:
raise ValueError(f"Unsupported param format: {raw_param}")
tools, plugins, text = [], [], None
if agent_type == "azure":
client = AzureResponsesAgent.create_client(credential=AzureCliCredential())
model = AzureOpenAISettings().chat_deployment_name
AgentClass = AzureResponsesAgent
else: # agent_type == "openai"
client = OpenAIResponsesAgent.create_client()
model = OpenAISettings().chat_model_id
AgentClass = OpenAIResponsesAgent
if params.get("enable_web_search"):
web_search_tool = OpenAIResponsesAgent.configure_web_search_tool()
tools.append(web_search_tool)
if params.get("enable_structured_outputs"):
text = OpenAIResponsesAgent.configure_response_format(Reasoning)
if params.get("enable_file_search"):
pdf_file_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "employees.pdf"
)
with open(pdf_file_path, "rb") as file:
file = await client.files.create(file=file, purpose="assistants")
vector_store = await client.vector_stores.create(
name="responses_file_search_int_tests",
file_ids=[file.id],
)
file_search_tool = (
AzureResponsesAgent.configure_file_search_tool(vector_store.id)
if agent_type == "azure"
else OpenAIResponsesAgent.configure_file_search_tool(vector_store.id)
)
tools.append(file_search_tool)
if params.get("enable_kernel_function"):
plugins.append(WeatherPlugin())
agent = AgentClass(
ai_model_id=model,
client=client,
name="SKPythonIntegrationTestResponsesAgent",
instructions="You are a helpful agent that help users with their questions.",
plugins=plugins,
tools=tools,
text=text,
)
yield agent # yield agent for test method to use
# region Simple 'Hello' messages tests
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test get response of the agent."""
response = await agent_test_base.get_response_with_retry(responses_agent, messages="Hello")
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
assert "thread_id" in response.message.metadata
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_get_response_with_thread(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test get response of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
response = await agent_test_base.get_response_with_retry(
responses_agent, messages=user_message, thread=thread
)
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent."""
async for response in responses_agent.invoke(messages="Hello"):
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_with_thread(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test invoke of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_with_retry(
responses_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test invoke stream of the agent."""
responses = await agent_test_base.get_invoke_stream_with_retry(responses_agent, messages="Hello")
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
@pytest.mark.parametrize("responses_agent", ["azure", "openai"], indirect=True, ids=["azure", "openai"])
async def test_invoke_stream_with_thread(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test invoke stream of the agent with a thread."""
thread = None
user_messages = ["Hello, I am John Doe.", "What is my name?"]
for user_message in user_messages:
responses = await agent_test_base.get_invoke_stream_with_retry(
responses_agent, messages=user_message, thread=thread
)
assert len(responses) > 0
for response in responses:
thread = response.thread
assert thread is not None
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
await thread.delete() if thread else None
# endregion
# region Web Search tests
@pytest.mark.parametrize(
"responses_agent",
[
# Azure OpenAI Responses API doesn't yet support the web search tool
("openai", {"enable_web_search": True}),
],
indirect=["responses_agent"],
ids=["openai-web-search-get-response"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when using the web search tool.")
async def test_web_search_get_response(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Find articles about the latest AI trends."
response = await responses_agent.get_response(messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content is not None
# endregion
# region File search tests
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["responses_agent"],
ids=["azure-file-search-get-response", "openai-file-search-get-response"],
)
@pytest.mark.xfail(reason="The Responses API is unstable and is throwing 500s.")
async def test_file_search_get_response(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
response = await agent_test_base.get_response_with_retry(responses_agent, messages=input_text)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["responses_agent"],
ids=["azure-file-search-invoke", "openai-file-search-invoke"],
)
@pytest.mark.xfail(reason="The Responses API is unstable and is throwing 500s.")
async def test_file_search_invoke(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test code interpreter."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_with_retry(responses_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_file_search": True}),
("openai", {"enable_file_search": True}),
],
indirect=["responses_agent"],
ids=["azure-file-search-invoke-stream", "openai-file-search-invoke-stream"],
)
@pytest.mark.xfail(reason="The Responses API is unstable and is throwing 500s.")
async def test_file_search_invoke_stream(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test code interpreter streaming."""
input_text = "Who is the youngest employee?"
responses = await agent_test_base.get_invoke_stream_with_retry(responses_agent, messages=input_text)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
# endregion
# region Function calling tests
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["responses_agent"],
ids=["azure-function-calling-get-response", "openai-function-calling-get-response"],
)
async def test_function_calling_get_response(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test function calling."""
response = await agent_test_base.get_response_with_retry(
responses_agent,
messages="What is the weather in Seattle?",
)
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["responses_agent"],
ids=["azure-function-calling-invoke", "openai-function-calling-invoke"],
)
async def test_function_calling_invoke(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test function calling."""
responses = await agent_test_base.get_invoke_with_retry(
responses_agent,
messages="What is the weather in Seattle?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert all(isinstance(item, TextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
assert "sunny" in response.message.content
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_kernel_function": True}),
("openai", {"enable_kernel_function": True}),
],
indirect=["responses_agent"],
ids=["azure-function-calling-invoke-stream", "openai-function-calling-invoke-stream"],
)
async def test_function_calling_stream(self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase):
"""Test function calling streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
responses_agent, messages="What is the weather in Seattle?"
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert all(isinstance(item, StreamingTextContent) for item in response.items)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert "sunny" in full_message
# endregion
# region Structured Outputs
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_structured_outputs": True}),
("openai", {"enable_structured_outputs": True}),
],
indirect=["responses_agent"],
ids=["azure-structured-outputs-get-response", "openai-structured-outputs-get-response"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when configuring structured outputs.")
async def test_structured_outputs_get_response(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test structured outputs get response."""
response = await agent_test_base.get_response_with_retry(
responses_agent,
messages="how can I solve 8x + 7y = -23, and 4x=12?",
)
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert Reasoning.model_validate_json(response.message.content)
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_structured_outputs": True}),
("openai", {"enable_structured_outputs": True}),
],
indirect=["responses_agent"],
ids=["azure-structured-outputs-invoke", "openai-structured-outputs-invoke"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when configuring structured outputs.")
async def test_structured_outputs_invoke(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test structured outputs invoke."""
responses = await agent_test_base.get_invoke_with_retry(
responses_agent,
messages="how can I solve 8x + 7y = -23, and 4x=12?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, ChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
assert Reasoning.model_validate_json(response.message.content)
@pytest.mark.parametrize(
"responses_agent",
[
("azure", {"enable_structured_outputs": True}),
("openai", {"enable_structured_outputs": True}),
],
indirect=["responses_agent"],
ids=["azure-structured-outputs-invoke-stream", "openai-structured-outputs-invoke-stream"],
)
@pytest.mark.xfail(reason="The Responses API is unstable when configuring structured outputs.")
async def test_structured_outputs_stream(
self, responses_agent: OpenAIResponsesAgent, agent_test_base: AgentTestBase
):
"""Test structured outputs streaming."""
full_message: str = ""
responses = await agent_test_base.get_invoke_stream_with_retry(
responses_agent,
messages="how can I solve 8x + 7y = -23, and 4x=12?",
)
assert len(responses) > 0
for response in responses:
assert isinstance(response.message, StreamingChatMessageContent)
assert response.message.role == AuthorRole.ASSISTANT
full_message += response.message.content
assert Reasoning.model_validate_json(full_message)
# endregion
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.audio_to_text_client_base import AudioToTextClientBase
from semantic_kernel.connectors.ai.open_ai import AzureAudioToText, OpenAIAudioToText
from tests.utils import is_service_setup_for_testing
# There is only the whisper model available on Azure OpenAI for audio to text. And that model is
# only available in the North Switzerland region. Therefore, the endpoint is different than the one
# we use for other services.
azure_setup = is_service_setup_for_testing(["AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT"])
class AudioToTextTestBase:
"""Base class for testing audio-to-text services."""
@pytest.fixture(scope="module")
def services(self) -> dict[str, AudioToTextClientBase]:
"""Return audio-to-text services."""
return {
"openai": OpenAIAudioToText(),
"azure_openai": AzureAudioToText(
endpoint=os.environ["AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT"], credential=AzureCliCredential()
)
if azure_setup
else None,
}
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from semantic_kernel.connectors.ai.audio_to_text_client_base import AudioToTextClientBase
from semantic_kernel.contents import AudioContent
from tests.integration.audio_to_text.audio_to_text_test_base import AudioToTextTestBase, azure_setup
pytestmark = pytest.mark.parametrize(
"service_id, audio_content, expected_text",
[
pytest.param(
"openai",
AudioContent.from_audio_file(os.path.join(os.path.dirname(__file__), "../../", "assets/sample_audio.mp3")),
["hi", "how", "are", "you", "doing"],
id="openai",
),
pytest.param(
"azure_openai",
AudioContent.from_audio_file(os.path.join(os.path.dirname(__file__), "../../", "assets/sample_audio.mp3")),
["hi", "how", "are", "you", "doing"],
marks=pytest.mark.skipif(not azure_setup, reason="Azure Audio to Text not setup."),
id="azure_openai",
),
],
)
class TestAudioToText(AudioToTextTestBase):
"""Test audio-to-text services."""
async def test_audio_to_text(
self,
services: dict[str, AudioToTextClientBase],
service_id: str,
audio_content: AudioContent,
expected_text: list[str],
) -> None:
"""Test audio-to-text services.
Args:
services: Audio-to-text services.
service_id: Service ID.
audio_content: Audio content.
expected_text: Expected text, list of words.
"""
service = services[service_id]
if not service:
pytest.mark.xfail("Azure Audio to Text not setup.")
result = await service.get_text_content(audio_content)
for word in expected_text:
assert word in result.text.lower(), (
f"Expected word '{word}' not found in result text: {result.text.lower()}"
)
@@ -0,0 +1,240 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import sys
from typing import Annotated
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from azure.ai.inference.aio import ChatCompletionsClient
from azure.identity import AzureCliCredential
from openai import AsyncAzureOpenAI
from semantic_kernel.connectors.ai.anthropic import AnthropicChatCompletion, AnthropicChatPromptExecutionSettings
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceChatCompletion,
AzureAIInferenceChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock import BedrockChatCompletion, BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.google.google_ai import GoogleAIChatCompletion, GoogleAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.mistral_ai import MistralAIChatCompletion, MistralAIChatPromptExecutionSettings
from semantic_kernel.connectors.ai.ollama import OllamaChatCompletion, OllamaChatPromptExecutionSettings
from semantic_kernel.connectors.ai.onnx import OnnxGenAIChatCompletion, OnnxGenAIPromptExecutionSettings, ONNXTemplate
from semantic_kernel.connectors.ai.open_ai import (
AzureChatCompletion,
AzureChatPromptExecutionSettings,
AzureOpenAISettings,
OpenAIChatCompletion,
OpenAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.core_plugins.math_plugin import MathPlugin
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing
# Make sure all services are setup for before running the tests
# The following exceptions apply:
# 1. OpenAI and Azure OpenAI services are always setup for testing.
azure_openai_setup: bool = True
# 2. Bedrock services don't use API keys and model providers are tested individually,
# so no environment variables are required.
mistral_ai_setup: bool = is_service_setup_for_testing(
["MISTRALAI_API_KEY", "MISTRALAI_CHAT_MODEL_ID"], raise_if_not_set=False
) # We don't have a MistralAI deployment
# There is no single model in Ollama that supports both image and tool call in chat completion
# We are splitting the Ollama test into three services: chat, image, and tool call. The chat model
# can be any model that supports chat completion. Also, Ollama is only available on Linux runners in our pipeline.
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID"])
ollama_image_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID_IMAGE"])
ollama_tool_call_setup: bool = is_service_setup_for_testing(["OLLAMA_CHAT_MODEL_ID_TOOL_CALL"])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing([
"GOOGLE_AI_CLOUD_PROJECT_ID",
"GOOGLE_AI_GEMINI_MODEL_ID",
"GOOGLE_AI_CLOUD_REGION",
])
onnx_setup: bool = is_service_setup_for_testing(
["ONNX_GEN_AI_CHAT_MODEL_FOLDER"], raise_if_not_set=False
) # Tests are optional for ONNX
anthropic_setup: bool = is_service_setup_for_testing(["ANTHROPIC_API_KEY", "ANTHROPIC_CHAT_MODEL_ID"])
# A mock plugin that contains a function that returns a complex object.
class PersonDetails(KernelBaseModel):
id: str
name: str
age: int
class PersonSearchPlugin:
@kernel_function(name="SearchPerson", description="Search details of a person given their id.")
def search_person(
self, person_id: Annotated[str, "The person ID to search"]
) -> Annotated[PersonDetails, "The details of the person"]:
return PersonDetails(id=person_id, name="John Doe", age=42)
class ChatCompletionTestBase(CompletionTestBase):
"""Base class for testing completion services."""
@override
@pytest.fixture(
scope="function"
) # This needs to be scoped to function to avoid resources getting cleaned up after each test
def services(self) -> dict[str, tuple[ServiceType | None, type[PromptExecutionSettings] | None]]:
azure_openai_setup = True
credential = AzureCliCredential()
azure_openai_settings = AzureOpenAISettings()
endpoint = str(azure_openai_settings.endpoint)
deployment_name = azure_openai_settings.chat_deployment_name
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
if not ad_token:
azure_openai_setup = False
api_version = azure_openai_settings.api_version
azure_custom_client = None
azure_ai_inference_client = None
if azure_openai_setup:
azure_custom_client = AzureChatCompletion(
async_client=AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
azure_ad_token=ad_token,
api_version=api_version,
default_headers={"Test-User-X-ID": "test"},
),
)
assert deployment_name
azure_ai_inference_client = AzureAIInferenceChatCompletion(
ai_model_id=deployment_name,
client=ChatCompletionsClient(
endpoint=f"{endpoint.strip('/')}/openai/deployments/{deployment_name}",
credential=credential, # type: ignore
credential_scopes=["https://cognitiveservices.azure.com/.default"],
),
)
return {
"openai": (OpenAIChatCompletion(), OpenAIChatPromptExecutionSettings),
"azure": (
AzureChatCompletion(credential=credential) if azure_openai_setup else None,
AzureChatPromptExecutionSettings,
),
"azure_custom_client": (azure_custom_client, AzureChatPromptExecutionSettings),
"azure_ai_inference": (azure_ai_inference_client, AzureAIInferenceChatPromptExecutionSettings),
"anthropic": (AnthropicChatCompletion() if anthropic_setup else None, AnthropicChatPromptExecutionSettings),
"mistral_ai": (
MistralAIChatCompletion() if mistral_ai_setup else None,
MistralAIChatPromptExecutionSettings,
),
"ollama": (OllamaChatCompletion() if ollama_setup else None, OllamaChatPromptExecutionSettings),
"ollama_image": (
OllamaChatCompletion(ai_model_id=os.environ["OLLAMA_CHAT_MODEL_ID_IMAGE"])
if ollama_image_setup
else None,
OllamaChatPromptExecutionSettings,
),
"ollama_tool_call": (
OllamaChatCompletion(ai_model_id=os.environ["OLLAMA_CHAT_MODEL_ID_TOOL_CALL"])
if ollama_tool_call_setup
else None,
OllamaChatPromptExecutionSettings,
),
"google_ai": (GoogleAIChatCompletion() if google_ai_setup else None, GoogleAIChatPromptExecutionSettings),
"vertex_ai": (
GoogleAIChatCompletion(use_vertexai=True) if vertex_ai_setup else None,
GoogleAIChatPromptExecutionSettings,
),
"onnx_gen_ai": (
OnnxGenAIChatCompletion(template=ONNXTemplate.PHI3V) if onnx_setup else None,
OnnxGenAIPromptExecutionSettings,
),
"bedrock_amazon_nova": (
self._try_create_bedrock_chat_completion_client("amazon.nova-lite-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_ai21labs": (
self._try_create_bedrock_chat_completion_client("ai21.jamba-1-5-mini-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_anthropic_claude": (
self._try_create_bedrock_chat_completion_client("anthropic.claude-3-sonnet-20240229-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_cohere_command": (
self._try_create_bedrock_chat_completion_client("cohere.command-r-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_meta_llama": (
self._try_create_bedrock_chat_completion_client("meta.llama3-70b-instruct-v1:0"),
BedrockChatPromptExecutionSettings,
),
"bedrock_mistralai": (
self._try_create_bedrock_chat_completion_client("mistral.mistral-small-2402-v1:0"),
BedrockChatPromptExecutionSettings,
),
}
def setup(self, kernel: Kernel):
"""Setup the kernel with the completion service and function."""
kernel.add_plugin(MathPlugin(), plugin_name="math")
kernel.add_plugin(PersonSearchPlugin(), plugin_name="search")
async def get_chat_completion_response(
self,
kernel: Kernel,
service: ServiceType,
execution_settings: PromptExecutionSettings,
chat_history: ChatHistory,
stream: bool,
) -> ChatMessageContent | StreamingChatMessageContent | None:
"""Get response from the service
Args:
kernel (Kernel): Kernel instance.
service (ChatCompletionClientBase): Chat completion service.
execution_settings (PromptExecutionSettings): Execution settings.
input (str): Input string.
stream (bool): Stream flag.
"""
assert isinstance(service, ChatCompletionClientBase)
if not stream:
return await service.get_chat_message_content(
chat_history,
execution_settings,
kernel=kernel,
)
parts: list[StreamingChatMessageContent] = [
part
async for part in service.get_streaming_chat_message_content(
chat_history,
execution_settings,
kernel=kernel,
)
if part
]
if parts:
return sum(parts[1:], parts[0])
raise AssertionError("No response")
def _try_create_bedrock_chat_completion_client(self, model_id: str) -> BedrockChatCompletion | None:
try:
return BedrockChatCompletion(model_id=model_id)
except Exception as ex:
from conftest import logger
logger.warning(ex)
# Returning None so that the test that uses this service will be skipped
return None
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.kernel import Kernel
ServiceType = ChatCompletionClientBase | TextCompletionClientBase
class CompletionTestBase:
"""Base class for testing completion services."""
def services(self) -> dict[str, tuple["ServiceType", type[PromptExecutionSettings]]]:
"""Return completion services."""
raise NotImplementedError
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str | ChatMessageContent | list[ChatMessageContent]],
kwargs: dict[str, Any],
) -> None:
"""Test completion service (Non-streaming).
Args:
kernel (Kernel): Kernel instance.
service_id (str): Service name.
services (dict[str, tuple[ServiceType, type[PromptExecutionSettings]]]): Completion services.
execution_settings_kwargs (dict[str, Any]): Execution settings keyword arguments.
inputs (list[str]): List of input strings.
kwargs (dict[str, Any]): Keyword arguments.
"""
raise NotImplementedError
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str | ChatMessageContent | list[ChatMessageContent]],
kwargs: dict[str, Any],
):
"""Test completion service (Streaming).
Args:
kernel (Kernel): Kernel instance.
service_id (str): Service name.
services (dict[str, tuple[ServiceType, type[PromptExecutionSettings]]]): Completion services.
execution_settings_kwargs (dict[str, Any]): Execution settings keyword arguments.
inputs (list[str]): List of input strings.
kwargs (dict[str, Any]): Keyword arguments.
"""
raise NotImplementedError
def evaluate(self, test_target: Any, **kwargs):
"""Evaluate the response.
Args:
test_target (Any): Test target.
kwargs (dict[str, Any]): Keyword arguments.
"""
raise NotImplementedError
@@ -0,0 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from semantic_kernel.utils.logging import setup_logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
setup_logging()
@@ -0,0 +1,158 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import time
from random import randint
import numpy as np
import pytest
import pytest_asyncio
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
ApiKeyAuthentication,
AzureAISearchDataSource,
AzureAISearchDataSourceParameters,
DataSourceFieldsMapping,
ExtraBody,
)
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
from semantic_kernel.memory.memory_record import MemoryRecord
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
try:
from semantic_kernel.connectors.memory_stores.azure_cognitive_search.azure_cognitive_search_memory_store import (
AzureCognitiveSearchMemoryStore,
)
azure_ai_search_installed = True
except ImportError:
azure_ai_search_installed = False
if os.environ.get("AZURE_COGNITIVE_SEARCH_ENDPOINT") and os.environ.get("AZURE_COGNITIVE_SEARCH_ADMIN_KEY"):
azure_ai_search_settings = True
else:
azure_ai_search_settings = False
pytestmark = pytest.mark.skipif(
not (azure_ai_search_installed and azure_ai_search_settings),
reason="Azure AI Search is not installed",
)
@pytest_asyncio.fixture
async def create_memory_store():
# Create an index and populate it with some data
collection = f"int-tests-chat-extensions-{randint(1000, 9999)}"
memory_store = AzureCognitiveSearchMemoryStore(vector_size=4)
await memory_store.create_collection(collection)
time.sleep(1)
try:
assert await memory_store.does_collection_exist(collection)
rec = MemoryRecord(
is_reference=False,
external_source_name=None,
id=None,
description="Emily and David's story.",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. \
Bonded by their love for the natural world and shared curiosity, they uncovered a \
groundbreaking phenomenon in glaciology that could potentially reshape our understanding \
of climate change.",
additional_metadata=None,
embedding=np.array([0.2, 0.1, 0.2, 0.7]),
)
await memory_store.upsert(collection, rec)
time.sleep(1)
return collection, memory_store
except Exception as e:
await memory_store.delete_collection(collection)
raise e
@pytest_asyncio.fixture
async def create_with_data_chat_function(kernel: Kernel, create_memory_store):
collection, memory_store = create_memory_store
try:
# Load Azure OpenAI with data settings
search_endpoint = os.getenv("AZURE_COGNITIVE_SEARCH_ENDPOINT")
search_api_key = os.getenv("AZURE_COGNITIVE_SEARCH_ADMIN_KEY")
extra = ExtraBody(
data_sources=[
AzureAISearchDataSource(
parameters=AzureAISearchDataSourceParameters(
index_name=collection,
endpoint=search_endpoint,
authentication=ApiKeyAuthentication(key=search_api_key),
query_type="simple",
fields_mapping=DataSourceFieldsMapping(
title_field="Description",
content_fields=["Text"],
),
top_n_documents=1,
),
)
]
)
chat_service = AzureChatCompletion(service_id="chat-gpt-extensions", credential=AzureCliCredential())
kernel.add_service(chat_service)
prompt = "{{$chat_history}}{{$input}}"
exec_settings = PromptExecutionSettings(
service_id="chat-gpt-extensions",
extension_data={"max_tokens": 2000, "temperature": 0.7, "top_p": 0.8, "extra_body": extra},
)
prompt_template_config = PromptTemplateConfig(
template=prompt, description="Chat", execution_settings=exec_settings
)
# Create the semantic function
kernel.add_function(function_name="chat", plugin_name="plugin", prompt_template_config=prompt_template_config)
chat_function = kernel.get_function("plugin", "chat")
return chat_function, kernel, collection, memory_store
except Exception as e:
await memory_store.delete_collection(collection)
raise e
@pytestmark
async def test_azure_e2e_chat_completion_with_extensions(create_with_data_chat_function):
# Create an index and populate it with some data
chat_function, kernel, collection, memory_store = create_with_data_chat_function
chat_history = ChatHistory()
chat_history.add_user_message("A story about Emily and David...")
arguments = KernelArguments(input="who are Emily and David?", chat_history=chat_history)
# TODO: get streaming working for this test
use_streaming = False
try:
result: StreamingChatMessageContent = None
if use_streaming:
async for message in kernel.invoke_stream(chat_function, arguments):
result = message[0] if not result else result + message[0]
print(message, end="")
print(f"Answer using input string: '{result}'")
for item in result.items:
if isinstance(item, FunctionResultContent):
print(f"Content: {item.result}")
assert "two passionate scientists" in item.result
else:
result = await kernel.invoke(chat_function, arguments)
print(f"Answer using input string: '{result}'")
await memory_store.delete_collection(collection)
except Exception as e:
await memory_store.delete_collection(collection)
raise e
@@ -0,0 +1,347 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import sys
from functools import partial
from typing import Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents import ChatHistory, ChatMessageContent, TextContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from tests.integration.completions.chat_completion_test_base import (
ChatCompletionTestBase,
ollama_image_setup,
onnx_setup,
vertex_ai_setup,
)
from tests.integration.completions.completion_test_base import ServiceType
from tests.utils import retry
# Use the repo's own sample image via raw GitHub URL for URI-based tests.
# Previously this pointed to a 17.5 MB Wikimedia image that got blocked by
# Wikimedia's User-Agent policy (Phabricator T400119), causing Azure's
# server-side image fetcher to fail with HTTP 403.
IMAGE_TEST_URL = "https://raw.githubusercontent.com/microsoft/semantic-kernel/main/python/tests/assets/sample_image.jpg"
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, inputs, kwargs",
[
pytest.param(
"openai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent(uri=IMAGE_TEST_URL),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="openai_image_input_uri",
),
pytest.param(
"openai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="openai_image_input_file",
),
pytest.param(
"azure",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent(uri=IMAGE_TEST_URL),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_image_input_uri",
),
pytest.param(
"azure",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_image_input_file",
),
pytest.param(
"onnx_gen_ai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
marks=(
pytest.mark.skipif(not onnx_setup, reason="Need a Onnx Model setup"),
pytest.mark.onnx,
),
id="onnx_gen_ai_image_input_file",
),
pytest.param(
"azure_ai_inference",
{
"max_tokens": 256,
},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent(uri=IMAGE_TEST_URL),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_ai_inference_image_input_uri",
),
pytest.param(
"azure_ai_inference",
{
"max_tokens": 256,
},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
id="azure_ai_inference_image_input_file",
),
pytest.param(
"google_ai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="Where was it made? Make a guess if you are not sure.")],
),
],
{},
marks=[
pytest.mark.skip(reason="Skipping due to occasional throttling from Google AI."),
# pytest.mark.skipif(not google_ai_setup, reason="Google AI Environment Variables not set"),
],
id="google_ai_image_input_file",
),
pytest.param(
"vertex_ai",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="Where was it made? Make a guess if you are not sure.")],
),
],
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI Environment Variables not set"),
id="vertex_ai_image_input_file",
),
pytest.param(
"ollama_image",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(
role=AuthorRole.USER,
items=[TextContent(text="Where was it made? Make a guess if you are not sure.")],
),
],
{},
marks=(
pytest.mark.skipif(not ollama_image_setup, reason="Ollama Environment Variables not set"),
pytest.mark.ollama,
),
id="ollama_image_input_file",
),
pytest.param(
"bedrock_anthropic_claude",
{},
[
ChatMessageContent(
role=AuthorRole.USER,
items=[
TextContent(text="What is in this image?"),
ImageContent.from_image_path(
image_path=os.path.join(os.path.dirname(__file__), "../../", "assets/sample_image.jpg")
),
],
),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Where was it made?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_anthropic_claude_image_input_file",
),
],
)
class TestChatCompletionWithImageInputTextOutput(ChatCompletionTestBase):
"""Test chat completion with image input and text output."""
@override
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
False,
)
@override
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
True,
)
@override
def evaluate(self, test_target: Any, **kwargs):
inputs = kwargs.get("inputs")
assert isinstance(inputs, list)
assert len(test_target) == len(inputs) * 2
for i in range(len(inputs)):
message = test_target[i * 2 + 1]
assert message.items, "No items in message"
assert len(message.items) == 1, "Unexpected number of items in message"
assert isinstance(message.items[0], TextContent), "Unexpected message item type"
assert message.items[0].text, "Empty message text"
async def _test_helper(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
stream: bool,
):
self.setup(kernel)
service, settings_type = services[service_id]
if service is None:
pytest.skip(f"Service {service_id} not set up")
history = ChatHistory()
for message in inputs:
history.add_message(message)
cmc: ChatMessageContent | None = await retry(
partial(
self.get_chat_completion_response,
kernel=kernel,
service=service,
execution_settings=settings_type(**execution_settings_kwargs),
chat_history=history,
stream=stream,
),
retries=5,
name="image_input",
)
if cmc:
history.add_message(cmc)
self.evaluate(history.messages, inputs=inputs)
@@ -0,0 +1,346 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from functools import partial
from typing import Any
import pytest
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai import PromptExecutionSettings
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent, TextContent
from semantic_kernel.kernel_pydantic import KernelBaseModel
from tests.integration.completions.chat_completion_test_base import (
ChatCompletionTestBase,
anthropic_setup,
mistral_ai_setup,
ollama_setup,
onnx_setup,
vertex_ai_setup,
)
from tests.integration.completions.completion_test_base import ServiceType
from tests.utils import retry
class Step(KernelBaseModel):
explanation: str
output: str
class Reasoning(KernelBaseModel):
steps: list[Step]
final_answer: str
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, inputs, kwargs",
[
# region OpenAI
pytest.param(
"openai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="openai_text_input",
),
pytest.param(
"openai",
{"response_format": Reasoning},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="openai_json_schema_response_format",
),
# endregion
# region Azure
pytest.param(
"azure",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="azure_text_input",
),
pytest.param(
"azure_custom_client",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="azure_custom_client",
),
# endregion
# region Azure AI Inference
pytest.param(
"azure_ai_inference",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="azure_ai_inference_text_input",
),
# endregion
# region Anthropic
pytest.param(
"anthropic",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skipif(not anthropic_setup, reason="Anthropic Environment Variables not set"),
id="anthropic_text_input",
),
# endregion
# region Mistral AI
pytest.param(
"mistral_ai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skipif(not mistral_ai_setup, reason="Mistral AI Environment Variables not set"),
id="mistral_ai_text_input",
),
# endregion
# region Ollama
pytest.param(
"ollama",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=(
pytest.mark.skipif(not ollama_setup, reason="Need local Ollama setup"),
pytest.mark.ollama,
),
id="ollama_text_input",
),
# endregion
# region Onnx Gen AI
pytest.param(
"onnx_gen_ai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=(
pytest.mark.skipif(not onnx_setup, reason="Need a Onnx Model setup"),
pytest.mark.onnx,
),
id="onnx_gen_ai",
),
# endregion
# region Google AI
pytest.param(
"google_ai",
{"top_p": 0.9, "temperature": 0.7},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=[
pytest.mark.skip(reason="Skipping due to occasional throttling from Google AI."),
# pytest.mark.skipif(not google_ai_setup, reason="Need Google AI setup"),
],
id="google_ai_text_input",
),
# endregion
# region Vertex AI
pytest.param(
"vertex_ai",
{"top_p": 0.9, "temperature": 0.7},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI Environment Variables not set"),
id="vertex_ai_text_input",
),
# endregion
# region Bedrock
pytest.param(
"bedrock_amazon_nova",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
id="bedrock_amazon_nova_text_input",
),
pytest.param(
"bedrock_ai21labs",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_ai21labs_text_input",
),
pytest.param(
"bedrock_anthropic_claude",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_anthropic_claude_text_input",
),
pytest.param(
"bedrock_cohere_command",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_cohere_command_text_input",
),
pytest.param(
"bedrock_meta_llama",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_meta_llama_text_input",
),
pytest.param(
"bedrock_mistralai",
{},
[
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="Hello")]),
ChatMessageContent(role=AuthorRole.USER, items=[TextContent(text="How are you today?")]),
],
{},
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_mistralai_text_input",
),
# endregion
],
)
class TestChatCompletion(ChatCompletionTestBase):
"""Test Chat Completions.
This only tests if the services can return text completions given text inputs.
"""
@override
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
False,
)
@override
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
kwargs: dict[str, Any],
):
await self._test_helper(
kernel,
service_id,
services,
execution_settings_kwargs,
inputs,
True,
)
@override
def evaluate(self, test_target: Any, **kwargs):
inputs = kwargs.get("inputs")
assert isinstance(inputs, list)
assert len(test_target) == len(inputs) * 2
for i in range(len(inputs)):
message = test_target[i * 2 + 1]
assert message.items, "No items in message"
assert len(message.items) == 1, "Unexpected number of items in message"
assert isinstance(message.items[0], TextContent), "Unexpected message item type"
assert message.items[0].text, "Empty message text"
async def _test_helper(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[ChatMessageContent],
stream: bool,
):
self.setup(kernel)
service, settings_type = services[service_id]
if service is None:
pytest.skip(f"Service {service_id} not set up")
history = ChatHistory()
for message in inputs:
history.add_message(message)
cmc: ChatMessageContent | None = await retry(
partial(
self.get_chat_completion_response,
kernel=kernel,
service=service,
execution_settings=settings_type(**execution_settings_kwargs),
chat_history=history,
stream=stream,
),
retries=5,
name="get_chat_completion_response",
)
if cmc:
history.add_message(cmc)
self.evaluate(history.messages, inputs=inputs)
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
import semantic_kernel.connectors.ai.open_ai as sk_oai
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.core_plugins.conversation_summary_plugin import ConversationSummaryPlugin
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
from tests.utils import retry
CHAT_TRANSCRIPT = """John: Hello, how are you?
Jane: I'm fine, thanks. How are you?
John: I'm doing well, writing some example code.
Jane: That's great! I'm writing some example code too.
John: What are you writing?
Jane: I'm writing a chatbot.
John: That's cool. I'm writing a chatbot too.
Jane: What language are you writing it in?
John: I'm writing it in C#.
Jane: I'm writing it in Python.
John: That's cool. I need to learn Python.
Jane: I need to learn C#.
John: Can I try out your chatbot?
Jane: Sure, here's the link.
John: Thanks!
Jane: You're welcome.
Jane: Look at this poem my chatbot wrote:
Jane: Roses are red
Jane: Violets are blue
Jane: I'm writing a chatbot
Jane: What about you?
John: That's cool. Let me see if mine will write a poem, too.
John: Here's a poem my chatbot wrote:
John: The singularity of the universe is a mystery.
Jane: You might want to try using a different model.
John: I'm using the GPT-2 model. That makes sense.
John: Here is a new poem after updating the model.
John: The universe is a mystery.
John: The universe is a mystery.
John: The universe is a mystery.
Jane: Sure, what's the problem?
John: Thanks for the help!
Jane: I'm now writing a bot to summarize conversations.
Jane: I have some bad news, we're only half way there.
John: Maybe there is a large piece of text we can use to generate a long conversation.
Jane: That's a good idea. Let me see if I can find one. Maybe Lorem Ipsum?
John: Yeah, that's a good idea."""
async def test_azure_summarize_conversation_using_plugin(kernel):
service_id = "text_completion"
execution_settings = PromptExecutionSettings(
service_id=service_id, max_tokens=ConversationSummaryPlugin._max_tokens, temperature=0.1, top_p=0.5
)
prompt_template_config = PromptTemplateConfig(
description="Given a section of a conversation transcript, summarize the part of the conversation.",
execution_settings={service_id: execution_settings},
)
kernel.add_service(sk_oai.OpenAIChatCompletion(service_id=service_id))
conversationSummaryPlugin = kernel.add_plugin(
ConversationSummaryPlugin(prompt_template_config), "conversationSummary"
)
arguments = KernelArguments(input=CHAT_TRANSCRIPT)
summary = await retry(
lambda: kernel.invoke(conversationSummaryPlugin["SummarizeConversation"], arguments), retries=5
)
output = str(summary).strip().lower()
print(output)
assert "john" in output and "jane" in output
assert len(output) < len(CHAT_TRANSCRIPT)
@@ -0,0 +1,345 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from functools import partial
from importlib import util
from typing import Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import pytest
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.bedrock import BedrockTextCompletion, BedrockTextPromptExecutionSettings
from semantic_kernel.connectors.ai.google.google_ai import GoogleAITextCompletion, GoogleAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.hugging_face import HuggingFacePromptExecutionSettings, HuggingFaceTextCompletion
from semantic_kernel.connectors.ai.ollama import OllamaTextCompletion, OllamaTextPromptExecutionSettings
from semantic_kernel.connectors.ai.onnx import OnnxGenAIPromptExecutionSettings, OnnxGenAITextCompletion
from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion, OpenAITextPromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents import StreamingTextContent, TextContent
from tests.integration.completions.completion_test_base import CompletionTestBase, ServiceType
from tests.utils import is_service_setup_for_testing, is_test_running_on_supported_platforms, retry
hugging_face_setup = util.find_spec("torch") is not None
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_TEXT_MODEL_ID"]) and is_test_running_on_supported_platforms([
"Linux"
])
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_GEMINI_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing([
"GOOGLE_AI_CLOUD_PROJECT_ID",
"GOOGLE_AI_GEMINI_MODEL_ID",
"GOOGLE_AI_CLOUD_REGION",
])
onnx_setup: bool = is_service_setup_for_testing(
["ONNX_GEN_AI_TEXT_MODEL_FOLDER"], raise_if_not_set=False
) # Tests are optional for ONNX
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, inputs, kwargs",
[
pytest.param(
"openai",
{},
["Repeat the word Hello once"],
{},
id="openai_text_completion",
),
pytest.param(
"hf_t2t",
{},
["translate English to Dutch: Hello"],
{},
id="huggingface_text_completion_translation",
),
pytest.param(
"hf_summ",
{},
[
"""Summarize: Whales are fully aquatic, open-ocean animals:
they can feed, mate, give birth, suckle and raise their young at sea.
Whales range in size from the 2.6 metres (8.5 ft) and 135 kilograms (298 lb)
dwarf sperm whale to the 29.9 metres (98 ft) and 190 tonnes (210 short tons) blue whale,
which is the largest known animal that has ever lived. The sperm whale is the largest
toothed predator on Earth. Several whale species exhibit sexual dimorphism,
in that the females are larger than males."""
],
{},
id="huggingface_text_completion_summarization",
),
pytest.param(
"hf_gen",
{},
["Hello, I like sleeping and "],
{},
id="huggingface_text_completion_generation",
),
pytest.param(
"ollama",
{},
["Repeat the word Hello once"],
{},
marks=(
pytest.mark.skip(
reason="Need local Ollama setup" if not ollama_setup else "Ollama responses are not always correct."
),
pytest.mark.ollama,
),
id="ollama_text_completion",
),
pytest.param(
"google_ai",
{},
["Repeat the word Hello once"],
{},
marks=[
pytest.mark.skip(reason="Skipping due to occasional throttling from Google AI."),
# pytest.mark.skipif(not google_ai_setup, reason="Need Google AI setup"),
],
id="google_ai_text_completion",
),
pytest.param(
"vertex_ai",
{},
["Repeat the word Hello once"],
{},
marks=pytest.mark.skipif(not vertex_ai_setup, reason="Need VertexAI setup"),
id="vertex_ai_text_completion",
),
pytest.param(
"onnx_gen_ai",
{},
["<|user|>Repeat the word Hello<|end|><|assistant|>"],
{},
marks=(
pytest.mark.skipif(not onnx_setup, reason="Need a Onnx Model setup"),
pytest.mark.onnx,
),
id="onnx_gen_ai_text_completion",
),
pytest.param(
"bedrock_anthropic_claude",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_anthropic_claude_text_completion",
),
pytest.param(
"bedrock_cohere_command",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_cohere_command_text_completion",
),
pytest.param(
"bedrock_ai21labs",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_ai21labs_text_completion",
),
pytest.param(
"bedrock_meta_llama",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_meta_llama_text_completion",
),
pytest.param(
"bedrock_mistralai",
{},
["Repeat the word Hello once"],
{"streaming": False}, # Streaming is not supported for models from this provider
marks=pytest.mark.skip(reason="Skipping due to occasional throttling from Bedrock."),
id="bedrock_mistralai_text_completion",
),
],
)
class TestTextCompletion(CompletionTestBase):
"""Test class for text completion"""
@override
@pytest.fixture(scope="class")
def services(self) -> dict[str, tuple[ServiceType | None, type[PromptExecutionSettings] | None]]:
"""Get the services to be tested"""
return {
"openai": (OpenAITextCompletion(), OpenAITextPromptExecutionSettings),
"ollama": (OllamaTextCompletion() if ollama_setup else None, OllamaTextPromptExecutionSettings),
"google_ai": (GoogleAITextCompletion() if google_ai_setup else None, GoogleAITextPromptExecutionSettings),
"vertex_ai": (
GoogleAITextCompletion(use_vertexai=True) if vertex_ai_setup else None,
GoogleAITextPromptExecutionSettings,
),
"hf_t2t": (
HuggingFaceTextCompletion(
service_id="patrickvonplaten/t5-tiny-random",
ai_model_id="patrickvonplaten/t5-tiny-random",
task="text2text-generation",
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"hf_summ": (
HuggingFaceTextCompletion(
service_id="Falconsai/text_summarization",
ai_model_id="Falconsai/text_summarization",
task="summarization",
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"hf_gen": (
HuggingFaceTextCompletion(
service_id="HuggingFaceM4/tiny-random-LlamaForCausalLM",
ai_model_id="HuggingFaceM4/tiny-random-LlamaForCausalLM",
task="text-generation",
)
if hugging_face_setup
else None,
HuggingFacePromptExecutionSettings,
),
"onnx_gen_ai": (
OnnxGenAITextCompletion() if onnx_setup else None,
OnnxGenAIPromptExecutionSettings,
),
# Amazon Bedrock supports models from multiple providers but requests to and responses from the models are
# inconsistent. So we need to test each model separately.
"bedrock_anthropic_claude": (
self._try_create_bedrock_text_completion_client("anthropic.claude-v2"),
BedrockTextPromptExecutionSettings,
),
"bedrock_cohere_command": (
self._try_create_bedrock_text_completion_client("cohere.command-text-v14"),
BedrockTextPromptExecutionSettings,
),
"bedrock_ai21labs": (
self._try_create_bedrock_text_completion_client("ai21.j2-mid-v1"),
BedrockTextPromptExecutionSettings,
),
"bedrock_meta_llama": (
self._try_create_bedrock_text_completion_client("meta.llama3-70b-instruct-v1:0"),
BedrockTextPromptExecutionSettings,
),
"bedrock_mistralai": (
self._try_create_bedrock_text_completion_client("mistral.mistral-7b-instruct-v0:2"),
BedrockTextPromptExecutionSettings,
),
}
async def get_text_completion_response(
self,
service: ServiceType,
execution_settings: PromptExecutionSettings,
prompt: str,
stream: bool,
) -> Any:
"""Get response from the service
Args:
kernel (Kernel): Kernel instance.
service (ChatCompletionClientBase): Chat completion service.
execution_settings (PromptExecutionSettings): Execution settings.
prompt (str): Input string.
stream (bool): Stream flag.
"""
assert isinstance(service, TextCompletionClientBase)
if stream:
response = service.get_streaming_text_content(
prompt=prompt,
settings=execution_settings,
)
parts: list[StreamingTextContent] = [part async for part in response if part is not None]
if parts:
return sum(parts[1:], parts[0])
raise AssertionError("No response")
return await service.get_text_content(
prompt=prompt,
settings=execution_settings,
)
return response
@override
async def test_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str],
kwargs: dict[str, Any],
) -> None:
await self._test_helper(service_id, services, execution_settings_kwargs, inputs, False)
@override
async def test_streaming_completion(
self,
kernel: Kernel,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str],
kwargs: dict[str, Any],
):
if "streaming" in kwargs and not kwargs["streaming"]:
pytest.skip("Skipping streaming test")
await self._test_helper(service_id, services, execution_settings_kwargs, inputs, True)
@override
def evaluate(self, test_target: Any, **kwargs):
print(test_target)
if isinstance(test_target, TextContent):
# Test is considered successful if the test_target is not empty
assert test_target.text, "Error: Empty test target"
return
raise AssertionError(f"Unexpected output: {test_target}, type: {type(test_target)}")
async def _test_helper(
self,
service_id: str,
services: dict[str, tuple[ServiceType, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
inputs: list[str],
stream: bool,
):
service, settings_type = services[service_id]
if not service:
pytest.skip(f"Setup not ready for {service_id if service_id else 'None'}")
for test_input in inputs:
response = await retry(
partial(
self.get_text_completion_response,
service=service,
execution_settings=settings_type(**execution_settings_kwargs),
prompt=test_input,
stream=stream,
),
retries=5,
name="text completions",
)
self.evaluate(response)
def _try_create_bedrock_text_completion_client(self, model_id: str) -> BedrockTextCompletion | None:
try:
return BedrockTextCompletion(model_id=model_id)
except Exception as ex:
from conftest import logger
logger.warning(ex)
# Returning None so that the test that uses this service will be skipped
return None
@@ -0,0 +1,197 @@
{
"openapi": "3.0.1",
"info": {
"title": "Light Bulb API",
"version": "v1"
},
"servers": [
{
"url": "https://127.0.0.1"
}
],
"paths": {
"/Lights/{id}": {
"get": {
"operationId": "GetLightById",
"tags": [
"Lights"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"style": "simple",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
},
"put": {
"operationId": "PutLightById",
"tags": [
"Lights"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"style": "simple",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeStateRequest"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ChangeStateRequest"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ChangeStateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Success"
}
}
},
"delete": {
"operationId": "DeleteLightById",
"tags": [
"Lights"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"style": "simple",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/Lights": {
"get": {
"operationId": "GetLights",
"tags": [
"Lights"
],
"parameters": [
{
"name": "roomId",
"in": "query",
"style": "form",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"operationId": "CreateLights",
"tags": [
"Lights"
],
"parameters": [
{
"name": "roomId",
"in": "query",
"style": "form",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "lightName",
"in": "query",
"style": "form",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
}
},
"components": {
"schemas": {
"ChangeStateRequest": {
"type": "object",
"properties": {
"isOn": {
"type": "boolean",
"description": "Specifies whether the light is turned on or off."
},
"hexColor": {
"type": "string",
"description": "The hex color code for the light.",
"nullable": true
},
"brightness": {
"enum": [
"Low",
"Medium",
"High"
],
"type": "string",
"description": "The brightness level of the light."
},
"fadeDurationInMilliseconds": {
"type": "integer",
"description": "Duration for the light to fade to the new state, in milliseconds.",
"format": "int32"
},
"scheduledTime": {
"type": "string",
"description": "The time at which the change should occur.",
"format": "date-time"
}
},
"additionalProperties": false,
"description": "Represents a request to change the state of the light."
}
}
}
}
@@ -0,0 +1,10 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,18 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
},
{
"content": "Sure! The time in Seattle is currently 3:00 PM.",
"role": "assistant"
},
{
"content": "What about New York?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,7 @@
name: getTimes
description: Gets the time in various cities.
template: |
<message role="user">Can you help me tell the time in Seattle right now?</message>
<message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message>
<message role="user">What about New York?</message>
template_format: handlebars
@@ -0,0 +1,7 @@
name: getTimes
description: Gets the time in various cities.
template: |
<message role="user">Can you help me tell the time in Seattle right now?</message>
<message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message>
<message role="user">What about New York?</message>
template_format: jinja2
@@ -0,0 +1,10 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,14 @@
{
"messages": [
{
"content": "The current time is Sun, 04 Jun 1989 12:11:13 GMT",
"role": "system"
},
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,10 @@
{
"messages": [
{
"content": "Can you help me tell the time in Seattle right now?",
"role": "user"
}
],
"stream": false,
"model": "gpt-4.1-nano"
}
@@ -0,0 +1,9 @@
name: getTimeInCity
description: Gets the time in a specified city.
template: |
<message role="user">Can you help me tell the time in {{$city}} right now?</message>
template_format: semantic-kernel
input_variables:
- name: city
description: City for which time is desired
default: Seattle
@@ -0,0 +1,5 @@
name: getSeattleTime
description: Gets the time in Seattle.
template: |
<message role="user">Can you help me tell the time in Seattle right now?</message>
template_format: semantic-kernel
@@ -0,0 +1,834 @@
# Copyright (c) Microsoft. All rights reserved.
import contextlib
import datetime
import json
import logging
import os
from collections.abc import AsyncGenerator
from typing import Literal
import httpx
import pytest
import pytest_asyncio
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAISettings
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
logger = logging.getLogger(__name__)
OPENAI_MODEL_ID = "gpt-4.1-nano"
# region Test Prompts
simple_prompt = "Can you help me tell the time in Seattle right now?"
sk_simple_prompt = "Can you help me tell the time in {{$city}} right now?"
hb_simple_prompt = "Can you help me tell the time in {{city}} right now?"
j2_simple_prompt = "Can you help me tell the time in {{city}} right now?"
sk_prompt = '<message role="system">The current time is {{Time.Now}}</message><message role="user">Can you help me tell the time in {{$city}} right now?</message>' # noqa: E501
hb_prompt = '<message role="system">The current time is {{Time-Now}}</message><message role="user">Can you help me tell the time in {{city}} right now?</message>' # noqa: E501
j2_prompt = '<message role="system">The current time is {{Time_Now()}}</message><message role="user">Can you help me tell the time in {{city}} right now?</message>' # noqa: E501
# endregion
# region Custom Logging Class
class LoggingTransport(httpx.AsyncBaseTransport):
def __init__(self, inner=None):
self.inner = inner or httpx.AsyncHTTPTransport()
self.request_headers = {}
self.request_content = None
self.response_headers = {}
self.response_content = None
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
self.request_headers = dict(request.headers)
self.request_content = request.content.decode("utf-8") if request.content else None
logger.info(f"Request URL: {request.url}")
logger.info(f"Request Headers: {self.request_headers}")
logger.info(f"Request Content: {self.request_content}")
response = await self.inner.handle_async_request(request)
raw_response_bytes = await response.aread()
self.response_headers = dict(response.headers)
self.response_content = raw_response_bytes.decode(response.encoding or "utf-8", errors="replace")
logger.info(f"Response Headers: {self.response_headers}")
logger.info(f"Response Content: {self.response_content}")
headers_without_encoding = {k: v for k, v in response.headers.items() if k.lower() != "content-encoding"}
return httpx.Response(
status_code=response.status_code,
headers=headers_without_encoding,
content=raw_response_bytes,
request=request,
extensions=response.extensions,
)
class LoggingAsyncClient(httpx.AsyncClient):
def __init__(self, *args, **kwargs):
transport = kwargs.pop("transport", None)
self.logging_transport = LoggingTransport(transport or httpx.AsyncHTTPTransport())
super().__init__(*args, **kwargs, transport=self.logging_transport)
@property
def request_headers(self):
return self.logging_transport.request_headers
@property
def request_content(self):
return self.logging_transport.request_content
@property
def response_headers(self):
return self.logging_transport.response_headers
@property
def response_content(self):
return self.logging_transport.response_content
# endregion
# region Test Helper Methods
@pytest_asyncio.fixture
async def async_clients() -> AsyncGenerator[tuple[AsyncOpenAI, LoggingAsyncClient], None]:
openai_settings = OpenAISettings()
logging_async_client = LoggingAsyncClient()
async with AsyncOpenAI(
api_key=openai_settings.api_key.get_secret_value(), http_client=logging_async_client
) as client:
yield client, logging_async_client
async def run_prompt(
kernel: Kernel,
is_inline: bool = False,
is_streaming: bool = False,
template_format: Literal[
"semantic-kernel",
"handlebars",
"jinja2",
] = None,
prompt: str = None,
arguments: KernelArguments = None,
prompt_template_config: PromptTemplateConfig = None,
):
if is_inline:
if is_streaming:
try:
async for _ in kernel.invoke_prompt_stream(
function_name="func_test_stream",
plugin_name="plugin_test",
prompt=prompt,
arguments=arguments,
template_format=template_format,
prompt_template_config=prompt_template_config,
):
pass
except NotImplementedError:
pass
else:
await kernel.invoke_prompt(
function_name="func_test",
plugin_name="plugin_test_stream",
prompt=prompt,
arguments=arguments,
template_format=template_format,
prompt_template_config=prompt_template_config,
)
else:
function = KernelFunctionFromPrompt(
function_name="test_func",
plugin_name="test_plugin",
prompt=prompt,
template_format=template_format,
prompt_template_config=prompt_template_config,
)
await run_function(kernel, is_streaming, function=function, arguments=arguments)
async def run_function(
kernel: Kernel,
is_streaming: bool = False,
function: KernelFunction | None = None,
arguments: KernelArguments | None = None,
):
if is_streaming:
try:
async for _ in kernel.invoke_stream(function=function, arguments=arguments):
pass
except NotImplementedError:
pass
else:
await kernel.invoke(function=function, arguments=arguments)
class City:
def __init__(self, name):
self.name = name
# endregion
# region Test Prompt With Chat Roles
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(
True,
False,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_inline_non_streaming",
),
pytest.param(
True,
True,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_inline_streaming",
),
pytest.param(
False,
False,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_non_inline_non_streaming",
),
pytest.param(
False,
True,
"semantic-kernel",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="sk_non_inline_streaming",
),
pytest.param(
False,
False,
"handlebars",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="hb_non_inline_non_streaming",
),
pytest.param(
False,
True,
"handlebars",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="hb_non_inline_streaming",
),
pytest.param(
False,
False,
"jinja2",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="j2_non_inline_non_streaming",
),
pytest.param(
False,
True,
"jinja2",
'<message role="user">Can you help me tell the time in Seattle right now?</message><message role="assistant">Sure! The time in Seattle is currently 3:00 PM.</message><message role="user">What about New York?</message>', # noqa: E501
id="j2_non_inline_streaming",
),
],
)
async def test_prompt_with_chat_roles(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="test",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
template_format=template_format,
prompt=prompt,
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_chat_roles_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Prompt With Complex Objects
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(
False,
False,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_non_inline_non_streaming",
),
pytest.param(
False,
True,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_non_inline_streaming",
),
pytest.param(
False,
False,
"jinja2",
"Can you help me tell the time in {{city.name}} right now?",
id="j2_non_inline_non_streaming",
),
pytest.param(
False,
True,
"jinja2",
"Can you help me tell the time in {{city.name}} right now?",
id="j2_non_inline_streaming",
),
pytest.param(
True,
False,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_inline_non_streaming",
),
pytest.param(
True,
True,
"handlebars",
"Can you help me tell the time in {{city.name}} right now?",
id="hb_inline_streaming",
),
pytest.param(
True,
False,
"jinja2",
"Can you help me tell the time in {{city.name}} right now?",
id="j2_inline_non_streaming",
),
pytest.param(
True, True, "jinja2", "Can you help me tell the time in {{city.name}} right now?", id="j2_inline_streaming"
),
],
)
async def test_prompt_with_complex_objects(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
prompt=prompt,
template_format=template_format,
prompt_template_config=PromptTemplateConfig(
template=prompt, template_format=template_format, allow_dangerously_set_content=True
),
arguments=KernelArguments(city=City("Seattle")),
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_complex_objects_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Prompt With Helper Functions
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(True, False, "semantic-kernel", sk_prompt, id="sk_inline_non_streaming"),
pytest.param(True, True, "semantic-kernel", sk_prompt, id="sk_inline_streaming"),
pytest.param(False, False, "semantic-kernel", sk_prompt, id="sk_non_inline_non_streaming"),
pytest.param(False, True, "semantic-kernel", sk_prompt, id="sk_non_inline_streaming"),
pytest.param(
False,
False,
"handlebars",
hb_prompt,
id="hb_non_inline_non_streaming",
marks=pytest.mark.xfail(reason="Throws intermittent APIConnectionError errors"),
),
pytest.param(False, True, "handlebars", hb_prompt, id="hb_non_inline_streaming"),
pytest.param(False, False, "jinja2", j2_prompt, id="j2_non_inline_non_streaming"),
pytest.param(False, True, "jinja2", j2_prompt, id="j2_non_inline_streaming"),
],
)
async def test_prompt_with_helper_functions(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
func = KernelFunctionFromMethod(
method=kernel_function(
lambda: datetime.datetime(1989, 6, 4, 12, 11, 13, tzinfo=datetime.timezone.utc).strftime(
"%a, %d %b %Y %H:%M:%S GMT"
),
name="Now",
),
plugin_name="Time",
)
kernel.add_function(plugin_name="Time", function=func)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
prompt=prompt,
template_format=template_format,
prompt_template_config=PromptTemplateConfig(
template=prompt, template_format=template_format, allow_dangerously_set_content=True
),
arguments=KernelArguments(city="Seattle"),
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_helper_functions_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Prompt With Simple Variable
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(True, False, "semantic-kernel", sk_simple_prompt, id="sk_inline_non_streaming"),
pytest.param(True, True, "semantic-kernel", sk_simple_prompt, id="sk_inline_streaming"),
pytest.param(False, False, "semantic-kernel", sk_simple_prompt, id="sk_non_inline_non_streaming"),
pytest.param(False, True, "semantic-kernel", sk_simple_prompt, id="sk_non_inline_streaming"),
pytest.param(False, False, "handlebars", hb_simple_prompt, id="hb_non_inline_non_streaming"),
pytest.param(False, True, "handlebars", hb_simple_prompt, id="hb_non_inline_streaming"),
pytest.param(False, False, "jinja2", j2_simple_prompt, id="j2_non_inline_non_streaming"),
pytest.param(False, True, "jinja2", j2_simple_prompt, id="j2_non_inline_streaming"),
],
)
async def test_prompt_with_simple_variable(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
template_format=template_format,
prompt=prompt,
arguments=KernelArguments(city="Seattle"),
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_with_simple_variable_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test Simple Prompt
@pytest.mark.parametrize(
"is_inline, is_streaming, template_format, prompt",
[
pytest.param(True, False, "semantic-kernel", simple_prompt, id="sk_inline_non_streaming"),
pytest.param(True, True, "semantic-kernel", simple_prompt, id="sk_inline_streaming"),
pytest.param(False, False, "semantic-kernel", simple_prompt, id="sk_non_inline_non_streaming"),
pytest.param(False, True, "semantic-kernel", simple_prompt, id="sk_non_inline_streaming"),
pytest.param(False, False, "handlebars", simple_prompt, id="hb_non_inline_non_streaming"),
pytest.param(False, True, "handlebars", simple_prompt, id="hb_non_inline_streaming"),
pytest.param(False, False, "jinja2", simple_prompt, id="j2_non_inline_non_streaming"),
pytest.param(False, True, "jinja2", simple_prompt, id="j2_non_inline_streaming"),
],
)
async def test_simple_prompt(
is_inline, is_streaming, template_format, prompt, async_clients: tuple[AsyncOpenAI, LoggingAsyncClient]
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel = Kernel()
kernel.add_service(ai_service)
await run_prompt(
kernel=kernel,
is_inline=is_inline,
is_streaming=is_streaming,
template_format=template_format,
prompt=prompt,
)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", "prompt_simple_expected.json")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test YAML Prompts
@pytest.mark.parametrize(
"is_streaming, prompt_path, expected_result_path",
[
pytest.param(
False, "simple_prompt_test.yaml", "prompt_simple_expected.json", id="simple_prompt_test_non_streaming"
),
pytest.param(True, "simple_prompt_test.yaml", "prompt_simple_expected.json", id="simple_prompt_test_streaming"),
pytest.param(
False,
"prompt_with_chat_roles_test_hb.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_hb_non_streaming",
),
pytest.param(
True,
"prompt_with_chat_roles_test_hb.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_hb_streaming",
),
pytest.param(
False,
"prompt_with_chat_roles_test_j2.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_j2_non_streaming",
),
pytest.param(
True,
"prompt_with_chat_roles_test_j2.yaml",
"prompt_with_chat_roles_expected.json",
id="prompt_with_chat_roles_test_j2_streaming",
),
pytest.param(
False,
"prompt_with_simple_variable_test.yaml",
"prompt_with_simple_variable_expected.json",
id="prompt_with_simple_variable_non_streaming",
),
pytest.param(
True,
"prompt_with_simple_variable_test.yaml",
"prompt_with_simple_variable_expected.json",
id="prompt_with_simple_variable_streaming",
),
],
)
async def test_yaml_prompt(
is_streaming,
prompt_path,
expected_result_path,
kernel: Kernel,
async_clients: tuple[AsyncOpenAI, LoggingAsyncClient],
):
client, logging_async_client = async_clients
ai_service = OpenAIChatCompletion(
service_id="default",
ai_model_id=OPENAI_MODEL_ID,
async_client=client,
)
kernel.add_service(ai_service)
prompt_dir = os.path.join(os.path.dirname(__file__), "data", f"{prompt_path}")
with open(prompt_dir) as f:
prompt_str = f.read()
function = KernelFunctionFromPrompt.from_yaml(yaml_str=prompt_str, plugin_name="yaml_plugin")
await run_function(kernel=kernel, is_streaming=is_streaming, function=function)
request_content = logging_async_client.request_content
assert request_content is not None
response_content = logging_async_client.response_content
assert response_content is not None
obtained_object = json.loads(request_content)
assert obtained_object is not None
data_directory = os.path.join(os.path.dirname(__file__), "data", f"{expected_result_path}")
with open(data_directory) as f:
expected = f.read()
expected_object = json.loads(expected)
assert expected_object is not None
if is_streaming:
expected_object["stream"] = True
expected_object["stream_options"] = {"include_usage": True}
assert obtained_object == expected_object
# endregion
# region Test OpenAPI Plugin Load
async def setup_openapi_function_call(kernel: Kernel, function_name, arguments):
from semantic_kernel.connectors.openapi_plugin import OpenAPIFunctionExecutionParameters
openapi_spec_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", "light_bulb_api.json")
request_details = None
async def mock_request(request: httpx.Request):
nonlocal request_details
if request.method in ["POST", "PUT"]:
request_body = None
if request.content:
request_body = request.content.decode()
elif request.stream:
try:
stream_content = await request.stream.read()
if stream_content:
request_body = stream_content.decode()
except Exception:
request_body = None
request_details = {
"method": request.method,
"url": str(request.url),
"body": request_body,
"headers": dict(request.headers),
}
else:
request_details = {"method": request.method, "url": str(request.url), "params": dict(request.url.params)}
transport = httpx.MockTransport(mock_request)
async with httpx.AsyncClient(transport=transport) as client:
plugin = kernel.add_plugin_from_openapi(
plugin_name="LightControl",
openapi_document_path=openapi_spec_file,
execution_settings=OpenAPIFunctionExecutionParameters(
http_client=client,
server_url_validation_allowed_base_urls=["https://127.0.0.1"],
),
)
assert plugin is not None
with contextlib.suppress(Exception):
# It is expected that the API call will fail, ignore
await run_function(kernel=kernel, is_streaming=False, function=plugin[function_name], arguments=arguments)
return request_details
async def test_openapi_get_lights(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="GetLights", arguments=KernelArguments(roomId=1)
)
assert request_content is not None
assert request_content.get("method") == "GET"
assert request_content.get("url") == "https://127.0.0.1/Lights?roomId=1"
assert request_content.get("params") == {"roomId": "1"}
async def test_openapi_get_light_by_id(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="GetLightById", arguments=KernelArguments(id=1)
)
assert request_content is not None
assert request_content.get("method") == "GET"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
async def test_openapi_delete_light_by_id(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="DeleteLightById", arguments=KernelArguments(id=1)
)
assert request_content is not None
assert request_content.get("method") == "DELETE"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
async def test_openapi_create_lights(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="CreateLights", arguments=KernelArguments(roomId=1, lightName="disco")
)
assert request_content is not None
assert request_content.get("method") == "POST"
assert request_content.get("url") == "https://127.0.0.1/Lights?roomId=1&lightName=disco"
async def test_openapi_put_light_by_id(kernel: Kernel):
request_content = await setup_openapi_function_call(
kernel, function_name="PutLightById", arguments=KernelArguments(id=1, hexColor="11EE11")
)
assert request_content is not None
assert request_content.get("method") == "PUT"
assert request_content.get("url") == "https://127.0.0.1/Lights/1"
assert request_content.get("body") == '{"hexColor":"11EE11"}'
# endregion
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import pytest
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from tests.integration.embeddings.test_embedding_service_base import (
EmbeddingServiceTestBase,
google_ai_setup,
mistral_ai_setup,
ollama_setup,
vertex_ai_setup,
)
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs, output_dimensionality",
[
pytest.param(
"openai",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="openai",
),
pytest.param(
"azure",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="azure",
),
pytest.param(
"azure_custom_client",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="azure_custom_client",
),
pytest.param(
"azure_ai_inference",
{},
1536, # text-embedding-ada-002 doesn't support custom output dimensionality
id="azure_ai_inference",
),
pytest.param(
"mistral_ai",
{},
1024,
marks=pytest.mark.skipif(not mistral_ai_setup, reason="Mistral AI environment variables not set"),
id="mistral_ai",
),
pytest.param(
"hugging_face",
{},
384,
id="hugging_face",
),
pytest.param(
"ollama",
{},
768,
marks=(
pytest.mark.skipif(not ollama_setup, reason="Ollama not setup"),
pytest.mark.ollama,
),
id="ollama",
),
pytest.param(
"google_ai",
{"output_dimensionality": 10},
10,
marks=pytest.mark.skipif(not google_ai_setup, reason="Google AI environment variables not set"),
id="google_ai",
),
pytest.param(
"vertex_ai",
{"output_dimensionality": 10},
10,
marks=(
pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
pytest.mark.timeout(300), # Vertex AI may take longer time
),
id="vertex_ai",
),
pytest.param(
"bedrock_amazon_titan-v1",
{},
1536, # This model doesn't support custom output dimensionality
id="bedrock_amazon_titan-v1",
),
pytest.param(
"bedrock_amazon_titan-v2",
{"extension_data": {"dimensions": 256}},
256,
id="bedrock_amazon_titan-v2",
),
pytest.param(
"bedrock_cohere",
{},
1024,
id="bedrock_cohere",
),
],
)
class TestEmbeddingService(EmbeddingServiceTestBase):
"""Test embedding service with memory.
This tests if the embedding service can be used with the semantic memory.
"""
async def test_embedding_service(
self,
service_id,
services: dict[str, tuple[EmbeddingGeneratorBase, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
output_dimensionality: int,
):
embedding_generator, settings_type = services[service_id]
embeddings = await embedding_generator.generate_embeddings(
texts=["Hello, world!", "Hello, universe!"],
settings=settings_type(**execution_settings_kwargs),
)
assert embeddings.shape == (2, output_dimensionality)
@@ -0,0 +1,129 @@
# Copyright (c) Microsoft. All rights reserved.
from importlib import util
import pytest
from azure.ai.inference.aio import EmbeddingsClient
from azure.identity import AzureCliCredential
from openai import AsyncAzureOpenAI
from semantic_kernel.connectors.ai.azure_ai_inference import (
AzureAIInferenceEmbeddingPromptExecutionSettings,
AzureAIInferenceTextEmbedding,
)
from semantic_kernel.connectors.ai.bedrock import BedrockEmbeddingPromptExecutionSettings, BedrockTextEmbedding
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.google.google_ai import (
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAITextEmbedding,
)
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextEmbedding
from semantic_kernel.connectors.ai.mistral_ai import MistralAITextEmbedding
from semantic_kernel.connectors.ai.ollama import OllamaEmbeddingPromptExecutionSettings, OllamaTextEmbedding
from semantic_kernel.connectors.ai.open_ai import (
AzureOpenAISettings,
AzureTextEmbedding,
OpenAIEmbeddingPromptExecutionSettings,
OpenAITextEmbedding,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
from tests.utils import is_service_setup_for_testing
hugging_face_setup = util.find_spec("torch") is not None
# Make sure all services are setup for before running the tests
# The following exceptions apply:
# 1. OpenAI and Azure OpenAI services are always setup for testing.
azure_openai_setup = True
# 2. The current Hugging Face service don't require any environment variables.
# 3. Bedrock services don't use API keys and model providers are tested individually,
# so no environment variables are required.
mistral_ai_setup: bool = is_service_setup_for_testing(
["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"], raise_if_not_set=False
) # We don't have a MistralAI deployment
google_ai_setup: bool = is_service_setup_for_testing(["GOOGLE_AI_API_KEY", "GOOGLE_AI_EMBEDDING_MODEL_ID"])
vertex_ai_setup: bool = is_service_setup_for_testing([
"GOOGLE_AI_CLOUD_PROJECT_ID",
"GOOGLE_AI_EMBEDDING_MODEL_ID",
"GOOGLE_AI_CLOUD_REGION",
])
ollama_setup: bool = is_service_setup_for_testing(["OLLAMA_EMBEDDING_MODEL_ID"])
# When testing Bedrock, after logging into AWS CLI this has been set, so we can use it to check if the service is setup
bedrock_setup: bool = is_service_setup_for_testing(["AWS_DEFAULT_REGION"], raise_if_not_set=False)
class EmbeddingServiceTestBase:
@pytest.fixture(scope="class")
def services(self) -> dict[str, tuple[EmbeddingGeneratorBase | None, type[PromptExecutionSettings]]]:
azure_openai_setup = True
credential = AzureCliCredential()
azure_openai_settings = AzureOpenAISettings()
endpoint = str(azure_openai_settings.endpoint)
deployment_name = azure_openai_settings.embedding_deployment_name
ad_token = get_entra_auth_token(credential, azure_openai_settings.token_endpoint)
if not ad_token:
azure_openai_setup = False
api_version = azure_openai_settings.api_version
azure_custom_client = None
azure_ai_inference_client = None
if azure_openai_setup:
azure_custom_client = AzureTextEmbedding(
async_client=AsyncAzureOpenAI(
azure_endpoint=endpoint,
azure_deployment=deployment_name,
azure_ad_token=ad_token,
api_version=api_version,
default_headers={"Test-User-X-ID": "test"},
),
credential=credential,
)
azure_ai_inference_client = AzureAIInferenceTextEmbedding(
ai_model_id=deployment_name,
client=EmbeddingsClient(
endpoint=f"{endpoint.strip('/')}/openai/deployments/{deployment_name}",
credential=credential,
credential_scopes=["https://cognitiveservices.azure.com/.default"],
),
)
return {
"openai": (OpenAITextEmbedding(), OpenAIEmbeddingPromptExecutionSettings),
"azure": (
AzureTextEmbedding(credential=credential) if azure_openai_setup else None,
OpenAIEmbeddingPromptExecutionSettings,
),
"azure_custom_client": (azure_custom_client, OpenAIEmbeddingPromptExecutionSettings),
"azure_ai_inference": (azure_ai_inference_client, AzureAIInferenceEmbeddingPromptExecutionSettings),
"mistral_ai": (
MistralAITextEmbedding() if mistral_ai_setup else None,
PromptExecutionSettings,
),
"hugging_face": (
HuggingFaceTextEmbedding(ai_model_id="sentence-transformers/all-MiniLM-L6-v2")
if hugging_face_setup
else None,
PromptExecutionSettings,
),
"ollama": (OllamaTextEmbedding() if ollama_setup else None, OllamaEmbeddingPromptExecutionSettings),
"google_ai": (
GoogleAITextEmbedding() if google_ai_setup else None,
GoogleAIEmbeddingPromptExecutionSettings,
),
"vertex_ai": (
GoogleAITextEmbedding(use_vertexai=True) if vertex_ai_setup else None,
GoogleAIEmbeddingPromptExecutionSettings,
),
"bedrock_amazon_titan-v1": (
BedrockTextEmbedding(model_id="amazon.titan-embed-text-v1") if bedrock_setup else None,
BedrockEmbeddingPromptExecutionSettings,
),
"bedrock_amazon_titan-v2": (
BedrockTextEmbedding(model_id="amazon.titan-embed-text-v2:0") if bedrock_setup else None,
BedrockEmbeddingPromptExecutionSettings,
),
"bedrock_cohere": (
BedrockTextEmbedding(model_id="cohere.embed-english-v3") if bedrock_setup else None,
BedrockEmbeddingPromptExecutionSettings,
),
}
@@ -0,0 +1,169 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
import pytest
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory
from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore
from tests.integration.embeddings.test_embedding_service_base import (
EmbeddingServiceTestBase,
google_ai_setup,
mistral_ai_setup,
ollama_setup,
vertex_ai_setup,
)
pytestmark = pytest.mark.parametrize(
"service_id, execution_settings_kwargs",
[
pytest.param(
"openai",
{},
id="openai",
),
pytest.param(
"azure",
{},
id="azure",
),
pytest.param(
"azure_custom_client",
{},
id="azure_custom_client",
),
pytest.param(
"azure_ai_inference",
{},
id="azure_ai_inference",
),
pytest.param(
"mistral_ai",
{},
marks=pytest.mark.skipif(not mistral_ai_setup, reason="Mistral AI environment variables not set"),
id="mistral_ai",
),
pytest.param(
"hugging_face",
{},
id="hugging_face",
),
pytest.param(
"ollama",
{},
marks=(
pytest.mark.skipif(not ollama_setup, reason="Ollama environment variables not set"),
pytest.mark.ollama,
),
id="ollama",
),
pytest.param(
"google_ai",
{},
marks=pytest.mark.skipif(not google_ai_setup, reason="Google AI environment variables not set"),
id="google_ai",
),
pytest.param(
"vertex_ai",
{},
marks=(
pytest.mark.skipif(not vertex_ai_setup, reason="Vertex AI environment variables not set"),
pytest.mark.timeout(300), # Vertex AI may take longer time
),
id="vertex_ai",
),
pytest.param(
"bedrock_amazon_titan-v1",
{},
id="bedrock_amazon_titan-v1",
),
pytest.param(
"bedrock_amazon_titan-v2",
{},
marks=pytest.mark.skip(reason="This is known to fail to get the correct answer for 'What are birds?'"),
id="bedrock_amazon_titan-v2",
),
pytest.param(
"bedrock_cohere",
{},
id="bedrock_cohere",
),
],
)
class TestEmbeddingServiceWithMemory(EmbeddingServiceTestBase):
"""Test embedding service with memory.
This tests if the embedding service can be used with the semantic memory.
"""
async def test_embedding_service(
self,
service_id,
services: dict[str, tuple[EmbeddingGeneratorBase, type[PromptExecutionSettings]]],
execution_settings_kwargs: dict[str, Any],
):
embedding_generator, settings_type = services[service_id]
if embedding_generator is None:
pytest.skip(f"Service {service_id} not set up")
memory = SemanticTextMemory(
storage=VolatileMemoryStore(),
embeddings_generator=embedding_generator,
)
# Add some documents to the semantic memory
embeddings_kwargs = {"settings": settings_type(**execution_settings_kwargs)}
await memory.save_information(
"test",
id="info1",
text="Sharks are fish.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info2",
text="Whales are mammals.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info3",
text="Penguins are birds.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info4",
text="Dolphins are mammals.",
embeddings_kwargs=embeddings_kwargs,
)
await memory.save_information(
"test",
id="info5",
text="Flies are insects.",
embeddings_kwargs=embeddings_kwargs,
)
# Search for documents
query = "What are mammals?"
result = await memory.search("test", query, limit=2, min_relevance_score=0.0)
assert "mammals." in result[0].text
assert "mammals." in result[1].text
query = "What are fish?"
result = await memory.search("test", query, limit=1, min_relevance_score=0.0)
assert result[0].text == "Sharks are fish."
query = "What are insects?"
result = await memory.search("test", query, limit=1, min_relevance_score=0.0)
assert result[0].text == "Flies are insects."
query = "What are birds?"
result = await memory.search("test", query, limit=1, min_relevance_score=0.0)
assert result[0].text == "Penguins are birds."
@@ -0,0 +1,25 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class EmailPluginFake:
@kernel_function(
description="Given an email address and message body, send an email",
name="SendEmail",
)
def send_email(self, input: str) -> str:
return f"Sent email to: . Body: {input}"
@kernel_function(
description="Lookup an email address for a person given a name",
name="GetEmailAddress",
)
def get_email_address(self, input: str) -> str:
if input == "":
return "johndoe1234@example.com"
return f"{input}@example.com"
@kernel_function(description="Write a short poem for an e-mail", name="WritePoem")
def write_poem(self, input: str) -> str:
return f"Roses are red, violets are blue, {input} is hard, so is this test."
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
# TODO: this fake plugin is temporal usage.
# C# supports import plugin from samples dir by using test helper and python should do the same
# `semantic-kernel/dotnet/src/IntegrationTests/TestHelpers.cs`
class FunPluginFake:
@kernel_function(
description="Write a joke",
name="WriteJoke",
)
def write_joke(self) -> str:
return "WriteJoke"
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.functions.kernel_function_decorator import kernel_function
# TODO: this fake plugin is temporal usage.
# C# supports import plugin from samples dir by using test helper and python should do the same
# `semantic-kernel/dotnet/src/IntegrationTests/TestHelpers.cs`
class SummarizePluginFake:
@kernel_function(
description="Summarize",
name="Summarize",
)
def translate(self) -> str:
return "Summarize"
@@ -0,0 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated
from semantic_kernel.functions import kernel_function
# TODO: this fake plugin is temporal usage.
# C# supports import plugin from samples dir by using test helper and python should do the same
# `semantic-kernel/dotnet/src/IntegrationTests/TestHelpers.cs`
class WriterPluginFake:
@kernel_function(
description="Translate",
name="Translate",
)
def translate(self, language: str) -> str:
return f"Translate: {language}"
@kernel_function(name="NovelOutline")
def write_novel_outline(
self,
input: Annotated[str, "The input of the function"],
name: Annotated[str, "The name of the function"] = "endMarker",
description: Annotated[str, "The marker to use to end each chapter"] = "Write an outline for a novel.",
default_value: Annotated[str, "The default value used for the function"] = "<!--===ENDPART===-->",
) -> str:
return f"Novel outline: {input}"
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
from semantic_kernel.kernel import Kernel
def test_kernel_deep_copy_fail_with_services():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
with pytest.raises(TypeError):
# This will fail because OpenAIChatCompletion is not serializable, more specifically,
# the client is not serializable
kernel.model_copy(deep=True)
async def test_kernel_clone():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
kernel_clone = kernel.clone()
assert kernel_clone is not None
assert kernel_clone.services is not None and len(kernel_clone.services) > 0
function_result = await kernel.invoke_prompt("Hello World")
assert function_result is not None
assert function_result.value is not None
assert len(str(function_result)) > 0
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from typing import TYPE_CHECKING
from semantic_kernel.connectors.mcp import MCPStdioPlugin
from semantic_kernel.functions.kernel_arguments import KernelArguments
if TYPE_CHECKING:
from semantic_kernel import Kernel
async def test_from_mcp(kernel: "Kernel"):
mcp_server_path = os.path.join(
os.path.dirname(__file__), "../../assets/test_plugins", "TestMCPPlugin", "mcp_server.py"
)
async with MCPStdioPlugin(
name="TestMCPPlugin",
command="python",
args=[mcp_server_path],
) as plugin:
assert plugin is not None
assert plugin.name == "TestMCPPlugin"
loaded_plugin = kernel.add_plugin(plugin)
assert loaded_plugin is not None
assert loaded_plugin.name == "TestMCPPlugin"
assert len(loaded_plugin.functions) == 2
result = await loaded_plugin.functions["echo_tool"].invoke(kernel, arguments=KernelArguments(message="test"))
assert "Tool echo: test" in result.value[0].text
result = await loaded_plugin.functions["echo_prompt"].invoke(kernel, arguments=KernelArguments(message="test"))
assert "test" in result.value[0].content
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft. All rights reserved.
from dataclasses import field
from typing import Annotated, Any
from uuid import uuid4
from pydantic import BaseModel
from pytest import fixture
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
@fixture
def data_record() -> dict[str, Any]:
return {
"id": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"description": "This is a test record",
"product_type": "test",
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
}
@fixture
def record_type() -> type:
@vectorstoremodel
class TestDataModelType(BaseModel):
vector: Annotated[
list[float] | None,
VectorStoreField(
"vector",
index_kind="flat",
dimensions=5,
distance_function="cosine_similarity",
type="float",
),
] = None
id: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
product_type: Annotated[str, VectorStoreField("data")] = "N/A"
description: Annotated[
str, VectorStoreField("data", has_embedding=True, embedding_property_name="vector", type="str")
] = "N/A"
return TestDataModelType
@fixture
def data_record_with_key_as_key_field() -> dict[str, Any]:
return {
"key": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"description": "This is a test record",
"product_type": "test",
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
}
@fixture
def record_type_with_key_as_key_field() -> type:
@vectorstoremodel
class TestDataModelType(BaseModel):
vector: Annotated[
list[float] | None,
VectorStoreField(
"vector",
index_kind="flat",
dimensions=5,
distance_function="cosine_similarity",
type="float",
),
] = None
key: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
product_type: Annotated[str, VectorStoreField("data")] = "N/A"
description: Annotated[
str, VectorStoreField("data", has_embedding=True, embedding_property_name="vector", type="str")
] = "N/A"
return TestDataModelType
@@ -0,0 +1,231 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import platform
from collections.abc import Callable
from typing import Any
import pytest
from azure.cosmos.aio import CosmosClient
from azure.cosmos.partition_key import PartitionKey
from semantic_kernel.connectors.azure_cosmos_db import CosmosNoSqlCompositeKey, CosmosNoSqlStore
from semantic_kernel.data.vector import VectorStore
from semantic_kernel.exceptions.memory_connector_exceptions import MemoryConnectorException
from tests.integration.memory.vector_store_test_base import VectorStoreTestBase
@pytest.mark.skipif(
platform.system() != "Windows",
reason="The Azure Cosmos DB Emulator is only available on Windows.",
)
class TestCosmosDBNoSQL(VectorStoreTestBase):
"""Test Cosmos DB NoSQL store functionality."""
async def test_list_collection_names(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
):
"""Test list collection names."""
async with stores["azure_cosmos_db_no_sql"]() as store:
assert await store.list_collection_names() == []
collection_name = "list_collection_names"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
await collection.ensure_collection_exists()
collection_names = await store.list_collection_names()
assert collection_name in collection_names
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
collection_names = await store.list_collection_names()
assert collection_name not in collection_names
async def test_collection_not_created(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test get without collection."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "collection_not_created"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
assert await collection.collection_exists() is False
with pytest.raises(
MemoryConnectorException, match="The collection does not exist yet. Create the collection first."
):
await collection.upsert(record_type(**data_record))
with pytest.raises(
MemoryConnectorException, match="The collection does not exist yet. Create the collection first."
):
await collection.get(data_record["id"])
with pytest.raises(MemoryConnectorException):
await collection.delete(data_record["id"])
with pytest.raises(MemoryConnectorException, match="Container could not be deleted."):
await collection.ensure_collection_deleted()
async def test_custom_partition_key(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test custom partition key."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "custom_partition_key"
collection = store.get_collection(
collection_name=collection_name,
record_type=record_type,
partition_key=PartitionKey(path="/product_type"),
)
composite_key = CosmosNoSqlCompositeKey(key=data_record["id"], partition_key=data_record["product_type"])
# Upsert
await collection.ensure_collection_exists()
await collection.upsert(record_type(**data_record))
# Verify
record = await collection.get(composite_key)
assert record is not None
assert isinstance(record, record_type)
# Remove
await collection.delete(composite_key)
record = await collection.get(composite_key)
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_get_include_vector(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test get with include_vector."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "get_include_vector"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
# Upsert
await collection.ensure_collection_exists()
await collection.upsert(record_type(**data_record))
# Verify
record = await collection.get(data_record["id"], include_vectors=True)
assert record is not None
assert isinstance(record, record_type)
assert record.vector == data_record["vector"]
# Remove
await collection.delete(data_record["id"])
record = await collection.get(data_record["id"])
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_get_not_include_vector(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type: type,
data_record: dict[str, Any],
):
"""Test get with include_vector."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "get_not_include_vector"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
# Upsert
await collection.ensure_collection_exists()
await collection.upsert(record_type(**data_record))
# Verify
record = await collection.get(data_record["id"], include_vectors=False)
assert record is not None
assert isinstance(record, record_type)
assert record.vector is None
# Remove
await collection.delete(data_record["id"])
record = await collection.get(data_record["id"])
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_collection_with_key_as_key_field(
self,
stores: dict[str, Callable[[], VectorStore]],
record_type_with_key_as_key_field: type,
data_record_with_key_as_key_field: dict[str, Any],
):
"""Test collection with key as key field."""
async with stores["azure_cosmos_db_no_sql"]() as store:
collection_name = "collection_with_key_as_key_field"
collection = store.get_collection(
collection_name=collection_name, record_type=record_type_with_key_as_key_field
)
# Upsert
await collection.ensure_collection_exists()
result = await collection.upsert(record_type_with_key_as_key_field(**data_record_with_key_as_key_field))
assert data_record_with_key_as_key_field["key"] == result
# Verify
record = await collection.get(data_record_with_key_as_key_field["key"])
assert record is not None
assert isinstance(record, record_type_with_key_as_key_field)
assert record.key == data_record_with_key_as_key_field["key"]
# Remove
await collection.delete(data_record_with_key_as_key_field["key"])
record = await collection.get(data_record_with_key_as_key_field["key"])
assert record is None
# Remove collection
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
async def test_custom_client(
self,
record_type: type,
):
"""Test list collection names."""
url = os.environ.get("AZURE_COSMOS_DB_NO_SQL_URL")
key = os.environ.get("AZURE_COSMOS_DB_NO_SQL_KEY")
async with (
CosmosClient(url, key) as custom_client,
CosmosNoSqlStore(
database_name="test_database",
cosmos_client=custom_client,
create_database=True,
) as store,
):
assert await store.list_collection_names() == []
collection_name = "list_collection_names"
collection = store.get_collection(collection_name=collection_name, record_type=record_type)
await collection.ensure_collection_exists()
collection_names = await store.list_collection_names()
assert collection_name in collection_names
await collection.ensure_collection_deleted()
assert await collection.collection_exists() is False
collection_names = await store.list_collection_names()
assert collection_name not in collection_names
@@ -0,0 +1,145 @@
# Copyright (c) Microsoft. All rights reserved.
import numpy as np
RAW_RECORD_LIST = {
"id": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"content": "test content",
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
}
RAW_RECORD_ARRAY = {
"id": "e6103c03-487f-4d7d-9c23-4723651c17f4",
"content": "test content",
"vector": np.array([0.1, 0.2, 0.3, 0.4, 0.5]),
}
# PANDAS_RECORD_DEFINITION = VectorStoreRecordDefinition(
# fields={
# "vector": VectorStoreRecordVectorField(
# name="vector",
# index_kind="hnsw",
# dimensions=5,
# distance_function="cosine_similarity",
# property_type="float",
# ),
# "id": VectorStoreRecordKeyField(name="id"),
# "content": VectorStoreRecordDataField(
# name="content", has_embedding=True, embedding_property_name="vector", property_type="str"
# ),
# },
# container_mode=True,
# to_dict=lambda x: x.to_dict(orient="records"),
# from_dict=lambda x, **_: pd.DataFrame(x),
# )
# A Pandas record definition with flat index kind
# PANDAS_RECORD_DEFINITION_FLAT = VectorStoreRecordDefinition(
# fields={
# "vector": VectorStoreRecordVectorField(
# name="vector",
# index_kind="flat",
# dimensions=5,
# distance_function="cosine_similarity",
# property_type="float",
# ),
# "id": VectorStoreRecordKeyField(name="id"),
# "content": VectorStoreRecordDataField(
# name="content", has_embedding=True, embedding_property_name="vector", property_type="str"
# ),
# },
# container_mode=True,
# to_dict=lambda x: x.to_dict(orient="records"),
# from_dict=lambda x, **_: pd.DataFrame(x),
# )
# @vectorstoremodel
# @dataclass
# class TestDataModelArray(distance_function: str):
# """A data model where the vector is a numpy array."""
# vector: Annotated[
# np.ndarray | None,
# VectorStoreRecordVectorField(
# index_kind="hnsw",
# dimensions=5,
# distance_function=distance_function,
# property_type="float",
# serialize_function=np.ndarray.tolist,
# deserialize_function=np.array,
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
# @vectorstoremodel
# @dataclass
# class TestDataModelArrayFlat(distance_function:str):
# """A data model where the vector is a numpy array and the index kind is IndexKind.Flat."""
# vector: Annotated[
# np.ndarray | None,
# VectorStoreRecordVectorField(
# index_kind="flat",
# dimensions=5,
# distance_function=distance_function,
# property_type="float",
# serialize_function=np.ndarray.tolist,
# deserialize_function=np.array,
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
# @vectorstoremodel
# @dataclass
# class TestDataModelList(distance_function: str):
# """A data model where the vector is a list."""
# vector: Annotated[
# list[float] | None,
# VectorStoreRecordVectorField(
# index_kind="hnsw",
# dimensions=5,
# distance_function=distance_function,
# property_type="float",
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
# @vectorstoremodel
# @dataclass
# class TestDataModelListFlat:
# """A data model where the vector is a list and the index kind is IndexKind.Flat."""
# vector: Annotated[
# list[float] | None,
# VectorStoreRecordVectorField(
# index_kind="flat",
# dimensions=5,
# distance_function="cosine_similarity",
# property_type="float",
# ),
# ] = None
# other: str | None = None
# id: Annotated[str, VectorStoreRecordKeyField()] = field(default_factory=lambda: str(uuid4()))
# content: Annotated[
# str, VectorStoreRecordDataField(has_embedding=True, embedding_property_name="vector", property_type="str")
# ] = "content1"
@@ -0,0 +1,259 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
from collections.abc import AsyncGenerator, Sequence
from contextlib import asynccontextmanager
from typing import Annotated, Any
import pandas as pd
import pytest
import pytest_asyncio
from pydantic import BaseModel
from semantic_kernel.connectors.postgres import PostgresCollection, PostgresSettings, PostgresStore
from semantic_kernel.data.vector import (
DistanceFunction,
IndexKind,
VectorStoreCollectionDefinition,
VectorStoreField,
vectorstoremodel,
)
from semantic_kernel.exceptions.memory_connector_exceptions import (
MemoryConnectorConnectionException,
MemoryConnectorInitializationError,
)
try:
import psycopg # noqa: F401
import psycopg_pool # noqa: F401
psycopg_pool_installed = True
except ImportError:
psycopg_pool_installed = False
pg_settings: PostgresSettings = PostgresSettings()
try:
connection_params_present = any(pg_settings.get_connection_args().values())
except MemoryConnectorInitializationError:
connection_params_present = False
pytestmark = pytest.mark.skipif(
not (psycopg_pool_installed or connection_params_present),
reason="psycopg_pool is not installed" if not psycopg_pool_installed else "No connection parameters provided",
)
@vectorstoremodel
class SimpleDataModel(BaseModel):
id: Annotated[int, VectorStoreField("key")]
embedding: Annotated[
list[float] | str | None,
VectorStoreField(
"vector",
index_kind=IndexKind.HNSW,
dimensions=3,
distance_function=DistanceFunction.COSINE_SIMILARITY,
),
] = None
data: Annotated[
dict[str, Any],
VectorStoreField("data", type="JSONB"),
]
def model_post_init(self, context: Any) -> None:
if self.embedding is None:
self.embedding = self.data
def DataModelPandas(record) -> tuple:
definition = VectorStoreCollectionDefinition(
fields=[
VectorStoreField(
"vector",
name="embedding",
index_kind="hnsw",
dimensions=3,
distance_function="cosine_similarity",
type="float",
),
VectorStoreField("key", name="id", type="int"),
VectorStoreField("data", name="data", type="dict"),
],
container_mode=True,
to_dict=lambda x: x.to_dict(orient="records"),
from_dict=lambda x, **_: pd.DataFrame(x),
)
df = pd.DataFrame([record])
return definition, df
@pytest_asyncio.fixture
async def vector_store() -> AsyncGenerator[PostgresStore, None]:
try:
async with await pg_settings.create_connection_pool() as pool:
yield PostgresStore(connection_pool=pool)
except MemoryConnectorConnectionException:
pytest.skip("Postgres connection not available")
yield None
return
@asynccontextmanager
async def create_simple_collection(
vector_store: PostgresStore,
) -> AsyncGenerator[PostgresCollection[int, SimpleDataModel], None]:
"""Returns a collection with a unique name that is deleted after the context.
This can be moved to use a fixture with scope=function and loop_scope=session
after upgrade to pytest-asyncio 0.24. With the current version, the fixture
would both cache and use the event loop of the declared scope.
"""
suffix = str(uuid.uuid4()).replace("-", "")[:8]
collection_id = f"test_collection_{suffix}"
collection = vector_store.get_collection(collection_name=collection_id, record_type=SimpleDataModel)
assert isinstance(collection, PostgresCollection)
await collection.ensure_collection_exists()
try:
yield collection
finally:
await collection.ensure_collection_deleted()
def test_create_store(vector_store):
assert vector_store is not None
assert vector_store.connection_pool is not None
async def test_ensure_collection_exists_exists_and_delete(vector_store: PostgresStore):
suffix = str(uuid.uuid4()).replace("-", "")[:8]
collection = vector_store.get_collection(collection_name=f"test_collection_{suffix}", record_type=SimpleDataModel)
does_exist_1 = await collection.collection_exists()
assert does_exist_1 is False
await collection.ensure_collection_exists()
does_exist_2 = await collection.collection_exists()
assert does_exist_2 is True
await collection.ensure_collection_deleted()
does_exist_3 = await collection.collection_exists()
assert does_exist_3 is False
async def test_list_collection_names(vector_store):
async with create_simple_collection(vector_store) as simple_collection:
simple_collection_id = simple_collection.collection_name
result = await vector_store.list_collection_names()
assert simple_collection_id in result
async def test_upsert_get_and_delete(vector_store: PostgresStore):
record = SimpleDataModel(id=1, embedding=[1.1, 2.2, 3.3], data={"key": "value"})
async with create_simple_collection(vector_store) as simple_collection:
result_before_upsert = await simple_collection.get(1)
assert result_before_upsert is None
await simple_collection.upsert(record)
result = await simple_collection.get(1)
assert result is not None
assert result.id == record.id
assert result.embedding == record.embedding
assert result.data == record.data
# Check that the table has an index
connection_pool = simple_collection.connection_pool
async with connection_pool.connection() as conn, conn.cursor() as cur:
await cur.execute(
"SELECT indexname FROM pg_indexes WHERE tablename = %s", (simple_collection.collection_name,)
)
rows = await cur.fetchall()
index_names = [index[0] for index in rows]
assert any("embedding_idx" in index_name for index_name in index_names)
await simple_collection.delete(1)
result_after_delete = await simple_collection.get(1)
assert result_after_delete is None
async def test_upsert_get_and_delete_pandas(vector_store):
record = SimpleDataModel(id=1, embedding=[1.1, 2.2, 3.3], data={"key": "value"})
definition, df = DataModelPandas(record.model_dump())
suffix = str(uuid.uuid4()).replace("-", "")[:8]
collection = vector_store.get_collection(
collection_name=f"test_collection_{suffix}",
record_type=pd.DataFrame,
definition=definition,
)
await collection.ensure_collection_exists()
try:
result_before_upsert = await collection.get(1)
assert result_before_upsert is None
await collection.upsert(df)
result: pd.DataFrame = await collection.get(1)
assert result is not None
row = result.iloc[0]
assert row.id == record.id
assert row.embedding == record.embedding
assert row.data == record.data
await collection.delete(1)
result_after_delete = await collection.get(1)
assert result_after_delete is None
finally:
await collection.ensure_collection_deleted()
async def test_upsert_get_and_delete_multiple(vector_store: PostgresStore):
async with create_simple_collection(vector_store) as simple_collection:
record1 = SimpleDataModel(id=1, embedding=[1.1, 2.2, 3.3], data={"key": "value"})
record2 = SimpleDataModel(id=2, embedding=[4.4, 5.5, 6.6], data={"key": "value"})
result_before_upsert = await simple_collection.get([1, 2])
assert result_before_upsert is None
await simple_collection.upsert([record1, record2])
# Test get for the two existing keys and one non-existing key;
# this should return only the two existing records.
result = await simple_collection.get([1, 2, 3])
assert result is not None
assert isinstance(result, Sequence)
assert len(result) == 2
assert result[0] is not None
assert result[0].id == record1.id
assert result[0].embedding == record1.embedding
assert result[0].data == record1.data
assert result[1] is not None
assert result[1].id == record2.id
assert result[1].embedding == record2.embedding
assert result[1].data == record2.data
await simple_collection.delete([1, 2])
result_after_delete = await simple_collection.get([1, 2])
assert result_after_delete is None
async def test_search(vector_store: PostgresStore):
async with create_simple_collection(vector_store) as simple_collection:
records = [
SimpleDataModel(id=1, embedding=[1.0, 0.0, 0.0], data={"key": "value1"}),
SimpleDataModel(id=2, embedding=[0.8, 0.2, 0.0], data={"key": "value2"}),
SimpleDataModel(id=3, embedding=[0.6, 0.0, 0.4], data={"key": "value3"}),
SimpleDataModel(id=4, embedding=[1.0, 1.0, 0.0], data={"key": "value4"}),
SimpleDataModel(id=5, embedding=[0.0, 1.0, 1.0], data={"key": "value5"}),
SimpleDataModel(id=6, embedding=[1.0, 0.0, 1.0], data={"key": "value6"}),
]
await simple_collection.upsert(records)
try:
search_results = await simple_collection.search(vector=[1.0, 0.0, 0.0], top=3, include_total_count=True)
assert search_results is not None
assert search_results.total_count == 3
assert {result.record.id async for result in search_results.results} == {1, 2, 3}
finally:
await simple_collection.delete([r.id for r in records])
@@ -0,0 +1,363 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import platform
from collections.abc import Callable
from typing import Any
import pandas as pd
import pytest
from semantic_kernel.connectors.redis import RedisCollectionTypes
from semantic_kernel.data.vector import VectorStore
from semantic_kernel.exceptions import MemoryConnectorConnectionException
from tests.integration.memory.data_records import RAW_RECORD_LIST
from tests.integration.memory.vector_store_test_base import VectorStoreTestBase
logger: logging.Logger = logging.getLogger(__name__)
class TestVectorStore(VectorStoreTestBase):
"""Test vector store functionality.
This only tests if the vector stores can upsert, get, and delete records.
"""
@pytest.mark.parametrize(
[
"store_id",
"collection_name",
"collection_options",
"record_type",
"definition",
"distance_function",
"index_kind",
"vector_property_type",
"dimensions",
"record",
],
[
# region Redis
pytest.param(
"redis",
"redis_json_list_data_model",
{"collection_type": RedisCollectionTypes.JSON},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_json_list_data_model",
),
pytest.param(
"redis",
"redis_json_pandas_data_model",
{"collection_type": RedisCollectionTypes.JSON},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_json_pandas_data_model",
),
pytest.param(
"redis",
"redis_hashset_list_data_model",
{"collection_type": RedisCollectionTypes.HASHSET},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_hashset_list_data_model",
),
pytest.param(
"redis",
"redis_hashset_pandas_data_model",
{"collection_type": RedisCollectionTypes.HASHSET},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="redis_hashset_pandas_data_model",
),
# endregion
# region Azure AI Search
pytest.param(
"azure_ai_search",
"azure_ai_search_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="azure_ai_search_list_data_model",
),
pytest.param(
"azure_ai_search",
"azure_ai_search_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="azure_ai_search_pandas_data_model",
),
# endregion
# region Qdrant
pytest.param(
"qdrant",
"qdrant_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_list_data_model",
),
pytest.param(
"qdrant",
"qdrant_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_pandas_data_model",
),
pytest.param(
"qdrant_in_memory",
"qdrant_in_memory_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_in_memory_list_data_model",
),
pytest.param(
"qdrant_in_memory",
"qdrant_in_memory_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_in_memory_pandas_data_model",
),
pytest.param(
"qdrant",
"qdrant_grpc_list_data_model",
{"prefer_grpc": True},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_grpc_list_data_model",
),
pytest.param(
"qdrant",
"qdrant_grpc_pandas_data_model",
{"prefer_grpc": True},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="qdrant_grpc_pandas_data_model",
),
# endregion
# region Weaviate
pytest.param(
"weaviate_local",
"weaviate_local_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Linux",
reason="The Weaviate docker image is only available on Linux"
" but some GitHubs job runs in a Windows container.",
),
id="weaviate_local_list_data_model",
),
pytest.param(
"weaviate_local",
"weaviate_local_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Linux",
reason="The Weaviate docker image is only available on Linux"
" but some GitHubs job runs in a Windows container.",
),
id="weaviate_local_pandas_data_model",
),
# endregion
# region Azure Cosmos DB
pytest.param(
"azure_cosmos_db_no_sql",
"azure_cosmos_db_no_sql_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
"flat",
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Windows",
reason="The Azure Cosmos DB Emulator is only available on Windows.",
),
id="azure_cosmos_db_no_sql_list_data_model",
),
pytest.param(
"azure_cosmos_db_no_sql",
"azure_cosmos_db_no_sql_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
"flat",
None,
5,
RAW_RECORD_LIST,
marks=pytest.mark.skipif(
platform.system() != "Windows",
reason="The Azure Cosmos DB Emulator is only available on Windows.",
),
id="azure_cosmos_db_no_sql_pandas_data_model",
),
# endregion
# region Chroma
pytest.param(
"chroma",
"chroma_list_data_model",
{},
"dataclass_vector_data_model",
None,
None,
None,
None,
5,
RAW_RECORD_LIST,
id="chroma_list_data_model",
),
pytest.param(
"chroma",
"chroma_pandas_data_model",
{},
pd.DataFrame,
"definition_pandas",
None,
None,
None,
5,
RAW_RECORD_LIST,
id="chroma_pandas_data_model",
),
# endregion
],
)
# region test function
async def test_vector_store(
self,
stores: dict[str, Callable[[], VectorStore]],
store_id: str,
collection_name: str,
collection_options: dict[str, Any],
record_type: str | type,
definition: str | None,
distance_function,
index_kind,
vector_property_type,
dimensions,
record: dict[str, Any],
request,
):
"""Test vector store functionality."""
if isinstance(record_type, str):
record_type = request.getfixturevalue(record_type)
if definition is not None:
definition = request.getfixturevalue(definition)
try:
async with (
stores[store_id]() as vector_store,
vector_store.get_collection(
record_type=record_type,
definition=definition,
collection_name=collection_name,
**collection_options,
) as collection,
):
try:
await collection.ensure_collection_deleted()
except Exception as exc:
logger.warning(f"Failed to delete collection: {exc}")
try:
await collection.ensure_collection_exists()
except Exception as exc:
pytest.fail(f"Failed to create collection: {exc}")
# Upsert record
await collection.upsert(record_type([record]) if record_type is pd.DataFrame else record_type(**record))
# Get record
result = await collection.get(record["id"])
assert result is not None
# Delete record
await collection.delete(record["id"])
# Get record again, expect None
result = await collection.get(record["id"])
assert result is None
try:
await collection.ensure_collection_deleted()
except Exception as exc:
pytest.fail(f"Failed to delete collection: {exc}")
except MemoryConnectorConnectionException as exc:
pytest.xfail(f"Failed to connect to store: {exc}")
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Callable
import pytest
from semantic_kernel.data.vector import VectorStore
def get_redis_store():
from semantic_kernel.connectors.redis import RedisStore
return RedisStore()
def get_azure_ai_search_store():
from semantic_kernel.connectors.azure_ai_search import AzureAISearchStore
return AzureAISearchStore()
def get_qdrant_store():
from semantic_kernel.connectors.qdrant import QdrantStore
return QdrantStore()
def get_qdrant_store_in_memory():
from semantic_kernel.connectors.qdrant import QdrantStore
return QdrantStore(location=":memory:")
def get_weaviate_store():
from semantic_kernel.connectors.weaviate import WeaviateStore
return WeaviateStore(local_host="localhost")
def get_azure_cosmos_db_no_sql_store():
from semantic_kernel.connectors.azure_cosmos_db import CosmosNoSqlStore
return CosmosNoSqlStore(database_name="test_database", create_database=True)
def get_chroma_store():
from semantic_kernel.connectors.chroma import ChromaStore
return ChromaStore()
class VectorStoreTestBase:
@pytest.fixture
def stores(self) -> dict[str, Callable[[], VectorStore]]:
"""Return a dictionary of vector stores to test."""
return {
"redis": get_redis_store,
"azure_ai_search": get_azure_ai_search_store,
"qdrant": get_qdrant_store,
"qdrant_in_memory": get_qdrant_store_in_memory,
"weaviate_local": get_weaviate_store,
"azure_cosmos_db_no_sql": get_azure_cosmos_db_no_sql_store,
"chroma": get_chroma_store,
}
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase
from semantic_kernel.contents import AudioContent
from tests.integration.text_to_audio.text_to_audio_test_base import TextToAudioTestBase, azure_setup
pytestmark = pytest.mark.parametrize(
"service_id, text",
[
pytest.param(
"openai",
"Hello World!",
id="openai",
),
pytest.param(
"azure_openai",
"Hello World!",
marks=pytest.mark.skipif(not azure_setup, reason="Azure Audio to Text not setup."),
id="azure_openai",
),
],
)
class TestTextToAudio(TextToAudioTestBase):
"""Test text-to-audio services."""
async def test_audio_to_text(
self,
services: dict[str, TextToAudioClientBase],
service_id: str,
text: str,
) -> None:
"""Test text-to-audio services.
Args:
services: text-to-audio services.
service_id: Service ID.
text: Text content.
"""
service = services[service_id]
result = await service.get_audio_content(text)
assert isinstance(result, AudioContent)
assert result.data is not None
@@ -0,0 +1,30 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai import AzureTextToAudio, OpenAITextToAudio
from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase
from tests.utils import is_service_setup_for_testing
# TTS model on Azure model is not available in regions at which we have chat completion models.
# Therefore, we need to use a different endpoint for testing.
azure_setup = is_service_setup_for_testing(["AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT"])
class TextToAudioTestBase:
"""Base class for testing text-to-audio services."""
@pytest.fixture(scope="module")
def services(self) -> dict[str, TextToAudioClientBase]:
"""Return text-to-audio services."""
return {
"openai": OpenAITextToAudio(),
"azure_openai": AzureTextToAudio(
endpoint=os.environ["AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT"], credential=AzureCliCredential()
)
if azure_setup
else None,
}
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
from tests.integration.text_to_image.text_to_image_test_base import TextToImageTestBase
pytestmark = pytest.mark.parametrize(
"service_id, prompt",
[
pytest.param(
"openai",
"A cute tuxedo cat driving a race car.",
id="openai",
),
pytest.param(
"azure_openai",
"A cute tuxedo cat driving a race car.",
id="azure_openai",
marks=[
pytest.mark.xfail(
reason="Temporary failure due to Internal Server Error (500) from Azure OpenAI.",
),
],
),
],
)
class TestTextToImage(TextToImageTestBase):
"""Test text-to-image services."""
async def test_text_to_image(
self,
services: dict[str, TextToImageClientBase],
service_id: str,
prompt: str,
):
service = services[service_id]
image_url = await service.generate_image(prompt, 1024, 1024)
assert image_url
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from azure.identity import AzureCliCredential
from semantic_kernel.connectors.ai.open_ai.services.azure_text_to_image import AzureTextToImage
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_to_image import OpenAITextToImage
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
class TextToImageTestBase:
"""Base class for testing text-to-image services."""
@pytest.fixture(scope="module")
def services(self) -> dict[str, TextToImageClientBase]:
"""Return text-to-image services."""
return {
"openai": OpenAITextToImage(),
"azure_openai": AzureTextToImage(credential=AzureCliCredential()),
}
+386
View File
@@ -0,0 +1,386 @@
# Copyright (c) Microsoft. All rights reserved.
import copy
import os
from collections.abc import Awaitable, Callable
from typing import Any
import pytest
from pytest import mark, param
from samples.concepts.auto_function_calling.chat_completion_with_auto_function_calling import (
main as chat_completion_with_function_calling,
)
from samples.concepts.auto_function_calling.functions_defined_in_json_prompt import (
main as function_defined_in_json_prompt,
)
from samples.concepts.auto_function_calling.functions_defined_in_yaml_prompt import (
main as function_defined_in_yaml_prompt,
)
from samples.concepts.caching.semantic_caching import main as semantic_caching
from samples.concepts.chat_completion.simple_chatbot import main as simple_chatbot
from samples.concepts.chat_completion.simple_chatbot_kernel_function import main as simple_chatbot_kernel_function
from samples.concepts.chat_completion.simple_chatbot_logit_bias import main as simple_chatbot_logit_bias
from samples.concepts.chat_completion.simple_chatbot_streaming import main as simple_chatbot_streaming
from samples.concepts.chat_completion.simple_chatbot_with_image import main as simple_chatbot_with_image
from samples.concepts.embedding.text_embedding_generation import main as text_embedding_generation
from samples.concepts.filtering.auto_function_invoke_filters import main as auto_function_invoke_filters
from samples.concepts.filtering.function_invocation_filters import main as function_invocation_filters
from samples.concepts.filtering.function_invocation_filters_stream import main as function_invocation_filters_stream
from samples.concepts.filtering.prompt_filters import main as prompt_filters
from samples.concepts.filtering.retry_with_different_model import main as retry_with_different_model
from samples.concepts.functions.kernel_arguments import main as kernel_arguments
from samples.concepts.grounding.grounded import main as grounded
from samples.concepts.images.image_generation import main as image_generation
from samples.concepts.local_models.lm_studio_chat_completion import main as lm_studio_chat_completion
from samples.concepts.local_models.lm_studio_text_embedding import main as lm_studio_text_embedding
from samples.concepts.local_models.ollama_chat_completion import main as ollama_chat_completion
from samples.concepts.mcp.agent_with_mcp_agent import main as agent_with_mcp_agent
from samples.concepts.memory.simple_memory import main as simple_memory
from samples.concepts.plugins.openai_function_calling_with_custom_plugin import (
main as openai_function_calling_with_custom_plugin,
)
from samples.concepts.plugins.plugins_from_dir import main as plugins_from_dir
from samples.concepts.prompt_templates.azure_chat_gpt_api_handlebars import main as azure_chat_gpt_api_handlebars
from samples.concepts.prompt_templates.azure_chat_gpt_api_jinja2 import main as azure_chat_gpt_api_jinja2
from samples.concepts.prompt_templates.configuring_prompts import main as configuring_prompts
from samples.concepts.prompt_templates.load_yaml_prompt import main as load_yaml_prompt
from samples.concepts.prompt_templates.template_language import main as template_language
from samples.concepts.rag.rag_with_vector_collection import main as rag_with_vector_collection
from samples.concepts.service_selector.custom_service_selector import main as custom_service_selector
from samples.concepts.text_completion.text_completion import main as text_completion
from samples.getting_started_with_agents.chat_completion.step01_chat_completion_agent_simple import (
main as step1_chat_completion_agent_simple,
)
from samples.getting_started_with_agents.chat_completion.step03_chat_completion_agent_with_kernel import (
main as step2_chat_completion_agent_with_kernel,
)
from samples.getting_started_with_agents.chat_completion.step04_chat_completion_agent_plugin_simple import (
main as step3_chat_completion_agent_plugin_simple,
)
from samples.getting_started_with_agents.chat_completion.step05_chat_completion_agent_plugin_with_kernel import (
main as step4_chat_completion_agent_plugin_with_kernel,
)
from samples.getting_started_with_agents.chat_completion.step06_chat_completion_agent_group_chat import (
main as step5_chat_completion_agent_group_chat,
)
from samples.getting_started_with_agents.openai_assistant.step1_assistant import main as step1_openai_assistant
from tests.utils import retry
# These environment variable names are used to control which samples are run during integration testing.
# This has to do with the setup of the tests and the services they depend on.
COMPLETIONS_CONCEPT_SAMPLE = "COMPLETIONS_CONCEPT_SAMPLE"
MEMORY_CONCEPT_SAMPLE = "MEMORY_CONCEPT_SAMPLE"
concepts = [
param(
semantic_caching,
[],
id="semantic_caching",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
simple_chatbot,
["Why is the sky blue in one sentence?", "exit"],
id="simple_chatbot",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
simple_chatbot_streaming,
["Why is the sky blue in one sentence?", "exit"],
id="simple_chatbot_streaming",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
simple_chatbot_with_image,
["exit"],
id="simple_chatbot_with_image",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
simple_chatbot_logit_bias,
["Who has the most career points in NBA history?", "exit"],
id="simple_chatbot_logit_bias",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
simple_chatbot_kernel_function,
["Why is the sky blue in one sentence?", "exit"],
id="simple_chatbot_kernel_function",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
chat_completion_with_function_calling,
["What is 3+3?", "exit"],
id="chat_completion_with_function_calling",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
auto_function_invoke_filters,
["What is 3+3?", "exit"],
id="auto_function_invoke_filters",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
function_invocation_filters,
["What is 3+3?", "exit"],
id="function_invocation_filters",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
function_invocation_filters_stream,
["What is 3+3?", "exit"],
id="function_invocation_filters_stream",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
prompt_filters,
["What is the fastest animal?", "exit"],
id="prompt_filters",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
retry_with_different_model,
[],
id="retry_with_different_model",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None,
reason="Not running completion samples.",
),
),
param(
kernel_arguments,
[],
id="kernel_arguments",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
grounded,
[],
id="grounded",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
openai_function_calling_with_custom_plugin,
[],
id="openai_function_calling_with_custom_plugin",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
plugins_from_dir,
[],
id="plugins_from_dir",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
azure_chat_gpt_api_handlebars,
["What is 3+3?", "exit"],
id="azure_chat_gpt_api_handlebars",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
azure_chat_gpt_api_jinja2,
["What is 3+3?", "exit"],
id="azure_chat_gpt_api_jinja2",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
agent_with_mcp_agent,
["what restaurants can I choose from?", "the farm sounds nice, what are the specials there?", "exit"],
id="agent_with_mcp_agent",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
configuring_prompts,
["What is my name?", "exit"],
id="configuring_prompts",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
load_yaml_prompt,
[],
id="load_yaml_prompt",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
template_language,
[],
id="template_language",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
simple_memory,
[],
id="simple_memory",
marks=pytest.mark.skipif(os.getenv(MEMORY_CONCEPT_SAMPLE, None) is None, reason="Not running memory samples."),
),
param(rag_with_vector_collection, [], id="rag_with_vector_collection"),
param(
custom_service_selector,
[],
id="custom_service_selector",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
function_defined_in_json_prompt,
["What is 3+3?", "exit"],
id="function_defined_in_json_prompt",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
function_defined_in_yaml_prompt,
["What is 3+3?", "exit"],
id="function_defined_in_yaml_prompt",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
step1_chat_completion_agent_simple,
[],
id="step1_chat_completion_agent_simple",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
step2_chat_completion_agent_with_kernel,
[],
id="step2_chat_completion_agent_with_kernel",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
step3_chat_completion_agent_plugin_simple,
[],
id="step3_chat_completion_agent_plugin_simple",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
step4_chat_completion_agent_plugin_with_kernel,
[],
id="step4_chat_completion_agent_plugin_with_kernel",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
step5_chat_completion_agent_group_chat,
[],
id="step5_chat_completion_agent_group_chat",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
step1_openai_assistant,
[],
id="step1_openai_assistant",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
ollama_chat_completion,
["Why is the sky blue?", "exit"],
id="ollama_chat_completion",
marks=pytest.mark.skip(reason="Need to set up Ollama locally. Check out the module for more details."),
),
param(
lm_studio_chat_completion,
["Why is the sky blue?", "exit"],
id="lm_studio_chat_completion",
marks=pytest.mark.skip(reason="Need to set up LM Studio locally. Check out the module for more details."),
),
param(
lm_studio_text_embedding,
[],
id="lm_studio_text_embedding",
marks=pytest.mark.skip(reason="Need to set up LM Studio locally. Check out the module for more details."),
),
param(
image_generation,
[],
id="image_generation",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
text_completion,
[],
id="text_completion",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
text_embedding_generation,
[],
id="text_embedding_generation",
marks=pytest.mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
]
@mark.parametrize("sample, responses", concepts)
async def test_concepts(sample: Callable[..., Awaitable[Any]], responses: list[str], monkeypatch):
saved_responses = copy.deepcopy(responses)
def reset():
responses.clear()
responses.extend(saved_responses)
monkeypatch.setattr("builtins.input", lambda _: responses.pop(0))
await retry(sample, retries=3, reset=reset)
@@ -0,0 +1,99 @@
# Copyright (c) Microsoft. All rights reserved.
import os
import nbformat
from nbconvert.preprocessors import ExecutePreprocessor
from pytest import mark, param
from traitlets.config import Config
c = Config()
c.RegexRemovePreprocessor.patterns = ["^!pip .*"]
c.ExecutePreprocessor.exclude_input_prompt = True
# These environment variable names are used to control which samples are run during integration testing.
# This has to do with the setup of the tests and the services they depend on.
COMPLETIONS_CONCEPT_SAMPLE = "COMPLETIONS_CONCEPT_SAMPLE"
MEMORY_CONCEPT_SAMPLE = "MEMORY_CONCEPT_SAMPLE"
def run_notebook(notebook_name: str):
with open(f"samples/getting_started/{notebook_name}") as f:
nb = nbformat.read(f, as_version=4)
ep = ExecutePreprocessor(timeout=600, kernel_name="python3", config=c)
ep.preprocess(nb, {"metadata": {"path": "samples/getting_started/"}})
notebooks = [
param(
"00-getting-started.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"01-basic-loading-the-kernel.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"02-running-prompts-from-file.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"03-prompt-function-inline.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"04-kernel-arguments-chat.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"05-memory-and-embeddings.ipynb",
marks=mark.skipif(
True, reason="Issue with missing property. Need to investigate and fix. Skip to unblock CI/CD pipeline."
),
),
param(
"06-hugging-face-for-plugins.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"07-native-function-inline.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"08-groundedness-checking.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"09-multiple-results-per-prompt.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
"10-streaming-completions.ipynb",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
]
@mark.parametrize("name", notebooks)
def test_notebooks(name):
run_notebook(name)
@@ -0,0 +1,111 @@
# Copyright (c) Microsoft. All rights reserved.
import copy
import os
from pytest import mark, param
from samples.learn_resources.ai_services import main as ai_services
from samples.learn_resources.configuring_prompts import main as configuring_prompts
from samples.learn_resources.creating_functions import main as creating_functions
from samples.learn_resources.functions_within_prompts import main as functions_within_prompts
from samples.learn_resources.plugin import main as plugin
from samples.learn_resources.serializing_prompts import main as serializing_prompts
from samples.learn_resources.templates import main as templates
from samples.learn_resources.using_the_kernel import main as using_the_kernel
from samples.learn_resources.your_first_prompt import main as your_first_prompt
from tests.utils import retry
# These environment variable names are used to control which samples are run during integration testing.
# This has to do with the setup of the tests and the services they depend on.
COMPLETIONS_CONCEPT_SAMPLE = "COMPLETIONS_CONCEPT_SAMPLE"
learn_resources = [
param(
ai_services,
[],
id="ai_services",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
configuring_prompts,
["Hello, who are you?", "exit"],
id="configuring_prompts",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
creating_functions,
["What is 3+3?", "exit"],
id="creating_functions",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
functions_within_prompts,
["Hello, who are you?", "exit"],
id="functions_within_prompts",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
plugin,
[],
id="plugin",
# will run anyway, no services called.
),
param(
serializing_prompts,
["Hello, who are you?", "exit"],
id="serializing_prompts",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
templates,
["Hello, who are you?", "Thanks, see you next time!"],
id="templates",
marks=(
mark.skipif(os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."),
mark.xfail(reason="This sample is not working as expected."),
),
),
param(
using_the_kernel,
[],
id="using_the_kernel",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
param(
your_first_prompt,
["I want to send an email to my manager!"],
id="your_first_prompt",
marks=mark.skipif(
os.getenv(COMPLETIONS_CONCEPT_SAMPLE, None) is None, reason="Not running completion samples."
),
),
]
@mark.parametrize("func,responses", learn_resources)
async def test_learn_resources(func, responses, monkeypatch):
saved_responses = copy.deepcopy(responses)
def reset():
responses.clear()
responses.extend(saved_responses)
monkeypatch.setattr("builtins.input", lambda _: responses.pop(0))
if func.__module__ == "samples.learn_resources.your_first_prompt":
await retry(lambda: func(delay=10), reset=reset)
return
await retry(lambda: func(), reset=reset, retries=5)
@@ -0,0 +1,128 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock
import pytest
from autogen import ConversableAgent
from semantic_kernel.agents.autogen.autogen_conversable_agent import (
AutoGenConversableAgent,
AutoGenConversableAgentThread,
)
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException
@pytest.fixture
def mock_conversable_agent():
agent = MagicMock(spec=ConversableAgent)
agent.name = "MockName"
agent.description = "MockDescription"
agent.system_message = "MockSystemMessage"
return agent
async def test_autogen_conversable_agent_initialization(mock_conversable_agent):
agent = AutoGenConversableAgent(mock_conversable_agent, id="mock_id")
assert agent.name == "MockName"
assert agent.description == "MockDescription"
assert agent.instructions == "MockSystemMessage"
assert agent.conversable_agent == mock_conversable_agent
async def test_autogen_conversable_agent_get_response(mock_conversable_agent):
mock_conversable_agent.a_generate_reply = AsyncMock(return_value="Mocked assistant response")
agent = AutoGenConversableAgent(mock_conversable_agent)
thread: AutoGenConversableAgentThread = None
response = await agent.get_response("Hello", thread=thread)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content == "Mocked assistant response"
assert response.thread is not None
async def test_autogen_conversable_agent_get_response_exception(mock_conversable_agent):
mock_conversable_agent.a_generate_reply = AsyncMock(return_value=None)
agent = AutoGenConversableAgent(mock_conversable_agent)
with pytest.raises(AgentInvokeException):
await agent.get_response("Hello")
async def test_autogen_conversable_agent_invoke_with_recipient(mock_conversable_agent):
mock_conversable_agent.a_initiate_chat = AsyncMock()
mock_conversable_agent.a_initiate_chat.return_value = MagicMock(
chat_history=[
{"role": "user", "content": "Hello from user!"},
{"role": "assistant", "content": "Hello from assistant!"},
]
)
agent = AutoGenConversableAgent(mock_conversable_agent)
recipient_agent = MagicMock(spec=AutoGenConversableAgent)
recipient_agent.conversable_agent = MagicMock(spec=ConversableAgent)
messages = []
async for response in agent.invoke(recipient=recipient_agent, messages="Test message", arg1="arg1"):
messages.append(response)
mock_conversable_agent.a_initiate_chat.assert_awaited_once()
assert len(messages) == 2
assert messages[0].message.role == AuthorRole.USER
assert messages[0].message.content == "Hello from user!"
assert messages[1].message.role == AuthorRole.ASSISTANT
assert messages[1].message.content == "Hello from assistant!"
async def test_autogen_conversable_agent_invoke_without_recipient_string_reply(mock_conversable_agent):
mock_conversable_agent.a_generate_reply = AsyncMock(return_value="Mocked assistant response")
agent = AutoGenConversableAgent(mock_conversable_agent)
responses = []
async for response in agent.invoke(messages="Hello"):
responses.append(response)
mock_conversable_agent.a_generate_reply.assert_awaited_once()
assert len(responses) == 1
assert responses[0].message.role == AuthorRole.ASSISTANT
assert responses[0].message.content == "Mocked assistant response"
async def test_autogen_conversable_agent_invoke_without_recipient_dict_reply(mock_conversable_agent):
mock_conversable_agent.a_generate_reply = AsyncMock(
return_value={
"content": "Mocked assistant response",
"role": "assistant",
"name": "AssistantName",
}
)
agent = AutoGenConversableAgent(mock_conversable_agent)
responses = []
async for response in agent.invoke(messages="Hello"):
responses.append(response)
mock_conversable_agent.a_generate_reply.assert_awaited_once()
assert len(responses) == 1
assert responses[0].message.role == AuthorRole.ASSISTANT
assert responses[0].message.content == "Mocked assistant response"
assert responses[0].message.name == "AssistantName"
async def test_autogen_conversable_agent_invoke_without_recipient_unexpected_type(mock_conversable_agent):
mock_conversable_agent.a_generate_reply = AsyncMock(return_value=12345)
agent = AutoGenConversableAgent(mock_conversable_agent)
with pytest.raises(AgentInvokeException):
async for _ in agent.invoke(messages="Hello"):
pass
async def test_autogen_conversable_agent_invoke_with_invalid_recipient_type(mock_conversable_agent):
mock_conversable_agent.a_generate_reply = AsyncMock(return_value=12345)
agent = AutoGenConversableAgent(mock_conversable_agent)
recipient = MagicMock()
with pytest.raises(AgentInvokeException):
async for _ in agent.invoke(recipient=recipient, messages="Hello"):
pass
@@ -0,0 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock
import pytest
from azure.ai.agents.models import Agent as AzureAIAgentModel
from azure.ai.projects.aio import AIProjectClient
@pytest.fixture
def ai_project_client() -> AsyncMock:
client = AsyncMock(spec=AIProjectClient)
agents_mock = MagicMock()
client.agents = agents_mock
return client
@pytest.fixture
def ai_agent_definition() -> AsyncMock:
definition = AsyncMock(spec=AzureAIAgentModel)
definition.id = "agent123"
definition.name = "agentName"
definition.description = "desc"
definition.instructions = "test agent"
return definition
@@ -0,0 +1,464 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
from azure.ai.agents.models import (
MessageDelta,
MessageDeltaChunk,
MessageDeltaImageFileContent,
MessageDeltaImageFileContentObject,
MessageDeltaTextContent,
MessageDeltaTextContentObject,
MessageDeltaTextFileCitationAnnotation,
MessageDeltaTextFileCitationAnnotationObject,
MessageDeltaTextFilePathAnnotation,
MessageDeltaTextFilePathAnnotationObject,
MessageDeltaTextUrlCitationAnnotation,
MessageDeltaTextUrlCitationDetails,
MessageImageFileContent,
MessageImageFileDetails,
MessageTextContent,
MessageTextDetails,
MessageTextFileCitationAnnotation,
MessageTextFileCitationDetails,
MessageTextFilePathAnnotation,
MessageTextFilePathDetails,
MessageTextUrlCitationAnnotation,
MessageTextUrlCitationDetails,
RequiredFunctionToolCall,
RunStep,
RunStepBingCustomSearchToolCall,
RunStepBingGroundingToolCall,
RunStepDeltaFunction,
RunStepDeltaFunctionToolCall,
RunStepDeltaToolCallObject,
RunStepFunctionToolCall,
RunStepFunctionToolCallDetails,
ThreadMessage,
)
from semantic_kernel.agents.azure_ai.agent_content_generation import (
THREAD_MESSAGE_ID,
generate_annotation_content,
generate_bing_grounding_content,
generate_code_interpreter_content,
generate_function_call_content,
generate_function_result_content,
generate_message_content,
generate_streaming_annotation_content,
generate_streaming_code_interpreter_content,
generate_streaming_function_content,
generate_streaming_message_content,
get_function_call_contents,
get_message_contents,
)
from semantic_kernel.contents.annotation_content import AnnotationContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.streaming_annotation_content import StreamingAnnotationContent
from semantic_kernel.contents.streaming_file_reference_content import StreamingFileReferenceContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
def test_get_message_contents_all_types():
chat_msg = ChatMessageContent(role=AuthorRole.USER, content="")
chat_msg.items.append(TextContent(text="hello world"))
chat_msg.items.append(ImageContent(uri="http://example.com/image.png"))
chat_msg.items.append(FileReferenceContent(file_id="file123"))
chat_msg.items.append(FunctionResultContent(id="func1", result={"a": 1}))
results = get_message_contents(chat_msg)
assert len(results) == 4
assert results[0]["type"] == "text"
assert results[1]["type"] == "image_url"
assert results[2]["type"] == "image_file"
assert results[3]["type"] == "text"
def test_generate_message_content_text_and_image():
thread_msg = ThreadMessage(
content=[],
role="user",
)
image = MessageImageFileContent(image_file=MessageImageFileDetails(file_id="test_file_id"))
text = MessageTextContent(
text=MessageTextDetails(
value="some text",
annotations=[
MessageTextFileCitationAnnotation(
text="text",
file_citation=MessageTextFileCitationDetails(file_id="file_id", quote="some quote"),
start_index=0,
end_index=9,
),
MessageTextFilePathAnnotation(
text="text again",
file_path=MessageTextFilePathDetails(file_id="file_id_2"),
start_index=1,
end_index=10,
),
MessageTextUrlCitationAnnotation(
text="text",
url_citation=MessageTextUrlCitationDetails(title="some title", url="http://example.com"),
start_index=1,
end_index=10,
),
],
)
)
thread_msg.content = [image, text]
step = RunStep(id="step_id", run_id="run_id", thread_id="thread_id", agent_id="agent_id")
out = generate_message_content("assistant", thread_msg, step)
assert len(out.items) == 5
assert isinstance(out.items[0], FileReferenceContent)
assert isinstance(out.items[1], TextContent)
assert isinstance(out.items[2], AnnotationContent)
assert isinstance(out.items[3], AnnotationContent)
assert isinstance(out.items[4], AnnotationContent)
assert out.items[0].file_id == "test_file_id"
assert out.items[1].text == "some text"
assert out.items[2].file_id == "file_id"
assert out.items[2].quote == "text"
assert out.items[2].start_index == 0
assert out.items[2].end_index == 9
assert out.items[2].citation_type == "file_citation"
assert out.items[3].file_id == "file_id_2"
assert out.items[3].quote == "text again"
assert out.items[3].start_index == 1
assert out.items[3].end_index == 10
assert out.items[3].citation_type == "file_path"
assert out.items[4].url == "http://example.com"
assert out.items[4].quote == "text"
assert out.items[4].start_index == 1
assert out.items[4].end_index == 10
assert out.items[4].title == "some title"
assert out.items[4].citation_type == "url_citation"
assert out.metadata["step_id"] == "step_id"
assert out.role == AuthorRole.USER
def test_generate_annotation_content():
message_text_file_path_ann = MessageTextFilePathAnnotation(
text="some text",
file_path=MessageTextFilePathDetails(file_id="file123"),
start_index=0,
end_index=9,
)
message_text_file_citation_ann = MessageTextFileCitationAnnotation(
text="some text",
file_citation=MessageTextFileCitationDetails(file_id="file123"),
start_index=0,
end_index=9,
)
for fake_ann in [message_text_file_path_ann, message_text_file_citation_ann]:
out = generate_annotation_content(fake_ann)
assert out.file_id == "file123"
assert out.quote == "some text"
assert out.start_index == 0
assert out.end_index == 9
def test_generate_streaming_message_content_text_annotations():
message_delta_image_file_content = MessageDeltaImageFileContent(
index=0,
image_file=MessageDeltaImageFileContentObject(file_id="image_file"),
)
MessageDeltaTextFileCitationAnnotation, MessageDeltaTextFilePathAnnotation
message_delta_text_content = MessageDeltaTextContent(
index=0,
text=MessageDeltaTextContentObject(
value="some text",
annotations=[
MessageDeltaTextFileCitationAnnotation(
index=0,
file_citation=MessageDeltaTextFileCitationAnnotationObject(file_id="file123", quote="some text"),
start_index=0,
end_index=9,
text="some text",
),
MessageDeltaTextFilePathAnnotation(
index=0,
file_path=MessageDeltaTextFilePathAnnotationObject(file_id="file123"),
start_index=1,
end_index=10,
text="some text",
),
MessageDeltaTextUrlCitationAnnotation(
index=0,
url_citation=MessageDeltaTextUrlCitationDetails(
title="some title",
url="http://example.com",
),
start_index=2,
end_index=11,
),
],
),
)
delta = MessageDeltaChunk(
id="chunk123",
delta=MessageDelta(role="user", content=[message_delta_image_file_content, message_delta_text_content]),
)
out = generate_streaming_message_content("assistant", delta)
assert out is not None
assert out.content == "some text"
assert len(out.items) == 5
assert out.items[0].file_id == "image_file"
assert isinstance(out.items[0], StreamingFileReferenceContent)
assert isinstance(out.items[1], StreamingTextContent)
assert isinstance(out.items[2], StreamingAnnotationContent)
assert out.items[2].file_id == "file123"
assert out.items[2].quote == "some text"
assert out.items[2].start_index == 0
assert out.items[2].end_index == 9
assert out.items[2].citation_type == "file_citation"
assert isinstance(out.items[3], StreamingAnnotationContent)
assert out.items[3].file_id == "file123"
assert out.items[3].quote == "some text"
assert out.items[3].start_index == 1
assert out.items[3].end_index == 10
assert out.items[3].citation_type == "file_path"
assert isinstance(out.items[4], StreamingAnnotationContent)
assert out.items[4].url == "http://example.com"
assert out.items[4].title == "some title"
assert out.items[4].start_index == 2
assert out.items[4].end_index == 11
assert out.items[4].citation_type == "url_citation"
def test_generate_annotation_content_url_annotation_without_indices():
ann = MessageTextUrlCitationAnnotation(
text="url text",
url_citation=MessageTextUrlCitationDetails(title="", url="http://ex.com"),
start_index=None,
end_index=None,
)
out = generate_annotation_content(ann)
assert out.file_id is None
assert out.url == "http://ex.com"
assert out.title == "" # preserve empty title
assert out.quote == "url text"
assert out.start_index is None
assert out.end_index is None
assert out.citation_type == "url_citation"
def test_generate_streaming_annotation_content_url_quote_none_and_missing_indices():
ann = MessageDeltaTextUrlCitationAnnotation(
index=0,
url_citation=MessageDeltaTextUrlCitationDetails(title="", url="http://ex.com"),
start_index=None,
end_index=None,
)
out = generate_streaming_annotation_content(ann)
assert out.file_id is None
assert out.url == "http://ex.com"
assert out.title == ""
assert out.quote is None # no .text on URL annotation
assert out.start_index is None
assert out.end_index is None
assert out.citation_type == "url_citation"
def test_generate_streaming_message_content_text_only_no_annotations():
delta = MessageDeltaChunk(
id="c1",
delta=MessageDelta(
role="assistant",
content=[
MessageDeltaTextContent(
index=0,
text=MessageDeltaTextContentObject(value="just text", annotations=[]),
)
],
),
)
out = generate_streaming_message_content("assistant", delta, thread_msg_id="thread_1")
assert out.content == "just text"
assert len(out.items) == 1
assert isinstance(out.items[0], StreamingTextContent)
assert out.items[0].text == "just text"
assert out.metadata.get(THREAD_MESSAGE_ID) == "thread_1"
def test_generate_annotation_content_empty_title_and_url_only():
ann = MessageTextUrlCitationAnnotation(
text=None,
url_citation=MessageTextUrlCitationDetails(title=None, url="http://empty.com"),
start_index=5,
end_index=10,
)
out = generate_annotation_content(ann)
assert out.quote is None # allow None text
assert out.url == "http://empty.com"
assert out.title is None # allow None title
assert out.start_index == 5
assert out.end_index == 10
def test_generate_streaming_annotation_content_file_and_citation_have_text():
file_ann = MessageDeltaTextFileCitationAnnotation(
index=0,
file_citation=MessageDeltaTextFileCitationAnnotationObject(file_id="f1", quote="q1"),
start_index=2,
end_index=4,
text="q1",
)
out = generate_streaming_annotation_content(file_ann)
assert out.file_id == "f1"
assert out.quote == "q1"
assert out.citation_type == "file_citation"
assert out.start_index == 2
assert out.end_index == 4
def test_generate_streaming_function_content_with_function():
step_details = RunStepDeltaToolCallObject(
tool_calls=[
RunStepDeltaFunctionToolCall(
index=0, id="tool123", function=RunStepDeltaFunction(name="some_func", arguments={"arg": "val"})
)
]
)
out = generate_streaming_function_content("my_agent", step_details)
assert out is not None
assert len(out.items) == 1
assert isinstance(out.items[0], FunctionCallContent)
assert out.items[0].function_name == "some_func"
assert out.items[0].arguments == "{'arg': 'val'}"
def test_get_function_call_contents_no_action():
run = type("ThreadRunFake", (), {"required_action": None})()
fc = get_function_call_contents(run, {})
assert fc == []
def test_get_function_call_contents_submit_tool_outputs():
fake_function = MagicMock()
fake_function.name = "test_function"
fake_function.arguments = {"arg": "val"}
fake_tool_call = MagicMock(spec=RequiredFunctionToolCall)
fake_tool_call.id = "tool_id"
fake_tool_call.function = fake_function
run = MagicMock()
run.required_action.submit_tool_outputs.tool_calls = [fake_tool_call]
function_steps = {}
fc = get_function_call_contents(run, function_steps)
assert len(fc) == 1
assert fc[0].id == "tool_id"
assert fc[0].name == "test_function"
assert fc[0].arguments == {"arg": "val"}
def test_generate_function_call_content():
fcc = FunctionCallContent(id="id123", name="func_name", arguments={"x": 1})
msg = generate_function_call_content("my_agent", [fcc])
assert len(msg.items) == 1
assert msg.role == AuthorRole.ASSISTANT
def test_generate_function_result_content():
step = FunctionCallContent(id="123", name="func_name", arguments={"k": "v"})
tool_call = RunStepFunctionToolCall(
id="123",
function=RunStepFunctionToolCallDetails({
"name": "func_name",
"arguments": '{"k": "v"}',
"output": "result_data",
}),
)
msg = generate_function_result_content("my_agent", step, tool_call)
assert len(msg.items) == 1
assert msg.items[0].result == "result_data"
assert msg.role == AuthorRole.TOOL
def test_generate_code_interpreter_content():
msg = generate_code_interpreter_content("my_agent", "some_code()")
assert msg.content == "some_code()"
assert msg.metadata["code"] is True
def test_generate_streaming_code_interpreter_content_no_calls():
step_details = type("Details", (), {"tool_calls": None})
assert generate_streaming_code_interpreter_content("my_agent", step_details) is None
def test_generate_bing_grounding_content():
"""Test generate_bing_grounding_content with RunStepBingGroundingToolCall."""
bing_grounding_tool_call = RunStepBingGroundingToolCall(
id="call_gvgTmSL4hgdxWP4O7LLnwMlt",
bing_grounding={
"requesturl": "https://api.bing.microsoft.com/v7.0/search?q=search",
"response_metadata": "{'market': 'en-US', 'num_docs_retrieved': 5, 'num_docs_actually_used': 5}",
},
)
msg = generate_bing_grounding_content("my_agent", bing_grounding_tool_call)
assert len(msg.items) == 1
assert msg.role == AuthorRole.ASSISTANT
assert isinstance(msg.items[0], FunctionCallContent)
assert msg.items[0].id == "call_gvgTmSL4hgdxWP4O7LLnwMlt"
assert msg.items[0].name == "bing_grounding"
assert msg.items[0].function_name == "bing_grounding"
assert msg.items[0].arguments["requesturl"] == "https://api.bing.microsoft.com/v7.0/search?q=search"
assert msg.items[0].arguments["response_metadata"] == (
"{'market': 'en-US', 'num_docs_retrieved': 5, 'num_docs_actually_used': 5}"
)
def test_generate_bing_custom_search_content():
"""Test generate_bing_grounding_content with RunStepBingCustomSearchToolCall."""
bing_custom_search_tool_call = RunStepBingCustomSearchToolCall(
id="call_abc123def456ghi",
bing_custom_search={
"query": "semantic kernel python",
"custom_config_id": "config_123",
"search_results": "{'num_results': 10, 'top_result': 'Microsoft Semantic Kernel'}",
},
)
msg = generate_bing_grounding_content("my_agent", bing_custom_search_tool_call)
assert len(msg.items) == 1
assert msg.role == AuthorRole.ASSISTANT
assert isinstance(msg.items[0], FunctionCallContent)
assert msg.items[0].id == "call_abc123def456ghi"
assert msg.items[0].name == "bing_custom_search"
assert msg.items[0].function_name == "bing_custom_search"
assert msg.items[0].arguments["query"] == "semantic kernel python"
assert msg.items[0].arguments["custom_config_id"] == "config_123"
assert msg.items[0].arguments["search_results"] == (
"{'num_results': 10, 'top_result': 'Microsoft Semantic Kernel'}"
)
@@ -0,0 +1,662 @@
# Copyright (c) Microsoft. All rights reserved.
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from azure.ai.agents.models import (
MessageTextContent,
MessageTextDetails,
RequiredFunctionToolCall,
RequiredFunctionToolCallDetails,
RunStep,
RunStepCodeInterpreterToolCall,
RunStepCodeInterpreterToolCallDetails,
RunStepFunctionToolCall,
RunStepFunctionToolCallDetails,
RunStepMessageCreationDetails,
RunStepMessageCreationReference,
RunStepToolCallDetails,
SubmitToolOutputsAction,
SubmitToolOutputsDetails,
ThreadMessage,
ThreadRun,
)
from azure.ai.projects.aio import AIProjectClient
from pytest import fixture
from semantic_kernel.agents.azure_ai.agent_thread_actions import AgentThreadActions
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents import FunctionCallContent, FunctionResultContent, TextContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel import Kernel
@fixture
def mock_client():
mock_thread = AsyncMock()
mock_thread.id = "thread123"
mock_threads = MagicMock()
mock_threads.create = AsyncMock(return_value=mock_thread)
mock_message = AsyncMock()
mock_message.id = "message456"
mock_messages = MagicMock()
mock_messages.create = AsyncMock(return_value="someMessage")
mock_agents = MagicMock()
mock_agents.threads = mock_threads
mock_agents.messages = mock_messages
mock_client = AsyncMock(spec=AIProjectClient)
mock_client.agents = mock_agents
return mock_client
async def test_agent_thread_actions_create_thread(mock_client):
thread_id = await AgentThreadActions.create_thread(mock_client)
assert thread_id == "thread123"
async def test_agent_thread_actions_create_message(mock_client):
msg = ChatMessageContent(role=AuthorRole.USER, content="some content")
out = await AgentThreadActions.create_message(mock_client, "threadXYZ", msg)
assert out == "someMessage"
async def test_agent_thread_actions_create_message_no_content():
class FakeAgentClient:
create_message = AsyncMock(return_value="should_not_be_called")
class FakeClient:
agents = FakeAgentClient()
message = ChatMessageContent(role=AuthorRole.USER, content=" ")
out = await AgentThreadActions.create_message(FakeClient(), "threadXYZ", message)
assert out is None
assert FakeAgentClient.create_message.await_count == 0
async def test_agent_thread_actions_invoke(ai_project_client: AIProjectClient, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
# Properly construct nested mocks without re-spec'ing from a mock
mock_thread_run = ThreadRun(
id="run123",
thread_id="thread123",
status="running",
instructions="test agent",
created_at=int(datetime.now(timezone.utc).timestamp()),
model="model",
)
agent.client.agents.runs = MagicMock()
agent.client.agents.runs.create = AsyncMock(return_value=mock_thread_run)
agent.client.agents.runs.get = AsyncMock(return_value=mock_thread_run)
async def mock_poll_run_status(*args, **kwargs):
yield RunStep(
type="message_creation",
id="msg123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
step_details=RunStepMessageCreationDetails(
message_creation=RunStepMessageCreationReference(
message_id="msg123",
),
),
)
agent.client.agents.run_steps = MagicMock()
agent.client.agents.run_steps.list = mock_poll_run_status
mock_message = ThreadMessage(
id="msg123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
role="assistant",
content=[MessageTextContent(text=MessageTextDetails(value="some message", annotations=[]))],
)
agent.client.agents.messages = MagicMock()
agent.client.agents.messages.get = AsyncMock(return_value=mock_message)
async for is_visible, message in AgentThreadActions.invoke(
agent=agent, thread_id="thread123", kernel=AsyncMock(spec=Kernel)
):
assert str(message.content) == "some message"
break
async def test_agent_thread_actions_invoke_with_requires_action(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = MagicMock()
mock_thread_run = ThreadRun(
id="run123",
thread_id="thread123",
status="running",
instructions="test agent",
created_at=int(datetime.now(timezone.utc).timestamp()),
model="model",
)
agent.client.agents = MagicMock()
agent.client.agents.runs = MagicMock()
agent.client.agents.runs.create = AsyncMock(return_value=mock_thread_run)
agent.client.agents.runs.get = AsyncMock(return_value=mock_thread_run)
agent.client.agents.runs.submit_tool_outputs = AsyncMock()
poll_count = 0
async def mock_poll_run_status(*args, **kwargs):
nonlocal poll_count
if poll_count == 0:
mock_thread_run.status = "requires_action"
mock_thread_run.required_action = SubmitToolOutputsAction(
submit_tool_outputs=SubmitToolOutputsDetails(
tool_calls=[
RequiredFunctionToolCall(
id="tool_call_id",
function=RequiredFunctionToolCallDetails(
name="mock_function_call", arguments={"arg": "value"}
),
)
]
)
)
else:
mock_thread_run.status = "completed"
poll_count += 1
return mock_thread_run
def mock_get_function_call_contents(run: ThreadRun, function_steps: dict):
function_call_content = FunctionCallContent(
name="mock_function_call",
arguments={"arg": "value"},
id="tool_call_id",
)
function_steps[function_call_content.id] = function_call_content
return [function_call_content]
mock_run_step_tool_calls = RunStep(
type="tool_calls",
id="tool_step123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
step_details=RunStepToolCallDetails(
tool_calls=[
# 1. This will yield FunctionResultContent
RunStepFunctionToolCall(
id="tool_call_id",
function=RunStepFunctionToolCallDetails({
"name": "mock_function_call",
"arguments": '{"arg": "value"}',
"output": "some output",
}),
),
# 2. This will yield TextContent
RunStepCodeInterpreterToolCall(
id="tool_call_id",
code_interpreter=RunStepCodeInterpreterToolCallDetails(
input="some code",
),
),
]
),
)
mock_run_step_message_creation = RunStep(
type="message_creation",
id="msg_step123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
step_details=RunStepMessageCreationDetails(
message_creation=RunStepMessageCreationReference(message_id="msg123")
),
)
mock_run_steps = [mock_run_step_tool_calls, mock_run_step_message_creation]
async def mock_list_run_steps(*args, **kwargs):
for step in mock_run_steps:
yield step
agent.client.agents.run_steps = MagicMock()
agent.client.agents.run_steps.list = mock_list_run_steps
mock_message = ThreadMessage(
id="msg123",
thread_id="thread123",
run_id="run123",
created_at=int(datetime.now(timezone.utc).timestamp()),
completed_at=int(datetime.now(timezone.utc).timestamp()),
status="completed",
agent_id="agent123",
role="assistant",
content=[MessageTextContent(text=MessageTextDetails(value="some message", annotations=[]))],
)
agent.client.agents.runs.get = AsyncMock(return_value=mock_message)
agent.client.agents.runs.submit_tool_outputs = AsyncMock()
with (
patch.object(AgentThreadActions, "_poll_run_status", side_effect=mock_poll_run_status),
patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.get_function_call_contents",
side_effect=mock_get_function_call_contents,
),
patch.object(AgentThreadActions, "_invoke_function_calls", return_value=[None]),
):
messages = []
async for is_visible, content in AgentThreadActions.invoke(
agent=agent,
thread_id="thread123",
kernel=AsyncMock(spec=Kernel),
):
messages.append((is_visible, content))
assert len(messages) == 3, "There should be three yields in total."
assert isinstance(messages[0][1].items[0], FunctionCallContent)
assert isinstance(messages[1][1].items[0], FunctionResultContent)
assert isinstance(messages[2][1].items[0], TextContent)
agent.client.agents.runs.submit_tool_outputs.assert_awaited_once()
class MockEvent:
def __init__(self, event, data):
self.event = event
self.data = data
def __iter__(self):
return iter((self.event, self.data, None))
class MockRunData:
def __init__(self, id, status, content: str | None = None):
self.id = id
self.status = status
self.content = content
class MockAsyncIterable:
def __init__(self, items):
self.items = items.copy()
def __aiter__(self):
self._iter = iter(self.items)
return self
async def __anext__(self):
try:
return next(self._iter)
except StopIteration:
raise StopAsyncIteration
class MockStream:
def __init__(self, events):
self.events = events
async def __aenter__(self):
return MockAsyncIterable(self.events)
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
async def test_agent_thread_actions_invoke_stream(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = AsyncMock()
events = [
MockEvent("thread.run.created", MockRunData(id="run_1", status="queued")),
MockEvent("thread.message.created", MockRunData(id="msg_1", status="created", content="Hello")),
MockEvent("thread.run.in_progress", MockRunData(id="run_1", status="in_progress")),
MockEvent("thread.run.completed", MockRunData(id="run_1", status="completed")),
]
main_run_stream = MockStream(events)
agent.client.agents.create_stream.return_value = main_run_stream
with (
patch.object(AgentThreadActions, "_invoke_function_calls", return_value=None),
patch.object(AgentThreadActions, "_format_tool_outputs", return_value=[{"type": "mock_tool_output"}]),
):
collected_messages = []
async for content in AgentThreadActions.invoke_stream(
agent=agent,
thread_id="thread123",
kernel=AsyncMock(spec=Kernel),
):
collected_messages.append(content)
assert isinstance(content, ChatMessageContent)
assert content.metadata.get("message_id") == "msg_1"
# region Security tests for tools override and function_choice_behavior
async def test_validate_function_choice_behavior_rejects_required():
"""Required FCB is not supported for agent invocations."""
with pytest.raises(AgentInvokeException, match="not supported"):
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.Required())
async def test_validate_function_choice_behavior_accepts_auto():
"""Auto FCB should be accepted without error."""
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.Auto())
async def test_validate_function_choice_behavior_rejects_none_invoke():
"""NoneInvoke FCB is not supported for agent invocations."""
with pytest.raises(AgentInvokeException, match="not supported"):
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.NoneInvoke())
async def test_validate_function_choice_behavior_accepts_none():
"""None (no FCB) should be accepted."""
AgentThreadActions._validate_function_choice_behavior(None)
async def test_validate_function_choice_behavior_rejects_auto_invoke_false():
"""Auto with auto_invoke=False is not supported for agent invocations."""
with pytest.raises(AgentInvokeException, match="auto_invoke"):
AgentThreadActions._validate_function_choice_behavior(FunctionChoiceBehavior.Auto(auto_invoke=False))
async def test_validate_function_choice_behavior_rejects_empty_filters():
"""Empty filters dict should be rejected."""
fcb = FunctionChoiceBehavior.Auto()
fcb.filters = {}
with pytest.raises(AgentInvokeException, match="must not be empty"):
AgentThreadActions._validate_function_choice_behavior(fcb)
async def test_validate_function_choice_behavior_rejects_unknown_filter_keys():
"""Unknown filter keys should be rejected."""
fcb = FunctionChoiceBehavior.Auto()
# Bypass Pydantic validation to simulate a mistyped key reaching the validator
object.__setattr__(fcb, "filters", {"include_functions": ["foo"]})
with pytest.raises(AgentInvokeException, match="Unknown filter key"):
AgentThreadActions._validate_function_choice_behavior(fcb)
async def test_validate_function_choice_behavior_accepts_valid_filters():
"""Valid filter keys should be accepted."""
AgentThreadActions._validate_function_choice_behavior(
FunctionChoiceBehavior.Auto(filters={"included_functions": ["plugin-func"]})
)
async def test_get_tools_with_tools_override(ai_project_client, ai_agent_definition):
"""When tools_override is provided, it should replace agent.definition.tools."""
from azure.ai.agents.models import CodeInterpreterToolDefinition
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
kernel = MagicMock(spec=Kernel)
kernel.get_full_list_of_function_metadata.return_value = []
override_tool = CodeInterpreterToolDefinition()
tools = AgentThreadActions._get_tools(agent=agent, kernel=kernel, tools_override=[override_tool])
# Should contain the override tool, not agent.definition.tools
assert any(
(isinstance(t, CodeInterpreterToolDefinition) or (isinstance(t, dict) and t.get("type") == "code_interpreter"))
for t in tools
)
async def test_get_tools_with_fcb_filters(ai_project_client, ai_agent_definition):
"""When function_choice_behavior has filters, only matching functions should be included."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
kernel = MagicMock(spec=Kernel)
# Simulate filtered metadata
mock_metadata = MagicMock()
mock_metadata.fully_qualified_name = "Plugin-AllowedFunc"
mock_metadata.name = "AllowedFunc"
mock_metadata.plugin_name = "Plugin"
mock_metadata.description = "An allowed function"
mock_metadata.parameters = []
mock_metadata.is_prompt = False
mock_metadata.return_parameter = MagicMock()
mock_metadata.return_parameter.description = ""
mock_metadata.return_parameter.type_ = "str"
mock_metadata.additional_properties = {}
kernel.get_list_of_function_metadata.return_value = [mock_metadata]
kernel.get_full_list_of_function_metadata.return_value = []
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-AllowedFunc"]})
AgentThreadActions._get_tools(agent=agent, kernel=kernel, function_choice_behavior=fcb)
# Should have called get_list_of_function_metadata with the filters
kernel.get_list_of_function_metadata.assert_called_once_with(fcb.filters)
async def test_get_tools_with_fcb_disable_kernel_functions(ai_project_client, ai_agent_definition):
"""When enable_kernel_functions=False, no kernel functions should be included."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
kernel = MagicMock(spec=Kernel)
fcb = FunctionChoiceBehavior.Auto(enable_kernel_functions=False)
AgentThreadActions._get_tools(agent=agent, kernel=kernel, function_choice_behavior=fcb)
# Full list is called for validation, but filtered list should not be called
kernel.get_full_list_of_function_metadata.assert_called_once()
kernel.get_list_of_function_metadata.assert_not_called()
async def test_invoke_function_calls_passes_function_behavior():
"""_invoke_function_calls should pass function_behavior to kernel.invoke_function_call."""
mock_kernel = AsyncMock(spec=Kernel)
mock_kernel.invoke_function_call.return_value = None
fcc = FunctionCallContent(name="Plugin-Func", arguments={}, id="call1")
from semantic_kernel.contents.chat_history import ChatHistory
chat_history = ChatHistory()
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-Func"]})
await AgentThreadActions._invoke_function_calls(
kernel=mock_kernel,
fccs=[fcc],
chat_history=chat_history,
arguments=KernelArguments(),
function_choice_behavior=fcb,
)
mock_kernel.invoke_function_call.assert_awaited_once()
call_kwargs = mock_kernel.invoke_function_call.call_args
assert call_kwargs.kwargs.get("function_behavior") is fcb
async def test_invoke_function_calls_passes_disabled_kernel_functions():
"""_invoke_function_calls should pass enable_kernel_functions=False FCB to kernel."""
mock_kernel = AsyncMock(spec=Kernel)
mock_kernel.invoke_function_call.return_value = None
fcc = FunctionCallContent(name="Plugin-Func", arguments={}, id="call1")
from semantic_kernel.contents.chat_history import ChatHistory
chat_history = ChatHistory()
fcb = FunctionChoiceBehavior.Auto(enable_kernel_functions=False)
await AgentThreadActions._invoke_function_calls(
kernel=mock_kernel,
fccs=[fcc],
chat_history=chat_history,
arguments=KernelArguments(),
function_choice_behavior=fcb,
)
mock_kernel.invoke_function_call.assert_awaited_once()
call_kwargs = mock_kernel.invoke_function_call.call_args
passed_behavior = call_kwargs.kwargs.get("function_behavior")
assert passed_behavior is fcb
assert not passed_behavior.enable_kernel_functions
async def test_invoke_function_calls_blocks_disallowed_function():
"""A real Kernel should block a function call not in the FCB allowlist.
This verifies that the enforcement in kernel.invoke_function_call actually
rejects a disallowed function name when filters are provided, rather than
only asserting that the kwarg is forwarded.
"""
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
@kernel_function
def allowed_func() -> str:
return "allowed"
@kernel_function
def disallowed_func() -> str:
return "disallowed"
kernel = Kernel()
kernel.add_plugin(
KernelPlugin(
name="Plugin",
functions=[
KernelFunctionFromMethod(method=allowed_func, plugin_name="Plugin"),
KernelFunctionFromMethod(method=disallowed_func, plugin_name="Plugin"),
],
)
)
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-allowed_func"]})
# Call a function NOT in the allowlist
fcc = FunctionCallContent(
name="Plugin-disallowed_func",
plugin_name="Plugin",
function_name="disallowed_func",
arguments={},
id="call1",
)
chat_history = ChatHistory()
result = await kernel.invoke_function_call(
function_call=fcc,
chat_history=chat_history,
function_behavior=fcb,
)
# invoke_function_call catches the FunctionExecutionException and returns None,
# adding an error message to chat_history instead of raising.
assert result is None
assert len(chat_history.messages) == 1
result_item = chat_history.messages[0].items[0]
assert "not part of the provided tools" in str(result_item.result)
async def test_invoke_function_calls_allows_permitted_function():
"""A real Kernel should allow a function call that IS in the FCB allowlist."""
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
@kernel_function
def allowed_func() -> str:
return "ok"
@kernel_function
def other_func() -> str:
return "other"
kernel = Kernel()
kernel.add_plugin(
KernelPlugin(
name="Plugin",
functions=[
KernelFunctionFromMethod(method=allowed_func, plugin_name="Plugin"),
KernelFunctionFromMethod(method=other_func, plugin_name="Plugin"),
],
)
)
fcb = FunctionChoiceBehavior.Auto(filters={"included_functions": ["Plugin-allowed_func"]})
fcc = FunctionCallContent(
name="Plugin-allowed_func",
plugin_name="Plugin",
function_name="allowed_func",
arguments={},
id="call1",
)
chat_history = ChatHistory()
await kernel.invoke_function_call(
function_call=fcc,
chat_history=chat_history,
function_behavior=fcb,
)
# Should succeed — the function result should be in chat_history
assert len(chat_history.messages) == 1
result_item = chat_history.messages[0].items[0]
assert "ok" in str(result_item.result)
async def test_invoke_raises_for_non_auto_fcb(ai_project_client, ai_agent_definition):
"""Calling AgentThreadActions.invoke() with a non-Auto FCB should raise before any API call."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = AsyncMock()
with pytest.raises(AgentInvokeException, match="not supported"):
async for _ in AgentThreadActions.invoke(
agent=agent,
thread_id="thread123",
kernel=Kernel(),
function_choice_behavior=FunctionChoiceBehavior.Required(),
):
pass
# No API calls should have been made
agent.client.agents.runs.create.assert_not_awaited()
async def test_invoke_stream_raises_for_non_auto_fcb(ai_project_client, ai_agent_definition):
"""Calling AgentThreadActions.invoke_stream() with a non-Auto FCB should raise before any API call."""
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
agent.client.agents = AsyncMock()
with pytest.raises(AgentInvokeException, match="not supported"):
async for _ in AgentThreadActions.invoke_stream(
agent=agent,
thread_id="thread123",
kernel=Kernel(),
function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
):
pass
# No API calls should have been made
agent.client.agents.create_stream.assert_not_called()
# endregion
@@ -0,0 +1,478 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest
from azure.ai.projects.aio import AIProjectClient
from azure.core.credentials_async import AsyncTokenCredential
from semantic_kernel.agents.agent import AgentResponseItem
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent, AzureAIAgentThread
from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
async def test_azure_ai_agent_init(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
assert agent.id == "agent123"
assert agent.name == "agentName"
assert agent.description == "desc"
async def test_azure_ai_agent_init_with_plugins_via_constructor(
ai_project_client, ai_agent_definition, custom_plugin_class
):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition, plugins=[custom_plugin_class()])
assert agent.id == "agent123"
assert agent.name == "agentName"
assert agent.description == "desc"
assert agent.kernel.plugins is not None
assert len(agent.kernel.plugins) == 1
async def test_azure_ai_agent_get_response(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
response = await agent.get_response(messages="message", thread=thread)
assert response.message.role == AuthorRole.ASSISTANT
assert response.message.content == "content"
assert response.thread is not None
async def test_azure_ai_agent_get_response_exception(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
async def fake_invoke(*args, **kwargs):
yield False, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with (
patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
),
pytest.raises(AgentInvokeException),
):
await agent.get_response(messages="message", thread=thread)
async def test_azure_ai_agent_invoke(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
results = []
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for item in agent.invoke(messages="message", thread=thread):
results.append(item)
assert len(results) == 1
async def test_azure_ai_agent_invoke_yields_visible_assistant_message(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
results = []
assistant_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="assistant says hi")
async def fake_invoke(*args, **kwargs):
yield True, assistant_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for item in agent.invoke(messages="message", thread=thread):
results.append(item)
assert len(results) == 1
assert results[0].message is assistant_msg
async def test_azure_ai_agent_invoke_emits_tool_message_via_callback_only(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
callback_results = []
async def handle_callback(msg: ChatMessageContent) -> None:
callback_results.append(msg)
tool_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="tool call")
tool_msg.items = [FunctionCallContent(name="tool", arguments="{}")]
async def fake_invoke(*args, **kwargs):
yield False, tool_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for _ in agent.invoke(messages="message", thread=thread, on_intermediate_message=handle_callback):
pass
assert callback_results == [tool_msg]
async def test_azure_ai_agent_invoke_suppresses_tool_message_without_callback(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
tool_msg = ChatMessageContent(role=AuthorRole.ASSISTANT, content="tool call")
tool_msg.items = [FunctionCallContent(name="tool", arguments="{}")]
async def fake_invoke(*args, **kwargs):
yield False, tool_msg # Not visible, no callback
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
results = [item async for item in agent.invoke(messages="message", thread=thread)]
assert results == [] # Tool message should be suppressed
async def test_azure_ai_agent_invoke_stream(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
results = []
async def fake_invoke(*args, **kwargs):
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for item in agent.invoke_stream(messages="message", thread=thread):
results.append(item)
assert len(results) == 1
async def test_azure_ai_agent_invoke_stream_with_on_new_message_callback(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
results = []
final_chat_history = ChatHistory()
async def handle_stream_completion(message: ChatMessageContent) -> None:
final_chat_history.add_message(message)
# Fake collected messages
fake_message = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="fake content", choice_index=0)
async def fake_invoke(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(fake_message)
yield fake_message
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for item in agent.invoke_stream(
messages="message", thread=thread, on_intermediate_message=handle_stream_completion
):
results.append(item)
assert len(results) == 1
assert results[0].message.content == "fake content"
assert len(final_chat_history.messages) == 1
assert final_chat_history.messages[0].content == "fake content"
async def test_azure_ai_agent_invoke_stream_tool_message_only_goes_to_callback(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
received_callback_messages = []
async def async_append(msg: ChatMessageContent):
received_callback_messages.append(msg)
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT, content="tool call", items=[FunctionCallContent(name="ToolA", arguments="{}")]
)
streamed_msg = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT, content="assistant streaming...", choice_index=0
)
async def fake_invoke_stream(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield streamed_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke_stream,
):
results = []
async for item in agent.invoke_stream(messages="message", thread=thread, on_intermediate_message=async_append):
results.append(item)
assert results == [AgentResponseItem(message=streamed_msg, thread=thread)]
assert received_callback_messages == [tool_msg]
async def test_azure_ai_agent_invoke_stream_tool_message_suppressed_without_callback(
ai_project_client, ai_agent_definition
):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
content="tool result",
items=[FunctionResultContent(id="test-id", name="ToolA", result="result")],
)
streamed_msg = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="assistant says hi", choice_index=0)
async def fake_invoke_stream(*args, output_messages=None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield streamed_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke_stream,
):
results = []
async for item in agent.invoke_stream(messages="message", thread=thread):
results.append(item)
# Only assistant-visible content should be yielded
assert len(results) == 1
assert results[0].message == streamed_msg
async def test_azure_ai_agent_invoke_stream_mixed_messages(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
thread.id = "test_thread_id"
callback_results = []
async def async_append(msg: ChatMessageContent):
callback_results.append(msg)
tool_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT, content="tool call", items=[FunctionCallContent(name="tool", arguments="{}")]
)
text_msg = StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="streamed text", choice_index=0)
async def fake_invoke_stream(*args, output_messages: list = None, **kwargs):
if output_messages is not None:
output_messages.append(tool_msg)
yield text_msg
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke_stream,
):
results = []
async for item in agent.invoke_stream(messages="message", thread=thread, on_intermediate_message=async_append):
results.append(item)
assert callback_results == [tool_msg]
assert results == [AgentResponseItem(message=text_msg, thread=thread)]
def test_azure_ai_agent_get_channel_keys(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
keys = list(agent.get_channel_keys())
assert len(keys) >= 2
async def test_azure_ai_agent_create_channel(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
with (
patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.create_thread",
side_effect="t",
),
patch(
"semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentThread.create",
new_callable=AsyncMock,
),
patch(
"semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentThread.id",
new_callable=PropertyMock,
) as mock_id,
):
mock_id.return_value = "mock-thread-id"
ch = await agent.create_channel()
assert isinstance(ch, AgentChannel)
assert ch.thread_id == "mock-thread-id"
def test_create_client_with_explicit_endpoint():
credential = MagicMock(spec=AsyncTokenCredential)
with patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AIProjectClient") as mock_client_cls:
mock_client = MagicMock(spec=AIProjectClient)
mock_client_cls.return_value = mock_client
result = AzureAIAgent.create_client(
credential=credential,
endpoint="https://my-endpoint",
extra_arg="extra_value",
)
mock_client_cls.assert_called_once()
_, kwargs = mock_client_cls.call_args
assert kwargs["credential"] is credential
assert kwargs["endpoint"] == "https://my-endpoint"
assert kwargs["extra_arg"] == "extra_value"
assert result is mock_client
def test_create_client_uses_settings_when_endpoint_none():
credential = MagicMock(spec=AsyncTokenCredential)
with (
patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentSettings") as mock_settings_cls,
patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AIProjectClient") as mock_client_cls,
):
mock_settings = MagicMock()
mock_settings.endpoint = "https://configured-endpoint"
mock_settings_cls.return_value = mock_settings
mock_client = MagicMock(spec=AIProjectClient)
mock_client_cls.return_value = mock_client
result = AzureAIAgent.create_client(credential=credential)
mock_client_cls.assert_called_once()
_, kwargs = mock_client_cls.call_args
assert kwargs["endpoint"] == "https://configured-endpoint"
assert result is mock_client
def test_create_client_raises_if_no_endpoint():
credential = MagicMock(spec=AsyncTokenCredential)
with patch("semantic_kernel.agents.azure_ai.azure_ai_agent.AzureAIAgentSettings") as mock_settings_cls:
mock_settings = MagicMock()
mock_settings.endpoint = None
mock_settings_cls.return_value = mock_settings
try:
AzureAIAgent.create_client(credential=credential)
except AgentInitializationException as e:
assert "Azure AI endpoint" in str(e)
else:
assert False, "Expected AgentInitializationException to be raised"
async def test_azure_ai_agent_get_response_passes_function_choice_behavior(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
await agent.get_response(messages="message", thread=thread, function_choice_behavior=fcb)
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_azure_ai_agent_invoke_passes_function_choice_behavior(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
async for _ in agent.invoke(messages="message", thread=thread, function_choice_behavior=fcb):
pass
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_azure_ai_agent_invoke_stream_passes_function_choice_behavior(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
fcb = FunctionChoiceBehavior.Auto()
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
async for _ in agent.invoke_stream(messages="message", thread=thread, function_choice_behavior=fcb):
pass
assert captured_kwargs.get("function_choice_behavior") is fcb
async def test_azure_ai_agent_get_response_no_fcb_passes_none(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
thread = AsyncMock(spec=AzureAIAgentThread)
captured_kwargs = {}
async def fake_invoke(*args, **kwargs):
captured_kwargs.update(kwargs)
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
await agent.get_response(messages="message", thread=thread)
assert captured_kwargs.get("function_choice_behavior") is None
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import Field, SecretStr, ValidationError
from semantic_kernel.kernel_pydantic import KernelBaseSettings
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class AzureAIAgentSettings(KernelBaseSettings):
"""Slightly modified to ensure invalid data raises ValidationError."""
env_prefix = "AZURE_AI_AGENT_"
model_deployment_name: str = Field(min_length=1)
project_connection_string: SecretStr = Field(..., min_length=1)
def test_azure_ai_agent_settings_valid():
settings = AzureAIAgentSettings(
model_deployment_name="test_model",
project_connection_string="secret_value",
)
assert settings.model_deployment_name == "test_model"
assert settings.project_connection_string.get_secret_value() == "secret_value"
def test_azure_ai_agent_settings_invalid():
with pytest.raises(ValidationError):
# Should fail due to min_length=1 constraints
AzureAIAgentSettings(
model_deployment_name="", # empty => invalid
project_connection_string="",
)
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from azure.ai.agents.models import MessageAttachment, MessageRole
from semantic_kernel.agents.azure_ai.azure_ai_agent_utils import AzureAIAgentUtils
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
from semantic_kernel.contents.utils.author_role import AuthorRole
def test_azure_ai_agent_utils_get_thread_messages_none():
msgs = AzureAIAgentUtils.get_thread_messages([])
assert msgs is None
def test_azure_ai_agent_utils_get_thread_messages():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="Hello!")
msg1.items.append(FileReferenceContent(file_id="file123"))
results = AzureAIAgentUtils.get_thread_messages([msg1])
assert len(results) == 1
assert results[0].content == "Hello!"
assert results[0].role == MessageRole.USER
assert len(results[0].attachments) == 1
assert isinstance(results[0].attachments[0], MessageAttachment)
def test_azure_ai_agent_utils_get_attachments_empty():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="No file items")
atts = AzureAIAgentUtils.get_attachments(msg1)
assert atts == []
def test_azure_ai_agent_utils_get_attachments_file():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="One file item")
msg1.items.append(FileReferenceContent(file_id="file123"))
atts = AzureAIAgentUtils.get_attachments(msg1)
assert len(atts) == 1
assert atts[0].file_id == "file123"
def test_azure_ai_agent_utils_get_metadata():
msg1 = ChatMessageContent(role=AuthorRole.USER, content="has meta", metadata={"k": 123})
meta = AzureAIAgentUtils.get_metadata(msg1)
assert meta["k"] == "123"
def test_azure_ai_agent_utils_get_tool_definition():
gen = AzureAIAgentUtils._get_tool_definition(["file_search", "code_interpreter", "non_existent"])
# file_search & code_interpreter exist, non_existent yields nothing
tools_list = list(gen)
assert len(tools_list) == 2
@@ -0,0 +1,88 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, patch
import pytest
from azure.ai.projects.aio import AIProjectClient
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent
from semantic_kernel.agents.azure_ai.azure_ai_channel import AzureAIChannel
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
async def test_azure_ai_channel_invoke_invalid_agent():
channel = AzureAIChannel(AsyncMock(spec=AIProjectClient), "thread123")
with pytest.raises(AgentChatException):
async for _ in channel.invoke(object()):
pass
async def test_azure_ai_channel_invoke_valid_agent(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke",
side_effect=fake_invoke,
):
channel = AzureAIChannel(ai_project_client, "thread123")
results = []
async for is_visible, msg in channel.invoke(agent):
results.append((is_visible, msg))
assert len(results) == 1
async def test_azure_ai_channel_invoke_stream_valid_agent(ai_project_client, ai_agent_definition):
agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition)
async def fake_invoke(*args, **kwargs):
yield True, ChatMessageContent(role=AuthorRole.ASSISTANT, content="content")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.invoke_stream",
side_effect=fake_invoke,
):
channel = AzureAIChannel(ai_project_client, "thread123")
results = []
async for is_visible, msg in channel.invoke_stream(agent, messages=[]):
results.append((is_visible, msg))
assert len(results) == 1
async def test_azure_ai_channel_get_history():
# We need to return an async iterable, so let's do an AsyncMock returning an _async_gen
class FakeAgentClient:
delete_thread = AsyncMock()
# We'll patch get_messages directly below
class FakeClient:
agents = FakeAgentClient()
channel = AzureAIChannel(FakeClient(), "threadXYZ")
async def fake_get_messages(client, thread_id):
# Must produce an async iterable
yield ChatMessageContent(role=AuthorRole.ASSISTANT, content="Previous msg")
with patch(
"semantic_kernel.agents.azure_ai.agent_thread_actions.AgentThreadActions.get_messages",
new=fake_get_messages, # direct replacement with a coroutine
):
results = []
async for item in channel.get_history():
results.append(item)
assert len(results) == 1
assert results[0].content == "Previous msg"
# Helper for returning an async generator
async def _async_gen(items):
for i in items:
yield i
@@ -0,0 +1,187 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Callable
import pytest
from semantic_kernel.agents.bedrock.models.bedrock_agent_event_type import BedrockAgentEventType
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
from semantic_kernel.agents.bedrock.models.bedrock_agent_status import BedrockAgentStatus
from semantic_kernel.kernel import Kernel
@pytest.fixture()
def bedrock_agent_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Amazon Bedrock Agent unit tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN": "TEST_BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN",
"BEDROCK_AGENT_FOUNDATION_MODEL": "TEST_BEDROCK_AGENT_FOUNDATION_MODEL",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
@pytest.fixture
def kernel_with_function(kernel: Kernel, decorated_native_function: Callable) -> Kernel:
kernel.add_function("test_plugin", function=decorated_native_function)
return kernel
@pytest.fixture
def new_agent_name():
return "test_agent_name"
@pytest.fixture
def bedrock_agent_model():
return BedrockAgentModel(
agent_name="test_agent_name",
foundation_model="test_foundation_model",
agent_status=BedrockAgentStatus.NOT_PREPARED,
)
@pytest.fixture
def bedrock_agent_model_with_id():
return BedrockAgentModel(
agent_id="test_agent_id",
agent_name="test_agent_name",
foundation_model="test_foundation_model",
agent_status=BedrockAgentStatus.NOT_PREPARED,
)
@pytest.fixture
def bedrock_agent_model_with_id_prepared_dict():
return {
"agent": {
"agentId": "test_agent_id",
"agentName": "test_agent_name",
"foundationModel": "test_foundation_model",
"agentStatus": "PREPARED",
}
}
@pytest.fixture
def bedrock_agent_model_with_id_preparing_dict():
return {
"agent": {
"agentId": "test_agent_id",
"agentName": "test_agent_name",
"foundationModel": "test_foundation_model",
"agentStatus": "PREPARING",
}
}
@pytest.fixture
def bedrock_agent_model_with_id_not_prepared_dict():
return {
"agent": {
"agentId": "test_agent_id",
"agentName": "test_agent_name",
"foundationModel": "test_foundation_model",
"agentStatus": "NOT_PREPARED",
}
}
@pytest.fixture
def existing_agent_not_prepared_model():
return BedrockAgentModel(
agent_id="test_agent_id",
agent_name="test_agent_name",
foundation_model="test_foundation_model",
agent_status=BedrockAgentStatus.NOT_PREPARED,
)
@pytest.fixture
def bedrock_action_group_mode_dict():
return {
"agentActionGroup": {
"actionGroupId": "test_action_group_id",
"actionGroupName": "test_action_group_name",
}
}
@pytest.fixture
def simple_response():
return "test response"
@pytest.fixture
def bedrock_agent_non_streaming_empty_response():
return {
"completion": [],
}
@pytest.fixture
def bedrock_agent_non_streaming_simple_response(simple_response):
return {
"completion": [
{
"chunk": {"bytes": bytes(simple_response, "utf-8")},
},
],
}
@pytest.fixture
def bedrock_agent_streaming_simple_response(simple_response):
return {
"completion": [
{
"chunk": {"bytes": bytes(chunk, "utf-8")},
}
for chunk in simple_response
]
}
@pytest.fixture
def bedrock_agent_function_call_response():
return {
"completion": [
{
BedrockAgentEventType.RETURN_CONTROL: {
"invocationId": "test_invocation_id",
"invocationInputs": [
{
"functionInvocationInput": {
"function": "test_function",
"parameters": [
{"name": "test_parameter_name", "value": "test_parameter_value"},
],
},
},
],
},
},
],
}
@pytest.fixture
def bedrock_agent_create_session_response():
return {
"sessionId": "test_session_id",
}
@@ -0,0 +1,93 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.agents.bedrock.action_group_utils import (
BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES,
kernel_function_parameter_type_to_bedrock_function_parameter_type,
kernel_function_to_bedrock_function_schema,
parse_function_result_contents,
parse_return_control_payload,
)
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.kernel import Kernel
def test_kernel_function_to_bedrock_function_schema(kernel_with_function: Kernel):
# Test the conversion of kernel function to bedrock function schema
function_choice_behavior = FunctionChoiceBehavior.Auto()
function_choice_configuration = function_choice_behavior.get_config(kernel_with_function)
result = kernel_function_to_bedrock_function_schema(function_choice_configuration)
assert result == {
"functions": [
{
"name": "test_plugin-getLightStatus",
"parameters": {
"arg1": {
"type": "string",
"required": True,
}
},
"requireConfirmation": "DISABLED",
}
]
}
def test_kernel_function_parameter_type_to_bedrock_function_parameter_type():
# Test the conversion of kernel function parameter type to bedrock function parameter type
schema_data = {"type": "string"}
result = kernel_function_parameter_type_to_bedrock_function_parameter_type(schema_data)
assert result == "string"
def test_kernel_function_parameter_type_to_bedrock_function_parameter_type_invalid():
# Test the conversion of invalid kernel function parameter type to bedrock function parameter type
schema_data = {"type": "invalid_type"}
with pytest.raises(
ValueError,
match="Type invalid_type is not allowed in bedrock function parameter type. "
f"Allowed types are {BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES}.",
):
kernel_function_parameter_type_to_bedrock_function_parameter_type(schema_data)
def test_parse_return_control_payload():
# Test the parsing of return control payload to function call contents
return_control_payload = {
"invocationId": "test_invocation_id",
"invocationInputs": [
{
"functionInvocationInput": {
"function": "test_function",
"parameters": [
{"name": "param1", "value": "value1"},
{"name": "param2", "value": "value2"},
],
}
}
],
}
result = parse_return_control_payload(return_control_payload)
assert len(result) == 1
assert result[0].id == "test_invocation_id"
assert result[0].name == "test_function"
assert result[0].arguments == {"param1": "value1", "param2": "value2"}
def test_parse_function_result_contents():
# Test the parsing of function result contents to be returned to the agent
function_result_contents = [
FunctionResultContent(
id="test_id",
name="test_function",
result="test_result",
metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
)
]
result = parse_function_result_contents(function_result_contents)
assert len(result) == 1
assert result[0]["functionResult"]["actionGroup"] == "test_action_group"
assert result[0]["functionResult"]["function"] == "test_function"
assert result[0]["functionResult"]["responseBody"]["TEXT"]["body"] == "test_result"
@@ -0,0 +1,33 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import ValidationError
from semantic_kernel.agents.bedrock.models.bedrock_action_group_model import BedrockActionGroupModel
def test_bedrock_action_group_model_valid():
"""Test case to verify the BedrockActionGroupModel with valid data."""
model = BedrockActionGroupModel(actionGroupId="test_id", actionGroupName="test_name")
assert model.action_group_id == "test_id"
assert model.action_group_name == "test_name"
def test_bedrock_action_group_model_missing_action_group_id():
"""Test case to verify error handling when actionGroupId is missing."""
with pytest.raises(ValidationError):
BedrockActionGroupModel(actionGroupName="test_name")
def test_bedrock_action_group_model_missing_action_group_name():
"""Test case to verify error handling when actionGroupName is missing."""
with pytest.raises(ValidationError):
BedrockActionGroupModel(actionGroupId="test_id")
def test_bedrock_action_group_model_extra_field():
"""Test case to verify the BedrockActionGroupModel with an extra field."""
model = BedrockActionGroupModel(actionGroupId="test_id", actionGroupName="test_name", extraField="extra_value")
assert model.action_group_id == "test_id"
assert model.action_group_name == "test_name"
assert model.extraField == "extra_value"
@@ -0,0 +1,751 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
import boto3
import pytest
from semantic_kernel.agents.bedrock.action_group_utils import (
kernel_function_to_bedrock_function_schema,
parse_function_result_contents,
)
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent, BedrockAgentThread
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
# region Agent Initialization Tests
# Test case to verify BedrockAgent initialization
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_initialization(client, bedrock_agent_model_with_id):
agent = BedrockAgent(bedrock_agent_model_with_id)
assert agent.name == bedrock_agent_model_with_id.agent_name
assert agent.agent_model.agent_name == bedrock_agent_model_with_id.agent_name
assert agent.agent_model.agent_id == bedrock_agent_model_with_id.agent_id
assert agent.agent_model.foundation_model == bedrock_agent_model_with_id.foundation_model
# Test case to verify error handling during BedrockAgent initialization with non-auto function choice
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_initialization_error_with_non_auto_function_choice(client, bedrock_agent_model_with_id):
with pytest.raises(ValueError, match="Only FunctionChoiceType.AUTO is supported."):
BedrockAgent(
bedrock_agent_model_with_id,
function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
)
# Test case to verify the creation of BedrockAgent
@patch.object(boto3, "client", return_value=Mock())
@pytest.mark.parametrize(
"kernel, function_choice_behavior, arguments",
[
(None, None, None),
(Kernel(), None, None),
(Kernel(), FunctionChoiceBehavior.Auto(), None),
(Kernel(), FunctionChoiceBehavior.Auto(), KernelArguments()),
],
)
async def test_bedrock_agent_create_and_prepare_agent(
client,
bedrock_agent_model_with_id_not_prepared_dict,
bedrock_agent_unit_test_env,
kernel,
function_choice_behavior,
arguments,
):
with (
patch.object(client, "create_agent") as mock_create_agent,
patch.object(BedrockAgent, "_wait_for_agent_status", new_callable=AsyncMock),
patch.object(BedrockAgent, "prepare_agent_and_wait_until_prepared", new_callable=AsyncMock),
):
mock_create_agent.return_value = bedrock_agent_model_with_id_not_prepared_dict
agent = await BedrockAgent.create_and_prepare_agent(
name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
instructions="test_instructions",
bedrock_client=client,
env_file_path="fake_path",
kernel=kernel,
function_choice_behavior=function_choice_behavior,
arguments=arguments,
)
mock_create_agent.assert_called_once_with(
agentName=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
foundationModel=bedrock_agent_unit_test_env["BEDROCK_AGENT_FOUNDATION_MODEL"],
agentResourceRoleArn=bedrock_agent_unit_test_env["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
instruction="test_instructions",
)
assert agent.agent_model.agent_id == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentId"]
assert agent.id == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentId"]
assert agent.agent_model.agent_name == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"]
assert agent.name == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"]
assert (
agent.agent_model.foundation_model
== bedrock_agent_model_with_id_not_prepared_dict["agent"]["foundationModel"]
)
assert agent.kernel is not None
assert agent.function_choice_behavior is not None
if arguments:
assert agent.arguments is not None
# Test case to verify the creation of BedrockAgent
@pytest.mark.parametrize(
"exclude_list",
[
["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
["BEDROCK_AGENT_FOUNDATION_MODEL"],
],
indirect=True,
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_create_and_prepare_agent_settings_validation_error(
client,
bedrock_agent_model_with_id_not_prepared_dict,
bedrock_agent_unit_test_env,
):
with pytest.raises(AgentInitializationException):
await BedrockAgent.create_and_prepare_agent(
name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
instructions="test_instructions",
env_file_path="fake_path",
)
# Test case to verify the creation of BedrockAgent
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_create_and_prepare_agent_service_exception(
client,
bedrock_agent_model_with_id_not_prepared_dict,
bedrock_agent_unit_test_env,
):
with (
patch.object(client, "create_agent") as mock_create_agent,
patch.object(BedrockAgent, "prepare_agent_and_wait_until_prepared", new_callable=AsyncMock),
):
from botocore.exceptions import ClientError
mock_create_agent.side_effect = ClientError({}, "create_agent")
with pytest.raises(AgentInitializationException):
await BedrockAgent.create_and_prepare_agent(
name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
instructions="test_instructions",
bedrock_client=client,
env_file_path="fake_path",
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_prepare_agent_and_wait_until_prepared(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_prepared_dict,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with (
patch.object(client, "get_agent") as mock_get_agent,
patch.object(client, "prepare_agent") as mock_prepare_agent,
):
mock_get_agent.side_effect = [
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_prepared_dict,
]
await agent.prepare_agent_and_wait_until_prepared()
mock_prepare_agent.assert_called_once_with(agentId=bedrock_agent_model_with_id.agent_id)
assert mock_get_agent.call_count == 2
assert agent.agent_model.agent_status == "PREPARED"
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_prepare_agent_and_wait_until_prepared_fail(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_model_with_id_preparing_dict,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with (
patch.object(client, "get_agent") as mock_get_agent,
patch.object(client, "prepare_agent"),
):
mock_get_agent.side_effect = [
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_preparing_dict,
bedrock_agent_model_with_id_preparing_dict,
]
with pytest.raises(TimeoutError):
await agent.prepare_agent_and_wait_until_prepared()
# Test case to verify the creation of a code interpreter action group
@patch.object(boto3, "client", return_value=Mock())
async def test_create_code_interpreter_action_group(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_action_group_mode_dict,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with (
patch.object(client, "create_agent_action_group") as mock_create_action_group,
patch.object(
BedrockAgent, "prepare_agent_and_wait_until_prepared"
) as mock_prepare_agent_and_wait_until_prepared,
):
mock_create_action_group.return_value = bedrock_action_group_mode_dict
action_group_model = await agent.create_code_interpreter_action_group()
mock_create_action_group.assert_called_once_with(
agentId=agent.agent_model.agent_id,
agentVersion=agent.agent_model.agent_version or "DRAFT",
actionGroupName=f"{agent.agent_model.agent_name}_code_interpreter",
actionGroupState="ENABLED",
parentActionGroupSignature="AMAZON.CodeInterpreter",
)
assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
# Test case to verify the creation of BedrockAgent with plugins
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_create_with_plugin_via_constructor(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
custom_plugin_class,
):
agent = BedrockAgent(
bedrock_agent_model_with_id,
plugins=[custom_plugin_class()],
bedrock_client=client,
)
assert agent.kernel.plugins is not None
assert len(agent.kernel.plugins) == 1
# Test case to verify the creation of a user input action group
@patch.object(boto3, "client", return_value=Mock())
async def test_create_user_input_action_group(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_action_group_mode_dict,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with (
patch.object(agent.bedrock_client, "create_agent_action_group") as mock_create_action_group,
patch.object(
BedrockAgent, "prepare_agent_and_wait_until_prepared"
) as mock_prepare_agent_and_wait_until_prepared,
):
mock_create_action_group.return_value = bedrock_action_group_mode_dict
action_group_model = await agent.create_user_input_action_group()
mock_create_action_group.assert_called_once_with(
agentId=agent.agent_model.agent_id,
agentVersion=agent.agent_model.agent_version or "DRAFT",
actionGroupName=f"{agent.agent_model.agent_name}_user_input",
actionGroupState="ENABLED",
parentActionGroupSignature="AMAZON.UserInput",
)
assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
# Test case to verify the creation of a kernel function action group
@patch.object(boto3, "client", return_value=Mock())
async def test_create_kernel_function_action_group(
client,
kernel_with_function,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_action_group_mode_dict,
):
agent = BedrockAgent(bedrock_agent_model_with_id, kernel=kernel_with_function, bedrock_client=client)
with (
patch.object(agent.bedrock_client, "create_agent_action_group") as mock_create_action_group,
patch.object(
BedrockAgent, "prepare_agent_and_wait_until_prepared"
) as mock_prepare_agent_and_wait_until_prepared,
):
mock_create_action_group.return_value = bedrock_action_group_mode_dict
action_group_model = await agent.create_kernel_function_action_group()
mock_create_action_group.assert_called_once_with(
agentId=agent.agent_model.agent_id,
agentVersion=agent.agent_model.agent_version or "DRAFT",
actionGroupName=f"{agent.agent_model.agent_name}_kernel_function",
actionGroupState="ENABLED",
actionGroupExecutor={"customControl": "RETURN_CONTROL"},
functionSchema=kernel_function_to_bedrock_function_schema(
agent.function_choice_behavior.get_config(kernel_with_function)
),
)
assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
# Test case to verify the association of an agent with a knowledge base
@patch.object(boto3, "client", return_value=Mock())
async def test_associate_agent_knowledge_base(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with (
patch.object(agent.bedrock_client, "associate_agent_knowledge_base") as mock_associate_knowledge_base,
patch.object(
BedrockAgent, "prepare_agent_and_wait_until_prepared"
) as mock_prepare_agent_and_wait_until_prepared,
):
await agent.associate_agent_knowledge_base("test_knowledge_base_id")
mock_associate_knowledge_base.assert_called_once_with(
agentId=agent.agent_model.agent_id,
agentVersion=agent.agent_model.agent_version,
knowledgeBaseId="test_knowledge_base_id",
)
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
# Test case to verify the disassociation of an agent with a knowledge base
@patch.object(boto3, "client", return_value=Mock())
async def test_disassociate_agent_knowledge_base(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with (
patch.object(agent.bedrock_client, "disassociate_agent_knowledge_base") as mock_disassociate_knowledge_base,
patch.object(
BedrockAgent, "prepare_agent_and_wait_until_prepared"
) as mock_prepare_agent_and_wait_until_prepared,
):
await agent.disassociate_agent_knowledge_base("test_knowledge_base_id")
mock_disassociate_knowledge_base.assert_called_once_with(
agentId=agent.agent_model.agent_id,
agentVersion=agent.agent_model.agent_version,
knowledgeBaseId="test_knowledge_base_id",
)
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
# Test case to verify listing associated knowledge bases with an agent
@patch.object(boto3, "client", return_value=Mock())
async def test_list_associated_agent_knowledge_bases(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with patch.object(agent.bedrock_client, "list_agent_knowledge_bases") as mock_list_knowledge_bases:
await agent.list_associated_agent_knowledge_bases()
mock_list_knowledge_bases.assert_called_once_with(
agentId=agent.agent_model.agent_id,
agentVersion=agent.agent_model.agent_version,
)
# endregion
# region Agent Deletion Tests
@patch.object(boto3, "client", return_value=Mock())
async def test_delete_agent(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
agent_id = bedrock_agent_model_with_id.agent_id
with patch.object(agent.bedrock_client, "delete_agent") as mock_delete_agent:
await agent.delete_agent()
mock_delete_agent.assert_called_once_with(agentId=agent_id)
assert agent.agent_model.agent_id is None
# Test case to verify error handling when deleting an agent that does not exist
@patch.object(boto3, "client", return_value=Mock())
async def test_delete_agent_twice_error(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with patch.object(agent.bedrock_client, "delete_agent"):
await agent.delete_agent()
with pytest.raises(ValueError):
await agent.delete_agent()
# Test case to verify error handling when there is a client error during agent deletion
@patch.object(boto3, "client", return_value=Mock())
async def test_delete_agent_client_error(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
with patch.object(agent.bedrock_client, "delete_agent") as mock_delete_agent:
from botocore.exceptions import ClientError
mock_delete_agent.side_effect = ClientError({"Error": {"Code": "500"}}, "delete_agent")
with pytest.raises(ClientError):
await agent.delete_agent()
# endregion
# region Agent Invoke Tests
# Test case to verify the `get_response` method of BedrockAgent
@pytest.mark.parametrize(
"thread",
[
None,
BedrockAgentThread(None, session_id="test_session_id"),
],
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_get_response(
client,
thread,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_non_streaming_simple_response,
simple_response,
):
with (
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
patch(
"semantic_kernel.agents.bedrock.bedrock_agent.BedrockAgentThread.id",
new_callable=PropertyMock,
) as mock_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id)
mock_id.return_value = "mock-thread-id"
mock_invoke_agent.return_value = bedrock_agent_non_streaming_simple_response
mock_start.return_value = "test_session_id"
response = await agent.get_response(messages="test_input_text", thread=thread)
assert response.message.content == simple_response
mock_invoke_agent.assert_called_once()
# Test case to verify the `get_response` method of BedrockAgent
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_get_response_exception(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_non_streaming_empty_response,
):
with (
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
patch(
"semantic_kernel.agents.bedrock.bedrock_agent.BedrockAgentThread.id",
new_callable=PropertyMock,
) as mock_id,
):
agent = BedrockAgent(bedrock_agent_model_with_id)
mock_id.return_value = "mock-thread-id"
mock_invoke_agent.return_value = bedrock_agent_non_streaming_empty_response
mock_start.return_value = "test_session_id"
with pytest.raises(AgentInvokeException):
await agent.get_response(messages="test_input_text")
# Test case to verify the invocation of BedrockAgent
@pytest.mark.parametrize(
"thread",
[
None,
BedrockAgentThread(None, session_id="test_session_id"),
],
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke(
client,
thread,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_non_streaming_simple_response,
simple_response,
):
with (
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
patch.object(BedrockAgentThread, "id", "test_session_id"),
):
agent = BedrockAgent(bedrock_agent_model_with_id)
mock_invoke_agent.return_value = bedrock_agent_non_streaming_simple_response
mock_start.return_value = "test_session_id"
async for response in agent.invoke(messages="test_input_text", thread=thread):
assert response.message.content == simple_response
mock_invoke_agent.assert_called_once_with(
"test_session_id",
"test_input_text",
None,
streamingConfigurations={"streamFinalResponse": False},
sessionState={},
)
# Test case to verify the streaming invocation of BedrockAgent
@pytest.mark.parametrize(
"thread",
[
None,
BedrockAgentThread(None, session_id="test_session_id"),
],
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke_stream(
client,
thread,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_streaming_simple_response,
simple_response,
):
with (
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
patch.object(BedrockAgentThread, "id", "test_session_id"),
):
agent = BedrockAgent(bedrock_agent_model_with_id)
mock_invoke_agent.return_value = bedrock_agent_streaming_simple_response
mock_start.return_value = "test_session_id"
full_message = ""
async for response in agent.invoke_stream(messages="test_input_text", thread=thread):
full_message += response.message.content
assert full_message == simple_response
mock_invoke_agent.assert_called_once_with(
"test_session_id",
"test_input_text",
None,
streamingConfigurations={"streamFinalResponse": True},
sessionState={},
)
# Test case to verify the invocation of BedrockAgent with function call
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke_with_function_call(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_function_call_response,
bedrock_agent_non_streaming_simple_response,
):
with (
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
patch.object(BedrockAgent, "_handle_function_call_contents") as mock_handle_function_call_contents,
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
patch.object(BedrockAgentThread, "id", "test_session_id"),
):
agent = BedrockAgent(bedrock_agent_model_with_id)
function_result_contents = [
FunctionResultContent(
id="test_id",
name="test_function",
result="test_result",
metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
)
]
mock_handle_function_call_contents.return_value = function_result_contents
agent.function_choice_behavior.maximum_auto_invoke_attempts = 2
mock_invoke_agent.side_effect = [
bedrock_agent_function_call_response,
bedrock_agent_non_streaming_simple_response,
]
mock_start.return_value = "test_session_id"
async for _ in agent.invoke(messages="test_input_text"):
mock_invoke_agent.assert_called_with(
"test_session_id",
"test_input_text",
None,
streamingConfigurations={"streamFinalResponse": False},
sessionState={
"invocationId": "test_invocation_id",
"returnControlInvocationResults": parse_function_result_contents(function_result_contents),
},
)
# Test case to verify the streaming invocation of BedrockAgent with function call
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke_stream_with_function_call(
client,
bedrock_agent_unit_test_env,
bedrock_agent_model_with_id,
bedrock_agent_function_call_response,
bedrock_agent_streaming_simple_response,
):
with (
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
patch.object(BedrockAgent, "_handle_function_call_contents") as mock_handle_function_call_contents,
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
patch.object(BedrockAgentThread, "id", "test_session_id"),
):
agent = BedrockAgent(bedrock_agent_model_with_id)
function_result_contents = [
FunctionResultContent(
id="test_id",
name="test_function",
result="test_result",
metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
)
]
mock_handle_function_call_contents.return_value = function_result_contents
agent.function_choice_behavior.maximum_auto_invoke_attempts = 2
mock_invoke_agent.side_effect = [
bedrock_agent_function_call_response,
bedrock_agent_streaming_simple_response,
]
mock_start.return_value = "test_session_id"
async for _ in agent.invoke_stream(messages="test_input_text"):
mock_invoke_agent.assert_called_with(
"test_session_id",
"test_input_text",
None,
streamingConfigurations={"streamFinalResponse": True},
sessionState={
"invocationId": "test_invocation_id",
"returnControlInvocationResults": parse_function_result_contents(function_result_contents),
},
)
# endregion
# region Filename Sanitization Tests
def test_sanitize_filename_simple():
"""Test _sanitize_filename with a simple filename."""
assert BedrockAgent._sanitize_filename("file.txt") == "file.txt"
def test_sanitize_filename_with_spaces():
"""Test _sanitize_filename with spaces in filename."""
assert BedrockAgent._sanitize_filename("my file.txt") == "my file.txt"
def test_sanitize_filename_directory_traversal_unix():
"""Test _sanitize_filename strips Unix-style directory traversal."""
assert BedrockAgent._sanitize_filename("../../../etc/passwd") == "passwd"
assert BedrockAgent._sanitize_filename("../../file.txt") == "file.txt"
assert BedrockAgent._sanitize_filename("/etc/passwd") == "passwd"
def test_sanitize_filename_directory_traversal_windows():
"""Test _sanitize_filename strips Windows-style directory traversal."""
assert BedrockAgent._sanitize_filename("..\\..\\..\\Windows\\System32\\config") == "config"
assert BedrockAgent._sanitize_filename("C:\\Users\\file.txt") == "file.txt"
assert BedrockAgent._sanitize_filename("\\\\server\\share\\file.txt") == "file.txt"
def test_sanitize_filename_mixed_separators():
"""Test _sanitize_filename with mixed path separators."""
assert BedrockAgent._sanitize_filename("../path\\to/file.txt") == "file.txt"
assert BedrockAgent._sanitize_filename("..\\path/to\\file.txt") == "file.txt"
def test_sanitize_filename_null_byte():
"""Test _sanitize_filename removes null bytes."""
assert BedrockAgent._sanitize_filename("file\x00.txt") == "file.txt"
assert BedrockAgent._sanitize_filename("file.txt\x00.exe") == "file.txt.exe"
def test_sanitize_filename_empty():
"""Test _sanitize_filename returns empty string for empty result."""
assert BedrockAgent._sanitize_filename("") == ""
assert BedrockAgent._sanitize_filename("../") == ""
assert BedrockAgent._sanitize_filename("..\\") == ""
def test_sanitize_filename_only_dots():
"""Test _sanitize_filename handles edge cases with dots."""
# Note: os.path.basename("..") returns ".." which is kept as-is
# Only "../" or "..\" patterns get stripped to empty string
assert BedrockAgent._sanitize_filename(".") == "."
def test_sanitize_filename_logs_warning(caplog):
"""Test _sanitize_filename logs warning when filename is sanitized."""
import logging
with caplog.at_level(logging.WARNING):
result = BedrockAgent._sanitize_filename("../malicious/file.txt")
assert result == "file.txt"
assert "potentially malicious path components" in caplog.text
assert "../malicious/file.txt" in caplog.text
assert "file.txt" in caplog.text
def test_sanitize_filename_no_warning_for_clean_filename(caplog):
"""Test _sanitize_filename does not log warning for clean filenames."""
import logging
with caplog.at_level(logging.WARNING):
result = BedrockAgent._sanitize_filename("clean_file.txt")
assert result == "clean_file.txt"
assert "potentially malicious" not in caplog.text
# endregion
@@ -0,0 +1,331 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterable
from unittest.mock import MagicMock, Mock, patch
import boto3
import pytest
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
from semantic_kernel.agents.channels.bedrock_agent_channel import BedrockAgentChannel
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
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
@pytest.fixture
@patch.object(boto3, "client", return_value=Mock())
def mock_channel(client):
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgentThread
BedrockAgentChannel.model_rebuild()
thread = BedrockAgentThread(client, session_id="test_session_id")
return BedrockAgentChannel(thread=thread)
class ConcreteAgent(Agent):
async def get_response(self, *args, **kwargs) -> ChatMessageContent:
raise NotImplementedError
def invoke(self, *args, **kwargs) -> AsyncIterable[ChatMessageContent]:
raise NotImplementedError
def invoke_stream(self, *args, **kwargs) -> AsyncIterable[StreamingChatMessageContent]:
raise NotImplementedError
@pytest.fixture
def chat_history() -> list[ChatMessageContent]:
return [
ChatMessageContent(role="user", content="Hello, Bedrock!"),
ChatMessageContent(role="assistant", content="Hello, User!"),
ChatMessageContent(role="user", content="How are you?"),
ChatMessageContent(role="assistant", content="I'm good, thank you!"),
]
@pytest.fixture
def chat_history_not_alternate_role() -> list[ChatMessageContent]:
return [
ChatMessageContent(role="user", content="Hello, Bedrock!"),
ChatMessageContent(role="user", content="Hello, User!"),
ChatMessageContent(role="assistant", content="How are you?"),
ChatMessageContent(role="assistant", content="I'm good, thank you!"),
]
@pytest.fixture
def mock_agent():
"""
Fixture that creates a mock BedrockAgent.
"""
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent
# Create mocks
mock_agent = MagicMock(spec=BedrockAgent)
# Set the name and agent_model properties
mock_agent.name = "MockBedrockAgent"
mock_agent.agent_model = MagicMock(spec=BedrockAgentModel)
mock_agent.agent_model.foundation_model = "mock-foundation-model"
return mock_agent
async def test_receive_message(mock_channel, chat_history):
# Test to verify the receive_message functionality
await mock_channel.receive(chat_history)
assert len(mock_channel) == len(chat_history)
async def test_channel_receive_message_with_no_message(mock_channel):
# Test to verify receive_message when no message is received
await mock_channel.receive([])
assert len(mock_channel) == 0
async def test_chat_history_alternation(mock_channel, chat_history_not_alternate_role):
# Test to verify chat history alternates between user and assistant messages
await mock_channel.receive(chat_history_not_alternate_role)
assert all(
mock_channel.messages[i].role != mock_channel.messages[i + 1].role
for i in range(len(chat_history_not_alternate_role) - 1)
)
assert mock_channel.messages[1].content == mock_channel.MESSAGE_PLACEHOLDER
assert mock_channel.messages[4].content == mock_channel.MESSAGE_PLACEHOLDER
async def test_channel_reset(mock_channel, chat_history):
# Test to verify the reset functionality
await mock_channel.receive(chat_history)
assert len(mock_channel) == len(chat_history)
assert len(mock_channel.messages) == len(chat_history)
await mock_channel.reset()
assert len(mock_channel) == 0
assert len(mock_channel.messages) == 0
async def test_receive_appends_history_correctly(mock_channel):
"""Test that the receive method appends messages while ensuring they alternate in author role."""
# Provide a list of messages with identical roles to see if placeholders are inserted
incoming_messages = [
ChatMessageContent(role=AuthorRole.USER, content="User message 1"),
ChatMessageContent(role=AuthorRole.USER, content="User message 2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 2"),
]
await mock_channel.receive(incoming_messages)
# The final channel.messages should be:
# user message 1, user placeholder, user message 2, assistant placeholder, assistant message 1,
# assistant placeholder, assistant message 2
expected_roles = [
AuthorRole.USER,
AuthorRole.ASSISTANT, # placeholder
AuthorRole.USER,
AuthorRole.ASSISTANT,
AuthorRole.USER, # placeholder
AuthorRole.ASSISTANT,
]
expected_contents = [
"User message 1",
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
"User message 2",
"Assistant message 1",
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
"Assistant message 2",
]
assert len(mock_channel.messages) == len(expected_roles)
for i, (msg, exp_role, exp_content) in enumerate(zip(mock_channel.messages, expected_roles, expected_contents)):
assert msg.role == exp_role, f"Role mismatch at index {i}"
assert msg.content == exp_content, f"Content mismatch at index {i}"
async def test_invoke_raises_exception_for_non_bedrock_agent(mock_channel):
"""Test invoke method raises AgentChatException if the agent provided is not a BedrockAgent."""
# Place a message in the channel so it's not empty
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="User message"))
# Create a dummy agent that is not BedrockAgent
non_bedrock_agent = ConcreteAgent()
with pytest.raises(AgentChatException) as exc_info:
_ = [msg async for msg in mock_channel.invoke(non_bedrock_agent)]
assert "Agent is not of the expected type" in str(exc_info.value)
async def test_invoke_raises_exception_if_no_history(mock_channel, mock_agent):
"""Test invoke method raises AgentChatException if no chat history is available."""
with pytest.raises(AgentChatException) as exc_info:
_ = [msg async for msg in mock_channel.invoke(mock_agent)]
assert "No chat history available" in str(exc_info.value)
async def test_invoke_inserts_placeholders_when_history_needs_to_alternate(mock_channel, mock_agent):
"""Test invoke ensures _ensure_history_alternates and _ensure_last_message_is_user are called."""
# Put messages in the channel such that the last message is an assistant's
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant 1"))
# Mock agent.invoke to return an async generator
async def mock_invoke(messages: str, thread: AgentThread, sessionState=None, **kwargs):
# We just yield one message as if the agent responded
yield AgentResponseItem(
message=ChatMessageContent(role=AuthorRole.ASSISTANT, content="Mock Agent Response"),
thread=mock_channel.thread,
)
mock_agent.invoke = mock_invoke
# Because the last message is from the assistant, we expect a placeholder user message to be appended
# also the history might need to alternate.
# But since there's only one message, there's nothing to fix except the last message is user.
# We will now add a user message so we do not get the "No chat history available" error
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="User 1"))
# Now we do invoke
outputs = [msg async for msg in mock_channel.invoke(mock_agent)]
# We'll check if the response is appended to channel.messages
assert len(outputs) == 1
assert outputs[0][0] is True, "Expected a user-facing response"
agent_response = outputs[0][1]
assert agent_response.content == "Mock Agent Response"
# The channel messages should now have 3 messages: the assistant, the user, and the new agent message
assert len(mock_channel.messages) == 3
assert mock_channel.messages[-1].role == AuthorRole.ASSISTANT
assert mock_channel.messages[-1].content == "Mock Agent Response"
async def test_invoke_stream_raises_error_for_non_bedrock_agent(mock_channel):
"""Test invoke_stream raises AgentChatException if the agent provided is not a BedrockAgent."""
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="User message"))
non_bedrock_agent = ConcreteAgent()
with pytest.raises(AgentChatException) as exc_info:
_ = [msg async for msg in mock_channel.invoke_stream(non_bedrock_agent, [])]
assert "Agent is not of the expected type" in str(exc_info.value)
async def test_invoke_stream_raises_no_chat_history(mock_channel, mock_agent):
"""Test invoke_stream raises AgentChatException if no messages in the channel."""
with pytest.raises(AgentChatException) as exc_info:
_ = [msg async for msg in mock_channel.invoke_stream(mock_agent, [])]
assert "No chat history available." in str(exc_info.value)
async def test_invoke_stream_appends_response_message(mock_channel, mock_agent):
"""Test invoke_stream properly yields streaming content and appends an aggregated message at the end."""
# Put a user message in the channel so it won't raise No chat history
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="Last user message"))
async def mock_invoke_stream(
messages: str, thread: AgentThread, sessionState=None, **kwargs
) -> AsyncIterable[StreamingChatMessageContent]:
yield AgentResponseItem(
message=StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
content="Hello",
),
thread=mock_channel.thread,
)
yield AgentResponseItem(
message=StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
content=" World",
),
thread=mock_channel.thread,
)
mock_agent.invoke_stream = mock_invoke_stream
# Check that we get the streamed messages and that the summarized message is appended afterward
messages_param = [ChatMessageContent(role=AuthorRole.USER, content="Last user message")] # just to pass the param
streamed_content = [msg async for msg in mock_channel.invoke_stream(mock_agent, messages_param)]
# We expect two streamed chunks: "Hello" and " World"
assert len(streamed_content) == 2
assert streamed_content[0].content == "Hello"
assert streamed_content[1].content == " World"
# Then we expect the channel to append an aggregated ChatMessageContent with "Hello World"
assert len(messages_param) == 2
appended = messages_param[1]
assert appended.role == AuthorRole.ASSISTANT
assert appended.content == "Hello World"
async def test_get_history(mock_channel, chat_history):
"""Test get_history yields messages in reverse order."""
mock_channel.messages = chat_history
reversed_history = [msg async for msg in mock_channel.get_history()]
# Should be reversed
assert reversed_history[0].content == "I'm good, thank you!"
assert reversed_history[1].content == "How are you?"
assert reversed_history[2].content == "Hello, User!"
assert reversed_history[3].content == "Hello, Bedrock!"
async def test_invoke_alternates_history_and_ensures_last_user_message(mock_channel, mock_agent):
"""Test invoke method ensures history alternates and last message is user."""
mock_channel.messages = [
ChatMessageContent(role=AuthorRole.USER, content="User1"),
ChatMessageContent(role=AuthorRole.USER, content="User2"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assist1"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assist2"),
ChatMessageContent(role=AuthorRole.USER, content="User3"),
ChatMessageContent(role=AuthorRole.USER, content="User4"),
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assist3"),
]
async for _, msg in mock_channel.invoke(mock_agent):
pass
# let's define expected roles from that final structure:
expected_roles = [
AuthorRole.USER,
AuthorRole.ASSISTANT, # placeholder
AuthorRole.USER,
AuthorRole.ASSISTANT,
AuthorRole.USER, # placeholder
AuthorRole.ASSISTANT,
AuthorRole.USER,
AuthorRole.ASSISTANT, # placeholder
AuthorRole.USER,
AuthorRole.ASSISTANT,
AuthorRole.USER, # placeholder
]
expected_contents = [
"User1",
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
"User2",
"Assist1",
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
"Assist2",
"User3",
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
"User4",
"Assist3",
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
]
assert len(mock_channel.messages) == len(expected_roles)
for i, (msg, exp_role, exp_content) in enumerate(zip(mock_channel.messages, expected_roles, expected_contents)):
assert msg.role == exp_role, f"Role mismatch at index {i}. Got {msg.role}, expected {exp_role}"
assert msg.content == exp_content, f"Content mismatch at index {i}. Got {msg.content}, expected {exp_content}"
@@ -0,0 +1,27 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.agents.bedrock.models.bedrock_agent_event_type import BedrockAgentEventType
def test_bedrock_agent_event_type_values():
"""Test case to verify the values of BedrockAgentEventType enum."""
assert BedrockAgentEventType.CHUNK.value == "chunk"
assert BedrockAgentEventType.TRACE.value == "trace"
assert BedrockAgentEventType.RETURN_CONTROL.value == "returnControl"
assert BedrockAgentEventType.FILES.value == "files"
def test_bedrock_agent_event_type_enum():
"""Test case to verify the type of BedrockAgentEventType enum members."""
assert isinstance(BedrockAgentEventType.CHUNK, BedrockAgentEventType)
assert isinstance(BedrockAgentEventType.TRACE, BedrockAgentEventType)
assert isinstance(BedrockAgentEventType.RETURN_CONTROL, BedrockAgentEventType)
assert isinstance(BedrockAgentEventType.FILES, BedrockAgentEventType)
def test_bedrock_agent_event_type_invalid():
"""Test case to verify error handling for invalid BedrockAgentEventType value."""
with pytest.raises(ValueError):
BedrockAgentEventType("invalid_value")
@@ -0,0 +1,67 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
def test_bedrock_agent_model_valid():
"""Test case to verify the BedrockAgentModel with valid data."""
model = BedrockAgentModel(
agentId="test_id",
agentName="test_name",
agentVersion="1.0",
foundationModel="test_model",
agentStatus="CREATING",
)
assert model.agent_id == "test_id"
assert model.agent_name == "test_name"
assert model.agent_version == "1.0"
assert model.foundation_model == "test_model"
assert model.agent_status == "CREATING"
def test_bedrock_agent_model_missing_agent_id():
"""Test case to verify the BedrockAgentModel with missing agentId."""
model = BedrockAgentModel(
agentName="test_name",
agentVersion="1.0",
foundationModel="test_model",
agentStatus="CREATING",
)
assert model.agent_id is None
assert model.agent_name == "test_name"
assert model.agent_version == "1.0"
assert model.foundation_model == "test_model"
assert model.agent_status == "CREATING"
def test_bedrock_agent_model_missing_agent_name():
"""Test case to verify the BedrockAgentModel with missing agentName."""
model = BedrockAgentModel(
agentId="test_id",
agentVersion="1.0",
foundationModel="test_model",
agentStatus="CREATING",
)
assert model.agent_id == "test_id"
assert model.agent_name is None
assert model.agent_version == "1.0"
assert model.foundation_model == "test_model"
assert model.agent_status == "CREATING"
def test_bedrock_agent_model_extra_field():
"""Test case to verify the BedrockAgentModel with an extra field."""
model = BedrockAgentModel(
agentId="test_id",
agentName="test_name",
agentVersion="1.0",
foundationModel="test_model",
agentStatus="CREATING",
extraField="extra_value",
)
assert model.agent_id == "test_id"
assert model.agent_name == "test_name"
assert model.agent_version == "1.0"
assert model.foundation_model == "test_model"
assert model.agent_status == "CREATING"
assert model.extraField == "extra_value"
@@ -0,0 +1,28 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from pydantic import ValidationError
from semantic_kernel.agents.bedrock.bedrock_agent_settings import BedrockAgentSettings
def test_bedrock_agent_settings_from_env_vars(bedrock_agent_unit_test_env):
"""Test loading BedrockAgentSettings from environment variables."""
settings = BedrockAgentSettings(env_file_path="fake_path")
assert settings.agent_resource_role_arn == bedrock_agent_unit_test_env["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"]
assert settings.foundation_model == bedrock_agent_unit_test_env["BEDROCK_AGENT_FOUNDATION_MODEL"]
@pytest.mark.parametrize(
"exclude_list",
[
["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
["BEDROCK_AGENT_FOUNDATION_MODEL"],
],
indirect=True,
)
def test_bedrock_agent_settings_from_env_vars_missing_required(bedrock_agent_unit_test_env):
"""Test loading BedrockAgentSettings from environment variables with missing required fields."""
with pytest.raises(ValidationError):
BedrockAgentSettings(env_file_path="fake_path")
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.agents.bedrock.models.bedrock_agent_status import BedrockAgentStatus
def test_bedrock_agent_status_values():
"""Test case to verify the values of BedrockAgentStatus enum."""
assert BedrockAgentStatus.CREATING == "CREATING"
assert BedrockAgentStatus.PREPARING == "PREPARING"
assert BedrockAgentStatus.PREPARED == "PREPARED"
assert BedrockAgentStatus.NOT_PREPARED == "NOT_PREPARED"
assert BedrockAgentStatus.DELETING == "DELETING"
assert BedrockAgentStatus.FAILED == "FAILED"
assert BedrockAgentStatus.VERSIONING == "VERSIONING"
assert BedrockAgentStatus.UPDATING == "UPDATING"
def test_bedrock_agent_status_invalid_value():
"""Test case to verify error handling for invalid BedrockAgentStatus value."""
with pytest.raises(ValueError):
BedrockAgentStatus("INVALID_STATUS")
@@ -0,0 +1,26 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, create_autospec
import pytest
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.kernel import Kernel
@pytest.fixture
def kernel_with_ai_service():
kernel = create_autospec(Kernel)
mock_ai_service_client = create_autospec(ChatCompletionClientBase)
mock_prompt_execution_settings = create_autospec(PromptExecutionSettings)
mock_prompt_execution_settings.function_choice_behavior = None
kernel.select_ai_service.return_value = (mock_ai_service_client, mock_prompt_execution_settings)
mock_ai_service_client.get_chat_message_contents = AsyncMock(
return_value=[ChatMessageContent(role=AuthorRole.SYSTEM, content="Processed Message")]
)
kernel.plugins = {} # Ensure plugins dict is initialized to avoid AttributeError during tests
return kernel, mock_ai_service_client
@@ -0,0 +1,471 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncGenerator, Callable
from types import MethodType
from unittest.mock import AsyncMock, create_autospec, patch
import pytest
from pydantic import ValidationError
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.agents.channels.chat_history_channel import ChatHistoryChannel
from semantic_kernel.agents.chat_completion.chat_completion_agent import ChatHistoryAgentThread
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
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.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.history_reducer.chat_history_truncation_reducer import ChatHistoryTruncationReducer
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions import KernelServiceNotFoundError
from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel
@pytest.fixture
def mock_streaming_chat_completion_response() -> Callable[..., AsyncGenerator[list[ChatMessageContent], None]]:
async def mock_response(
chat_history: ChatHistory,
settings: PromptExecutionSettings,
kernel: Kernel,
arguments: KernelArguments,
) -> AsyncGenerator[list[ChatMessageContent], None]:
content1 = ChatMessageContent(role=AuthorRole.SYSTEM, content="Processed Message 1")
content2 = ChatMessageContent(role=AuthorRole.TOOL, content="Processed Message 2")
chat_history.messages.append(content1)
chat_history.messages.append(content2)
yield [content1]
yield [content2]
return mock_response
async def test_initialization():
agent = ChatCompletionAgent(
name="TestAgent",
id="test_id",
description="Test Description",
instructions="Test Instructions",
)
assert agent.name == "TestAgent"
assert agent.id == "test_id"
assert agent.description == "Test Description"
assert agent.instructions == "Test Instructions"
async def test_initialization_invalid_name_throws():
with pytest.raises(ValidationError):
_ = ChatCompletionAgent(
name="Test Agent",
id="test_id",
description="Test Description",
instructions="Test Instructions",
)
def test_initialization_with_kernel(kernel: Kernel):
agent = ChatCompletionAgent(
kernel=kernel,
name="TestAgent",
id="test_id",
description="Test Description",
instructions="Test Instructions",
)
assert kernel == agent.kernel
assert agent.name == "TestAgent"
assert agent.id == "test_id"
assert agent.description == "Test Description"
assert agent.instructions == "Test Instructions"
def test_initialization_with_kernel_and_service(kernel: Kernel, azure_openai_unit_test_env, openai_unit_test_env):
kernel.add_service(AzureChatCompletion(service_id="test_azure"))
agent = ChatCompletionAgent(
service=OpenAIChatCompletion(),
kernel=kernel,
name="TestAgent",
id="test_id",
description="Test Description",
instructions="Test Instructions",
)
assert kernel == agent.kernel
assert len(kernel.services) == 2
assert agent.name == "TestAgent"
assert agent.id == "test_id"
assert agent.description == "Test Description"
assert agent.instructions == "Test Instructions"
def test_initialization_with_plugins_via_constructor(custom_plugin_class):
agent = ChatCompletionAgent(
name="TestAgent",
id="test_id",
description="Test Description",
instructions="Test Instructions",
plugins=[custom_plugin_class()],
)
assert agent.name == "TestAgent"
assert agent.id == "test_id"
assert agent.description == "Test Description"
assert agent.instructions == "Test Instructions"
assert agent.kernel.plugins is not None
assert len(agent.kernel.plugins) == 1
def test_initialization_with_service_via_constructor(openai_unit_test_env):
agent = ChatCompletionAgent(
name="TestAgent",
id="test_id",
description="Test Description",
instructions="Test Instructions",
service=OpenAIChatCompletion(),
)
assert agent.name == "TestAgent"
assert agent.id == "test_id"
assert agent.description == "Test Description"
assert agent.instructions == "Test Instructions"
assert agent.service is not None
assert agent.kernel.services["test_chat_model_id"] == agent.service
def test_initialize_chat_history_agent_thread_with_id():
thread = ChatHistoryAgentThread(thread_id="test_thread_id")
assert thread is not None
assert thread.id == "test_thread_id"
def test_initialize_with_base_chat_history():
base_history = ChatHistory()
thread = ChatHistoryAgentThread(chat_history=base_history, thread_id="base_test_thread")
assert thread is not None
assert thread.id == "base_test_thread"
assert isinstance(thread._chat_history, ChatHistory)
assert not isinstance(thread._chat_history, ChatHistoryTruncationReducer)
def test_initialize_with_reducer_chat_history():
reducer = ChatHistoryTruncationReducer(
service=AsyncMock(spec=ChatCompletionClientBase), target_count=10, threshold_count=2
)
thread = ChatHistoryAgentThread(chat_history=reducer, thread_id="reducer_test_thread")
assert thread is not None
assert thread.id == "reducer_test_thread"
assert isinstance(thread._chat_history, ChatHistoryTruncationReducer)
async def test_get_response(kernel_with_ai_service: tuple[Kernel, ChatCompletionClientBase]):
kernel, _ = kernel_with_ai_service
agent = ChatCompletionAgent(
kernel=kernel,
name="TestAgent",
instructions="Test Instructions",
)
thread = ChatHistoryAgentThread()
response = await agent.get_response(messages="test", thread=thread)
assert response.message.content == "Processed Message"
assert response.thread is not None
async def test_get_response_exception(kernel_with_ai_service: tuple[Kernel, ChatCompletionClientBase]):
kernel, mock_ai_service_client = kernel_with_ai_service
mock_ai_service_client.get_chat_message_contents = AsyncMock(return_value=[])
agent = ChatCompletionAgent(
kernel=kernel,
name="TestAgent",
instructions="Test Instructions",
)
thread = ChatHistoryAgentThread()
with pytest.raises(AgentInvokeException):
await agent.get_response(messages="test", thread=thread)
async def test_invoke(kernel_with_ai_service: tuple[Kernel, ChatCompletionClientBase]):
kernel, _ = kernel_with_ai_service
agent = ChatCompletionAgent(
kernel=kernel,
name="TestAgent",
instructions="Test Instructions",
)
thread = ChatHistoryAgentThread()
messages = [message async for message in agent.invoke(messages="test", thread=thread)]
assert len(messages) == 1
assert messages[0].message.content == "Processed Message"
async def test_invoke_emits_tool_call_then_result_then_text(kernel_with_ai_service):
kernel, chat_client = kernel_with_ai_service
agent = ChatCompletionAgent(kernel=kernel, name="TestAgent")
thread = ChatHistoryAgentThread()
call_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[FunctionCallContent(id="test-id", name="get_specials", arguments="{}")],
)
result_msg = ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="test-id", name="get_specials", result="Clam Chowder")],
)
final_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
content="Clam Chowder is today's soup.",
)
chat_client.get_chat_message_contents = AsyncMock(return_value=[final_msg])
async def fake_drain(self, *_args, **_kwargs):
if not fake_drain.called:
fake_drain.called = True
return [call_msg, result_msg]
return []
fake_drain.called = False
with patch.object(ChatCompletionAgent, "_drain_mutated_messages", new=AsyncMock(side_effect=fake_drain)):
cb_messages: list[ChatMessageContent] = []
async def on_msg(m: ChatMessageContent):
cb_messages.append(m)
messages = [
m
async for m in agent.invoke(
messages="What's the special soup?", thread=thread, on_intermediate_message=on_msg
)
]
assert [type(m.items[0]) for m in cb_messages] == [
FunctionCallContent,
FunctionResultContent,
]
assert len(messages) == 1
assert isinstance(messages[0].message, ChatMessageContent)
assert messages[0].message.content.startswith("Clam Chowder")
assert messages[0].message.name == agent.name
async def test_invoke_tool_call_not_added(kernel_with_ai_service: tuple[Kernel, ChatCompletionClientBase]):
kernel, mock_ai_service_client = kernel_with_ai_service
agent = ChatCompletionAgent(
kernel=kernel,
name="TestAgent",
)
thread = ChatHistoryAgentThread()
async def mock_get_chat_message_contents(
chat_history: ChatHistory,
settings: PromptExecutionSettings,
kernel: Kernel,
arguments: KernelArguments,
):
responses = [
ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(result="Tool Call Result")],
),
]
chat_history.messages.extend(responses)
return responses
mock_ai_service_client.get_chat_message_contents = AsyncMock(side_effect=mock_get_chat_message_contents)
messages = [message async for message in agent.invoke(messages="test", thread=thread)]
assert len(messages) == 1
assert messages[0].message.items[0].result == "Tool Call Result"
assert messages[0].message.role == AuthorRole.TOOL
assert messages[0].message.name == "TestAgent"
thread: ChatHistoryAgentThread = messages[-1].thread
thread_messages = [message async for message in thread.get_messages()]
assert len(thread_messages) == 2
assert thread_messages[0].content == "test"
assert thread_messages[1].items[0].result == "Tool Call Result"
assert thread_messages[1].name == "TestAgent"
assert thread_messages[1].role == AuthorRole.TOOL
async def test_invoke_no_service_throws(kernel: Kernel):
agent = ChatCompletionAgent(kernel=kernel, name="TestAgent")
with pytest.raises(KernelServiceNotFoundError):
async for _ in agent.invoke(messages="test", thread=None):
pass
async def test_invoke_stream(kernel_with_ai_service: tuple[Kernel, ChatCompletionClientBase]):
kernel, _ = kernel_with_ai_service
agent = ChatCompletionAgent(kernel=kernel, name="TestAgent")
thread = ChatHistoryAgentThread()
with patch(
"semantic_kernel.connectors.ai.chat_completion_client_base.ChatCompletionClientBase.get_streaming_chat_message_contents",
return_value=AsyncMock(),
) as mock:
mock.return_value.__aiter__.return_value = [
[ChatMessageContent(role=AuthorRole.USER, content="Initial Message")]
]
async for response in agent.invoke_stream(messages="Initial Message", thread=thread):
assert response.message.role == AuthorRole.USER
assert response.message.content == "Initial Message"
async def test_invoke_stream_emits_tool_call_then_result_then_text(kernel_with_ai_service):
kernel, chat_client = kernel_with_ai_service
agent = ChatCompletionAgent(kernel=kernel, name="TestAgent")
thread = ChatHistoryAgentThread()
call_msg = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[FunctionCallContent(id="test-id", name="get_specials", arguments="{}")],
)
result_msg = ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="test-id", name="get_specials", result="Clam Chowder")],
)
text_msg = StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
content="Clam Chowder is today's soup.",
items=[StreamingTextContent(text="Clam Chowder is today's soup.", choice_index=0)],
choice_index=0,
)
async def fake_stream(*_args, **_kwargs):
yield [StreamingChatMessageContent(role=AuthorRole.ASSISTANT, content="", items=[], choice_index=0)]
yield [text_msg]
chat_client.get_streaming_chat_message_contents = MethodType(fake_stream, chat_client)
async def fake_drain(self, *_args, **_kwargs):
if not fake_drain.called:
fake_drain.called = True
return [call_msg, result_msg]
return []
fake_drain.called = False
with patch.object(ChatCompletionAgent, "_drain_mutated_messages", new=AsyncMock(side_effect=fake_drain)):
cb_messages: list[ChatMessageContent] = []
async def on_msg(m: ChatMessageContent):
cb_messages.append(m)
yielded_text: list[StreamingChatMessageContent] = []
async for resp in agent.invoke_stream(
messages="What's the special soup?",
thread=thread,
on_intermediate_message=on_msg,
):
yielded_text.append(resp.message)
assert [type(m.items[0]) for m in cb_messages] == [
FunctionCallContent,
FunctionResultContent,
]
assert len(yielded_text) == 1
assert isinstance(yielded_text[0], StreamingChatMessageContent)
assert yielded_text[0].content.startswith("Clam Chowder")
assert yielded_text[0].name == agent.name
async def test_invoke_stream_tool_call_added(
kernel_with_ai_service: tuple[Kernel, ChatCompletionClientBase],
mock_streaming_chat_completion_response,
):
kernel, mock_ai_service_client = kernel_with_ai_service
agent = ChatCompletionAgent(kernel=kernel, name="TestAgent")
thread = ChatHistoryAgentThread()
mock_ai_service_client.get_streaming_chat_message_contents = mock_streaming_chat_completion_response
async for response in agent.invoke_stream(messages="Initial Message", thread=thread):
assert response.message.role in [AuthorRole.SYSTEM, AuthorRole.TOOL]
assert response.message.content in ["Processed Message 1", "Processed Message 2"]
async def test_invoke_stream_no_service_throws(kernel: Kernel):
agent = ChatCompletionAgent(kernel=kernel, name="TestAgent")
thread = ChatHistoryAgentThread()
with pytest.raises(KernelServiceNotFoundError):
async for _ in agent.invoke_stream(messages="test", thread=thread):
pass
def test_get_channel_keys():
agent = ChatCompletionAgent()
keys = agent.get_channel_keys()
for key in keys:
assert isinstance(key, str)
async def test_create_channel():
agent = ChatCompletionAgent()
channel = await agent.create_channel()
assert isinstance(channel, ChatHistoryChannel)
async def test_prepare_agent_chat_history_with_formatted_instructions():
agent = ChatCompletionAgent(
name="TestAgent", id="test_id", description="Test Description", instructions="Test Instructions"
)
with patch.object(
ChatCompletionAgent, "format_instructions", new=AsyncMock(return_value="Formatted instructions for testing")
) as mock_format_instructions:
dummy_kernel = create_autospec(Kernel)
dummy_args = KernelArguments(param="value")
user_message = ChatMessageContent(role=AuthorRole.USER, content="User message")
history = ChatHistory(messages=[user_message])
result_history = await agent._prepare_agent_chat_history(history, dummy_kernel, dummy_args)
mock_format_instructions.assert_awaited_once_with(dummy_kernel, dummy_args)
assert len(result_history.messages) == 2
system_message = result_history.messages[0]
assert system_message.role == AuthorRole.SYSTEM
assert system_message.content == "Formatted instructions for testing"
assert system_message.name == agent.name
assert result_history.messages[1] == user_message
async def test_prepare_agent_chat_history_without_formatted_instructions():
agent = ChatCompletionAgent(
name="TestAgent", id="test_id", description="Test Description", instructions="Test Instructions"
)
with patch.object(
ChatCompletionAgent, "format_instructions", new=AsyncMock(return_value=None)
) as mock_format_instructions:
dummy_kernel = create_autospec(Kernel)
dummy_args = KernelArguments(param="value")
user_message = ChatMessageContent(role=AuthorRole.USER, content="User message")
history = ChatHistory(messages=[user_message])
result_history = await agent._prepare_agent_chat_history(history, dummy_kernel, dummy_args)
mock_format_instructions.assert_awaited_once_with(dummy_kernel, dummy_args)
assert len(result_history.messages) == 1
assert result_history.messages[0] == user_message

Some files were not shown because too many files have changed in this diff Show More