diff --git a/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py b/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py index ada6bcd112ee..2bd4a2d1e5f9 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/hf_prompt_execution_settings.py @@ -8,9 +8,11 @@ if TYPE_CHECKING: from transformers import GenerationConfig - -imported = importlib.import_module("transformers") -ready = imported is not None and hasattr(imported, "GenerationConfig") +try: + imported = importlib.import_module("transformers") + ready = imported is not None and hasattr(imported, "GenerationConfig") +except (ImportError, ModuleNotFoundError): + ready = False class HuggingFacePromptExecutionSettings(PromptExecutionSettings): @@ -27,10 +29,12 @@ class HuggingFacePromptExecutionSettings(PromptExecutionSettings): def get_generation_config(self) -> "GenerationConfig": """Get the generation config.""" - from transformers import GenerationConfig - if not ready: - raise ImportError("transformers is not installed.") + raise ImportError( + "transformers is not installed. Please install it with `pip install semantic-kernel[hugging_face]`." + ) + + from transformers import GenerationConfig return GenerationConfig( **self.model_dump( diff --git a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py index 87b53e316d38..88b83f33232d 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_completion.py @@ -11,8 +11,13 @@ else: from typing_extensions import override # pragma: no cover -import torch -from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, Pipeline, TextIteratorStreamer, pipeline +try: + import torch + from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, Pipeline, TextIteratorStreamer, pipeline + + ready = True +except (ImportError, ModuleNotFoundError): + ready = False from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import HuggingFacePromptExecutionSettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings @@ -35,7 +40,7 @@ def __init__( self, model: Any, tokenizer: Any, - device: torch.device, + device: Any, output_key: str, prefix: str, default_call_kwargs: dict[str, Any], @@ -180,6 +185,12 @@ def __init__( Note that this model will be downloaded from the Hugging Face model hub. """ + if not ready: + raise ImportError( + "torch and transformers are not installed. " + "Please install them with `pip install semantic-kernel[hugging_face]`." + ) + generator: _Seq2SeqGenerator | Pipeline if task in {"summarization", "text2text-generation"}: generator = _create_seq2seq_generator(ai_model_id, task, device, model_kwargs, pipeline_kwargs) diff --git a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_embedding.py b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_embedding.py index e41dcf8f557a..ba43cf42dffd 100644 --- a/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_embedding.py +++ b/python/semantic_kernel/connectors/ai/hugging_face/services/hf_text_embedding.py @@ -9,7 +9,12 @@ else: from typing_extensions import override # pragma: no cover -import torch +try: + import torch + + torch_installed = True +except (ImportError, ModuleNotFoundError): + torch_installed = False from numpy import ndarray from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase @@ -48,6 +53,11 @@ def __init__( Note that this model will be downloaded from the Hugging Face model hub. """ + if not torch_installed: + raise ImportError( + "torch is not installed. Please install it with `pip install semantic-kernel[hugging_face]`." + ) + from sentence_transformers import SentenceTransformer resolved_device = f"cuda:{device}" if device >= 0 and torch.cuda.is_available() else "cpu" diff --git a/python/semantic_kernel/connectors/ai/onnx/services/onnx_gen_ai_completion_base.py b/python/semantic_kernel/connectors/ai/onnx/services/onnx_gen_ai_completion_base.py index 9bafaa38b31f..33d15ed16c9d 100644 --- a/python/semantic_kernel/connectors/ai/onnx/services/onnx_gen_ai_completion_base.py +++ b/python/semantic_kernel/connectors/ai/onnx/services/onnx_gen_ai_completion_base.py @@ -37,7 +37,9 @@ def __init__(self, ai_model_path: str, **kwargs) -> None: ServiceInitializationError: When model cannot be loaded """ if not ready: - raise ImportError("onnxruntime-genai is not installed.") + raise ImportError( + "onnxruntime-genai is not installed. Please install it with `pip install semantic-kernel[onnx]`." + ) try: json_gen_ai_config = os.path.join(ai_model_path + "/genai_config.json") with open(json_gen_ai_config) as file: @@ -86,8 +88,7 @@ async def _generate_next_token_async( while not generator.is_done(): generator.generate_next_token() - new_token_choices = [self.tokenizer_stream.decode(token) for token in generator.get_next_tokens()] - yield new_token_choices + yield [self.tokenizer_stream.decode(token) for token in generator.get_next_tokens()] del generator except Exception as ex: raise ServiceInvalidResponseError("Failed Inference with ONNX", ex) from ex diff --git a/python/semantic_kernel/connectors/memory_stores/chroma/chroma_memory_store.py b/python/semantic_kernel/connectors/memory_stores/chroma/chroma_memory_store.py index 7d2cffe1b870..ede9c5d744eb 100644 --- a/python/semantic_kernel/connectors/memory_stores/chroma/chroma_memory_store.py +++ b/python/semantic_kernel/connectors/memory_stores/chroma/chroma_memory_store.py @@ -76,7 +76,10 @@ def __init__( except ImportError as exc: raise ServiceInitializationError( - "Could not import chromadb python package. Please install it with `pip install chromadb`." + ( + "Could not import chromadb python package. " + "Please install it with `pip install semantic-kernel[chroma]`." + ) ) from exc if client_settings: diff --git a/python/tests/unit/connectors/test_optional_dependencies.py b/python/tests/unit/connectors/test_optional_dependencies.py new file mode 100644 index 000000000000..1428efc663a3 --- /dev/null +++ b/python/tests/unit/connectors/test_optional_dependencies.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft. All rights reserved. + +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from semantic_kernel.exceptions import ServiceInitializationError + + +def test_hugging_face_settings_instantiation_without_transformers(): + """HuggingFacePromptExecutionSettings can be imported and instantiated without transformers installed.""" + from semantic_kernel.connectors.ai.hugging_face import HuggingFacePromptExecutionSettings + + settings = HuggingFacePromptExecutionSettings( + max_new_tokens=100, + temperature=0.7, + ) + assert settings.max_new_tokens == 100 + assert settings.temperature == 0.7 + + +def test_hugging_face_settings_get_generation_config_missing_dependency(): + """Calling get_generation_config without transformers produces an actionable error naming the install extra.""" + from semantic_kernel.connectors.ai.hugging_face import ( + HuggingFacePromptExecutionSettings, + hf_prompt_execution_settings, + ) + + settings = HuggingFacePromptExecutionSettings() + + with patch.object(hf_prompt_execution_settings, "ready", False): + with pytest.raises(ImportError) as exc_info: + settings.get_generation_config() + + err_msg = str(exc_info.value) + assert "transformers is not installed" in err_msg + assert "semantic-kernel[hugging_face]" in err_msg + + +def test_hugging_face_settings_get_generation_config_available(): + """When transformers is available, get_generation_config constructs the GenerationConfig.""" + from semantic_kernel.connectors.ai.hugging_face import ( + HuggingFacePromptExecutionSettings, + hf_prompt_execution_settings, + ) + + mock_gen_config_cls = MagicMock() + mock_transformers = MagicMock() + mock_transformers.GenerationConfig = mock_gen_config_cls + + settings = HuggingFacePromptExecutionSettings(temperature=0.8, top_p=0.9) + + with ( + patch.object(hf_prompt_execution_settings, "ready", True), + patch.dict(sys.modules, {"transformers": mock_transformers}), + ): + config = settings.get_generation_config() + assert config == mock_gen_config_cls.return_value + mock_gen_config_cls.assert_called_once() + + +def test_hugging_face_text_completion_missing_dependency(): + """HuggingFaceTextCompletion raises an actionable ImportError naming semantic-kernel[hugging_face].""" + from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletion + from semantic_kernel.connectors.ai.hugging_face.services import hf_text_completion + + with patch.object(hf_text_completion, "ready", False): + with pytest.raises(ImportError) as exc_info: + HuggingFaceTextCompletion(ai_model_id="test_model") + + err_msg = str(exc_info.value) + assert "torch and transformers are not installed" in err_msg + assert "semantic-kernel[hugging_face]" in err_msg + + +def test_hugging_face_text_embedding_missing_dependency(): + """HuggingFaceTextEmbedding raises an actionable ImportError naming semantic-kernel[hugging_face].""" + from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextEmbedding + from semantic_kernel.connectors.ai.hugging_face.services import hf_text_embedding + + with patch.object(hf_text_embedding, "torch_installed", False): + with pytest.raises(ImportError) as exc_info: + HuggingFaceTextEmbedding(ai_model_id="test_model") + + err_msg = str(exc_info.value) + assert "torch is not installed" in err_msg + assert "semantic-kernel[hugging_face]" in err_msg + + +def test_onnx_gen_ai_missing_dependency(): + """OnnxGenAICompletionBase raises an actionable ImportError naming semantic-kernel[onnx].""" + from semantic_kernel.connectors.ai.onnx.services import onnx_gen_ai_completion_base + from semantic_kernel.connectors.ai.onnx.services.onnx_gen_ai_completion_base import ( + OnnxGenAICompletionBase, + ) + + with patch.object(onnx_gen_ai_completion_base, "ready", False): + with pytest.raises(ImportError) as exc_info: + OnnxGenAICompletionBase(ai_model_path="dummy_path") + + err_msg = str(exc_info.value) + assert "onnxruntime-genai is not installed" in err_msg + assert "semantic-kernel[onnx]" in err_msg + + +def test_chroma_memory_store_missing_dependency(): + """ChromaMemoryStore raises ServiceInitializationError naming semantic-kernel[chroma].""" + from semantic_kernel.connectors.memory_stores.chroma.chroma_memory_store import ( + ChromaMemoryStore, + ) + + with patch.dict(sys.modules, {"chromadb": None, "chromadb.config": None}): + with pytest.raises(ServiceInitializationError) as exc_info: + ChromaMemoryStore() + + err_msg = str(exc_info.value) + assert "chromadb" in err_msg + assert "semantic-kernel[chroma]" in err_msg