diff --git a/api/app/core/workflow/nodes/if_else/config.py b/api/app/core/workflow/nodes/if_else/config.py index 302d469f..4a5b3860 100644 --- a/api/app/core/workflow/nodes/if_else/config.py +++ b/api/app/core/workflow/nodes/if_else/config.py @@ -10,8 +10,18 @@ class SubVariableConditionItem(BaseModel): """A single condition on a file object's field, used inside sub_variable_condition.""" key: str = Field(..., description="Field name of the file object, e.g. type, size, name") operator: ComparisonOperator = Field(..., description="Comparison operator") - value: Any = Field(default=None, description="Value to compare with") - var_type: str = Field(default="string", description="Field value type: string or number") + value: Any = Field(default=None, description="Value to compare with, or variable selector when input_type=variable") + input_type: ValueInputType = Field(default=ValueInputType.CONSTANT, description="constant or variable") + + @field_validator("input_type", mode="before") + @classmethod + def lower_input_type(cls, v): + if isinstance(v, str): + try: + return ValueInputType(v.lower()) + except ValueError: + raise ValueError(f"Invalid input_type: {v}") + return v class SubVariableCondition(BaseModel): diff --git a/api/app/core/workflow/nodes/if_else/node.py b/api/app/core/workflow/nodes/if_else/node.py index faecd87c..c4d3a0e6 100644 --- a/api/app/core/workflow/nodes/if_else/node.py +++ b/api/app/core/workflow/nodes/if_else/node.py @@ -103,7 +103,7 @@ class IfElseNode(BaseNode): left_value = None if expression.sub_variable_condition is not None and isinstance(left_value, list): - evaluator = ArrayFileContainsOperator(left_value, expression.sub_variable_condition) + evaluator = ArrayFileContainsOperator(left_value, expression.sub_variable_condition, variable_pool) else: evaluator = ConditionExpressionResolver.resolve_by_value(left_value)( variable_pool, diff --git a/api/app/core/workflow/nodes/operators.py b/api/app/core/workflow/nodes/operators.py index 30634bbe..62eebbfe 100644 --- a/api/app/core/workflow/nodes/operators.py +++ b/api/app/core/workflow/nodes/operators.py @@ -396,47 +396,49 @@ class NoneObjectComparisonOperator: class ArrayFileContainsOperator: - """Handles contains/not_contains on array[file] with sub_variable_condition. + """Handles contains/not_contains on array[file] with sub_variable_condition.""" - Evaluates whether any (contains) or no (not_contains) file element - in the array satisfies all sub-conditions. - """ - - def __init__(self, left_value: list[dict], sub_variable_condition: Any): + def __init__(self, left_value: list[dict], sub_variable_condition: Any, pool: VariablePool | None = None): self.left_value = left_value self.sub_variable_condition = sub_variable_condition + self.pool = pool + + def _resolve_value(self, cond: Any) -> Any: + if cond.input_type == ValueInputType.VARIABLE and self.pool is not None: + pattern = r"\{\{\s*(.*?)\s*\}\}" + selector = re.sub(pattern, r"\1", str(cond.value)).strip() + return self.pool.get_value(selector, default=None, strict=False) + return cond.value def _match_item(self, file_item: dict) -> bool: - """Check if a single file dict satisfies all sub-conditions.""" results = [] for cond in self.sub_variable_condition.conditions: field_val = file_item.get(cond.key) - result = self._eval_sub(field_val, cond) + expected = self._resolve_value(cond) + result = self._eval_sub(field_val, cond.operator.value, expected) results.append(result) if self.sub_variable_condition.logical_operator.value == "and": return all(results) return any(results) @staticmethod - def _eval_sub(field_val: Any, cond: Any) -> bool: - op = cond.operator.value - expected = cond.value + def _eval_sub(field_val: Any, op: str, expected: Any) -> bool: if field_val is None: - return op in ("empty", "not_empty") and op == "empty" + return op == "empty" match op: - case "eq": return str(field_val) == str(expected) - case "ne": return str(field_val) != str(expected) - case "contains": return isinstance(field_val, str) and str(expected) in field_val + case "eq": return str(field_val) == str(expected) + case "ne": return str(field_val) != str(expected) + case "contains": return isinstance(field_val, str) and str(expected) in field_val case "not_contains": return isinstance(field_val, str) and str(expected) not in field_val - case "in": return field_val in (expected if isinstance(expected, list) else [expected]) - case "not_in": return field_val not in (expected if isinstance(expected, list) else [expected]) - case "gt": return isinstance(field_val, (int, float)) and field_val > float(expected) - case "ge": return isinstance(field_val, (int, float)) and field_val >= float(expected) - case "lt": return isinstance(field_val, (int, float)) and field_val < float(expected) - case "le": return isinstance(field_val, (int, float)) and field_val <= float(expected) - case "empty": return field_val in (None, "", 0) - case "not_empty": return field_val not in (None, "", 0) - case _: return False + case "in": return field_val in (expected if isinstance(expected, list) else [expected]) + case "not_in": return field_val not in (expected if isinstance(expected, list) else [expected]) + case "gt": return isinstance(field_val, (int, float)) and field_val > float(expected) + case "ge": return isinstance(field_val, (int, float)) and field_val >= float(expected) + case "lt": return isinstance(field_val, (int, float)) and field_val < float(expected) + case "le": return isinstance(field_val, (int, float)) and field_val <= float(expected) + case "empty": return field_val in (None, "", 0) + case "not_empty": return field_val not in (None, "", 0) + case _: return False def contains(self) -> bool: return any(self._match_item(f) for f in self.left_value if isinstance(f, dict))