mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-08 00:15:12 +00:00
Compare commits
13 Commits
1.15.2a2
...
rn/con-109
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a41e6f972 | ||
|
|
6a714c42dd | ||
|
|
051e9e4fed | ||
|
|
4b018a2b5b | ||
|
|
402f45078a | ||
|
|
8b423d9a1b | ||
|
|
a76cef6af9 | ||
|
|
0b92d2f69c | ||
|
|
5cc6cb0a27 | ||
|
|
6604f0fe3b | ||
|
|
d65e0485b4 | ||
|
|
8d192c1715 | ||
|
|
3546a48922 |
@@ -212,8 +212,9 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default_factory=TaskOutputStorageHandler
|
||||
)
|
||||
_kickoff_event_id: str | None = PrivateAttr(default=None)
|
||||
_name_was_explicit: bool = PrivateAttr(default=False)
|
||||
|
||||
name: str | None = Field(default="crew")
|
||||
name: str | None = Field(default=None)
|
||||
cache: bool = Field(default=True)
|
||||
tasks: list[Task] = Field(default_factory=list)
|
||||
agents: Annotated[
|
||||
@@ -546,6 +547,16 @@ class Crew(FlowTrackable, BaseModel):
|
||||
# TODO: Improve typing
|
||||
return json.loads(v) if isinstance(v, Json) else v # type: ignore
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _resolve_name(self) -> Self:
|
||||
"""Fall back to the class name when no explicit name is provided."""
|
||||
# Snapshot whether `name` was user-provided before the assignment below
|
||||
# pollutes `model_fields_set`.
|
||||
self._name_was_explicit = "name" in self.model_fields_set
|
||||
if not self._name_was_explicit:
|
||||
self.name = type(self).__name__
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_private_attrs(self) -> Crew:
|
||||
"""set private attributes."""
|
||||
@@ -581,8 +592,8 @@ class Crew(FlowTrackable, BaseModel):
|
||||
"""
|
||||
from crewai.memory.utils import sanitize_scope_name
|
||||
|
||||
# Compute sanitized crew name for root_scope
|
||||
crew_name = sanitize_scope_name(self.name or "crew")
|
||||
# `name` is guaranteed non-None by the `_resolve_name` validator.
|
||||
crew_name = sanitize_scope_name(cast(str, self.name))
|
||||
crew_root_scope = f"/crew/{crew_name}"
|
||||
|
||||
if self.memory is True:
|
||||
@@ -889,7 +900,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTrainFailedEvent(error=str(e), crew_name=self.name),
|
||||
CrewTrainFailedEvent(
|
||||
error=str(e),
|
||||
crew_name=self.name,
|
||||
),
|
||||
)
|
||||
self._logger.log("error", f"Training failed: {e}", color="red")
|
||||
CrewTrainingHandler(TRAINING_DATA_FILE).clear()
|
||||
@@ -2078,7 +2092,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTestFailedEvent(error=str(e), crew_name=self.name),
|
||||
CrewTestFailedEvent(
|
||||
error=str(e),
|
||||
crew_name=self.name,
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Iterable, Mapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from opentelemetry import baggage
|
||||
|
||||
@@ -309,9 +309,10 @@ def prepare_kickoff(
|
||||
from crewai.events.utils.console_formatter import ConsoleFormatter
|
||||
|
||||
fmt = ConsoleFormatter(verbose=True)
|
||||
# `name` is guaranteed non-None by the `_resolve_name` validator.
|
||||
content = fmt.create_status_content(
|
||||
"Resuming from Checkpoint",
|
||||
crew.name or "Crew",
|
||||
cast(str, crew.name),
|
||||
"bright_magenta",
|
||||
ID=str(crew.id),
|
||||
)
|
||||
@@ -319,7 +320,10 @@ def prepare_kickoff(
|
||||
content, "\U0001f504 Resuming from Checkpoint", "bright_magenta"
|
||||
)
|
||||
else:
|
||||
started_event = CrewKickoffStartedEvent(crew_name=crew.name, inputs=normalized)
|
||||
started_event = CrewKickoffStartedEvent(
|
||||
crew_name=crew.name,
|
||||
inputs=normalized,
|
||||
)
|
||||
crew._kickoff_event_id = started_event.event_id
|
||||
future = crewai_event_bus.emit(crew, started_event)
|
||||
if future is not None:
|
||||
|
||||
@@ -715,7 +715,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
"""
|
||||
user_context = self._get_user_context()
|
||||
execution_metadata = {
|
||||
"crew_name": getattr(event, "crew_name", "Unknown Crew"),
|
||||
"crew_name": getattr(event, "crew_name", None) or "Unknown Crew",
|
||||
"execution_start": event.timestamp,
|
||||
"crewai_version": get_crewai_version(),
|
||||
}
|
||||
@@ -791,7 +791,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
if not self.batch_manager.is_batch_initialized():
|
||||
user_context = self._get_user_context()
|
||||
execution_metadata = {
|
||||
"crew_name": getattr(source, "name", "Unknown Crew"),
|
||||
"crew_name": getattr(source, "name", None) or "Unknown Crew",
|
||||
"crewai_version": get_crewai_version(),
|
||||
}
|
||||
self._initialize_batch(user_context, execution_metadata)
|
||||
|
||||
@@ -238,6 +238,12 @@ def crew(
|
||||
|
||||
crew_instance: Crew = _call_method(meth, self, *args, **kwargs)
|
||||
|
||||
# Propagate the @CrewBase class name only when the user didn't pass an
|
||||
# explicit `name=` to the Crew constructor inside the factory method.
|
||||
crewbase_name = getattr(self, "_crew_name", None)
|
||||
if crewbase_name and not crew_instance._name_was_explicit:
|
||||
crew_instance.name = crewbase_name
|
||||
|
||||
def callback_wrapper(
|
||||
hook: Callable[Concatenate[CrewInstance, P2], R2], instance: CrewInstance
|
||||
) -> Callable[P2, R2]:
|
||||
|
||||
@@ -8,6 +8,7 @@ from concurrent.futures import Future
|
||||
from hashlib import md5
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, MagicMock, call, patch
|
||||
|
||||
from crewai.agent import Agent
|
||||
@@ -17,6 +18,7 @@ from crewai.crew import Crew
|
||||
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,
|
||||
@@ -4738,7 +4740,46 @@ def test_default_crew_name(researcher, writer):
|
||||
Task(description="Task 2", expected_output="output", agent=writer),
|
||||
],
|
||||
)
|
||||
assert crew.name == "crew"
|
||||
assert crew.name == "Crew"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"explicit_name,expected",
|
||||
[
|
||||
(None, "ResearchAutomation"),
|
||||
("My Research Automation", "My Research Automation"),
|
||||
],
|
||||
ids=["class_name_fallback", "explicit_name_preserved"],
|
||||
)
|
||||
def test_crew_kickoff_started_emits_display_name(
|
||||
researcher, writer, explicit_name, expected
|
||||
):
|
||||
"""CrewKickoffStartedEvent emits crew.name when set, class name otherwise."""
|
||||
from crewai.crews.utils import prepare_kickoff
|
||||
|
||||
class ResearchAutomation(Crew):
|
||||
pass
|
||||
|
||||
crew_kwargs: dict[str, Any] = {
|
||||
"agents": [researcher, writer],
|
||||
"tasks": [
|
||||
Task(description="Task 1", expected_output="output", agent=researcher),
|
||||
],
|
||||
}
|
||||
if explicit_name is not None:
|
||||
crew_kwargs["name"] = explicit_name
|
||||
crew = ResearchAutomation(**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)
|
||||
|
||||
prepare_kickoff(crew, inputs=None)
|
||||
|
||||
assert captured == [expected]
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
|
||||
@@ -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,65 @@ 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."""
|
||||
internal_crew_cls = cast(type[Any], InternalCrew)
|
||||
crew_instance: Crew = cast(Any, internal_crew_cls()).crew()
|
||||
assert crew_instance.name == "InternalCrew"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_crew_decorator_preserves_explicit_name_matching_class_name():
|
||||
"""Explicit name that happens to equal the class-name fallback must still win."""
|
||||
sample_agent = Agent(role="r", goal="g", backstory="b")
|
||||
sample_task = Task(description="d", expected_output="o", agent=sample_agent)
|
||||
|
||||
@CrewBase
|
||||
class AmbiguousFactory:
|
||||
agents_config = None
|
||||
tasks_config = None
|
||||
agents: list[BaseAgent] = [sample_agent]
|
||||
tasks: list[Task] = [sample_task]
|
||||
|
||||
@crew
|
||||
def crew(self):
|
||||
# "Crew" is the class-name fallback the validator would use, but the
|
||||
# user set it explicitly — propagation must still skip the override.
|
||||
return Crew(
|
||||
name="Crew",
|
||||
agents=[sample_agent],
|
||||
tasks=[sample_task],
|
||||
)
|
||||
|
||||
factory_cls = cast(type[Any], AmbiguousFactory)
|
||||
crew_instance: Crew = cast(Any, factory_cls()).crew()
|
||||
assert crew_instance.name == "Crew"
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool():
|
||||
"""Return 'Hi!'"""
|
||||
|
||||
@@ -276,7 +276,12 @@ class TestTraceListenerSetup:
|
||||
expected_output="hello world",
|
||||
agent=agent,
|
||||
)
|
||||
crew = Crew(agents=[agent], tasks=[task], verbose=True)
|
||||
crew = Crew(
|
||||
name="Hello World Crew",
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
|
||||
@@ -304,7 +309,7 @@ class TestTraceListenerSetup:
|
||||
# Verify the first completion event has proper structure
|
||||
completion_event = completion_events[0]
|
||||
assert "crew_name" in completion_event.event_data
|
||||
assert completion_event.event_data["crew_name"] == "crew"
|
||||
assert completion_event.event_data["crew_name"] == "Hello World Crew"
|
||||
|
||||
# Verify all events have proper structure
|
||||
for call in add_event_mock.call_args_list:
|
||||
|
||||
Reference in New Issue
Block a user