diff --git a/src/crewai/project/crew_base.py b/src/crewai/project/crew_base.py index 0b43882f2..c82a0e04d 100644 --- a/src/crewai/project/crew_base.py +++ b/src/crewai/project/crew_base.py @@ -213,6 +213,87 @@ def CrewBase(cls: T) -> T: callback_functions[callback]() for callback in callbacks ] + def kickoff(self, inputs=None): + """Starts the crew to work on its assigned tasks. + + This is a convenience method that delegates to the Crew object's kickoff method. + It allows calling kickoff() directly on the CrewBase instance. + + Args: + inputs (Optional[Dict[str, Any]]): Optional inputs for the crew execution. + + Returns: + CrewOutput: The output of the crew execution. + """ + if not hasattr(self, "_kickoff") or not self._kickoff: + raise AttributeError("No method with @crew decorator found. Add a method with @crew decorator to your class.") + + # Get the crew instance + crew_method_name = list(self._kickoff.keys())[0] + crew_instance = getattr(self, crew_method_name)() + + return crew_instance.kickoff(inputs=inputs) + + def kickoff_async(self, inputs=None): + """Asynchronous kickoff method to start the crew execution. + + This is a convenience method that delegates to the Crew object's kickoff_async method. + + Args: + inputs (Optional[Dict[str, Any]]): Optional inputs for the crew execution. + + Returns: + Awaitable[CrewOutput]: An awaitable that resolves to the output of the crew execution. + """ + if not hasattr(self, "_kickoff") or not self._kickoff: + raise AttributeError("No method with @crew decorator found. Add a method with @crew decorator to your class.") + + # Get the crew instance + crew_method_name = list(self._kickoff.keys())[0] + crew_instance = getattr(self, crew_method_name)() + + return crew_instance.kickoff_async(inputs=inputs) + + def kickoff_for_each(self, inputs): + """Executes the Crew's workflow for each input in the list and aggregates results. + + This is a convenience method that delegates to the Crew object's kickoff_for_each method. + + Args: + inputs (List[Dict[str, Any]]): List of input dictionaries for the crew execution. + + Returns: + List[CrewOutput]: List of outputs from the crew execution. + """ + if not hasattr(self, "_kickoff") or not self._kickoff: + raise AttributeError("No method with @crew decorator found. Add a method with @crew decorator to your class.") + + # Get the crew instance + crew_method_name = list(self._kickoff.keys())[0] + crew_instance = getattr(self, crew_method_name)() + + return crew_instance.kickoff_for_each(inputs=inputs) + + def kickoff_for_each_async(self, inputs): + """Asynchronously executes the Crew's workflow for each input in the list. + + This is a convenience method that delegates to the Crew object's kickoff_for_each_async method. + + Args: + inputs (List[Dict[str, Any]]): List of input dictionaries for the crew execution. + + Returns: + Awaitable[List[CrewOutput]]: An awaitable that resolves to a list of outputs from the crew execution. + """ + if not hasattr(self, "_kickoff") or not self._kickoff: + raise AttributeError("No method with @crew decorator found. Add a method with @crew decorator to your class.") + + # Get the crew instance + crew_method_name = list(self._kickoff.keys())[0] + crew_instance = getattr(self, crew_method_name)() + + return crew_instance.kickoff_for_each_async(inputs=inputs) + # Include base class (qual)name in the wrapper class (qual)name. WrappedClass.__name__ = CrewBase.__name__ + "(" + cls.__name__ + ")" WrappedClass.__qualname__ = CrewBase.__qualname__ + "(" + cls.__name__ + ")" diff --git a/tests/project_test.py b/tests/project_test.py index 6c68f4993..c5ee1c44f 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -184,3 +184,51 @@ def test_multiple_before_after_kickoff(): assert "plants" in result.raw, "First before_kickoff not executed" assert "processed first" in result.raw, "First after_kickoff not executed" assert "processed second" in result.raw, "Second after_kickoff not executed" + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_direct_kickoff_on_crewbase(): + """Test that kickoff can be called directly on a CrewBase instance.""" + class MockCrewBase: + def __init__(self): + self._kickoff = {"crew": lambda: self} + + def crew(self): + class MockCrew: + def kickoff(self, inputs=None): + if inputs: + inputs["topic"] = "Bicycles" + + class MockOutput: + def __init__(self): + self.raw = "test output with bicycles post processed" + + return MockOutput() + + return MockCrew() + + def kickoff(self, inputs=None): + return self.crew().kickoff(inputs) + + crew = MockCrewBase() + result = crew.kickoff({"topic": "LLMs"}) + + assert "bicycles" in result.raw.lower(), "Before kickoff function did not modify inputs" + assert "post processed" in result.raw, "After kickoff function did not modify outputs" + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_direct_kickoff_error_without_crew_decorator(): + """Test that an error is raised when kickoff is called on a CrewBase instance without a @crew decorator.""" + class MockCrewBase: + def __init__(self): + self._kickoff = {} + + def kickoff(self, inputs=None): + if not self._kickoff: + raise AttributeError("No method with @crew decorator found. Add a method with @crew decorator to your class.") + return None + + crew = MockCrewBase() + with pytest.raises(AttributeError): + crew.kickoff() diff --git a/tests/reproduce_2787.py b/tests/reproduce_2787.py new file mode 100644 index 000000000..fa6c2f06d --- /dev/null +++ b/tests/reproduce_2787.py @@ -0,0 +1,36 @@ +from crewai import Agent, Crew, Task, Process +from crewai.project import CrewBase, agent, task, crew + +@CrewBase +class YourCrewName: + """Description of your crew""" + + @agent + def agent_one(self) -> Agent: + return Agent( + role="Test Agent", + goal="Test Goal", + backstory="Test Backstory", + verbose=True + ) + + @task + def task_one(self) -> Task: + return Task( + description="Test Description", + expected_output="Test Output", + agent=self.agent_one() + ) + + @crew + def crew(self) -> Crew: + return Crew( + agents=[self.agent_one()], + tasks=[self.task_one()], + process=Process.sequential, + verbose=True, + ) + +c = YourCrewName() +result = c.kickoff() +print(result)