瀏覽代碼

feat: support bool type variable frontend (#24437)

Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
tags/1.8.0
Joel 2 月之前
父節點
當前提交
dac72b078d
沒有連結到貢獻者的電子郵件帳戶。
共有 100 個檔案被更改,包括 3459 行新增456 行删除
  1. 11
    0
      api/child_class.py
  2. 23
    2
      api/core/app/app_config/easy_ui_based_app/variables/manager.py
  3. 1
    0
      api/core/app/app_config/entities.py
  4. 22
    12
      api/core/app/apps/base_app_generator.py
  5. 12
    0
      api/core/variables/segments.py
  6. 56
    3
      api/core/variables/types.py
  7. 12
    0
      api/core/variables/variables.py
  8. 67
    10
      api/core/workflow/nodes/code/code_node.py
  9. 23
    3
      api/core/workflow/nodes/code/entities.py
  10. 31
    24
      api/core/workflow/nodes/list_operator/entities.py
  11. 62
    41
      api/core/workflow/nodes/list_operator/node.py
  12. 3
    3
      api/core/workflow/nodes/llm/node.py
  13. 2
    0
      api/core/workflow/nodes/loop/entities.py
  14. 18
    9
      api/core/workflow/nodes/loop/loop_node.py
  15. 60
    19
      api/core/workflow/nodes/parameter_extractor/entities.py
  16. 25
    0
      api/core/workflow/nodes/parameter_extractor/exc.py
  17. 79
    80
      api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py
  18. 5
    2
      api/core/workflow/nodes/variable_assigner/v1/node.py
  19. 2
    0
      api/core/workflow/nodes/variable_assigner/v2/constants.py
  20. 11
    17
      api/core/workflow/nodes/variable_assigner/v2/helpers.py
  21. 1
    1
      api/core/workflow/utils/condition/entities.py
  22. 46
    19
      api/core/workflow/utils/condition/processor.py
  23. 26
    8
      api/factories/variable_factory.py
  24. 11
    0
      api/lazy_load_class.py
  25. 3
    0
      api/mypy.ini
  26. 2
    0
      api/tests/unit_tests/core/variables/test_segment_type.py
  27. 729
    0
      api/tests/unit_tests/core/variables/test_segment_type_validation.py
  28. 0
    0
      api/tests/unit_tests/core/workflow/nodes/parameter_extractor/__init__.py
  29. 27
    0
      api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_entities.py
  30. 567
    0
      api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py
  31. 219
    0
      api/tests/unit_tests/core/workflow/nodes/test_if_else.py
  32. 3
    2
      api/tests/unit_tests/core/workflow/nodes/test_list_operator.py
  33. 39
    10
      api/tests/unit_tests/factories/test_variable_factory.py
  34. 47
    0
      simple_boolean_test.py
  35. 118
    0
      test_boolean_conditions.py
  36. 67
    0
      test_boolean_contains_fix.py
  37. 99
    0
      test_boolean_factory.py
  38. 230
    0
      test_boolean_variable_assigner.py
  39. 24
    0
      web/app/components/app/configuration/config-var/config-modal/config.ts
  40. 8
    1
      web/app/components/app/configuration/config-var/config-modal/field.tsx
  41. 104
    40
      web/app/components/app/configuration/config-var/config-modal/index.tsx
  42. 97
    0
      web/app/components/app/configuration/config-var/config-modal/type-select.tsx
  43. 25
    3
      web/app/components/app/configuration/config-var/index.tsx
  44. 1
    0
      web/app/components/app/configuration/config-var/select-var-type.tsx
  45. 2
    0
      web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
  46. 12
    1
      web/app/components/app/configuration/debug/chat-user-input.tsx
  47. 2
    2
      web/app/components/app/configuration/debug/index.tsx
  48. 1
    1
      web/app/components/app/configuration/index.tsx
  49. 16
    5
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  50. 3
    2
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  51. 16
    1
      web/app/components/base/chat/chat-with-history/hooks.tsx
  52. 31
    6
      web/app/components/base/chat/chat-with-history/inputs-form/content.tsx
  53. 1
    1
      web/app/components/base/chat/chat/check-input-forms-hooks.ts
  54. 6
    0
      web/app/components/base/chat/chat/utils.ts
  55. 1
    1
      web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
  56. 15
    1
      web/app/components/base/chat/embedded-chatbot/hooks.tsx
  57. 25
    0
      web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx
  58. 1
    1
      web/app/components/base/form/types.ts
  59. 0
    1
      web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx
  60. 0
    1
      web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx
  61. 0
    1
      web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx
  62. 14
    0
      web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx
  63. 2
    0
      web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx
  64. 5
    2
      web/app/components/share/text-generation/result/index.tsx
  65. 27
    2
      web/app/components/share/text-generation/run-once/index.tsx
  66. 2
    0
      web/app/components/tools/utils/to-form-schema.ts
  67. 38
    0
      web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx
  68. 24
    1
      web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
  69. 3
    1
      web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx
  70. 1
    1
      web/app/components/workflow/nodes/_base/components/form-input-item.tsx
  71. 3
    1
      web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx
  72. 22
    3
      web/app/components/workflow/nodes/_base/components/variable/utils.ts
  73. 1
    1
      web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx
  74. 1
    1
      web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
  75. 1
    3
      web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx
  76. 1
    2
      web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts
  77. 29
    28
      web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts
  78. 0
    1
      web/app/components/workflow/nodes/_base/hooks/use-toggle-expend.ts
  79. 18
    6
      web/app/components/workflow/nodes/assigner/components/var-list/index.tsx
  80. 1
    1
      web/app/components/workflow/nodes/assigner/default.ts
  81. 1
    1
      web/app/components/workflow/nodes/assigner/utils.ts
  82. 2
    3
      web/app/components/workflow/nodes/code/use-config.ts
  83. 28
    5
      web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx
  84. 4
    1
      web/app/components/workflow/nodes/if-else/components/condition-value.tsx
  85. 3
    3
      web/app/components/workflow/nodes/if-else/default.ts
  86. 4
    7
      web/app/components/workflow/nodes/if-else/node.tsx
  87. 1
    1
      web/app/components/workflow/nodes/if-else/types.ts
  88. 1
    1
      web/app/components/workflow/nodes/if-else/use-config.ts
  89. 6
    0
      web/app/components/workflow/nodes/if-else/utils.ts
  90. 1
    1
      web/app/components/workflow/nodes/iteration/use-config.ts
  91. 10
    2
      web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx
  92. 2
    2
      web/app/components/workflow/nodes/list-operator/default.ts
  93. 1
    1
      web/app/components/workflow/nodes/list-operator/types.ts
  94. 8
    3
      web/app/components/workflow/nodes/list-operator/use-config.ts
  95. 0
    3
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx
  96. 2
    4
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx
  97. 1
    0
      web/app/components/workflow/nodes/llm/utils.ts
  98. 13
    4
      web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx
  99. 28
    26
      web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx
  100. 0
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx

+ 11
- 0
api/child_class.py 查看文件

from tests.integration_tests.utils.parent_class import ParentClass


class ChildClass(ParentClass):
"""Test child class for module import helper tests"""

def __init__(self, name):
super().__init__(name)

def get_name(self):
return f"Child: {self.name}"

+ 23
- 2
api/core/app/app_config/easy_ui_based_app/variables/manager.py 查看文件

from core.app.app_config.entities import ExternalDataVariableEntity, VariableEntity, VariableEntityType from core.app.app_config.entities import ExternalDataVariableEntity, VariableEntity, VariableEntityType
from core.external_data_tool.factory import ExternalDataToolFactory from core.external_data_tool.factory import ExternalDataToolFactory


_ALLOWED_VARIABLE_ENTITY_TYPE = frozenset(
[
VariableEntityType.TEXT_INPUT,
VariableEntityType.SELECT,
VariableEntityType.PARAGRAPH,
VariableEntityType.NUMBER,
VariableEntityType.EXTERNAL_DATA_TOOL,
VariableEntityType.CHECKBOX,
]
)



class BasicVariablesConfigManager: class BasicVariablesConfigManager:
@classmethod @classmethod
VariableEntityType.PARAGRAPH, VariableEntityType.PARAGRAPH,
VariableEntityType.NUMBER, VariableEntityType.NUMBER,
VariableEntityType.SELECT, VariableEntityType.SELECT,
VariableEntityType.CHECKBOX,
}: }:
variable = variables[variable_type] variable = variables[variable_type]
variable_entities.append( variable_entities.append(
variables = [] variables = []
for item in config["user_input_form"]: for item in config["user_input_form"]:
key = list(item.keys())[0] key = list(item.keys())[0]
if key not in {"text-input", "select", "paragraph", "number", "external_data_tool"}:
raise ValueError("Keys in user_input_form list can only be 'text-input', 'paragraph' or 'select'")
# if key not in {"text-input", "select", "paragraph", "number", "external_data_tool"}:
if key not in {
VariableEntityType.TEXT_INPUT,
VariableEntityType.SELECT,
VariableEntityType.PARAGRAPH,
VariableEntityType.NUMBER,
VariableEntityType.EXTERNAL_DATA_TOOL,
VariableEntityType.CHECKBOX,
}:
allowed_keys = ", ".join(i.value for i in _ALLOWED_VARIABLE_ENTITY_TYPE)
raise ValueError(f"Keys in user_input_form list can only be {allowed_keys}")


form_item = item[key] form_item = item[key]
if "label" not in form_item: if "label" not in form_item:

+ 1
- 0
api/core/app/app_config/entities.py 查看文件

EXTERNAL_DATA_TOOL = "external_data_tool" EXTERNAL_DATA_TOOL = "external_data_tool"
FILE = "file" FILE = "file"
FILE_LIST = "file-list" FILE_LIST = "file-list"
CHECKBOX = "checkbox"




class VariableEntity(BaseModel): class VariableEntity(BaseModel):

+ 22
- 12
api/core/app/apps/base_app_generator.py 查看文件

f"(type '{variable_entity.type}') {variable_entity.variable} in input form must be a string" 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: match variable_entity.type:
case VariableEntityType.SELECT: case VariableEntityType.SELECT:
raise ValueError( raise ValueError(
f"{variable_entity.variable} in input form must be less than {variable_entity.max_length} files" f"{variable_entity.variable} in input form must be less than {variable_entity.max_length} files"
) )
case VariableEntityType.CHECKBOX:
if not isinstance(value, bool):
raise ValueError(f"{variable_entity.variable} in input form must be a valid boolean value")
case _:
raise AssertionError("this statement should be unreachable.")


return value return value



+ 12
- 0
api/core/variables/segments.py 查看文件

return "" return ""




class BooleanSegment(Segment):
value_type: SegmentType = SegmentType.BOOLEAN
value: bool


class ArrayAnySegment(ArraySegment): class ArrayAnySegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_ANY value_type: SegmentType = SegmentType.ARRAY_ANY
value: Sequence[Any] value: Sequence[Any]
return "" return ""




class ArrayBooleanSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_BOOLEAN
value: Sequence[bool]


def get_segment_discriminator(v: Any) -> SegmentType | None: def get_segment_discriminator(v: Any) -> SegmentType | None:
if isinstance(v, Segment): if isinstance(v, Segment):
return v.value_type return v.value_type
| Annotated[IntegerSegment, Tag(SegmentType.INTEGER)] | Annotated[IntegerSegment, Tag(SegmentType.INTEGER)]
| Annotated[ObjectSegment, Tag(SegmentType.OBJECT)] | Annotated[ObjectSegment, Tag(SegmentType.OBJECT)]
| Annotated[FileSegment, Tag(SegmentType.FILE)] | Annotated[FileSegment, Tag(SegmentType.FILE)]
| Annotated[BooleanSegment, Tag(SegmentType.BOOLEAN)]
| Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)] | Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)]
| Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)] | Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)]
| Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)] | Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)]
| Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)] | Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)]
| Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)] | Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)]
| Annotated[ArrayBooleanSegment, Tag(SegmentType.ARRAY_BOOLEAN)]
), ),
Discriminator(get_segment_discriminator), Discriminator(get_segment_discriminator),
] ]

+ 56
- 3
api/core/variables/types.py 查看文件





class ArrayValidation(StrEnum): class ArrayValidation(StrEnum):
"""Strategy for validating array elements"""
"""Strategy for validating array elements.

Note:
The `NONE` and `FIRST` strategies are primarily for compatibility purposes.
Avoid using them in new code whenever possible.
"""


# Skip element validation (only check array container) # Skip element validation (only check array container)
NONE = "none" NONE = "none"
SECRET = "secret" SECRET = "secret"


FILE = "file" FILE = "file"
BOOLEAN = "boolean"


ARRAY_ANY = "array[any]" ARRAY_ANY = "array[any]"
ARRAY_STRING = "array[string]" ARRAY_STRING = "array[string]"
ARRAY_NUMBER = "array[number]" ARRAY_NUMBER = "array[number]"
ARRAY_OBJECT = "array[object]" ARRAY_OBJECT = "array[object]"
ARRAY_FILE = "array[file]" ARRAY_FILE = "array[file]"
ARRAY_BOOLEAN = "array[boolean]"


NONE = "none" NONE = "none"


return SegmentType.ARRAY_FILE return SegmentType.ARRAY_FILE
case SegmentType.NONE: case SegmentType.NONE:
return SegmentType.ARRAY_ANY return SegmentType.ARRAY_ANY
case SegmentType.BOOLEAN:
return SegmentType.ARRAY_BOOLEAN
case _: case _:
# This should be unreachable. # This should be unreachable.
raise ValueError(f"not supported value {value}") raise ValueError(f"not supported value {value}")
if value is None: if value is None:
return SegmentType.NONE return SegmentType.NONE
elif isinstance(value, int) and not isinstance(value, bool):
# Important: The check for `bool` must precede the check for `int`,
# as `bool` is a subclass of `int` in Python's type hierarchy.
elif isinstance(value, bool):
return SegmentType.BOOLEAN
elif isinstance(value, int):
return SegmentType.INTEGER return SegmentType.INTEGER
elif isinstance(value, float): elif isinstance(value, float):
return SegmentType.FLOAT return SegmentType.FLOAT
else: else:
return all(element_type.is_valid(i, array_validation=ArrayValidation.NONE) for i in value) return all(element_type.is_valid(i, array_validation=ArrayValidation.NONE) for i in value)


def is_valid(self, value: Any, array_validation: ArrayValidation = ArrayValidation.FIRST) -> bool:
def is_valid(self, value: Any, array_validation: ArrayValidation = ArrayValidation.ALL) -> bool:
""" """
Check if a value matches the segment type. Check if a value matches the segment type.
Users of `SegmentType` should call this method, instead of using Users of `SegmentType` should call this method, instead of using
""" """
if self.is_array_type(): if self.is_array_type():
return self._validate_array(value, array_validation) return self._validate_array(value, array_validation)
# Important: The check for `bool` must precede the check for `int`,
# as `bool` is a subclass of `int` in Python's type hierarchy.
elif self == SegmentType.BOOLEAN:
return isinstance(value, bool)
elif self in [SegmentType.INTEGER, SegmentType.FLOAT, SegmentType.NUMBER]: elif self in [SegmentType.INTEGER, SegmentType.FLOAT, SegmentType.NUMBER]:
return isinstance(value, (int, float)) return isinstance(value, (int, float))
elif self == SegmentType.STRING: elif self == SegmentType.STRING:
else: else:
raise AssertionError("this statement should be unreachable.") raise AssertionError("this statement should be unreachable.")


@staticmethod
def cast_value(value: Any, type_: "SegmentType") -> Any:
# Cast Python's `bool` type to `int` when the runtime type requires
# an integer or number.
#
# This ensures compatibility with existing workflows that may use `bool` as
# `int`, since in Python's type system, `bool` is a subtype of `int`.
#
# This function exists solely to maintain compatibility with existing workflows.
# It should not be used to compromise the integrity of the runtime type system.
# No additional casting rules should be introduced to this function.

if type_ in (
SegmentType.INTEGER,
SegmentType.NUMBER,
) and isinstance(value, bool):
return int(value)
if type_ == SegmentType.ARRAY_NUMBER and all(isinstance(i, bool) for i in value):
return [int(i) for i in value]
return value

def exposed_type(self) -> "SegmentType": def exposed_type(self) -> "SegmentType":
"""Returns the type exposed to the frontend. """Returns the type exposed to the frontend.


return SegmentType.NUMBER return SegmentType.NUMBER
return self return self


def element_type(self) -> "SegmentType | None":
"""Return the element type of the current segment type, or `None` if the element type is undefined.

Raises:
ValueError: If the current segment type is not an array type.

Note:
For certain array types, such as `SegmentType.ARRAY_ANY`, their element types are not defined
by the runtime system. In such cases, this method will return `None`.
"""
if not self.is_array_type():
raise ValueError(f"element_type is only supported by array type, got {self}")
return _ARRAY_ELEMENT_TYPES_MAPPING.get(self)



_ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = { _ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = {
# ARRAY_ANY does not have corresponding element type. # ARRAY_ANY does not have corresponding element type.
SegmentType.ARRAY_NUMBER: SegmentType.NUMBER, SegmentType.ARRAY_NUMBER: SegmentType.NUMBER,
SegmentType.ARRAY_OBJECT: SegmentType.OBJECT, SegmentType.ARRAY_OBJECT: SegmentType.OBJECT,
SegmentType.ARRAY_FILE: SegmentType.FILE, SegmentType.ARRAY_FILE: SegmentType.FILE,
SegmentType.ARRAY_BOOLEAN: SegmentType.BOOLEAN,
} }


_ARRAY_TYPES = frozenset( _ARRAY_TYPES = frozenset(

+ 12
- 0
api/core/variables/variables.py 查看文件



from .segments import ( from .segments import (
ArrayAnySegment, ArrayAnySegment,
ArrayBooleanSegment,
ArrayFileSegment, ArrayFileSegment,
ArrayNumberSegment, ArrayNumberSegment,
ArrayObjectSegment, ArrayObjectSegment,
ArraySegment, ArraySegment,
ArrayStringSegment, ArrayStringSegment,
BooleanSegment,
FileSegment, FileSegment,
FloatSegment, FloatSegment,
IntegerSegment, IntegerSegment,
pass pass




class BooleanVariable(BooleanSegment, Variable):
pass


class ArrayFileVariable(ArrayFileSegment, ArrayVariable): class ArrayFileVariable(ArrayFileSegment, ArrayVariable):
pass pass




class ArrayBooleanVariable(ArrayBooleanSegment, ArrayVariable):
pass


# The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic. # The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic.
# Use `Variable` for type hinting when serialization is not required. # Use `Variable` for type hinting when serialization is not required.
# #
| Annotated[IntegerVariable, Tag(SegmentType.INTEGER)] | Annotated[IntegerVariable, Tag(SegmentType.INTEGER)]
| Annotated[ObjectVariable, Tag(SegmentType.OBJECT)] | Annotated[ObjectVariable, Tag(SegmentType.OBJECT)]
| Annotated[FileVariable, Tag(SegmentType.FILE)] | Annotated[FileVariable, Tag(SegmentType.FILE)]
| Annotated[BooleanVariable, Tag(SegmentType.BOOLEAN)]
| Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)] | Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)]
| Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)] | Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)]
| Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)] | Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)]
| Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)] | Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)]
| Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)] | Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)]
| Annotated[ArrayBooleanVariable, Tag(SegmentType.ARRAY_BOOLEAN)]
| Annotated[SecretVariable, Tag(SegmentType.SECRET)] | Annotated[SecretVariable, Tag(SegmentType.SECRET)]
), ),
Discriminator(get_segment_discriminator), Discriminator(get_segment_discriminator),

+ 67
- 10
api/core/workflow/nodes/code/code_node.py 查看文件

from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider
from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider
from core.variables.segments import ArrayFileSegment from core.variables.segments import ArrayFileSegment
from core.variables.types import SegmentType
from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.nodes.base import BaseNode from core.workflow.nodes.base import BaseNode


return value.replace("\x00", "") return value.replace("\x00", "")


def _check_boolean(self, value: bool | None, variable: str) -> bool | None:
if value is None:
return None
if not isinstance(value, bool):
raise OutputValidationError(f"Output variable `{variable}` must be a boolean")

return value

def _check_number(self, value: int | float | None, variable: str) -> int | float | None: def _check_number(self, value: int | float | None, variable: str) -> int | float | None:
""" """
Check number Check number
prefix=f"{prefix}.{output_name}" if prefix else output_name, prefix=f"{prefix}.{output_name}" if prefix else output_name,
depth=depth + 1, depth=depth + 1,
) )
elif isinstance(output_value, bool):
self._check_boolean(output_value, variable=f"{prefix}.{output_name}" if prefix else output_name)
elif isinstance(output_value, int | float): elif isinstance(output_value, int | float):
self._check_number( self._check_number(
value=output_value, variable=f"{prefix}.{output_name}" if prefix else output_name value=output_value, variable=f"{prefix}.{output_name}" if prefix else output_name
if output_name not in result: if output_name not in result:
raise OutputValidationError(f"Output {prefix}{dot}{output_name} is missing.") raise OutputValidationError(f"Output {prefix}{dot}{output_name} is missing.")


if output_config.type == "object":
if output_config.type == SegmentType.OBJECT:
# check if output is object # check if output is object
if not isinstance(result.get(output_name), dict): if not isinstance(result.get(output_name), dict):
if result[output_name] is None: if result[output_name] is None:
prefix=f"{prefix}.{output_name}", prefix=f"{prefix}.{output_name}",
depth=depth + 1, depth=depth + 1,
) )
elif output_config.type == "number":
elif output_config.type == SegmentType.NUMBER:
# check if number available # check if number available
transformed_result[output_name] = self._check_number(
value=result[output_name], variable=f"{prefix}{dot}{output_name}"
)
elif output_config.type == "string":
checked = self._check_number(value=result[output_name], variable=f"{prefix}{dot}{output_name}")
# If the output is a boolean and the output schema specifies a NUMBER type,
# convert the boolean value to an integer.
#
# This ensures compatibility with existing workflows that may use
# `True` and `False` as values for NUMBER type outputs.
transformed_result[output_name] = self._convert_boolean_to_int(checked)

elif output_config.type == SegmentType.STRING:
# check if string available # check if string available
transformed_result[output_name] = self._check_string( transformed_result[output_name] = self._check_string(
value=result[output_name], value=result[output_name],
variable=f"{prefix}{dot}{output_name}", variable=f"{prefix}{dot}{output_name}",
) )
elif output_config.type == "array[number]":
elif output_config.type == SegmentType.BOOLEAN:
transformed_result[output_name] = self._check_boolean(
value=result[output_name],
variable=f"{prefix}{dot}{output_name}",
)
elif output_config.type == SegmentType.ARRAY_NUMBER:
# check if array of number available # check if array of number available
if not isinstance(result[output_name], list): if not isinstance(result[output_name], list):
if result[output_name] is None: if result[output_name] is None:
) )


transformed_result[output_name] = [ transformed_result[output_name] = [
self._check_number(value=value, variable=f"{prefix}{dot}{output_name}[{i}]")
# If the element is a boolean and the output schema specifies a `array[number]` type,
# convert the boolean value to an integer.
#
# This ensures compatibility with existing workflows that may use
# `True` and `False` as values for NUMBER type outputs.
self._convert_boolean_to_int(
self._check_number(value=value, variable=f"{prefix}{dot}{output_name}[{i}]"),
)
for i, value in enumerate(result[output_name]) for i, value in enumerate(result[output_name])
] ]
elif output_config.type == "array[string]":
elif output_config.type == SegmentType.ARRAY_STRING:
# check if array of string available # check if array of string available
if not isinstance(result[output_name], list): if not isinstance(result[output_name], list):
if result[output_name] is None: if result[output_name] is None:
self._check_string(value=value, variable=f"{prefix}{dot}{output_name}[{i}]") self._check_string(value=value, variable=f"{prefix}{dot}{output_name}[{i}]")
for i, value in enumerate(result[output_name]) for i, value in enumerate(result[output_name])
] ]
elif output_config.type == "array[object]":
elif output_config.type == SegmentType.ARRAY_OBJECT:
# check if array of object available # check if array of object available
if not isinstance(result[output_name], list): if not isinstance(result[output_name], list):
if result[output_name] is None: if result[output_name] is None:
) )
for i, value in enumerate(result[output_name]) for i, value in enumerate(result[output_name])
] ]
elif output_config.type == SegmentType.ARRAY_BOOLEAN:
# check if array of object available
if not isinstance(result[output_name], list):
if result[output_name] is None:
transformed_result[output_name] = None
else:
raise OutputValidationError(
f"Output {prefix}{dot}{output_name} is not an array,"
f" got {type(result.get(output_name))} instead."
)
else:
transformed_result[output_name] = [
self._check_boolean(value=value, variable=f"{prefix}{dot}{output_name}[{i}]")
for i, value in enumerate(result[output_name])
]

else: else:
raise OutputValidationError(f"Output type {output_config.type} is not supported.") raise OutputValidationError(f"Output type {output_config.type} is not supported.")


@property @property
def retry(self) -> bool: def retry(self) -> bool:
return self._node_data.retry_config.retry_enabled return self._node_data.retry_config.retry_enabled

@staticmethod
def _convert_boolean_to_int(value: bool | int | float | None) -> int | float | None:
"""This function convert boolean to integers when the output schema specifies a NUMBER type.

This ensures compatibility with existing workflows that may use
`True` and `False` as values for NUMBER type outputs.
"""
if value is None:
return None
if isinstance(value, bool):
return int(value)
return value

+ 23
- 3
api/core/workflow/nodes/code/entities.py 查看文件

from typing import Literal, Optional
from typing import Annotated, Literal, Optional


