Compare commits

..

2 Commits

Author SHA1 Message Date
Devin AI
cc4bc371c0 Fix lint errors: Import Any from typing and remove quotes from type annotation
Co-Authored-By: João <joao@crewai.com>
2025-10-23 10:02:46 +00:00
Devin AI
edfbec4740 Fix: Set crew attribute on planning agent to prevent EventBus errors
- Added crew parameter to CrewPlanner.__init__() to accept crew reference
- Modified _create_planning_agent() to set crew attribute on planning agent
- Updated Crew._handle_crew_planning() to pass crew reference to CrewPlanner
- Added test to verify planning agent has crew attribute set
- Renamed test class from InternalCrewPlanner to TestCrewPlanner for pytest compatibility

Fixes #3782

Co-Authored-By: João <joao@crewai.com>
2025-10-23 09:59:40 +00:00
14 changed files with 131 additions and 120 deletions

View File

@@ -12,7 +12,7 @@ dependencies = [
"pytube>=15.0.0",
"requests>=2.32.5",
"docker>=7.1.0",
"crewai==1.2.0",
"crewai==1.1.0",
"lancedb>=0.5.4",
"tiktoken>=0.8.0",
"beautifulsoup4>=4.13.4",

View File

@@ -287,4 +287,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.2.0"
__version__ = "1.1.0"

View File

@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.2.0",
"crewai-tools==1.1.0",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.2.0"
__version__ = "1.1.0"
_telemetry_submitted = False

View File

@@ -322,7 +322,7 @@ MODELS = {
],
}
DEFAULT_LLM_MODEL = "gpt-4.1-mini"
DEFAULT_LLM_MODEL = "gpt-4o-mini"
JSON_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.2.0"
"crewai[tools]==1.1.0"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.2.0"
"crewai[tools]==1.1.0"
]
[project.scripts]

View File

