mirror of https://github.com/langgenius/dify.git
fix(api): enhance compatibility with existing workflow
Fix issues flagged by unit tests.
This commit is contained in:
parent
65ae25e09b
commit
fa98505e09
|
|
@ -103,18 +103,23 @@ class BaseAppGenerator:
|
|||
f"(type '{variable_entity.type}') {variable_entity.variable} in input form must be a string"
|
||||
)
|
||||
|
||||
if variable_entity.type == VariableEntityType.NUMBER and isinstance(value, str):
|
||||
# handle empty string case
|
||||
if not value.strip():
|
||||
return None
|
||||
# may raise ValueError if user_input_value is not a valid number
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
else:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"{variable_entity.variable} in input form must be a valid number")
|
||||
if variable_entity.type == VariableEntityType.NUMBER:
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
# handle empty string case
|
||||
if not value.strip():
|
||||
return None
|
||||
# may raise ValueError if user_input_value is not a valid number
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
else:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"{variable_entity.variable} in input form must be a valid number")
|
||||
else:
|
||||
raise TypeError(f"expected value type int, float or str, got {type(value)}, value: {value}")
|
||||
|
||||
match variable_entity.type:
|
||||
case VariableEntityType.SELECT:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,27 @@
|
|||
import json
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, Union
|
||||
|
||||
from core.file import FileAttribute, file_manager
|
||||
from core.variables import ArrayFileSegment
|
||||
from core.variables.segments import BooleanSegment
|
||||
from core.variables.segments import ArrayBooleanSegment, BooleanSegment
|
||||
from core.workflow.entities.variable_pool import VariablePool
|
||||
|
||||
from .entities import Condition, SubCondition, SupportedComparisonOperator
|
||||
|
||||
|
||||
def _convert_to_bool(value: Any) -> 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 ConditionProcessor:
|
||||
def process_conditions(
|
||||
self,
|
||||
|
|
@ -53,8 +66,12 @@ class ConditionProcessor:
|
|||
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) and not variable.value_type.is_valid(expected_value):
|
||||
raise TypeError(f"unexpected value: type={type(expected_value)}, value={expected_value}")
|
||||
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,
|
||||
|
|
@ -81,7 +98,7 @@ def _evaluate_condition(
|
|||
*,
|
||||
operator: SupportedComparisonOperator,
|
||||
value: Any,
|
||||
expected: Union[str, Sequence[str], bool, None],
|
||||
expected: Union[str, Sequence[str], bool | Sequence[bool], None],
|
||||
) -> bool:
|
||||
match operator:
|
||||
case "contains":
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class TestSegmentTypeIsArrayType:
|
|||
SegmentType.ARRAY_NUMBER,
|
||||
SegmentType.ARRAY_OBJECT,
|
||||
SegmentType.ARRAY_FILE,
|
||||
SegmentType.ARRAY_BOOLEAN,
|
||||
]
|
||||
expected_non_array_types = [
|
||||
SegmentType.INTEGER,
|
||||
|
|
@ -35,6 +36,7 @@ class TestSegmentTypeIsArrayType:
|
|||
SegmentType.FILE,
|
||||
SegmentType.NONE,
|
||||
SegmentType.GROUP,
|
||||
SegmentType.BOOLEAN,
|
||||
]
|
||||
|
||||
for seg_type in expected_array_types:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import time
|
|||
import uuid
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.file import File, FileTransferMethod, FileType
|
||||
from core.variables import ArrayFileSegment
|
||||
|
|
@ -274,7 +276,36 @@ def test_array_file_contains_file_name():
|
|||
assert result.outputs["result"] is True
|
||||
|
||||
|
||||
def test_execute_if_else_boolean_conditions():
|
||||
def _get_test_conditions() -> list:
|
||||
conditions = [
|
||||
# Test boolean "is" operator
|
||||
{"comparison_operator": "is", "variable_selector": ["start", "bool_true"], "value": "true"},
|
||||
# Test boolean "is not" operator
|
||||
{"comparison_operator": "is not", "variable_selector": ["start", "bool_false"], "value": "true"},
|
||||
# Test boolean "=" operator
|
||||
{"comparison_operator": "=", "variable_selector": ["start", "bool_true"], "value": "1"},
|
||||
# Test boolean "≠" operator
|
||||
{"comparison_operator": "≠", "variable_selector": ["start", "bool_false"], "value": "1"},
|
||||
# Test boolean "not null" operator
|
||||
{"comparison_operator": "not null", "variable_selector": ["start", "bool_true"]},
|
||||
# Test boolean array "contains" operator
|
||||
{"comparison_operator": "contains", "variable_selector": ["start", "bool_array"], "value": "true"},
|
||||
# Test boolean "in" operator
|
||||
{
|
||||
"comparison_operator": "in",
|
||||
"variable_selector": ["start", "bool_true"],
|
||||
"value": ["true", "false"],
|
||||
},
|
||||
]
|
||||
return [Condition.model_validate(i) for i in conditions]
|
||||
|
||||
|
||||
def _get_condition_test_id(c: Condition):
|
||||
return c.comparison_operator
|
||||
|
||||
|
||||
@pytest.mark.parametrize("condition", _get_test_conditions(), ids=_get_condition_test_id)
|
||||
def test_execute_if_else_boolean_conditions(condition: Condition):
|
||||
"""Test IfElseNode with boolean conditions using various operators"""
|
||||
graph_config = {"edges": [], "nodes": [{"data": {"type": "start"}, "id": "start"}]}
|
||||
|
||||
|
|
@ -294,47 +325,27 @@ def test_execute_if_else_boolean_conditions():
|
|||
|
||||
# construct variable pool with boolean values
|
||||
pool = VariablePool(
|
||||
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={}
|
||||
system_variables=SystemVariable(files=[], user_id="aaa"),
|
||||
)
|
||||
pool.add(["start", "bool_true"], True)
|
||||
pool.add(["start", "bool_false"], False)
|
||||
pool.add(["start", "bool_array"], [True, False, True])
|
||||
pool.add(["start", "mixed_array"], [True, "false", 1, 0])
|
||||
|
||||
node_data = {
|
||||
"title": "Boolean Test",
|
||||
"type": "if-else",
|
||||
"logical_operator": "and",
|
||||
"conditions": [condition.model_dump()],
|
||||
}
|
||||
node = IfElseNode(
|
||||
id=str(uuid.uuid4()),
|
||||
graph_init_params=init_params,
|
||||
graph=graph,
|
||||
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
|
||||
config={
|
||||
"id": "if-else",
|
||||
"data": {
|
||||
"title": "Boolean Test",
|
||||
"type": "if-else",
|
||||
"logical_operator": "and",
|
||||
"conditions": [
|
||||
# Test boolean "is" operator
|
||||
{"comparison_operator": "is", "variable_selector": ["start", "bool_true"], "value": "true"},
|
||||
# Test boolean "is not" operator
|
||||
{"comparison_operator": "is not", "variable_selector": ["start", "bool_false"], "value": "true"},
|
||||
# Test boolean "=" operator
|
||||
{"comparison_operator": "=", "variable_selector": ["start", "bool_true"], "value": "1"},
|
||||
# Test boolean "≠" operator
|
||||
{"comparison_operator": "≠", "variable_selector": ["start", "bool_false"], "value": "1"},
|
||||
# Test boolean "not null" operator
|
||||
{"comparison_operator": "not null", "variable_selector": ["start", "bool_true"]},
|
||||
# Test boolean array "contains" operator
|
||||
{"comparison_operator": "contains", "variable_selector": ["start", "bool_array"], "value": "true"},
|
||||
# Test boolean "in" operator
|
||||
{
|
||||
"comparison_operator": "in",
|
||||
"variable_selector": ["start", "bool_true"],
|
||||
"value": ["true", "false"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
config={"id": "if-else", "data": node_data},
|
||||
)
|
||||
node.init_node_data(node_data)
|
||||
|
||||
# Mock db.session.close()
|
||||
db.session.close = MagicMock()
|
||||
|
|
@ -367,12 +378,30 @@ def test_execute_if_else_boolean_false_conditions():
|
|||
|
||||
# construct variable pool with boolean values
|
||||
pool = VariablePool(
|
||||
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={}
|
||||
system_variables=SystemVariable(files=[], user_id="aaa"),
|
||||
)
|
||||
pool.add(["start", "bool_true"], True)
|
||||
pool.add(["start", "bool_false"], False)
|
||||
pool.add(["start", "bool_array"], [True, False, True])
|
||||
|
||||
node_data = {
|
||||
"title": "Boolean False Test",
|
||||
"type": "if-else",
|
||||
"logical_operator": "or",
|
||||
"conditions": [
|
||||
# Test boolean "is" operator (should be false)
|
||||
{"comparison_operator": "is", "variable_selector": ["start", "bool_true"], "value": "false"},
|
||||
# Test boolean "=" operator (should be false)
|
||||
{"comparison_operator": "=", "variable_selector": ["start", "bool_false"], "value": "1"},
|
||||
# Test boolean "not contains" operator (should be false)
|
||||
{
|
||||
"comparison_operator": "not contains",
|
||||
"variable_selector": ["start", "bool_array"],
|
||||
"value": "true",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
node = IfElseNode(
|
||||
id=str(uuid.uuid4()),
|
||||
graph_init_params=init_params,
|
||||
|
|
@ -380,25 +409,10 @@ def test_execute_if_else_boolean_false_conditions():
|
|||
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
|
||||
config={
|
||||
"id": "if-else",
|
||||
"data": {
|
||||
"title": "Boolean False Test",
|
||||
"type": "if-else",
|
||||
"logical_operator": "or",
|
||||
"conditions": [
|
||||
# Test boolean "is" operator (should be false)
|
||||
{"comparison_operator": "is", "variable_selector": ["start", "bool_true"], "value": "false"},
|
||||
# Test boolean "=" operator (should be false)
|
||||
{"comparison_operator": "=", "variable_selector": ["start", "bool_false"], "value": "1"},
|
||||
# Test boolean "not contains" operator (should be false)
|
||||
{
|
||||
"comparison_operator": "not contains",
|
||||
"variable_selector": ["start", "bool_array"],
|
||||
"value": "true",
|
||||
},
|
||||
],
|
||||
},
|
||||
"data": node_data,
|
||||
},
|
||||
)
|
||||
node.init_node_data(node_data)
|
||||
|
||||
# Mock db.session.close()
|
||||
db.session.close = MagicMock()
|
||||
|
|
@ -431,42 +445,41 @@ def test_execute_if_else_boolean_cases_structure():
|
|||
|
||||
# construct variable pool with boolean values
|
||||
pool = VariablePool(
|
||||
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={}
|
||||
system_variables=SystemVariable(files=[], user_id="aaa"),
|
||||
)
|
||||
pool.add(["start", "bool_true"], True)
|
||||
pool.add(["start", "bool_false"], False)
|
||||
|
||||
node_data = {
|
||||
"title": "Boolean Cases Test",
|
||||
"type": "if-else",
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "true",
|
||||
"logical_operator": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"comparison_operator": "is",
|
||||
"variable_selector": ["start", "bool_true"],
|
||||
"value": "true",
|
||||
},
|
||||
{
|
||||
"comparison_operator": "is not",
|
||||
"variable_selector": ["start", "bool_false"],
|
||||
"value": "true",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
node = IfElseNode(
|
||||
id=str(uuid.uuid4()),
|
||||
graph_init_params=init_params,
|
||||
graph=graph,
|
||||
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
|
||||
config={
|
||||
"id": "if-else",
|
||||
"data": {
|
||||
"title": "Boolean Cases Test",
|
||||
"type": "if-else",
|
||||
"cases": [
|
||||
{
|
||||
"case_id": "true",
|
||||
"logical_operator": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"comparison_operator": "is",
|
||||
"variable_selector": ["start", "bool_true"],
|
||||
"value": "true",
|
||||
},
|
||||
{
|
||||
"comparison_operator": "is not",
|
||||
"variable_selector": ["start", "bool_false"],
|
||||
"value": "true",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
config={"id": "if-else", "data": node_data},
|
||||
)
|
||||
node.init_node_data(node_data)
|
||||
|
||||
# Mock db.session.close()
|
||||
db.session.close = MagicMock()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from core.workflow.nodes.list_operator.entities import (
|
|||
Limit,
|
||||
ListOperatorNodeData,
|
||||
Order,
|
||||
OrderByConfig,
|
||||
)
|
||||
from core.workflow.nodes.list_operator.exc import InvalidKeyError
|
||||
from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func
|
||||
|
|
@ -27,7 +28,7 @@ def list_operator_node():
|
|||
FilterCondition(key="type", comparison_operator="in", value=[FileType.IMAGE, FileType.DOCUMENT])
|
||||
],
|
||||
),
|
||||
"order_by": Order(enabled=False, value="asc"),
|
||||
"order_by": OrderByConfig(enabled=False, value=Order.ASC),
|
||||
"limit": Limit(enabled=False, size=0),
|
||||
"extract_by": ExtractConfig(enabled=False, serial="1"),
|
||||
"title": "Test Title",
|
||||
|
|
|
|||
Loading…
Reference in New Issue