from pydantic import BaseModel
from pydantic import AfterValidator, BaseModel


from core.helper.code_executor.code_executor import CodeLanguage from core.helper.code_executor.code_executor import CodeLanguage
from core.variables.types import SegmentType
from core.workflow.entities.variable_entities import VariableSelector from core.workflow.entities.variable_entities import VariableSelector
from core.workflow.nodes.base import BaseNodeData from core.workflow.nodes.base import BaseNodeData


_ALLOWED_OUTPUT_FROM_CODE = frozenset(
[
SegmentType.STRING,
SegmentType.NUMBER,
SegmentType.OBJECT,
SegmentType.BOOLEAN,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_BOOLEAN,
]
)


def _validate_type(segment_type: SegmentType) -> SegmentType:
if segment_type not in _ALLOWED_OUTPUT_FROM_CODE:
raise ValueError(f"invalid type for code output, expected {_ALLOWED_OUTPUT_FROM_CODE}, actual {segment_type}")
return segment_type



class CodeNodeData(BaseNodeData): class CodeNodeData(BaseNodeData):
""" """
""" """


class Output(BaseModel): class Output(BaseModel):
type: Literal["string", "number", "object", "array[string]", "array[number]", "array[object]"]
type: Annotated[SegmentType, AfterValidator(_validate_type)]
children: Optional[dict[str, "CodeNodeData.Output"]] = None children: Optional[dict[str, "CodeNodeData.Output"]] = None


class Dependency(BaseModel): class Dependency(BaseModel):

+ 31
- 24
api/core/workflow/nodes/list_operator/entities.py 查看文件

from collections.abc import Sequence from collections.abc import Sequence
from typing import Literal
from enum import StrEnum


from pydantic import BaseModel, Field from pydantic import BaseModel, Field


from core.workflow.nodes.base import BaseNodeData from core.workflow.nodes.base import BaseNodeData


_Condition = Literal[

class FilterOperator(StrEnum):
# string conditions # string conditions
"contains",
"start with",
"end with",
"is",
"in",
"empty",
"not contains",
"is not",
"not in",
"not empty",
CONTAINS = "contains"
START_WITH = "start with"
END_WITH = "end with"
IS = "is"
IN = "in"
EMPTY = "empty"
NOT_CONTAINS = "not contains"
IS_NOT = "is not"
NOT_IN = "not in"
NOT_EMPTY = "not empty"
# number conditions # number conditions
"=",
"≠",
"<",
">",
"≥",
"≤",
]
EQUAL = "="
NOT_EQUAL = "≠"
LESS_THAN = "<"
GREATER_THAN = ">"
GREATER_THAN_OR_EQUAL = "≥"
LESS_THAN_OR_EQUAL = "≤"


class Order(StrEnum):
ASC = "asc"
DESC = "desc"




class FilterCondition(BaseModel): class FilterCondition(BaseModel):
key: str = "" key: str = ""
comparison_operator: _Condition = "contains"
value: str | Sequence[str] = ""
comparison_operator: FilterOperator = FilterOperator.CONTAINS
# the value is bool if the filter operator is comparing with
# a boolean constant.
value: str | Sequence[str] | bool = ""




class FilterBy(BaseModel): class FilterBy(BaseModel):
conditions: Sequence[FilterCondition] = Field(default_factory=list) conditions: Sequence[FilterCondition] = Field(default_factory=list)




class OrderBy(BaseModel):
class OrderByConfig(BaseModel):
enabled: bool = False enabled: bool = False
key: str = "" key: str = ""
value: Literal["asc", "desc"] = "asc"
value: Order = Order.ASC




class Limit(BaseModel): class Limit(BaseModel):
class ListOperatorNodeData(BaseNodeData): class ListOperatorNodeData(BaseNodeData):
variable: Sequence[str] = Field(default_factory=list) variable: Sequence[str] = Field(default_factory=list)
filter_by: FilterBy filter_by: FilterBy
order_by: OrderBy
order_by: OrderByConfig
limit: Limit limit: Limit
extract_by: ExtractConfig = Field(default_factory=ExtractConfig) extract_by: ExtractConfig = Field(default_factory=ExtractConfig)

+ 62
- 41
api/core/workflow/nodes/list_operator/node.py 查看文件

from collections.abc import Callable, Mapping, Sequence from collections.abc import Callable, Mapping, Sequence
from typing import Any, Literal, Optional, Union
from typing import Any, Optional, TypeAlias, TypeVar


from core.file import File from core.file import File
from core.variables import ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment from core.variables import ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment
from core.variables.segments import ArrayAnySegment, ArraySegment
from core.variables.segments import ArrayAnySegment, ArrayBooleanSegment, ArraySegment
from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
from core.workflow.nodes.base import BaseNode from core.workflow.nodes.base import BaseNode
from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig
from core.workflow.nodes.enums import ErrorStrategy, NodeType from core.workflow.nodes.enums import ErrorStrategy, NodeType


from .entities import ListOperatorNodeData
from .entities import FilterOperator, ListOperatorNodeData, Order
from .exc import InvalidConditionError, InvalidFilterValueError, InvalidKeyError, ListOperatorError from .exc import InvalidConditionError, InvalidFilterValueError, InvalidKeyError, ListOperatorError


_SUPPORTED_TYPES_TUPLE = (
ArrayFileSegment,
ArrayNumberSegment,
ArrayStringSegment,
ArrayBooleanSegment,
)
_SUPPORTED_TYPES_ALIAS: TypeAlias = ArrayFileSegment | ArrayNumberSegment | ArrayStringSegment | ArrayBooleanSegment


_T = TypeVar("_T")


def _negation(filter_: Callable[[_T], bool]) -> Callable[[_T], bool]:
"""Returns the negation of a given filter function. If the original filter
returns `True` for a value, the negated filter will return `False`, and vice versa.
"""

def wrapper(value: _T) -> bool:
return not filter_(value)

return wrapper



class ListOperatorNode(BaseNode): class ListOperatorNode(BaseNode):
_node_type = NodeType.LIST_OPERATOR _node_type = NodeType.LIST_OPERATOR
process_data=process_data, process_data=process_data,
outputs=outputs, outputs=outputs,
) )
if not isinstance(variable, ArrayFileSegment | ArrayNumberSegment | ArrayStringSegment):
error_message = (
f"Variable {self._node_data.variable} is not an ArrayFileSegment, ArrayNumberSegment "
"or ArrayStringSegment"
)
if not isinstance(variable, _SUPPORTED_TYPES_TUPLE):
error_message = f"Variable {self._node_data.variable} is not an array type, actual type: {type(variable)}"
return NodeRunResult( return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED, error=error_message, inputs=inputs, outputs=outputs status=WorkflowNodeExecutionStatus.FAILED, error=error_message, inputs=inputs, outputs=outputs
) )
outputs=outputs, outputs=outputs,
) )


def _apply_filter(
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
def _apply_filter(self, variable: _SUPPORTED_TYPES_ALIAS) -> _SUPPORTED_TYPES_ALIAS:
filter_func: Callable[[Any], bool] filter_func: Callable[[Any], bool]
result: list[Any] = [] result: list[Any] = []
for condition in self._node_data.filter_by.conditions: for condition in self._node_data.filter_by.conditions:
) )
result = list(filter(filter_func, variable.value)) result = list(filter(filter_func, variable.value))
variable = variable.model_copy(update={"value": result}) variable = variable.model_copy(update={"value": result})
elif isinstance(variable, ArrayBooleanSegment):
if not isinstance(condition.value, bool):
raise InvalidFilterValueError(f"Invalid filter value: {condition.value}")
filter_func = _get_boolean_filter_func(condition=condition.comparison_operator, value=condition.value)
result = list(filter(filter_func, variable.value))
variable = variable.model_copy(update={"value": result})
else:
raise AssertionError("this statment should be unreachable.")
return variable return variable


def _apply_order(
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
if isinstance(variable, ArrayStringSegment):
result = _order_string(order=self._node_data.order_by.value, array=variable.value)
variable = variable.model_copy(update={"value": result})
elif isinstance(variable, ArrayNumberSegment):
result = _order_number(order=self._node_data.order_by.value, array=variable.value)
def _apply_order(self, variable: _SUPPORTED_TYPES_ALIAS) -> _SUPPORTED_TYPES_ALIAS:
if isinstance(variable, (ArrayStringSegment, ArrayNumberSegment, ArrayBooleanSegment)):
result = sorted(variable.value, reverse=self._node_data.order_by == Order.DESC)
variable = variable.model_copy(update={"value": result}) variable = variable.model_copy(update={"value": result})
elif isinstance(variable, ArrayFileSegment): elif isinstance(variable, ArrayFileSegment):
result = _order_file( result = _order_file(
order=self._node_data.order_by.value, order_by=self._node_data.order_by.key, array=variable.value order=self._node_data.order_by.value, order_by=self._node_data.order_by.key, array=variable.value
) )
variable = variable.model_copy(update={"value": result}) variable = variable.model_copy(update={"value": result})
else:
raise AssertionError("this statement should be unreachable")

return variable return variable


def _apply_slice(
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
def _apply_slice(self, variable: _SUPPORTED_TYPES_ALIAS) -> _SUPPORTED_TYPES_ALIAS:
result = variable.value[: self._node_data.limit.size] result = variable.value[: self._node_data.limit.size]
return variable.model_copy(update={"value": result}) return variable.model_copy(update={"value": result})


def _extract_slice(
self, variable: Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]
) -> Union[ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment]:
def _extract_slice(self, variable: _SUPPORTED_TYPES_ALIAS) -> _SUPPORTED_TYPES_ALIAS:
value = int(self.graph_runtime_state.variable_pool.convert_template(self._node_data.extract_by.serial).text) value = int(self.graph_runtime_state.variable_pool.convert_template(self._node_data.extract_by.serial).text)
if value < 1: if value < 1:
raise ValueError(f"Invalid serial index: must be >= 1, got {value}") raise ValueError(f"Invalid serial index: must be >= 1, got {value}")
case "empty": case "empty":
return lambda x: x == "" return lambda x: x == ""
case "not contains": case "not contains":
return lambda x: not _contains(value)(x)
return _negation(_contains(value))
case "is not": case "is not":
return lambda x: not _is(value)(x)
return _negation(_is(value))
case "not in": case "not in":
return lambda x: not _in(value)(x)
return _negation(_in(value))
case "not empty": case "not empty":
return lambda x: x != "" return lambda x: x != ""
case _: case _:
case "in": case "in":
return _in(value) return _in(value)
case "not in": case "not in":
return lambda x: not _in(value)(x)
return _negation(_in(value))
case _: case _:
raise InvalidConditionError(f"Invalid condition: {condition}") raise InvalidConditionError(f"Invalid condition: {condition}")


raise InvalidConditionError(f"Invalid condition: {condition}") raise InvalidConditionError(f"Invalid condition: {condition}")




def _get_boolean_filter_func(*, condition: FilterOperator, value: bool) -> Callable[[bool], bool]:
match condition:
case FilterOperator.IS:
return _is(value)
case FilterOperator.IS_NOT:
return _negation(_is(value))
case _:
raise InvalidConditionError(f"Invalid condition: {condition}")


def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str]) -> Callable[[File], bool]: def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str]) -> Callable[[File], bool]:
extract_func: Callable[[File], Any] extract_func: Callable[[File], Any]
if key in {"name", "extension", "mime_type", "url"} and isinstance(value, str): if key in {"name", "extension", "mime_type", "url"} and isinstance(value, str):
return lambda x: x.endswith(value) return lambda x: x.endswith(value)




def _is(value: str) -> Callable[[str], bool]:
def _is(value: _T) -> Callable[[_T], bool]:
return lambda x: x == value return lambda x: x == value




return lambda x: x >= value return lambda x: x >= value




def _order_number(*, order: Literal["asc", "desc"], array: Sequence[int | float]):
return sorted(array, key=lambda x: x, reverse=order == "desc")


def _order_string(*, order: Literal["asc", "desc"], array: Sequence[str]):
return sorted(array, key=lambda x: x, reverse=order == "desc")


def _order_file(*, order: Literal["asc", "desc"], order_by: str = "", array: Sequence[File]):
def _order_file(*, order: Order, order_by: str = "", array: Sequence[File]):
extract_func: Callable[[File], Any] extract_func: Callable[[File], Any]
if order_by in {"name", "type", "extension", "mime_type", "transfer_method", "url"}: if order_by in {"name", "type", "extension", "mime_type", "transfer_method", "url"}:
extract_func = _get_file_extract_string_func(key=order_by) extract_func = _get_file_extract_string_func(key=order_by)
return sorted(array, key=lambda x: extract_func(x), reverse=order == "desc")
return sorted(array, key=lambda x: extract_func(x), reverse=order == Order.DESC)
elif order_by == "size": elif order_by == "size":
extract_func = _get_file_extract_number_func(key=order_by) extract_func = _get_file_extract_number_func(key=order_by)
return sorted(array, key=lambda x: extract_func(x), reverse=order == "desc")
return sorted(array, key=lambda x: extract_func(x), reverse=order == Order.DESC)
else: else:
raise InvalidKeyError(f"Invalid order key: {order_by}") raise InvalidKeyError(f"Invalid order key: {order_by}")

+ 3
- 3
api/core/workflow/nodes/llm/node.py 查看文件

import json import json
import logging import logging
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Optional, Union


from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
from core.file import FileType, file_manager from core.file import FileType, file_manager
from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.variable_pool import VariablePool
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.graph_engine.entities.event import InNodeEvent
from core.workflow.nodes.base import BaseNode from core.workflow.nodes.base import BaseNode
from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig
from core.workflow.nodes.enums import ErrorStrategy, NodeType from core.workflow.nodes.enums import ErrorStrategy, NodeType
if TYPE_CHECKING: if TYPE_CHECKING:
from core.file.models import File from core.file.models import File
from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
from core.workflow.graph_engine.entities.event import InNodeEvent


logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)


def version(cls) -> str: def version(cls) -> str:
return "1" return "1"


def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]:
def _run(self) -> Generator[Union[NodeEvent, "InNodeEvent"], None, None]:
node_inputs: Optional[dict[str, Any]] = None node_inputs: Optional[dict[str, Any]] = None
process_data = None process_data = None
result_text = "" result_text = ""

+ 2
- 0
api/core/workflow/nodes/loop/entities.py 查看文件

SegmentType.STRING, SegmentType.STRING,
SegmentType.NUMBER, SegmentType.NUMBER,
SegmentType.OBJECT, SegmentType.OBJECT,
SegmentType.BOOLEAN,
SegmentType.ARRAY_STRING, SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER, SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT, SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_BOOLEAN,
] ]
) )



+ 18
- 9
api/core/workflow/nodes/loop/loop_node.py 查看文件

for node_id in loop_graph.node_ids: for node_id in loop_graph.node_ids:
variable_pool.remove([node_id]) variable_pool.remove([node_id])


_outputs = {}
_outputs: dict[str, Segment | int | None] = {}
for loop_variable_key, loop_variable_selector in loop_variable_selectors.items(): for loop_variable_key, loop_variable_selector in loop_variable_selectors.items():
_loop_variable_segment = variable_pool.get(loop_variable_selector) _loop_variable_segment = variable_pool.get(loop_variable_selector)
if _loop_variable_segment: if _loop_variable_segment:
_outputs[loop_variable_key] = _loop_variable_segment.value
_outputs[loop_variable_key] = _loop_variable_segment
else: else:
_outputs[loop_variable_key] = None _outputs[loop_variable_key] = None


return variable_mapping return variable_mapping


@staticmethod @staticmethod
def _get_segment_for_constant(var_type: SegmentType, value: Any) -> Segment:
def _get_segment_for_constant(var_type: SegmentType, original_value: Any) -> Segment:
"""Get the appropriate segment type for a constant value.""" """Get the appropriate segment type for a constant value."""
if var_type in ["array[string]", "array[number]", "array[object]"]:
if value and isinstance(value, str):
value = json.loads(value)
if var_type in [
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_STRING,
]:
if original_value and isinstance(original_value, str):
value = json.loads(original_value)
else: else:
logger.warning("unexpected value for LoopNode, value_type=%s, value=%s", original_value, var_type)
value = [] value = []
elif var_type == SegmentType.ARRAY_BOOLEAN:
value = original_value
else:
raise AssertionError("this statement should be unreachable.")
try: try:
return build_segment_with_type(var_type, value)
return build_segment_with_type(var_type, value=value)
except TypeMismatchError as type_exc: except TypeMismatchError as type_exc:
# Attempt to parse the value as a JSON-encoded string, if applicable. # Attempt to parse the value as a JSON-encoded string, if applicable.
if not isinstance(value, str):
if not isinstance(original_value, str):
raise raise
try: try:
value = json.loads(value)
value = json.loads(original_value)
except ValueError: except ValueError:
raise type_exc raise type_exc
return build_segment_with_type(var_type, value) return build_segment_with_type(var_type, value)

+ 60
- 19
api/core/workflow/nodes/parameter_extractor/entities.py 查看文件

from typing import Any, Literal, Optional
from typing import Annotated, Any, Literal, Optional


from pydantic import BaseModel, Field, field_validator
from pydantic import (
BaseModel,
BeforeValidator,
Field,
field_validator,
)


from core.prompt.entities.advanced_prompt_entities import MemoryConfig from core.prompt.entities.advanced_prompt_entities import MemoryConfig
from core.variables.types import SegmentType
from core.workflow.nodes.base import BaseNodeData from core.workflow.nodes.base import BaseNodeData
from core.workflow.nodes.llm import ModelConfig, VisionConfig
from core.workflow.nodes.llm.entities import ModelConfig, VisionConfig

_OLD_BOOL_TYPE_NAME = "bool"
_OLD_SELECT_TYPE_NAME = "select"

_VALID_PARAMETER_TYPES = frozenset(
[
SegmentType.STRING, # "string",
SegmentType.NUMBER, # "number",
SegmentType.BOOLEAN,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_BOOLEAN,
_OLD_BOOL_TYPE_NAME, # old boolean type used by Parameter Extractor node
_OLD_SELECT_TYPE_NAME, # string type with enumeration choices.
]
)


def _validate_type(parameter_type: str) -> SegmentType:
if not isinstance(parameter_type, str):
raise TypeError(f"type should be str, got {type(parameter_type)}, value={parameter_type}")
if parameter_type not in _VALID_PARAMETER_TYPES:
raise ValueError(f"type {parameter_type} is not allowd to use in Parameter Extractor node.")

if parameter_type == _OLD_BOOL_TYPE_NAME:
return SegmentType.BOOLEAN
elif parameter_type == _OLD_SELECT_TYPE_NAME:
return SegmentType.STRING
return SegmentType(parameter_type)




class _ParameterConfigError(Exception): class _ParameterConfigError(Exception):
""" """


name: str name: str
type: Literal["string", "number", "bool", "select", "array[string]", "array[number]", "array[object]"]
type: Annotated[SegmentType, BeforeValidator(_validate_type)]
options: Optional[list[str]] = None options: Optional[list[str]] = None
description: str description: str
required: bool required: bool
return str(value) return str(value)


def is_array_type(self) -> bool: def is_array_type(self) -> bool:
return self.type in ("array[string]", "array[number]", "array[object]")
return self.type.is_array_type()


def element_type(self) -> Literal["string", "number", "object"]:
if self.type == "array[number]":
return "number"
elif self.type == "array[string]":
return "string"
elif self.type == "array[object]":
return "object"
else:
raise _ParameterConfigError(f"{self.type} is not array type.")
def element_type(self) -> SegmentType:
"""Return the element type of the parameter.

Raises a ValueError if the parameter's type is not an array type.
"""
element_type = self.type.element_type()
# At this point, self.type is guaranteed to be one of `ARRAY_STRING`,
# `ARRAY_NUMBER`, `ARRAY_OBJECT`, or `ARRAY_BOOLEAN`.
#
# See: _VALID_PARAMETER_TYPES for reference.
assert element_type is not None, f"the element type should not be None, {self.type=}"
return element_type




class ParameterExtractorNodeData(BaseNodeData): class ParameterExtractorNodeData(BaseNodeData):
for parameter in self.parameters: for parameter in self.parameters:
parameter_schema: dict[str, Any] = {"description": parameter.description} parameter_schema: dict[str, Any] = {"description": parameter.description}


if parameter.type in {"string", "select"}:
if parameter.type == SegmentType.STRING:
parameter_schema["type"] = "string" parameter_schema["type"] = "string"
elif parameter.type.startswith("array"):
elif parameter.type.is_array_type():
parameter_schema["type"] = "array" parameter_schema["type"] = "array"
nested_type = parameter.type[6:-1]
parameter_schema["items"] = {"type": nested_type}
element_type = parameter.type.element_type()
if element_type is None:
raise AssertionError("element type should not be None.")
parameter_schema["items"] = {"type": element_type.value}
else: else:
parameter_schema["type"] = parameter.type parameter_schema["type"] = parameter.type


if parameter.type == "select":
if parameter.options:
parameter_schema["enum"] = parameter.options parameter_schema["enum"] = parameter.options


parameters["properties"][parameter.name] = parameter_schema parameters["properties"][parameter.name] = parameter_schema

+ 25
- 0
api/core/workflow/nodes/parameter_extractor/exc.py 查看文件

from typing import Any

from core.variables.types import SegmentType


class ParameterExtractorNodeError(ValueError): class ParameterExtractorNodeError(ValueError):
"""Base error for ParameterExtractorNode.""" """Base error for ParameterExtractorNode."""




class InvalidModelModeError(ParameterExtractorNodeError): class InvalidModelModeError(ParameterExtractorNodeError):
"""Raised when the model mode is invalid.""" """Raised when the model mode is invalid."""


class InvalidValueTypeError(ParameterExtractorNodeError):
def __init__(
self,
/,
parameter_name: str,
expected_type: SegmentType,
actual_type: SegmentType | None,
value: Any,
) -> None:
message = (
f"Invalid value for parameter {parameter_name}, expected segment type: {expected_type}, "
f"actual_type: {actual_type}, python_type: {type(value)}, value: {value}"
)
super().__init__(message)
self.parameter_name = parameter_name
self.expected_type = expected_type
self.actual_type = actual_type
self.value = value

+ 79
- 80
api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py 查看文件

from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate
from core.prompt.simple_prompt_transform import ModelMode from core.prompt.simple_prompt_transform import ModelMode
from core.prompt.utils.prompt_message_util import PromptMessageUtil from core.prompt.utils.prompt_message_util import PromptMessageUtil
from core.variables.types import SegmentType
from core.variables.types import ArrayValidation, SegmentType
from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.variable_pool import VariablePool
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus


