Skip to content
Closed
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 @@ -9,8 +9,11 @@
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 = hasattr(imported, "GenerationConfig")
except ModuleNotFoundError:
ready = False


class HuggingFacePromptExecutionSettings(PromptExecutionSettings):
Expand All @@ -27,10 +30,13 @@ 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. Install the 'hugging_face' extra 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 @@ -11,8 +11,15 @@
else:
from typing_extensions import override # pragma: no cover

import torch
from transformers import AutoTokenizer, TextIteratorStreamer, pipeline
try:
import torch
from transformers import AutoTokenizer, TextIteratorStreamer, pipeline
except ModuleNotFoundError as exc:
missing_package = exc.name or "Hugging Face dependencies"
Comment thread
Copilot marked this conversation as resolved.
raise ImportError(
f"{missing_package} is not installed. Install the 'hugging_face' extra with "
"`pip install semantic-kernel[hugging_face]`."
) from exc

from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import HuggingFacePromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
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 ImportError(
"onnxruntime-genai is not installed. Install the 'onnx' extra 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
87 changes: 87 additions & 0 deletions python/tests/unit/connectors/test_optional_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft. All rights reserved.

import subprocess
import sys

import pytest


@pytest.mark.parametrize(
("module_name", "class_name", "package_name", "extra_name", "method_name", "load_directly"),
[
(
"semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings",
"HuggingFacePromptExecutionSettings",
"transformers",
"hugging_face",
"get_generation_config",
True,
),
(
"semantic_kernel.connectors.ai.onnx.services.onnx_gen_ai_completion_base",
"OnnxGenAICompletionBase",
"onnxruntime_genai",
"onnx",
"",
False,
),
],
)
def test_missing_optional_dependency_names_install_extra(
module_name: str,
class_name: str,
package_name: str,
extra_name: str,
method_name: str,
load_directly: bool,
) -> None:
script = """
import importlib
import importlib.abc
import importlib.util
from pathlib import Path
import sys

class MissingDependencyFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
if fullname == sys.argv[3] or fullname.startswith(f"{sys.argv[3]}."):
raise ModuleNotFoundError(f"No module named '{sys.argv[3]}'", name=sys.argv[3])
return None

sys.meta_path.insert(0, MissingDependencyFinder())
try:
if sys.argv[6] == "True":
package_spec = importlib.util.find_spec("semantic_kernel")
module_path = Path(package_spec.origin).parent.joinpath(*sys.argv[1].split(".")[1:]).with_suffix(".py")
module_spec = importlib.util.spec_from_file_location("optional_dependency_module", module_path)
module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(module)
else:
module = importlib.import_module(sys.argv[1])
connector = getattr(module, sys.argv[2])() if sys.argv[5] else getattr(module, sys.argv[2])("unused")
if sys.argv[5]:
getattr(connector, sys.argv[5])()
except ImportError as exc:
print(exc)
else:
raise AssertionError("Expected an ImportError for the blocked optional dependency")
"""
result = subprocess.run(
[
sys.executable,
"-c",
script,
module_name,
class_name,
package_name,
extra_name,
method_name,
str(load_directly),
],
capture_output=True,
check=True,
text=True,
)

assert package_name.replace("_", "-") in result.stdout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the Hugging Face parametrization this assertion is environment-dependent and does not cover the path it names. Importing ...hugging_face.hf_prompt_execution_settings first executes the package __init__.py, which imports hf_text_completion, whose top-level import torch runs before from transformers import .... The meta-path finder blocks only transformers (argv[3]), so when torch is not installed the raised message is "torch is not installed..." and this assertion ("transformers" in stdout) fails — reproduced as 1 failed, 1 passed in an environment without the hugging_face extra. It passes in CI only because unit-all-except-dapr runs uv sync --all-extras, so torch is present and transformers surfaces first. A side effect is that the ImportError is raised at import time from hf_text_completion, so get_generation_config() (the method_name this row targets) is never invoked and its new message goes untested. Make the case hermetic: block both torch and transformers in the finder, or assert only on the extra token (f"semantic-kernel[{extra_name}]", already checked on the next line) rather than a specific package name that is not guaranteed to surface first.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 984c0d3. The Hugging Face case now loads hf_prompt_execution_settings.py directly, bypassing the package init imports, blocks transformers, and invokes get_generation_config(). I reproduced the old environment-dependent behavior by forcing torch to be missing (the old assertion failed with the torch message), then verified the updated focused test passes for both connectors.

assert f"semantic-kernel[{extra_name}]" in result.stdout
Loading