From 4fabb025a954d87cd7514205032c0467d0163f89 Mon Sep 17 00:00:00 2001 From: Dex Hunter Date: Sun, 12 Jul 2026 22:14:15 +0000 Subject: [PATCH 1/2] perf(rag): defer provider config imports Importing crewai eagerly loads the RAG provider config chain even when RAG and knowledge storage are unused. Defer those imports to their call sites while preserving package assignment and factory patching contracts. On current main with all extras installed, two paired 11-process runs pinned to the same CPU reduced median cold import time from 2671.659 ms to 2024.536 ms (-24.22%, 1.32x). Refs #5510. Co-Authored-By: Aiden --- .../knowledge/storage/knowledge_storage.py | 39 +++++++-- lib/crewai/src/crewai/rag/__init__.py | 16 +++- lib/crewai/tests/test_imports.py | 81 +++++++++++++++++++ 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py b/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py index 3c9615946f..337a7c9b1d 100644 --- a/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py +++ b/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py @@ -1,24 +1,47 @@ import logging import traceback -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import warnings from pydantic import Field, PrivateAttr, model_validator from typing_extensions import Self from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage -from crewai.rag.chromadb.config import ChromaDBConfig -from crewai.rag.chromadb.types import ChromaEmbeddingFunctionWrapper -from crewai.rag.config.utils import get_rag_client from crewai.rag.core.base_client import BaseClient from crewai.rag.core.base_embeddings_provider import BaseEmbeddingsProvider from crewai.rag.embeddings.factory import build_embedder from crewai.rag.embeddings.types import ProviderSpec -from crewai.rag.factory import create_client from crewai.rag.types import BaseRecord, SearchResult from crewai.utilities.logger import Logger +if TYPE_CHECKING: + from crewai.rag.chromadb.types import ChromaEmbeddingFunctionWrapper + + +def create_client(config: Any) -> BaseClient: + """Create a RAG client for the given config. + + Thin wrapper around crewai.rag.factory.create_client, imported lazily so + that `import crewai` does not pull in the chromadb/qdrant provider chain. + """ + from crewai.rag.factory import create_client as _create_client + + return _create_client(config) + + +def get_rag_client() -> BaseClient: + """Get the global RAG client. + + Thin wrapper around crewai.rag.config.utils.get_rag_client, imported + lazily so that `import crewai` does not pull in the chromadb/qdrant + provider chain. + """ + from crewai.rag.config.utils import get_rag_client as _get_rag_client + + return _get_rag_client() + + class KnowledgeStorage(BaseKnowledgeStorage): """ Extends Storage to handle embeddings for memory entries, improving @@ -43,10 +66,14 @@ def _init_client(self) -> Self: ) if self.embedder: + # Imported lazily so that `import crewai` does not pull in the + # chromadb provider chain at import time. + from crewai.rag.chromadb.config import ChromaDBConfig + embedding_function = build_embedder(self.embedder) # type: ignore[arg-type] config = ChromaDBConfig( embedding_function=cast( - ChromaEmbeddingFunctionWrapper, embedding_function + "ChromaEmbeddingFunctionWrapper", embedding_function ) ) self._client = create_client(config) diff --git a/lib/crewai/src/crewai/rag/__init__.py b/lib/crewai/src/crewai/rag/__init__.py index 865910b427..644228a2a3 100644 --- a/lib/crewai/src/crewai/rag/__init__.py +++ b/lib/crewai/src/crewai/rag/__init__.py @@ -5,9 +5,6 @@ from types import ModuleType from typing import Any -from crewai.rag.config.types import RagConfigType -from crewai.rag.config.utils import set_rag_config - _module_path = __path__ _module_file = __file__ @@ -27,7 +24,7 @@ def __init__(self, module_name: str): """ super().__init__(module_name) - def __setattr__(self, name: str, value: RagConfigType) -> None: + def __setattr__(self, name: str, value: Any) -> None: """Set module attributes. Args: @@ -35,6 +32,17 @@ def __setattr__(self, name: str, value: RagConfigType) -> None: value: Attribute value. """ if name == "config": + if isinstance(value, ModuleType) and value is sys.modules.get( + f"{self.__name__}.config" + ): + # importlib registers the crewai.rag.config submodule on the + # package when it is first imported; store it as a plain + # module attribute instead of treating it as a RAG config. + return super().__setattr__(name, value) + # Imported lazily so that `import crewai.rag` does not pull in the + # provider config chain (chromadb, qdrant) at import time. + from crewai.rag.config.utils import set_rag_config + return set_rag_config(value) raise AttributeError(f"Setting attribute '{name}' is not allowed.") diff --git a/lib/crewai/tests/test_imports.py b/lib/crewai/tests/test_imports.py index bbbc3bbdd6..bf80d13bc8 100644 --- a/lib/crewai/tests/test_imports.py +++ b/lib/crewai/tests/test_imports.py @@ -13,3 +13,84 @@ def test_crew_output_import(): from crewai import CrewOutput assert CrewOutput is not None + + +def test_import_crewai_does_not_load_qdrant(): + """Importing crewai must not eagerly load the qdrant_client dependency chain. + + Regression test for lazy RAG config imports: the provider config union + (crewai.rag.config.types -> qdrant_client/chromadb) must only load when + the RAG config surface is actually used, not at `import crewai` time. + """ + import subprocess + import sys + + code = ( + "import sys; import crewai; " + "sys.exit(1 if 'qdrant_client' in sys.modules else 0)" + ) + result = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True + ) + assert result.returncode == 0, ( + "qdrant_client was imported eagerly by `import crewai`" + f"\nstderr: {result.stderr}" + ) + + +def test_rag_module_setattr_contract(): + """crewai.rag still routes `config` assignment and rejects other attributes.""" + from types import ModuleType + + import crewai.rag as rag_mod + import crewai.rag.config.utils as rag_config_utils + + # Non-config attributes are rejected. + try: + rag_mod.some_random_attribute = 1 + except AttributeError: + pass + else: + raise AssertionError("crewai.rag accepted an arbitrary attribute") + + # `config` assignment routes to set_rag_config. + recorded = [] + original = rag_config_utils.set_rag_config + rag_config_utils.set_rag_config = recorded.append + try: + sentinel = object() + rag_mod.config = sentinel + unrelated_module = ModuleType("unrelated") + rag_mod.config = unrelated_module + assert recorded == [sentinel, unrelated_module] + finally: + rag_config_utils.set_rag_config = original + + +def test_lazy_import_annotations_resolve(): + """Lazy imports must not leave unresolved runtime annotations.""" + import typing + + import crewai.rag as rag_mod + from crewai.knowledge.storage import knowledge_storage + + assert typing.get_type_hints(type(rag_mod).__setattr__)["value"] is typing.Any + assert typing.get_type_hints(knowledge_storage.create_client)["config"] is typing.Any + + +def test_rag_config_types_still_resolve(): + """The provider config union still exposes the real config classes.""" + import typing + + from crewai.rag.config.types import SupportedProviderConfig + + names = sorted(m.__name__ for m in typing.get_args(SupportedProviderConfig)) + assert names == ["ChromaDBConfig", "QdrantConfig"] + + +def test_knowledge_storage_instantiates(): + """KnowledgeStorage still builds its pydantic model with lazy rag imports.""" + from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage + + storage = KnowledgeStorage(collection_name="import-regression") + assert storage.collection_name == "import-regression" From d05229c1f04234b64255460a015ec36ca2246809 Mon Sep 17 00:00:00 2001 From: Dex Hunter Date: Sun, 12 Jul 2026 22:14:15 +0000 Subject: [PATCH 2/2] test(rag): bound lazy import subprocess Keep the lazy-import regression isolated in a fresh interpreter with a finite timeout. Refs #5510. Co-Authored-By: Aiden --- lib/crewai/tests/test_imports.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/crewai/tests/test_imports.py b/lib/crewai/tests/test_imports.py index bf80d13bc8..f9acbe9ef5 100644 --- a/lib/crewai/tests/test_imports.py +++ b/lib/crewai/tests/test_imports.py @@ -30,7 +30,7 @@ def test_import_crewai_does_not_load_qdrant(): "sys.exit(1 if 'qdrant_client' in sys.modules else 0)" ) result = subprocess.run( - [sys.executable, "-c", code], capture_output=True, text=True + [sys.executable, "-c", code], capture_output=True, text=True, timeout=30 ) assert result.returncode == 0, ( "qdrant_client was imported eagerly by `import crewai`" @@ -78,16 +78,6 @@ def test_lazy_import_annotations_resolve(): assert typing.get_type_hints(knowledge_storage.create_client)["config"] is typing.Any -def test_rag_config_types_still_resolve(): - """The provider config union still exposes the real config classes.""" - import typing - - from crewai.rag.config.types import SupportedProviderConfig - - names = sorted(m.__name__ for m in typing.get_args(SupportedProviderConfig)) - assert names == ["ChromaDBConfig", "QdrantConfig"] - - def test_knowledge_storage_instantiates(): """KnowledgeStorage still builds its pydantic model with lazy rag imports.""" from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage