mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 10:17:55 +00:00
fix(workflows): validate CEL at save time and document which fields use it
Workflows ship two expression languages in adjacent fields. Agent
`prompt_template` and end `output_template` are Jinja2, so `{{query}}` is
correct there. State and condition `expression` fields are bare CEL,
where the same string is a parse error.
Nothing told users this. docs/content/Agents/nodes.mdx documented the
pre-CEL Set State node with `{{variable_name}}` examples and Set /
Increment / Append operations that no longer exist — "CEL" appeared
nowhere in the docs. The builder's own placeholder was `input.foo + 1`,
referencing an `input` namespace that is not in workflow state, while the
panel beside it advertised `{{ agent.variable }}`.
There was also no validation anywhere: neither validateWorkflow nor
validate_workflow_structure looked at state nodes at all, and conditions
were only checked for a non-empty string. A workflow containing
`{{query}}` saved and published clean, then aborted on the first message
with a bare caret dump that sanitize_api_error collapsed into "An error
occurred … please try again later" — advice that sends the user round the
same loop, which is what the 2026-08-01 report shows.
Adds validate_cel_expression (compile-only, since state is built at run
time and unresolved names are not knowable when saving) and wires it into
the workflow save path for both node types, plus the half-configured
state operations the engine silently skips. `{{ }}` gets a targeted hint,
raised only after compilation has already failed so valid CEL that
contains braces — `x == "{{y}}"`, or a nested map literal — is not
rejected.
celpy errors are now summarized: undeclared-reference messages embed a
repr of the entire activation, thousands of characters including the
user's own query, and others interpolate the offending state value.
Quoted fragments that are not bare identifiers are redacted, since
config errors now bypass sanitize_api_error to reach the user, and a
shared agent's runner is not its owner.
Docs rewritten with a table of which field takes which syntax, CEL
examples for Set State, and the Condition node section that basics.mdx
has been linking to all along.
This commit is contained in:
1 parent
a51dd10cc7
commit
23e249fca1
7 files changed
+426
-25
No files matched your search
@@ -146,6 +146,96 @@ def test_execute_agent_node_falls_back_to_text_when_schema_not_configured(monkey
|
||||
assert engine.state["result"] == "plain text answer"
|
||||
|
||||
|
||||
def _state_workflow(operations, nested=True):
|
||||
"""Start → state → end, with state config in either accepted shape."""
|
||||
data = {"config": {"operations": operations}} if nested else {
|
||||
"operations": operations
|
||||
}
|
||||
nodes = [
|
||||
{"id": "start", "type": "start", "title": "Start", "data": {}},
|
||||
{"id": "state", "type": "state", "title": "Build reply", "data": data},
|
||||
{"id": "end", "type": "end", "title": "End", "data": {}},
|
||||
]
|
||||
edges = [
|
||||
{"id": "edge_1", "source": "start", "target": "state"},
|
||||
{"id": "edge_2", "source": "state", "target": "end"},
|
||||
]
|
||||
return nodes, edges
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nested", [True, False])
|
||||
def test_validate_workflow_structure_rejects_template_syntax_in_state_node(nested):
|
||||
"""``{{query}}`` compiles nowhere but used to save and publish clean.
|
||||
|
||||
It then aborted the run on the first message with a bare caret dump that
|
||||
``sanitize_api_error`` collapsed into "try again later" — the 2026-08-01
|
||||
report, where a new user retried eight times over seven hours.
|
||||
"""
|
||||
nodes, edges = _state_workflow(
|
||||
[{"expression": "{{query}}", "target_variable": "reply"}], nested=nested
|
||||
)
|
||||
|
||||
errors = validate_workflow_structure(nodes, edges)
|
||||
|
||||
assert any(
|
||||
"Set State node 'Build reply'" in err and "invalid expression" in err
|
||||
for err in errors
|
||||
), errors
|
||||
# The message must teach the correction, not just report a failure.
|
||||
assert any("CEL" in err for err in errors), errors
|
||||
|
||||
|
||||
def test_validate_workflow_structure_accepts_valid_state_expression():
|
||||
nodes, edges = _state_workflow(
|
||||
[{"expression": 'query + "!"', "target_variable": "reply"}]
|
||||
)
|
||||
assert validate_workflow_structure(nodes, edges) == []
|
||||
|
||||
|
||||
def test_validate_workflow_structure_accepts_runtime_only_state_reference():
|
||||
"""Names resolve from run state, so they cannot be checked at save time."""
|
||||
nodes, edges = _state_workflow(
|
||||
[{"expression": "node_agent_1_output", "target_variable": "reply"}]
|
||||
)
|
||||
assert validate_workflow_structure(nodes, edges) == []
|
||||
|
||||
|
||||
def test_validate_workflow_structure_rejects_half_configured_state_operation():
|
||||
"""The engine skips these silently, so downstream reads an unset var."""
|
||||
nodes, edges = _state_workflow(
|
||||
[{"expression": "query", "target_variable": ""}]
|
||||
)
|
||||
errors = validate_workflow_structure(nodes, edges)
|
||||
assert any("no target variable" in err for err in errors), errors
|
||||
|
||||
|
||||
def test_validate_workflow_structure_rejects_invalid_condition_expression():
|
||||
nodes = [
|
||||
{"id": "start", "type": "start", "title": "Start", "data": {}},
|
||||
{
|
||||
"id": "cond",
|
||||
"type": "condition",
|
||||
"title": "Route",
|
||||
"data": {
|
||||
"cases": [
|
||||
{"expression": "{{query}}", "sourceHandle": "case_0"},
|
||||
{"expression": "true", "sourceHandle": "else"},
|
||||
]
|
||||
},
|
||||
},
|
||||
{"id": "end", "type": "end", "title": "End", "data": {}},
|
||||
]
|
||||
edges = [
|
||||
{"id": "e1", "source": "start", "target": "cond"},
|
||||
{"id": "e2", "source": "cond", "target": "end", "sourceHandle": "case_0"},
|
||||
{"id": "e3", "source": "cond", "target": "end", "sourceHandle": "else"},
|
||||
]
|
||||
|
||||
errors = validate_workflow_structure(nodes, edges)
|
||||
|
||||
assert any("invalid expression" in err for err in errors), errors
|
||||
|
||||
|
||||
def test_validate_workflow_structure_rejects_invalid_agent_json_schema():
|
||||
nodes = [
|
||||
{"id": "start", "type": "start", "title": "Start", "data": {}},
|
||||
|
||||
Reference in new issue
Block a user