mirror of
https://github.com/langgenius/dify.git
synced 2026-03-04 15:26:21 +08:00
505 lines
17 KiB
Python
505 lines
17 KiB
Python
import json
|
|
from collections.abc import Mapping, Sequence
|
|
from typing import Literal, NamedTuple
|
|
|
|
from dify_graph.file import FileAttribute, file_manager
|
|
from dify_graph.runtime import VariablePool
|
|
from dify_graph.variables import ArrayFileSegment
|
|
from dify_graph.variables.segments import ArrayBooleanSegment, BooleanSegment
|
|
|
|
from .entities import Condition, SubCondition, SupportedComparisonOperator
|
|
|
|
|
|
def _convert_to_bool(value: object) -> bool:
|
|
if isinstance(value, int):
|
|
return bool(value)
|
|
|
|
if isinstance(value, str):
|
|
loaded = json.loads(value)
|
|
if isinstance(loaded, (int, bool)):
|
|
return bool(loaded)
|
|
|
|
raise TypeError(f"unexpected value: type={type(value)}, value={value}")
|
|
|
|
|
|
class ConditionCheckResult(NamedTuple):
|
|
inputs: Sequence[Mapping[str, object]]
|
|
group_results: Sequence[bool]
|
|
final_result: bool
|
|
|
|
|
|
class ConditionProcessor:
|
|
def process_conditions(
|
|
self,
|
|
*,
|
|
variable_pool: VariablePool,
|
|
conditions: Sequence[Condition],
|
|
operator: Literal["and", "or"],
|
|
) -> ConditionCheckResult:
|
|
input_conditions: list[Mapping[str, object]] = []
|
|
group_results: list[bool] = []
|
|
|
|
for condition in conditions:
|
|
variable = variable_pool.get(condition.variable_selector)
|
|
if variable is None:
|
|
raise ValueError(f"Variable {condition.variable_selector} not found")
|
|
|
|
if isinstance(variable, ArrayFileSegment) and condition.comparison_operator in {
|
|
"contains",
|
|
"not contains",
|
|
"all of",
|
|
}:
|
|
# check sub conditions
|
|
if not condition.sub_variable_condition:
|
|
raise ValueError("Sub variable is required")
|
|
result = _process_sub_conditions(
|
|
variable=variable,
|
|
sub_conditions=condition.sub_variable_condition.conditions,
|
|
operator=condition.sub_variable_condition.logical_operator,
|
|
)
|
|
elif condition.comparison_operator in {
|
|
"exists",
|
|
"not exists",
|
|
}:
|
|
result = _evaluate_condition(
|
|
value=variable.value,
|
|
operator=condition.comparison_operator,
|
|
expected=None,
|
|
)
|
|
else:
|
|
actual_value = variable.value if variable else None
|
|
expected_value: str | Sequence[str] | bool | list[bool] | None = condition.value
|
|
if isinstance(expected_value, str):
|
|
expected_value = variable_pool.convert_template(expected_value).text
|
|
# Here we need to explicit convet the input string to boolean.
|
|
if isinstance(variable, (BooleanSegment, ArrayBooleanSegment)) and expected_value is not None:
|
|
# The following two lines is for compatibility with existing workflows.
|
|
if isinstance(expected_value, list):
|
|
expected_value = [_convert_to_bool(i) for i in expected_value]
|
|
else:
|
|
expected_value = _convert_to_bool(expected_value)
|
|
input_conditions.append(
|
|
{
|
|
"actual_value": actual_value,
|
|
"expected_value": expected_value,
|
|
"comparison_operator": condition.comparison_operator,
|
|
}
|
|
)
|
|
result = _evaluate_condition(
|
|
value=actual_value,
|
|
operator=condition.comparison_operator,
|
|
expected=expected_value,
|
|
)
|
|
group_results.append(result)
|
|
# Implemented short-circuit evaluation for logical conditions
|
|
if (operator == "and" and not result) or (operator == "or" and result):
|
|
final_result = result
|
|
return ConditionCheckResult(input_conditions, group_results, final_result)
|
|
|
|
final_result = all(group_results) if operator == "and" else any(group_results)
|
|
return ConditionCheckResult(input_conditions, group_results, final_result)
|
|
|
|
|
|
def _evaluate_condition(
|
|
*,
|
|
operator: SupportedComparisonOperator,
|
|
value: object,
|
|
expected: str | Sequence[str] | bool | Sequence[bool] | None,
|
|
) -> bool:
|
|
match operator:
|
|
case "contains":
|
|
return _assert_contains(value=value, expected=expected)
|
|
case "not contains":
|
|
return _assert_not_contains(value=value, expected=expected)
|
|
case "start with":
|
|
return _assert_start_with(value=value, expected=expected)
|
|
case "end with":
|
|
return _assert_end_with(value=value, expected=expected)
|
|
case "is":
|
|
return _assert_is(value=value, expected=expected)
|
|
case "is not":
|
|
return _assert_is_not(value=value, expected=expected)
|
|
case "empty":
|
|
return _assert_empty(value=value)
|
|
case "not empty":
|
|
return _assert_not_empty(value=value)
|
|
case "=":
|
|
return _assert_equal(value=value, expected=expected)
|
|
case "≠":
|
|
return _assert_not_equal(value=value, expected=expected)
|
|
case ">":
|
|
return _assert_greater_than(value=value, expected=expected)
|
|
case "<":
|
|
return _assert_less_than(value=value, expected=expected)
|
|
case "≥":
|
|
return _assert_greater_than_or_equal(value=value, expected=expected)
|
|
case "≤":
|
|
return _assert_less_than_or_equal(value=value, expected=expected)
|
|
case "null":
|
|
return _assert_null(value=value)
|
|
case "not null":
|
|
return _assert_not_null(value=value)
|
|
case "in":
|
|
return _assert_in(value=value, expected=expected)
|
|
case "not in":
|
|
return _assert_not_in(value=value, expected=expected)
|
|
case "all of" if isinstance(expected, list):
|
|
# Type narrowing: at this point expected is a list, could be list[str] or list[bool]
|
|
if all(isinstance(item, str) for item in expected):
|
|
# Create a new typed list to satisfy type checker
|
|
str_list: list[str] = [item for item in expected if isinstance(item, str)]
|
|
return _assert_all_of(value=value, expected=str_list)
|
|
elif all(isinstance(item, bool) for item in expected):
|
|
# Create a new typed list to satisfy type checker
|
|
bool_list: list[bool] = [item for item in expected if isinstance(item, bool)]
|
|
return _assert_all_of_bool(value=value, expected=bool_list)
|
|
else:
|
|
raise ValueError("all of operator expects homogeneous list of strings or booleans")
|
|
case "exists":
|
|
return _assert_exists(value=value)
|
|
case "not exists":
|
|
return _assert_not_exists(value=value)
|
|
case _:
|
|
raise ValueError(f"Unsupported operator: {operator}")
|
|
|
|
|
|
def _assert_contains(*, value: object, expected: object) -> bool:
|
|
if not value:
|
|
return False
|
|
|
|
if not isinstance(value, (str, list)):
|
|
raise ValueError("Invalid actual value type: string or array")
|
|
|
|
# Type checking ensures value is str or list at this point
|
|
if isinstance(value, str):
|
|
if not isinstance(expected, str):
|
|
expected = str(expected)
|
|
if expected not in value:
|
|
return False
|
|
else: # value is list
|
|
if expected not in value:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_not_contains(*, value: object, expected: object) -> bool:
|
|
if not value:
|
|
return True
|
|
|
|
if not isinstance(value, (str, list)):
|
|
raise ValueError("Invalid actual value type: string or array")
|
|
|
|
# Type checking ensures value is str or list at this point
|
|
if isinstance(value, str):
|
|
if not isinstance(expected, str):
|
|
expected = str(expected)
|
|
if expected in value:
|
|
return False
|
|
else: # value is list
|
|
if expected in value:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_start_with(*, value: object, expected: object) -> bool:
|
|
if not value:
|
|
return False
|
|
|
|
if not isinstance(value, str):
|
|
raise ValueError("Invalid actual value type: string")
|
|
|
|
if not isinstance(expected, str):
|
|
raise ValueError("Expected value must be a string for startswith")
|
|
if not value.startswith(expected):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_end_with(*, value: object, expected: object) -> bool:
|
|
if not value:
|
|
return False
|
|
|
|
if not isinstance(value, str):
|
|
raise ValueError("Invalid actual value type: string")
|
|
|
|
if not isinstance(expected, str):
|
|
raise ValueError("Expected value must be a string for endswith")
|
|
if not value.endswith(expected):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_is(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (str, bool)):
|
|
raise ValueError("Invalid actual value type: string or boolean")
|
|
|
|
if value != expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_is_not(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (str, bool)):
|
|
raise ValueError("Invalid actual value type: string or boolean")
|
|
|
|
if value == expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_empty(*, value: object) -> bool:
|
|
if not value:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _assert_not_empty(*, value: object) -> bool:
|
|
if value:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _normalize_numeric_values(value: int | float, expected: object) -> tuple[int | float, int | float]:
|
|
"""
|
|
Normalize value and expected to compatible numeric types for comparison.
|
|
|
|
Args:
|
|
value: The actual numeric value (int or float)
|
|
expected: The expected value (int, float, or str)
|
|
|
|
Returns:
|
|
A tuple of (normalized_value, normalized_expected) with compatible types
|
|
|
|
Raises:
|
|
ValueError: If expected cannot be converted to a number
|
|
"""
|
|
if not isinstance(expected, (int, float, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to number")
|
|
|
|
# Convert expected to appropriate numeric type
|
|
if isinstance(expected, str):
|
|
# Try to convert to float first to handle decimal strings
|
|
try:
|
|
expected_float = float(expected)
|
|
except ValueError as e:
|
|
raise ValueError(f"Cannot convert '{expected}' to number") from e
|
|
|
|
# If value is int and expected is a whole number, keep as int comparison
|
|
if isinstance(value, int) and expected_float.is_integer():
|
|
return value, int(expected_float)
|
|
else:
|
|
# Otherwise convert value to float for comparison
|
|
return float(value) if isinstance(value, int) else value, expected_float
|
|
elif isinstance(expected, float):
|
|
# If expected is already float, convert int value to float
|
|
return float(value) if isinstance(value, int) else value, expected
|
|
else:
|
|
# expected is int
|
|
return value, expected
|
|
|
|
|
|
def _assert_equal(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (int, float, bool)):
|
|
raise ValueError("Invalid actual value type: number or boolean")
|
|
|
|
# Handle boolean comparison
|
|
if isinstance(value, bool):
|
|
if not isinstance(expected, (bool, int, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to bool")
|
|
expected = bool(expected)
|
|
elif isinstance(value, int):
|
|
if not isinstance(expected, (int, float, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to int")
|
|
expected = int(expected)
|
|
else:
|
|
if not isinstance(expected, (int, float, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to float")
|
|
expected = float(expected)
|
|
|
|
if value != expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_not_equal(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (int, float, bool)):
|
|
raise ValueError("Invalid actual value type: number or boolean")
|
|
|
|
# Handle boolean comparison
|
|
if isinstance(value, bool):
|
|
if not isinstance(expected, (bool, int, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to bool")
|
|
expected = bool(expected)
|
|
elif isinstance(value, int):
|
|
if not isinstance(expected, (int, float, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to int")
|
|
expected = int(expected)
|
|
else:
|
|
if not isinstance(expected, (int, float, str)):
|
|
raise ValueError(f"Cannot convert {type(expected)} to float")
|
|
expected = float(expected)
|
|
|
|
if value == expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_greater_than(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (int, float)):
|
|
raise ValueError("Invalid actual value type: number")
|
|
|
|
value, expected = _normalize_numeric_values(value, expected)
|
|
return value > expected
|
|
|
|
|
|
def _assert_less_than(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (int, float)):
|
|
raise ValueError("Invalid actual value type: number")
|
|
|
|
value, expected = _normalize_numeric_values(value, expected)
|
|
return value < expected
|
|
|
|
|
|
def _assert_greater_than_or_equal(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (int, float)):
|
|
raise ValueError("Invalid actual value type: number")
|
|
|
|
value, expected = _normalize_numeric_values(value, expected)
|
|
return value >= expected
|
|
|
|
|
|
def _assert_less_than_or_equal(*, value: object, expected: object) -> bool:
|
|
if value is None:
|
|
return False
|
|
|
|
if not isinstance(value, (int, float)):
|
|
raise ValueError("Invalid actual value type: number")
|
|
|
|
value, expected = _normalize_numeric_values(value, expected)
|
|
return value <= expected
|
|
|
|
|
|
def _assert_null(*, value: object) -> bool:
|
|
if value is None:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _assert_not_null(*, value: object) -> bool:
|
|
if value is not None:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _assert_in(*, value: object, expected: object) -> bool:
|
|
if not value:
|
|
return False
|
|
|
|
if not isinstance(expected, list):
|
|
raise ValueError("Invalid expected value type: array")
|
|
|
|
if value not in expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_not_in(*, value: object, expected: object) -> bool:
|
|
if not value:
|
|
return True
|
|
|
|
if not isinstance(expected, list):
|
|
raise ValueError("Invalid expected value type: array")
|
|
|
|
if value in expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _assert_all_of(*, value: object, expected: Sequence[str]) -> bool:
|
|
if not value:
|
|
return False
|
|
|
|
# Ensure value is a container that supports 'in' operator
|
|
if not isinstance(value, (list, tuple, set, str)):
|
|
return False
|
|
|
|
return all(item in value for item in expected)
|
|
|
|
|
|
def _assert_all_of_bool(*, value: object, expected: Sequence[bool]) -> bool:
|
|
if not value:
|
|
return False
|
|
|
|
# Ensure value is a container that supports 'in' operator
|
|
if not isinstance(value, (list, tuple, set)):
|
|
return False
|
|
|
|
return all(item in value for item in expected)
|
|
|
|
|
|
def _assert_exists(*, value: object) -> bool:
|
|
return value is not None
|
|
|
|
|
|
def _assert_not_exists(*, value: object) -> bool:
|
|
return value is None
|
|
|
|
|
|
def _process_sub_conditions(
|
|
variable: ArrayFileSegment,
|
|
sub_conditions: Sequence[SubCondition],
|
|
operator: Literal["and", "or"],
|
|
) -> bool:
|
|
files = variable.value
|
|
group_results: list[bool] = []
|
|
for condition in sub_conditions:
|
|
key = FileAttribute(condition.key)
|
|
values = [file_manager.get_attr(file=file, attr=key) for file in files]
|
|
expected_value = condition.value
|
|
if key == FileAttribute.EXTENSION:
|
|
if not isinstance(expected_value, str):
|
|
raise TypeError("Expected value must be a string when key is FileAttribute.EXTENSION")
|
|
if expected_value and not expected_value.startswith("."):
|
|
expected_value = "." + expected_value
|
|
|
|
normalized_values: list[object] = []
|
|
for value in values:
|
|
if value and isinstance(value, str):
|
|
if not value.startswith("."):
|
|
value = "." + value
|
|
normalized_values.append(value)
|
|
values = normalized_values
|
|
sub_group_results: list[bool] = [
|
|
_evaluate_condition(
|
|
value=value,
|
|
operator=condition.comparison_operator,
|
|
expected=expected_value,
|
|
)
|
|
for value in values
|
|
]
|
|
# Determine the result based on the presence of "not" in the comparison operator
|
|
result = all(sub_group_results) if "not" in condition.comparison_operator else any(sub_group_results)
|
|
group_results.append(result)
|
|
return all(group_results) if operator == "and" else any(group_results)
|