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