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