mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-05 22:11:49 +00:00
Compare commits
4 Commits
docs/agent
...
luzk/tool-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c666a646f3 | ||
|
|
e819cd5f2b | ||
|
|
319a20c028 | ||
|
|
8320178697 |
@@ -376,6 +376,27 @@ def handle_internet_search(self) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
### Naming handlers
|
||||
|
||||
The string in `@listen("…")` is a **router route label** (an event name), not the Python method name. Route labels and method completion events share one trigger namespace, so naming a handler the same as its route causes the handler to re-trigger itself in a loop.
|
||||
|
||||
Use a different method name — the docs examples use a `handle_*` prefix:
|
||||
|
||||
```python
|
||||
@listen("create_video")
|
||||
def handle_create_video(self) -> str:
|
||||
"""User wants a new video."""
|
||||
...
|
||||
```
|
||||
|
||||
Do **not** mirror the route label on the method:
|
||||
|
||||
```python
|
||||
@listen("create_video")
|
||||
def create_video(self) -> str: # rejected at flow instantiation
|
||||
...
|
||||
```
|
||||
|
||||
…and the router LLM sees:
|
||||
|
||||
```
|
||||
|
||||
@@ -671,7 +671,7 @@ def handle_create_video(self):
|
||||
...
|
||||
```
|
||||
|
||||
If validation were bypassed, matching names would also cause the handler to re-trigger itself in a loop at runtime. This applies to all flows; it is especially common in **conversational flows** (`conversational = True`), where `@listen("...")` is a router intent name.
|
||||
If validation were bypassed, matching names would also cause the handler to re-trigger itself in a loop at runtime. This applies to all flows. It is especially common in **conversational flows** (`conversational = True`), where `@listen("...")` is a router intent name — do not name the handler after the route it serves.
|
||||
|
||||
### Structured State
|
||||
```python
|
||||
|
||||
@@ -39,8 +39,8 @@ Pick the simplest action that does the job.
|
||||
- `state` is the initial shared data shape. Action results do not automatically merge into `state`.
|
||||
- Read method results with `outputs.method_name` after that method can run.
|
||||
- `listen` targets a method name or a router-emitted event name.
|
||||
- Methods must not listen to their own method name.
|
||||
- Method names and emitted event names share one namespace. Avoid reusing the same string for both unless the user explicitly wants that.
|
||||
- Methods must not listen to their own method name — including when the `listen` value is a route label that matches the method name (e.g. `listen: create_video` on method `create_video`).
|
||||
- Method names and emitted event names share one namespace. Do not reuse the same string for a method's `listen` target and its method name.
|
||||
- Use `router: true` plus `emit` when one method chooses between named branches.
|
||||
- A router action must return exactly one emitted event string. It must not return JSON, a list, or an explanation.
|
||||
- Use `start: true` for the single entrypoint.
|
||||
@@ -107,8 +107,8 @@ Dynamic value rules:
|
||||
- Do not make `do` a list.
|
||||
- Do not use CEL `+` to build text in action mappings. Keep the text literal and insert each dynamic value with `${...}`.
|
||||
- Do not reference `outputs.some_method` before `some_method` can run.
|
||||
- Do not set a method's `listen` to its own method name.
|
||||
- Do not use the same string for an emitted event and a method name unless the user asks for it.
|
||||
- Do not set a method's `listen` to its own method name (including matching route labels such as `listen: create_video` on method `create_video`).
|
||||
- Do not use the same string for a method's `listen` target and its method name.
|
||||
- Do not use `emit` without `router: true`.
|
||||
- Do not rely on crew action-level `inputs` alone to ground agent behavior. Inputs that do not match placeholders are effectively unused by the prompt.
|
||||
- Do not ask agents to infer missing facts when accuracy matters. Tell them to mark missing dates, amounts, offers, logs, or constraints as unknown.
|
||||
|
||||
@@ -17,6 +17,7 @@ from crewai_tools.tools.crewai_platform_tools.misc import (
|
||||
|
||||
|
||||
class CrewAIPlatformActionTool(BaseTool):
|
||||
app: str = Field(description="The integration slug for this action")
|
||||
action_name: str = Field(default="", description="The name of the action")
|
||||
action_schema: dict[str, Any] = Field(
|
||||
default_factory=dict, description="The schema of the action"
|
||||
@@ -25,6 +26,7 @@ class CrewAIPlatformActionTool(BaseTool):
|
||||
def __init__(
|
||||
self,
|
||||
description: str,
|
||||
app: str,
|
||||
action_name: str,
|
||||
action_schema: dict[str, Any],
|
||||
):
|
||||
@@ -46,6 +48,7 @@ class CrewAIPlatformActionTool(BaseTool):
|
||||
name=action_name.lower().replace(" ", "_"),
|
||||
description=description,
|
||||
args_schema=args_schema,
|
||||
app=app,
|
||||
)
|
||||
self.action_name = action_name
|
||||
self.action_schema = action_schema
|
||||
|
||||
@@ -89,6 +89,7 @@ class CrewaiPlatformToolBuilder:
|
||||
|
||||
tool = CrewAIPlatformActionTool(
|
||||
description=description,
|
||||
app=function_details["app"],
|
||||
action_name=action_name,
|
||||
action_schema=action_schema,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ class TestCrewAIPlatformActionToolVerify:
|
||||
def create_test_tool(self):
|
||||
return CrewAIPlatformActionTool(
|
||||
description="Test action tool",
|
||||
app="test_app",
|
||||
action_name="test_action",
|
||||
action_schema=self.action_schema
|
||||
)
|
||||
|
||||
@@ -171,6 +171,10 @@ class TestCrewaiPlatformToolBuilder(unittest.TestCase):
|
||||
tool_names = [tool.action_name for tool in tools]
|
||||
assert "create_issue" in tool_names
|
||||
assert "send_message" in tool_names
|
||||
assert {tool.action_name: tool.app for tool in tools} == {
|
||||
"create_issue": "github",
|
||||
"send_message": "slack",
|
||||
}
|
||||
|
||||
github_tool = next((t for t in tools if t.action_name == "create_issue"), None)
|
||||
slack_tool = next((t for t in tools if t.action_name == "send_message"), None)
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import logging
|
||||
from typing import Any, ParamSpec, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
from crewai.flow.flow_definition import (
|
||||
@@ -432,6 +432,20 @@ def _iter_flow_methods(flow_class: type) -> dict[str, Any]:
|
||||
return methods
|
||||
|
||||
|
||||
def _flow_definition_validation_error(
|
||||
flow_class: type, exc: ValidationError
|
||||
) -> ValueError:
|
||||
errors = exc.errors()
|
||||
if errors:
|
||||
detail = errors[0].get("msg", str(exc))
|
||||
if isinstance(detail, str) and detail.startswith("Value error, "):
|
||||
detail = detail.removeprefix("Value error, ")
|
||||
else:
|
||||
detail = str(exc)
|
||||
class_name = getattr(flow_class, "__name__", "Flow")
|
||||
return ValueError(f"Invalid flow definition for {class_name}: {detail}")
|
||||
|
||||
|
||||
def _build_flow_definition_from_class(
|
||||
flow_class: type,
|
||||
namespace: dict[str, Any] | None = None,
|
||||
@@ -455,15 +469,18 @@ def _build_flow_definition_from_class(
|
||||
if docstring:
|
||||
description = docstring.strip()
|
||||
|
||||
definition = FlowDefinition(
|
||||
name=getattr(flow_class, "__name__", "Flow"),
|
||||
description=description,
|
||||
state=_build_state_definition(flow_class),
|
||||
config=_build_config_definition(flow_class),
|
||||
persist=_build_persistence_definition(flow_class),
|
||||
conversational=_build_conversational_definition(flow_class),
|
||||
methods=methods,
|
||||
)
|
||||
try:
|
||||
definition = FlowDefinition(
|
||||
name=getattr(flow_class, "__name__", "Flow"),
|
||||
description=description,
|
||||
state=_build_state_definition(flow_class),
|
||||
config=_build_config_definition(flow_class),
|
||||
persist=_build_persistence_definition(flow_class),
|
||||
conversational=_build_conversational_definition(flow_class),
|
||||
methods=methods,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise _flow_definition_validation_error(flow_class, exc) from exc
|
||||
log_flow_definition_issues(definition)
|
||||
return definition
|
||||
|
||||
|
||||
@@ -775,7 +775,11 @@ class FlowDefinition(BaseModel):
|
||||
for method_name, method in self.methods.items():
|
||||
if _condition_references(method.listen, method_name):
|
||||
raise ValueError(
|
||||
f"methods.{method_name}.listen must not reference itself"
|
||||
_self_listen_error(
|
||||
method_name=method_name,
|
||||
listen=method.listen,
|
||||
definition=self,
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -888,6 +892,39 @@ def _condition_references(condition: FlowDefinitionCondition | None, name: str)
|
||||
)
|
||||
|
||||
|
||||
def _format_listen_condition(condition: FlowDefinitionCondition | None) -> str:
|
||||
if condition is None:
|
||||
return "None"
|
||||
return repr(condition)
|
||||
|
||||
|
||||
def _self_listen_error(
|
||||
*,
|
||||
method_name: str,
|
||||
listen: FlowDefinitionCondition | None,
|
||||
definition: FlowDefinition,
|
||||
) -> str:
|
||||
path = f"methods.{method_name}.listen"
|
||||
listen_display = _format_listen_condition(listen)
|
||||
conversational = (
|
||||
definition.conversational is not None and definition.conversational.enabled
|
||||
)
|
||||
if conversational:
|
||||
return (
|
||||
f"{path} listen condition {listen_display} matches the handler name "
|
||||
f"{method_name!r}. In conversational flows, @listen labels are router "
|
||||
"route names — they share the same trigger namespace as method completion "
|
||||
"events, so this handler would re-run in a loop. Rename the handler "
|
||||
f"(for example, handle_{method_name}) or use a different route label."
|
||||
)
|
||||
|
||||
return (
|
||||
f"{path} listen condition {listen_display} references the handler name "
|
||||
f"{method_name!r}. A listener triggered by its own completion creates an "
|
||||
"infinite loop. Listen to a different method or event, or rename the handler."
|
||||
)
|
||||
|
||||
|
||||
def _validate_action_cel(
|
||||
action: FlowActionDefinition,
|
||||
*,
|
||||
|
||||
@@ -2158,7 +2158,7 @@ def test_self_listening_method_is_rejected():
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ValueError, match="methods.process.listen"):
|
||||
with pytest.raises(ValueError, match="Invalid flow definition for SelfListenFlow"):
|
||||
SelfListenFlow.flow_definition()
|
||||
|
||||
|
||||
@@ -2176,7 +2176,7 @@ def test_or_condition_self_listen_is_rejected():
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(ValueError, match="methods.process.listen"):
|
||||
with pytest.raises(ValueError, match="Invalid flow definition for OrSelfListenFlow"):
|
||||
OrSelfListenFlow.flow_definition()
|
||||
|
||||
|
||||
@@ -2190,7 +2190,7 @@ def test_router_self_listening_method_is_rejected():
|
||||
def route(self):
|
||||
return "done"
|
||||
|
||||
with pytest.raises(ValueError, match="methods.route.listen"):
|
||||
with pytest.raises(ValueError, match="Invalid flow definition for RouterSelfListenFlow"):
|
||||
RouterSelfListenFlow.flow_definition()
|
||||
|
||||
|
||||
|
||||
@@ -1231,7 +1231,7 @@ def test_static_string_listener_is_allowed_by_contract():
|
||||
@pytest.mark.parametrize("listen", ["publish", {"or": ["publish", "revise"]}])
|
||||
@pytest.mark.parametrize("router_enabled", [False, True])
|
||||
def test_flow_definition_rejects_method_self_listen(listen, router_enabled):
|
||||
with pytest.raises(ValueError, match="methods.publish.listen"):
|
||||
with pytest.raises(ValueError, match="listen condition"):
|
||||
flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
@@ -1252,6 +1252,49 @@ def test_flow_definition_rejects_method_self_listen(listen, router_enabled):
|
||||
)
|
||||
|
||||
|
||||
def test_flow_definition_rejects_conversational_route_handler_name_collision():
|
||||
with pytest.raises(ValueError, match=r"listen condition 'create_video'"):
|
||||
flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "VideoFlow",
|
||||
"conversational": {
|
||||
"enabled": True,
|
||||
"router": {
|
||||
"route_descriptions": {
|
||||
"create_video": "User wants a new video.",
|
||||
},
|
||||
},
|
||||
},
|
||||
"methods": {
|
||||
"begin": {
|
||||
"do": {"ref": "loaded_flows:VideoFlow.begin"},
|
||||
"start": True,
|
||||
},
|
||||
"create_video": {
|
||||
"do": {"ref": "loaded_flows:VideoFlow.create_video"},
|
||||
"listen": "create_video",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_build_flow_definition_wraps_validation_error_with_class_name():
|
||||
class VideoFlow(Flow):
|
||||
conversational = True
|
||||
|
||||
@listen("create_video")
|
||||
def create_video(self):
|
||||
return "made a video"
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid flow definition for VideoFlow"):
|
||||
VideoFlow.flow_definition()
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid flow definition for VideoFlow"):
|
||||
VideoFlow()
|
||||
|
||||
|
||||
def test_start_false_not_classified_as_start_method():
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
|
||||
@@ -1698,6 +1698,7 @@ class TestPlatformActionTool:
|
||||
|
||||
return mod.CrewAIPlatformActionTool(
|
||||
description="Send a Slack message",
|
||||
app="slack",
|
||||
action_name="slackbot_send_message",
|
||||
action_schema={
|
||||
"function": {
|
||||
|
||||
Reference in New Issue
Block a user