From 52e0a84829a0503ea09f87c3cf35c77cf7f3fc10 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Mon, 3 Mar 2025 20:57:41 +0530 Subject: [PATCH 1/7] Added .copy for manager agent and shallow copy for manager llm --- src/crewai/crew.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 9cecfed3a..c3acf4a80 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1111,13 +1111,19 @@ class Crew(BaseModel): "_short_term_memory", "_long_term_memory", "_entity_memory", + "_telemetry", "agents", "tasks", "knowledge_sources", "knowledge", + "manager_agent", + "manager_llm", + } cloned_agents = [agent.copy() for agent in self.agents] + manager_agent = self.manager_agent.copy() if self.manager_agent else None + manager_llm = shallow_copy(self.manager_llm) if self.manager_llm else None task_mapping = {} @@ -1150,10 +1156,14 @@ class Crew(BaseModel): tasks=cloned_tasks, knowledge_sources=existing_knowledge_sources, knowledge=existing_knowledge, + manager_agent=manager_agent, + manager_llm=manager_llm, ) return copied_crew + + def _set_tasks_callbacks(self) -> None: """Sets callback for every task suing task_callback""" for task in self.tasks: From cf1864ce0fd02204f04c15e283e1b995a8b1acd4 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Mon, 3 Mar 2025 21:12:21 +0530 Subject: [PATCH 2/7] Added docstring --- src/crewai/crew.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index c3acf4a80..b19eea20c 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1099,7 +1099,16 @@ class Crew(BaseModel): return required_inputs def copy(self): - """Create a deep copy of the Crew.""" + """ + Creates a deep copy of the Crew instance. + + Handles copying of: + - Basic attributes + - Manager agent and LLM configurations + + Returns: + Crew: A new instance with copied components + """ exclude = { "id", From 1e49d1b5928c865670dafca7f4784d07f3c744ac Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 20 Mar 2025 22:47:46 +0530 Subject: [PATCH 3/7] Fixed doc string of copy function --- src/crewai/crew.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 8fb56452a..57b3f07c3 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1122,11 +1122,7 @@ class Crew(BaseModel): def copy(self): """ Creates a deep copy of the Crew instance. - - Handles copying of: - - Basic attributes - - Manager agent and LLM configurations - + Returns: Crew: A new instance with copied components """ From 6145331ee48ecff2e4c735dffea5c02f96f6e190 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 27 Mar 2025 00:12:38 +0530 Subject: [PATCH 4/7] Added test cases mentioned in the issue --- src/crewai/crew.py | 2 -- tests/crew_test.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 78429468b..0f6db8c4a 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1224,8 +1224,6 @@ class Crew(BaseModel): return copied_crew - - def _set_tasks_callbacks(self) -> None: """Sets callback for every task suing task_callback""" for task in self.tasks: diff --git a/tests/crew_test.py b/tests/crew_test.py index 39a3e9a08..bc137a214 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -11,7 +11,9 @@ import pydantic_core import pytest from crewai.agent import Agent +from crewai.agents import CacheHandler from crewai.agents.cache import CacheHandler +from crewai.agents.crew_agent_executor import CrewAgentExecutor from crewai.crew import Crew from crewai.crews.crew_output import CrewOutput from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource @@ -4025,3 +4027,52 @@ def test_crew_with_knowledge_sources_works_with_copy(): assert len(crew_copy.tasks) == len(crew.tasks) assert len(crew_copy.tasks) == len(crew.tasks) + + +def test_crew_kickoff_for_each_works_with_manager_agent_copy(): + researcher = Agent( + role="Researcher", + goal="Conduct thorough research and analysis on AI and AI agents", + backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", + allow_delegation=False + ) + + writer = Agent( + role="Senior Writer", + goal="Create compelling content about AI and AI agents", + backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", + allow_delegation=False + ) + + # Define task + task = Task( + description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", + expected_output="5 bullet points, each with a paragraph and accompanying notes.", + ) + + # Define manager agent + manager = Agent( + role="Project Manager", + goal="Efficiently manage the crew and ensure high-quality task completion", + backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", + allow_delegation=True + ) + + # Instantiate crew with a custom manager + crew = Crew( + agents=[researcher, writer], + tasks=[task], + manager_agent=manager, + process=Process.hierarchical, + verbose=True + ) + + crew_copy = crew.copy() + assert crew_copy.manager_agent is not None + assert crew_copy.manager_agent.id != crew.manager_agent.id + assert crew_copy.manager_agent.role == crew.manager_agent.role + assert crew_copy.manager_agent.goal == crew.manager_agent.goal + assert crew_copy.manager_agent.backstory == crew.manager_agent.backstory + assert isinstance(crew_copy.manager_agent.agent_executor, CrewAgentExecutor) + assert isinstance(crew_copy.manager_agent.cache_handler, CacheHandler) + From af7983be43d34685d18a974c9df673880a32f587 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 27 Mar 2025 08:12:47 +0530 Subject: [PATCH 5/7] Fixed Intent --- tests/crew_test.py | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/crew_test.py b/tests/crew_test.py index 694be09f4..43cb9f6ea 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -4069,41 +4069,41 @@ def test_crew_with_knowledge_sources_works_with_copy(): def test_crew_kickoff_for_each_works_with_manager_agent_copy(): researcher = Agent( - role="Researcher", - goal="Conduct thorough research and analysis on AI and AI agents", - backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", - allow_delegation=False - ) + role="Researcher", + goal="Conduct thorough research and analysis on AI and AI agents", + backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", + allow_delegation=False + ) writer = Agent( - role="Senior Writer", - goal="Create compelling content about AI and AI agents", - backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", - allow_delegation=False - ) + role="Senior Writer", + goal="Create compelling content about AI and AI agents", + backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", + allow_delegation=False + ) # Define task task = Task( - description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", - expected_output="5 bullet points, each with a paragraph and accompanying notes.", - ) + description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", + expected_output="5 bullet points, each with a paragraph and accompanying notes.", + ) # Define manager agent manager = Agent( - role="Project Manager", - goal="Efficiently manage the crew and ensure high-quality task completion", - backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", - allow_delegation=True - ) + role="Project Manager", + goal="Efficiently manage the crew and ensure high-quality task completion", + backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", + allow_delegation=True + ) # Instantiate crew with a custom manager crew = Crew( - agents=[researcher, writer], - tasks=[task], - manager_agent=manager, - process=Process.hierarchical, - verbose=True - ) + agents=[researcher, writer], + tasks=[task], + manager_agent=manager, + process=Process.hierarchical, + verbose=True + ) crew_copy = crew.copy() assert crew_copy.manager_agent is not None From 02f790ffcbc4e0d48e67d711b67415e71e841810 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 27 Mar 2025 08:14:07 +0530 Subject: [PATCH 6/7] Fixed Intent --- tests/crew_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/crew_test.py b/tests/crew_test.py index 43cb9f6ea..402e82d9b 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -4073,20 +4073,20 @@ def test_crew_kickoff_for_each_works_with_manager_agent_copy(): goal="Conduct thorough research and analysis on AI and AI agents", backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", allow_delegation=False - ) + ) writer = Agent( role="Senior Writer", goal="Create compelling content about AI and AI agents", backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", allow_delegation=False - ) + ) # Define task task = Task( description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", expected_output="5 bullet points, each with a paragraph and accompanying notes.", - ) + ) # Define manager agent manager = Agent( @@ -4094,7 +4094,7 @@ def test_crew_kickoff_for_each_works_with_manager_agent_copy(): goal="Efficiently manage the crew and ensure high-quality task completion", backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", allow_delegation=True - ) + ) # Instantiate crew with a custom manager crew = Crew( @@ -4103,7 +4103,7 @@ def test_crew_kickoff_for_each_works_with_manager_agent_copy(): manager_agent=manager, process=Process.hierarchical, verbose=True - ) + ) crew_copy = crew.copy() assert crew_copy.manager_agent is not None From f845fac4da7ec0255f966bf5f5999e713f0535e5 Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Thu, 27 Mar 2025 15:42:11 -0300 Subject: [PATCH 7/7] Refactor event base classes (#2491) - Renamed `CrewEvent` to `BaseEvent` across the codebase for consistency - Created a `CrewBaseEvent` that automatically identifies fingerprints for DRY - Added a new `to_json()` method for serializing events --- docs/concepts/event-listener.mdx | 4 +- src/crewai/utilities/events/agent_events.py | 29 ++-- src/crewai/utilities/events/base_events.py | 18 ++- src/crewai/utilities/events/crew_events.py | 153 +++++------------- .../utilities/events/crewai_event_bus.py | 8 +- src/crewai/utilities/events/flow_events.py | 4 +- src/crewai/utilities/events/llm_events.py | 12 +- src/crewai/utilities/events/task_events.py | 38 +++-- .../utilities/events/tool_usage_events.py | 20 ++- .../serialization.py} | 26 +-- .../utilities/events/test_crewai_event_bus.py | 6 +- .../test_serialization.py} | 56 +++---- 12 files changed, 155 insertions(+), 219 deletions(-) rename src/crewai/{flow/state_utils.py => utilities/serialization.py} (76%) rename tests/{flow/test_state_utils.py => utilities/test_serialization.py} (74%) diff --git a/docs/concepts/event-listener.mdx b/docs/concepts/event-listener.mdx index 28825c8f4..d641687f0 100644 --- a/docs/concepts/event-listener.mdx +++ b/docs/concepts/event-listener.mdx @@ -13,7 +13,7 @@ CrewAI provides a powerful event system that allows you to listen for and react CrewAI uses an event bus architecture to emit events throughout the execution lifecycle. The event system is built on the following components: 1. **CrewAIEventsBus**: A singleton event bus that manages event registration and emission -2. **CrewEvent**: Base class for all events in the system +2. **BaseEvent**: Base class for all events in the system 3. **BaseEventListener**: Abstract base class for creating custom event listeners When specific actions occur in CrewAI (like a Crew starting execution, an Agent completing a task, or a tool being used), the system emits corresponding events. You can register handlers for these events to execute custom code when they occur. @@ -234,7 +234,7 @@ Each event handler receives two parameters: 1. **source**: The object that emitted the event 2. **event**: The event instance, containing event-specific data -The structure of the event object depends on the event type, but all events inherit from `CrewEvent` and include: +The structure of the event object depends on the event type, but all events inherit from `BaseEvent` and include: - **timestamp**: The time when the event was emitted - **type**: A string identifier for the event type diff --git a/src/crewai/utilities/events/agent_events.py b/src/crewai/utilities/events/agent_events.py index 3d325b41c..0bb6b4f38 100644 --- a/src/crewai/utilities/events/agent_events.py +++ b/src/crewai/utilities/events/agent_events.py @@ -4,13 +4,13 @@ from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.tools.base_tool import BaseTool from crewai.tools.structured_tool import CrewStructuredTool -from .base_events import CrewEvent +from .base_events import BaseEvent if TYPE_CHECKING: from crewai.agents.agent_builder.base_agent import BaseAgent -class AgentExecutionStartedEvent(CrewEvent): +class AgentExecutionStartedEvent(BaseEvent): """Event emitted when an agent starts executing a task""" agent: BaseAgent @@ -24,14 +24,17 @@ class AgentExecutionStartedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata -class AgentExecutionCompletedEvent(CrewEvent): +class AgentExecutionCompletedEvent(BaseEvent): """Event emitted when an agent completes executing a task""" agent: BaseAgent @@ -42,14 +45,17 @@ class AgentExecutionCompletedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata -class AgentExecutionErrorEvent(CrewEvent): +class AgentExecutionErrorEvent(BaseEvent): """Event emitted when an agent encounters an error during execution""" agent: BaseAgent @@ -60,8 +66,11 @@ class AgentExecutionErrorEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata diff --git a/src/crewai/utilities/events/base_events.py b/src/crewai/utilities/events/base_events.py index 52e600f5f..46648500b 100644 --- a/src/crewai/utilities/events/base_events.py +++ b/src/crewai/utilities/events/base_events.py @@ -3,12 +3,26 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, Field +from crewai.utilities.serialization import to_serializable -class CrewEvent(BaseModel): - """Base class for all crew events""" + +class BaseEvent(BaseModel): + """Base class for all events""" timestamp: datetime = Field(default_factory=datetime.now) type: str source_fingerprint: Optional[str] = None # UUID string of the source entity source_type: Optional[str] = None # "agent", "task", "crew" fingerprint_metadata: Optional[Dict[str, Any]] = None # Any relevant metadata + + def to_json(self, exclude: set[str] | None = None): + """ + Converts the event to a JSON-serializable dictionary. + + Args: + exclude (set[str], optional): Set of keys to exclude from the result. Defaults to None. + + Returns: + dict: A JSON-serializable dictionary. + """ + return to_serializable(self, exclude=exclude) diff --git a/src/crewai/utilities/events/crew_events.py b/src/crewai/utilities/events/crew_events.py index 4a10772a8..d73cd95d3 100644 --- a/src/crewai/utilities/events/crew_events.py +++ b/src/crewai/utilities/events/crew_events.py @@ -1,171 +1,102 @@ -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from pydantic import InstanceOf +from crewai.utilities.events.base_events import BaseEvent -from crewai.utilities.events.base_events import CrewEvent +if TYPE_CHECKING: + from crewai.crew import Crew +else: + Crew = Any -class CrewKickoffStartedEvent(CrewEvent): +class CrewBaseEvent(BaseEvent): + """Base class for crew events with fingerprint handling""" + + crew_name: Optional[str] + crew: Optional[Crew] = None + + def __init__(self, **data): + super().__init__(**data) + self.set_crew_fingerprint() + + def set_crew_fingerprint(self) -> None: + if self.crew and hasattr(self.crew, "fingerprint") and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if ( + hasattr(self.crew.fingerprint, "metadata") + and self.crew.fingerprint.metadata + ): + self.fingerprint_metadata = self.crew.fingerprint.metadata + + def to_json(self, exclude: set[str] | None = None): + if exclude is None: + exclude = set() + exclude.add("crew") + return super().to_json(exclude=exclude) + + +class CrewKickoffStartedEvent(CrewBaseEvent): """Event emitted when a crew starts execution""" - crew_name: Optional[str] inputs: Optional[Dict[str, Any]] type: str = "crew_kickoff_started" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewKickoffCompletedEvent(CrewEvent): +class CrewKickoffCompletedEvent(CrewBaseEvent): """Event emitted when a crew completes execution""" - crew_name: Optional[str] output: Any type: str = "crew_kickoff_completed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewKickoffFailedEvent(CrewEvent): +class CrewKickoffFailedEvent(CrewBaseEvent): """Event emitted when a crew fails to complete execution""" error: str - crew_name: Optional[str] type: str = "crew_kickoff_failed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTrainStartedEvent(CrewEvent): +class CrewTrainStartedEvent(CrewBaseEvent): """Event emitted when a crew starts training""" - crew_name: Optional[str] n_iterations: int filename: str inputs: Optional[Dict[str, Any]] type: str = "crew_train_started" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTrainCompletedEvent(CrewEvent): +class CrewTrainCompletedEvent(CrewBaseEvent): """Event emitted when a crew completes training""" - crew_name: Optional[str] n_iterations: int filename: str type: str = "crew_train_completed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTrainFailedEvent(CrewEvent): +class CrewTrainFailedEvent(CrewBaseEvent): """Event emitted when a crew fails to complete training""" error: str - crew_name: Optional[str] type: str = "crew_train_failed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTestStartedEvent(CrewEvent): +class CrewTestStartedEvent(CrewBaseEvent): """Event emitted when a crew starts testing""" - crew_name: Optional[str] n_iterations: int eval_llm: Optional[Union[str, Any]] inputs: Optional[Dict[str, Any]] type: str = "crew_test_started" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTestCompletedEvent(CrewEvent): +class CrewTestCompletedEvent(CrewBaseEvent): """Event emitted when a crew completes testing""" - crew_name: Optional[str] type: str = "crew_test_completed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTestFailedEvent(CrewEvent): +class CrewTestFailedEvent(CrewBaseEvent): """Event emitted when a crew fails to complete testing""" error: str - crew_name: Optional[str] type: str = "crew_test_failed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata diff --git a/src/crewai/utilities/events/crewai_event_bus.py b/src/crewai/utilities/events/crewai_event_bus.py index 5df5ee689..9cde461ca 100644 --- a/src/crewai/utilities/events/crewai_event_bus.py +++ b/src/crewai/utilities/events/crewai_event_bus.py @@ -4,10 +4,10 @@ from typing import Any, Callable, Dict, List, Type, TypeVar, cast from blinker import Signal -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent from crewai.utilities.events.event_types import EventTypes -EventT = TypeVar("EventT", bound=CrewEvent) +EventT = TypeVar("EventT", bound=BaseEvent) class CrewAIEventsBus: @@ -30,7 +30,7 @@ class CrewAIEventsBus: def _initialize(self) -> None: """Initialize the event bus internal state""" self._signal = Signal("crewai_event_bus") - self._handlers: Dict[Type[CrewEvent], List[Callable]] = {} + self._handlers: Dict[Type[BaseEvent], List[Callable]] = {} def on( self, event_type: Type[EventT] @@ -59,7 +59,7 @@ class CrewAIEventsBus: return decorator - def emit(self, source: Any, event: CrewEvent) -> None: + def emit(self, source: Any, event: BaseEvent) -> None: """ Emit an event to all registered handlers diff --git a/src/crewai/utilities/events/flow_events.py b/src/crewai/utilities/events/flow_events.py index 8800b301b..7f48215e9 100644 --- a/src/crewai/utilities/events/flow_events.py +++ b/src/crewai/utilities/events/flow_events.py @@ -2,10 +2,10 @@ from typing import Any, Dict, Optional, Union from pydantic import BaseModel, ConfigDict -from .base_events import CrewEvent +from .base_events import BaseEvent -class FlowEvent(CrewEvent): +class FlowEvent(BaseEvent): """Base class for all flow events""" type: str diff --git a/src/crewai/utilities/events/llm_events.py b/src/crewai/utilities/events/llm_events.py index b92072340..07a17a48b 100644 --- a/src/crewai/utilities/events/llm_events.py +++ b/src/crewai/utilities/events/llm_events.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Union -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent class LLMCallType(Enum): @@ -11,9 +11,9 @@ class LLMCallType(Enum): LLM_CALL = "llm_call" -class LLMCallStartedEvent(CrewEvent): +class LLMCallStartedEvent(BaseEvent): """Event emitted when a LLM call starts - + Attributes: messages: Content can be either a string or a list of dictionaries that support multimodal content (text, images, etc.) @@ -26,7 +26,7 @@ class LLMCallStartedEvent(CrewEvent): available_functions: Optional[Dict[str, Any]] = None -class LLMCallCompletedEvent(CrewEvent): +class LLMCallCompletedEvent(BaseEvent): """Event emitted when a LLM call completes""" type: str = "llm_call_completed" @@ -34,14 +34,14 @@ class LLMCallCompletedEvent(CrewEvent): call_type: LLMCallType -class LLMCallFailedEvent(CrewEvent): +class LLMCallFailedEvent(BaseEvent): """Event emitted when a LLM call fails""" error: str type: str = "llm_call_failed" -class LLMStreamChunkEvent(CrewEvent): +class LLMStreamChunkEvent(BaseEvent): """Event emitted when a streaming chunk is received""" type: str = "llm_stream_chunk" diff --git a/src/crewai/utilities/events/task_events.py b/src/crewai/utilities/events/task_events.py index 7c7bb8964..1bf5baf8c 100644 --- a/src/crewai/utilities/events/task_events.py +++ b/src/crewai/utilities/events/task_events.py @@ -1,10 +1,10 @@ from typing import Any, Optional from crewai.tasks.task_output import TaskOutput -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent -class TaskStartedEvent(CrewEvent): +class TaskStartedEvent(BaseEvent): """Event emitted when a task starts""" type: str = "task_started" @@ -14,14 +14,17 @@ class TaskStartedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata -class TaskCompletedEvent(CrewEvent): +class TaskCompletedEvent(BaseEvent): """Event emitted when a task completes""" output: TaskOutput @@ -31,14 +34,17 @@ class TaskCompletedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata -class TaskFailedEvent(CrewEvent): +class TaskFailedEvent(BaseEvent): """Event emitted when a task fails""" error: str @@ -48,14 +54,17 @@ class TaskFailedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata -class TaskEvaluationEvent(CrewEvent): +class TaskEvaluationEvent(BaseEvent): """Event emitted when a task evaluation is completed""" type: str = "task_evaluation" @@ -65,8 +74,11 @@ class TaskEvaluationEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata diff --git a/src/crewai/utilities/events/tool_usage_events.py b/src/crewai/utilities/events/tool_usage_events.py index d4202f563..8ab22f667 100644 --- a/src/crewai/utilities/events/tool_usage_events.py +++ b/src/crewai/utilities/events/tool_usage_events.py @@ -1,10 +1,10 @@ from datetime import datetime from typing import Any, Callable, Dict, Optional -from .base_events import CrewEvent +from .base_events import BaseEvent -class ToolUsageEvent(CrewEvent): +class ToolUsageEvent(BaseEvent): """Base event for tool usage tracking""" agent_key: str @@ -21,10 +21,13 @@ class ToolUsageEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if self.agent and hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if self.agent and hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata @@ -65,7 +68,7 @@ class ToolSelectionErrorEvent(ToolUsageEvent): type: str = "tool_selection_error" -class ToolExecutionErrorEvent(CrewEvent): +class ToolExecutionErrorEvent(BaseEvent): """Event emitted when a tool execution encounters an error""" error: Any @@ -78,8 +81,11 @@ class ToolExecutionErrorEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if self.agent and hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if self.agent and hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata diff --git a/src/crewai/flow/state_utils.py b/src/crewai/utilities/serialization.py similarity index 76% rename from src/crewai/flow/state_utils.py rename to src/crewai/utilities/serialization.py index 533bc5e00..c3c0c3d47 100644 --- a/src/crewai/flow/state_utils.py +++ b/src/crewai/utilities/serialization.py @@ -5,35 +5,17 @@ from typing import Any, Dict, List, Union from pydantic import BaseModel -from crewai.flow import Flow - SerializablePrimitive = Union[str, int, float, bool, None] Serializable = Union[ SerializablePrimitive, List["Serializable"], Dict[str, "Serializable"] ] -def export_state(flow: Flow) -> dict[str, Serializable]: - """Exports the Flow's internal state as JSON-compatible data structures. - - Performs a one-way transformation of a Flow's state into basic Python types - that can be safely serialized to JSON. To prevent infinite recursion with - circular references, the conversion is limited to a depth of 5 levels. - - Args: - flow: The Flow object whose state needs to be exported - - Returns: - dict[str, Any]: The transformed state using JSON-compatible Python - types. - """ - result = to_serializable(flow._state) - assert isinstance(result, dict) - return result - - def to_serializable( - obj: Any, exclude: set[str] | None = None, max_depth: int = 5, _current_depth: int = 0 + obj: Any, + exclude: set[str] | None = None, + max_depth: int = 5, + _current_depth: int = 0, ) -> Serializable: """Converts a Python object into a JSON-compatible representation. diff --git a/tests/utilities/events/test_crewai_event_bus.py b/tests/utilities/events/test_crewai_event_bus.py index 0dd8c8b34..315fbe138 100644 --- a/tests/utilities/events/test_crewai_event_bus.py +++ b/tests/utilities/events/test_crewai_event_bus.py @@ -1,10 +1,10 @@ from unittest.mock import Mock -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent from crewai.utilities.events.crewai_event_bus import crewai_event_bus -class TestEvent(CrewEvent): +class TestEvent(BaseEvent): pass @@ -24,7 +24,7 @@ def test_specific_event_handler(): def test_wildcard_event_handler(): mock_handler = Mock() - @crewai_event_bus.on(CrewEvent) + @crewai_event_bus.on(BaseEvent) def handler(source, event): mock_handler(source, event) diff --git a/tests/flow/test_state_utils.py b/tests/utilities/test_serialization.py similarity index 74% rename from tests/flow/test_state_utils.py rename to tests/utilities/test_serialization.py index 48564f297..b1e042639 100644 --- a/tests/flow/test_state_utils.py +++ b/tests/utilities/test_serialization.py @@ -5,8 +5,7 @@ from unittest.mock import Mock import pytest from pydantic import BaseModel -from crewai.flow import Flow -from crewai.flow.state_utils import export_state, to_serializable, to_string +from crewai.utilities.serialization import to_serializable, to_string class Address(BaseModel): @@ -23,16 +22,6 @@ class Person(BaseModel): skills: List[str] -@pytest.fixture -def mock_flow(): - def create_flow(state): - flow = Mock(spec=Flow) - flow._state = state - return flow - - return create_flow - - @pytest.mark.parametrize( "test_input,expected", [ @@ -47,9 +36,8 @@ def mock_flow(): ({"nested": [1, [2, 3], {4, 5}]}, {"nested": [1, [2, 3], [4, 5]]}), ], ) -def test_basic_serialization(mock_flow, test_input, expected): - flow = mock_flow(test_input) - result = export_state(flow) +def test_basic_serialization(test_input, expected): + result = to_serializable(test_input) assert result == expected @@ -60,9 +48,8 @@ def test_basic_serialization(mock_flow, test_input, expected): (datetime(2024, 1, 1, 12, 30), "2024-01-01T12:30:00"), ], ) -def test_temporal_serialization(mock_flow, input_date, expected): - flow = mock_flow({"date": input_date}) - result = export_state(flow) +def test_temporal_serialization(input_date, expected): + result = to_serializable({"date": input_date}) assert result["date"] == expected @@ -75,9 +62,8 @@ def test_temporal_serialization(mock_flow, input_date, expected): ("normal", "value", str), ], ) -def test_dictionary_key_serialization(mock_flow, key, value, expected_key_type): - flow = mock_flow({key: value}) - result = export_state(flow) +def test_dictionary_key_serialization(key, value, expected_key_type): + result = to_serializable({key: value}) assert len(result) == 1 result_key = next(iter(result.keys())) assert isinstance(result_key, expected_key_type) @@ -91,14 +77,13 @@ def test_dictionary_key_serialization(mock_flow, key, value, expected_key_type): (str.upper, "upper"), ], ) -def test_callable_serialization(mock_flow, callable_obj, expected_in_result): - flow = mock_flow({"func": callable_obj}) - result = export_state(flow) +def test_callable_serialization(callable_obj, expected_in_result): + result = to_serializable({"func": callable_obj}) assert isinstance(result["func"], str) assert expected_in_result in result["func"].lower() -def test_pydantic_model_serialization(mock_flow): +def test_pydantic_model_serialization(): address = Address(street="123 Main St", city="Tech City", country="Pythonia") person = Person( @@ -109,23 +94,21 @@ def test_pydantic_model_serialization(mock_flow): skills=["Python", "Testing"], ) - flow = mock_flow( - { - "single_model": address, - "nested_model": person, - "model_list": [address, address], - "model_dict": {"home": address}, - } - ) + data = { + "single_model": address, + "nested_model": person, + "model_list": [address, address], + "model_dict": {"home": address}, + } - result = export_state(flow) + result = to_serializable(data) assert ( to_string(result) == '{"single_model": {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}, "nested_model": {"name": "John Doe", "age": 30, "address": {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}, "birthday": "1994-01-01", "skills": ["Python", "Testing"]}, "model_list": [{"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}, {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}], "model_dict": {"home": {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}}}' ) -def test_depth_limit(mock_flow): +def test_depth_limit(): """Test max depth handling with a deeply nested structure""" def create_nested(depth): @@ -134,8 +117,7 @@ def test_depth_limit(mock_flow): return {"next": create_nested(depth - 1)} deep_structure = create_nested(10) - flow = mock_flow(deep_structure) - result = export_state(flow) + result = to_serializable(deep_structure) assert result == { "next": {