feat: implement crewAI lite version with optional dependencies

- Restructure pyproject.toml to move non-essential dependencies to optional extras
- Add graceful handling for missing optional dependencies in core modules
- Create memory, knowledge, telemetry, visualization, auth, and llm-integrations extras
- Implement helpful ImportError messages directing users to install specific extras
- Add comprehensive test suite for lite installation scenarios
- Maintain backward compatibility with existing installations
- Support minimal core installation with Agent/Crew/Task functionality

Addresses GitHub issue #3026 for lightweight crewAI installation

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-06-18 10:13:42 +00:00
parent db1e9e9b9a
commit bccb27ab2e
12 changed files with 692 additions and 52 deletions

View File

@@ -0,0 +1,193 @@
"""Test that crewAI lite installation works with minimal dependencies."""
import pytest
import subprocess
import sys
import tempfile
import os
from pathlib import Path
from unittest.mock import patch, Mock
def test_core_imports_work_without_optional_deps():
"""Test that core crewAI functionality can be imported without optional dependencies."""
try:
from crewai import Agent, Crew, Task, LLM
from crewai.lite_agent import LiteAgent
from crewai.process import Process
assert Agent is not None
assert Crew is not None
assert Task is not None
assert LLM is not None
assert LiteAgent is not None
assert Process is not None
except ImportError as e:
pytest.fail(f"Core imports should work without optional dependencies: {e}")
def test_optional_memory_import_error():
"""Test that memory functionality raises helpful error without chromadb."""
with patch.dict('sys.modules', {'chromadb': None}):
with patch('crewai.memory.storage.rag_storage.CHROMADB_AVAILABLE', False):
from crewai.memory.storage.rag_storage import RAGStorage
with pytest.raises(ImportError) as exc_info:
RAGStorage("test")
assert "ChromaDB is required" in str(exc_info.value)
assert "crewai[memory]" in str(exc_info.value)
def test_optional_knowledge_import_error():
"""Test that knowledge functionality raises helpful error without dependencies."""
with patch.dict('sys.modules', {'chromadb': None}):
with patch('crewai.knowledge.storage.knowledge_storage.CHROMADB_AVAILABLE', False):
from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage
with pytest.raises(ImportError) as exc_info:
KnowledgeStorage()
assert "ChromaDB is required" in str(exc_info.value)
assert "crewai[knowledge]" in str(exc_info.value)
def test_optional_pdf_import_error():
"""Test that PDF knowledge source raises helpful error without pdfplumber."""
with patch.dict('sys.modules', {'pdfplumber': None}):
with patch('crewai.knowledge.source.pdf_knowledge_source.PDFPLUMBER_AVAILABLE', False):
from crewai.knowledge.source.pdf_knowledge_source import PDFKnowledgeSource
knowledge_dir = Path("knowledge/tmp")
knowledge_dir.mkdir(parents=True, exist_ok=True)
test_file = knowledge_dir / "test.pdf"
test_file.touch()
try:
with pytest.raises(ImportError) as exc_info:
PDFKnowledgeSource(file_paths=["tmp/test.pdf"])
assert "pdfplumber is required" in str(exc_info.value)
assert "crewai[knowledge]" in str(exc_info.value)
finally:
if test_file.exists():
test_file.unlink()
if knowledge_dir.exists() and not any(knowledge_dir.iterdir()):
knowledge_dir.rmdir()
def test_optional_visualization_import_error():
"""Test that flow visualization raises helpful error without pyvis."""
with patch.dict('sys.modules', {'pyvis': None}):
with patch('crewai.flow.flow_visualizer.PYVIS_AVAILABLE', False):
from crewai.flow.flow_visualizer import plot_flow
mock_flow = Mock()
with pytest.raises(ImportError) as exc_info:
plot_flow(mock_flow)
assert "Pyvis is required" in str(exc_info.value)
assert "crewai[visualization]" in str(exc_info.value)
def test_telemetry_disabled_without_opentelemetry():
"""Test that telemetry is disabled gracefully without opentelemetry."""
from crewai.telemetry.telemetry import Telemetry
telemetry = Telemetry()
assert isinstance(telemetry.ready, bool)
assert isinstance(telemetry._is_telemetry_disabled(), bool)
def test_lite_agent_works_without_optional_deps():
"""Test that LiteAgent can be created and used without optional dependencies."""
from crewai.lite_agent import LiteAgent
from crewai import LLM
from unittest.mock import Mock
mock_llm = Mock(spec=LLM)
mock_llm.call.return_value = "Test response"
mock_llm.model = "test-model"
agent = LiteAgent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
llm=mock_llm,
verbose=False
)
assert agent.role == "Test Agent"
assert agent.goal == "Test Goal"
assert agent.backstory == "Test Backstory"
def test_basic_crew_creation_without_optional_deps():
"""Test that basic Crew can be created without optional dependencies."""
from crewai import Agent, Crew, Task, LLM
from unittest.mock import Mock
mock_llm = Mock(spec=LLM)
mock_llm.call.return_value = "Test response"
mock_llm.model = "test-model"
agent = Agent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
llm=mock_llm,
verbose=False
)
task = Task(
description="Test task",
agent=agent,
expected_output="Test output"
)
crew = Crew(
agents=[agent],
tasks=[task],
verbose=False
)
assert crew.agents[0].role == "Test Agent"
assert crew.tasks[0].description == "Test task"
def test_core_functionality_without_optional_deps():
"""Test that core crewAI functionality works without optional dependencies."""
from crewai import Agent, Task, Crew, LLM
from unittest.mock import Mock
mock_llm = Mock(spec=LLM)
mock_llm.call.return_value = "Test response"
mock_llm.model = "test-model"
agent = Agent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
llm=mock_llm,
verbose=False
)
task = Task(
description="Test task description",
agent=agent,
expected_output="Test expected output"
)
crew = Crew(
agents=[agent],
tasks=[task],
verbose=False
)
assert agent.role == "Test Agent"
assert task.description == "Test task description"
assert len(crew.agents) == 1
assert len(crew.tasks) == 1