from .entities import ParameterExtractorNodeData from .entities import ParameterExtractorNodeData
from .exc import ( from .exc import (
InvalidArrayValueError,
InvalidBoolValueError,
InvalidInvokeResultError, InvalidInvokeResultError,
InvalidModelModeError, InvalidModelModeError,
InvalidModelTypeError, InvalidModelTypeError,
InvalidNumberOfParametersError, InvalidNumberOfParametersError,
InvalidNumberValueError,
InvalidSelectValueError, InvalidSelectValueError,
InvalidStringValueError,
InvalidTextContentTypeError, InvalidTextContentTypeError,
InvalidValueTypeError,
ModelSchemaNotFoundError, ModelSchemaNotFoundError,
ParameterExtractorNodeError, ParameterExtractorNodeError,
RequiredParameterMissingError, RequiredParameterMissingError,
return prompt_messages return prompt_messages


def _validate_result(self, data: ParameterExtractorNodeData, result: dict) -> dict: def _validate_result(self, data: ParameterExtractorNodeData, result: dict) -> dict:
"""
Validate result.
"""
if len(data.parameters) != len(result): if len(data.parameters) != len(result):
raise InvalidNumberOfParametersError("Invalid number of parameters") raise InvalidNumberOfParametersError("Invalid number of parameters")


if parameter.required and parameter.name not in result: if parameter.required and parameter.name not in result:
raise RequiredParameterMissingError(f"Parameter {parameter.name} is required") raise RequiredParameterMissingError(f"Parameter {parameter.name} is required")


if parameter.type == "select" and parameter.options and result.get(parameter.name) not in parameter.options:
raise InvalidSelectValueError(f"Invalid `select` value for parameter {parameter.name}")

if parameter.type == "number" and not isinstance(result.get(parameter.name), int | float):
raise InvalidNumberValueError(f"Invalid `number` value for parameter {parameter.name}")

if parameter.type == "bool" and not isinstance(result.get(parameter.name), bool):
raise InvalidBoolValueError(f"Invalid `bool` value for parameter {parameter.name}")

if parameter.type == "string" and not isinstance(result.get(parameter.name), str):
raise InvalidStringValueError(f"Invalid `string` value for parameter {parameter.name}")

if parameter.type.startswith("array"):
parameters = result.get(parameter.name)
if not isinstance(parameters, list):
raise InvalidArrayValueError(f"Invalid `array` value for parameter {parameter.name}")
nested_type = parameter.type[6:-1]
for item in parameters:
if nested_type == "number" and not isinstance(item, int | float):
raise InvalidArrayValueError(f"Invalid `array[number]` value for parameter {parameter.name}")
if nested_type == "string" and not isinstance(item, str):
raise InvalidArrayValueError(f"Invalid `array[string]` value for parameter {parameter.name}")
if nested_type == "object" and not isinstance(item, dict):
raise InvalidArrayValueError(f"Invalid `array[object]` value for parameter {parameter.name}")
param_value = result.get(parameter.name)
if not parameter.type.is_valid(param_value, array_validation=ArrayValidation.ALL):
inferred_type = SegmentType.infer_segment_type(param_value)
raise InvalidValueTypeError(
parameter_name=parameter.name,
expected_type=parameter.type,
actual_type=inferred_type,
value=param_value,
)
if parameter.type == SegmentType.STRING and parameter.options:
if param_value not in parameter.options:
raise InvalidSelectValueError(f"Invalid `select` value for parameter {parameter.name}")
return result return result


@staticmethod
def _transform_number(value: int | float | str | bool) -> int | float | None:
"""
Attempts to transform the input into an integer or float.

Returns:
int or float: The transformed number if the conversion is successful.
None: If the transformation fails.

Note:
Boolean values `True` and `False` are converted to integers `1` and `0`, respectively.
This behavior ensures compatibility with existing workflows that may use boolean types as integers.
"""
if isinstance(value, bool):
return int(value)
elif isinstance(value, (int, float)):
return value
elif not isinstance(value, str):
return None
if "." in value:
try:
return float(value)
except ValueError:
return None
else:
try:
return int(value)
except ValueError:
return None

def _transform_result(self, data: ParameterExtractorNodeData, result: dict) -> dict: def _transform_result(self, data: ParameterExtractorNodeData, result: dict) -> dict:
""" """
Transform result into standard format. Transform result into standard format.
""" """
transformed_result = {}
transformed_result: dict[str, Any] = {}
for parameter in data.parameters: for parameter in data.parameters:
if parameter.name in result: if parameter.name in result:
param_value = result[parameter.name]
# transform value # transform value
if parameter.type == "number":
if isinstance(result[parameter.name], int | float):
transformed_result[parameter.name] = result[parameter.name]
elif isinstance(result[parameter.name], str):
try:
if "." in result[parameter.name]:
result[parameter.name] = float(result[parameter.name])
else:
result[parameter.name] = int(result[parameter.name])
except ValueError:
pass
else:
pass
# TODO: bool is not supported in the current version
# elif parameter.type == 'bool':
# if isinstance(result[parameter.name], bool):
# transformed_result[parameter.name] = bool(result[parameter.name])
# elif isinstance(result[parameter.name], str):
# if result[parameter.name].lower() in ['true', 'false']:
# transformed_result[parameter.name] = bool(result[parameter.name].lower() == 'true')
# elif isinstance(result[parameter.name], int):
# transformed_result[parameter.name] = bool(result[parameter.name])
elif parameter.type in {"string", "select"}:
if isinstance(result[parameter.name], str):
transformed_result[parameter.name] = result[parameter.name]
if parameter.type == SegmentType.NUMBER:
transformed = self._transform_number(param_value)
if transformed is not None:
transformed_result[parameter.name] = transformed
elif parameter.type == SegmentType.BOOLEAN:
if isinstance(result[parameter.name], (bool, int)):
transformed_result[parameter.name] = bool(result[parameter.name])
# elif isinstance(result[parameter.name], str):
# if result[parameter.name].lower() in ["true", "false"]:
# transformed_result[parameter.name] = bool(result[parameter.name].lower() == "true")
elif parameter.type == SegmentType.STRING:
if isinstance(param_value, str):
transformed_result[parameter.name] = param_value
elif parameter.is_array_type(): elif parameter.is_array_type():
if isinstance(result[parameter.name], list):
if isinstance(param_value, list):
nested_type = parameter.element_type() nested_type = parameter.element_type()
assert nested_type is not None assert nested_type is not None
segment_value = build_segment_with_type(segment_type=SegmentType(parameter.type), value=[]) segment_value = build_segment_with_type(segment_type=SegmentType(parameter.type), value=[])
transformed_result[parameter.name] = segment_value transformed_result[parameter.name] = segment_value
for item in result[parameter.name]:
if nested_type == "number":
if isinstance(item, int | float):
segment_value.value.append(item)
elif isinstance(item, str):
try:
if "." in item:
segment_value.value.append(float(item))
else:
segment_value.value.append(int(item))
except ValueError:
pass
elif nested_type == "string":
for item in param_value:
if nested_type == SegmentType.NUMBER:
transformed = self._transform_number(item)
if transformed is not None:
segment_value.value.append(transformed)
elif nested_type == SegmentType.STRING:
if isinstance(item, str): if isinstance(item, str):
segment_value.value.append(item) segment_value.value.append(item)
elif nested_type == "object":
elif nested_type == SegmentType.OBJECT:
if isinstance(item, dict): if isinstance(item, dict):
segment_value.value.append(item) segment_value.value.append(item)
elif nested_type == SegmentType.BOOLEAN:
if isinstance(item, bool):
segment_value.value.append(item)


if parameter.name not in transformed_result: if parameter.name not in transformed_result:
if parameter.type == "number":
transformed_result[parameter.name] = 0
elif parameter.type == "bool":
transformed_result[parameter.name] = False
elif parameter.type in {"string", "select"}:
transformed_result[parameter.name] = ""
elif parameter.type.startswith("array"):
if parameter.type.is_array_type():
transformed_result[parameter.name] = build_segment_with_type( transformed_result[parameter.name] = build_segment_with_type(
segment_type=SegmentType(parameter.type), value=[] segment_type=SegmentType(parameter.type), value=[]
) )
elif parameter.type in (SegmentType.STRING, SegmentType.SECRET):
transformed_result[parameter.name] = ""
elif parameter.type == SegmentType.NUMBER:
transformed_result[parameter.name] = 0
elif parameter.type == SegmentType.BOOLEAN:
transformed_result[parameter.name] = False
else:
raise AssertionError("this statement should be unreachable.")


return transformed_result return transformed_result



+ 5
- 2
api/core/workflow/nodes/variable_assigner/v1/node.py 查看文件

from typing import TYPE_CHECKING, Any, Optional, TypeAlias from typing import TYPE_CHECKING, Any, Optional, TypeAlias


from core.variables import SegmentType, Variable from core.variables import SegmentType, Variable
from core.variables.segments import BooleanSegment
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID
from core.workflow.conversation_variable_updater import ConversationVariableUpdater from core.workflow.conversation_variable_updater import ConversationVariableUpdater
from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.node_entities import NodeRunResult
def get_zero_value(t: SegmentType): def get_zero_value(t: SegmentType):
# TODO(QuantumGhost): this should be a method of `SegmentType`. # TODO(QuantumGhost): this should be a method of `SegmentType`.
match t: match t:
case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER:
return variable_factory.build_segment([])
case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER | SegmentType.ARRAY_BOOLEAN:
return variable_factory.build_segment_with_type(t, [])
case SegmentType.OBJECT: case SegmentType.OBJECT:
return variable_factory.build_segment({}) return variable_factory.build_segment({})
case SegmentType.STRING: case SegmentType.STRING:
return variable_factory.build_segment(0.0) return variable_factory.build_segment(0.0)
case SegmentType.NUMBER: case SegmentType.NUMBER:
return variable_factory.build_segment(0) return variable_factory.build_segment(0)
case SegmentType.BOOLEAN:
return BooleanSegment(value=False)
case _: case _:
raise VariableOperatorNodeError(f"unsupported variable type: {t}") raise VariableOperatorNodeError(f"unsupported variable type: {t}")

+ 2
- 0
api/core/workflow/nodes/variable_assigner/v2/constants.py 查看文件

EMPTY_VALUE_MAPPING = { EMPTY_VALUE_MAPPING = {
SegmentType.STRING: "", SegmentType.STRING: "",
SegmentType.NUMBER: 0, SegmentType.NUMBER: 0,
SegmentType.BOOLEAN: False,
SegmentType.OBJECT: {}, SegmentType.OBJECT: {},
SegmentType.ARRAY_ANY: [], SegmentType.ARRAY_ANY: [],
SegmentType.ARRAY_STRING: [], SegmentType.ARRAY_STRING: [],
SegmentType.ARRAY_NUMBER: [], SegmentType.ARRAY_NUMBER: [],
SegmentType.ARRAY_OBJECT: [], SegmentType.ARRAY_OBJECT: [],
SegmentType.ARRAY_BOOLEAN: [],
} }

+ 11
- 17
api/core/workflow/nodes/variable_assigner/v2/helpers.py 查看文件

SegmentType.NUMBER, SegmentType.NUMBER,
SegmentType.INTEGER, SegmentType.INTEGER,
SegmentType.FLOAT, SegmentType.FLOAT,
SegmentType.BOOLEAN,
} }
case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE: case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE:
# Only number variable can be added, subtracted, multiplied or divided # Only number variable can be added, subtracted, multiplied or divided
return variable_type in {SegmentType.NUMBER, SegmentType.INTEGER, SegmentType.FLOAT} return variable_type in {SegmentType.NUMBER, SegmentType.INTEGER, SegmentType.FLOAT}
case Operation.APPEND | Operation.EXTEND:
case Operation.APPEND | Operation.EXTEND | Operation.REMOVE_FIRST | Operation.REMOVE_LAST:
# Only array variable can be appended or extended # Only array variable can be appended or extended
return variable_type in {
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_FILE,
}
case Operation.REMOVE_FIRST | Operation.REMOVE_LAST:
# Only array variable can have elements removed # Only array variable can have elements removed
return variable_type in {
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_FILE,
}
return variable_type.is_array_type()
case _: case _:
return False return False




def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation): def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation):
match variable_type: match variable_type:
case SegmentType.STRING | SegmentType.OBJECT:
case SegmentType.STRING | SegmentType.OBJECT | SegmentType.BOOLEAN:
return operation in {Operation.OVER_WRITE, Operation.SET} return operation in {Operation.OVER_WRITE, Operation.SET}
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
return operation in { return operation in {
case SegmentType.STRING: case SegmentType.STRING:
return isinstance(value, str) return isinstance(value, str)


case SegmentType.BOOLEAN:
return isinstance(value, bool)

case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
if not isinstance(value, int | float): if not isinstance(value, int | float):
return False return False
return isinstance(value, int | float) return isinstance(value, int | float)
case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND: case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND:
return isinstance(value, dict) return isinstance(value, dict)
case SegmentType.ARRAY_BOOLEAN if operation == Operation.APPEND:
return isinstance(value, bool)


# Array & Extend / Overwrite # Array & Extend / Overwrite
case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}: case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, int | float) for item in value) return isinstance(value, list) and all(isinstance(item, int | float) for item in value)
case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}: case SegmentType.ARRAY_OBJECT if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, dict) for item in value) return isinstance(value, list) and all(isinstance(item, dict) for item in value)
case SegmentType.ARRAY_BOOLEAN if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
return isinstance(value, list) and all(isinstance(item, bool) for item in value)


case _: case _:
return False return False

+ 1
- 1
api/core/workflow/utils/condition/entities.py 查看文件

class Condition(BaseModel): class Condition(BaseModel):
variable_selector: list[str] variable_selector: list[str]
comparison_operator: SupportedComparisonOperator comparison_operator: SupportedComparisonOperator
value: str | Sequence[str] | None = None
value: str | Sequence[str] | bool | None = None
sub_variable_condition: SubVariableCondition | None = None sub_variable_condition: SubVariableCondition | None = None

+ 46
- 19
api/core/workflow/utils/condition/processor.py 查看文件

import json
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Literal
from typing import Any, Literal, Union


from core.file import FileAttribute, file_manager from core.file import FileAttribute, file_manager
from core.variables import ArrayFileSegment from core.variables import ArrayFileSegment
from core.variables.segments import ArrayBooleanSegment, BooleanSegment
from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.variable_pool import VariablePool


