mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
Remove redundant CEL text helper (#6528)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
This commit removes the redundant CEL text helper, in favor of the easier interpolation syntax.
This commit is contained in:
@@ -59,9 +59,9 @@ Use these expression forms correctly:
|
||||
- Raw CEL: use in `expr`. Do not wrap raw CEL in `${...}`.
|
||||
- Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`.
|
||||
- Use `state` for input data. Use `outputs.step_name` for a completed method result.
|
||||
- In action mapping strings, keep literal text outside `${...}` and interpolate each Flow value directly. Write `Ticket: ${state.ticket_id}`; do not assemble the string with CEL `+`.
|
||||
- If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists.
|
||||
- If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text.
|
||||
- Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`.
|
||||
|
||||
Expression examples:
|
||||
|
||||
@@ -78,12 +78,6 @@ domains: "${state.domains}"
|
||||
limit: "${state.limit}"
|
||||
```
|
||||
|
||||
Use a default for missing text:
|
||||
|
||||
```yaml
|
||||
input: "Ticket ${text(state, \"ticket.id\", \"unknown\")}"
|
||||
```
|
||||
|
||||
- Crew text: use `{name}` placeholders from crew inputs. Example: `Research {topic}`.
|
||||
- Crew inputs become prompt text only when agent or task text references matching `{name}` placeholders.
|
||||
- Passing an input that is not referenced by any `{name}` placeholder does not ground the crew. If the crew needs a field, put that placeholder in an agent `goal`, task `description`, or task `expected_output`.
|
||||
@@ -111,6 +105,7 @@ Dynamic value rules:
|
||||
- Do not use fields outside the declaration schema.
|
||||
- Do not put more than one action under a method's `do`.
|
||||
- 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.
|
||||
@@ -246,7 +241,7 @@ Shape:
|
||||
Fields:
|
||||
- `call` (required): must be `crew`. Action discriminator. Use crew to run an inline Crew definition. Example: `crew`
|
||||
- `with` (required): inline crew definition. Inline Crew definition to load and execute for this action. Example: `{"agents": {"researcher": {"backstory": "Knows the domain.", "goal": "Research {topic}", "role": "Researcher"}}, "name": "inline_research", "tasks": [{"agent": "researcher", "description": "Research {topic}", "expected_output": "Findings about {topic}", "name": "research_task"}]}`
|
||||
- `inputs` (optional): map of string to expression data | null; default `null`. Actual kickoff inputs passed to the Crew. Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`. Use `state` for input data. Use `outputs.step_name` for a completed method result. If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists. If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text. Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`. The evaluated values are available to crew agent and task interpolation as `{name}` placeholders; reference each input the crew needs in agent or task text. Example: `{"topic": "${state.topic}"}`
|
||||
- `inputs` (optional): map of string to expression data | null; default `null`. Runtime inputs passed to the Crew. Insert Flow values with `${...}` and reference each input as `{name}` in agent or task text. Example: `{"topic": "${state.topic}"}`
|
||||
|
||||
#### Crew Definition (`methods.<name>.do[call=crew].with`)
|
||||
|
||||
@@ -311,7 +306,7 @@ Fields:
|
||||
- `tools` (optional): list[string | map of string to any] | null; default `null`. Tool refs or serialized tool definitions available to this agent. String refs can use CrewAI tool names, `custom:<name>`, or fully qualified `module:Class` references. Example: `["crewai_tools:SerperDevTool", "custom:file_read"]`
|
||||
- `apps` (optional): list[string] | null; default `null`. Platform apps available to this agent. Can contain app names such as `gmail` or app/action refs such as `gmail/send_email`. Example: `["gmail", "slack/send_message"]`
|
||||
- `mcps` (optional): list[string | map of string to any] | null; default `null`. MCP server refs or serialized MCP server configs available to this agent. String refs can use HTTPS URLs, connected MCP integration slugs, or refs with a `#tool_name` suffix for specific tools. Example: `["https://api.weather.com/mcp#get_current_weather", "snowflake", "stripe#list_invoices", {"cache_tools_list": true, "headers": {"Authorization": "Bearer your_token"}, "streamable": true, "url": "https://api.example.com/mcp"}]`
|
||||
- `input` (required): string. Input passed to the individual agent kickoff outside of a crew. Use one string. Use `${...}` inside action mapping strings to read Flow data with CEL. Example value: `Ticket: ${state.ticket_id}`. Use `state` for input data. Use `outputs.step_name` for a completed method result. If a value is only one `${...}` expression, the result keeps its type. Use this for numbers, booleans, objects, and lists. If the string has other text, the final value is text. Non-text values become JSON. `null` becomes empty text. Use `text(root, "path", "default")` for values that may be missing or null. The default is optional and is `""`. When an agent needs multiple fields, write one string with labels and separators, for example `Ticket ID: ${state.ticket_id}; Message: ${state.message}`. Example: `${state.ticket.body}`
|
||||
- `input` (required): string. Agent prompt template. Insert Flow values with `${...}`, for example `Ticket: ${state.ticket_id}`. Example: `${state.ticket.body}`
|
||||
|
||||
#### LLM Definition
|
||||
|
||||
@@ -349,4 +344,3 @@ Fields:
|
||||
- Crew action-level `inputs` are the Crew kickoff inputs; use CEL-wrapped strings there for runtime values.
|
||||
- Crew agent/task interpolation uses `{name}` placeholders from evaluated crew inputs.
|
||||
- Agent `with.input` must be text. Use `${outputs.method_name.raw}` or a text field like `${outputs.method_name.json_dict.summary}`.
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ def test_create_flow_declarative_project_can_run(
|
||||
agents_md = (project_root / "AGENTS.md").read_text(encoding="utf-8")
|
||||
assert "CrewAI Flow declaration" in agents_md
|
||||
assert "schema: crewai.flow/v1" in agents_md
|
||||
assert 'text(root, "path", "default")' in agents_md
|
||||
assert "do not assemble the string with CEL `+`" in agents_md
|
||||
assert "Agent prompt template. Insert Flow values with `${...}`" in agents_md
|
||||
assert "Runtime inputs passed to the Crew" in agents_md
|
||||
assert "call: expression" in agents_md
|
||||
assert "call: tool" not in agents_md
|
||||
assert "call: script" not in agents_md
|
||||
|
||||
@@ -11,9 +11,6 @@ from crewai.utilities.serialization import to_serializable
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from celpy.celtypes import StringType
|
||||
from celpy.evaluation import CELFunction
|
||||
|
||||
from crewai.flow.runtime import Flow
|
||||
else:
|
||||
from typing_extensions import TypeAliasType
|
||||
@@ -24,29 +21,6 @@ _CEL_MACROS_WITH_LOCAL_BINDINGS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
def _handle_text_custom_expression(
|
||||
root: Any, path: Any, default: Any = ""
|
||||
) -> StringType:
|
||||
from celpy.celtypes import StringType
|
||||
|
||||
fallback = StringType("" if default is None else str(default))
|
||||
current = root
|
||||
for part in str(path).split("."):
|
||||
if current is None:
|
||||
return fallback
|
||||
try:
|
||||
if isinstance(current, list):
|
||||
current = current[int(part)]
|
||||
else:
|
||||
current = current[StringType(part)]
|
||||
except (KeyError, IndexError, TypeError, ValueError):
|
||||
return fallback
|
||||
|
||||
if current is None:
|
||||
return fallback
|
||||
return StringType(_stringify_cel_value(current))
|
||||
|
||||
|
||||
def _stringify_cel_value(value: Any) -> str:
|
||||
from celpy.adapter import CELJSONEncoder
|
||||
|
||||
@@ -98,21 +72,18 @@ def _parse_template_segments(value: str) -> tuple[str | _ExpressionSegment, ...]
|
||||
return tuple(segments)
|
||||
|
||||
|
||||
_EXPRESSION_FUNCTIONS: dict[str, CELFunction] = {
|
||||
"text": _handle_text_custom_expression,
|
||||
}
|
||||
|
||||
FLOW_TEMPLATE_EXPRESSION_RULES: tuple[str, ...] = (
|
||||
"Use `${...}` inside action mapping strings to read Flow data with CEL. "
|
||||
"Example value: `Ticket: ${state.ticket_id}`.",
|
||||
"Use `state` for input data. Use `outputs.step_name` for a completed "
|
||||
"method result.",
|
||||
"In action mapping strings, keep literal text outside `${...}` and "
|
||||
"interpolate each Flow value directly. Write `Ticket: ${state.ticket_id}`; "
|
||||
"do not assemble the string with CEL `+`.",
|
||||
"If a value is only one `${...}` expression, the result keeps its type. "
|
||||
"Use this for numbers, booleans, objects, and lists.",
|
||||
"If the string has other text, the final value is text. Non-text values "
|
||||
"become JSON. `null` becomes empty text.",
|
||||
'Use `text(root, "path", "default")` for values that may be missing '
|
||||
'or null. The default is optional and is `""`.',
|
||||
)
|
||||
FLOW_TEMPLATE_EXPRESSION_CONTRACT = " ".join(FLOW_TEMPLATE_EXPRESSION_RULES)
|
||||
FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = {
|
||||
@@ -125,10 +96,6 @@ FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = {
|
||||
"title": "Keep a list or number type",
|
||||
"code": 'domains: "${state.domains}"\nlimit: "${state.limit}"',
|
||||
},
|
||||
{
|
||||
"title": "Use a default for missing text",
|
||||
"code": 'input: "Ticket ${text(state, \\"ticket.id\\", \\"unknown\\")}"',
|
||||
},
|
||||
),
|
||||
"json": (
|
||||
{
|
||||
@@ -141,14 +108,6 @@ FLOW_TEMPLATE_EXPRESSION_EXAMPLES: dict[str, tuple[dict[str, str], ...]] = {
|
||||
'{\n "domains": "${state.domains}",\n "limit": "${state.limit}"\n}'
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "Use a default for missing text",
|
||||
"code": (
|
||||
"{\n"
|
||||
' "input": "Ticket ${text(state, \\"ticket.id\\", \\"unknown\\")}"\n'
|
||||
"}"
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -374,8 +333,7 @@ class Expression:
|
||||
|
||||
environment = Environment()
|
||||
program = environment.program(
|
||||
Expression._compile_cel(expression, environment=environment),
|
||||
functions=_EXPRESSION_FUNCTIONS,
|
||||
Expression._compile_cel(expression, environment=environment)
|
||||
)
|
||||
result = program.evaluate(cast(Context, json_to_cel(context)))
|
||||
return json.loads(json.dumps(result, cls=CELJSONEncoder))
|
||||
|
||||
@@ -11,7 +11,6 @@ from jinja2 import Environment, FileSystemLoader
|
||||
import yaml
|
||||
|
||||
from crewai.flow.expressions import (
|
||||
FLOW_TEMPLATE_EXPRESSION_CONTRACT,
|
||||
FLOW_TEMPLATE_EXPRESSION_EXAMPLES,
|
||||
FLOW_TEMPLATE_EXPRESSION_RULES,
|
||||
)
|
||||
@@ -186,7 +185,14 @@ MODEL_SPECS: tuple[ModelSpec, ...] = (
|
||||
hidden=True,
|
||||
),
|
||||
ModelSpec("FlowScriptActionDefinition", "Action", "methods.<name>.do[call=script]"),
|
||||
ModelSpec("FlowToolActionDefinition", "Action", "methods.<name>.do[call=tool]"),
|
||||
ModelSpec(
|
||||
"FlowToolActionDefinition",
|
||||
"Action",
|
||||
"methods.<name>.do[call=tool]",
|
||||
descriptions={
|
||||
"with": "Tool input arguments. Insert Flow values with `${...}`.",
|
||||
},
|
||||
),
|
||||
ModelSpec(
|
||||
"FlowCrewActionDefinition",
|
||||
"Action",
|
||||
@@ -194,7 +200,7 @@ MODEL_SPECS: tuple[ModelSpec, ...] = (
|
||||
examples=True,
|
||||
descriptions={
|
||||
"call": "Action discriminator. Use crew to run an inline Crew definition.",
|
||||
"inputs": f"Actual kickoff inputs passed to the Crew. {FLOW_TEMPLATE_EXPRESSION_CONTRACT} The evaluated values are available to crew agent and task interpolation as `{{name}}` placeholders; reference each input the crew needs in agent or task text.",
|
||||
"inputs": "Runtime inputs passed to the Crew. Insert Flow values with `${...}` and reference each input as `{name}` in agent or task text.",
|
||||
},
|
||||
),
|
||||
ModelSpec(
|
||||
@@ -262,7 +268,7 @@ MODEL_SPECS: tuple[ModelSpec, ...] = (
|
||||
hidden=True,
|
||||
examples=True,
|
||||
descriptions={
|
||||
"input": f"Input passed to the individual agent kickoff outside of a crew. Use one string. {FLOW_TEMPLATE_EXPRESSION_CONTRACT} When an agent needs multiple fields, write one string with labels and separators, for example `Ticket ID: ${{state.ticket_id}}; Message: ${{state.message}}`.",
|
||||
"input": "Agent prompt template. Insert Flow values with `${...}`, for example `Ticket: ${state.ticket_id}`.",
|
||||
"llm": "Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`.",
|
||||
"planning_config": "Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution.",
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ Pick the simplest action that does the job.
|
||||
- Use `call: tool` for packaged deterministic work: API calls, searches, lookups, scoring, file work, or custom CrewAI tools.
|
||||
{% endif %}
|
||||
- Use `call: agent` for one AI worker that classifies, decides, summarizes, writes, or drafts. Put `role`, `goal`, `backstory`, and `input` under `with`. Do not add an action-level `inputs` map to an agent.
|
||||
- Repository-backed agents may set `from_repository` and omit inline `role`, `goal`, and `backstory`. Explicitly provided fields override repository values.
|
||||
- Use `call: crew` for coordinated AI work with multiple agents or tasks. Define the crew under `with`. Pass runtime values with the action-level `inputs` map.
|
||||
{% if include_each_action %}
|
||||
- Use `call: each` when the same ordered mini-pipeline must run once per item. Give every step a `name`.
|
||||
@@ -137,6 +138,7 @@ Dynamic value rules:
|
||||
- Do not use fields outside the declaration schema{% if include_tool_action %} or tool refs shown here{% endif %}.
|
||||
- Do not put more than one action under a method's `do`.
|
||||
- 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.
|
||||
|
||||
@@ -1348,7 +1348,15 @@ def test_skill_documents_flow_wiring():
|
||||
assert "```yaml" in skill
|
||||
assert "[Method](#method-methods)" in skill
|
||||
assert 'input: "Reviewed research: ${outputs.research_brief.raw}"' in skill
|
||||
assert 'text(root, "path", "default")' in skill
|
||||
assert "do not assemble the string with CEL `+`" in skill
|
||||
assert "Do not use CEL `+` to build text in action mappings" in skill
|
||||
assert "Agent prompt template. Insert Flow values with `${...}`" in skill
|
||||
assert (
|
||||
"Repository-backed agents may set `from_repository` and omit inline "
|
||||
"`role`, `goal`, and `backstory`" in skill
|
||||
)
|
||||
assert "Runtime inputs passed to the Crew" in skill
|
||||
assert "Tool input arguments. Insert Flow values with `${...}`" in skill
|
||||
assert "trust CrewAI defaults and omit them" in skill
|
||||
assert "#### LLM Definition" in skill
|
||||
assert "`max_tokens` (optional): integer | null; default `null`" in skill
|
||||
|
||||
@@ -1102,7 +1102,7 @@ methods:
|
||||
)
|
||||
|
||||
|
||||
def test_tool_action_renders_text_custom_expression_inputs():
|
||||
def test_tool_action_renders_interpolated_inputs():
|
||||
yaml_str = f"""
|
||||
schema: crewai.flow/v1
|
||||
name: ToolFlow
|
||||
@@ -1112,8 +1112,8 @@ methods:
|
||||
call: tool
|
||||
ref: {__name__}:StaticSearchTool
|
||||
with:
|
||||
search_query: "${{'Ticket ID: ' + text(state, 'ticket.id') + '; Subject: ' + text(state, 'ticket.subject') + '; Priority: ' + text(state, 'priority', 'unknown') + '; Message: ' + text(state, 'messages.0.body')}}"
|
||||
prefix: "${{text(state, 'ticket')}}"
|
||||
search_query: "Ticket ID: ${{state.ticket.id}}; Subject: ${{state.ticket.subject}}; Message: ${{state.messages[0].body}}"
|
||||
prefix: "${{state.prefix}}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
@@ -1124,9 +1124,10 @@ methods:
|
||||
inputs={
|
||||
"ticket": {"id": 123, "subject": None},
|
||||
"messages": [{"body": "Initial report"}],
|
||||
"prefix": "ticket",
|
||||
}
|
||||
)
|
||||
== '{"id": 123, "subject": null}:Ticket ID: 123; Subject: ; Priority: unknown; Message: Initial report'
|
||||
== "ticket:Ticket ID: 123; Subject: ; Message: Initial report"
|
||||
)
|
||||
|
||||
|
||||
@@ -1319,7 +1320,7 @@ methods:
|
||||
role: Analyst
|
||||
goal: Answer questions
|
||||
backstory: Knows things.
|
||||
input: "Ticket ID: ${text(state, 'ticket.id')}; Subject: ${text(state, 'ticket.subject')}"
|
||||
input: "Ticket ID: ${state.ticket.id}; Subject: ${state.ticket.subject}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
@@ -2909,37 +2910,6 @@ def test_explicit_cel_fields_accept_expression_markers():
|
||||
assert Flow.from_declaration(contents=definition).kickoff(inputs={"score": 90}) == "qualified"
|
||||
|
||||
|
||||
def test_expression_action_runs_text_custom_expression():
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ExpressionFlow",
|
||||
"methods": {
|
||||
"summarize": {
|
||||
"start": True,
|
||||
"do": {
|
||||
"call": "expression",
|
||||
"expr": (
|
||||
"'Ticket ID: ' + text(state, 'ticket.id') + "
|
||||
"'; Tags: ' + text(state, 'tags')"
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert (
|
||||
Flow.from_declaration(contents=definition).kickoff(
|
||||
inputs={
|
||||
"ticket": {"id": 123},
|
||||
"tags": ["urgent", "billing"],
|
||||
}
|
||||
)
|
||||
== 'Ticket ID: 123; Tags: ["urgent", "billing"]'
|
||||
)
|
||||
|
||||
|
||||
def test_expression_local_context_recurses_into_dataclass_values():
|
||||
from crewai.flow.expressions import Expression
|
||||
|
||||
|
||||
Reference in New Issue
Block a user