Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
from typing import TYPE_CHECKING, Any

from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import ServiceInitializationError

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:
ready = False


class HuggingFacePromptExecutionSettings(PromptExecutionSettings):
Expand All @@ -27,10 +31,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 ServiceInitializationError(
"transformers is not installed. Please install it with `pip install semantic-kernel[hugging_face]`."
)

from transformers import GenerationConfig

return GenerationConfig(
**self.model_dump(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ServiceInitializationError(
"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:
Expand Down Expand Up @@ -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
Expand Down
38 changes: 33 additions & 5 deletions python/semantic_kernel/connectors/memory.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.

import importlib
from typing import Any

_IMPORTS = {
_IMPORTS: dict[str, str] = {
"AzureAISearchCollection": ".azure_ai_search",
"AzureAISearchSettings": ".azure_ai_search",
"AzureAISearchStore": ".azure_ai_search",
Expand All @@ -25,6 +26,9 @@
"MongoDBAtlasCollection": ".mongodb",
"MongoDBAtlasSettings": ".mongodb",
"MongoDBAtlasStore": ".mongodb",
"OracleCollection": ".oracle",
"OracleSettings": ".oracle",
"OracleStore": ".oracle",
"RedisStore": ".redis",
"RedisSettings": ".redis",
"RedisCollectionTypes": ".redis",
Expand All @@ -44,14 +48,38 @@
"SqlSettings": ".sql_server",
}

_EXTRA_MAP: dict[str, str] = {
".azure_ai_search": "azure",
".azure_cosmos_db": "azure",
".chroma": "chroma",
".faiss": "faiss",
".mongodb": "mongo",
".oracle": "oracledb",
".pinecone": "pinecone",
".postgres": "postgres",
".qdrant": "qdrant",
".redis": "redis",
".sql_server": "sql",
".weaviate": "weaviate",
}


def __getattr__(name: str):
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
submod_name = _IMPORTS[name]
module = importlib.import_module(submod_name, package=__name__)
return getattr(module, name)
try:
module = importlib.import_module(submod_name, package=__package__)
return getattr(module, name)
except (ModuleNotFoundError, ImportError) as ex:
extra = _EXTRA_MAP.get(submod_name)
if extra:
raise ModuleNotFoundError(
f"Could not import {name} from {submod_name}. "
f"Please install the optional dependency with `pip install semantic-kernel[{extra}]`."
) from ex
raise
Comment on lines +73 to +80
raise AttributeError(f"module {__name__} has no attribute {name}")


def __dir__():
def __dir__() -> list[str]:
return list(_IMPORTS.keys())
4 changes: 4 additions & 0 deletions python/semantic_kernel/connectors/memory.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ from .chroma import ChromaCollection, ChromaStore
from .faiss import FaissCollection, FaissStore
from .in_memory import InMemoryCollection, InMemoryStore
from .mongodb import MongoDBAtlasCollection, MongoDBAtlasSettings, MongoDBAtlasStore
from .oracle import OracleCollection, OracleSettings, OracleStore
from .pinecone import PineconeCollection, PineconeSettings, PineconeStore
from .postgres import PostgresCollection, PostgresSettings, PostgresStore
from .qdrant import QdrantCollection, QdrantSettings, QdrantStore
Expand Down Expand Up @@ -41,6 +42,9 @@ __all__ = [
"MongoDBAtlasCollection",
"MongoDBAtlasSettings",
"MongoDBAtlasStore",
"OracleCollection",
"OracleSettings",
"OracleStore",
"PineconeCollection",
"PineconeSettings",
"PineconeStore",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import chromadb.config
from chromadb.api.models.Collection import Collection

if sys.version_info >= (3, 12):
if sys.version_info >= (3, 13):
from warnings import deprecated
else:
from typing_extensions import deprecated
Expand Down Expand Up @@ -76,7 +76,8 @@ 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:
Expand Down
12 changes: 6 additions & 6 deletions python/semantic_kernel/connectors/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import importlib

_IMPORTS = {
"GoogleSearch": ".google",
"GoogleSearchSettings": ".google",
"GoogleSearchResult": ".google",
"GoogleSearchResponse": ".google",
"GoogleSearchInformation": ".google",
"GoogleSearch": ".google_search",
"GoogleSearchSettings": ".google_search",
"GoogleSearchResult": ".google_search",
"GoogleSearchResponse": ".google_search",
"GoogleSearchInformation": ".google_search",
"BraveSearch": ".brave",
"BraveSettings": ".brave",
"BraveWebPages": ".brave",
Expand All @@ -19,7 +19,7 @@
def __getattr__(name: str):
if name in _IMPORTS:
submod_name = _IMPORTS[name]
module = importlib.import_module(submod_name, package=__name__)
module = importlib.import_module(submod_name, package=__package__)
return getattr(module, name)
raise AttributeError(f"module {__name__} has no attribute {name}")

Expand Down
163 changes: 163 additions & 0 deletions python/tests/unit/connectors/test_optional_connector_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Copyright (c) Microsoft. All rights reserved.

import importlib
import sys
from unittest.mock import patch

import pytest

import semantic_kernel.connectors.memory as memory_module
import semantic_kernel.connectors.search as search_module
from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import (
HuggingFacePromptExecutionSettings,
)
from semantic_kernel.connectors.ai.onnx.services.onnx_gen_ai_completion_base import (
OnnxGenAICompletionBase,
)
from semantic_kernel.connectors.memory_stores.chroma.chroma_memory_store import (
ChromaMemoryStore,
)
from semantic_kernel.exceptions import ServiceInitializationError


@pytest.mark.parametrize(
"symbol_name,expected_extra",
[
("AzureAISearchCollection", "azure"),
("AzureAISearchSettings", "azure"),
("AzureAISearchStore", "azure"),
("CosmosNoSqlCollection", "azure"),
("CosmosNoSqlCompositeKey", "azure"),
("CosmosNoSqlSettings", "azure"),
("CosmosNoSqlStore", "azure"),
("CosmosMongoCollection", "azure"),
("CosmosMongoSettings", "azure"),
("CosmosMongoStore", "azure"),
("ChromaCollection", "chroma"),
("ChromaStore", "chroma"),
("PostgresCollection", "postgres"),
("PostgresSettings", "postgres"),
("PostgresStore", "postgres"),
("FaissCollection", "faiss"),
("FaissStore", "faiss"),
("MongoDBAtlasCollection", "mongo"),
("MongoDBAtlasSettings", "mongo"),
("MongoDBAtlasStore", "mongo"),
("OracleCollection", "oracledb"),
("OracleSettings", "oracledb"),
("OracleStore", "oracledb"),
("RedisStore", "redis"),
("RedisSettings", "redis"),
("RedisCollectionTypes", "redis"),
("RedisHashsetCollection", "redis"),
("RedisJsonCollection", "redis"),
("QdrantCollection", "qdrant"),
("QdrantSettings", "qdrant"),
("QdrantStore", "qdrant"),
("WeaviateCollection", "weaviate"),
("WeaviateSettings", "weaviate"),
("WeaviateStore", "weaviate"),
("PineconeCollection", "pinecone"),
("PineconeSettings", "pinecone"),
("PineconeStore", "pinecone"),
("SqlServerCollection", "sql"),
("SqlServerStore", "sql"),
("SqlSettings", "sql"),
],
)
def test_memory_lazy_import_missing_dependency(symbol_name: str, expected_extra: str):
"""Test that importing a connector raises ModuleNotFoundError with the exact install extra."""
submod_name = memory_module._IMPORTS[symbol_name]

def mock_import_module(name: str, package: str | None = None):
if name == submod_name:
raise ModuleNotFoundError(f"No module named 'fake_{expected_extra}'", name=f"fake_{expected_extra}")
return importlib.__import__(name)

with (
patch("importlib.import_module", side_effect=mock_import_module),
pytest.raises(ModuleNotFoundError) as exc_info,
):
Comment on lines +72 to +80
getattr(memory_module, symbol_name)

assert f"pip install semantic-kernel[{expected_extra}]" in str(exc_info.value)
assert symbol_name in str(exc_info.value)


def test_memory_in_memory_import():
"""Test that built-in InMemory store and collection import successfully without extra dependencies."""
in_memory_col = getattr(memory_module, "InMemoryCollection")
assert in_memory_col is not None

in_memory_store = getattr(memory_module, "InMemoryStore")
assert in_memory_store is not None


def test_memory_unknown_attribute():
"""Test that accessing an unknown attribute in memory module raises AttributeError."""
with pytest.raises(
AttributeError,
match="module semantic_kernel.connectors.memory has no attribute NonExistentStore",
):
getattr(memory_module, "NonExistentStore")


def test_memory_dir():
"""Test that __dir__ lists all available memory symbols."""
dir_symbols = dir(memory_module)
for symbol in [
"ChromaCollection",
"QdrantCollection",
"WeaviateCollection",
"PineconeCollection",
"PostgresCollection",
"RedisStore",
"MongoDBAtlasCollection",
"FaissCollection",
"OracleCollection",
"SqlServerCollection",
"AzureAISearchCollection",
"CosmosNoSqlCollection",
"InMemoryCollection",
]:
assert symbol in dir_symbols


def test_search_imports():
"""Test that search connectors can be imported and non-existent attribute raises AttributeError."""
google_search = getattr(search_module, "GoogleSearch")
assert google_search is not None

brave_search = getattr(search_module, "BraveSearch")
assert brave_search is not None

with pytest.raises(AttributeError, match="has no attribute NonExistentSearch"):
getattr(search_module, "NonExistentSearch")


def test_chroma_memory_store_missing_dependency():
"""Test that ChromaMemoryStore raises ServiceInitializationError when chromadb is missing."""
with (
patch.dict(sys.modules, {"chromadb": None, "chromadb.config": None}),
pytest.raises(ServiceInitializationError, match=r"pip install semantic-kernel\[chroma\]"),
):
ChromaMemoryStore()


def test_onnx_gen_ai_completion_missing_dependency():
"""Test that OnnxGenAICompletionBase raises ServiceInitializationError when onnxruntime-genai is missing."""
with (
patch("semantic_kernel.connectors.ai.onnx.services.onnx_gen_ai_completion_base.ready", False),
pytest.raises(ServiceInitializationError, match=r"pip install semantic-kernel\[onnx\]"),
):
OnnxGenAICompletionBase(ai_model_path="fake_path")


def test_hugging_face_settings_missing_dependency():
"""Test that HuggingFacePromptExecutionSettings raises ServiceInitializationError when transformers is missing."""
with (
patch("semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings.ready", False),
pytest.raises(ServiceInitializationError, match=r"pip install semantic-kernel\[hugging_face\]"),
):
settings = HuggingFacePromptExecutionSettings()
settings.get_generation_config()
Loading