diff --git a/lib/crewai/src/crewai/project/annotations.py b/lib/crewai/src/crewai/project/annotations.py index c198c979a0..b4b4b69d38 100644 --- a/lib/crewai/src/crewai/project/annotations.py +++ b/lib/crewai/src/crewai/project/annotations.py @@ -237,6 +237,8 @@ def wrapper(self: CrewInstance, *args: Any, **kwargs: Any) -> Crew: self.tasks = instantiated_tasks crew_instance: Crew = _call_method(meth, self, *args, **kwargs) + if "name" not in crew_instance.model_fields_set: + crew_instance.name = getattr(self, "_crew_name", None) or crew_instance.name def callback_wrapper( hook: Callable[Concatenate[CrewInstance, P2], R2], instance: CrewInstance diff --git a/lib/crewai/tests/test_crew.py b/lib/crewai/tests/test_crew.py index 9db9ef4e2f..3d6fe4602e 100644 --- a/lib/crewai/tests/test_crew.py +++ b/lib/crewai/tests/test_crew.py @@ -8,6 +8,7 @@ from hashlib import md5 import re import sys +from typing import Any, cast from unittest.mock import ANY, MagicMock, call, patch from crewai.agent import Agent @@ -17,6 +18,7 @@ from crewai.crews.crew_output import CrewOutput from crewai.events.event_bus import crewai_event_bus from crewai.events.types.crew_events import ( + CrewKickoffStartedEvent, CrewTestCompletedEvent, CrewTestStartedEvent, CrewTrainCompletedEvent, @@ -4741,6 +4743,61 @@ def test_default_crew_name(researcher, writer): assert crew.name == "crew" +@pytest.mark.parametrize( + "explicit_name,expected", + [ + (None, "ResearchAutomation"), + ("My Research Automation", "My Research Automation"), + ], + ids=["class_name_from_decorator", "explicit_name_preserved"], +) +def test_crew_kickoff_started_emits_display_name( + researcher, writer, explicit_name, expected +): + """Kickoff events should use the decorator-provided display name when implicit.""" + from crewai.crews.utils import prepare_kickoff + from crewai.project import CrewBase, agent, crew, task + + @CrewBase + class ResearchAutomation: + agents_config = None + tasks_config = None + + @agent + def researcher(self): + return researcher + + @task + def first_task(self): + return Task( + description="Task 1", + expected_output="output", + agent=self.researcher(), + ) + + @crew + def crew(self): + crew_kwargs: dict[str, Any] = { + "agents": self.agents, + "tasks": self.tasks, + } + if explicit_name is not None: + crew_kwargs["name"] = explicit_name + return Crew(**crew_kwargs) + + captured: list[str | None] = [] + with crewai_event_bus.scoped_handlers(): + + @crewai_event_bus.on(CrewKickoffStartedEvent) + def _capture(_source: Any, event: CrewKickoffStartedEvent) -> None: + captured.append(event.crew_name) + + automation_cls = cast(type[Any], ResearchAutomation) + prepare_kickoff(cast(Any, automation_cls()).crew(), inputs=None) + + assert captured == [expected] + + @pytest.mark.vcr() def test_memory_remember_receives_task_content(): """With memory=True, extract_memories receives raw content with task, agent, expected output, and result.""" diff --git a/lib/crewai/tests/test_project.py b/lib/crewai/tests/test_project.py index 9d7f332da4..368afe7fd6 100644 --- a/lib/crewai/tests/test_project.py +++ b/lib/crewai/tests/test_project.py @@ -1,4 +1,4 @@ -from typing import Any, ClassVar +from typing import Any, ClassVar, cast from unittest.mock import Mock, create_autospec, patch import pytest @@ -261,6 +261,55 @@ def test_crew_name(): assert crew._crew_name == "InternalCrew" +def test_crew_decorator_propagates_class_name_to_instance(): + """@crew-decorated factory method should set Crew.name to the decorated class name.""" + sample_agent = Agent(role="r", goal="g", backstory="b") + sample_task = Task(description="d", expected_output="o", agent=sample_agent) + + @CrewBase + class ImplicitNameCrewFactory: + agents_config = None + tasks_config = None + agents: list[BaseAgent] = [sample_agent] + tasks: list[Task] = [sample_task] + + @crew + def crew(self): + return Crew( + agents=[sample_agent], + tasks=[sample_task], + ) + + factory_cls = cast(type[Any], ImplicitNameCrewFactory) + crew_instance: Crew = cast(Any, factory_cls()).crew() + assert crew_instance.name == "ImplicitNameCrewFactory" + + +def test_crew_decorator_preserves_explicit_name(): + """Explicit Crew(name=...) inside @crew should win over the @CrewBase class name.""" + sample_agent = Agent(role="r", goal="g", backstory="b") + sample_task = Task(description="d", expected_output="o", agent=sample_agent) + + @CrewBase + class NamedCrewFactory: + agents_config = None + tasks_config = None + agents: list[BaseAgent] = [sample_agent] + tasks: list[Task] = [sample_task] + + @crew + def crew(self): + return Crew( + name="My Explicit Name", + agents=[sample_agent], + tasks=[sample_task], + ) + + factory_cls = cast(type[Any], NamedCrewFactory) + crew_instance: Crew = cast(Any, factory_cls()).crew() + assert crew_instance.name == "My Explicit Name" + + @tool def simple_tool(): """Return 'Hi!'"""