From 14dfa7f268b923b75a26549e456e239671304418 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 11 Jun 2026 09:21:55 -0700 Subject: [PATCH] fix(tests): repoint lfx tests off moved providers; complete provider fallback map CI fail-fast had been masking these: the LFX test job runs in an engine-only env where openai/anthropic/chroma are now bundle-package shims, so every test that used them as the example category failed on all Python versions (3.14 just reported first), and the backend Group 2 leg failed collecting test_lfx_bundles_extras.py on Python 3.10. - flow_requirements: complete _PROVIDER_PACKAGE_FALLBACKS for the nine moved model providers (OpenAI, Anthropic, Amazon Bedrock, Groq, Google Generative AI, SambaNova, IBM watsonx.ai, Ollama + existing Azure OpenAI). In engine-only installs MODEL_PROVIDERS_DICT registers only in-tree providers, so the dynamic source-inspection path cannot resolve moved providers; the static fallbacks keep lfx run/serve requirements inference working. A dict hit still wins. - test_dynamic_imports / test_import_utils: use composio (still-in-tree lazy category with its SDK absent in the bare env) as the example category instead of openai/anthropic/chroma; patch import_module at its canonical home lfx.utils.lazy_import. - flow-builder tests (build_flow_from_spec, flow_builder_tools, propose_field_edit): specs use LanguageModelComponent (in-tree) instead of OpenAIModel. - test_lfx_bundles_extras: tomli fallback for Python 3.10 (tomllib is stdlib 3.11+; pytest guarantees tomli on <3.11). Full lfx unit suite: 4550 passed. Backend extras+pin tests: 24 passed. --- .../tests/unit/test_lfx_bundles_extras.py | 5 +- src/lfx/src/lfx/utils/flow_requirements.py | 16 +++- .../custom/component/test_dynamic_imports.py | 83 ++++++++++--------- .../tests/unit/test_build_flow_from_spec.py | 8 +- src/lfx/tests/unit/test_flow_builder_tools.py | 28 ++++--- src/lfx/tests/unit/test_import_utils.py | 23 +++-- src/lfx/tests/unit/test_propose_field_edit.py | 6 +- 7 files changed, 102 insertions(+), 67 deletions(-) diff --git a/src/backend/tests/unit/test_lfx_bundles_extras.py b/src/backend/tests/unit/test_lfx_bundles_extras.py index 683cb2afb7..597f773f9b 100644 --- a/src/backend/tests/unit/test_lfx_bundles_extras.py +++ b/src/backend/tests/unit/test_lfx_bundles_extras.py @@ -19,7 +19,10 @@ from __future__ import annotations import re from pathlib import Path -import tomllib +try: + import tomllib +except ModuleNotFoundError: # Python 3.10: tomllib is stdlib only on 3.11+ + import tomli as tomllib # pytest guarantees tomli on <3.11 REPO_ROOT = Path(__file__).resolve().parents[4] METAPACKAGE_DIR = REPO_ROOT / "src" / "bundles" / "lfx-bundles" diff --git a/src/lfx/src/lfx/utils/flow_requirements.py b/src/lfx/src/lfx/utils/flow_requirements.py index 23ddd4289b..cfb18f405a 100644 --- a/src/lfx/src/lfx/utils/flow_requirements.py +++ b/src/lfx/src/lfx/utils/flow_requirements.py @@ -86,10 +86,22 @@ _INTERNAL_IMPORT_NAMES: frozenset[str] = frozenset({"lfx", "langflow", "langflow _MODEL_FIELDS = {"model", "agent_llm", "embeddings_model", "embedding_model"} # Fallback provider → package mapping for providers whose component class may -# not be importable in every environment (e.g. Azure OpenAI shares -# langchain-openai with the regular OpenAI provider). +# not be importable in every environment. Since the bundle split, provider +# model components live in bundle distributions (lfx-openai, lfx-bundles, ...) +# that an engine-only install does not carry, so MODEL_PROVIDERS_DICT registers +# only in-tree providers there and the dynamic source-inspection path cannot +# run. These static entries keep requirements inference working for flows +# configured with those providers; a MODEL_PROVIDERS_DICT hit still wins. _PROVIDER_PACKAGE_FALLBACKS: dict[str, set[str]] = { + "Amazon Bedrock": {"langchain-aws"}, + "Anthropic": {"langchain-anthropic"}, "Azure OpenAI": {"langchain-openai"}, + "Google Generative AI": {"langchain-google-genai"}, + "Groq": {"langchain-groq"}, + "IBM watsonx.ai": {"langchain-ibm"}, + "Ollama": {"langchain-ollama"}, + "OpenAI": {"langchain-openai"}, + "SambaNova": {"langchain-sambanova"}, } diff --git a/src/lfx/tests/unit/custom/component/test_dynamic_imports.py b/src/lfx/tests/unit/custom/component/test_dynamic_imports.py index 64699e171b..21c1276cb9 100644 --- a/src/lfx/tests/unit/custom/component/test_dynamic_imports.py +++ b/src/lfx/tests/unit/custom/component/test_dynamic_imports.py @@ -20,9 +20,11 @@ class TestImportUtils: def test_import_mod_with_module_name(self): """Test importing specific attribute from a module with missing dependencies.""" - # Test importing a class that has missing dependencies - should raise ModuleNotFoundError + # composio: a still-in-tree category whose SDK is absent in the bare + # lfx test env (openai/anthropic/chroma graduated to bundle packages, + # so their lfx.components paths are shims now, not lazy categories). with pytest.raises(ModuleNotFoundError, match="No module named"): - import_mod("OpenAIModelComponent", "openai_chat_model", "lfx.components.openai") + import_mod("ComposioAPIComponent", "composio_api", "lfx.components.composio") def test_import_mod_without_module_name(self): """Test importing entire module when module_name is None.""" @@ -34,13 +36,13 @@ class TestImportUtils: def test_import_mod_module_not_found(self): """Test error handling when module doesn't exist.""" with pytest.raises(ImportError, match="not found"): - import_mod("NonExistentComponent", "nonexistent_module", "lfx.components.openai") + import_mod("NonExistentComponent", "nonexistent_module", "lfx.components.composio") def test_import_mod_attribute_not_found(self): """Test error handling when module has missing dependencies.""" - # The openai_chat_model module can't be imported due to missing dependencies + # The composio_api module can't be imported due to missing dependencies with pytest.raises(ModuleNotFoundError, match="No module named"): - import_mod("NonExistentComponent", "openai_chat_model", "lfx.components.openai") + import_mod("NonExistentComponent", "composio_api", "lfx.components.composio") class TestComponentDynamicImports: @@ -87,48 +89,51 @@ class TestComponentDynamicImports: _ = components.nonexistent_category def test_category_module_dynamic_import(self): - """Test dynamic import behavior in category modules like openai.""" - import lfx.components.openai as openai_components + """Test dynamic import behavior in category modules like composio.""" + # composio: still-in-tree lazy category whose SDK is absent in the + # bare lfx test env (openai graduated to the lfx-openai package, so + # lfx.components.openai is a shim now, not a lazy category). + import lfx.components.composio as composio_components # Test that components are in __all__ - assert "OpenAIModelComponent" in openai_components.__all__ - assert "OpenAIEmbeddingsComponent" in openai_components.__all__ + assert "ComposioAPIComponent" in composio_components.__all__ + assert "ComposioGmailAPIComponent" in composio_components.__all__ - # Access component - this should raise AttributeError due to missing langchain-openai - with pytest.raises(AttributeError, match="Could not import 'OpenAIModelComponent'"): - _ = openai_components.OpenAIModelComponent + # Access component - this should raise AttributeError due to missing composio SDK + with pytest.raises(AttributeError, match="Could not import 'ComposioAPIComponent'"): + _ = composio_components.ComposioAPIComponent # Test that the error is properly cached - second access should also fail - with pytest.raises(AttributeError, match="Could not import 'OpenAIModelComponent'"): - _ = openai_components.OpenAIModelComponent + with pytest.raises(AttributeError, match="Could not import 'ComposioAPIComponent'"): + _ = composio_components.ComposioAPIComponent def test_category_module_dir(self): """Test __dir__ functionality for category modules.""" - import lfx.components.openai as openai_components + import lfx.components.composio as composio_components - dir_result = dir(openai_components) - assert "OpenAIModelComponent" in dir_result - assert "OpenAIEmbeddingsComponent" in dir_result + dir_result = dir(composio_components) + assert "ComposioAPIComponent" in dir_result + assert "ComposioGmailAPIComponent" in dir_result def test_category_module_missing_component(self): """Test error handling for non-existent component in category.""" - import lfx.components.openai as openai_components + import lfx.components.composio as composio_components with pytest.raises(AttributeError, match="has no attribute 'NonExistentComponent'"): - _ = openai_components.NonExistentComponent + _ = composio_components.NonExistentComponent def test_multiple_category_modules(self): """Test dynamic imports work across multiple category modules.""" - import lfx.components.anthropic as anthropic_components import lfx.components.data as data_components + import lfx.components.huggingface as huggingface_components # Test different categories work independently - # AnthropicModelComponent should work if anthropic library is available + # HuggingFaceEndpointsComponent should work if its deps are available try: - anthropic_component = anthropic_components.AnthropicModelComponent + hf_component = huggingface_components.HuggingFaceEndpointsComponent # If it succeeds, just check it's a valid component - assert anthropic_component is not None - assert hasattr(anthropic_component, "__name__") + assert hf_component is not None + assert hasattr(hf_component, "__name__") except AttributeError: # If it fails due to missing dependencies, that's also expected pass @@ -139,7 +144,7 @@ class TestComponentDynamicImports: assert hasattr(api_component, "__name__") # Test that __all__ still works correctly despite import failures - assert "AnthropicModelComponent" in anthropic_components.__all__ + assert "HuggingFaceEndpointsComponent" in huggingface_components.__all__ assert "APIRequestComponent" in data_components.__all__ def test_backward_compatibility(self): @@ -195,15 +200,15 @@ class TestComponentDynamicImports: def test_consistency_check(self): """Test that __all__ and _dynamic_imports are consistent.""" - import lfx.components.openai as openai_components + import lfx.components.composio as composio_components # All items in __all__ should have corresponding entries in _dynamic_imports - for component_name in openai_components.__all__: - assert component_name in openai_components._dynamic_imports + for component_name in composio_components.__all__: + assert component_name in composio_components._dynamic_imports # All keys in _dynamic_imports should be in __all__ - for component_name in openai_components._dynamic_imports: - assert component_name in openai_components.__all__ + for component_name in composio_components._dynamic_imports: + assert component_name in composio_components.__all__ def test_type_checking_imports(self): """Test that TYPE_CHECKING imports work correctly with dynamic loading.""" @@ -235,11 +240,13 @@ class TestPerformanceCharacteristics: def test_lazy_loading_performance(self): """Test that components can be accessed and cached properly.""" - from lfx.components import chroma as chromamodules + # composio: still-in-tree lazy category with its SDK absent in the + # bare lfx test env (chroma graduated to the lfx-bundles metapackage). + from lfx.components import composio as composio_modules # Test that we can access a component - with pytest.raises(AttributeError, match=r"Could not import.*ChromaVectorStoreComponent"): - chromamodules.ChromaVectorStoreComponent # noqa: B018 + with pytest.raises(AttributeError, match=r"Could not import.*ComposioAPIComponent"): + composio_modules.ComposioAPIComponent # noqa: B018 def test_memory_usage_multiple_accesses(self): """Test memory behavior with multiple component accesses.""" @@ -293,16 +300,16 @@ class TestSpecialCases: from lfx import components # Test that we can access nested components through the hierarchy - # OpenAI component requires langchain_openai which isn't installed - with pytest.raises(AttributeError, match=r"Could not import.*OpenAIModelComponent"): - _ = components.openai.OpenAIModelComponent + # Composio components require the composio SDK which isn't installed + with pytest.raises(AttributeError, match=r"Could not import.*ComposioAPIComponent"): + _ = components.composio.ComposioAPIComponent # APIRequestComponent should work now that validators is installed api_component = components.data.APIRequestComponent assert api_component is not None # Test that both main module and submodules are properly cached - assert "openai" in components.__dict__ + assert "composio" in components.__dict__ assert "data" in components.__dict__ diff --git a/src/lfx/tests/unit/test_build_flow_from_spec.py b/src/lfx/tests/unit/test_build_flow_from_spec.py index e4dbbaec07..1e45585d5e 100644 --- a/src/lfx/tests/unit/test_build_flow_from_spec.py +++ b/src/lfx/tests/unit/test_build_flow_from_spec.py @@ -29,12 +29,14 @@ edges: assert "B" in result["node_id_map"] def test_three_node_flow(self): + # LanguageModelComponent: a model type that stays in-tree (provider + # model components live in bundle packages not installed in this env). spec = """\ name: Chat with Model nodes: A: ChatInput - B: OpenAIModel + B: LanguageModelComponent C: ChatOutput edges: @@ -262,7 +264,7 @@ name: Chat Flow nodes: A: ChatInput - B: OpenAIModel + B: LanguageModelComponent C: ChatOutput edges: @@ -273,7 +275,7 @@ edges: summary = flow_to_spec_summary(result["flow"]) assert "Chat Flow" in summary assert "ChatInput" in summary - assert "OpenAIModel" in summary + assert "LanguageModelComponent" in summary assert "ChatOutput" in summary assert "connections:" in summary diff --git a/src/lfx/tests/unit/test_flow_builder_tools.py b/src/lfx/tests/unit/test_flow_builder_tools.py index 507de984f0..1fa29f108f 100644 --- a/src/lfx/tests/unit/test_flow_builder_tools.py +++ b/src/lfx/tests/unit/test_flow_builder_tools.py @@ -299,20 +299,22 @@ class TestBuildFlowFromSpec: assert len(events) == 0 # Regression: the agent has been observed to build flows with components - # that have no edges (e.g. an OpenAIModel sitting next to an Agent with + # that have no edges (e.g. a model component sitting next to an Agent with # nothing wired to it). Reject those so the LLM auto-corrects via retry. def test_build_should_reject_orphan_components(self): reset_working_flow() comp = BuildFlowFromSpec() - # ChatInput connects to ChatOutput, but OpenAIModel is added without - # any edge. This must fail validation, not silently produce a flow. + # ChatInput connects to ChatOutput, but LanguageModelComponent is added + # without any edge. This must fail validation, not silently produce a + # flow. (In-tree model type; provider models live in bundle packages + # not installed in this env.) comp.set( spec=( "name: Has Orphan\n" "nodes:\n" " A: ChatInput\n" " B: ChatOutput\n" - " X: OpenAIModel\n" + " X: LanguageModelComponent\n" "edges:\n" " A.message -> B.input_value\n" ), @@ -837,9 +839,11 @@ class TestConnectComponents: reset_working_flow() - add_openai = AddComponent() - add_openai.set(component_type="OpenAIModel") - openai_id = add_openai.add_component().data["id"] + # In-tree model type; provider models live in bundle packages not + # installed in this env. + add_model = AddComponent() + add_model.set(component_type="LanguageModelComponent") + model_id = add_model.add_component().data["id"] add_agent = AddComponent() add_agent.set(component_type="Agent") @@ -849,7 +853,7 @@ class TestConnectComponents: conn = ConnectComponents() conn.set( - source_id=openai_id, + source_id=model_id, source_output="model_output", target_id=agent_id, target_input="model", @@ -874,9 +878,9 @@ class TestConnectComponents: """ # noqa: D205 reset_working_flow() - add_openai = AddComponent() - add_openai.set(component_type="OpenAIModel") - openai_id = add_openai.add_component().data["id"] + add_model = AddComponent() + add_model.set(component_type="LanguageModelComponent") + model_id = add_model.add_component().data["id"] add_agent = AddComponent() add_agent.set(component_type="Agent") @@ -886,7 +890,7 @@ class TestConnectComponents: conn = ConnectComponents() conn.set( - source_id=openai_id, + source_id=model_id, source_output="model_output", target_id=agent_id, target_input="model", diff --git a/src/lfx/tests/unit/test_import_utils.py b/src/lfx/tests/unit/test_import_utils.py index fdbd94050b..84be73db3e 100644 --- a/src/lfx/tests/unit/test_import_utils.py +++ b/src/lfx/tests/unit/test_import_utils.py @@ -32,8 +32,9 @@ class TestImportAttr: def test_import_modibute_from_module(self): """Test importing a specific attribute from a module.""" - # Test importing a class from a specific module - result = import_mod("AnthropicModelComponent", "anthropic", "lfx.components.anthropic") + # Test importing a class from a specific module (in-tree category; + # provider categories like anthropic are bundle-package shims now). + result = import_mod("LanguageModelComponent", "language_model", "lfx.components.models_and_agents") assert result is not None assert hasattr(result, "__name__") @@ -42,7 +43,7 @@ class TestImportAttr: def test_import_nonexistent_module(self): """Test error handling when module doesn't exist.""" with pytest.raises(ImportError, match="not found"): - import_mod("SomeComponent", "nonexistent_module", "lfx.components.openai") + import_mod("SomeComponent", "nonexistent_module", "lfx.components.composio") def test_module_not_found_with_none_module_name(self): """Test ModuleNotFoundError handling when module_name is None.""" @@ -57,7 +58,7 @@ class TestImportAttr: def test_import_nonexistent_attribute(self): """Test error handling when attribute doesn't exist in module.""" with pytest.raises(AttributeError): - import_mod("NonExistentComponent", "anthropic", "lfx.components.anthropic") + import_mod("NonExistentComponent", "language_model", "lfx.components.models_and_agents") def test_import_with_none_package(self): """Test behavior when package is None.""" @@ -67,7 +68,10 @@ class TestImportAttr: def test_module_not_found_error_handling(self): """Test specific ModuleNotFoundError handling.""" - with patch("lfx.components._importing.import_module") as mock_import_module: + # import_mod's canonical home is lfx.utils.lazy_import; patch the + # import_module it actually calls (lfx.components._importing is a + # re-export and no longer binds import_module). + with patch("lfx.utils.lazy_import.import_module") as mock_import_module: mock_import_module.side_effect = ModuleNotFoundError("No module named 'test.package.test_module'") with pytest.raises(ImportError, match=r"module .* not found"): @@ -115,13 +119,14 @@ class TestImportAttr: def test_return_value_types(self): """Test that import_mod returns appropriate types.""" - # Test module import - module_result = import_mod("openai", "__module__", "lfx.components") + # Test module import (composio: in-tree lazy category whose SDK is + # absent in the bare lfx env; openai is a bundle-package shim now) + module_result = import_mod("composio", "__module__", "lfx.components") assert hasattr(module_result, "__name__") - # Test class import - this should fail due to missing langchain-openai dependency + # Test class import - this should fail due to the missing composio SDK with pytest.raises((ImportError, ModuleNotFoundError)): - import_mod("OpenAIModelComponent", "openai_chat_model", "lfx.components.openai") + import_mod("ComposioAPIComponent", "composio_api", "lfx.components.composio") def test_caching_independence(self): """Test that import_mod doesn't interfere with Python's module caching.""" diff --git a/src/lfx/tests/unit/test_propose_field_edit.py b/src/lfx/tests/unit/test_propose_field_edit.py index b3cb87eb18..9762f651ad 100644 --- a/src/lfx/tests/unit/test_propose_field_edit.py +++ b/src/lfx/tests/unit/test_propose_field_edit.py @@ -14,11 +14,13 @@ from lfx.mcp.flow_builder_tools import ( reset_working_flow, ) +# LanguageModelComponent: in-tree model type (provider model components live +# in bundle packages not installed in this env). SIMPLE_FLOW_SPEC = """\ name: Test Flow nodes: A: ChatInput - B: OpenAIModel + B: LanguageModelComponent C: ChatOutput edges: A.message -> B.input_value @@ -193,7 +195,7 @@ class TestProposeFieldEditMultiple: flow = _build_test_flow() init_working_flow(flow, "test-flow-id") input_id = _get_component_id(flow, "ChatInput") - model_id = _get_component_id(flow, "OpenAIModel") + model_id = _get_component_id(flow, "LanguageModelComponent") tool1 = ProposeFieldEdit() tool1.set(component_id=input_id, field_name="input_value", new_value="hello")