Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions tests/test_action_id_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from velvetflow.verification.validation import validate_completed_workflow


def test_action_node_requires_action_id():
workflow = {
"workflow_name": "missing_action_id",
"nodes": [
{
"id": "action_missing",
"type": "action",
"action_id": None,
"params": {"prompt": "hello"},
}
],
}

errors = validate_completed_workflow(workflow, action_registry=[])

assert any(err.code == "MISSING_ACTION_ID" for err in errors)
10 changes: 10 additions & 0 deletions tests/test_jinja_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,13 @@ def test_condition_field_literal_wrapped_as_jinja():
assert errors == []
assert summary.get("applied") in {False, None}
assert normalized["nodes"][0]["params"]["expression"] == "{{ result_of.some_loop.exports.items | length > 0 }}"


def test_missing_jinja_expression_is_wrapped():
workflow = _workflow({"target": "result_of.source.items | length > 0"})

normalized, summary, errors = normalize_params_to_jinja(workflow)

assert errors == []
assert summary["applied"] is True
assert normalized["nodes"][0]["params"]["target"] == "{{ result_of.source.items | length > 0 }}"
1 change: 1 addition & 0 deletions velvetflow/planner/repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,4 @@ def submit_repaired_workflow(
"apply_rule_based_repairs",
"repair_workflow_with_llm",
]
"MISSING_ACTION_ID": "action 节点必须补齐 action_id,选择与 display_name/上下文最匹配的已注册动作,并同步更新 params。示例:将缺失 action_id 的发送邮件节点补为 comms.send_email 并补齐收件人字段。",
22 changes: 22 additions & 0 deletions velvetflow/verification/jinja_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@


_SIMPLE_PATH_RE = re.compile(r"^(result_of|loop)\.[A-Za-z_][\w.]*$")
POTENTIAL_JINJA_EXPR = re.compile(
r"\b[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_]+|\[[^\]]+\])+\b"
)


def looks_like_missing_jinja(s: str) -> bool:
if "{{" in s or "{%" in s:
return False
return bool(POTENTIAL_JINJA_EXPR.search(s))


def has_unwrapped_variable(template: str) -> bool:
if "{{" in template or "{%" in template:
return False
env = get_jinja_env()
try:
env.parse(f"{{{{ {template} }}}}")
except TemplateError:
return False
return looks_like_missing_jinja(template)


def _normalize_jinja_expr(value: Any) -> Tuple[Any, bool]:
Expand All @@ -29,6 +49,8 @@ def _normalize_jinja_expr(value: Any) -> Tuple[Any, bool]:
if isinstance(value, str):
stripped = value.strip()
if stripped and "{{" not in stripped and "{%" not in stripped:
if has_unwrapped_variable(stripped):
return f"{{{{ {stripped} }}}}", True
if _SIMPLE_PATH_RE.match(stripped):
return f"{{{{ {stripped} }}}}", True
# Fallback: wrap raw literals as Jinja templates so every param
Expand Down
13 changes: 13 additions & 0 deletions velvetflow/verification/node_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ def _flag_self_reference(field_path: str, ref: str) -> None:
)
)

if ntype == "action" and (not isinstance(action_id, str) or not action_id.strip()):
errors.append(
ValidationError(
code="MISSING_ACTION_ID",
node_id=nid,
field="action_id",
message=(
"action 节点缺少 action_id,无法匹配可执行动作。"
"请将错误信息、节点信息、上下文信息提交给 LLM 分析,并使用工具进行修复。"
),
)
)

if ntype == "action" and (not isinstance(params, Mapping) or len(params) == 0):
errors.append(
ValidationError(
Expand Down
Loading