from .entities import Condition, SubCondition, SupportedComparisonOperator 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: class ConditionProcessor:
def process_conditions( def process_conditions(
self, self,
) )
else: else:
actual_value = variable.value if variable else None actual_value = variable.value if variable else None
expected_value = condition.value
expected_value: str | Sequence[str] | bool | list[bool] | None = condition.value
if isinstance(expected_value, str): if isinstance(expected_value, str):
expected_value = variable_pool.convert_template(expected_value).text 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( input_conditions.append(
{ {
"actual_value": actual_value, "actual_value": actual_value,
*, *,
operator: SupportedComparisonOperator, operator: SupportedComparisonOperator,
value: Any, value: Any,
expected: str | Sequence[str] | None,
expected: Union[str, Sequence[str], bool | Sequence[bool], None],
) -> bool: ) -> bool:
match operator: match operator:
case "contains": case "contains":
if not value: if not value:
return False return False


if not isinstance(value, str | list):
if not isinstance(value, (str, list)):
raise ValueError("Invalid actual value type: string or array") raise ValueError("Invalid actual value type: string or array")


if expected not in value: if expected not in value:
if not value: if not value:
return True return True


if not isinstance(value, str | list):
if not isinstance(value, (str, list)):
raise ValueError("Invalid actual value type: string or array") raise ValueError("Invalid actual value type: string or array")


if expected in value: if expected in value:
if value is None: if value is None:
return False return False


if not isinstance(value, str):
raise ValueError("Invalid actual value type: string")
if not isinstance(value, (str, bool)):
raise ValueError("Invalid actual value type: string or boolean")


if value != expected: if value != expected:
return False return False
if value is None: if value is None:
return False return False


if not isinstance(value, str):
raise ValueError("Invalid actual value type: string")
if not isinstance(value, (str, bool)):
raise ValueError("Invalid actual value type: string or boolean")


if value == expected: if value == expected:
return False return False
if value is None: if value is None:
return False return False


if not isinstance(value, int | float):
raise ValueError("Invalid actual value type: number")
if not isinstance(value, (int, float, bool)):
raise ValueError("Invalid actual value type: number or boolean")


if isinstance(value, int):
# Handle boolean comparison
if isinstance(value, bool):
expected = bool(expected)
elif isinstance(value, int):
expected = int(expected) expected = int(expected)
else: else:
expected = float(expected) expected = float(expected)
if value is None: if value is None:
return False return False


if not isinstance(value, int | float):
raise ValueError("Invalid actual value type: number")
if not isinstance(value, (int, float, bool)):
raise ValueError("Invalid actual value type: number or boolean")


if isinstance(value, int):
# Handle boolean comparison
if isinstance(value, bool):
expected = bool(expected)
elif isinstance(value, int):
expected = int(expected) expected = int(expected)
else: else:
expected = float(expected) expected = float(expected)
if value is None: if value is None:
return False return False


if not isinstance(value, int | float):
if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")


if isinstance(value, int): if isinstance(value, int):
if value is None: if value is None:
return False return False


if not isinstance(value, int | float):
if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")


if isinstance(value, int): if isinstance(value, int):
if value is None: if value is None:
return False return False


if not isinstance(value, int | float):
if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")


if isinstance(value, int): if isinstance(value, int):
if value is None: if value is None:
return False return False


if not isinstance(value, int | float):
if not isinstance(value, (int, float)):
raise ValueError("Invalid actual value type: number") raise ValueError("Invalid actual value type: number")


if isinstance(value, int): if isinstance(value, int):

+ 26
- 8
api/factories/variable_factory.py 查看文件

from core.variables.exc import VariableError from core.variables.exc import VariableError
from core.variables.segments import ( from core.variables.segments import (
ArrayAnySegment, ArrayAnySegment,
ArrayBooleanSegment,
ArrayFileSegment, ArrayFileSegment,
ArrayNumberSegment, ArrayNumberSegment,
ArrayObjectSegment, ArrayObjectSegment,
ArraySegment, ArraySegment,
ArrayStringSegment, ArrayStringSegment,
BooleanSegment,
FileSegment, FileSegment,
FloatSegment, FloatSegment,
IntegerSegment, IntegerSegment,
from core.variables.types import SegmentType from core.variables.types import SegmentType
from core.variables.variables import ( from core.variables.variables import (
ArrayAnyVariable, ArrayAnyVariable,
ArrayBooleanVariable,
ArrayFileVariable, ArrayFileVariable,
ArrayNumberVariable, ArrayNumberVariable,
ArrayObjectVariable, ArrayObjectVariable,
ArrayStringVariable, ArrayStringVariable,
BooleanVariable,
FileVariable, FileVariable,
FloatVariable, FloatVariable,
IntegerVariable, IntegerVariable,


# Define the constant # Define the constant
SEGMENT_TO_VARIABLE_MAP = { SEGMENT_TO_VARIABLE_MAP = {
StringSegment: StringVariable,
IntegerSegment: IntegerVariable,
FloatSegment: FloatVariable,
ObjectSegment: ObjectVariable,
FileSegment: FileVariable,
ArrayStringSegment: ArrayStringVariable,
ArrayAnySegment: ArrayAnyVariable,
ArrayBooleanSegment: ArrayBooleanVariable,
ArrayFileSegment: ArrayFileVariable,
ArrayNumberSegment: ArrayNumberVariable, ArrayNumberSegment: ArrayNumberVariable,
ArrayObjectSegment: ArrayObjectVariable, ArrayObjectSegment: ArrayObjectVariable,
ArrayFileSegment: ArrayFileVariable,
ArrayAnySegment: ArrayAnyVariable,
ArrayStringSegment: ArrayStringVariable,
BooleanSegment: BooleanVariable,
FileSegment: FileVariable,
FloatSegment: FloatVariable,
IntegerSegment: IntegerVariable,
NoneSegment: NoneVariable, NoneSegment: NoneVariable,
ObjectSegment: ObjectVariable,
StringSegment: StringVariable,
} }




mapping = dict(mapping) mapping = dict(mapping)
mapping["value_type"] = SegmentType.FLOAT mapping["value_type"] = SegmentType.FLOAT
result = FloatVariable.model_validate(mapping) result = FloatVariable.model_validate(mapping)
case SegmentType.BOOLEAN:
result = BooleanVariable.model_validate(mapping)
case SegmentType.NUMBER if not isinstance(value, float | int): case SegmentType.NUMBER if not isinstance(value, float | int):
raise VariableError(f"invalid number value {value}") raise VariableError(f"invalid number value {value}")
case SegmentType.OBJECT if isinstance(value, dict): case SegmentType.OBJECT if isinstance(value, dict):
result = ArrayNumberVariable.model_validate(mapping) result = ArrayNumberVariable.model_validate(mapping)
case SegmentType.ARRAY_OBJECT if isinstance(value, list): case SegmentType.ARRAY_OBJECT if isinstance(value, list):
result = ArrayObjectVariable.model_validate(mapping) result = ArrayObjectVariable.model_validate(mapping)
case SegmentType.ARRAY_BOOLEAN if isinstance(value, list):
result = ArrayBooleanVariable.model_validate(mapping)
case _: case _:
raise VariableError(f"not supported value type {value_type}") raise VariableError(f"not supported value type {value_type}")
if result.size > dify_config.MAX_VARIABLE_SIZE: if result.size > dify_config.MAX_VARIABLE_SIZE:
return NoneSegment() return NoneSegment()
if isinstance(value, str): if isinstance(value, str):
return StringSegment(value=value) return StringSegment(value=value)
if isinstance(value, bool):
return BooleanSegment(value=value)
if isinstance(value, int): if isinstance(value, int):
return IntegerSegment(value=value) return IntegerSegment(value=value)
if isinstance(value, float): if isinstance(value, float):
return ArrayStringSegment(value=value) return ArrayStringSegment(value=value)
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
return ArrayNumberSegment(value=value) return ArrayNumberSegment(value=value)
case SegmentType.BOOLEAN:
return ArrayBooleanSegment(value=value)
case SegmentType.OBJECT: case SegmentType.OBJECT:
return ArrayObjectSegment(value=value) return ArrayObjectSegment(value=value)
case SegmentType.FILE: case SegmentType.FILE:
SegmentType.INTEGER: IntegerSegment, SegmentType.INTEGER: IntegerSegment,
SegmentType.FLOAT: FloatSegment, SegmentType.FLOAT: FloatSegment,
SegmentType.FILE: FileSegment, SegmentType.FILE: FileSegment,
SegmentType.BOOLEAN: BooleanSegment,
SegmentType.OBJECT: ObjectSegment, SegmentType.OBJECT: ObjectSegment,
# Array types # Array types
SegmentType.ARRAY_ANY: ArrayAnySegment, SegmentType.ARRAY_ANY: ArrayAnySegment,
SegmentType.ARRAY_NUMBER: ArrayNumberSegment, SegmentType.ARRAY_NUMBER: ArrayNumberSegment,
SegmentType.ARRAY_OBJECT: ArrayObjectSegment, SegmentType.ARRAY_OBJECT: ArrayObjectSegment,
SegmentType.ARRAY_FILE: ArrayFileSegment, SegmentType.ARRAY_FILE: ArrayFileSegment,
SegmentType.ARRAY_BOOLEAN: ArrayBooleanSegment,
} }




return ArrayAnySegment(value=value) return ArrayAnySegment(value=value)
elif segment_type == SegmentType.ARRAY_STRING: elif segment_type == SegmentType.ARRAY_STRING:
return ArrayStringSegment(value=value) return ArrayStringSegment(value=value)
elif segment_type == SegmentType.ARRAY_BOOLEAN:
return ArrayBooleanSegment(value=value)
elif segment_type == SegmentType.ARRAY_NUMBER: elif segment_type == SegmentType.ARRAY_NUMBER:
return ArrayNumberSegment(value=value) return ArrayNumberSegment(value=value)
elif segment_type == SegmentType.ARRAY_OBJECT: elif segment_type == SegmentType.ARRAY_OBJECT:

+ 11
- 0
api/lazy_load_class.py 查看文件

from tests.integration_tests.utils.parent_class import ParentClass


class LazyLoadChildClass(ParentClass):
"""Test lazy load child class for module import helper tests"""

def __init__(self, name):
super().__init__(name)

def get_name(self):
return self.name

+ 3
- 0
api/mypy.ini 查看文件



[mypy-flask_restx.inputs] [mypy-flask_restx.inputs]
ignore_missing_imports=True ignore_missing_imports=True

[mypy-google.cloud.storage]
ignore_missing_imports=True

+ 2
- 0
api/tests/unit_tests/core/variables/test_segment_type.py 查看文件

SegmentType.ARRAY_NUMBER, SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT, SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_FILE, SegmentType.ARRAY_FILE,
SegmentType.ARRAY_BOOLEAN,
] ]
expected_non_array_types = [ expected_non_array_types = [
SegmentType.INTEGER, SegmentType.INTEGER,
SegmentType.FILE, SegmentType.FILE,
SegmentType.NONE, SegmentType.NONE,
SegmentType.GROUP, SegmentType.GROUP,
SegmentType.BOOLEAN,
] ]


for seg_type in expected_array_types: for seg_type in expected_array_types:

+ 729
- 0
api/tests/unit_tests/core/variables/test_segment_type_validation.py 查看文件

"""
Comprehensive unit tests for SegmentType.is_valid and SegmentType._validate_array methods.

This module provides thorough testing of the validation logic for all SegmentType values,
including edge cases, error conditions, and different ArrayValidation strategies.
"""

from dataclasses import dataclass
from typing import Any

import pytest

from core.file.enums import FileTransferMethod, FileType
from core.file.models import File
from core.variables.types import ArrayValidation, SegmentType


def create_test_file(
file_type: FileType = FileType.DOCUMENT,
transfer_method: FileTransferMethod = FileTransferMethod.LOCAL_FILE,
filename: str = "test.txt",
extension: str = ".txt",
mime_type: str = "text/plain",
size: int = 1024,
) -> File:
"""Factory function to create File objects for testing."""
return File(
tenant_id="test-tenant",
type=file_type,
transfer_method=transfer_method,
filename=filename,
extension=extension,
mime_type=mime_type,
size=size,
related_id="test-file-id" if transfer_method != FileTransferMethod.REMOTE_URL else None,
remote_url="https://example.com/file.txt" if transfer_method == FileTransferMethod.REMOTE_URL else None,
storage_key="test-storage-key",
)


@dataclass
class ValidationTestCase:
"""Test case data structure for validation tests."""

segment_type: SegmentType
value: Any
expected: bool
description: str

def get_id(self):
return self.description


@dataclass
class ArrayValidationTestCase:
"""Test case data structure for array validation tests."""

segment_type: SegmentType
value: Any
array_validation: ArrayValidation
expected: bool
description: str

def get_id(self):
return self.description


# Test data construction functions
def get_boolean_cases() -> list[ValidationTestCase]:
return [
# valid values
ValidationTestCase(SegmentType.BOOLEAN, True, True, "True boolean"),
ValidationTestCase(SegmentType.BOOLEAN, False, True, "False boolean"),
# Invalid values
ValidationTestCase(SegmentType.BOOLEAN, 1, False, "Integer 1 (not boolean)"),
ValidationTestCase(SegmentType.BOOLEAN, 0, False, "Integer 0 (not boolean)"),
ValidationTestCase(SegmentType.BOOLEAN, "true", False, "String 'true'"),
ValidationTestCase(SegmentType.BOOLEAN, "false", False, "String 'false'"),
ValidationTestCase(SegmentType.BOOLEAN, None, False, "None value"),
ValidationTestCase(SegmentType.BOOLEAN, [], False, "Empty list"),
ValidationTestCase(SegmentType.BOOLEAN, {}, False, "Empty dict"),
]


def get_number_cases() -> list[ValidationTestCase]:
"""Get test cases for valid number values."""
return [
# valid values
ValidationTestCase(SegmentType.NUMBER, 42, True, "Positive integer"),
ValidationTestCase(SegmentType.NUMBER, -42, True, "Negative integer"),
ValidationTestCase(SegmentType.NUMBER, 0, True, "Zero integer"),
ValidationTestCase(SegmentType.NUMBER, 3.14, True, "Positive float"),
ValidationTestCase(SegmentType.NUMBER, -3.14, True, "Negative float"),
ValidationTestCase(SegmentType.NUMBER, 0.0, True, "Zero float"),
ValidationTestCase(SegmentType.NUMBER, float("inf"), True, "Positive infinity"),
ValidationTestCase(SegmentType.NUMBER, float("-inf"), True, "Negative infinity"),
ValidationTestCase(SegmentType.NUMBER, float("nan"), True, "float(NaN)"),
# invalid number values
ValidationTestCase(SegmentType.NUMBER, "42", False, "String number"),
ValidationTestCase(SegmentType.NUMBER, None, False, "None value"),
ValidationTestCase(SegmentType.NUMBER, [], False, "Empty list"),
ValidationTestCase(SegmentType.NUMBER, {}, False, "Empty dict"),
ValidationTestCase(SegmentType.NUMBER, "3.14", False, "String float"),
]


def get_string_cases() -> list[ValidationTestCase]:
"""Get test cases for valid string values."""
return [
# valid values
ValidationTestCase(SegmentType.STRING, "", True, "Empty string"),
ValidationTestCase(SegmentType.STRING, "hello", True, "Simple string"),
ValidationTestCase(SegmentType.STRING, "🚀", True, "Unicode emoji"),
ValidationTestCase(SegmentType.STRING, "line1\nline2", True, "Multiline string"),
# invalid values
ValidationTestCase(SegmentType.STRING, 123, False, "Integer"),
ValidationTestCase(SegmentType.STRING, 3.14, False, "Float"),
ValidationTestCase(SegmentType.STRING, True, False, "Boolean"),
ValidationTestCase(SegmentType.STRING, None, False, "None value"),
ValidationTestCase(SegmentType.STRING, [], False, "Empty list"),
ValidationTestCase(SegmentType.STRING, {}, False, "Empty dict"),
]


def get_object_cases() -> list[ValidationTestCase]:
"""Get test cases for valid object values."""
return [
# valid cases
ValidationTestCase(SegmentType.OBJECT, {}, True, "Empty dict"),
ValidationTestCase(SegmentType.OBJECT, {"key": "value"}, True, "Simple dict"),
ValidationTestCase(SegmentType.OBJECT, {"a": 1, "b": 2}, True, "Dict with numbers"),
ValidationTestCase(SegmentType.OBJECT, {"nested": {"key": "value"}}, True, "Nested dict"),
ValidationTestCase(SegmentType.OBJECT, {"list": [1, 2, 3]}, True, "Dict with list"),
ValidationTestCase(SegmentType.OBJECT, {"mixed": [1, "two", {"three": 3}]}, True, "Complex dict"),
# invalid cases
ValidationTestCase(SegmentType.OBJECT, "not a dict", False, "String"),
ValidationTestCase(SegmentType.OBJECT, 123, False, "Integer"),
ValidationTestCase(SegmentType.OBJECT, 3.14, False, "Float"),
ValidationTestCase(SegmentType.OBJECT, True, False, "Boolean"),
ValidationTestCase(SegmentType.OBJECT, None, False, "None value"),
ValidationTestCase(SegmentType.OBJECT, [], False, "Empty list"),
ValidationTestCase(SegmentType.OBJECT, [1, 2, 3], False, "List with values"),
]


def get_secret_cases() -> list[ValidationTestCase]:
"""Get test cases for valid secret values."""
return [
# valid cases
ValidationTestCase(SegmentType.SECRET, "", True, "Empty secret"),
ValidationTestCase(SegmentType.SECRET, "secret", True, "Simple secret"),
ValidationTestCase(SegmentType.SECRET, "api_key_123", True, "API key format"),
ValidationTestCase(SegmentType.SECRET, "very_long_secret_key_with_special_chars!@#", True, "Complex secret"),
# invalid cases
ValidationTestCase(SegmentType.SECRET, 123, False, "Integer"),
ValidationTestCase(SegmentType.SECRET, 3.14, False, "Float"),
ValidationTestCase(SegmentType.SECRET, True, False, "Boolean"),
ValidationTestCase(SegmentType.SECRET, None, False, "None value"),
ValidationTestCase(SegmentType.SECRET, [], False, "Empty list"),
ValidationTestCase(SegmentType.SECRET, {}, False, "Empty dict"),
]


def get_file_cases() -> list[ValidationTestCase]:
"""Get test cases for valid file values."""
test_file = create_test_file()
image_file = create_test_file(
file_type=FileType.IMAGE, filename="image.jpg", extension=".jpg", mime_type="image/jpeg"
)
remote_file = create_test_file(
transfer_method=FileTransferMethod.REMOTE_URL, filename="remote.pdf", extension=".pdf"
)

return [
# valid cases
ValidationTestCase(SegmentType.FILE, test_file, True, "Document file"),
ValidationTestCase(SegmentType.FILE, image_file, True, "Image file"),
ValidationTestCase(SegmentType.FILE, remote_file, True, "Remote file"),
# invalid cases
ValidationTestCase(SegmentType.FILE, "not a file", False, "String"),
ValidationTestCase(SegmentType.FILE, 123, False, "Integer"),
ValidationTestCase(SegmentType.FILE, {"filename": "test.txt"}, False, "Dict resembling file"),
ValidationTestCase(SegmentType.FILE, None, False, "None value"),
ValidationTestCase(SegmentType.FILE, [], False, "Empty list"),
ValidationTestCase(SegmentType.FILE, True, False, "Boolean"),
]


def get_none_cases() -> list[ValidationTestCase]:
"""Get test cases for valid none values."""
return [
# valid cases
ValidationTestCase(SegmentType.NONE, None, True, "None value"),
# invalid cases
ValidationTestCase(SegmentType.NONE, "", False, "Empty string"),
ValidationTestCase(SegmentType.NONE, 0, False, "Zero integer"),
ValidationTestCase(SegmentType.NONE, 0.0, False, "Zero float"),
ValidationTestCase(SegmentType.NONE, False, False, "False boolean"),
ValidationTestCase(SegmentType.NONE, [], False, "Empty list"),
ValidationTestCase(SegmentType.NONE, {}, False, "Empty dict"),
ValidationTestCase(SegmentType.NONE, "null", False, "String 'null'"),
]


def get_array_any_validation_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_ANY validation."""
return [
ArrayValidationTestCase(
SegmentType.ARRAY_ANY,
[1, "string", 3.14, {"key": "value"}, True],
ArrayValidation.NONE,
True,
"Mixed types with NONE validation",
),
ArrayValidationTestCase(
SegmentType.ARRAY_ANY,
[1, "string", 3.14, {"key": "value"}, True],
ArrayValidation.FIRST,
True,
"Mixed types with FIRST validation",
),
ArrayValidationTestCase(
SegmentType.ARRAY_ANY,
[1, "string", 3.14, {"key": "value"}, True],
ArrayValidation.ALL,
True,
"Mixed types with ALL validation",
),
ArrayValidationTestCase(
SegmentType.ARRAY_ANY, [None, None, None], ArrayValidation.ALL, True, "All None values"
),
]


def get_array_string_validation_none_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_STRING validation with NONE strategy."""
return [
ArrayValidationTestCase(
SegmentType.ARRAY_STRING,
["hello", "world"],
ArrayValidation.NONE,
True,
"Valid strings with NONE validation",
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING,
[123, 456],
ArrayValidation.NONE,
True,
"Invalid elements with NONE validation",
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING,
["valid", 123, True],
ArrayValidation.NONE,
True,
"Mixed types with NONE validation",
),
]


def get_array_string_validation_first_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_STRING validation with FIRST strategy."""
return [
ArrayValidationTestCase(
SegmentType.ARRAY_STRING, ["hello", "world"], ArrayValidation.FIRST, True, "All valid strings"
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING,
["hello", 123, True],
ArrayValidation.FIRST,
True,
"First valid, others invalid",
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING,
[123, "hello", "world"],
ArrayValidation.FIRST,
False,
"First invalid, others valid",
),
ArrayValidationTestCase(SegmentType.ARRAY_STRING, [None, "hello"], ArrayValidation.FIRST, False, "First None"),
]


def get_array_string_validation_all_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_STRING validation with ALL strategy."""
return [
ArrayValidationTestCase(
SegmentType.ARRAY_STRING, ["hello", "world", "test"], ArrayValidation.ALL, True, "All valid strings"
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING, ["hello", 123, "world"], ArrayValidation.ALL, False, "One invalid element"
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING, [123, 456, 789], ArrayValidation.ALL, False, "All invalid elements"
),
ArrayValidationTestCase(
SegmentType.ARRAY_STRING, ["valid", None, "also_valid"], ArrayValidation.ALL, False, "Contains None"
),
]


def get_array_number_validation_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_NUMBER validation with different strategies."""
return [
# NONE strategy
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, [1, 2.5, 3], ArrayValidation.NONE, True, "Valid numbers with NONE"
),
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, ["not", "numbers"], ArrayValidation.NONE, True, "Invalid elements with NONE"
),
# FIRST strategy
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, [42, "not a number"], ArrayValidation.FIRST, True, "First valid number"
),
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, ["not a number", 42], ArrayValidation.FIRST, False, "First invalid"
),
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, [3.14, 2.71, 1.41], ArrayValidation.FIRST, True, "All valid floats"
),
# ALL strategy
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, [1, 2, 3, 4.5], ArrayValidation.ALL, True, "All valid numbers"
),
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER, [1, "invalid", 3], ArrayValidation.ALL, False, "One invalid element"
),
ArrayValidationTestCase(
SegmentType.ARRAY_NUMBER,
[float("inf"), float("-inf"), float("nan")],
ArrayValidation.ALL,
True,
"Special float values",
),
]


def get_array_object_validation_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_OBJECT validation with different strategies."""
return [
# NONE strategy
ArrayValidationTestCase(
SegmentType.ARRAY_OBJECT, [{}, {"key": "value"}], ArrayValidation.NONE, True, "Valid objects with NONE"
),
ArrayValidationTestCase(
SegmentType.ARRAY_OBJECT, ["not", "objects"], ArrayValidation.NONE, True, "Invalid elements with NONE"
),
# FIRST strategy
ArrayValidationTestCase(
SegmentType.ARRAY_OBJECT,
[{"valid": "object"}, "not an object"],
ArrayValidation.FIRST,
True,
"First valid object",
),
ArrayValidationTestCase(
SegmentType.ARRAY_OBJECT,
["not an object", {"valid": "object"}],
ArrayValidation.FIRST,
False,
"First invalid",
),
# ALL strategy
ArrayValidationTestCase(
SegmentType.ARRAY_OBJECT,
[{}, {"a": 1}, {"nested": {"key": "value"}}],
ArrayValidation.ALL,
True,
"All valid objects",
),
ArrayValidationTestCase(
SegmentType.ARRAY_OBJECT,
[{"valid": "object"}, "invalid", {"another": "object"}],
ArrayValidation.ALL,
False,
"One invalid element",
),
]


def get_array_file_validation_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_FILE validation with different strategies."""
file1 = create_test_file(filename="file1.txt")
file2 = create_test_file(filename="file2.txt")

return [
# NONE strategy
ArrayValidationTestCase(
SegmentType.ARRAY_FILE, [file1, file2], ArrayValidation.NONE, True, "Valid files with NONE"
),
ArrayValidationTestCase(
SegmentType.ARRAY_FILE, ["not", "files"], ArrayValidation.NONE, True, "Invalid elements with NONE"
),
# FIRST strategy
ArrayValidationTestCase(
SegmentType.ARRAY_FILE, [file1, "not a file"], ArrayValidation.FIRST, True, "First valid file"
),
ArrayValidationTestCase(
SegmentType.ARRAY_FILE, ["not a file", file1], ArrayValidation.FIRST, False, "First invalid"
),
# ALL strategy
ArrayValidationTestCase(SegmentType.ARRAY_FILE, [file1, file2], ArrayValidation.ALL, True, "All valid files"),
ArrayValidationTestCase(
SegmentType.ARRAY_FILE, [file1, "invalid", file2], ArrayValidation.ALL, False, "One invalid element"
),
]


def get_array_boolean_validation_cases() -> list[ArrayValidationTestCase]:
"""Get test cases for ARRAY_BOOLEAN validation with different strategies."""
return [
# NONE strategy
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [True, False, True], ArrayValidation.NONE, True, "Valid booleans with NONE"
),
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [1, 0, "true"], ArrayValidation.NONE, True, "Invalid elements with NONE"
),
# FIRST strategy
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [True, 1, 0], ArrayValidation.FIRST, True, "First valid boolean"
),
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [1, True, False], ArrayValidation.FIRST, False, "First invalid (integer 1)"
),
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [0, True, False], ArrayValidation.FIRST, False, "First invalid (integer 0)"
),
# ALL strategy
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [True, False, True, False], ArrayValidation.ALL, True, "All valid booleans"
),
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN, [True, 1, False], ArrayValidation.ALL, False, "One invalid element (integer)"
),
ArrayValidationTestCase(
SegmentType.ARRAY_BOOLEAN,
[True, "false", False],
ArrayValidation.ALL,
False,
"One invalid element (string)",
),
]


class TestSegmentTypeIsValid:
"""Test suite for SegmentType.is_valid method covering all non-array types."""

@pytest.mark.parametrize("case", get_boolean_cases(), ids=lambda case: case.description)
def test_boolean_validation(self, case):
assert case.segment_type.is_valid(case.value) == case.expected

@pytest.mark.parametrize("case", get_number_cases(), ids=lambda case: case.description)
def test_number_validation(self, case: ValidationTestCase):
assert case.segment_type.is_valid(case.value) == case.expected

@pytest.mark.parametrize("case", get_string_cases(), ids=lambda case: case.description)
def test_string_validation(self, case):
assert case.segment_type.is_valid(case.value) == case.expected

@pytest.mark.parametrize("case", get_object_cases(), ids=lambda case: case.description)
def test_object_validation(self, case):
assert case.segment_type.is_valid(case.value) == case.expected

@pytest.mark.parametrize("case", get_secret_cases(), ids=lambda case: case.description)
def test_secret_validation(self, case):
assert case.segment_type.is_valid(case.value) == case.expected

@pytest.mark.parametrize("case", get_file_cases(), ids=lambda case: case.description)
def test_file_validation(self, case):
assert case.segment_type.is_valid(case.value) == case.expected

@pytest.mark.parametrize("case", get_none_cases(), ids=lambda case: case.description)
def test_none_validation_valid_cases(self, case):
assert case.segment_type.is_valid(case.value) == case.expected

def test_unsupported_segment_type_raises_assertion_error(self):
"""Test that unsupported SegmentType values raise AssertionError."""
# GROUP is not handled in is_valid method
with pytest.raises(AssertionError, match="this statement should be unreachable"):
SegmentType.GROUP.is_valid("any value")


class TestSegmentTypeArrayValidation:
"""Test suite for SegmentType._validate_array method and array type validation."""

def test_array_validation_non_list_values(self):
"""Test that non-list values return False for all array types."""
array_types = [
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_FILE,
SegmentType.ARRAY_BOOLEAN,
]

non_list_values = [
"not a list",
123,
3.14,
True,
None,
{"key": "value"},
create_test_file(),
]

for array_type in array_types:
for value in non_list_values:
assert array_type.is_valid(value) is False, f"{array_type} should reject {type(value).__name__}"

def test_empty_array_validation(self):
"""Test that empty arrays are valid for all array types regardless of validation strategy."""
array_types = [
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_FILE,
SegmentType.ARRAY_BOOLEAN,
]

validation_strategies = [ArrayValidation.NONE, ArrayValidation.FIRST, ArrayValidation.ALL]

for array_type in array_types:
for strategy in validation_strategies:
assert array_type.is_valid([], strategy) is True, (
f"{array_type} should accept empty array with {strategy}"
)

@pytest.mark.parametrize("case", get_array_any_validation_cases(), ids=lambda case: case.description)
def test_array_any_validation(self, case):
"""Test ARRAY_ANY validation accepts any list regardless of content."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_string_validation_none_cases(), ids=lambda case: case.description)
def test_array_string_validation_with_none_strategy(self, case):
"""Test ARRAY_STRING validation with NONE strategy (no element validation)."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_string_validation_first_cases(), ids=lambda case: case.description)
def test_array_string_validation_with_first_strategy(self, case):
"""Test ARRAY_STRING validation with FIRST strategy (validate first element only)."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_string_validation_all_cases(), ids=lambda case: case.description)
def test_array_string_validation_with_all_strategy(self, case):
"""Test ARRAY_STRING validation with ALL strategy (validate all elements)."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_number_validation_cases(), ids=lambda case: case.description)
def test_array_number_validation_with_different_strategies(self, case):
"""Test ARRAY_NUMBER validation with different validation strategies."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_object_validation_cases(), ids=lambda case: case.description)
def test_array_object_validation_with_different_strategies(self, case):
"""Test ARRAY_OBJECT validation with different validation strategies."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_file_validation_cases(), ids=lambda case: case.description)
def test_array_file_validation_with_different_strategies(self, case):
"""Test ARRAY_FILE validation with different validation strategies."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

@pytest.mark.parametrize("case", get_array_boolean_validation_cases(), ids=lambda case: case.description)
def test_array_boolean_validation_with_different_strategies(self, case):
"""Test ARRAY_BOOLEAN validation with different validation strategies."""
assert case.segment_type.is_valid(case.value, case.array_validation) == case.expected

def test_default_array_validation_strategy(self):
"""Test that default array validation strategy is FIRST."""
# When no array_validation parameter is provided, it should default to FIRST
assert SegmentType.ARRAY_STRING.is_valid(["valid", 123]) is False # First element valid
assert SegmentType.ARRAY_STRING.is_valid([123, "valid"]) is False # First element invalid

assert SegmentType.ARRAY_NUMBER.is_valid([42, "invalid"]) is False # First element valid
assert SegmentType.ARRAY_NUMBER.is_valid(["invalid", 42]) is False # First element invalid

def test_array_validation_edge_cases(self):
"""Test edge cases for array validation."""
# Test with nested arrays (should be invalid for specific array types)
nested_array = [["nested", "array"], ["another", "nested"]]

assert SegmentType.ARRAY_STRING.is_valid(nested_array, ArrayValidation.FIRST) is False
assert SegmentType.ARRAY_STRING.is_valid(nested_array, ArrayValidation.ALL) is False
assert SegmentType.ARRAY_ANY.is_valid(nested_array, ArrayValidation.ALL) is True

# Test with very large arrays (performance consideration)
large_valid_array = ["string"] * 1000
large_mixed_array = ["string"] * 999 + [123] # Last element invalid

assert SegmentType.ARRAY_STRING.is_valid(large_valid_array, ArrayValidation.ALL) is True
assert SegmentType.ARRAY_STRING.is_valid(large_mixed_array, ArrayValidation.ALL) is False
assert SegmentType.ARRAY_STRING.is_valid(large_mixed_array, ArrayValidation.FIRST) is True


class TestSegmentTypeValidationIntegration:
"""Integration tests for SegmentType validation covering interactions between methods."""

def test_non_array_types_ignore_array_validation_parameter(self):
"""Test that non-array types ignore the array_validation parameter."""
non_array_types = [
SegmentType.STRING,
SegmentType.NUMBER,
SegmentType.BOOLEAN,
SegmentType.OBJECT,
SegmentType.SECRET,
SegmentType.FILE,
SegmentType.NONE,
]

for segment_type in non_array_types:
# Create appropriate valid value for each type
valid_value: Any
if segment_type == SegmentType.STRING:
valid_value = "test"
elif segment_type == SegmentType.NUMBER:
valid_value = 42
elif segment_type == SegmentType.BOOLEAN:
valid_value = True
elif segment_type == SegmentType.OBJECT:
valid_value = {"key": "value"}
elif segment_type == SegmentType.SECRET:
valid_value = "secret"
elif segment_type == SegmentType.FILE:
valid_value = create_test_file()
elif segment_type == SegmentType.NONE:
valid_value = None
else:
continue # Skip unsupported types

# All array validation strategies should give the same result
result_none = segment_type.is_valid(valid_value, ArrayValidation.NONE)
result_first = segment_type.is_valid(valid_value, ArrayValidation.FIRST)
result_all = segment_type.is_valid(valid_value, ArrayValidation.ALL)

assert result_none == result_first == result_all == True, (
f"{segment_type} should ignore array_validation parameter"
)

def test_comprehensive_type_coverage(self):
"""Test that all SegmentType enum values are covered in validation tests."""
all_segment_types = set(SegmentType)

# Types that should be handled by is_valid method
handled_types = {
# Non-array types
SegmentType.STRING,
SegmentType.NUMBER,
SegmentType.BOOLEAN,
SegmentType.OBJECT,
SegmentType.SECRET,
SegmentType.FILE,
SegmentType.NONE,
# Array types
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_FILE,
SegmentType.ARRAY_BOOLEAN,
}

# Types that are not handled by is_valid (should raise AssertionError)
unhandled_types = {
SegmentType.GROUP,
SegmentType.INTEGER, # Handled by NUMBER validation logic
SegmentType.FLOAT, # Handled by NUMBER validation logic
}

# Verify all types are accounted for
assert handled_types | unhandled_types == all_segment_types, "All SegmentType values should be categorized"

# Test that handled types work correctly
for segment_type in handled_types:
if segment_type.is_array_type():
# Test with empty array (should always be valid)
assert segment_type.is_valid([]) is True, f"{segment_type} should accept empty array"
else:
# Test with appropriate valid value
if segment_type == SegmentType.STRING:
assert segment_type.is_valid("test") is True
elif segment_type == SegmentType.NUMBER:
assert segment_type.is_valid(42) is True
elif segment_type == SegmentType.BOOLEAN:
assert segment_type.is_valid(True) is True
elif segment_type == SegmentType.OBJECT:
assert segment_type.is_valid({}) is True
elif segment_type == SegmentType.SECRET:
assert segment_type.is_valid("secret") is True
elif segment_type == SegmentType.FILE:
assert segment_type.is_valid(create_test_file()) is True
elif segment_type == SegmentType.NONE:
assert segment_type.is_valid(None) is True

def test_boolean_vs_integer_type_distinction(self):
"""Test the important distinction between boolean and integer types in validation."""
# This tests the comment in the code about bool being a subclass of int

# Boolean type should only accept actual booleans, not integers
assert SegmentType.BOOLEAN.is_valid(True) is True
assert SegmentType.BOOLEAN.is_valid(False) is True
assert SegmentType.BOOLEAN.is_valid(1) is False # Integer 1, not boolean
assert SegmentType.BOOLEAN.is_valid(0) is False # Integer 0, not boolean

# Number type should accept both integers and floats, including booleans (since bool is subclass of int)
assert SegmentType.NUMBER.is_valid(42) is True
assert SegmentType.NUMBER.is_valid(3.14) is True
assert SegmentType.NUMBER.is_valid(True) is True # bool is subclass of int
assert SegmentType.NUMBER.is_valid(False) is True # bool is subclass of int

def test_array_validation_recursive_behavior(self):
"""Test that array validation correctly handles recursive validation calls."""
# When validating array elements, _validate_array calls is_valid recursively
# with ArrayValidation.NONE to avoid infinite recursion

# Test nested validation doesn't cause issues
nested_arrays = [["inner", "array"], ["another", "inner"]]

# ARRAY_ANY should accept nested arrays
assert SegmentType.ARRAY_ANY.is_valid(nested_arrays, ArrayValidation.ALL) is True

# ARRAY_STRING should reject nested arrays (first element is not a string)
assert SegmentType.ARRAY_STRING.is_valid(nested_arrays, ArrayValidation.FIRST) is False
assert SegmentType.ARRAY_STRING.is_valid(nested_arrays, ArrayValidation.ALL) is False

+ 0
- 0
api/tests/unit_tests/core/workflow/nodes/parameter_extractor/__init__.py 查看文件


+ 27
- 0
api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_entities.py 查看文件

from core.variables.types import SegmentType
from core.workflow.nodes.parameter_extractor.entities import ParameterConfig


class TestParameterConfig:
def test_select_type(self):
data = {
"name": "yes_or_no",
"type": "select",
"options": ["yes", "no"],
"description": "a simple select made of `yes` and `no`",
"required": True,
}

pc = ParameterConfig.model_validate(data)
assert pc.type == SegmentType.STRING
assert pc.options == data["options"]

def test_validate_bool_type(self):
data = {
"name": "boolean",
"type": "bool",
"description": "a simple boolean parameter",
"required": True,
}
pc = ParameterConfig.model_validate(data)
assert pc.type == SegmentType.BOOLEAN

+ 567
- 0
api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py 查看文件

"""
Test cases for ParameterExtractorNode._validate_result and _transform_result methods.
"""

from dataclasses import dataclass
from typing import Any

import pytest

from core.model_runtime.entities import LLMMode
from core.variables.types import SegmentType
from core.workflow.nodes.llm import ModelConfig, VisionConfig
from core.workflow.nodes.parameter_extractor.entities import ParameterConfig, ParameterExtractorNodeData
from core.workflow.nodes.parameter_extractor.exc import (
InvalidNumberOfParametersError,
InvalidSelectValueError,
InvalidValueTypeError,
RequiredParameterMissingError,
)
from core.workflow.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode
from factories.variable_factory import build_segment_with_type


@dataclass
class ValidTestCase:
"""Test case data for valid scenarios."""

name: str
parameters: list[ParameterConfig]
result: dict[str, Any]

def get_name(self) -> str:
return self.name


@dataclass
class ErrorTestCase:
"""Test case data for error scenarios."""

name: str
parameters: list[ParameterConfig]
result: dict[str, Any]
expected_exception: type[Exception]
expected_message: str

def get_name(self) -> str:
return self.name


@dataclass
class TransformTestCase:
"""Test case data for transformation scenarios."""

name: str
parameters: list[ParameterConfig]
input_result: dict[str, Any]
expected_result: dict[str, Any]

def get_name(self) -> str:
return self.name


class TestParameterExtractorNodeMethods:
"""Test helper class that provides access to the methods under test."""

def validate_result(self, data: ParameterExtractorNodeData, result: dict[str, Any]) -> dict[str, Any]:
"""Wrapper to call _validate_result method."""
node = ParameterExtractorNode.__new__(ParameterExtractorNode)
return node._validate_result(data=data, result=result)

def transform_result(self, data: ParameterExtractorNodeData, result: dict[str, Any]) -> dict[str, Any]:
"""Wrapper to call _transform_result method."""
node = ParameterExtractorNode.__new__(ParameterExtractorNode)
return node._transform_result(data=data, result=result)


class TestValidateResult:
"""Test cases for _validate_result method."""

@staticmethod
def get_valid_test_cases() -> list[ValidTestCase]:
"""Get test cases that should pass validation."""
return [
ValidTestCase(
name="single_string_parameter",
parameters=[ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True)],
result={"name": "John"},
),
ValidTestCase(
name="single_number_parameter_int",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
result={"age": 25},
),
ValidTestCase(
name="single_number_parameter_float",
parameters=[ParameterConfig(name="price", type=SegmentType.NUMBER, description="Price", required=True)],
result={"price": 19.99},
),
ValidTestCase(
name="single_bool_parameter_true",
parameters=[
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True)
],
result={"active": True},
),
ValidTestCase(
name="single_bool_parameter_true",
parameters=[
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True)
],
result={"active": True},
),
ValidTestCase(
name="single_bool_parameter_false",
parameters=[
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True)
],
result={"active": False},
),
ValidTestCase(
name="select_parameter_valid_option",
parameters=[
ParameterConfig(
name="status",
type="select", # pyright: ignore[reportArgumentType]
description="Status",
required=True,
options=["active", "inactive"],
)
],
result={"status": "active"},
),
ValidTestCase(
name="array_string_parameter",
parameters=[
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True)
],
result={"tags": ["tag1", "tag2", "tag3"]},
),
ValidTestCase(
name="array_number_parameter",
parameters=[
ParameterConfig(name="scores", type=SegmentType.ARRAY_NUMBER, description="Scores", required=True)
],
result={"scores": [85, 92.5, 78]},
),
ValidTestCase(
name="array_object_parameter",
parameters=[
ParameterConfig(name="items", type=SegmentType.ARRAY_OBJECT, description="Items", required=True)
],
result={"items": [{"name": "item1"}, {"name": "item2"}]},
),
ValidTestCase(
name="multiple_parameters",
parameters=[
ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True),
ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True),
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True),
],
result={"name": "John", "age": 25, "active": True},
),
ValidTestCase(
name="optional_parameter_present",
parameters=[
ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True),
ParameterConfig(name="nickname", type=SegmentType.STRING, description="Nickname", required=False),
],
result={"name": "John", "nickname": "Johnny"},
),
ValidTestCase(
name="empty_array_parameter",
parameters=[
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True)
],
result={"tags": []},
),
]

