346 lines
14 KiB
Python
346 lines
14 KiB
Python
# 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
|