# 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