@staticmethod
def get_error_test_cases() -> list[ErrorTestCase]:
"""Get test cases that should raise exceptions."""
return [
ErrorTestCase(
name="invalid_number_of_parameters_too_few",
parameters=[
ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True),
ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True),
],
result={"name": "John"},
expected_exception=InvalidNumberOfParametersError,
expected_message="Invalid number of parameters",
),
ErrorTestCase(
name="invalid_number_of_parameters_too_many",
parameters=[ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True)],
result={"name": "John", "age": 25},
expected_exception=InvalidNumberOfParametersError,
expected_message="Invalid number of parameters",
),
ErrorTestCase(
name="invalid_string_value_none",
parameters=[
ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True),
],
result={"name": None}, # Parameter present but None value, will trigger type check first
expected_exception=InvalidValueTypeError,
expected_message="Invalid value for parameter name, expected segment type: string, actual_type: none",
),
ErrorTestCase(
name="invalid_select_value",
parameters=[
ParameterConfig(
name="status",
type="select", # type: ignore
description="Status",
required=True,
options=["active", "inactive"],
)
],
result={"status": "pending"},
expected_exception=InvalidSelectValueError,
expected_message="Invalid `select` value for parameter status",
),
ErrorTestCase(
name="invalid_number_value_string",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
result={"age": "twenty-five"},
expected_exception=InvalidValueTypeError,
expected_message="Invalid value for parameter age, expected segment type: number, actual_type: string",
),
ErrorTestCase(
name="invalid_bool_value_string",
parameters=[
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True)
],
result={"active": "yes"},
expected_exception=InvalidValueTypeError,
expected_message=(
"Invalid value for parameter active, expected segment type: boolean, actual_type: string"
),
),
ErrorTestCase(
name="invalid_string_value_number",
parameters=[
ParameterConfig(
name="description", type=SegmentType.STRING, description="Description", required=True
)
],
result={"description": 123},
expected_exception=InvalidValueTypeError,
expected_message=(
"Invalid value for parameter description, expected segment type: string, actual_type: integer"
),
),
ErrorTestCase(
name="invalid_array_value_not_list",
parameters=[
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True)
],
result={"tags": "tag1,tag2,tag3"},
expected_exception=InvalidValueTypeError,
expected_message=(
"Invalid value for parameter tags, expected segment type: array[string], actual_type: string"
),
),
ErrorTestCase(
name="invalid_array_number_wrong_element_type",
parameters=[
ParameterConfig(name="scores", type=SegmentType.ARRAY_NUMBER, description="Scores", required=True)
],
result={"scores": [85, "ninety-two", 78]},
expected_exception=InvalidValueTypeError,
expected_message=(
"Invalid value for parameter scores, expected segment type: array[number], actual_type: array[any]"
),
),
ErrorTestCase(
name="invalid_array_string_wrong_element_type",
parameters=[
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True)
],
result={"tags": ["tag1", 123, "tag3"]},
expected_exception=InvalidValueTypeError,
expected_message=(
"Invalid value for parameter tags, expected segment type: array[string], actual_type: array[any]"
),
),
ErrorTestCase(
name="invalid_array_object_wrong_element_type",
parameters=[
ParameterConfig(name="items", type=SegmentType.ARRAY_OBJECT, description="Items", required=True)
],
result={"items": [{"name": "item1"}, "item2"]},
expected_exception=InvalidValueTypeError,
expected_message=(
"Invalid value for parameter items, expected segment type: array[object], actual_type: array[any]"
),
),
ErrorTestCase(
name="required_parameter_missing",
parameters=[
ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True),
ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=False),
],
result={"age": 25, "other": "value"}, # Missing required 'name' parameter, but has correct count
expected_exception=RequiredParameterMissingError,
expected_message="Parameter name is required",
),
]

@pytest.mark.parametrize("test_case", get_valid_test_cases(), ids=ValidTestCase.get_name)
def test_validate_result_valid_cases(self, test_case):
"""Test _validate_result with valid inputs."""
helper = TestParameterExtractorNodeMethods()

node_data = ParameterExtractorNodeData(
title="Test Node",
model=ModelConfig(provider="openai", name="gpt-3.5-turbo", mode=LLMMode.CHAT, completion_params={}),
query=["test_query"],
parameters=test_case.parameters,
reasoning_mode="function_call",
vision=VisionConfig(),
)

result = helper.validate_result(data=node_data, result=test_case.result)
assert result == test_case.result, f"Failed for case: {test_case.name}"

@pytest.mark.parametrize("test_case", get_error_test_cases(), ids=ErrorTestCase.get_name)
def test_validate_result_error_cases(self, test_case):
"""Test _validate_result with invalid inputs that should raise exceptions."""
helper = TestParameterExtractorNodeMethods()

node_data = ParameterExtractorNodeData(
title="Test Node",
model=ModelConfig(provider="openai", name="gpt-3.5-turbo", mode=LLMMode.CHAT, completion_params={}),
query=["test_query"],
parameters=test_case.parameters,
reasoning_mode="function_call",
vision=VisionConfig(),
)

with pytest.raises(test_case.expected_exception) as exc_info:
helper.validate_result(data=node_data, result=test_case.result)

assert test_case.expected_message in str(exc_info.value), f"Failed for case: {test_case.name}"


class TestTransformResult:
"""Test cases for _transform_result method."""

@staticmethod
def get_transform_test_cases() -> list[TransformTestCase]:
"""Get test cases for result transformation."""
return [
# String parameter transformation
TransformTestCase(
name="string_parameter_present",
parameters=[ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True)],
input_result={"name": "John"},
expected_result={"name": "John"},
),
TransformTestCase(
name="string_parameter_missing",
parameters=[ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True)],
input_result={},
expected_result={"name": ""},
),
# Number parameter transformation
TransformTestCase(
name="number_parameter_int_present",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
input_result={"age": 25},
expected_result={"age": 25},
),
TransformTestCase(
name="number_parameter_float_present",
parameters=[ParameterConfig(name="price", type=SegmentType.NUMBER, description="Price", required=True)],
input_result={"price": 19.99},
expected_result={"price": 19.99},
),
TransformTestCase(
name="number_parameter_missing",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
input_result={},
expected_result={"age": 0},
),
# Bool parameter transformation
TransformTestCase(
name="bool_parameter_missing",
parameters=[
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True)
],
input_result={},
expected_result={"active": False},
),
# Select parameter transformation
TransformTestCase(
name="select_parameter_present",
parameters=[
ParameterConfig(
name="status",
type="select", # type: ignore
description="Status",
required=True,
options=["active", "inactive"],
)
],
input_result={"status": "active"},
expected_result={"status": "active"},
),
TransformTestCase(
name="select_parameter_missing",
parameters=[
ParameterConfig(
name="status",
type="select", # type: ignore
description="Status",
required=True,
options=["active", "inactive"],
)
],
input_result={},
expected_result={"status": ""},
),
# Array parameter transformation - present cases
TransformTestCase(
name="array_string_parameter_present",
parameters=[
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True)
],
input_result={"tags": ["tag1", "tag2"]},
expected_result={
"tags": build_segment_with_type(segment_type=SegmentType.ARRAY_STRING, value=["tag1", "tag2"])
},
),
TransformTestCase(
name="array_number_parameter_present",
parameters=[
ParameterConfig(name="scores", type=SegmentType.ARRAY_NUMBER, description="Scores", required=True)
],
input_result={"scores": [85, 92.5]},
expected_result={
"scores": build_segment_with_type(segment_type=SegmentType.ARRAY_NUMBER, value=[85, 92.5])
},
),
TransformTestCase(
name="array_number_parameter_with_string_conversion",
parameters=[
ParameterConfig(name="scores", type=SegmentType.ARRAY_NUMBER, description="Scores", required=True)
],
input_result={"scores": [85, "92.5", "78"]},
expected_result={
"scores": build_segment_with_type(segment_type=SegmentType.ARRAY_NUMBER, value=[85, 92.5, 78])
},
),
TransformTestCase(
name="array_object_parameter_present",
parameters=[
ParameterConfig(name="items", type=SegmentType.ARRAY_OBJECT, description="Items", required=True)
],
input_result={"items": [{"name": "item1"}, {"name": "item2"}]},
expected_result={
"items": build_segment_with_type(
segment_type=SegmentType.ARRAY_OBJECT, value=[{"name": "item1"}, {"name": "item2"}]
)
},
),
# Array parameter transformation - missing cases
TransformTestCase(
name="array_string_parameter_missing",
parameters=[
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True)
],
input_result={},
expected_result={"tags": build_segment_with_type(segment_type=SegmentType.ARRAY_STRING, value=[])},
),
TransformTestCase(
name="array_number_parameter_missing",
parameters=[
ParameterConfig(name="scores", type=SegmentType.ARRAY_NUMBER, description="Scores", required=True)
],
input_result={},
expected_result={"scores": build_segment_with_type(segment_type=SegmentType.ARRAY_NUMBER, value=[])},
),
TransformTestCase(
name="array_object_parameter_missing",
parameters=[
ParameterConfig(name="items", type=SegmentType.ARRAY_OBJECT, description="Items", required=True)
],
input_result={},
expected_result={"items": build_segment_with_type(segment_type=SegmentType.ARRAY_OBJECT, value=[])},
),
# Multiple parameters transformation
TransformTestCase(
name="multiple_parameters_mixed",
parameters=[
ParameterConfig(name="name", type=SegmentType.STRING, description="Name", required=True),
ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True),
ParameterConfig(name="active", type=SegmentType.BOOLEAN, description="Active", required=True),
ParameterConfig(name="tags", type=SegmentType.ARRAY_STRING, description="Tags", required=True),
],
input_result={"name": "John", "age": 25},
expected_result={
"name": "John",
"age": 25,
"active": False,
"tags": build_segment_with_type(segment_type=SegmentType.ARRAY_STRING, value=[]),
},
),
# Number parameter transformation with string conversion
TransformTestCase(
name="number_parameter_string_to_float",
parameters=[ParameterConfig(name="price", type=SegmentType.NUMBER, description="Price", required=True)],
input_result={"price": "19.99"},
expected_result={"price": 19.99}, # String not converted, falls back to default
),
TransformTestCase(
name="number_parameter_string_to_int",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
input_result={"age": "25"},
expected_result={"age": 25}, # String not converted, falls back to default
),
TransformTestCase(
name="number_parameter_invalid_string",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
input_result={"age": "invalid_number"},
expected_result={"age": 0}, # Invalid string conversion fails, falls back to default
),
TransformTestCase(
name="number_parameter_non_string_non_number",
parameters=[ParameterConfig(name="age", type=SegmentType.NUMBER, description="Age", required=True)],
input_result={"age": ["not_a_number"]}, # Non-string, non-number value
expected_result={"age": 0}, # Falls back to default
),
TransformTestCase(
name="array_number_parameter_with_invalid_string_conversion",
parameters=[
ParameterConfig(name="scores", type=SegmentType.ARRAY_NUMBER, description="Scores", required=True)
],
input_result={"scores": [85, "invalid", "78"]},
expected_result={
"scores": build_segment_with_type(
segment_type=SegmentType.ARRAY_NUMBER, value=[85, 78]
) # Invalid string skipped
},
),
]

@pytest.mark.parametrize("test_case", get_transform_test_cases(), ids=TransformTestCase.get_name)
def test_transform_result_cases(self, test_case):
"""Test _transform_result with various inputs."""
helper = TestParameterExtractorNodeMethods()

node_data = ParameterExtractorNodeData(
title="Test Node",
model=ModelConfig(provider="openai", name="gpt-3.5-turbo", mode=LLMMode.CHAT, completion_params={}),
query=["test_query"],
parameters=test_case.parameters,
reasoning_mode="function_call",
vision=VisionConfig(),
)

result = helper.transform_result(data=node_data, result=test_case.input_result)
assert result == test_case.expected_result, (
f"Failed for case: {test_case.name}. Expected: {test_case.expected_result}, Got: {result}"
)

+ 219
- 0
api/tests/unit_tests/core/workflow/nodes/test_if_else.py 查看文件

import uuid import uuid
from unittest.mock import MagicMock, Mock from unittest.mock import MagicMock, Mock


import pytest

from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.file import File, FileTransferMethod, FileType from core.file import File, FileTransferMethod, FileType
from core.variables import ArrayFileSegment from core.variables import ArrayFileSegment
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None assert result.outputs is not None
assert result.outputs["result"] is True assert result.outputs["result"] is True


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"}]}

graph = Graph.init(graph_config=graph_config)

init_params = GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config=graph_config,
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)

# construct variable pool with boolean values
pool = VariablePool(
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": node_data},
)
node.init_node_data(node_data)

# Mock db.session.close()
db.session.close = MagicMock()

# execute node
result = node._run()

assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["result"] is True


def test_execute_if_else_boolean_false_conditions():
"""Test IfElseNode with boolean conditions that should evaluate to false"""
graph_config = {"edges": [], "nodes": [{"data": {"type": "start"}, "id": "start"}]}

graph = Graph.init(graph_config=graph_config)

init_params = GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config=graph_config,
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)

# construct variable pool with boolean values
pool = VariablePool(
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,
graph=graph,
graph_runtime_state=GraphRuntimeState(variable_pool=pool, start_at=time.perf_counter()),
config={
"id": "if-else",
"data": node_data,
},
)
node.init_node_data(node_data)

# Mock db.session.close()
db.session.close = MagicMock()

# execute node
result = node._run()

assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["result"] is False


def test_execute_if_else_boolean_cases_structure():
"""Test IfElseNode with boolean conditions using the new cases structure"""
graph_config = {"edges": [], "nodes": [{"data": {"type": "start"}, "id": "start"}]}

graph = Graph.init(graph_config=graph_config)

init_params = GraphInitParams(
tenant_id="1",
app_id="1",
workflow_type=WorkflowType.WORKFLOW,
workflow_id="1",
graph_config=graph_config,
user_id="1",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)

# construct variable pool with boolean values
pool = VariablePool(
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": node_data},
)
node.init_node_data(node_data)

# Mock db.session.close()
db.session.close = MagicMock()

# execute node
result = node._run()

assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert result.outputs is not None
assert result.outputs["result"] is True
assert result.outputs["selected_case_id"] == "true"

+ 3
- 2
api/tests/unit_tests/core/workflow/nodes/test_list_operator.py 查看文件

FilterCondition, FilterCondition,
Limit, Limit,
ListOperatorNodeData, ListOperatorNodeData,
OrderBy,
Order,
OrderByConfig,
) )
from core.workflow.nodes.list_operator.exc import InvalidKeyError from core.workflow.nodes.list_operator.exc import InvalidKeyError
from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func
FilterCondition(key="type", comparison_operator="in", value=[FileType.IMAGE, FileType.DOCUMENT]) FilterCondition(key="type", comparison_operator="in", value=[FileType.IMAGE, FileType.DOCUMENT])
], ],
), ),
"order_by": OrderBy(enabled=False, value="asc"),
"order_by": OrderByConfig(enabled=False, value=Order.ASC),
"limit": Limit(enabled=False, size=0), "limit": Limit(enabled=False, size=0),
"extract_by": ExtractConfig(enabled=False, serial="1"), "extract_by": ExtractConfig(enabled=False, serial="1"),
"title": "Test Title", "title": "Test Title",

+ 39
- 10
api/tests/unit_tests/factories/test_variable_factory.py 查看文件

ArrayNumberSegment, ArrayNumberSegment,
ArrayObjectSegment, ArrayObjectSegment,
ArrayStringSegment, ArrayStringSegment,
BooleanSegment,
FileSegment, FileSegment,
FloatSegment, FloatSegment,
IntegerSegment, IntegerSegment,
NoneSegment, NoneSegment,
ObjectSegment, ObjectSegment,
Segment,
StringSegment, StringSegment,
) )
from core.variables.types import SegmentType from core.variables.types import SegmentType
from factories import variable_factory from factories import variable_factory
from factories.variable_factory import TypeMismatchError, build_segment_with_type
from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type




def test_string_variable(): def test_string_variable():
assert isinstance(variable.value[1], float) assert isinstance(variable.value[1], float)




def test_build_segment_scalar_values():
@dataclass
class TestCase:
value: Any
expected: Segment
description: str

cases = [
TestCase(
value=True,
expected=BooleanSegment(value=True),
description="build_segment with boolean should yield BooleanSegment",
)
]

for idx, c in enumerate(cases, 1):
seg = build_segment(c.value)
assert seg == c.expected, f"Test case {idx} failed: {c.description}"


def test_array_object_variable(): def test_array_object_variable():
mapping = { mapping = {
"id": str(uuid4()), "id": str(uuid4()),
f"but got: {error_message}" f"but got: {error_message}"
) )


def test_build_segment_boolean_type_note(self):
"""Note: Boolean values are actually handled as integers in Python, so they don't raise ValueError."""
# Boolean values in Python are subclasses of int, so they get processed as integers
# True becomes IntegerSegment(value=1) and False becomes IntegerSegment(value=0)
def test_build_segment_boolean_type(self):
"""Test that Boolean values are correctly handled as boolean type, not integers."""
# Boolean values should now be processed as BooleanSegment, not IntegerSegment
# This is because the bool check now comes before the int check in build_segment
true_segment = variable_factory.build_segment(True) true_segment = variable_factory.build_segment(True)
false_segment = variable_factory.build_segment(False) false_segment = variable_factory.build_segment(False)


# Verify they are processed as integers, not as errors
assert true_segment.value == 1, "Test case 1 (boolean_true): Expected True to be processed as integer 1"
assert false_segment.value == 0, "Test case 2 (boolean_false): Expected False to be processed as integer 0"
assert true_segment.value_type == SegmentType.INTEGER
assert false_segment.value_type == SegmentType.INTEGER
# Verify they are processed as booleans, not integers
assert true_segment.value is True, "Test case 1 (boolean_true): Expected True to be processed as boolean True"
assert false_segment.value is False, (
"Test case 2 (boolean_false): Expected False to be processed as boolean False"
)
assert true_segment.value_type == SegmentType.BOOLEAN
assert false_segment.value_type == SegmentType.BOOLEAN

# Test array of booleans
bool_array_segment = variable_factory.build_segment([True, False, True])
assert bool_array_segment.value_type == SegmentType.ARRAY_BOOLEAN
assert bool_array_segment.value == [True, False, True]

+ 47
- 0
simple_boolean_test.py 查看文件

#!/usr/bin/env python3
"""
Simple test to verify boolean classes can be imported correctly.
"""

import sys
import os

# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))

try:
# Test that we can import the boolean classes
from core.variables.segments import BooleanSegment, ArrayBooleanSegment
from core.variables.variables import BooleanVariable, ArrayBooleanVariable
from core.variables.types import SegmentType

print("✅ Successfully imported BooleanSegment")
print("✅ Successfully imported ArrayBooleanSegment")
print("✅ Successfully imported BooleanVariable")
print("✅ Successfully imported ArrayBooleanVariable")
print("✅ Successfully imported SegmentType")

# Test that the segment types exist
print(f"✅ SegmentType.BOOLEAN = {SegmentType.BOOLEAN}")
print(f"✅ SegmentType.ARRAY_BOOLEAN = {SegmentType.ARRAY_BOOLEAN}")

# Test creating boolean segments directly
bool_seg = BooleanSegment(value=True)
print(f"✅ Created BooleanSegment: {bool_seg}")
print(f" Value type: {bool_seg.value_type}")
print(f" Value: {bool_seg.value}")

array_bool_seg = ArrayBooleanSegment(value=[True, False, True])
print(f"✅ Created ArrayBooleanSegment: {array_bool_seg}")
print(f" Value type: {array_bool_seg.value_type}")
print(f" Value: {array_bool_seg.value}")

print("\n🎉 All boolean class imports and basic functionality work correctly!")

except ImportError as e:
print(f"❌ Import error: {e}")
except Exception as e:
print(f"❌ Error: {e}")
import traceback

traceback.print_exc()

+ 118
- 0
test_boolean_conditions.py 查看文件

#!/usr/bin/env python3
"""
Simple test script to verify boolean condition support in IfElseNode
"""

import sys
import os

# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))

from core.workflow.utils.condition.processor import (
ConditionProcessor,
_evaluate_condition,
)


def test_boolean_conditions():
"""Test boolean condition evaluation"""
print("Testing boolean condition support...")

# Test boolean "is" operator
result = _evaluate_condition(value=True, operator="is", expected="true")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is' with True value passed")

result = _evaluate_condition(value=False, operator="is", expected="false")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is' with False value passed")

# Test boolean "is not" operator
result = _evaluate_condition(value=True, operator="is not", expected="false")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is not' with True value passed")

result = _evaluate_condition(value=False, operator="is not", expected="true")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'is not' with False value passed")

# Test boolean "=" operator
result = _evaluate_condition(value=True, operator="=", expected="1")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '=' with True=1 passed")

result = _evaluate_condition(value=False, operator="=", expected="0")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '=' with False=0 passed")

# Test boolean "≠" operator
result = _evaluate_condition(value=True, operator="≠", expected="0")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '≠' with True≠0 passed")

result = _evaluate_condition(value=False, operator="≠", expected="1")
assert result == True, f"Expected True, got {result}"
print("✓ Boolean '≠' with False≠1 passed")

# Test boolean "in" operator
result = _evaluate_condition(value=True, operator="in", expected=["true", "false"])
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'in' with True in array passed")

result = _evaluate_condition(value=False, operator="in", expected=["true", "false"])
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'in' with False in array passed")

# Test boolean "not in" operator
result = _evaluate_condition(value=True, operator="not in", expected=["false", "0"])
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'not in' with True not in [false, 0] passed")

# Test boolean "null" and "not null" operators
result = _evaluate_condition(value=True, operator="not null", expected=None)
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'not null' with True passed")

result = _evaluate_condition(value=False, operator="not null", expected=None)
assert result == True, f"Expected True, got {result}"
print("✓ Boolean 'not null' with False passed")

print("\n🎉 All boolean condition tests passed!")


def test_backward_compatibility():
"""Test that existing string and number conditions still work"""
print("\nTesting backward compatibility...")

# Test string conditions
result = _evaluate_condition(value="hello", operator="is", expected="hello")
assert result == True, f"Expected True, got {result}"
print("✓ String 'is' condition still works")

result = _evaluate_condition(value="hello", operator="contains", expected="ell")
assert result == True, f"Expected True, got {result}"
print("✓ String 'contains' condition still works")

# Test number conditions
result = _evaluate_condition(value=42, operator="=", expected="42")
assert result == True, f"Expected True, got {result}"
print("✓ Number '=' condition still works")

result = _evaluate_condition(value=42, operator=">", expected="40")
assert result == True, f"Expected True, got {result}"
print("✓ Number '>' condition still works")

