Files
ai-video-fullstack/backend/services/state_expressions.py
T

330 lines
12 KiB
Python

"""Typed, side-effect-free expressions used by built-in state updates.
Expressions always read one immutable snapshot. Callers validate first, then
evaluate every rule, and only commit after all values succeed.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
MAX_EXPRESSION_DEPTH = 8
MAX_UPDATE_RULES = 10
MAX_LLM_PARAMETERS = 10
class StateExpressionError(ValueError):
pass
def _field(value: Mapping[str, Any], snake: str, camel: str | None = None) -> Any:
if snake in value:
return value[snake]
return value.get(camel or snake)
def _value_type(value: Any) -> str:
if isinstance(value, bool):
return "boolean"
if isinstance(value, (int, float)):
return "number"
if isinstance(value, str):
return "string"
raise StateExpressionError("状态表达式只支持字符串、数字或布尔值")
def _definition_types(
definitions: Mapping[str, Mapping[str, Any]] | None,
) -> dict[str, str]:
result: dict[str, str] = {}
for name, definition in (definitions or {}).items():
value_type = (
(definition or {}).get("type")
if isinstance(definition, Mapping)
else getattr(definition, "type", None)
)
result[str(name)] = str(value_type or "string")
return result
@dataclass(frozen=True)
class LlmParameter:
name: str
value_type: str
description: str
def _inspect_expression(
expression: Mapping[str, Any],
variable_types: Mapping[str, str],
*,
allow_llm: bool,
depth: int,
parameters: dict[str, LlmParameter],
) -> str:
if depth > MAX_EXPRESSION_DEPTH:
raise StateExpressionError(
f"状态表达式深度不能超过 {MAX_EXPRESSION_DEPTH}"
)
kind = str(expression.get("type") or "")
if kind == "constant":
return _value_type(expression.get("value"))
if kind == "variable":
name = str(_field(expression, "variable_name", "variableName") or "")
if name not in variable_types:
raise StateExpressionError(f"状态表达式引用了未声明变量: {name}")
return variable_types[name]
if kind == "llm":
if not allow_llm:
raise StateExpressionError("确定性 Update State 节点不能使用 LLM 提取值")
name = str(_field(expression, "parameter_name", "parameterName") or "")
value_type = str(_field(expression, "value_type", "valueType") or "string")
description = str(expression.get("description") or "").strip()
if not name or value_type not in {"string", "number", "boolean"}:
raise StateExpressionError("LLM 提取参数名称或类型无效")
if not description:
raise StateExpressionError(f"LLM 提取参数 {name} 必须填写说明")
current = LlmParameter(name, value_type, description)
previous = parameters.get(name)
if previous is not None and previous != current:
raise StateExpressionError(f"LLM 参数 {name} 存在冲突定义")
parameters[name] = current
if len(parameters) > MAX_LLM_PARAMETERS:
raise StateExpressionError(
f"LLM 提取参数不能超过 {MAX_LLM_PARAMETERS} 个"
)
return value_type
if kind == "if":
condition = expression.get("condition")
then = expression.get("then")
otherwise = _field(expression, "otherwise")
if not all(isinstance(item, Mapping) for item in (condition, then, otherwise)):
raise StateExpressionError("if 表达式缺少 condition/then/otherwise")
condition_type = _inspect_expression(
condition,
variable_types,
allow_llm=allow_llm,
depth=depth + 1,
parameters=parameters,
)
if condition_type != "boolean":
raise StateExpressionError("if 条件必须是布尔值")
then_type = _inspect_expression(
then,
variable_types,
allow_llm=allow_llm,
depth=depth + 1,
parameters=parameters,
)
otherwise_type = _inspect_expression(
otherwise,
variable_types,
allow_llm=allow_llm,
depth=depth + 1,
parameters=parameters,
)
if then_type != otherwise_type:
raise StateExpressionError("if 的两个结果分支类型必须一致")
return then_type
operands = expression.get("operands")
if kind not in {
"and", "or", "eq", "neq", "gt", "gte", "lt", "lte",
"add", "subtract", "multiply", "divide",
} or not isinstance(operands, list) or len(operands) < 2:
raise StateExpressionError(f"状态表达式运算无效: {kind or 'empty'}")
if not all(isinstance(item, Mapping) for item in operands):
raise StateExpressionError(f"{kind} 的操作数必须是表达式")
operand_types = [
_inspect_expression(
item,
variable_types,
allow_llm=allow_llm,
depth=depth + 1,
parameters=parameters,
)
for item in operands
]
if kind in {"and", "or"}:
if any(item != "boolean" for item in operand_types):
raise StateExpressionError(f"{kind} 只能组合布尔值")
return "boolean"
if kind in {"eq", "neq"}:
if len(set(operand_types)) != 1:
raise StateExpressionError(f"{kind} 的操作数类型必须一致")
return "boolean"
if kind in {"gt", "gte", "lt", "lte"}:
if any(item != "number" for item in operand_types):
raise StateExpressionError(f"{kind} 只能比较数字")
return "boolean"
if kind == "add" and len(set(operand_types)) == 1:
result_type = operand_types[0]
if result_type in {"number", "string"}:
return result_type
if any(item != "number" for item in operand_types):
raise StateExpressionError(f"{kind} 只能用于数字")
return "number"
def validate_update_rules(
rules: list[Mapping[str, Any]] | None,
definitions: Mapping[str, Mapping[str, Any]] | None,
*,
allow_llm: bool,
) -> dict[str, dict[str, str]]:
"""Validate rules and return the LLM-visible JSON schema properties."""
rules = rules or []
if len(rules) > MAX_UPDATE_RULES:
raise StateExpressionError(
f"状态更新规则不能超过 {MAX_UPDATE_RULES} 条"
)
variable_types = _definition_types(definitions)
parameters: dict[str, LlmParameter] = {}
targets: set[str] = set()
llm_target_parameters: dict[str, str] = {}
for rule in rules:
target = str(_field(rule, "variable_name", "variableName") or "")
expression = rule.get("expression")
if target not in variable_types:
raise StateExpressionError(f"状态更新目标未声明: {target}")
if target in targets:
raise StateExpressionError(f"状态更新目标不能重复: {target}")
targets.add(target)
if not isinstance(expression, Mapping):
raise StateExpressionError(f"状态更新 {target} 缺少表达式")
result_type = _inspect_expression(
expression,
variable_types,
allow_llm=allow_llm,
depth=1,
parameters=parameters,
)
if result_type != variable_types[target]:
raise StateExpressionError(
f"状态更新 {target} 的结果类型应为 {variable_types[target]}"
)
if allow_llm:
llm_names: set[str] = set()
_collect_llm_names(expression, llm_names)
for parameter_name in llm_names:
previous_target = llm_target_parameters.get(parameter_name)
if previous_target is not None and previous_target != target:
raise StateExpressionError(
f"LLM 参数 {parameter_name} 不能同时更新多个变量"
)
llm_target_parameters[parameter_name] = target
return {
parameter.name: {
"type": parameter.value_type,
"description": parameter.description,
}
for parameter in parameters.values()
}
def _collect_llm_names(expression: Mapping[str, Any], result: set[str]) -> None:
kind = str(expression.get("type") or "")
if kind == "llm":
result.add(
str(_field(expression, "parameter_name", "parameterName") or "")
)
return
if kind == "if":
children = [
expression.get("condition"),
expression.get("then"),
expression.get("otherwise"),
]
else:
children = expression.get("operands") or []
for child in children:
if isinstance(child, Mapping):
_collect_llm_names(child, result)
def _evaluate(
expression: Mapping[str, Any],
snapshot: Mapping[str, Any],
arguments: Mapping[str, Any],
) -> Any:
kind = str(expression.get("type") or "")
if kind == "constant":
return expression.get("value")
if kind == "variable":
name = str(_field(expression, "variable_name", "variableName") or "")
if name not in snapshot:
raise StateExpressionError(f"状态变量尚未赋值: {name}")
return snapshot[name]
if kind == "llm":
name = str(_field(expression, "parameter_name", "parameterName") or "")
if name not in arguments:
raise StateExpressionError(f"缺少 LLM 提取参数: {name}")
return arguments[name]
if kind == "if":
branch = "then" if bool(
_evaluate(expression["condition"], snapshot, arguments)
) else "otherwise"
return _evaluate(expression[branch], snapshot, arguments)
values = [
_evaluate(item, snapshot, arguments)
for item in expression.get("operands") or []
]
if kind == "and":
return all(values)
if kind == "or":
return any(values)
if kind == "eq":
return all(value == values[0] for value in values[1:])
if kind == "neq":
return all(value != values[0] for value in values[1:])
if kind == "gt":
return all(left > right for left, right in zip(values, values[1:]))
if kind == "gte":
return all(left >= right for left, right in zip(values, values[1:]))
if kind == "lt":
return all(left < right for left, right in zip(values, values[1:]))
if kind == "lte":
return all(left <= right for left, right in zip(values, values[1:]))
if kind == "add":
result = values[0]
for value in values[1:]:
result += value
return result
if kind == "subtract":
result = values[0]
for value in values[1:]:
result -= value
return result
if kind == "multiply":
result = values[0]
for value in values[1:]:
result *= value
return result
if kind == "divide":
result = values[0]
for value in values[1:]:
if value == 0:
raise StateExpressionError("状态表达式不能除以零")
result /= value
return result
raise StateExpressionError(f"不支持的状态表达式: {kind}")
def evaluate_update_rules(
rules: list[Mapping[str, Any]] | None,
snapshot: Mapping[str, Any],
arguments: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Evaluate every rule from the same snapshot without mutating state."""
return {
str(_field(rule, "variable_name", "variableName")): _evaluate(
rule["expression"], snapshot, arguments or {}
)
for rule in rules or []
}