View File

@@ -0,0 +1,122 @@
"""Test optional dependency handling for crewAI lite version."""
import pytest
from unittest.mock import patch, Mock
import sys
class TestOptionalDependencies:
"""Test that optional dependencies are handled gracefully."""
def test_chromadb_import_error_memory(self):
"""Test that memory functionality raises helpful error without chromadb."""
with patch.dict('sys.modules', {'chromadb': None}):
with patch('crewai.memory.storage.rag_storage.CHROMADB_AVAILABLE', False):
from crewai.memory.storage.rag_storage import RAGStorage
with pytest.raises(ImportError) as exc_info:
RAGStorage("test")
assert "ChromaDB is required" in str(exc_info.value)
assert "crewai[memory]" in str(exc_info.value)
def test_chromadb_import_error_knowledge(self):
"""Test that knowledge functionality raises helpful error without chromadb."""
with patch.dict('sys.modules', {'chromadb': None}):
with patch('crewai.knowledge.storage.knowledge_storage.CHROMADB_AVAILABLE', False):
from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage
with pytest.raises(ImportError) as exc_info:
KnowledgeStorage()
assert "ChromaDB is required" in str(exc_info.value)
assert "crewai[knowledge]" in str(exc_info.value)
def test_pdfplumber_import_error(self):
"""Test that PDF knowledge source raises helpful error without pdfplumber."""
with patch.dict('sys.modules', {'pdfplumber': None}):
from crewai.knowledge.source.pdf_knowledge_source import PDFKnowledgeSource
pdf_source = PDFKnowledgeSource(file_paths=["test.pdf"])
with pytest.raises(ImportError) as exc_info:
pdf_source._import_pdfplumber()
assert "pdfplumber is required" in str(exc_info.value)
assert "crewai[knowledge]" in str(exc_info.value)
def test_pyvis_import_error(self):
"""Test that flow visualization raises helpful error without pyvis."""
with patch.dict('sys.modules', {'pyvis': None}):
with patch('crewai.flow.flow_visualizer.PYVIS_AVAILABLE', False):
from crewai.flow.flow_visualizer import plot_flow
mock_flow = Mock()
with pytest.raises(ImportError) as exc_info:
plot_flow(mock_flow)
assert "Pyvis is required" in str(exc_info.value)
assert "crewai[visualization]" in str(exc_info.value)
def test_auth0_import_error(self):
"""Test that authentication raises helpful error without auth0."""
with patch.dict('sys.modules', {'auth0': None}):
with patch('crewai.cli.authentication.utils.AUTH0_AVAILABLE', False):
from crewai.cli.authentication.utils import validate_token
with pytest.raises(ImportError) as exc_info:
validate_token("fake_token")
assert "Auth0 is required" in str(exc_info.value)
assert "crewai[auth]" in str(exc_info.value)
def test_aisuite_import_error(self):
"""Test that AISuite LLM raises helpful error without aisuite."""
with patch.dict('sys.modules', {'aisuite': None}):
with patch('crewai.llms.third_party.ai_suite.AISUITE_AVAILABLE', False):
from crewai.llms.third_party.ai_suite import AISuiteLLM
with pytest.raises(ImportError) as exc_info:
AISuiteLLM("test-model")
assert "AISuite is required" in str(exc_info.value)
assert "crewai[llm-integrations]" in str(exc_info.value)
def test_opentelemetry_graceful_degradation(self):
"""Test that telemetry degrades gracefully without opentelemetry."""
with patch.dict('sys.modules', {'opentelemetry': None}):
with patch('crewai.telemetry.telemetry.OPENTELEMETRY_AVAILABLE', False):
from crewai.telemetry.telemetry import Telemetry
telemetry = Telemetry()
assert not telemetry.ready
assert telemetry._is_telemetry_disabled()
assert not telemetry._should_execute_telemetry()
def test_embedding_configurator_import_error(self):
"""Test that embedding configurator raises helpful error without chromadb."""
with patch.dict('sys.modules', {'chromadb': None}):
with patch('crewai.utilities.embedding_configurator.CHROMADB_AVAILABLE', False):
from crewai.utilities.embedding_configurator import EmbeddingConfigurator
configurator = EmbeddingConfigurator()
with pytest.raises(ImportError) as exc_info:
configurator.configure_embedder(None)
assert "ChromaDB is required" in str(exc_info.value)
assert "crewai[memory]" in str(exc_info.value) or "crewai[knowledge]" in str(exc_info.value)
def test_docling_import_error(self):
"""Test that docling knowledge source raises helpful error without docling."""
with patch.dict('sys.modules', {'docling': None}):
with patch('crewai.knowledge.source.crew_docling_source.DOCLING_AVAILABLE', False):
from crewai.knowledge.source.crew_docling_source import CrewDoclingSource
with pytest.raises(ImportError) as exc_info:
CrewDoclingSource()
assert "docling package is required" in str(exc_info.value)
assert "uv add docling" in str(exc_info.value)