print("✓ Backward compatibility maintained!")


if __name__ == "__main__":
try:
test_boolean_conditions()
test_backward_compatibility()
print(
"\n✅ All tests passed! Boolean support has been successfully added to IfElseNode."
)
except Exception as e:
print(f"\n❌ Test failed: {e}")
sys.exit(1)

+ 67
- 0
test_boolean_contains_fix.py 查看文件

#!/usr/bin/env python3

"""
Test script to verify the boolean array comparison fix in condition processor.
"""

import sys
import os

# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))

from core.workflow.utils.condition.processor import (
_assert_contains,
_assert_not_contains,
)


def test_boolean_array_contains():
"""Test that boolean arrays work correctly with string comparisons."""

# Test case 1: Boolean array [True, False, True] contains "true"
bool_array = [True, False, True]

# Should return True because "true" converts to True and True is in the array
result1 = _assert_contains(value=bool_array, expected="true")
print(f"Test 1 - [True, False, True] contains 'true': {result1}")
assert result1 == True, "Expected True but got False"

# Should return True because "false" converts to False and False is in the array
result2 = _assert_contains(value=bool_array, expected="false")
print(f"Test 2 - [True, False, True] contains 'false': {result2}")
assert result2 == True, "Expected True but got False"

# Test case 2: Boolean array [True, True] does not contain "false"
bool_array2 = [True, True]
result3 = _assert_contains(value=bool_array2, expected="false")
print(f"Test 3 - [True, True] contains 'false': {result3}")
assert result3 == False, "Expected False but got True"

# Test case 3: Test not_contains
result4 = _assert_not_contains(value=bool_array2, expected="false")
print(f"Test 4 - [True, True] not contains 'false': {result4}")
assert result4 == True, "Expected True but got False"

result5 = _assert_not_contains(value=bool_array, expected="true")
print(f"Test 5 - [True, False, True] not contains 'true': {result5}")
assert result5 == False, "Expected False but got True"

# Test case 4: Test with different string representations
result6 = _assert_contains(
value=bool_array, expected="1"
) # "1" should convert to True
print(f"Test 6 - [True, False, True] contains '1': {result6}")
assert result6 == True, "Expected True but got False"

result7 = _assert_contains(
value=bool_array, expected="0"
) # "0" should convert to False
print(f"Test 7 - [True, False, True] contains '0': {result7}")
assert result7 == True, "Expected True but got False"

print("\n✅ All boolean array comparison tests passed!")


if __name__ == "__main__":
test_boolean_array_contains()

+ 99
- 0
test_boolean_factory.py 查看文件

#!/usr/bin/env python3
"""
Simple test script to verify boolean type inference in variable factory.
"""

import sys
import os

# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))

try:
from factories.variable_factory import build_segment, segment_to_variable
from core.variables.segments import BooleanSegment, ArrayBooleanSegment
from core.variables.variables import BooleanVariable, ArrayBooleanVariable
from core.variables.types import SegmentType

def test_boolean_inference():
print("Testing boolean type inference...")

# Test single boolean values
true_segment = build_segment(True)
false_segment = build_segment(False)

print(f"True value: {true_segment}")
print(f"Type: {type(true_segment)}")
print(f"Value type: {true_segment.value_type}")
print(f"Is BooleanSegment: {isinstance(true_segment, BooleanSegment)}")

print(f"\nFalse value: {false_segment}")
print(f"Type: {type(false_segment)}")
print(f"Value type: {false_segment.value_type}")
print(f"Is BooleanSegment: {isinstance(false_segment, BooleanSegment)}")

# Test array of booleans
bool_array_segment = build_segment([True, False, True])
print(f"\nBoolean array: {bool_array_segment}")
print(f"Type: {type(bool_array_segment)}")
print(f"Value type: {bool_array_segment.value_type}")
print(
f"Is ArrayBooleanSegment: {isinstance(bool_array_segment, ArrayBooleanSegment)}"
)

# Test empty boolean array
empty_bool_array = build_segment([])
print(f"\nEmpty array: {empty_bool_array}")
print(f"Type: {type(empty_bool_array)}")
print(f"Value type: {empty_bool_array.value_type}")

# Test segment to variable conversion
bool_var = segment_to_variable(
segment=true_segment, selector=["test", "bool_var"], name="test_boolean"
)
print(f"\nBoolean variable: {bool_var}")
print(f"Type: {type(bool_var)}")
print(f"Is BooleanVariable: {isinstance(bool_var, BooleanVariable)}")

array_bool_var = segment_to_variable(
segment=bool_array_segment,
selector=["test", "array_bool_var"],
name="test_array_boolean",
)
print(f"\nArray boolean variable: {array_bool_var}")
print(f"Type: {type(array_bool_var)}")
print(
f"Is ArrayBooleanVariable: {isinstance(array_bool_var, ArrayBooleanVariable)}"
)

# Test that bool comes before int (critical ordering)
print(f"\nTesting bool vs int precedence:")
print(f"True is instance of bool: {isinstance(True, bool)}")
print(f"True is instance of int: {isinstance(True, int)}")
print(f"False is instance of bool: {isinstance(False, bool)}")
print(f"False is instance of int: {isinstance(False, int)}")

# Verify that boolean values are correctly inferred as boolean, not int
assert true_segment.value_type == SegmentType.BOOLEAN, (
"True should be inferred as BOOLEAN"
)
assert false_segment.value_type == SegmentType.BOOLEAN, (
"False should be inferred as BOOLEAN"
)
assert bool_array_segment.value_type == SegmentType.ARRAY_BOOLEAN, (
"Boolean array should be inferred as ARRAY_BOOLEAN"
)

print("\n✅ All boolean inference tests passed!")

if __name__ == "__main__":
test_boolean_inference()

except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running this from the correct directory")
except Exception as e:
print(f"Error: {e}")
import traceback

traceback.print_exc()

+ 230
- 0
test_boolean_variable_assigner.py 查看文件

#!/usr/bin/env python3
"""
Test script to verify boolean support in VariableAssigner node
"""

import sys
import os

# Add the api directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "api"))

from core.variables import SegmentType
from core.workflow.nodes.variable_assigner.v2.helpers import (
is_operation_supported,
is_constant_input_supported,
is_input_value_valid,
)
from core.workflow.nodes.variable_assigner.v2.enums import Operation
from core.workflow.nodes.variable_assigner.v2.constants import EMPTY_VALUE_MAPPING


def test_boolean_operation_support():
"""Test that boolean types support the correct operations"""
print("Testing boolean operation support...")

# Boolean should support SET, OVER_WRITE, and CLEAR
assert is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET
)
assert is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE
)
assert is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.CLEAR
)

# Boolean should NOT support arithmetic operations
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.ADD
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.SUBTRACT
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.MULTIPLY
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.DIVIDE
)

# Boolean should NOT support array operations
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.APPEND
)
assert not is_operation_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.EXTEND
)

print("✓ Boolean operation support tests passed")


def test_array_boolean_operation_support():
"""Test that array boolean types support the correct operations"""
print("Testing array boolean operation support...")

# Array boolean should support APPEND, EXTEND, SET, OVER_WRITE, CLEAR
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.EXTEND
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.OVER_WRITE
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.CLEAR
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.REMOVE_FIRST
)
assert is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.REMOVE_LAST
)

# Array boolean should NOT support arithmetic operations
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.ADD
)
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.SUBTRACT
)
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.MULTIPLY
)
assert not is_operation_supported(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.DIVIDE
)

print("✓ Array boolean operation support tests passed")


def test_boolean_constant_input_support():
"""Test that boolean types support constant input for correct operations"""
print("Testing boolean constant input support...")

# Boolean should support constant input for SET and OVER_WRITE
assert is_constant_input_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET
)
assert is_constant_input_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE
)

# Boolean should NOT support constant input for arithmetic operations
assert not is_constant_input_supported(
variable_type=SegmentType.BOOLEAN, operation=Operation.ADD
)

print("✓ Boolean constant input support tests passed")


def test_boolean_input_validation():
"""Test that boolean input validation works correctly"""
print("Testing boolean input validation...")

# Boolean values should be valid for boolean type
assert is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=True
)
assert is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=False
)
assert is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.OVER_WRITE, value=True
)

# Non-boolean values should be invalid for boolean type
assert not is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value="true"
)
assert not is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=1
)
assert not is_input_value_valid(
variable_type=SegmentType.BOOLEAN, operation=Operation.SET, value=0
)

print("✓ Boolean input validation tests passed")


def test_array_boolean_input_validation():
"""Test that array boolean input validation works correctly"""
print("Testing array boolean input validation...")

# Boolean values should be valid for array boolean append
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND, value=True
)
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN, operation=Operation.APPEND, value=False
)

# Boolean arrays should be valid for extend/overwrite
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.EXTEND,
value=[True, False, True],
)
assert is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.OVER_WRITE,
value=[False, False],
)

# Non-boolean values should be invalid
assert not is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.APPEND,
value="true",
)
assert not is_input_value_valid(
variable_type=SegmentType.ARRAY_BOOLEAN,
operation=Operation.EXTEND,
value=[True, "false"],
)

print("✓ Array boolean input validation tests passed")


def test_empty_value_mapping():
"""Test that empty value mapping includes boolean types"""
print("Testing empty value mapping...")

# Check that boolean types have correct empty values
assert SegmentType.BOOLEAN in EMPTY_VALUE_MAPPING
assert EMPTY_VALUE_MAPPING[SegmentType.BOOLEAN] is False

assert SegmentType.ARRAY_BOOLEAN in EMPTY_VALUE_MAPPING
assert EMPTY_VALUE_MAPPING[SegmentType.ARRAY_BOOLEAN] == []

print("✓ Empty value mapping tests passed")


def main():
"""Run all tests"""
print("Running VariableAssigner boolean support tests...\n")

try:
test_boolean_operation_support()
test_array_boolean_operation_support()
test_boolean_constant_input_support()
test_boolean_input_validation()
test_array_boolean_input_validation()
test_empty_value_mapping()

print(
"\n🎉 All tests passed! Boolean support has been successfully added to VariableAssigner."
)

except Exception as e:
print(f"\n❌ Test failed: {e}")
import traceback

traceback.print_exc()
sys.exit(1)


if __name__ == "__main__":
main()

+ 24
- 0
web/app/components/app/configuration/config-var/config-modal/config.ts 查看文件

export const jsonObjectWrap = {
type: 'object',
properties: {},
required: [],
additionalProperties: true,
}

export const jsonConfigPlaceHolder = JSON.stringify(
{
foo: {
type: 'string',
},
bar: {
type: 'object',
properties: {
sub: {
type: 'number',
},
},
required: [],
additionalProperties: true,
},
}, null, 2,
)

+ 8
- 1
web/app/components/app/configuration/config-var/config-modal/field.tsx 查看文件

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'


type Props = { type Props = {
className?: string className?: string
title: string title: string
isOptional?: boolean
children: React.JSX.Element children: React.JSX.Element
} }


const Field: FC<Props> = ({ const Field: FC<Props> = ({
className, className,
title, title,
isOptional,
children, children,
}) => { }) => {
const { t } = useTranslation()
return ( return (
<div className={cn(className)}> <div className={cn(className)}>
<div className='system-sm-semibold leading-8 text-text-secondary'>{title}</div>
<div className='system-sm-semibold leading-8 text-text-secondary'>
{title}
{isOptional && <span className='system-xs-regular ml-1 text-text-tertiary'>({t('appDebug.variableConfig.optional')})</span>}
</div>
<div>{children}</div> <div>{children}</div>
</div> </div>
) )

+ 104
- 40
web/app/components/app/configuration/config-var/config-modal/index.tsx 查看文件

'use client' 'use client'
import type { ChangeEvent, FC } from 'react' import type { ChangeEvent, FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer' import produce from 'immer'
import ModalFoot from '../modal-foot' import ModalFoot from '../modal-foot'
import ConfigSelect from '../config-select' import ConfigSelect from '../config-select'
import ConfigString from '../config-string' import ConfigString from '../config-string'
import SelectTypeItem from '../select-type-item'
import Field from './field' import Field from './field'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants' import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import type { Item as SelectItem } from './type-select'
import TypeSelector from './type-select'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { jsonConfigPlaceHolder, jsonObjectWrap } from './config'
import { useStore as useAppStore } from '@/app/components/app/store'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
const [tempPayload, setTempPayload] = useState<InputVar>(payload || getNewVarInWorkflow('') as any) const [tempPayload, setTempPayload] = useState<InputVar>(payload || getNewVarInWorkflow('') as any)
const { type, label, variable, options, max_length } = tempPayload const { type, label, variable, options, max_length } = tempPayload
const modalRef = useRef<HTMLDivElement>(null) const modalRef = useRef<HTMLDivElement>(null)
const appDetail = useAppStore(state => state.appDetail)
const isBasicApp = appDetail?.mode !== 'advanced-chat' && appDetail?.mode !== 'workflow'
const isSupportJSON = false
const jsonSchemaStr = useMemo(() => {
const isJsonObject = type === InputVarType.jsonObject
if (!isJsonObject || !tempPayload.json_schema)
return ''
try {
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
}
catch (_e) {
return ''
}
}, [tempPayload.json_schema])
useEffect(() => { useEffect(() => {
// To fix the first input element auto focus, then directly close modal will raise error // To fix the first input element auto focus, then directly close modal will raise error
if (isShow) if (isShow)
} }
}, []) }, [])


const handleTypeChange = useCallback((type: InputVarType) => {
return () => {
const newPayload = produce(tempPayload, (draft) => {
draft.type = type
// Clear default value when switching types
draft.default = undefined
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
if (key !== 'max_length')
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
})
if (type === InputVarType.multiFiles)
draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
}
if (type === InputVarType.paragraph)
draft.max_length = DEFAULT_VALUE_MAX_LEN
})
setTempPayload(newPayload)
const handleJSONSchemaChange = useCallback((value: string) => {
try {
const v = JSON.parse(value)
const res = {
...jsonObjectWrap,
properties: v,
}
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
} }
catch (_e) {
return null
}
}, [handlePayloadChange])

const selectOptions: SelectItem[] = [
{
name: t('appDebug.variableConfig.text-input'),
value: InputVarType.textInput,
},
{
name: t('appDebug.variableConfig.paragraph'),
value: InputVarType.paragraph,
},
{
name: t('appDebug.variableConfig.select'),
value: InputVarType.select,
},
{
name: t('appDebug.variableConfig.number'),
value: InputVarType.number,
},
{
name: t('appDebug.variableConfig.checkbox'),
value: InputVarType.checkbox,
},
...(supportFile ? [
{
name: t('appDebug.variableConfig.single-file'),
value: InputVarType.singleFile,
},
{
name: t('appDebug.variableConfig.multi-files'),
value: InputVarType.multiFiles,
},
] : []),
...((!isBasicApp && isSupportJSON) ? [{
name: t('appDebug.variableConfig.json'),
value: InputVarType.jsonObject,
}] : []),
]

const handleTypeChange = useCallback((item: SelectItem) => {
const type = item.value as InputVarType

const newPayload = produce(tempPayload, (draft) => {
draft.type = type
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
if (key !== 'max_length')
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
})
if (type === InputVarType.multiFiles)
draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
}
if (type === InputVarType.paragraph)
draft.max_length = DEFAULT_VALUE_MAX_LEN
})
setTempPayload(newPayload)
}, [tempPayload]) }, [tempPayload])


const handleVarKeyBlur = useCallback((e: any) => { const handleVarKeyBlur = useCallback((e: any) => {
if (!isVariableNameValid) if (!isVariableNameValid)
return return


// TODO: check if key already exists. should the consider the edit case
// if (varKeys.map(key => key?.trim()).includes(tempPayload.variable.trim())) {
// Toast.notify({
// type: 'error',
// message: t('appDebug.varKeyError.keyAlreadyExists', { key: tempPayload.variable }),
// })
// return
// }

if (!tempPayload.label) { if (!tempPayload.label) {
Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.labelNameRequired') }) Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.labelNameRequired') })
return return
> >
<div className='mb-8' ref={modalRef} tabIndex={-1}> <div className='mb-8' ref={modalRef} tabIndex={-1}>
<div className='space-y-2'> <div className='space-y-2'>

<Field title={t('appDebug.variableConfig.fieldType')}> <Field title={t('appDebug.variableConfig.fieldType')}>
<div className='grid grid-cols-3 gap-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={handleTypeChange(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={handleTypeChange(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={handleTypeChange(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={handleTypeChange(InputVarType.number)} />
{supportFile && <>
<SelectTypeItem type={InputVarType.singleFile} selected={type === InputVarType.singleFile} onClick={handleTypeChange(InputVarType.singleFile)} />
<SelectTypeItem type={InputVarType.multiFiles} selected={type === InputVarType.multiFiles} onClick={handleTypeChange(InputVarType.multiFiles)} />
</>}
</div>
<TypeSelector value={type} items={selectOptions} onSelect={handleTypeChange} />
</Field> </Field>


<Field title={t('appDebug.variableConfig.varName')}> <Field title={t('appDebug.variableConfig.varName')}>
</> </>
)} )}


{type === InputVarType.jsonObject && (
<Field title={t('appDebug.variableConfig.jsonSchema')} isOptional>
<CodeEditor
language={CodeLanguage.json}
value={jsonSchemaStr}
onChange={handleJSONSchemaChange}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{jsonConfigPlaceHolder}</div>
}
/>
</Field>
)}

<div className='!mt-5 flex h-6 items-center space-x-2'> <div className='!mt-5 flex h-6 items-center space-x-2'>
<Checkbox checked={tempPayload.required} disabled={tempPayload.hide} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} /> <Checkbox checked={tempPayload.required} disabled={tempPayload.hide} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
<span className='system-sm-semibold text-text-secondary'>{t('appDebug.variableConfig.required')}</span> <span className='system-sm-semibold text-text-secondary'>{t('appDebug.variableConfig.required')}</span>

+ 97
- 0
web/app/components/app/configuration/config-var/config-modal/type-select.tsx 查看文件

'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import type { InputVarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge'
import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'

export type Item = {
value: InputVarType
name: string
}

type Props = {
value: string | number
onSelect: (value: Item) => void
items: Item[]
popupClassName?: string
popupInnerClassName?: string
readonly?: boolean
hideChecked?: boolean
}
const TypeSelector: FC<Props> = ({
value,
onSelect,
items,
popupInnerClassName,
readonly,
}) => {
const [open, setOpen] = useState(false)
const selectedItem = value ? items.find(item => item.value === value) : undefined

return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<PortalToFollowElemTrigger onClick={() => !readonly && setOpen(v => !v)} className='w-full'>
<div
className={classNames(`group flex h-9 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2 text-sm hover:bg-state-base-hover-alt ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'}`)}
title={selectedItem?.name}
>
<div className='flex items-center'>
<InputVarTypeIcon type={selectedItem?.value as InputVarType} className='size-4 shrink-0 text-text-secondary' />
<span
className={`
ml-1.5 ${!selectedItem?.name && 'text-components-input-text-placeholder'}
`}
>
{selectedItem?.name}
</span>
</div>
<div className='flex items-center space-x-1'>
<Badge uppercase={false}>{inputVarTypeToVarType(selectedItem?.value as InputVarType)}</Badge>
<ChevronDownIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
</div>
</div>

</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[61]'>
<div
className={classNames('w-[432px] rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm', popupInnerClassName)}
>
{items.map((item: Item) => (
<div
key={item.value}
className={'flex h-9 cursor-pointer items-center justify-between rounded-lg px-2 text-text-secondary hover:bg-state-base-hover'}
title={item.name}
onClick={() => {
onSelect(item)
setOpen(false)
}}
>
<div className='flex items-center space-x-2'>
<InputVarTypeIcon type={item.value} className='size-4 shrink-0 text-text-secondary' />
<span title={item.name}>{item.name}</span>
</div>
<Badge uppercase={false}>{inputVarTypeToVarType(item.value)}</Badge>
</div>
))}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

export default TypeSelector

+ 25
- 3
web/app/components/app/configuration/config-var/index.tsx 查看文件

import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import type { PromptVariable } from '@/models/debug' import type { PromptVariable } from '@/models/debug'
import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { getNewVar } from '@/utils/var'
import { getNewVar, hasDuplicateStr } from '@/utils/var'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
delete draft[currIndex].options delete draft[currIndex].options
}) })


const newList = newPromptVariables
let errorMsgKey = ''
let typeName = ''
if (hasDuplicateStr(newList.map(item => item.key))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.varName'
}
else if (hasDuplicateStr(newList.map(item => item.name as string))) {
errorMsgKey = 'appDebug.varKeyError.keyAlreadyExists'
typeName = 'appDebug.variableConfig.labelName'
}

if (errorMsgKey) {
Toast.notify({
type: 'error',
message: t(errorMsgKey, { key: t(typeName) }),
})
return false
}

onPromptVariablesChange?.(newPromptVariables) onPromptVariablesChange?.(newPromptVariables)
return true
} }


const { setShowExternalDataToolModal } = useModalContext() const { setShowExternalDataToolModal } = useModalContext()
const handleConfig = ({ key, type, index, name, config, icon, icon_background }: ExternalDataToolParams) => { const handleConfig = ({ key, type, index, name, config, icon, icon_background }: ExternalDataToolParams) => {
// setCurrKey(key) // setCurrKey(key)
setCurrIndex(index) setCurrIndex(index)
if (type !== 'string' && type !== 'paragraph' && type !== 'select' && type !== 'number') {
if (type !== 'string' && type !== 'paragraph' && type !== 'select' && type !== 'number' && type !== 'checkbox') {
handleOpenExternalDataToolModal({ key, type, index, name, config, icon, icon_background }, promptVariables) handleOpenExternalDataToolModal({ key, type, index, name, config, icon, icon_background }, promptVariables)
return return
} }
isShow={isShowEditModal} isShow={isShowEditModal}
onClose={hideEditModal} onClose={hideEditModal}
onConfirm={(item) => { onConfirm={(item) => {
updatePromptVariableItem(item)
const isValid = updatePromptVariableItem(item)
if (!isValid) return
hideEditModal() hideEditModal()
}} }}
varKeys={promptVariables.map(v => v.key)} varKeys={promptVariables.map(v => v.key)}

+ 1
- 0
web/app/components/app/configuration/config-var/select-var-type.tsx 查看文件

<SelectItem type={InputVarType.paragraph} value='paragraph' text={t('appDebug.variableConfig.paragraph')} onClick={handleChange}></SelectItem> <SelectItem type={InputVarType.paragraph} value='paragraph' text={t('appDebug.variableConfig.paragraph')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.select} value='select' text={t('appDebug.variableConfig.select')} onClick={handleChange}></SelectItem> <SelectItem type={InputVarType.select} value='select' text={t('appDebug.variableConfig.select')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.number} value='number' text={t('appDebug.variableConfig.number')} onClick={handleChange}></SelectItem> <SelectItem type={InputVarType.number} value='number' text={t('appDebug.variableConfig.number')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.checkbox} value='checkbox' text={t('appDebug.variableConfig.checkbox')} onClick={handleChange}></SelectItem>
</div> </div>
<div className='h-px border-t border-components-panel-border'></div> <div className='h-px border-t border-components-panel-border'></div>
<div className='p-1'> <div className='p-1'>

+ 2
- 0
web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx 查看文件

return t('tools.setBuiltInTools.number') return t('tools.setBuiltInTools.number')
if (type === 'text-input') if (type === 'text-input')
return t('tools.setBuiltInTools.string') return t('tools.setBuiltInTools.string')
if (type === 'checkbox')
return 'boolean'
if (type === 'file') if (type === 'file')
return t('tools.setBuiltInTools.file') return t('tools.setBuiltInTools.file')
return type return type

+ 12
- 1
web/app/components/app/configuration/debug/chat-user-input.tsx 查看文件

import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import type { Inputs } from '@/models/debug' import type { Inputs } from '@/models/debug'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'


type Props = { type Props = {
inputs: Inputs inputs: Inputs
return obj return obj
})() })()


const handleInputValueChange = (key: string, value: string) => {
const handleInputValueChange = (key: string, value: string | boolean) => {
if (!(key in promptVariableObj)) if (!(key in promptVariableObj))
return return


className='mb-4 last-of-type:mb-0' className='mb-4 last-of-type:mb-0'
> >
<div> <div>
{type !== 'checkbox' && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'> <div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{name || key}</div> <div className='truncate'>{name || key}</div>
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>} {!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div> </div>
)}
<div className='grow'> <div className='grow'>
{type === 'string' && ( {type === 'string' && (
<Input <Input
maxLength={max_length || DEFAULT_VALUE_MAX_LEN} maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/> />
)} )}
{type === 'checkbox' && (
<BoolInput
name={name || key}
value={!!inputs[key]}
required={required}
onChange={(value) => { handleInputValueChange(key, value) }}
/>
)}
</div> </div>
</div> </div>
</div> </div>

+ 2
- 2
web/app/components/app/configuration/debug/index.tsx 查看文件

import TooltipPlus from '@/app/components/base/tooltip' import TooltipPlus from '@/app/components/base/tooltip'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app' import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import { formatBooleanInputs, promptVariablesToUserInputsForm } from '@/utils/model-config'
import TextGeneration from '@/app/components/app/text-generate/item' import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import type { Inputs } from '@/models/debug' import type { Inputs } from '@/models/debug'
} }


const data: Record<string, any> = { const data: Record<string, any> = {
inputs,
inputs: formatBooleanInputs(modelConfig.configs.prompt_variables, inputs),
model_config: postModelConfig, model_config: postModelConfig,
} }



+ 1
- 1
web/app/components/app/configuration/index.tsx 查看文件

useModelListAndDefaultModelAndCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel,
useTextGenerationCurrentProviderAndModelAndModelList, useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks' } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { fetchCollectionList } from '@/service/tools'
import type { Collection } from '@/app/components/tools/types' import type { Collection } from '@/app/components/tools/types'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { import {
import { MittProvider } from '@/context/mitt-context' import { MittProvider } from '@/context/mitt-context'
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { fetchCollectionList } from '@/service/tools'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'


type PublishConfig = { type PublishConfig = {

+ 16
- 5
web/app/components/app/configuration/prompt-value-panel/index.tsx 查看文件

import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'


export type IPromptValuePanelProps = { export type IPromptValuePanelProps = {
appType: AppType appType: AppType
else { return !modelConfig.configs.prompt_template } else { return !modelConfig.configs.prompt_template }
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType]) }, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])


const handleInputValueChange = (key: string, value: string) => {
const handleInputValueChange = (key: string, value: string | boolean) => {
if (!(key in promptVariableObj)) if (!(key in promptVariableObj))
return return


className='mb-4 last-of-type:mb-0' className='mb-4 last-of-type:mb-0'
> >
<div> <div>
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{name || key}</div>
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
{type !== 'checkbox' && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{name || key}</div>
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
)}
<div className='grow'> <div className='grow'>
{type === 'string' && ( {type === 'string' && (
<Input <Input
maxLength={max_length || DEFAULT_VALUE_MAX_LEN} maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/> />
)} )}
{type === 'checkbox' && (
<BoolInput
name={name || key}
value={!!inputs[key]}
required={required}
onChange={(value) => { handleInputValueChange(key, value) }}
/>
)}
</div> </div>
</div> </div>
</div> </div>

+ 3
- 2
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx 查看文件

import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types' import type { FileEntity } from '../../file-uploader/types'
import { formatBooleanInputs } from '@/utils/model-config'
import Avatar from '../../avatar' import Avatar from '../../avatar'


const ChatWrapper = () => { const ChatWrapper = () => {


let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
if (requiredVars.length) { if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => { requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput) if (hasEmptyInput)
const data: any = { const data: any = {
query: message, query: message,
files, files,
inputs: currentConversationId ? currentConversationInputs : newConversationInputs,
inputs: formatBooleanInputs(inputsForms, currentConversationId ? currentConversationInputs : newConversationInputs),
conversation_id: currentConversationId, conversation_id: currentConversationId,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null, parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
} }

+ 16
- 1
web/app/components/base/chat/chat-with-history/hooks.tsx 查看文件

type: 'number', type: 'number',
} }
} }

if(item.checkbox) {
return {
...item.checkbox,
default: false,
type: 'checkbox',
}
}
if (item.select) { if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable]) const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return { return {
} }
} }


if (item.json_object) {
return {
...item.json_object,
type: 'json_object',
}
}

let value = initInputs[item['text-input'].variable] let value = initInputs[item['text-input'].variable]
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length) if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
value = value.slice(0, item['text-input'].max_length) value = value.slice(0, item['text-input'].max_length)


let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
if (requiredVars.length) { if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => { requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput) if (hasEmptyInput)

+ 31
- 6
web/app/components/base/chat/chat-with-history/inputs-form/content.tsx 查看文件

import { PortalSelect } from '@/app/components/base/select' import { PortalSelect } from '@/app/components/base/select'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'


type Props = { type Props = {
showTip?: boolean showTip?: boolean
<div className='space-y-4'> <div className='space-y-4'>
{visibleInputsForms.map(form => ( {visibleInputsForms.map(form => (
<div key={form.variable} className='space-y-1'> <div key={form.variable} className='space-y-1'>
<div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
{!form.required && (
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
{form.type !== InputVarType.checkbox && (
<div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div>
{!form.required && (
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
)}
</div>
)}
{form.type === InputVarType.textInput && ( {form.type === InputVarType.textInput && (
<Input <Input
value={inputsFormValue?.[form.variable] || ''} value={inputsFormValue?.[form.variable] || ''}
placeholder={form.label} placeholder={form.label}
/> />
)} )}
{form.type === InputVarType.checkbox && (
<BoolInput
name={form.label}
value={!!inputsFormValue?.[form.variable]}
required={form.required}
onChange={value => handleFormChange(form.variable, value)}
/>
)}
{form.type === InputVarType.select && ( {form.type === InputVarType.select && (
<PortalSelect <PortalSelect
popupClassName='w-[200px]' popupClassName='w-[200px]'
}} }}
/> />
)} )}
{form.type === InputVarType.jsonObject && (
<CodeEditor
language={CodeLanguage.json}
value={inputsFormValue?.[form.variable] || ''}
onChange={v => handleFormChange(form.variable, v)}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{form.json_schema}</div>
}
/>
)}
</div> </div>
))} ))}
{showTip && ( {showTip && (

+ 1
- 1
web/app/components/base/chat/chat/check-input-forms-hooks.ts 查看文件

const checkInputsForm = useCallback((inputs: Record<string, any>, inputsForm: InputForm[]) => { const checkInputsForm = useCallback((inputs: Record<string, any>, inputsForm: InputForm[]) => {
let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForm.filter(({ required }) => required)
const requiredVars = inputsForm.filter(({ required, type }) => required && type !== InputVarType.checkbox) // boolean can be not checked


if (requiredVars?.length) { if (requiredVars?.length) {
requiredVars.forEach(({ variable, label, type }) => { requiredVars.forEach(({ variable, label, type }) => {

+ 6
- 0
web/app/components/base/chat/chat/utils.ts 查看文件



inputsForm.forEach((item) => { inputsForm.forEach((item) => {
const inputValue = inputs[item.variable] const inputValue = inputs[item.variable]
// set boolean type default value
if(item.type === InputVarType.checkbox) {
processedInputs[item.variable] = !!inputValue
return
}

if (!inputValue) if (!inputValue)
return return



+ 1
- 1
web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx 查看文件



let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox) // boolean can be not checked
if (requiredVars.length) { if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => { requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput) if (hasEmptyInput)

+ 15
- 1
web/app/components/base/chat/embedded-chatbot/hooks.tsx 查看文件

type: 'number', type: 'number',
} }
} }
if (item.checkbox) {
return {
...item.checkbox,
default: false,
type: 'checkbox',
}
}
if (item.select) { if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable]) const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return { return {
} }
} }


if (item.json_object) {
return {
...item.json_object,
type: 'json_object',
}
}

let value = initInputs[item['text-input'].variable] let value = initInputs[item['text-input'].variable]
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length) if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
value = value.slice(0, item['text-input'].max_length) value = value.slice(0, item['text-input'].max_length)


let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
if (requiredVars.length) { if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => { requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput) if (hasEmptyInput)

+ 25
- 0
web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx 查看文件

import { PortalSelect } from '@/app/components/base/select' import { PortalSelect } from '@/app/components/base/select'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'


type Props = { type Props = {
showTip?: boolean showTip?: boolean
<div className='space-y-4'> <div className='space-y-4'>
{visibleInputsForms.map(form => ( {visibleInputsForms.map(form => (
<div key={form.variable} className='space-y-1'> <div key={form.variable} className='space-y-1'>
{form.type !== InputVarType.checkbox && (
<div className='flex h-6 items-center gap-1'> <div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div> <div className='system-md-semibold text-text-secondary'>{form.label}</div>
{!form.required && ( {!form.required && (
<div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div> <div className='system-xs-regular text-text-tertiary'>{t('appDebug.variableTable.optional')}</div>
)} )}
</div> </div>
)}
{form.type === InputVarType.textInput && ( {form.type === InputVarType.textInput && (
<Input <Input
value={inputsFormValue?.[form.variable] || ''} value={inputsFormValue?.[form.variable] || ''}
placeholder={form.label} placeholder={form.label}
/> />
)} )}
{form.type === InputVarType.checkbox && (
<BoolInput
name={form.label}
value={inputsFormValue?.[form.variable]}
required={form.required}
onChange={value => handleFormChange(form.variable, value)}
/>
)}
{form.type === InputVarType.select && ( {form.type === InputVarType.select && (
<PortalSelect <PortalSelect
popupClassName='w-[200px]' popupClassName='w-[200px]'
}} }}
/> />
)} )}
{form.type === InputVarType.jsonObject && (
<CodeEditor
language={CodeLanguage.json}
value={inputsFormValue?.[form.variable] || ''}
onChange={v => handleFormChange(form.variable, v)}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{form.json_schema}</div>
}
/>
)}
</div> </div>
))} ))}
{showTip && ( {showTip && (

+ 1
- 1
web/app/components/base/form/types.ts 查看文件

secretInput = 'secret-input', secretInput = 'secret-input',
select = 'select', select = 'select',
radio = 'radio', radio = 'radio',
boolean = 'boolean',
checkbox = 'checkbox',
files = 'files', files = 'files',
file = 'file', file = 'file',
modelSelector = 'model-selector', modelSelector = 'model-selector',

+ 0
- 1
web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx 查看文件

return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)), editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
) )
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])


return null return null

+ 0
- 1
web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx 查看文件

return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)), editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
) )
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])


return null return null

+ 0
- 1
web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx 查看文件

return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)), editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
) )
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])


return null return null

+ 14
- 0
web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx 查看文件

required: false, required: false,
} }
} }
if(item.checkbox) {
return {
...item.checkbox,
type: 'checkbox',
required: false,
}
}
if (item.select) { if (item.select) {
return { return {
...item.select, ...item.select,
} }
} }


if (item.json_object) {
return {
...item.json_object,
type: 'json_object',
}
}

return { return {
...item['text-input'], ...item['text-input'],
type: 'text-input', type: 'text-input',

+ 2
- 0
web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx 查看文件

return t('tools.setBuiltInTools.number') return t('tools.setBuiltInTools.number')
if (type === 'text-input') if (type === 'text-input')
return t('tools.setBuiltInTools.string') return t('tools.setBuiltInTools.string')
if (type === 'checkbox')
return 'boolean'
if (type === 'file') if (type === 'file')
return t('tools.setBuiltInTools.file') return t('tools.setBuiltInTools.file')
if (type === 'array[tools]') if (type === 'array[tools]')

+ 5
- 2
web/app/components/share/text-generation/result/index.tsx 查看文件

import { import {
getFilesInLogs, getFilesInLogs,
} from '@/app/components/base/file-uploader/utils' } from '@/app/components/base/file-uploader/utils'
import { formatBooleanInputs } from '@/utils/model-config'


export type IResultProps = { export type IResultProps = {
isWorkflow: boolean isWorkflow: boolean
} }


let hasEmptyInput = '' let hasEmptyInput = ''
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
if(type === 'boolean')
return false // boolean input is not required
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res return res
}) || [] // compatible with old version }) || [] // compatible with old version
return return


