diff --git a/lib/crewai/src/crewai/flow/flow_definition.py b/lib/crewai/src/crewai/flow/flow_definition.py index ae8be4ec5..b8a32d68e 100644 --- a/lib/crewai/src/crewai/flow/flow_definition.py +++ b/lib/crewai/src/crewai/flow/flow_definition.py @@ -28,6 +28,7 @@ from crewai.flow.conversational_definition import ( FlowConversationalDefinition, FlowConversationalRouterDefinition, ) +from crewai.project.crew_definition import CrewDefinition logger = logging.getLogger(__name__) @@ -41,6 +42,7 @@ __all__ = [ "FlowConfigDefinition", "FlowConversationalDefinition", "FlowConversationalRouterDefinition", + "FlowCrewActionDefinition", "FlowDefinition", "FlowDefinitionCondition", "FlowDefinitionDiagnostic", @@ -176,6 +178,15 @@ class FlowToolActionDefinition(BaseModel): with_: dict[str, Any] | None = Field(default=None, alias="with") +class FlowCrewActionDefinition(BaseModel): + """A Flow method action that builds and kicks off a CrewAI crew.""" + + model_config = ConfigDict(populate_by_name=True, extra="forbid") + + call: TypingLiteral["crew"] + with_: CrewDefinition = Field(alias="with") + + class FlowExpressionActionDefinition(BaseModel): """A Flow method action that evaluates a CEL expression.""" @@ -186,7 +197,10 @@ class FlowExpressionActionDefinition(BaseModel): FlowInnerActionDefinition = ( - FlowCodeActionDefinition | FlowToolActionDefinition | FlowExpressionActionDefinition + FlowCodeActionDefinition + | FlowToolActionDefinition + | FlowCrewActionDefinition + | FlowExpressionActionDefinition ) @@ -236,6 +250,7 @@ class FlowEachActionDefinition(BaseModel): FlowActionDefinition = ( FlowCodeActionDefinition | FlowToolActionDefinition + | FlowCrewActionDefinition | FlowExpressionActionDefinition | FlowEachActionDefinition ) diff --git a/lib/crewai/src/crewai/flow/runtime/_actions.py b/lib/crewai/src/crewai/flow/runtime/_actions.py index 480fbb982..97333e209 100644 --- a/lib/crewai/src/crewai/flow/runtime/_actions.py +++ b/lib/crewai/src/crewai/flow/runtime/_actions.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Protocol, cast from crewai.flow.flow_definition import ( FlowActionDefinition, FlowCodeActionDefinition, + FlowCrewActionDefinition, FlowEachActionDefinition, FlowEachInnerActionDefinition, FlowExpressionActionDefinition, @@ -104,6 +105,25 @@ class ToolAction: ) from e +class CrewAction: + definition_type = FlowCrewActionDefinition + + def __init__(self, flow: Flow[Any], definition: FlowCrewActionDefinition) -> None: + self.flow = flow + self.definition = definition + + async def run(self, *_args: Any, **kwargs: Any) -> Any: + from crewai.project.crew_loader import load_crew_from_definition + + local_context = _pop_local_context(kwargs) + crew_definition = self.definition.with_ + inputs = render_with_block( + self.flow, crew_definition.inputs, local_context=local_context + ) + crew, _ = load_crew_from_definition(crew_definition, source="crew action") + return await crew.kickoff_async(inputs=inputs) + + class ExpressionAction: definition_type = FlowExpressionActionDefinition @@ -177,6 +197,7 @@ _ACTION_TYPES: tuple[_ActionType, ...] = ( EachAction, CodeAction, ToolAction, + CrewAction, ExpressionAction, ) diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index e2b3a7ad4..946ebd336 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -41,6 +41,7 @@ def test_flow_public_exports_are_explicit(): "FlowConfigDefinition", "FlowConversationalDefinition", "FlowConversationalRouterDefinition", + "FlowCrewActionDefinition", "FlowDefinition", "FlowDefinitionCondition", "FlowDefinitionDiagnostic", diff --git a/lib/crewai/tests/test_flow_from_definition.py b/lib/crewai/tests/test_flow_from_definition.py index aac114c4d..69bb96816 100644 --- a/lib/crewai/tests/test_flow_from_definition.py +++ b/lib/crewai/tests/test_flow_from_definition.py @@ -765,6 +765,252 @@ methods: ) +def test_crew_action_runs_inline_yaml_definition(monkeypatch: pytest.MonkeyPatch): + from crewai import Crew + + async def fake_kickoff_async( + self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any + ) -> dict[str, Any]: + return { + "crew": self.name, + "agents": [agent.role for agent in self.agents], + "tasks": [task.description for task in self.tasks], + "inputs": inputs, + } + + monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async) + + yaml_str = """ +schema: crewai.flow/v1 +name: CrewFlow +methods: + research: + do: + call: crew + with: + name: inline_research + agents: + researcher: + role: Researcher + goal: Research {topic} + backstory: Knows things. + tasks: + - name: research_task + description: Research {topic} + expected_output: Findings about {topic} + agent: researcher + inputs: + topic: "${state.topic}" + start: true +""" + + flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str)) + + assert flow.kickoff(inputs={"topic": "AI"}) == { + "crew": "inline_research", + "agents": ["Researcher"], + "tasks": ["Research {topic}"], + "inputs": {"topic": "AI"}, + } + + +def test_crew_action_round_trips_with_inline_definition(): + definition = FlowDefinition.from_dict( + { + "schema": "crewai.flow/v1", + "name": "CrewFlow", + "methods": { + "research": { + "start": True, + "do": { + "call": "crew", + "with": { + "name": "inline_research", + "agents": { + "researcher": { + "role": "Researcher", + "goal": "Research {topic}", + "backstory": "Knows things.", + } + }, + "tasks": [ + { + "name": "research_task", + "description": "Research {topic}", + "expected_output": "Findings about {topic}", + "agent": "researcher", + } + ], + "inputs": {"topic": "${state.topic}"}, + }, + }, + } + }, + } + ) + + assert definition.to_dict()["methods"]["research"]["do"]["call"] == "crew" + assert ( + definition.to_dict()["methods"]["research"]["do"]["with"]["agents"][ + "researcher" + ]["role"] + == "Researcher" + ) + + +def test_crew_action_normalizes_named_agent_list_definition(): + definition = FlowDefinition.from_dict( + { + "schema": "crewai.flow/v1", + "name": "CrewFlow", + "methods": { + "research": { + "start": True, + "do": { + "call": "crew", + "with": { + "agents": [ + { + "name": "researcher", + "role": "Researcher", + "goal": "Research {topic}", + "backstory": "Knows things.", + } + ], + "tasks": [ + { + "description": "Research {topic}", + "expected_output": "Findings about {topic}", + "agent": "researcher", + } + ], + }, + }, + } + }, + } + ) + + assert ( + definition.to_dict()["methods"]["research"]["do"]["with"]["agents"][ + "researcher" + ]["role"] + == "Researcher" + ) + + +def test_crew_action_json_schema_describes_inline_crew_definitions(): + schema_defs = FlowDefinition.json_schema()["$defs"] + agents_schema = schema_defs["CrewDefinition"]["properties"]["agents"] + + assert set(schema_defs["CrewDefinition"]["properties"]) >= { + "agents", + "tasks", + "inputs", + } + assert {option["type"] for option in agents_schema["anyOf"]} == {"array", "object"} + assert set(schema_defs["CrewAgentDefinition"]["properties"]) >= { + "role", + "goal", + "backstory", + "settings", + } + assert set(schema_defs["CrewTaskDefinition"]["properties"]) >= { + "description", + "expected_output", + "agent", + "context", + } + + +def test_crew_action_rejects_incomplete_inline_agent_definition(): + with pytest.raises(ValidationError, match="goal"): + FlowDefinition.from_dict( + { + "schema": "crewai.flow/v1", + "name": "CrewFlow", + "methods": { + "research": { + "start": True, + "do": { + "call": "crew", + "with": { + "agents": { + "researcher": { + "role": "Researcher", + "backstory": "Knows things.", + } + }, + "tasks": [ + { + "description": "Research", + "expected_output": "Findings", + "agent": "researcher", + } + ], + }, + }, + } + }, + } + ) + + +def test_crew_action_rejects_ref(): + with pytest.raises(ValidationError, match="ref"): + FlowDefinition.from_dict( + { + "schema": "crewai.flow/v1", + "name": "CrewFlow", + "methods": { + "research": { + "start": True, + "do": { + "call": "crew", + "ref": "project.crew:build_crew", + "with": {"inputs": {"topic": "AI"}}, + }, + } + }, + } + ) + + +def test_crew_action_rejects_non_mapping_inputs_in_definition(): + with pytest.raises(ValidationError, match="crew.inputs must be a mapping"): + FlowDefinition.from_dict( + { + "schema": "crewai.flow/v1", + "name": "CrewFlow", + "methods": { + "research": { + "start": True, + "do": { + "call": "crew", + "with": { + "agents": { + "researcher": { + "role": "Researcher", + "goal": "Research", + "backstory": "Knows things.", + } + }, + "tasks": [ + { + "description": "Research", + "expected_output": "Findings", + "agent": "researcher", + } + ], + "inputs": "topic", + }, + }, + } + }, + } + ) + + def test_tool_action_reports_invalid_cel_expression(): yaml_str = f""" schema: crewai.flow/v1