Compare commits

..

2 Commits

Author SHA1 Message Date
Devin AI
ea783d83c9 Address PR feedback: refactor code, add type hints, and improve test coverage
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-05-08 11:51:28 +00:00
Devin AI
ca318d2bc2 Fix #2787: Add direct kickoff methods to CrewBase instances
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-05-08 11:46:41 +00:00
5 changed files with 247 additions and 53 deletions

View File

@@ -219,11 +219,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
"""Returns the list of all outputs from executed methods."""
return self._method_outputs
def _initialize_state(self, inputs: Optional[Dict[str, Any]] = None) -> None:
"""Initialize the state of the flow."""
if inputs is None:
return
def _initialize_state(self, inputs: Dict[str, Any]) -> None:
if isinstance(self._state, BaseModel):
# Structured state
try:
@@ -249,8 +245,6 @@ class Flow(Generic[T], metaclass=FlowMeta):
self._state.update(inputs)
else:
raise TypeError("State must be a BaseModel instance or a dictionary.")
self._interpolate_inputs_in_crew(inputs)
def kickoff(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
self.event_emitter.send(
@@ -412,11 +406,6 @@ class Flow(Generic[T], metaclass=FlowMeta):
traceback.print_exc()
def _interpolate_inputs_in_crew(self, inputs: Dict[str, Any]) -> None:
"""Interpolate inputs in the crew's tasks and agents if a crew is present."""
if hasattr(self, 'crew') and self.crew:
self.crew._interpolate_inputs(inputs)
def plot(self, filename: str = "crewai_flow") -> None:
self._telemetry.flow_plotting_span(
self.__class__.__name__, list(self._methods.keys())

View File

@@ -1,6 +1,6 @@
import inspect
from pathlib import Path
from typing import Any, Callable, Dict, TypeVar, cast
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, cast
import yaml
from dotenv import load_dotenv
@@ -213,6 +213,97 @@ def CrewBase(cls: T) -> T:
callback_functions[callback]() for callback in callbacks
]
def _validate_crew_decorator(self) -> None:
"""Validates that a crew decorator exists.
Raises:
AttributeError: If no method with @crew decorator is found.
"""
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.")
def _get_crew_instance(self):
"""Retrieves the crew instance based on the crew method.
Returns:
Crew: The crew instance created by the @crew decorated method.
Raises:
AttributeError: If no method with @crew decorator is found.
"""
self._validate_crew_decorator()
crew_method_name = list(self._kickoff.keys())[0]
return getattr(self, crew_method_name)()
def kickoff(self, inputs: Optional[Dict[str, Any]] = 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 inputs for the crew execution.
Returns:
CrewOutput: The output of the crew execution.
Raises:
AttributeError: If no method with @crew decorator is found.
"""
crew_instance = self._get_crew_instance()
return crew_instance.kickoff(inputs=inputs)
def kickoff_async(self, inputs: Optional[Dict[str, Any]] = 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 inputs for the crew execution.
Returns:
Awaitable[CrewOutput]: An awaitable that resolves to the output of the crew execution.
Raises:
AttributeError: If no method with @crew decorator is found.
"""
crew_instance = self._get_crew_instance()
return crew_instance.kickoff_async(inputs=inputs)
def kickoff_for_each(self, inputs: List[Dict[str, Any]]):
"""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 of input dictionaries for the crew execution.
Returns:
List[CrewOutput]: List of outputs from the crew execution.
Raises:
AttributeError: If no method with @crew decorator is found.
"""
crew_instance = self._get_crew_instance()
return crew_instance.kickoff_for_each(inputs=inputs)
def kickoff_for_each_async(self, inputs: List[Dict[str, Any]]):
"""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 of input dictionaries for the crew execution.
Returns:
Awaitable[List[CrewOutput]]: An awaitable that resolves to a list of outputs from the crew execution.
Raises:
AttributeError: If no method with @crew decorator is found.
"""
crew_instance = self._get_crew_instance()
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__ + ")"

View File

@@ -322,43 +322,3 @@ def test_router_with_multiple_conditions():
# final_step should run after router_and
assert execution_order.index("log_final_step") > execution_order.index("router_and")
def test_flow_inputs_passed_to_tasks():
"""Test that inputs passed to Flow's kickoff method are correctly interpolated in task descriptions."""
from crewai import Agent, Crew, Task
from crewai.llm import LLM
agent = Agent(
role="Test Agent",
goal="Test Goal",
backstory="Test Backstory",
llm=LLM(model="gpt-4o-mini")
)
task = Task(
description="Process data about {topic}",
expected_output="Information about {topic}",
agent=agent
)
crew = Crew(
agents=[agent],
tasks=[task]
)
class TestFlow(Flow):
def __init__(self):
super().__init__()
self.crew = crew
@start()
def start_process(self):
pass
flow = TestFlow()
inputs = {"topic": "artificial intelligence"}
flow.kickoff(inputs=inputs)
assert task.description == "Process data about artificial intelligence"
assert task.expected_output == "Information about artificial intelligence"

View File

@@ -184,3 +184,121 @@ 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()
@pytest.mark.vcr(filter_headers=["authorization"])
@pytest.mark.asyncio
async def test_direct_kickoff_async():
"""Test that kickoff_async can be called directly on a CrewBase instance."""
class MockCrewBase:
def __init__(self):
self._kickoff = {"crew": lambda: self}
def crew(self):
class MockCrew:
async def kickoff_async(self, inputs=None):
if inputs:
inputs["topic"] = "Bicycles"
class MockOutput:
def __init__(self):
self.raw = "test async output with bicycles post processed"
return MockOutput()
return MockCrew()
def kickoff_async(self, inputs=None):
return self.crew().kickoff_async(inputs=inputs)
crew = MockCrewBase()
result = await crew.kickoff_async({"topic": "LLMs"})
assert "bicycles" in result.raw.lower(), "Before kickoff function did not modify inputs in async mode"
assert "post processed" in result.raw, "After kickoff function did not modify outputs in async mode"
@pytest.mark.vcr(filter_headers=["authorization"])
@pytest.mark.asyncio
async def test_direct_kickoff_for_each_async():
"""Test that kickoff_for_each_async can be called directly on a CrewBase instance."""
class MockCrewBase:
def __init__(self):
self._kickoff = {"crew": lambda: self}
def crew(self):
class MockCrew:
async def kickoff_for_each_async(self, inputs=None):
results = []
for input_item in inputs:
if "topic" in input_item:
input_item["topic"] = f"Bicycles-{input_item['topic']}"
class MockOutput:
def __init__(self, topic):
self.raw = f"test for_each_async output with {topic} post processed"
results.append(MockOutput(input_item.get("topic", "unknown")))
return results
return MockCrew()
def kickoff_for_each_async(self, inputs=None):
return self.crew().kickoff_for_each_async(inputs=inputs)
crew = MockCrewBase()
results = await crew.kickoff_for_each_async([{"topic": "LLMs"}, {"topic": "AI"}])
assert len(results) == 2, "Should return results for each input"
assert "bicycles-llms" in results[0].raw.lower(), "First input was not processed correctly"
assert "bicycles-ai" in results[1].raw.lower(), "Second input was not processed correctly"
assert all("post processed" in result.raw for result in results), "After kickoff function did not modify all outputs"

36
tests/reproduce_2787.py Normal file
View File

@@ -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)