const data: Record<string, any> = { const data: Record<string, any> = {
inputs,
inputs: formatBooleanInputs(promptConfig?.prompt_variables, inputs),
} }
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) { if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
data.files = completionFiles.map((item) => { data.files = completionFiles.map((item) => {

+ 27
- 2
web/app/components/share/text-generation/run-once/index.tsx 查看文件

import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'


export type IRunOnceProps = { export type IRunOnceProps = {
siteInfo: SiteInfo siteInfo: SiteInfo
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null {(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
: promptConfig.prompt_variables.map(item => ( : promptConfig.prompt_variables.map(item => (
<div className='mt-4 w-full' key={item.key}> <div className='mt-4 w-full' key={item.key}>
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
{item.type !== 'boolean' && (
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
)}
<div className='mt-1'> <div className='mt-1'>
{item.type === 'select' && ( {item.type === 'select' && (
<Select <Select
className='h-[104px] sm:text-xs' className='h-[104px] sm:text-xs'
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`} placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[item.key]} value={inputs[item.key]}
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/> />
)} )}
{item.type === 'number' && ( {item.type === 'number' && (
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }} onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/> />
)} )}
{item.type === 'boolean' && (
<BoolInput
name={item.name || item.key}
value={!!inputs[item.key]}
required={item.required}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'file' && ( {item.type === 'file' && (
<FileUploaderInAttachmentWrapper <FileUploaderInAttachmentWrapper
value={inputs[item.key] ? [inputs[item.key]] : []} value={inputs[item.key] ? [inputs[item.key]] : []}
}} }}
/> />
)} )}
{item.type === 'json_object' && (
<CodeEditor
language={CodeLanguage.json}
value={inputs[item.key]}
onChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{item.json_schema}</div>
}
/>
)}
</div> </div>
</div> </div>
))} ))}

+ 2
- 0
web/app/components/tools/utils/to-form-schema.ts 查看文件

return 'text-input' return 'text-input'
case 'number': case 'number':
return 'number-input' return 'number-input'
case 'boolean':
return 'checkbox'
default: default:
return type return type
} }

+ 38
- 0
web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx 查看文件

'use client'
import Checkbox from '@/app/components/base/checkbox'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'

type Props = {
name: string
value: boolean
required?: boolean
onChange: (value: boolean) => void
}

const BoolInput: FC<Props> = ({
value,
onChange,
name,
required,
}) => {
const { t } = useTranslation()
const handleChange = useCallback(() => {
onChange(!value)
}, [value, onChange])
return (
<div className='flex h-6 items-center gap-2'>
<Checkbox
className='!h-4 !w-4'
checked={!!value}
onCheck={handleChange}
/>
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
{name}
{!required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
</div>
</div>
)
}
export default React.memo(BoolInput)

+ 24
- 1
web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx 查看文件

import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FileEntity } from '@/app/components/base/file-uploader/types'
import BoolInput from './bool-input'


type Props = { type Props = {
payload: InputVar payload: InputVar
return '' return ''
})() })()


const isBooleanType = type === InputVarType.checkbox
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type) const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
const isContext = type === InputVarType.contexts const isContext = type === InputVarType.contexts
const isIterator = type === InputVarType.iterator const isIterator = type === InputVarType.iterator


return ( return (
<div className={cn(className)}> <div className={cn(className)}>
{!isArrayLikeType && (
{!isArrayLikeType && !isBooleanType && (
<div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'> <div className='system-sm-semibold mb-1 flex h-6 items-center gap-1 text-text-secondary'>
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div> <div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
{!payload.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>} {!payload.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
) )
} }


{isBooleanType && (
<BoolInput
name={payload.label as string}
value={!!value}
required={payload.required}
onChange={onChange}
/>
)}

{ {
type === InputVarType.json && ( type === InputVarType.json && (
<CodeEditor <CodeEditor
/> />
) )
} }
{ type === InputVarType.jsonObject && (
<CodeEditor
value={value}
language={CodeLanguage.json}
onChange={onChange}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{payload.json_schema}</div>
}
/>
)}
{(type === InputVarType.singleFile) && ( {(type === InputVarType.singleFile) && (
<FileUploaderInAttachmentWrapper <FileUploaderInAttachmentWrapper
value={singleFileValue} value={singleFileValue}

+ 3
- 1
web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx 查看文件

} & Partial<SpecialResultPanelProps> } & Partial<SpecialResultPanelProps>


function formatValue(value: string | any, type: InputVarType) { function formatValue(value: string | any, type: InputVarType) {
if(type === InputVarType.checkbox)
return !!value
if(value === undefined || value === null) if(value === undefined || value === null)
return value return value
if (type === InputVarType.number) if (type === InputVarType.number)


form.inputs.forEach((input) => { form.inputs.forEach((input) => {
const value = form.values[input.variable] as any const value = form.values[input.variable] as any
if (!errMsg && input.required && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
if (!errMsg && input.required && (input.type !== InputVarType.checkbox) && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label }) errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label })


if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) { if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) {

+ 1
- 1
web/app/components/workflow/nodes/_base/components/form-input-item.tsx 查看文件

const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isAppSelector = type === FormTypeEnum.appSelector const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector const isModelSelector = type === FormTypeEnum.modelSelector
const showTypeSwitch = isNumber || isObject || isArray
const showTypeSwitch = isNumber || isBoolean || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable const showVariableSelector = isFile || varInput?.type === VarKindType.variable



+ 3
- 1
web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx 查看文件

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { RiAlignLeft, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
import { RiAlignLeft, RiBracesLine, RiCheckboxLine, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
import { InputVarType } from '../../../types' import { InputVarType } from '../../../types'


type Props = { type Props = {
[InputVarType.paragraph]: RiAlignLeft, [InputVarType.paragraph]: RiAlignLeft,
[InputVarType.select]: RiCheckboxMultipleLine, [InputVarType.select]: RiCheckboxMultipleLine,
[InputVarType.number]: RiHashtag, [InputVarType.number]: RiHashtag,
[InputVarType.checkbox]: RiCheckboxLine,
[InputVarType.jsonObject]: RiBracesLine,
[InputVarType.singleFile]: RiFileList2Line, [InputVarType.singleFile]: RiFileList2Line,
[InputVarType.multiFiles]: RiFileCopy2Line, [InputVarType.multiFiles]: RiFileCopy2Line,
} as any)[type] || RiTextSnippet } as any)[type] || RiTextSnippet

+ 22
- 3
web/app/components/workflow/nodes/_base/components/variable/utils.ts 查看文件

) )
} }


const inputVarTypeToVarType = (type: InputVarType): VarType => {
export const inputVarTypeToVarType = (type: InputVarType): VarType => {
return ({ return ({
[InputVarType.number]: VarType.number, [InputVarType.number]: VarType.number,
[InputVarType.checkbox]: VarType.boolean,
[InputVarType.singleFile]: VarType.file, [InputVarType.singleFile]: VarType.file,
[InputVarType.multiFiles]: VarType.arrayFile, [InputVarType.multiFiles]: VarType.arrayFile,
[InputVarType.jsonObject]: VarType.object,
} as any)[type] || VarType.string } as any)[type] || VarType.string
} }


variables, variables,
} = data as StartNodeType } = data as StartNodeType
res.vars = variables.map((v) => { res.vars = variables.map((v) => {
return {
const type = inputVarTypeToVarType(v.type)
const varRes: Var = {
variable: v.variable, variable: v.variable,
type: inputVarTypeToVarType(v.type),
type,
isParagraph: v.type === InputVarType.paragraph, isParagraph: v.type === InputVarType.paragraph,
isSelect: v.type === InputVarType.select, isSelect: v.type === InputVarType.select,
options: v.options, options: v.options,
required: v.required, required: v.required,
} }
try {
if(type === VarType.object && v.json_schema) {
varRes.children = {
schema: JSON.parse(v.json_schema),
}
}
}
catch (error) {
console.error('Error formatting variable:', error)
}

return varRes
}) })
if (isChatMode) { if (isChatMode) {
res.vars.push({ res.vars.push({
return VarType.string return VarType.string
case VarType.arrayNumber: case VarType.arrayNumber:
return VarType.number return VarType.number
case VarType.arrayBoolean:
return VarType.boolean
case VarType.arrayObject: case VarType.arrayObject:
return VarType.object return VarType.object
case VarType.array: case VarType.array:
return VarType.number return VarType.number
case VarType.arrayObject: case VarType.arrayObject:
return VarType.object return VarType.object
case VarType.arrayBoolean:
return VarType.boolean
case VarType.array: case VarType.array:
return VarType.any return VarType.any
case VarType.arrayFile: case VarType.arrayFile:

+ 1
- 1
web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx 查看文件

onChange: (value: string) => void onChange: (value: string) => void
} }


const TYPES = [VarType.string, VarType.number, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.object]
const TYPES = [VarType.string, VarType.number, VarType.boolean, VarType.arrayNumber, VarType.arrayString, VarType.arrayBoolean, VarType.arrayObject, VarType.object]
const VarReferencePicker: FC<Props> = ({ const VarReferencePicker: FC<Props> = ({
readonly, readonly,
className, className,

+ 1
- 1
web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx 查看文件

isRunAfterSingleRun={isRunAfterSingleRun} isRunAfterSingleRun={isRunAfterSingleRun}
updateNodeRunningStatus={updateNodeRunningStatus} updateNodeRunningStatus={updateNodeRunningStatus}
onSingleRunClicked={handleSingleRun} onSingleRunClicked={handleSingleRun}
nodeInfo={nodeInfo}
nodeInfo={nodeInfo!}
singleRunResult={runResult!} singleRunResult={runResult!}
isPaused={isPaused} isPaused={isPaused}
{...passedLogParams} {...passedLogParams}

+ 1
- 3
web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx 查看文件

updateNodeRunningStatus(hidePageOneStepFinishedStatus) updateNodeRunningStatus(hidePageOneStepFinishedStatus)
resetHidePageStatus() resetHidePageStatus()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus]) }, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus])


useEffect(() => { useEffect(() => {


useEffect(() => { useEffect(() => {
resetHidePageStatus() resetHidePageStatus()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodeId]) }, [nodeId])


const handlePageVisibilityChange = useCallback(() => { const handlePageVisibilityChange = useCallback(() => {
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)} status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens} total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens}
created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by} created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by}
nodeInfo={nodeInfo}
nodeInfo={runResult as NodeTracing}
showSteps={false} showSteps={false}
/> />
</div> </div>

+ 1
- 2
web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts 查看文件

checkValid, checkValid,
} = oneStepRunRes } = oneStepRunRes


const nodeInfo = runResult
const { const {
nodeInfo,
...singleRunParams ...singleRunParams
} = useSingleRunFormParamsHooks(blockType)({ } = useSingleRunFormParamsHooks(blockType)({
id, id,
setTabType(TabType.lastRun) setTabType(TabType.lastRun)


setInitShowLastRunTab(false) setInitShowLastRunTab(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initShowLastRunTab]) }, [initShowLastRunTab])
const invalidLastRun = useInvalidLastRun(appId!, id) const invalidLastRun = useInvalidLastRun(appId!, id)



+ 29
- 28
web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts 查看文件

return InputVarType.paragraph return InputVarType.paragraph
if (type === VarType.number) if (type === VarType.number)
return InputVarType.number return InputVarType.number
if (type === VarType.boolean)
return InputVarType.checkbox
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type)) if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
return InputVarType.json return InputVarType.json
if (type === VarType.file) if (type === VarType.file)
const isPaused = isPausedRef.current const isPaused = isPausedRef.current


// The backend don't support pause the single run, so the frontend handle the pause state. // The backend don't support pause the single run, so the frontend handle the pause state.
if(isPaused)
if (isPaused)
return return


const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded
if(!canRunLastRun) {
if (!canRunLastRun) {
doSetRunResult(data) doSetRunResult(data)
return return
} }
const { getNodes } = store.getState() const { getNodes } = store.getState()
const nodes = getNodes() const nodes = getNodes()
appendNodeInspectVars(id, vars, nodes) appendNodeInspectVars(id, vars, nodes)
if(data?.status === NodeRunningStatus.Succeeded) {
if (data?.status === NodeRunningStatus.Succeeded) {
invalidLastRun() invalidLastRun()
if(isStartNode)
if (isStartNode)
invalidateSysVarValues() invalidateSysVarValues()
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh. invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
} }
}) })
} }
const checkValidWrap = () => { const checkValidWrap = () => {
if(!checkValid)
if (!checkValid)
return { isValid: true, errorMessage: '' } return { isValid: true, errorMessage: '' }
const res = checkValid(data, t, moreDataForCheckValid) const res = checkValid(data, t, moreDataForCheckValid)
if(!res.isValid) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: res.errorMessage,
})
if (!res.isValid) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: res.errorMessage,
})
} }
return res return res
} }
const { isValid } = checkValidWrap() const { isValid } = checkValidWrap()
setCanShowSingleRun(isValid) setCanShowSingleRun(isValid)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data._isSingleRun]) }, [data._isSingleRun])


useEffect(() => { useEffect(() => {
if (!isIteration && !isLoop) { if (!isIteration && !isLoop) {
const isStartNode = data.type === BlockEnum.Start const isStartNode = data.type === BlockEnum.Start
const postData: Record<string, any> = {} const postData: Record<string, any> = {}
if(isStartNode) {
if (isStartNode) {
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
if(isChatMode)
if (isChatMode)
postData.conversation_id = '' postData.conversation_id = ''


postData.inputs = inputs postData.inputs = inputs
{ {
onWorkflowStarted: noop, onWorkflowStarted: noop,
onWorkflowFinished: (params) => { onWorkflowFinished: (params) => {
if(isPausedRef.current)
if (isPausedRef.current)
return return
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
setIterationRunResult(newIterationRunResult) setIterationRunResult(newIterationRunResult)
}, },
onError: () => { onError: () => {
if(isPausedRef.current)
if (isPausedRef.current)
return return
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
{ {
onWorkflowStarted: noop, onWorkflowStarted: noop,
onWorkflowFinished: (params) => { onWorkflowFinished: (params) => {
if(isPausedRef.current)
if (isPausedRef.current)
return return
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
setLoopRunResult(newLoopRunResult) setLoopRunResult(newLoopRunResult)
}, },
onError: () => { onError: () => {
if(isPausedRef.current)
if (isPausedRef.current)
return return
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
hasError = true hasError = true
invalidLastRun() invalidLastRun()
if (!isIteration && !isLoop) { if (!isIteration && !isLoop) {
if(isPausedRef.current)
if (isPausedRef.current)
return return
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
}) })
} }
} }
if(isPausedRef.current)
if (isPausedRef.current)
return return


if (!isIteration && !isLoop && !hasError) { if (!isIteration && !isLoop && !hasError) {
if(isPausedRef.current)
if (isPausedRef.current)
return return
handleNodeDataUpdate({ handleNodeDataUpdate({
id, id,
} }
} }
return { return {
label: item.label || item.variable,
label: (typeof item.label === 'object' ? item.label.variable : item.label) || item.variable,
variable: item.variable, variable: item.variable,
type: varTypeToInputVarType(originalVar.type, { type: varTypeToInputVarType(originalVar.type, {
isSelect: !!originalVar.isSelect, isSelect: !!originalVar.isSelect,

+ 0
- 1
web/app/components/workflow/nodes/_base/hooks/use-toggle-expend.ts 查看文件

useEffect(() => { useEffect(() => {
if (!ref?.current) return if (!ref?.current) return
setWrapHeight(ref.current?.clientHeight) setWrapHeight(ref.current?.clientHeight)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isExpand]) }, [isExpand])


const wrapClassName = (() => { const wrapClassName = (() => {

+ 18
- 6
web/app/components/workflow/nodes/assigner/components/var-list/index.tsx 查看文件

import type { AssignerNodeOperation } from '../../types' import type { AssignerNodeOperation } from '../../types'
import ListNoDataPlaceholder from '@/app/components/workflow/nodes/_base/components/list-no-data-placeholder' import ListNoDataPlaceholder from '@/app/components/workflow/nodes/_base/components/list-no-data-placeholder'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import type { ValueSelector, Var, VarType } from '@/app/components/workflow/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'


type Props = { type Props = {
readonly: boolean readonly: boolean
} }
}, [list, onChange]) }, [list, onChange])


const handleOperationChange = useCallback((index: number) => {
const handleOperationChange = useCallback((index: number, varType: VarType) => {
return (item: { value: string | number }) => { return (item: { value: string | number }) => {
const newList = produce(list, (draft) => { const newList = produce(list, (draft) => {
draft[index].operation = item.value as WriteMode draft[index].operation = item.value as WriteMode
draft[index].value = '' // Clear value when operation changes draft[index].value = '' // Clear value when operation changes
if (item.value === WriteMode.set || item.value === WriteMode.increment || item.value === WriteMode.decrement if (item.value === WriteMode.set || item.value === WriteMode.increment || item.value === WriteMode.decrement
|| item.value === WriteMode.multiply || item.value === WriteMode.divide)
|| item.value === WriteMode.multiply || item.value === WriteMode.divide) {
if(varType === VarType.boolean)
draft[index].value = false
draft[index].input_type = AssignerNodeInputType.constant draft[index].input_type = AssignerNodeInputType.constant
else
}
else {
draft[index].input_type = AssignerNodeInputType.variable draft[index].input_type = AssignerNodeInputType.variable
}
}) })
onChange(newList) onChange(newList)
} }
}, [list, onChange]) }, [list, onChange])


const handleToAssignedVarChange = useCallback((index: number) => { const handleToAssignedVarChange = useCallback((index: number) => {
return (value: ValueSelector | string | number) => {
return (value: ValueSelector | string | number | boolean) => {
const newList = produce(list, (draft) => { const newList = produce(list, (draft) => {
draft[index].value = value as ValueSelector draft[index].value = value as ValueSelector
}) })
value={item.operation} value={item.operation}
placeholder='Operation' placeholder='Operation'
disabled={!item.variable_selector || item.variable_selector.length === 0} disabled={!item.variable_selector || item.variable_selector.length === 0}
onSelect={handleOperationChange(index)}
onSelect={handleOperationChange(index, assignedVarType!)}
assignedVarType={assignedVarType} assignedVarType={assignedVarType}
writeModeTypes={writeModeTypes} writeModeTypes={writeModeTypes}
writeModeTypesArr={writeModeTypesArr} writeModeTypesArr={writeModeTypesArr}
className='w-full' className='w-full'
/> />
)} )}
{assignedVarType === 'boolean' && (
<BoolValue
value={item.value as boolean}
onChange={value => handleToAssignedVarChange(index)(value)}
/>
)}
{assignedVarType === 'object' && ( {assignedVarType === 'object' && (
<CodeEditor <CodeEditor
value={item.value as string} value={item.value as string}

+ 1
- 1
web/app/components/workflow/nodes/assigner/default.ts 查看文件

if (value.operation === WriteMode.set || value.operation === WriteMode.increment if (value.operation === WriteMode.set || value.operation === WriteMode.increment
|| value.operation === WriteMode.decrement || value.operation === WriteMode.multiply || value.operation === WriteMode.decrement || value.operation === WriteMode.multiply
|| value.operation === WriteMode.divide) { || value.operation === WriteMode.divide) {
if (!value.value && typeof value.value !== 'number')
if (!value.value && value.value !== false && typeof value.value !== 'number')
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.variable') }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.variable') })
} }
else if (!value.value?.length) { else if (!value.value?.length) {

+ 1
- 1
web/app/components/workflow/nodes/assigner/utils.ts 查看文件

] ]
} }


if (writeModeTypes && ['string', 'object'].includes(assignedVarType || '')) {
if (writeModeTypes && ['string', 'boolean', 'object'].includes(assignedVarType || '')) {
return writeModeTypes.map(type => ({ return writeModeTypes.map(type => ({
value: type, value: type,
name: type, name: type,

+ 2
- 3
web/app/components/workflow/nodes/code/use-config.ts 查看文件

}) })
syncOutputKeyOrders(defaultConfig.outputs) syncOutputKeyOrders(defaultConfig.outputs)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultConfig]) }, [defaultConfig])


const handleCodeChange = useCallback((code: string) => { const handleCodeChange = useCallback((code: string) => {
}, [allLanguageDefault, inputs, setInputs]) }, [allLanguageDefault, inputs, setInputs])


const handleSyncFunctionSignature = useCallback(() => { const handleSyncFunctionSignature = useCallback(() => {
const generateSyncSignatureCode = (code: string) => {
const generateSyncSignatureCode = (code: string) => {
let mainDefRe let mainDefRe
let newMainDef let newMainDef
if (inputs.code_language === CodeLanguage.javascript) { if (inputs.code_language === CodeLanguage.javascript) {
}) })


const filterVar = useCallback((varPayload: Var) => { const filterVar = useCallback((varPayload: Var) => {
return [VarType.string, VarType.number, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.file, VarType.arrayFile].includes(varPayload.type)
return [VarType.string, VarType.number, VarType.boolean, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayBoolean, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, []) }, [])


const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => { const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => {

+ 28
- 5
web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx 查看文件

import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { SimpleSelect as Select } from '@/app/components/base/select' import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow' import { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName' const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'


const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type' const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'


const handleUpdateConditionValue = useCallback((value: string) => {
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
const handleUpdateConditionValue = useCallback((value: string | boolean) => {
if (value === condition.value || (isArrayValue && value === (condition.value as string[])?.[0]))
return return
const newCondition = { const newCondition = {
...condition, ...condition,
value: isArrayValue ? [value] : value,
value: isArrayValue ? [value as string] : value,
} }
doUpdateCondition(newCondition) doUpdateCondition(newCondition)
}, [condition, doUpdateCondition, isArrayValue]) }, [condition, doUpdateCondition, isArrayValue])
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition]) }, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])


const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => { const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
const {
conversationVariables,
} = workflowStore.getState()
const resolvedVarType = getVarType({ const resolvedVarType = getVarType({
valueSelector, valueSelector,
conversationVariables,
availableNodes, availableNodes,
isChatMode, isChatMode,
}) })
const newCondition = produce(condition, (draft) => { const newCondition = produce(condition, (draft) => {
draft.variable_selector = valueSelector draft.variable_selector = valueSelector
draft.varType = resolvedVarType draft.varType = resolvedVarType
draft.value = ''
draft.value = resolvedVarType === VarType.boolean ? false : ''
draft.comparison_operator = getOperators(resolvedVarType)[0] draft.comparison_operator = getOperators(resolvedVarType)[0]
setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
}) })
setOpen(false) setOpen(false)
}, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey]) }, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey])


const showBooleanInput = useMemo(() => {
if(condition.varType === VarType.boolean)
return true
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if(condition.varType === VarType.arrayBoolean && [ComparisonOperator.contains, ComparisonOperator.notContains].includes(condition.comparison_operator!))
return true
return false
}, [condition])
return ( return (
<div className={cn('mb-1 flex last-of-type:mb-0', className)}> <div className={cn('mb-1 flex last-of-type:mb-0', className)}>
<div className={cn( <div className={cn(
/> />
</div> </div>
{ {
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && !showBooleanInput && (
<div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'> <div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'>
<ConditionInput <ConditionInput
disabled={disabled} disabled={disabled}
</div> </div>
) )
} }
{
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && showBooleanInput && (
<div className='p-1'>
<BoolValue
value={condition.value as boolean}
onChange={handleUpdateConditionValue}
/>
</div>
)
}
{ {
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && ( !comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'> <div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>

+ 4
- 1
web/app/components/workflow/nodes/if-else/components/condition-value.tsx 查看文件

variableSelector: string[] variableSelector: string[]
labelName?: string labelName?: string
operator: ComparisonOperator operator: ComparisonOperator
value: string | string[]
value: string | string[] | boolean
} }
const ConditionValue = ({ const ConditionValue = ({
variableSelector, variableSelector,
if (Array.isArray(value)) // transfer method if (Array.isArray(value)) // transfer method
return value[0] return value[0]


if(value === true || value === false)
return value ? 'True' : 'False'

return value.replace(/{{#([^#]*)#}}/g, (a, b) => { return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
const arr: string[] = b.split('.') const arr: string[] = b.split('.')
if (isSystemVar(arr)) if (isSystemVar(arr))

+ 3
- 3
web/app/components/workflow/nodes/if-else/default.ts 查看文件

import { BlockEnum, type NodeDefault } from '../../types'
import { BlockEnum, type NodeDefault, VarType } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types' import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils' import { isEmptyRelatedOperator } from './utils'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks' import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
if (isEmptyRelatedOperator(c.comparison_operator!)) if (isEmptyRelatedOperator(c.comparison_operator!))
return true return true


return !!c.value
return (c.varType === VarType.boolean || c.varType === VarType.arrayBoolean) ? c.value === undefined : !!c.value
}) })
if (!isSet) if (!isSet)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
} }
else { else {
if (!isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
if (!isEmptyRelatedOperator(condition.comparison_operator!) && ((condition.varType === VarType.boolean || condition.varType === VarType.arrayBoolean) ? condition.value === undefined : !condition.value))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
} }
} }

+ 4
- 7
web/app/components/workflow/nodes/if-else/node.tsx 查看文件

import type { Condition, IfElseNodeType } from './types' import type { Condition, IfElseNodeType } from './types'
import ConditionValue from './components/condition-value' import ConditionValue from './components/condition-value'
import ConditionFilesListValue from './components/condition-files-list-value' import ConditionFilesListValue from './components/condition-files-list-value'
import { VarType } from '../../types'
const i18nPrefix = 'workflow.nodes.ifElse' const i18nPrefix = 'workflow.nodes.ifElse'


const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => { const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
if (!c.comparison_operator) if (!c.comparison_operator)
return false return false


if (isEmptyRelatedOperator(c.comparison_operator!))
return true

return !!c.value
return (c.varType === VarType.boolean || c.varType === VarType.arrayBoolean) ? true : !!c.value
}) })
return isSet return isSet
} }
else { else {
if (isEmptyRelatedOperator(condition.comparison_operator!)) if (isEmptyRelatedOperator(condition.comparison_operator!))
return true return true

return !!condition.value
return (condition.varType === VarType.boolean || condition.varType === VarType.arrayBoolean) ? true : !!condition.value
} }
}, []) }, [])
const conditionNotSet = (<div className='flex h-6 items-center space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'> const conditionNotSet = (<div className='flex h-6 items-center space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'>
<ConditionValue <ConditionValue
variableSelector={condition.variable_selector!} variableSelector={condition.variable_selector!}
operator={condition.comparison_operator!} operator={condition.comparison_operator!}
value={condition.value}
value={condition.varType === VarType.boolean ? (!condition.value ? 'False' : condition.value) : condition.value}
/> />
) )



+ 1
- 1
web/app/components/workflow/nodes/if-else/types.ts 查看文件

variable_selector?: ValueSelector variable_selector?: ValueSelector
key?: string // sub variable key key?: string // sub variable key
comparison_operator?: ComparisonOperator comparison_operator?: ComparisonOperator
value: string | string[]
value: string | string[] | boolean
numberVarType?: NumberVarType numberVarType?: NumberVarType
sub_variable_condition?: CaseItem sub_variable_condition?: CaseItem
} }

+ 1
- 1
web/app/components/workflow/nodes/if-else/use-config.ts 查看文件

varType: varItem.type, varType: varItem.type,
variable_selector: valueSelector, variable_selector: valueSelector,
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0], comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
value: '',
value: (varItem.type === VarType.boolean || varItem.type === VarType.arrayBoolean) ? false : '',
}) })
} }
}) })

+ 6
- 0
web/app/components/workflow/nodes/if-else/utils.ts 查看文件

ComparisonOperator.empty, ComparisonOperator.empty,
ComparisonOperator.notEmpty, ComparisonOperator.notEmpty,
] ]
case VarType.boolean:
return [
ComparisonOperator.is,
ComparisonOperator.isNot,
]
case VarType.file: case VarType.file:
return [ return [
ComparisonOperator.exists, ComparisonOperator.exists,
] ]
case VarType.arrayString: case VarType.arrayString:
case VarType.arrayNumber: case VarType.arrayNumber:
case VarType.arrayBoolean:
return [ return [
ComparisonOperator.contains, ComparisonOperator.contains,
ComparisonOperator.notContains, ComparisonOperator.notContains,

+ 1
- 1
web/app/components/workflow/nodes/iteration/use-config.ts 查看文件

const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload) const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)


const filterInputVar = useCallback((varPayload: Var) => { const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
return [VarType.array, VarType.arrayString, VarType.arrayBoolean, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
}, []) }, [])


const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => { const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {

+ 10
- 2
web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx 查看文件

import SubVariablePicker from './sub-variable-picker' import SubVariablePicker from './sub-variable-picker'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/constants' import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/constants'
import { SimpleSelect as Select } from '@/app/components/base/select' import { SimpleSelect as Select } from '@/app/components/base/select'
import BoolValue from '../../../panel/chat-variable-panel/components/bool-value'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var' import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'


type Props = { type Props = {
condition: Condition condition: Condition
onChange: (condition: Condition) => void
varType: VarType varType: VarType
onChange: (condition: Condition) => void
hasSubVariable: boolean hasSubVariable: boolean
readOnly: boolean readOnly: boolean
nodeId: string nodeId: string


const isSelect = [ComparisonOperator.in, ComparisonOperator.notIn, ComparisonOperator.allOf].includes(condition.comparison_operator) const isSelect = [ComparisonOperator.in, ComparisonOperator.notIn, ComparisonOperator.allOf].includes(condition.comparison_operator)
const isArrayValue = condition.key === 'transfer_method' || condition.key === 'type' const isArrayValue = condition.key === 'transfer_method' || condition.key === 'type'
const isBoolean = varType === VarType.boolean


const selectOptions = useMemo(() => { const selectOptions = useMemo(() => {
if (isSelect) { if (isSelect) {
/> />
) )
} }
else if (isBoolean) {
inputElement = (<BoolValue
value={condition.value as boolean}
onChange={handleChange('value')}
/>)
}
else if (supportVariableInput) { else if (supportVariableInput) {
inputElement = ( inputElement = (
<Input <Input
<div className='flex space-x-1'> <div className='flex space-x-1'>
<ConditionOperator <ConditionOperator
className='h-8 bg-components-input-bg-normal' className='h-8 bg-components-input-bg-normal'
varType={expectedVarType ?? VarType.string}
varType={expectedVarType ?? varType ?? VarType.string}
value={condition.comparison_operator} value={condition.comparison_operator}
onSelect={handleChange('comparison_operator')} onSelect={handleChange('comparison_operator')}
file={hasSubVariable ? { key: condition.key } : undefined} file={hasSubVariable ? { key: condition.key } : undefined}

+ 2
- 2
web/app/components/workflow/nodes/list-operator/default.ts 查看文件

}, },
checkValid(payload: ListFilterNodeType, t: any) { checkValid(payload: ListFilterNodeType, t: any) {
let errorMessages = '' let errorMessages = ''
const { variable, var_type, filter_by } = payload
const { variable, var_type, filter_by, item_var_type } = payload


if (!errorMessages && !variable?.length) if (!errorMessages && !variable?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.inputVar') }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.inputVar') })
if (!errorMessages && !filter_by.conditions[0]?.comparison_operator) if (!errorMessages && !filter_by.conditions[0]?.comparison_operator)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonOperator') }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonOperator') })


if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && !filter_by.conditions[0]?.value)
if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && (item_var_type === VarType.boolean ? !filter_by.conditions[0]?.value === undefined : !filter_by.conditions[0]?.value))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonValue') }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonValue') })
} }



+ 1
- 1
web/app/components/workflow/nodes/list-operator/types.ts 查看文件

export type Condition = { export type Condition = {
key: string key: string
comparison_operator: ComparisonOperator comparison_operator: ComparisonOperator
value: string | number | string[]
value: string | number | boolean | string[]
} }


export type ListFilterNodeType = CommonNodeType & { export type ListFilterNodeType = CommonNodeType & {

+ 8
- 3
web/app/components/workflow/nodes/list-operator/use-config.ts 查看文件

isChatMode, isChatMode,
isConstant: false, isConstant: false,
}) })
let itemVarType = varType
let itemVarType
switch (varType) { switch (varType) {
case VarType.arrayNumber: case VarType.arrayNumber:
itemVarType = VarType.number itemVarType = VarType.number
case VarType.arrayObject: case VarType.arrayObject:
itemVarType = VarType.object itemVarType = VarType.object
break break
case VarType.arrayBoolean:
itemVarType = VarType.boolean
break
default:
itemVarType = varType
} }
return { varType, itemVarType } return { varType, itemVarType }
}, [availableNodes, getCurrentVariableType, inputs.variable, isChatMode, isInIteration, iterationNode, loopNode]) }, [availableNodes, getCurrentVariableType, inputs.variable, isChatMode, isInIteration, iterationNode, loopNode])
draft.filter_by.conditions = [{ draft.filter_by.conditions = [{
key: (isFileArray && !draft.filter_by.conditions[0]?.key) ? 'name' : '', key: (isFileArray && !draft.filter_by.conditions[0]?.key) ? 'name' : '',
comparison_operator: getOperators(itemVarType, isFileArray ? { key: 'name' } : undefined)[0], comparison_operator: getOperators(itemVarType, isFileArray ? { key: 'name' } : undefined)[0],
value: '',
value: itemVarType === VarType.boolean ? false : '',
}] }]
if (isFileArray && draft.order_by.enabled && !draft.order_by.key) if (isFileArray && draft.order_by.enabled && !draft.order_by.key)
draft.order_by.key = 'name' draft.order_by.key = 'name'


const filterVar = useCallback((varPayload: Var) => { const filterVar = useCallback((varPayload: Var) => {
// Don't know the item struct of VarType.arrayObject, so not support it // Don't know the item struct of VarType.arrayObject, so not support it
return [VarType.arrayNumber, VarType.arrayString, VarType.arrayFile].includes(varPayload.type)
return [VarType.arrayNumber, VarType.arrayString, VarType.arrayBoolean, VarType.arrayFile].includes(varPayload.type)
}, []) }, [])


const handleFilterEnabledChange = useCallback((enabled: boolean) => { const handleFilterEnabledChange = useCallback((enabled: boolean) => {

+ 0
- 3
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx 查看文件

import SchemaEditor from './schema-editor' import SchemaEditor from './schema-editor'
import { import {
checkJsonSchemaDepth, checkJsonSchemaDepth,
convertBooleanToString,
getValidationErrorMessage, getValidationErrorMessage,
jsonToSchema, jsonToSchema,
preValidateSchema, preValidateSchema,
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`) setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
return return
} }
convertBooleanToString(schema)
const validationErrors = validateSchemaAgainstDraft7(schema) const validationErrors = validateSchemaAgainstDraft7(schema)
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
setValidationError(getValidationErrorMessage(validationErrors)) setValidationError(getValidationErrorMessage(validationErrors))
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`) setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
return return
} }
convertBooleanToString(schema)
const validationErrors = validateSchemaAgainstDraft7(schema) const validationErrors = validateSchemaAgainstDraft7(schema)
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
setValidationError(getValidationErrorMessage(validationErrors)) setValidationError(getValidationErrorMessage(validationErrors))

+ 2
- 4
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/index.tsx 查看文件

const TYPE_OPTIONS = [ const TYPE_OPTIONS = [
{ value: Type.string, text: 'string' }, { value: Type.string, text: 'string' },
{ value: Type.number, text: 'number' }, { value: Type.number, text: 'number' },
// { value: Type.boolean, text: 'boolean' },
{ value: Type.boolean, text: 'boolean' },
{ value: Type.object, text: 'object' }, { value: Type.object, text: 'object' },
{ value: ArrayType.string, text: 'array[string]' }, { value: ArrayType.string, text: 'array[string]' },
{ value: ArrayType.number, text: 'array[number]' }, { value: ArrayType.number, text: 'array[number]' },
// { value: ArrayType.boolean, text: 'array[boolean]' },
{ value: ArrayType.object, text: 'array[object]' }, { value: ArrayType.object, text: 'array[object]' },
] ]


const MAXIMUM_DEPTH_TYPE_OPTIONS = [ const MAXIMUM_DEPTH_TYPE_OPTIONS = [
{ value: Type.string, text: 'string' }, { value: Type.string, text: 'string' },
{ value: Type.number, text: 'number' }, { value: Type.number, text: 'number' },
// { value: Type.boolean, text: 'boolean' },
{ value: Type.boolean, text: 'boolean' },
{ value: ArrayType.string, text: 'array[string]' }, { value: ArrayType.string, text: 'array[string]' },
{ value: ArrayType.number, text: 'array[number]' }, { value: ArrayType.number, text: 'array[number]' },
// { value: ArrayType.boolean, text: 'array[boolean]' },
] ]


const EditCard: FC<EditCardProps> = ({ const EditCard: FC<EditCardProps> = ({

+ 1
- 0
web/app/components/workflow/nodes/llm/utils.ts 查看文件

return message return message
} }


// Previous Not support boolean type, so transform boolean to string when paste it into schema editor
export const convertBooleanToString = (schema: any) => { export const convertBooleanToString = (schema: any) => {
if (schema.type === Type.boolean) if (schema.type === Type.boolean)
schema.type = Type.string schema.type = Type.string

+ 13
- 4
web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx 查看文件

import { SimpleSelect as Select } from '@/app/components/base/select' import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import ConditionVarSelector from './condition-var-selector' import ConditionVarSelector from './condition-var-selector'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'


const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName' const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'




const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type' const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'


const handleUpdateConditionValue = useCallback((value: string) => {
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
const handleUpdateConditionValue = useCallback((value: string | boolean) => {
if (value === condition.value || (isArrayValue && value === (condition.value as string[])?.[0]))
return return
const newCondition = { const newCondition = {
...condition, ...condition,
value: isArrayValue ? [value] : value,
value: isArrayValue ? [value as string] : value,
} }
doUpdateCondition(newCondition) doUpdateCondition(newCondition)
}, [condition, doUpdateCondition, isArrayValue]) }, [condition, doUpdateCondition, isArrayValue])
/> />
</div> </div>
{ {
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && condition.varType !== VarType.boolean && (
<div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'> <div className='max-h-[100px] overflow-y-auto border-t border-t-divider-subtle px-2 py-1'>
<ConditionInput <ConditionInput
disabled={disabled} disabled={disabled}
</div> </div>
) )
} }
{!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType === VarType.boolean
&& <div className='p-1'>
<BoolValue
value={condition.value as boolean}
onChange={handleUpdateConditionValue}
/>
</div>
}
{ {
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && ( !comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'> <div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>

+ 28
- 26
web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx 查看文件

ValueType, ValueType,
VarType, VarType,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'


const objectPlaceholder = `# example
# {
# "name": "ray",
# "age": 20
# }`
const arrayStringPlaceholder = `# example
# [
# "value1",
# "value2"
# ]`
const arrayNumberPlaceholder = `# example
# [
# 100,
# 200
# ]`
const arrayObjectPlaceholder = `# example
# [
# {
# "name": "ray",
# "age": 20
# },
# {
# "name": "lily",
# "age": 18
# }
# ]`
import {
arrayBoolPlaceholder,
arrayNumberPlaceholder,
arrayObjectPlaceholder,
arrayStringPlaceholder,
objectPlaceholder,
} from '@/app/components/workflow/panel/chat-variable-panel/utils'
import ArrayBoolList from '@/app/components/workflow/panel/chat-variable-panel/components/array-bool-list'


type FormItemProps = { type FormItemProps = {
nodeId: string nodeId: string
return arrayNumberPlaceholder return arrayNumberPlaceholder
if (var_type === VarType.arrayObject) if (var_type === VarType.arrayObject)
return arrayObjectPlaceholder return arrayObjectPlaceholder
if (var_type === VarType.arrayBoolean)
return arrayBoolPlaceholder
return objectPlaceholder return objectPlaceholder
}, [var_type]) }, [var_type])


/> />
) )
} }
{
value_type === ValueType.constant && var_type === VarType.boolean && (
<BoolValue
value={value}
onChange={handleChange}
/>
)
}
{ {
value_type === ValueType.constant value_type === ValueType.constant
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject) && (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
</div> </div>
) )
} }
{
value_type === ValueType.constant && var_type === VarType.arrayBoolean && (
<ArrayBoolList
className='mt-2'
list={value || [false]}
onChange={handleChange}
/>
)
}
</div> </div>
) )
} }

+ 0
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx 查看文件


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存