330 lines
12 KiB
Python
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 []
|
|
}
|