mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-07 18:19:00 +00:00
feat: add support for agents to invoke Flows as tools
- Add FlowTool(BaseTool) in crewai/tools/flow_tool.py that wraps a Flow class as a callable tool (instantiates, calls kickoff, returns result) - Add create_flow_tools() helper to convert list of Flow classes to tools - Add Agent.flows parameter (list of Flow classes) - Wire _set_flow_tools() into Agent.post_init_setup to auto-convert flows into tools and merge them into the agent's tool list - Export FlowTool and create_flow_tools from crewai/__init__.py - Add comprehensive tests (14 tests covering unit + integration)
This commit is contained in:
189
lib/crewai/tests/test_flow_as_tool.py
Normal file
189
lib/crewai/tests/test_flow_as_tool.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""Tests for Flow-as-tool functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai.tools.flow_tool import FlowTool, create_flow_tools
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test Flow classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SimpleFlow(Flow):
|
||||
"""A simple flow that greets the user."""
|
||||
|
||||
@start()
|
||||
def greet(self) -> str:
|
||||
return "Hello from SimpleFlow!"
|
||||
|
||||
|
||||
class MathFlow(Flow):
|
||||
"""Performs basic math operations."""
|
||||
|
||||
@start()
|
||||
def compute(self) -> str:
|
||||
return "42"
|
||||
|
||||
|
||||
class NoDocFlow(Flow):
|
||||
@start()
|
||||
def run_it(self) -> str:
|
||||
return "no doc"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FlowTool unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFlowTool:
|
||||
def test_wrap_simple_flow(self) -> None:
|
||||
tool = FlowTool(
|
||||
name="simple_flow",
|
||||
description="A simple flow that greets the user.",
|
||||
flow_class=SimpleFlow,
|
||||
)
|
||||
assert tool.name == "simple_flow"
|
||||
assert "greets the user" in tool.description
|
||||
|
||||
def test_run_invokes_kickoff(self) -> None:
|
||||
mock_flow = MagicMock()
|
||||
mock_flow.return_value = mock_flow # __init__ returns self
|
||||
mock_flow.kickoff.return_value = "mocked result"
|
||||
|
||||
tool = FlowTool(
|
||||
name="test_flow",
|
||||
description="test",
|
||||
flow_class=mock_flow,
|
||||
)
|
||||
result = tool._run(inputs="{}")
|
||||
assert result == "mocked result"
|
||||
mock_flow.kickoff.assert_called_once()
|
||||
|
||||
def test_run_with_json_inputs(self) -> None:
|
||||
mock_flow = MagicMock()
|
||||
mock_flow.return_value = mock_flow
|
||||
mock_flow.kickoff.return_value = "result with inputs"
|
||||
|
||||
tool = FlowTool(
|
||||
name="test_flow",
|
||||
description="test",
|
||||
flow_class=mock_flow,
|
||||
)
|
||||
result = tool._run(inputs='{"key": "value"}')
|
||||
assert result == "result with inputs"
|
||||
mock_flow.kickoff.assert_called_once_with(inputs={"key": "value"})
|
||||
|
||||
def test_run_with_invalid_json_defaults_to_empty(self) -> None:
|
||||
mock_flow = MagicMock()
|
||||
mock_flow.return_value = mock_flow
|
||||
mock_flow.kickoff.return_value = "ok"
|
||||
|
||||
tool = FlowTool(
|
||||
name="test_flow",
|
||||
description="test",
|
||||
flow_class=mock_flow,
|
||||
)
|
||||
result = tool._run(inputs="not valid json")
|
||||
assert result == "ok"
|
||||
mock_flow.kickoff.assert_called_once_with(inputs=None)
|
||||
|
||||
def test_run_returns_string(self) -> None:
|
||||
mock_flow = MagicMock()
|
||||
mock_flow.return_value = mock_flow
|
||||
mock_flow.kickoff.return_value = 42
|
||||
|
||||
tool = FlowTool(
|
||||
name="test_flow",
|
||||
description="test",
|
||||
flow_class=mock_flow,
|
||||
)
|
||||
result = tool._run()
|
||||
assert result == "42"
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_flow_tools tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateFlowTools:
|
||||
def test_creates_tools_from_flow_classes(self) -> None:
|
||||
tools = create_flow_tools([SimpleFlow, MathFlow])
|
||||
assert len(tools) == 2
|
||||
names = {t.name for t in tools}
|
||||
assert "simple_flow" in names
|
||||
assert "math_flow" in names
|
||||
|
||||
def test_description_from_docstring(self) -> None:
|
||||
tools = create_flow_tools([SimpleFlow])
|
||||
assert len(tools) == 1
|
||||
assert "greets the user" in tools[0].description
|
||||
|
||||
def test_description_fallback_when_no_docstring(self) -> None:
|
||||
tools = create_flow_tools([NoDocFlow])
|
||||
assert len(tools) == 1
|
||||
assert "NoDocFlow" in tools[0].description
|
||||
|
||||
def test_empty_list_returns_empty(self) -> None:
|
||||
assert create_flow_tools([]) == []
|
||||
|
||||
def test_none_returns_empty(self) -> None:
|
||||
assert create_flow_tools(None) == []
|
||||
|
||||
def test_tools_are_base_tool_instances(self) -> None:
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
|
||||
tools = create_flow_tools([SimpleFlow])
|
||||
for tool in tools:
|
||||
assert isinstance(tool, BaseTool)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent integration tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAgentFlowIntegration:
|
||||
def test_agent_with_flows_has_flow_tools(self) -> None:
|
||||
from crewai.agent.core import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test flows",
|
||||
backstory="I test things",
|
||||
flows=[SimpleFlow, MathFlow],
|
||||
)
|
||||
tool_names = {t.name for t in (agent.tools or [])}
|
||||
assert "simple_flow" in tool_names
|
||||
assert "math_flow" in tool_names
|
||||
|
||||
def test_agent_without_flows_no_extra_tools(self) -> None:
|
||||
from crewai.agent.core import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test",
|
||||
backstory="I test things",
|
||||
)
|
||||
# Should not have any flow tools
|
||||
flow_tool_names = {
|
||||
t.name for t in (agent.tools or []) if isinstance(t, FlowTool)
|
||||
}
|
||||
assert len(flow_tool_names) == 0
|
||||
|
||||
def test_flow_tool_executes_real_flow(self) -> None:
|
||||
"""Test that a FlowTool actually runs the Flow's kickoff."""
|
||||
tools = create_flow_tools([SimpleFlow])
|
||||
tool = tools[0]
|
||||
result = tool.run(inputs="{}")
|
||||
assert "Hello from SimpleFlow" in result
|
||||
Reference in New Issue
Block a user