@@ -779,7 +779,7 @@ class Crew(FlowTrackable, BaseModel):
"""Handles the Crew planning."""
self._logger.log("info", "Planning the crew execution")
result = CrewPlanner(
tasks=self.tasks, planning_agent_llm=self.planning_llm
tasks=self.tasks, planning_agent_llm=self.planning_llm, crew=self
)._handle_crew_planning()
for task, step_plan in zip(

View File

@@ -29,8 +29,8 @@ def create_llm(
try:
return LLM(model=llm_value)
except Exception as e:
logger.error(f"Error instantiating LLM from string: {e}")
raise e
logger.debug(f"Failed to instantiate LLM with model='{llm_value}': {e}")
return None
if llm_value is None:
return _llm_via_environment_or_fallback()
@@ -62,8 +62,8 @@ def create_llm(
)
except Exception as e:
logger.error(f"Error instantiating LLM from unknown object type: {e}")
raise e
logger.debug(f"Error instantiating LLM from unknown object type: {e}")
return None
UNACCEPTED_ATTRIBUTES: Final[list[str]] = [
@@ -176,10 +176,10 @@ def _llm_via_environment_or_fallback() -> LLM | None:
try:
return LLM(**llm_params)
except Exception as e:
logger.error(
logger.debug(
f"Error instantiating LLM from environment/fallback: {type(e).__name__}: {e}"
)
raise e
return None
def _normalize_key_name(key_name: str) -> str:

View File

@@ -1,6 +1,7 @@
"""Handles planning and coordination of crew tasks."""
import logging
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, Field
@@ -9,6 +10,10 @@ from crewai.llms.base_llm import BaseLLM
from crewai.task import Task
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
@@ -37,19 +42,25 @@ class CrewPlanner:
Attributes:
tasks: List of tasks to be planned.
planning_agent_llm: Optional LLM model for the planning agent.
crew: Optional reference to the crew instance.
"""
def __init__(
self, tasks: list[Task], planning_agent_llm: str | BaseLLM | None = None
self,
tasks: list[Task],
planning_agent_llm: str | BaseLLM | None = None,
crew: Any = None,
) -> None:
"""Initialize CrewPlanner with tasks and optional planning agent LLM.
Args:
tasks: List of tasks to be planned.
planning_agent_llm: Optional LLM model for the planning agent. Defaults to None.
crew: Optional reference to the crew instance. Defaults to None.
"""
self.tasks = tasks
self.planning_agent_llm = planning_agent_llm or "gpt-4o-mini"
self.crew = crew
def _handle_crew_planning(self) -> PlannerTaskPydanticOutput:
"""Handles the Crew planning by creating detailed step-by-step plans for each task.
@@ -80,7 +91,7 @@ class CrewPlanner:
Returns:
An Agent instance configured for planning tasks.
"""
return Agent(
planning_agent = Agent(
role="Task Execution Planner",
goal=(
"Your goal is to create an extremely detailed, step-by-step plan based on the tasks and tools "
@@ -89,6 +100,9 @@ class CrewPlanner:
backstory="Planner agent for crew planning",
llm=self.planning_agent_llm,
)
if self.crew:
planning_agent.crew = self.crew
return planning_agent
@staticmethod
def _create_planner_task(planning_agent: Agent, tasks_summary: str) -> Task:

View File

@@ -6,7 +6,6 @@ from unittest import mock
from unittest.mock import MagicMock, patch
from crewai.agents.crew_agent_executor import AgentFinish, CrewAgentExecutor
from crewai.cli.constants import DEFAULT_LLM_MODEL
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.tool_usage_events import ToolUsageFinishedEvent
from crewai.knowledge.knowledge import Knowledge
@@ -136,7 +135,7 @@ def test_agent_with_missing_response_template():
def test_agent_default_values():
agent = Agent(role="test role", goal="test goal", backstory="test backstory")
assert agent.llm.model == DEFAULT_LLM_MODEL
assert agent.llm.model == "gpt-4o-mini"
assert agent.allow_delegation is False
@@ -226,7 +225,7 @@ def test_logging_tool_usage():
verbose=True,
)
assert agent.llm.model == DEFAULT_LLM_MODEL
assert agent.llm.model == "gpt-4o-mini"
assert agent.tools_handler.last_used_tool is None
task = Task(
description="What is 3 times 4?",

View File

@@ -1,79 +1,77 @@
import os
from typing import Any
from unittest.mock import patch
from crewai.cli.constants import DEFAULT_LLM_MODEL
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
from crewai.utilities.llm_utils import create_llm
import pytest
def test_create_llm_with_llm_instance() -> None:
try:
from litellm.exceptions import BadRequestError
except ImportError:
BadRequestError = Exception
def test_create_llm_with_llm_instance():
existing_llm = LLM(model="gpt-4o")
llm = create_llm(llm_value=existing_llm)
assert llm is existing_llm
def test_create_llm_with_valid_model_string():
llm = create_llm(llm_value="gpt-4o")
assert isinstance(llm, BaseLLM)
assert llm.model == "gpt-4o"
def test_create_llm_with_invalid_model_string():
# For invalid model strings, create_llm succeeds but call() fails with API error
llm = create_llm(llm_value="invalid-model")
assert llm is not None
assert isinstance(llm, BaseLLM)
# The error should occur when making the actual API call
# We expect some kind of API error (NotFoundError, etc.)
with pytest.raises(Exception): # noqa: B017
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
def test_create_llm_with_unknown_object_missing_attributes():
class UnknownObject:
pass
unknown_obj = UnknownObject()
llm = create_llm(llm_value=unknown_obj)
# Should succeed because str(unknown_obj) provides a model name
assert llm is not None
assert isinstance(llm, BaseLLM)
def test_create_llm_with_none_uses_default_model():
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
existing_llm = LLM(model="gpt-4o")
llm = create_llm(llm_value=existing_llm)
assert llm is existing_llm
def test_create_llm_with_valid_model_string() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
llm = create_llm(llm_value="gpt-4o")
assert isinstance(llm, BaseLLM)
assert llm.model == "gpt-4o"
def test_create_llm_with_invalid_model_string() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
# For invalid model strings, create_llm succeeds but call() fails with API error
llm = create_llm(llm_value="invalid-model")
assert llm is not None
assert isinstance(llm, BaseLLM)
# The error should occur when making the actual API call
# We expect some kind of API error (NotFoundError, etc.)
with pytest.raises(Exception): # noqa: B017
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
def test_create_llm_with_unknown_object_missing_attributes() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
class UnknownObject:
pass
unknown_obj = UnknownObject()
llm = create_llm(llm_value=unknown_obj)
# Should succeed because str(unknown_obj) provides a model name
assert llm is not None
assert isinstance(llm, BaseLLM)
def test_create_llm_with_none_uses_default_model() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
with patch("crewai.utilities.llm_utils.DEFAULT_LLM_MODEL", DEFAULT_LLM_MODEL):
with patch("crewai.utilities.llm_utils.DEFAULT_LLM_MODEL", "gpt-4o-mini"):
llm = create_llm(llm_value=None)
assert isinstance(llm, BaseLLM)
assert llm.model == DEFAULT_LLM_MODEL
assert llm.model == "gpt-4o-mini"
def test_create_llm_with_unknown_object() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
class UnknownObject:
model_name = "gpt-4o"
temperature = 0.7
max_tokens = 1500
def test_create_llm_with_unknown_object():
class UnknownObject:
model_name = "gpt-4o"
temperature = 0.7
max_tokens = 1500
unknown_obj = UnknownObject()
llm = create_llm(llm_value=unknown_obj)
assert isinstance(llm, BaseLLM)
assert llm.model == "gpt-4o"
assert llm.temperature == 0.7
if hasattr(llm, 'max_tokens'):
assert llm.max_tokens == 1500
unknown_obj = UnknownObject()
llm = create_llm(llm_value=unknown_obj)
assert isinstance(llm, BaseLLM)
assert llm.model == "gpt-4o"
assert llm.temperature == 0.7
assert llm.max_tokens == 1500
def test_create_llm_from_env_with_unaccepted_attributes() -> None:
def test_create_llm_from_env_with_unaccepted_attributes():
with patch.dict(
os.environ,
{
@@ -92,47 +90,25 @@ def test_create_llm_from_env_with_unaccepted_attributes() -> None:
assert not hasattr(llm, "AWS_REGION_NAME")
def test_create_llm_with_partial_attributes() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
class PartialAttributes:
model_name = "gpt-4o"
# temperature is missing
def test_create_llm_with_partial_attributes():
class PartialAttributes:
model_name = "gpt-4o"
# temperature is missing
obj = PartialAttributes()
llm = create_llm(llm_value=obj)
assert isinstance(llm, BaseLLM)
assert llm.model == "gpt-4o"
assert llm.temperature is None # Should handle missing attributes gracefully
obj = PartialAttributes()
llm = create_llm(llm_value=obj)
assert isinstance(llm, BaseLLM)
assert llm.model == "gpt-4o"
assert llm.temperature is None # Should handle missing attributes gracefully
def test_create_llm_with_invalid_type() -> None:
with patch.dict(os.environ, {"OPENAI_API_KEY": "fake-key"}, clear=True):
# For integers, create_llm succeeds because str(42) becomes "42"
llm = create_llm(llm_value=42)
assert llm is not None
assert isinstance(llm, BaseLLM)
assert llm.model == "42"
def test_create_llm_with_invalid_type():
# For integers, create_llm succeeds because str(42) becomes "42"
llm = create_llm(llm_value=42)
assert llm is not None
assert isinstance(llm, BaseLLM)
assert llm.model == "42"
# The error should occur when making the actual API call
with pytest.raises(Exception): # noqa: B017
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])
def test_create_llm_openai_missing_api_key() -> None:
"""Test that create_llm raises error when OpenAI API key is missing"""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises((ValueError, ImportError)) as exc_info:
create_llm(llm_value="gpt-4o")
error_message = str(exc_info.value).lower()
assert "openai_api_key" in error_message or "api_key" in error_message
def test_create_llm_anthropic_missing_dependency() -> None:
"""Test that create_llm raises error when Anthropic dependency is missing"""
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "fake-key"}, clear=True):
with patch("crewai.llm.LLM.__new__", side_effect=ImportError('Anthropic native provider not available, to install: uv add "crewai[anthropic]"')):
with pytest.raises(ImportError) as exc_info:
create_llm(llm_value="anthropic/claude-3-sonnet")
assert "Anthropic native provider not available, to install: uv add \"crewai[anthropic]\"" in str(exc_info.value)
# The error should occur when making the actual API call
with pytest.raises(Exception): # noqa: B017
llm.call(messages=[{"role": "user", "content": "Hello, world!"}])

View File

@@ -13,7 +13,7 @@ from crewai.utilities.planning_handler import (
)
class InternalCrewPlanner:
class TestCrewPlanner:
@pytest.fixture
def crew_planner(self):
tasks = [
@@ -177,3 +177,25 @@ class InternalCrewPlanner:
crew_planner_different_llm.tasks
)
execute.assert_called_once()
def test_planning_agent_has_crew_attribute(self):
"""Test that planning agent has crew attribute set to avoid EventBus errors."""
from crewai.crew import Crew
# Create a crew with planning enabled
agent = Agent(role="Test Agent", goal="Test Goal", backstory="Test Backstory")
task = Task(
description="Test task",
expected_output="Test output",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], planning=True)
planner = CrewPlanner(tasks=[task], planning_agent_llm="gpt-4o-mini", crew=crew)
planning_agent = planner._create_planning_agent()
# Verify the planning agent has crew attribute set
assert planning_agent.crew is not None
assert planning_agent.crew == crew
# Verify that accessing agent.crew.key doesn't raise an error
assert planning_agent.crew.key is not None

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.2.0"
__version__ = "1.1.0"