fix(flow): report the real CEL error for failures inside map literals (#6793)
Some checks failed
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled

This commit is contained in:
Gabe Milani
2026-08-03 16:47:02 -03:00
committed by GitHub
parent 766d71aefb
commit 26518e0dec
2 changed files with 67 additions and 0 deletions

View File

@@ -21,6 +21,25 @@ _CEL_MACROS_WITH_LOCAL_BINDINGS = frozenset(
)
def _find_cel_eval_error(value: Any) -> Exception | None:
from celpy.evaluation import CELEvalError
if isinstance(value, CELEvalError):
return value
if isinstance(value, dict):
for key, item in value.items():
if (error := _find_cel_eval_error(key)) is not None:
return error
if (error := _find_cel_eval_error(item)) is not None:
return error
return None
if isinstance(value, (list, tuple)):
for item in value:
if (error := _find_cel_eval_error(item)) is not None:
return error
return None
def _stringify_cel_value(value: Any) -> str:
from celpy.adapter import CELJSONEncoder
@@ -336,6 +355,8 @@ class Expression:
Expression._compile_cel(expression, environment=environment)
)
result = program.evaluate(cast(Context, json_to_cel(context)))
if (eval_error := _find_cel_eval_error(result)) is not None:
raise eval_error
return json.loads(json.dumps(result, cls=CELJSONEncoder))
except Exception as e:
raise ExpressionError(

View File

@@ -2952,6 +2952,52 @@ def test_expression_template_empty_context_overrides_stored_context():
expression.render_template({})
@pytest.mark.parametrize(
"expression",
[
"{'a': 1/0}",
"{'a': 1, 'b': state.missing}",
"{'a': {'b': 1/0}}",
"{'a': [1/0]}",
],
)
def test_expression_raises_for_cel_eval_error_returned_as_data(expression):
"""celpy returns a map literal holding a CELEvalError instead of raising it."""
from crewai.flow.expressions import Expression, ExpressionError
with pytest.raises(ExpressionError, match="failed to evaluate CEL expression"):
Expression(expression, context={"state": {"score": 90}}).evaluate()
def test_expression_nested_cel_eval_error_reports_underlying_cause():
from crewai.flow.expressions import Expression, ExpressionError
expression = Expression("{'a': 1/0}", context={"state": {}})
with pytest.raises(ExpressionError, match="modulus or divide by zero"):
expression.evaluate()
def test_expression_keeps_short_circuited_cel_errors():
"""Errors that CEL logic intentionally silences must still evaluate."""
from crewai.flow.expressions import Expression
context = {"state": {"tags": ["a", "b"]}}
assert Expression("{'ok': false && 1/0 == 1}", context=context).evaluate() == {
"ok": False
}
assert Expression("{'ok': true || 1/0 == 1}", context=context).evaluate() == {
"ok": True
}
assert (
Expression(
"state.tags.exists(t, t == 'a' || 1/0 == 1)", context=context
).evaluate()
is True
)
def test_expression_action_can_route_like_if_else():
yaml_str = f"""
schema: crewai.flow/v1