From 6533b5f1bb00930016a9427e5daf433f10a72a1d Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 5 May 2026 10:04:33 -0500 Subject: [PATCH 1/2] fix: reference correct PyPI package names in provider load error messages Co-Authored-By: Claude Sonnet 4.6 --- .../src/ldai/providers/runner_factory.py | 14 ++-- .../server-ai/tests/test_runner_factory.py | 84 +++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 packages/sdk/server-ai/tests/test_runner_factory.py diff --git a/packages/sdk/server-ai/src/ldai/providers/runner_factory.py b/packages/sdk/server-ai/src/ldai/providers/runner_factory.py index b7548791..fec02bda 100644 --- a/packages/sdk/server-ai/src/ldai/providers/runner_factory.py +++ b/packages/sdk/server-ai/src/ldai/providers/runner_factory.py @@ -13,6 +13,12 @@ # Multi-provider packages should be last in the list. SUPPORTED_AI_PROVIDERS = ('openai', 'langchain') +# Mapping from internal Python module name to the pip-installable PyPI package name. +_PYPI_PACKAGE_NAMES = { + 'ldai_openai': 'launchdarkly-server-sdk-ai-openai', + 'ldai_langchain': 'launchdarkly-server-sdk-ai-langchain', +} + class RunnerFactory: """ @@ -51,10 +57,7 @@ def _get_provider_factory(provider_type: str) -> Optional[AIProvider]: ) return None except ImportError as error: - log.warning( - f"Could not load provider '{provider_type}': {error}. " - f"Make sure the corresponding package is installed." - ) + log.warning(f"Could not load provider '{provider_type}': {error}") return None @staticmethod @@ -188,4 +191,5 @@ def _pkg_exists(package_name: str) -> None: :param package_name: Name of the package to check """ if util.find_spec(package_name) is None: - raise ImportError(f"Package '{package_name}' not found") + pypi_name = _PYPI_PACKAGE_NAMES.get(package_name, package_name) + raise ImportError(f"Package '{pypi_name}' not found. Run: pip install {pypi_name}") diff --git a/packages/sdk/server-ai/tests/test_runner_factory.py b/packages/sdk/server-ai/tests/test_runner_factory.py new file mode 100644 index 00000000..397970fd --- /dev/null +++ b/packages/sdk/server-ai/tests/test_runner_factory.py @@ -0,0 +1,84 @@ +"""Tests for RunnerFactory provider loading and error messages.""" + +from unittest.mock import patch + +import pytest + +from ldai.providers.runner_factory import RunnerFactory, _PYPI_PACKAGE_NAMES + + +class TestPkgExists: + """_pkg_exists raises ImportError with the PyPI package name when a module is missing.""" + + def test_raises_import_error_with_pypi_name_for_openai(self): + with patch('ldai.providers.runner_factory.util') as mock_util: + mock_util.find_spec.return_value = None + with pytest.raises(ImportError) as exc_info: + RunnerFactory._pkg_exists('ldai_openai') + assert 'launchdarkly-server-sdk-ai-openai' in str(exc_info.value) + assert 'pip install launchdarkly-server-sdk-ai-openai' in str(exc_info.value) + + def test_raises_import_error_with_pypi_name_for_langchain(self): + with patch('ldai.providers.runner_factory.util') as mock_util: + mock_util.find_spec.return_value = None + with pytest.raises(ImportError) as exc_info: + RunnerFactory._pkg_exists('ldai_langchain') + assert 'launchdarkly-server-sdk-ai-langchain' in str(exc_info.value) + assert 'pip install launchdarkly-server-sdk-ai-langchain' in str(exc_info.value) + + def test_raises_import_error_with_module_name_when_no_mapping(self): + """Unknown module names fall back to the module name itself.""" + with patch('ldai.providers.runner_factory.util') as mock_util: + mock_util.find_spec.return_value = None + with pytest.raises(ImportError) as exc_info: + RunnerFactory._pkg_exists('some_unknown_module') + assert 'some_unknown_module' in str(exc_info.value) + + def test_does_not_raise_when_package_is_found(self): + with patch('ldai.providers.runner_factory.util') as mock_util: + mock_util.find_spec.return_value = object() # non-None means found + # Should not raise + RunnerFactory._pkg_exists('ldai_openai') + + +class TestPypiPackageNameMapping: + """The _PYPI_PACKAGE_NAMES mapping covers all supported providers.""" + + def test_openai_module_maps_to_pypi_name(self): + assert _PYPI_PACKAGE_NAMES['ldai_openai'] == 'launchdarkly-server-sdk-ai-openai' + + def test_langchain_module_maps_to_pypi_name(self): + assert _PYPI_PACKAGE_NAMES['ldai_langchain'] == 'launchdarkly-server-sdk-ai-langchain' + + +class TestGetProviderFactory: + """_get_provider_factory logs the PyPI package name in its warning when a package is missing.""" + + def test_warning_includes_pypi_name_for_openai(self): + with patch('ldai.providers.runner_factory.util') as mock_util, \ + patch('ldai.providers.runner_factory.log') as mock_log: + mock_util.find_spec.return_value = None + result = RunnerFactory._get_provider_factory('openai') + assert result is None + warning_text = mock_log.warning.call_args[0][0] + assert 'launchdarkly-server-sdk-ai-openai' in warning_text + assert 'ldai_openai' not in warning_text + + def test_warning_includes_pypi_name_for_langchain(self): + with patch('ldai.providers.runner_factory.util') as mock_util, \ + patch('ldai.providers.runner_factory.log') as mock_log: + mock_util.find_spec.return_value = None + result = RunnerFactory._get_provider_factory('langchain') + assert result is None + warning_text = mock_log.warning.call_args[0][0] + assert 'launchdarkly-server-sdk-ai-langchain' in warning_text + assert 'ldai_langchain' not in warning_text + + def test_warning_does_not_contain_make_sure_text(self): + """The old boilerplate text should be replaced by actionable pip install instructions.""" + with patch('ldai.providers.runner_factory.util') as mock_util, \ + patch('ldai.providers.runner_factory.log') as mock_log: + mock_util.find_spec.return_value = None + RunnerFactory._get_provider_factory('openai') + warning_text = mock_log.warning.call_args[0][0] + assert 'Make sure the corresponding package is installed' not in warning_text From 1fb4635ffa1d82bad4963f5b29d2575c77de1292 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 5 May 2026 10:25:30 -0500 Subject: [PATCH 2/2] fix: use package-manager-agnostic message when provider package is missing Co-Authored-By: Claude Sonnet 4.6 --- .../sdk/server-ai/src/ldai/providers/runner_factory.py | 2 +- packages/sdk/server-ai/tests/test_runner_factory.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/sdk/server-ai/src/ldai/providers/runner_factory.py b/packages/sdk/server-ai/src/ldai/providers/runner_factory.py index fec02bda..c1262bfe 100644 --- a/packages/sdk/server-ai/src/ldai/providers/runner_factory.py +++ b/packages/sdk/server-ai/src/ldai/providers/runner_factory.py @@ -192,4 +192,4 @@ def _pkg_exists(package_name: str) -> None: """ if util.find_spec(package_name) is None: pypi_name = _PYPI_PACKAGE_NAMES.get(package_name, package_name) - raise ImportError(f"Package '{pypi_name}' not found. Run: pip install {pypi_name}") + raise ImportError(f"Package '{pypi_name}' not found. Make sure it is installed.") diff --git a/packages/sdk/server-ai/tests/test_runner_factory.py b/packages/sdk/server-ai/tests/test_runner_factory.py index 397970fd..2d972ea8 100644 --- a/packages/sdk/server-ai/tests/test_runner_factory.py +++ b/packages/sdk/server-ai/tests/test_runner_factory.py @@ -16,7 +16,7 @@ def test_raises_import_error_with_pypi_name_for_openai(self): with pytest.raises(ImportError) as exc_info: RunnerFactory._pkg_exists('ldai_openai') assert 'launchdarkly-server-sdk-ai-openai' in str(exc_info.value) - assert 'pip install launchdarkly-server-sdk-ai-openai' in str(exc_info.value) + assert 'pip install' not in str(exc_info.value) def test_raises_import_error_with_pypi_name_for_langchain(self): with patch('ldai.providers.runner_factory.util') as mock_util: @@ -24,7 +24,7 @@ def test_raises_import_error_with_pypi_name_for_langchain(self): with pytest.raises(ImportError) as exc_info: RunnerFactory._pkg_exists('ldai_langchain') assert 'launchdarkly-server-sdk-ai-langchain' in str(exc_info.value) - assert 'pip install launchdarkly-server-sdk-ai-langchain' in str(exc_info.value) + assert 'pip install' not in str(exc_info.value) def test_raises_import_error_with_module_name_when_no_mapping(self): """Unknown module names fall back to the module name itself.""" @@ -74,11 +74,11 @@ def test_warning_includes_pypi_name_for_langchain(self): assert 'launchdarkly-server-sdk-ai-langchain' in warning_text assert 'ldai_langchain' not in warning_text - def test_warning_does_not_contain_make_sure_text(self): - """The old boilerplate text should be replaced by actionable pip install instructions.""" + def test_warning_does_not_reference_pip(self): + """Warning should be package-manager agnostic — no pip install command.""" with patch('ldai.providers.runner_factory.util') as mock_util, \ patch('ldai.providers.runner_factory.log') as mock_log: mock_util.find_spec.return_value = None RunnerFactory._get_provider_factory('openai') warning_text = mock_log.warning.call_args[0][0] - assert 'Make sure the corresponding package is installed' not in warning_text + assert 'pip install' not in warning_text