mirror of https://github.com/langgenius/dify.git
feat: support bool type variable frontend (#24437)
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
This commit is contained in:
parent
b5c2756261
commit
dac72b078d
|
|
@ -0,0 +1,11 @@
|
|||
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}"
|
||||
|
|
@ -3,6 +3,17 @@ import re
|
|||
from core.app.app_config.entities import ExternalDataVariableEntity, VariableEntity, VariableEntityType
|
||||
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:
|
||||
@classmethod
|
||||
|
|
@ -47,6 +58,7 @@ class BasicVariablesConfigManager:
|
|||
VariableEntityType.PARAGRAPH,
|
||||
VariableEntityType.NUMBER,
|
||||
VariableEntityType.SELECT,
|
||||
VariableEntityType.CHECKBOX,
|
||||
}:
|
||||
variable = variables[variable_type]
|
||||
variable_entities.append(
|
||||
|
|
@ -96,8 +108,17 @@ class BasicVariablesConfigManager:
|
|||
variables = []
|
||||
for item in config["user_input_form"]:
|
||||
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]
|
||||
if "label" not in form_item:
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ class VariableEntityType(StrEnum):
|
|||
EXTERNAL_DATA_TOOL = "external_data_tool"
|
||||
FILE = "file"
|
||||
FILE_LIST = "file-list"
|
||||
CHECKBOX = "checkbox"
|
||||
|
||||
|
||||
class VariableEntity(BaseModel):
|
||||
|
|
|
|||
|
|
@ -103,18 +103,23 @@ class BaseAppGenerator:
|
|||
f"(type '{variable_entity.type}') {variable_entity.variable} in input form must be a string"
|
||||
)
|
||||
|
||||
if variable_entity.type == VariableEntityType.NUMBER and isinstance(value, str):
|
||||
# handle empty string case
|
||||
if not value.strip():
|
||||
return None
|
||||
# may raise ValueError if user_input_value is not a valid number
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
else:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"{variable_entity.variable} in input form must be a valid number")
|
||||
if variable_entity.type == VariableEntityType.NUMBER:
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
# handle empty string case
|
||||
if not value.strip():
|
||||
return None
|
||||
# may raise ValueError if user_input_value is not a valid number
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
else:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"{variable_entity.variable} in input form must be a valid number")
|
||||
else:
|
||||
raise TypeError(f"expected value type int, float or str, got {type(value)}, value: {value}")
|
||||
|
||||
match variable_entity.type:
|
||||
case VariableEntityType.SELECT:
|
||||
|
|
@ -144,6 +149,11 @@ class BaseAppGenerator:
|
|||
raise ValueError(
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,11 @@ class FileSegment(Segment):
|
|||
return ""
|
||||
|
||||
|
||||
class BooleanSegment(Segment):
|
||||
value_type: SegmentType = SegmentType.BOOLEAN
|
||||
value: bool
|
||||
|
||||
|
||||
class ArrayAnySegment(ArraySegment):
|
||||
value_type: SegmentType = SegmentType.ARRAY_ANY
|
||||
value: Sequence[Any]
|
||||
|
|
@ -198,6 +203,11 @@ class ArrayFileSegment(ArraySegment):
|
|||
return ""
|
||||
|
||||
|
||||
class ArrayBooleanSegment(ArraySegment):
|
||||
value_type: SegmentType = SegmentType.ARRAY_BOOLEAN
|
||||
value: Sequence[bool]
|
||||
|
||||
|
||||
def get_segment_discriminator(v: Any) -> SegmentType | None:
|
||||
if isinstance(v, Segment):
|
||||
return v.value_type
|
||||
|
|
@ -231,11 +241,13 @@ SegmentUnion: TypeAlias = Annotated[
|
|||
| Annotated[IntegerSegment, Tag(SegmentType.INTEGER)]
|
||||
| Annotated[ObjectSegment, Tag(SegmentType.OBJECT)]
|
||||
| Annotated[FileSegment, Tag(SegmentType.FILE)]
|
||||
| Annotated[BooleanSegment, Tag(SegmentType.BOOLEAN)]
|
||||
| Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)]
|
||||
| Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)]
|
||||
| Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)]
|
||||
| Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)]
|
||||
| Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)]
|
||||
| Annotated[ArrayBooleanSegment, Tag(SegmentType.ARRAY_BOOLEAN)]
|
||||
),
|
||||
Discriminator(get_segment_discriminator),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ from core.file.models import File
|
|||
|
||||
|
||||
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)
|
||||
NONE = "none"
|
||||
|
|
@ -27,12 +32,14 @@ class SegmentType(StrEnum):
|
|||
SECRET = "secret"
|
||||
|
||||
FILE = "file"
|
||||
BOOLEAN = "boolean"
|
||||
|
||||
ARRAY_ANY = "array[any]"
|
||||
ARRAY_STRING = "array[string]"
|
||||
ARRAY_NUMBER = "array[number]"
|
||||
ARRAY_OBJECT = "array[object]"
|
||||
ARRAY_FILE = "array[file]"
|
||||
ARRAY_BOOLEAN = "array[boolean]"
|
||||
|
||||
NONE = "none"
|
||||
|
||||
|
|
@ -76,12 +83,18 @@ class SegmentType(StrEnum):
|
|||
return SegmentType.ARRAY_FILE
|
||||
case SegmentType.NONE:
|
||||
return SegmentType.ARRAY_ANY
|
||||
case SegmentType.BOOLEAN:
|
||||
return SegmentType.ARRAY_BOOLEAN
|
||||
case _:
|
||||
# This should be unreachable.
|
||||
raise ValueError(f"not supported value {value}")
|
||||
if value is 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
|
||||
elif isinstance(value, float):
|
||||
return SegmentType.FLOAT
|
||||
|
|
@ -111,7 +124,7 @@ class SegmentType(StrEnum):
|
|||
else:
|
||||
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.
|
||||
Users of `SegmentType` should call this method, instead of using
|
||||
|
|
@ -126,6 +139,10 @@ class SegmentType(StrEnum):
|
|||
"""
|
||||
if self.is_array_type():
|
||||
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]:
|
||||
return isinstance(value, (int, float))
|
||||
elif self == SegmentType.STRING:
|
||||
|
|
@ -141,6 +158,27 @@ class SegmentType(StrEnum):
|
|||
else:
|
||||
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":
|
||||
"""Returns the type exposed to the frontend.
|
||||
|
||||
|
|
@ -150,6 +188,20 @@ class SegmentType(StrEnum):
|
|||
return SegmentType.NUMBER
|
||||
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_ANY does not have corresponding element type.
|
||||
|
|
@ -157,6 +209,7 @@ _ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = {
|
|||
SegmentType.ARRAY_NUMBER: SegmentType.NUMBER,
|
||||
SegmentType.ARRAY_OBJECT: SegmentType.OBJECT,
|
||||
SegmentType.ARRAY_FILE: SegmentType.FILE,
|
||||
SegmentType.ARRAY_BOOLEAN: SegmentType.BOOLEAN,
|
||||
}
|
||||
|
||||
_ARRAY_TYPES = frozenset(
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ from core.helper import encrypter
|
|||
|
||||
from .segments import (
|
||||
ArrayAnySegment,
|
||||
ArrayBooleanSegment,
|
||||
ArrayFileSegment,
|
||||
ArrayNumberSegment,
|
||||
ArrayObjectSegment,
|
||||
ArraySegment,
|
||||
ArrayStringSegment,
|
||||
BooleanSegment,
|
||||
FileSegment,
|
||||
FloatSegment,
|
||||
IntegerSegment,
|
||||
|
|
@ -96,10 +98,18 @@ class FileVariable(FileSegment, Variable):
|
|||
pass
|
||||
|
||||
|
||||
class BooleanVariable(BooleanSegment, Variable):
|
||||
pass
|
||||
|
||||
|
||||
class ArrayFileVariable(ArrayFileSegment, ArrayVariable):
|
||||
pass
|
||||
|
||||
|
||||
class ArrayBooleanVariable(ArrayBooleanSegment, ArrayVariable):
|
||||
pass
|
||||
|
||||
|
||||
# The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic.
|
||||
# Use `Variable` for type hinting when serialization is not required.
|
||||
#
|
||||
|
|
@ -114,11 +124,13 @@ VariableUnion: TypeAlias = Annotated[
|
|||
| Annotated[IntegerVariable, Tag(SegmentType.INTEGER)]
|
||||
| Annotated[ObjectVariable, Tag(SegmentType.OBJECT)]
|
||||
| Annotated[FileVariable, Tag(SegmentType.FILE)]
|
||||
| Annotated[BooleanVariable, Tag(SegmentType.BOOLEAN)]
|
||||
| Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)]
|
||||
| Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)]
|
||||
| Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)]
|
||||
| Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)]
|
||||
| Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)]
|
||||
| Annotated[ArrayBooleanVariable, Tag(SegmentType.ARRAY_BOOLEAN)]
|
||||
| Annotated[SecretVariable, Tag(SegmentType.SECRET)]
|
||||
),
|
||||
Discriminator(get_segment_discriminator),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from core.helper.code_executor.code_node_provider import CodeNodeProvider
|
|||
from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider
|
||||
from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider
|
||||
from core.variables.segments import ArrayFileSegment
|
||||
from core.variables.types import SegmentType
|
||||
from core.workflow.entities.node_entities import NodeRunResult
|
||||
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus
|
||||
from core.workflow.nodes.base import BaseNode
|
||||
|
|
@ -119,6 +120,14 @@ class CodeNode(BaseNode):
|
|||
|
||||
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:
|
||||
"""
|
||||
Check number
|
||||
|
|
@ -173,6 +182,8 @@ class CodeNode(BaseNode):
|
|||
prefix=f"{prefix}.{output_name}" if prefix else output_name,
|
||||
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):
|
||||
self._check_number(
|
||||
value=output_value, variable=f"{prefix}.{output_name}" if prefix else output_name
|
||||
|
|
@ -232,7 +243,7 @@ class CodeNode(BaseNode):
|
|||
if output_name not in result:
|
||||
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
|
||||
if not isinstance(result.get(output_name), dict):
|
||||
if result[output_name] is None:
|
||||
|
|
@ -249,18 +260,28 @@ class CodeNode(BaseNode):
|
|||
prefix=f"{prefix}.{output_name}",
|
||||
depth=depth + 1,
|
||||
)
|
||||
elif output_config.type == "number":
|
||||
elif output_config.type == SegmentType.NUMBER:
|
||||
# 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
|
||||
transformed_result[output_name] = self._check_string(
|
||||
value=result[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
|
||||
if not isinstance(result[output_name], list):
|
||||
if result[output_name] is None:
|
||||
|
|
@ -278,10 +299,17 @@ class CodeNode(BaseNode):
|
|||
)
|
||||
|
||||
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])
|
||||
]
|
||||
elif output_config.type == "array[string]":
|
||||
elif output_config.type == SegmentType.ARRAY_STRING:
|
||||
# check if array of string available
|
||||
if not isinstance(result[output_name], list):
|
||||
if result[output_name] is None:
|
||||
|
|
@ -302,7 +330,7 @@ class CodeNode(BaseNode):
|
|||
self._check_string(value=value, variable=f"{prefix}{dot}{output_name}[{i}]")
|
||||
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
|
||||
if not isinstance(result[output_name], list):
|
||||
if result[output_name] is None:
|
||||
|
|
@ -340,6 +368,22 @@ class CodeNode(BaseNode):
|
|||
)
|
||||
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:
|
||||
raise OutputValidationError(f"Output type {output_config.type} is not supported.")
|
||||
|
||||
|
|
@ -374,3 +418,16 @@ class CodeNode(BaseNode):
|
|||
@property
|
||||
def retry(self) -> bool:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,31 @@
|
|||
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.variables.types import SegmentType
|
||||
from core.workflow.entities.variable_entities import VariableSelector
|
||||
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):
|
||||
"""
|
||||
|
|
@ -13,7 +33,7 @@ class CodeNodeData(BaseNodeData):
|
|||
"""
|
||||
|
||||
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
|
||||
|
||||
class Dependency(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,36 +1,43 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Literal
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.workflow.nodes.base import BaseNodeData
|
||||
|
||||
_Condition = Literal[
|
||||
|
||||
class FilterOperator(StrEnum):
|
||||
# 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
|
||||
"=",
|
||||
"≠",
|
||||
"<",
|
||||
">",
|
||||
"≥",
|
||||
"≤",
|
||||
]
|
||||
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):
|
||||
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):
|
||||
|
|
@ -38,10 +45,10 @@ class FilterBy(BaseModel):
|
|||
conditions: Sequence[FilterCondition] = Field(default_factory=list)
|
||||
|
||||
|
||||
class OrderBy(BaseModel):
|
||||
class OrderByConfig(BaseModel):
|
||||
enabled: bool = False
|
||||
key: str = ""
|
||||
value: Literal["asc", "desc"] = "asc"
|
||||
value: Order = Order.ASC
|
||||
|
||||
|
||||
class Limit(BaseModel):
|
||||
|
|
@ -57,6 +64,6 @@ class ExtractConfig(BaseModel):
|
|||
class ListOperatorNodeData(BaseNodeData):
|
||||
variable: Sequence[str] = Field(default_factory=list)
|
||||
filter_by: FilterBy
|
||||
order_by: OrderBy
|
||||
order_by: OrderByConfig
|
||||
limit: Limit
|
||||
extract_by: ExtractConfig = Field(default_factory=ExtractConfig)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,40 @@
|
|||
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.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.workflow_node_execution import WorkflowNodeExecutionStatus
|
||||
from core.workflow.nodes.base import BaseNode
|
||||
from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig
|
||||
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
|
||||
|
||||
_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):
|
||||
_node_type = NodeType.LIST_OPERATOR
|
||||
|
|
@ -69,11 +91,8 @@ class ListOperatorNode(BaseNode):
|
|||
process_data=process_data,
|
||||
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(
|
||||
status=WorkflowNodeExecutionStatus.FAILED, error=error_message, inputs=inputs, outputs=outputs
|
||||
)
|
||||
|
|
@ -122,9 +141,7 @@ class ListOperatorNode(BaseNode):
|
|||
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]
|
||||
result: list[Any] = []
|
||||
for condition in self._node_data.filter_by.conditions:
|
||||
|
|
@ -154,33 +171,35 @@ class ListOperatorNode(BaseNode):
|
|||
)
|
||||
result = list(filter(filter_func, variable.value))
|
||||
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
|
||||
|
||||
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})
|
||||
elif isinstance(variable, ArrayFileSegment):
|
||||
result = _order_file(
|
||||
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})
|
||||
else:
|
||||
raise AssertionError("this statement should be unreachable")
|
||||
|
||||
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]
|
||||
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)
|
||||
if value < 1:
|
||||
raise ValueError(f"Invalid serial index: must be >= 1, got {value}")
|
||||
|
|
@ -232,11 +251,11 @@ def _get_string_filter_func(*, condition: str, value: str) -> Callable[[str], bo
|
|||
case "empty":
|
||||
return lambda x: x == ""
|
||||
case "not contains":
|
||||
return lambda x: not _contains(value)(x)
|
||||
return _negation(_contains(value))
|
||||
case "is not":
|
||||
return lambda x: not _is(value)(x)
|
||||
return _negation(_is(value))
|
||||
case "not in":
|
||||
return lambda x: not _in(value)(x)
|
||||
return _negation(_in(value))
|
||||
case "not empty":
|
||||
return lambda x: x != ""
|
||||
case _:
|
||||
|
|
@ -248,7 +267,7 @@ def _get_sequence_filter_func(*, condition: str, value: Sequence[str]) -> Callab
|
|||
case "in":
|
||||
return _in(value)
|
||||
case "not in":
|
||||
return lambda x: not _in(value)(x)
|
||||
return _negation(_in(value))
|
||||
case _:
|
||||
raise InvalidConditionError(f"Invalid condition: {condition}")
|
||||
|
||||
|
|
@ -271,6 +290,16 @@ def _get_number_filter_func(*, condition: str, value: int | float) -> Callable[[
|
|||
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]:
|
||||
extract_func: Callable[[File], Any]
|
||||
if key in {"name", "extension", "mime_type", "url"} and isinstance(value, str):
|
||||
|
|
@ -298,7 +327,7 @@ def _endswith(value: str) -> Callable[[str], bool]:
|
|||
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
|
||||
|
||||
|
||||
|
|
@ -330,21 +359,13 @@ def _ge(value: int | float) -> Callable[[int | float], bool]:
|
|||
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]
|
||||
if order_by in {"name", "type", "extension", "mime_type", "transfer_method", "url"}:
|
||||
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":
|
||||
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:
|
||||
raise InvalidKeyError(f"Invalid order key: {order_by}")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import io
|
|||
import json
|
||||
import logging
|
||||
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.file import FileType, file_manager
|
||||
|
|
@ -55,7 +55,6 @@ from core.workflow.entities.variable_entities import VariableSelector
|
|||
from core.workflow.entities.variable_pool import VariablePool
|
||||
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus
|
||||
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.entities import BaseNodeData, RetryConfig
|
||||
from core.workflow.nodes.enums import ErrorStrategy, NodeType
|
||||
|
|
@ -90,6 +89,7 @@ from .file_saver import FileSaverImpl, LLMFileSaver
|
|||
if TYPE_CHECKING:
|
||||
from core.file.models import File
|
||||
from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState
|
||||
from core.workflow.graph_engine.entities.event import InNodeEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -161,7 +161,7 @@ class LLMNode(BaseNode):
|
|||
def version(cls) -> str:
|
||||
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
|
||||
process_data = None
|
||||
result_text = ""
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ _VALID_VAR_TYPE = frozenset(
|
|||
SegmentType.STRING,
|
||||
SegmentType.NUMBER,
|
||||
SegmentType.OBJECT,
|
||||
SegmentType.BOOLEAN,
|
||||
SegmentType.ARRAY_STRING,
|
||||
SegmentType.ARRAY_NUMBER,
|
||||
SegmentType.ARRAY_OBJECT,
|
||||
SegmentType.ARRAY_BOOLEAN,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -404,11 +404,11 @@ class LoopNode(BaseNode):
|
|||
for node_id in loop_graph.node_ids:
|
||||
variable_pool.remove([node_id])
|
||||
|
||||
_outputs = {}
|
||||
_outputs: dict[str, Segment | int | None] = {}
|
||||
for loop_variable_key, loop_variable_selector in loop_variable_selectors.items():
|
||||
_loop_variable_segment = variable_pool.get(loop_variable_selector)
|
||||
if _loop_variable_segment:
|
||||
_outputs[loop_variable_key] = _loop_variable_segment.value
|
||||
_outputs[loop_variable_key] = _loop_variable_segment
|
||||
else:
|
||||
_outputs[loop_variable_key] = None
|
||||
|
||||
|
|
@ -522,21 +522,30 @@ class LoopNode(BaseNode):
|
|||
return variable_mapping
|
||||
|
||||
@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."""
|
||||
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:
|
||||
logger.warning("unexpected value for LoopNode, value_type=%s, value=%s", original_value, var_type)
|
||||
value = []
|
||||
elif var_type == SegmentType.ARRAY_BOOLEAN:
|
||||
value = original_value
|
||||
else:
|
||||
raise AssertionError("this statement should be unreachable.")
|
||||
try:
|
||||
return build_segment_with_type(var_type, value)
|
||||
return build_segment_with_type(var_type, value=value)
|
||||
except TypeMismatchError as type_exc:
|
||||
# Attempt to parse the value as a JSON-encoded string, if applicable.
|
||||
if not isinstance(value, str):
|
||||
if not isinstance(original_value, str):
|
||||
raise
|
||||
try:
|
||||
value = json.loads(value)
|
||||
value = json.loads(original_value)
|
||||
except ValueError:
|
||||
raise type_exc
|
||||
return build_segment_with_type(var_type, value)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,46 @@
|
|||
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.variables.types import SegmentType
|
||||
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):
|
||||
|
|
@ -17,7 +53,7 @@ class ParameterConfig(BaseModel):
|
|||
"""
|
||||
|
||||
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
|
||||
description: str
|
||||
required: bool
|
||||
|
|
@ -32,17 +68,20 @@ class ParameterConfig(BaseModel):
|
|||
return str(value)
|
||||
|
||||
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):
|
||||
|
|
@ -74,16 +113,18 @@ class ParameterExtractorNodeData(BaseNodeData):
|
|||
for parameter in self.parameters:
|
||||
parameter_schema: dict[str, Any] = {"description": parameter.description}
|
||||
|
||||
if parameter.type in {"string", "select"}:
|
||||
if parameter.type == SegmentType.STRING:
|
||||
parameter_schema["type"] = "string"
|
||||
elif parameter.type.startswith("array"):
|
||||
elif parameter.type.is_array_type():
|
||||
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:
|
||||
parameter_schema["type"] = parameter.type
|
||||
|
||||
if parameter.type == "select":
|
||||
if parameter.options:
|
||||
parameter_schema["enum"] = parameter.options
|
||||
|
||||
parameters["properties"][parameter.name] = parameter_schema
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
from typing import Any
|
||||
|
||||
from core.variables.types import SegmentType
|
||||
|
||||
|
||||
class ParameterExtractorNodeError(ValueError):
|
||||
"""Base error for ParameterExtractorNode."""
|
||||
|
||||
|
|
@ -48,3 +53,23 @@ class InvalidArrayValueError(ParameterExtractorNodeError):
|
|||
|
||||
class InvalidModelModeError(ParameterExtractorNodeError):
|
||||
"""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
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
|
|||
from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate
|
||||
from core.prompt.simple_prompt_transform import ModelMode
|
||||
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.variable_pool import VariablePool
|
||||
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus
|
||||
|
|
@ -39,16 +39,13 @@ from factories.variable_factory import build_segment_with_type
|
|||
|
||||
from .entities import ParameterExtractorNodeData
|
||||
from .exc import (
|
||||
InvalidArrayValueError,
|
||||
InvalidBoolValueError,
|
||||
InvalidInvokeResultError,
|
||||
InvalidModelModeError,
|
||||
InvalidModelTypeError,
|
||||
InvalidNumberOfParametersError,
|
||||
InvalidNumberValueError,
|
||||
InvalidSelectValueError,
|
||||
InvalidStringValueError,
|
||||
InvalidTextContentTypeError,
|
||||
InvalidValueTypeError,
|
||||
ModelSchemaNotFoundError,
|
||||
ParameterExtractorNodeError,
|
||||
RequiredParameterMissingError,
|
||||
|
|
@ -549,9 +546,6 @@ class ParameterExtractorNode(BaseNode):
|
|||
return prompt_messages
|
||||
|
||||
def _validate_result(self, data: ParameterExtractorNodeData, result: dict) -> dict:
|
||||
"""
|
||||
Validate result.
|
||||
"""
|
||||
if len(data.parameters) != len(result):
|
||||
raise InvalidNumberOfParametersError("Invalid number of parameters")
|
||||
|
||||
|
|
@ -559,101 +553,106 @@ class ParameterExtractorNode(BaseNode):
|
|||
if parameter.required and parameter.name not in result:
|
||||
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
|
||||
|
||||
@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:
|
||||
"""
|
||||
Transform result into standard format.
|
||||
"""
|
||||
transformed_result = {}
|
||||
transformed_result: dict[str, Any] = {}
|
||||
for parameter in data.parameters:
|
||||
if parameter.name in result:
|
||||
param_value = result[parameter.name]
|
||||
# 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():
|
||||
if isinstance(result[parameter.name], list):
|
||||
if isinstance(param_value, list):
|
||||
nested_type = parameter.element_type()
|
||||
assert nested_type is not None
|
||||
segment_value = build_segment_with_type(segment_type=SegmentType(parameter.type), 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):
|
||||
segment_value.value.append(item)
|
||||
elif nested_type == "object":
|
||||
elif nested_type == SegmentType.OBJECT:
|
||||
if isinstance(item, dict):
|
||||
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.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(
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from collections.abc import Callable, Mapping, Sequence
|
|||
from typing import TYPE_CHECKING, Any, Optional, TypeAlias
|
||||
|
||||
from core.variables import SegmentType, Variable
|
||||
from core.variables.segments import BooleanSegment
|
||||
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID
|
||||
from core.workflow.conversation_variable_updater import ConversationVariableUpdater
|
||||
from core.workflow.entities.node_entities import NodeRunResult
|
||||
|
|
@ -158,8 +159,8 @@ class VariableAssignerNode(BaseNode):
|
|||
def get_zero_value(t: SegmentType):
|
||||
# TODO(QuantumGhost): this should be a method of `SegmentType`.
|
||||
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:
|
||||
return variable_factory.build_segment({})
|
||||
case SegmentType.STRING:
|
||||
|
|
@ -170,5 +171,7 @@ def get_zero_value(t: SegmentType):
|
|||
return variable_factory.build_segment(0.0)
|
||||
case SegmentType.NUMBER:
|
||||
return variable_factory.build_segment(0)
|
||||
case SegmentType.BOOLEAN:
|
||||
return BooleanSegment(value=False)
|
||||
case _:
|
||||
raise VariableOperatorNodeError(f"unsupported variable type: {t}")
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ from core.variables import SegmentType
|
|||
EMPTY_VALUE_MAPPING = {
|
||||
SegmentType.STRING: "",
|
||||
SegmentType.NUMBER: 0,
|
||||
SegmentType.BOOLEAN: False,
|
||||
SegmentType.OBJECT: {},
|
||||
SegmentType.ARRAY_ANY: [],
|
||||
SegmentType.ARRAY_STRING: [],
|
||||
SegmentType.ARRAY_NUMBER: [],
|
||||
SegmentType.ARRAY_OBJECT: [],
|
||||
SegmentType.ARRAY_BOOLEAN: [],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,28 +16,15 @@ def is_operation_supported(*, variable_type: SegmentType, operation: Operation):
|
|||
SegmentType.NUMBER,
|
||||
SegmentType.INTEGER,
|
||||
SegmentType.FLOAT,
|
||||
SegmentType.BOOLEAN,
|
||||
}
|
||||
case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE:
|
||||
# Only number variable can be added, subtracted, multiplied or divided
|
||||
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
|
||||
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
|
||||
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 _:
|
||||
return False
|
||||
|
||||
|
|
@ -50,7 +37,7 @@ def is_variable_input_supported(*, operation: Operation):
|
|||
|
||||
def is_constant_input_supported(*, variable_type: SegmentType, operation: Operation):
|
||||
match variable_type:
|
||||
case SegmentType.STRING | SegmentType.OBJECT:
|
||||
case SegmentType.STRING | SegmentType.OBJECT | SegmentType.BOOLEAN:
|
||||
return operation in {Operation.OVER_WRITE, Operation.SET}
|
||||
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
|
||||
return operation in {
|
||||
|
|
@ -72,6 +59,9 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
|
|||
case SegmentType.STRING:
|
||||
return isinstance(value, str)
|
||||
|
||||
case SegmentType.BOOLEAN:
|
||||
return isinstance(value, bool)
|
||||
|
||||
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
|
||||
if not isinstance(value, int | float):
|
||||
return False
|
||||
|
|
@ -91,6 +81,8 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
|
|||
return isinstance(value, int | float)
|
||||
case SegmentType.ARRAY_OBJECT if operation == Operation.APPEND:
|
||||
return isinstance(value, dict)
|
||||
case SegmentType.ARRAY_BOOLEAN if operation == Operation.APPEND:
|
||||
return isinstance(value, bool)
|
||||
|
||||
# Array & Extend / Overwrite
|
||||
case SegmentType.ARRAY_ANY if operation in {Operation.EXTEND, Operation.OVER_WRITE}:
|
||||
|
|
@ -101,6 +93,8 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va
|
|||
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}:
|
||||
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 _:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -45,5 +45,5 @@ class SubVariableCondition(BaseModel):
|
|||
class Condition(BaseModel):
|
||||
variable_selector: list[str]
|
||||
comparison_operator: SupportedComparisonOperator
|
||||
value: str | Sequence[str] | None = None
|
||||
value: str | Sequence[str] | bool | None = None
|
||||
sub_variable_condition: SubVariableCondition | None = None
|
||||
|
|
|
|||
|
|
@ -1,13 +1,27 @@
|
|||
import json
|
||||
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.variables import ArrayFileSegment
|
||||
from core.variables.segments import ArrayBooleanSegment, BooleanSegment
|
||||
from core.workflow.entities.variable_pool import VariablePool
|
||||
|
||||
from .entities import Condition, SubCondition, SupportedComparisonOperator
|
||||
|
||||
|
||||
def _convert_to_bool(value: Any) -> bool:
|
||||
if isinstance(value, int):
|
||||
return bool(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
loaded = json.loads(value)
|
||||
if isinstance(loaded, (int, bool)):
|
||||
return bool(loaded)
|
||||
|
||||
raise TypeError(f"unexpected value: type={type(value)}, value={value}")
|
||||
|
||||
|
||||
class ConditionProcessor:
|
||||
def process_conditions(
|
||||
self,
|
||||
|
|
@ -48,9 +62,16 @@ class ConditionProcessor:
|
|||
)
|
||||
else:
|
||||
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):
|
||||
expected_value = variable_pool.convert_template(expected_value).text
|
||||
# Here we need to explicit convet the input string to boolean.
|
||||
if isinstance(variable, (BooleanSegment, ArrayBooleanSegment)) and expected_value is not None:
|
||||
# The following two lines is for compatibility with existing workflows.
|
||||
if isinstance(expected_value, list):
|
||||
expected_value = [_convert_to_bool(i) for i in expected_value]
|
||||
else:
|
||||
expected_value = _convert_to_bool(expected_value)
|
||||
input_conditions.append(
|
||||
{
|
||||
"actual_value": actual_value,
|
||||
|
|
@ -77,7 +98,7 @@ def _evaluate_condition(
|
|||
*,
|
||||
operator: SupportedComparisonOperator,
|
||||
value: Any,
|
||||
expected: str | Sequence[str] | None,
|
||||
expected: Union[str, Sequence[str], bool | Sequence[bool], None],
|
||||
) -> bool:
|
||||
match operator:
|
||||
case "contains":
|
||||
|
|
@ -130,7 +151,7 @@ def _assert_contains(*, value: Any, expected: Any) -> bool:
|
|||
if not value:
|
||||
return False
|
||||
|
||||
if not isinstance(value, str | list):
|
||||
if not isinstance(value, (str, list)):
|
||||
raise ValueError("Invalid actual value type: string or array")
|
||||
|
||||
if expected not in value:
|
||||
|
|
@ -142,7 +163,7 @@ def _assert_not_contains(*, value: Any, expected: Any) -> bool:
|
|||
if not value:
|
||||
return True
|
||||
|
||||
if not isinstance(value, str | list):
|
||||
if not isinstance(value, (str, list)):
|
||||
raise ValueError("Invalid actual value type: string or array")
|
||||
|
||||
if expected in value:
|
||||
|
|
@ -178,8 +199,8 @@ def _assert_is(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
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:
|
||||
return False
|
||||
|
|
@ -190,8 +211,8 @@ def _assert_is_not(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
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:
|
||||
return False
|
||||
|
|
@ -214,10 +235,13 @@ def _assert_equal(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
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)
|
||||
else:
|
||||
expected = float(expected)
|
||||
|
|
@ -231,10 +255,13 @@ def _assert_not_equal(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
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)
|
||||
else:
|
||||
expected = float(expected)
|
||||
|
|
@ -248,7 +275,7 @@ def _assert_greater_than(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
return False
|
||||
|
||||
if not isinstance(value, int | float):
|
||||
if not isinstance(value, (int, float)):
|
||||
raise ValueError("Invalid actual value type: number")
|
||||
|
||||
if isinstance(value, int):
|
||||
|
|
@ -265,7 +292,7 @@ def _assert_less_than(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
return False
|
||||
|
||||
if not isinstance(value, int | float):
|
||||
if not isinstance(value, (int, float)):
|
||||
raise ValueError("Invalid actual value type: number")
|
||||
|
||||
if isinstance(value, int):
|
||||
|
|
@ -282,7 +309,7 @@ def _assert_greater_than_or_equal(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
return False
|
||||
|
||||
if not isinstance(value, int | float):
|
||||
if not isinstance(value, (int, float)):
|
||||
raise ValueError("Invalid actual value type: number")
|
||||
|
||||
if isinstance(value, int):
|
||||
|
|
@ -299,7 +326,7 @@ def _assert_less_than_or_equal(*, value: Any, expected: Any) -> bool:
|
|||
if value is None:
|
||||
return False
|
||||
|
||||
if not isinstance(value, int | float):
|
||||
if not isinstance(value, (int, float)):
|
||||
raise ValueError("Invalid actual value type: number")
|
||||
|
||||
if isinstance(value, int):
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ from core.file import File
|
|||
from core.variables.exc import VariableError
|
||||
from core.variables.segments import (
|
||||
ArrayAnySegment,
|
||||
ArrayBooleanSegment,
|
||||
ArrayFileSegment,
|
||||
ArrayNumberSegment,
|
||||
ArrayObjectSegment,
|
||||
ArraySegment,
|
||||
ArrayStringSegment,
|
||||
BooleanSegment,
|
||||
FileSegment,
|
||||
FloatSegment,
|
||||
IntegerSegment,
|
||||
|
|
@ -23,10 +25,12 @@ from core.variables.segments import (
|
|||
from core.variables.types import SegmentType
|
||||
from core.variables.variables import (
|
||||
ArrayAnyVariable,
|
||||
ArrayBooleanVariable,
|
||||
ArrayFileVariable,
|
||||
ArrayNumberVariable,
|
||||
ArrayObjectVariable,
|
||||
ArrayStringVariable,
|
||||
BooleanVariable,
|
||||
FileVariable,
|
||||
FloatVariable,
|
||||
IntegerVariable,
|
||||
|
|
@ -49,17 +53,19 @@ class TypeMismatchError(Exception):
|
|||
|
||||
# Define the constant
|
||||
SEGMENT_TO_VARIABLE_MAP = {
|
||||
StringSegment: StringVariable,
|
||||
IntegerSegment: IntegerVariable,
|
||||
FloatSegment: FloatVariable,
|
||||
ObjectSegment: ObjectVariable,
|
||||
FileSegment: FileVariable,
|
||||
ArrayStringSegment: ArrayStringVariable,
|
||||
ArrayAnySegment: ArrayAnyVariable,
|
||||
ArrayBooleanSegment: ArrayBooleanVariable,
|
||||
ArrayFileSegment: ArrayFileVariable,
|
||||
ArrayNumberSegment: ArrayNumberVariable,
|
||||
ArrayObjectSegment: ArrayObjectVariable,
|
||||
ArrayFileSegment: ArrayFileVariable,
|
||||
ArrayAnySegment: ArrayAnyVariable,
|
||||
ArrayStringSegment: ArrayStringVariable,
|
||||
BooleanSegment: BooleanVariable,
|
||||
FileSegment: FileVariable,
|
||||
FloatSegment: FloatVariable,
|
||||
IntegerSegment: IntegerVariable,
|
||||
NoneSegment: NoneVariable,
|
||||
ObjectSegment: ObjectVariable,
|
||||
StringSegment: StringVariable,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -99,6 +105,8 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen
|
|||
mapping = dict(mapping)
|
||||
mapping["value_type"] = SegmentType.FLOAT
|
||||
result = FloatVariable.model_validate(mapping)
|
||||
case SegmentType.BOOLEAN:
|
||||
result = BooleanVariable.model_validate(mapping)
|
||||
case SegmentType.NUMBER if not isinstance(value, float | int):
|
||||
raise VariableError(f"invalid number value {value}")
|
||||
case SegmentType.OBJECT if isinstance(value, dict):
|
||||
|
|
@ -109,6 +117,8 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen
|
|||
result = ArrayNumberVariable.model_validate(mapping)
|
||||
case SegmentType.ARRAY_OBJECT if isinstance(value, list):
|
||||
result = ArrayObjectVariable.model_validate(mapping)
|
||||
case SegmentType.ARRAY_BOOLEAN if isinstance(value, list):
|
||||
result = ArrayBooleanVariable.model_validate(mapping)
|
||||
case _:
|
||||
raise VariableError(f"not supported value type {value_type}")
|
||||
if result.size > dify_config.MAX_VARIABLE_SIZE:
|
||||
|
|
@ -129,6 +139,8 @@ def build_segment(value: Any, /) -> Segment:
|
|||
return NoneSegment()
|
||||
if isinstance(value, str):
|
||||
return StringSegment(value=value)
|
||||
if isinstance(value, bool):
|
||||
return BooleanSegment(value=value)
|
||||
if isinstance(value, int):
|
||||
return IntegerSegment(value=value)
|
||||
if isinstance(value, float):
|
||||
|
|
@ -152,6 +164,8 @@ def build_segment(value: Any, /) -> Segment:
|
|||
return ArrayStringSegment(value=value)
|
||||
case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT:
|
||||
return ArrayNumberSegment(value=value)
|
||||
case SegmentType.BOOLEAN:
|
||||
return ArrayBooleanSegment(value=value)
|
||||
case SegmentType.OBJECT:
|
||||
return ArrayObjectSegment(value=value)
|
||||
case SegmentType.FILE:
|
||||
|
|
@ -170,6 +184,7 @@ _segment_factory: Mapping[SegmentType, type[Segment]] = {
|
|||
SegmentType.INTEGER: IntegerSegment,
|
||||
SegmentType.FLOAT: FloatSegment,
|
||||
SegmentType.FILE: FileSegment,
|
||||
SegmentType.BOOLEAN: BooleanSegment,
|
||||
SegmentType.OBJECT: ObjectSegment,
|
||||
# Array types
|
||||
SegmentType.ARRAY_ANY: ArrayAnySegment,
|
||||
|
|
@ -177,6 +192,7 @@ _segment_factory: Mapping[SegmentType, type[Segment]] = {
|
|||
SegmentType.ARRAY_NUMBER: ArrayNumberSegment,
|
||||
SegmentType.ARRAY_OBJECT: ArrayObjectSegment,
|
||||
SegmentType.ARRAY_FILE: ArrayFileSegment,
|
||||
SegmentType.ARRAY_BOOLEAN: ArrayBooleanSegment,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -225,6 +241,8 @@ def build_segment_with_type(segment_type: SegmentType, value: Any) -> Segment:
|
|||
return ArrayAnySegment(value=value)
|
||||
elif segment_type == SegmentType.ARRAY_STRING:
|
||||
return ArrayStringSegment(value=value)
|
||||
elif segment_type == SegmentType.ARRAY_BOOLEAN:
|
||||
return ArrayBooleanSegment(value=value)
|
||||
elif segment_type == SegmentType.ARRAY_NUMBER:
|
||||
return ArrayNumberSegment(value=value)
|
||||
elif segment_type == SegmentType.ARRAY_OBJECT:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
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
|
||||
|
|
@ -20,3 +20,6 @@ ignore_missing_imports=True
|
|||
|
||||
[mypy-flask_restx.inputs]
|
||||
ignore_missing_imports=True
|
||||
|
||||
[mypy-google.cloud.storage]
|
||||
ignore_missing_imports=True
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class TestSegmentTypeIsArrayType:
|
|||
SegmentType.ARRAY_NUMBER,
|
||||
SegmentType.ARRAY_OBJECT,
|
||||
SegmentType.ARRAY_FILE,
|
||||
SegmentType.ARRAY_BOOLEAN,
|
||||
]
|
||||
expected_non_array_types = [
|
||||
SegmentType.INTEGER,
|
||||
|
|
@ -34,6 +35,7 @@ class TestSegmentTypeIsArrayType:
|
|||
SegmentType.FILE,
|
||||
SegmentType.NONE,
|
||||
SegmentType.GROUP,
|
||||
SegmentType.BOOLEAN,
|
||||
]
|
||||
|
||||
for seg_type in expected_array_types:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,729 @@
|
|||
"""
|
||||
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 +1,27 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,567 @@
|
|||
"""
|
||||
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}"
|
||||
)
|
||||
|
|
@ -2,6 +2,8 @@ import time
|
|||
import uuid
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.file import File, FileTransferMethod, FileType
|
||||
from core.variables import ArrayFileSegment
|
||||
|
|
@ -272,3 +274,220 @@ def test_array_file_contains_file_name():
|
|||
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
|
||||
assert result.outputs is not None
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ from core.workflow.nodes.list_operator.entities import (
|
|||
FilterCondition,
|
||||
Limit,
|
||||
ListOperatorNodeData,
|
||||
OrderBy,
|
||||
Order,
|
||||
OrderByConfig,
|
||||
)
|
||||
from core.workflow.nodes.list_operator.exc import InvalidKeyError
|
||||
from core.workflow.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func
|
||||
|
|
@ -27,7 +28,7 @@ def list_operator_node():
|
|||
FilterCondition(key="type", comparison_operator="in", value=[FileType.IMAGE, FileType.DOCUMENT])
|
||||
],
|
||||
),
|
||||
"order_by": OrderBy(enabled=False, value="asc"),
|
||||
"order_by": OrderByConfig(enabled=False, value=Order.ASC),
|
||||
"limit": Limit(enabled=False, size=0),
|
||||
"extract_by": ExtractConfig(enabled=False, serial="1"),
|
||||
"title": "Test Title",
|
||||
|
|
|
|||
|
|
@ -24,16 +24,18 @@ from core.variables.segments import (
|
|||
ArrayNumberSegment,
|
||||
ArrayObjectSegment,
|
||||
ArrayStringSegment,
|
||||
BooleanSegment,
|
||||
FileSegment,
|
||||
FloatSegment,
|
||||
IntegerSegment,
|
||||
NoneSegment,
|
||||
ObjectSegment,
|
||||
Segment,
|
||||
StringSegment,
|
||||
)
|
||||
from core.variables.types import SegmentType
|
||||
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():
|
||||
|
|
@ -139,6 +141,26 @@ def test_array_number_variable():
|
|||
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():
|
||||
mapping = {
|
||||
"id": str(uuid4()),
|
||||
|
|
@ -847,15 +869,22 @@ class TestBuildSegmentValueErrors:
|
|||
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)
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
#!/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()
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
#!/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)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
#!/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()
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#!/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()
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
#!/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()
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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,
|
||||
)
|
||||
|
|
@ -2,21 +2,28 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title: string
|
||||
isOptional?: boolean
|
||||
children: React.JSX.Element
|
||||
}
|
||||
|
||||
const Field: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
isOptional,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
'use client'
|
||||
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 { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import ConfigSelect from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
import SelectTypeItem from '../select-type-item'
|
||||
import Field from './field'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
|
@ -20,7 +19,13 @@ import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/
|
|||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
|
||||
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 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 { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
|
@ -51,6 +56,20 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
const [tempPayload, setTempPayload] = useState<InputVar>(payload || getNewVarInWorkflow('') as any)
|
||||
const { type, label, variable, options, max_length } = tempPayload
|
||||
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(() => {
|
||||
// To fix the first input element auto focus, then directly close modal will raise error
|
||||
if (isShow)
|
||||
|
|
@ -82,25 +101,74 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
}
|
||||
}, [])
|
||||
|
||||
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])
|
||||
|
||||
const handleVarKeyBlur = useCallback((e: any) => {
|
||||
|
|
@ -142,15 +210,6 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
if (!isVariableNameValid)
|
||||
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) {
|
||||
Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.labelNameRequired') })
|
||||
return
|
||||
|
|
@ -204,18 +263,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
>
|
||||
<div className='mb-8' ref={modalRef} tabIndex={-1}>
|
||||
<div className='space-y-2'>
|
||||
|
||||
<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 title={t('appDebug.variableConfig.varName')}>
|
||||
|
|
@ -330,6 +379,21 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{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'>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
'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
|
||||
|
|
@ -12,7 +12,7 @@ import SelectVarType from './select-var-type'
|
|||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
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 Confirm from '@/app/components/base/confirm'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
|
|
@ -80,7 +80,28 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
|||
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)
|
||||
return true
|
||||
}
|
||||
|
||||
const { setShowExternalDataToolModal } = useModalContext()
|
||||
|
|
@ -190,7 +211,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
|||
const handleConfig = ({ key, type, index, name, config, icon, icon_background }: ExternalDataToolParams) => {
|
||||
// setCurrKey(key)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
@ -245,7 +266,8 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
|||
isShow={isShowEditModal}
|
||||
onClose={hideEditModal}
|
||||
onConfirm={(item) => {
|
||||
updatePromptVariableItem(item)
|
||||
const isValid = updatePromptVariableItem(item)
|
||||
if (!isValid) return
|
||||
hideEditModal()
|
||||
}}
|
||||
varKeys={promptVariables.map(v => v.key)}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ const SelectVarType: FC<Props> = ({
|
|||
<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.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 className='h-px border-t border-components-panel-border'></div>
|
||||
<div className='p-1'>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||
return t('tools.setBuiltInTools.number')
|
||||
if (type === 'text-input')
|
||||
return t('tools.setBuiltInTools.string')
|
||||
if (type === 'checkbox')
|
||||
return 'boolean'
|
||||
if (type === 'file')
|
||||
return t('tools.setBuiltInTools.file')
|
||||
return type
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Textarea from '@/app/components/base/textarea'
|
|||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import type { Inputs } from '@/models/debug'
|
||||
import cn from '@/utils/classnames'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
|
||||
type Props = {
|
||||
inputs: Inputs
|
||||
|
|
@ -31,7 +32,7 @@ const ChatUserInput = ({
|
|||
return obj
|
||||
})()
|
||||
|
||||
const handleInputValueChange = (key: string, value: string) => {
|
||||
const handleInputValueChange = (key: string, value: string | boolean) => {
|
||||
if (!(key in promptVariableObj))
|
||||
return
|
||||
|
||||
|
|
@ -55,10 +56,12 @@ const ChatUserInput = ({
|
|||
className='mb-4 last-of-type:mb-0'
|
||||
>
|
||||
<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'>
|
||||
{type === 'string' && (
|
||||
<Input
|
||||
|
|
@ -96,6 +99,14 @@ const ChatUserInput = ({
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows
|
|||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
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 { IS_CE_EDITION } from '@/config'
|
||||
import type { Inputs } from '@/models/debug'
|
||||
|
|
@ -259,7 +259,7 @@ const Debug: FC<IDebug> = ({
|
|||
}
|
||||
|
||||
const data: Record<string, any> = {
|
||||
inputs,
|
||||
inputs: formatBooleanInputs(modelConfig.configs.prompt_variables, inputs),
|
||||
model_config: postModelConfig,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ import {
|
|||
useModelListAndDefaultModelAndCurrentProviderAndModel,
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { fetchCollectionList } from '@/service/tools'
|
||||
import type { Collection } from '@/app/components/tools/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
|
|
@ -82,6 +81,7 @@ import { supportFunctionCall } from '@/utils/tool-call'
|
|||
import { MittProvider } from '@/context/mitt-context'
|
||||
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { fetchCollectionList } from '@/service/tools'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
type PublishConfig = {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type { VisionFile, VisionSettings } from '@/types/app'
|
|||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import cn from '@/utils/classnames'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
|
||||
export type IPromptValuePanelProps = {
|
||||
appType: AppType
|
||||
|
|
@ -66,7 +67,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||
else { return !modelConfig.configs.prompt_template }
|
||||
}, [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))
|
||||
return
|
||||
|
||||
|
|
@ -109,10 +110,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||
className='mb-4 last-of-type:mb-0'
|
||||
>
|
||||
<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'>
|
||||
{type === 'string' && (
|
||||
<Input
|
||||
|
|
@ -151,6 +154,14 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested
|
|||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileEntity } from '../../file-uploader/types'
|
||||
import { formatBooleanInputs } from '@/utils/model-config'
|
||||
import Avatar from '../../avatar'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
|
|
@ -89,7 +90,7 @@ const ChatWrapper = () => {
|
|||
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required }) => required)
|
||||
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
|
||||
if (requiredVars.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
|
|
@ -131,7 +132,7 @@ const ChatWrapper = () => {
|
|||
const data: any = {
|
||||
query: message,
|
||||
files,
|
||||
inputs: currentConversationId ? currentConversationInputs : newConversationInputs,
|
||||
inputs: formatBooleanInputs(inputsForms, currentConversationId ? currentConversationInputs : newConversationInputs),
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,14 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
type: 'number',
|
||||
}
|
||||
}
|
||||
|
||||
if(item.checkbox) {
|
||||
return {
|
||||
...item.checkbox,
|
||||
default: false,
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
if (item.select) {
|
||||
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
|
||||
return {
|
||||
|
|
@ -245,6 +253,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (item.json_object) {
|
||||
return {
|
||||
...item.json_object,
|
||||
type: 'json_object',
|
||||
}
|
||||
}
|
||||
|
||||
let value = initInputs[item['text-input'].variable]
|
||||
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
|
||||
value = value.slice(0, item['text-input'].max_length)
|
||||
|
|
@ -340,7 +355,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
|||
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required }) => required)
|
||||
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
|
||||
if (requiredVars.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import Textarea from '@/app/components/base/textarea'
|
|||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
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 = {
|
||||
showTip?: boolean
|
||||
|
|
@ -42,12 +45,14 @@ const InputsFormContent = ({ showTip }: Props) => {
|
|||
<div className='space-y-4'>
|
||||
{visibleInputsForms.map(form => (
|
||||
<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 && (
|
||||
<Input
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
|
|
@ -70,6 +75,14 @@ const InputsFormContent = ({ showTip }: Props) => {
|
|||
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 && (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
|
|
@ -105,6 +118,18 @@ const InputsFormContent = ({ showTip }: Props) => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
))}
|
||||
{showTip && (
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const useCheckInputsForms = () => {
|
|||
const checkInputsForm = useCallback((inputs: Record<string, any>, inputsForm: InputForm[]) => {
|
||||
let hasEmptyInput = ''
|
||||
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) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ export const getProcessedInputs = (inputs: Record<string, any>, inputsForm: Inpu
|
|||
|
||||
inputsForm.forEach((item) => {
|
||||
const inputValue = inputs[item.variable]
|
||||
// set boolean type default value
|
||||
if(item.type === InputVarType.checkbox) {
|
||||
processedInputs[item.variable] = !!inputValue
|
||||
return
|
||||
}
|
||||
|
||||
if (!inputValue)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const ChatWrapper = () => {
|
|||
|
||||
let hasEmptyInput = ''
|
||||
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) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
|
|
|
|||
|
|
@ -195,6 +195,13 @@ export const useEmbeddedChatbot = () => {
|
|||
type: 'number',
|
||||
}
|
||||
}
|
||||
if (item.checkbox) {
|
||||
return {
|
||||
...item.checkbox,
|
||||
default: false,
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
if (item.select) {
|
||||
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
|
||||
return {
|
||||
|
|
@ -218,6 +225,13 @@ export const useEmbeddedChatbot = () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (item.json_object) {
|
||||
return {
|
||||
...item.json_object,
|
||||
type: 'json_object',
|
||||
}
|
||||
}
|
||||
|
||||
let value = initInputs[item['text-input'].variable]
|
||||
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
|
||||
value = value.slice(0, item['text-input'].max_length)
|
||||
|
|
@ -312,7 +326,7 @@ export const useEmbeddedChatbot = () => {
|
|||
|
||||
let hasEmptyInput = ''
|
||||
let fileIsUploading = false
|
||||
const requiredVars = inputsForms.filter(({ required }) => required)
|
||||
const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox)
|
||||
if (requiredVars.length) {
|
||||
requiredVars.forEach(({ variable, label, type }) => {
|
||||
if (hasEmptyInput)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import Textarea from '@/app/components/base/textarea'
|
|||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
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 = {
|
||||
showTip?: boolean
|
||||
|
|
@ -42,12 +45,14 @@ const InputsFormContent = ({ showTip }: Props) => {
|
|||
<div className='space-y-4'>
|
||||
{visibleInputsForms.map(form => (
|
||||
<div key={form.variable} className='space-y-1'>
|
||||
{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 && (
|
||||
<Input
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
|
|
@ -70,6 +75,14 @@ const InputsFormContent = ({ showTip }: Props) => {
|
|||
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 && (
|
||||
<PortalSelect
|
||||
popupClassName='w-[200px]'
|
||||
|
|
@ -105,6 +118,18 @@ const InputsFormContent = ({ showTip }: Props) => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
))}
|
||||
{showTip && (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export enum FormTypeEnum {
|
|||
secretInput = 'secret-input',
|
||||
select = 'select',
|
||||
radio = 'radio',
|
||||
boolean = 'boolean',
|
||||
checkbox = 'checkbox',
|
||||
files = 'files',
|
||||
file = 'file',
|
||||
modelSelector = 'model-selector',
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ const CurrentBlockReplacementBlock = ({
|
|||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ const ErrorMessageBlockReplacementBlock = ({
|
|||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ const LastRunReplacementBlock = ({
|
|||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -77,6 +77,13 @@ const AppInputsPanel = ({
|
|||
required: false,
|
||||
}
|
||||
}
|
||||
if(item.checkbox) {
|
||||
return {
|
||||
...item.checkbox,
|
||||
type: 'checkbox',
|
||||
required: false,
|
||||
}
|
||||
}
|
||||
if (item.select) {
|
||||
return {
|
||||
...item.select,
|
||||
|
|
@ -103,6 +110,13 @@ const AppInputsPanel = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (item.json_object) {
|
||||
return {
|
||||
...item.json_object,
|
||||
type: 'json_object',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...item['text-input'],
|
||||
type: 'text-input',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ const StrategyDetail: FC<Props> = ({
|
|||
return t('tools.setBuiltInTools.number')
|
||||
if (type === 'text-input')
|
||||
return t('tools.setBuiltInTools.string')
|
||||
if (type === 'checkbox')
|
||||
return 'boolean'
|
||||
if (type === 'file')
|
||||
return t('tools.setBuiltInTools.file')
|
||||
if (type === 'array[tools]')
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
|
|||
import {
|
||||
getFilesInLogs,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
import { formatBooleanInputs } from '@/utils/model-config'
|
||||
|
||||
export type IResultProps = {
|
||||
isWorkflow: boolean
|
||||
|
|
@ -124,7 +125,9 @@ const Result: FC<IResultProps> = ({
|
|||
}
|
||||
|
||||
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)
|
||||
return res
|
||||
}) || [] // compatible with old version
|
||||
|
|
@ -158,7 +161,7 @@ const Result: FC<IResultProps> = ({
|
|||
return
|
||||
|
||||
const data: Record<string, any> = {
|
||||
inputs,
|
||||
inputs: formatBooleanInputs(promptConfig?.prompt_variables, inputs),
|
||||
}
|
||||
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
|
||||
data.files = completionFiles.map((item) => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uplo
|
|||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
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 = {
|
||||
siteInfo: SiteInfo
|
||||
|
|
@ -93,7 +96,9 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
|
||||
: promptConfig.prompt_variables.map(item => (
|
||||
<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'>
|
||||
{item.type === 'select' && (
|
||||
<Select
|
||||
|
|
@ -118,7 +123,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||
className='h-[104px] sm:text-xs'
|
||||
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
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' && (
|
||||
|
|
@ -129,6 +134,14 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||
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' && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={inputs[item.key] ? [inputs[item.key]] : []}
|
||||
|
|
@ -149,6 +162,18 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export const toType = (type: string) => {
|
|||
return 'text-input'
|
||||
case 'number':
|
||||
return 'number-input'
|
||||
case 'boolean':
|
||||
return 'checkbox'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
'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)
|
||||
|
|
@ -25,6 +25,7 @@ import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
|||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import BoolInput from './bool-input'
|
||||
|
||||
type Props = {
|
||||
payload: InputVar
|
||||
|
|
@ -92,6 +93,7 @@ const FormItem: FC<Props> = ({
|
|||
return ''
|
||||
})()
|
||||
|
||||
const isBooleanType = type === InputVarType.checkbox
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
|
||||
const isContext = type === InputVarType.contexts
|
||||
const isIterator = type === InputVarType.iterator
|
||||
|
|
@ -113,7 +115,7 @@ const FormItem: FC<Props> = ({
|
|||
|
||||
return (
|
||||
<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='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
|
||||
{!payload.required && <span className='system-xs-regular text-text-tertiary'>{t('workflow.panel.optional')}</span>}
|
||||
|
|
@ -166,6 +168,15 @@ const FormItem: FC<Props> = ({
|
|||
)
|
||||
}
|
||||
|
||||
{isBooleanType && (
|
||||
<BoolInput
|
||||
name={payload.label as string}
|
||||
value={!!value}
|
||||
required={payload.required}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
type === InputVarType.json && (
|
||||
<CodeEditor
|
||||
|
|
@ -176,6 +187,18 @@ const FormItem: FC<Props> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
{ 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) && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={singleFileValue}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export type BeforeRunFormProps = {
|
|||
} & Partial<SpecialResultPanelProps>
|
||||
|
||||
function formatValue(value: string | any, type: InputVarType) {
|
||||
if(type === InputVarType.checkbox)
|
||||
return !!value
|
||||
if(value === undefined || value === null)
|
||||
return value
|
||||
if (type === InputVarType.number)
|
||||
|
|
@ -87,7 +89,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
|
||||
form.inputs.forEach((input) => {
|
||||
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 })
|
||||
|
||||
if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const FormInputItem: FC<Props> = ({
|
|||
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
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 showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import type { FC } 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'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -15,6 +15,8 @@ const getIcon = (type: InputVarType) => {
|
|||
[InputVarType.paragraph]: RiAlignLeft,
|
||||
[InputVarType.select]: RiCheckboxMultipleLine,
|
||||
[InputVarType.number]: RiHashtag,
|
||||
[InputVarType.checkbox]: RiCheckboxLine,
|
||||
[InputVarType.jsonObject]: RiBracesLine,
|
||||
[InputVarType.singleFile]: RiFileList2Line,
|
||||
[InputVarType.multiFiles]: RiFileCopy2Line,
|
||||
} as any)[type] || RiTextSnippet
|
||||
|
|
|
|||
|
|
@ -57,11 +57,13 @@ export const hasValidChildren = (children: any): boolean => {
|
|||
)
|
||||
}
|
||||
|
||||
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
export const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
return ({
|
||||
[InputVarType.number]: VarType.number,
|
||||
[InputVarType.checkbox]: VarType.boolean,
|
||||
[InputVarType.singleFile]: VarType.file,
|
||||
[InputVarType.multiFiles]: VarType.arrayFile,
|
||||
[InputVarType.jsonObject]: VarType.object,
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
|
|
@ -228,14 +230,27 @@ const formatItem = (
|
|||
variables,
|
||||
} = data as StartNodeType
|
||||
res.vars = variables.map((v) => {
|
||||
return {
|
||||
const type = inputVarTypeToVarType(v.type)
|
||||
const varRes: Var = {
|
||||
variable: v.variable,
|
||||
type: inputVarTypeToVarType(v.type),
|
||||
type,
|
||||
isParagraph: v.type === InputVarType.paragraph,
|
||||
isSelect: v.type === InputVarType.select,
|
||||
options: v.options,
|
||||
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) {
|
||||
res.vars.push({
|
||||
|
|
@ -690,6 +705,8 @@ const getIterationItemType = ({
|
|||
return VarType.string
|
||||
case VarType.arrayNumber:
|
||||
return VarType.number
|
||||
case VarType.arrayBoolean:
|
||||
return VarType.boolean
|
||||
case VarType.arrayObject:
|
||||
return VarType.object
|
||||
case VarType.array:
|
||||
|
|
@ -743,6 +760,8 @@ const getLoopItemType = ({
|
|||
return VarType.number
|
||||
case VarType.arrayObject:
|
||||
return VarType.object
|
||||
case VarType.arrayBoolean:
|
||||
return VarType.boolean
|
||||
case VarType.array:
|
||||
return VarType.any
|
||||
case VarType.arrayFile:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type Props = {
|
|||
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> = ({
|
||||
readonly,
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
isRunAfterSingleRun={isRunAfterSingleRun}
|
||||
updateNodeRunningStatus={updateNodeRunningStatus}
|
||||
onSingleRunClicked={handleSingleRun}
|
||||
nodeInfo={nodeInfo}
|
||||
nodeInfo={nodeInfo!}
|
||||
singleRunResult={runResult!}
|
||||
isPaused={isPaused}
|
||||
{...passedLogParams}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ const LastRun: FC<Props> = ({
|
|||
updateNodeRunningStatus(hidePageOneStepFinishedStatus)
|
||||
resetHidePageStatus()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -77,7 +76,6 @@ const LastRun: FC<Props> = ({
|
|||
|
||||
useEffect(() => {
|
||||
resetHidePageStatus()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodeId])
|
||||
|
||||
const handlePageVisibilityChange = useCallback(() => {
|
||||
|
|
@ -117,7 +115,7 @@ const LastRun: FC<Props> = ({
|
|||
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
|
||||
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}
|
||||
nodeInfo={nodeInfo}
|
||||
nodeInfo={runResult as NodeTracing}
|
||||
showSteps={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@ const useLastRun = <T>({
|
|||
checkValid,
|
||||
} = oneStepRunRes
|
||||
|
||||
const nodeInfo = runResult
|
||||
const {
|
||||
nodeInfo,
|
||||
...singleRunParams
|
||||
} = useSingleRunFormParamsHooks(blockType)({
|
||||
id,
|
||||
|
|
@ -197,7 +197,6 @@ const useLastRun = <T>({
|
|||
setTabType(TabType.lastRun)
|
||||
|
||||
setInitShowLastRunTab(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initShowLastRunTab])
|
||||
const invalidLastRun = useInvalidLastRun(appId!, id)
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ const varTypeToInputVarType = (type: VarType, {
|
|||
return InputVarType.paragraph
|
||||
if (type === VarType.number)
|
||||
return InputVarType.number
|
||||
if (type === VarType.boolean)
|
||||
return InputVarType.checkbox
|
||||
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
|
||||
return InputVarType.json
|
||||
if (type === VarType.file)
|
||||
|
|
@ -185,11 +187,11 @@ const useOneStepRun = <T>({
|
|||
const isPaused = isPausedRef.current
|
||||
|
||||
// The backend don't support pause the single run, so the frontend handle the pause state.
|
||||
if(isPaused)
|
||||
if (isPaused)
|
||||
return
|
||||
|
||||
const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded
|
||||
if(!canRunLastRun) {
|
||||
if (!canRunLastRun) {
|
||||
doSetRunResult(data)
|
||||
return
|
||||
}
|
||||
|
|
@ -199,9 +201,9 @@ const useOneStepRun = <T>({
|
|||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
appendNodeInspectVars(id, vars, nodes)
|
||||
if(data?.status === NodeRunningStatus.Succeeded) {
|
||||
if (data?.status === NodeRunningStatus.Succeeded) {
|
||||
invalidLastRun()
|
||||
if(isStartNode)
|
||||
if (isStartNode)
|
||||
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.
|
||||
}
|
||||
|
|
@ -218,21 +220,21 @@ const useOneStepRun = <T>({
|
|||
})
|
||||
}
|
||||
const checkValidWrap = () => {
|
||||
if(!checkValid)
|
||||
if (!checkValid)
|
||||
return { isValid: true, errorMessage: '' }
|
||||
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
|
||||
}
|
||||
|
|
@ -251,7 +253,6 @@ const useOneStepRun = <T>({
|
|||
const { isValid } = checkValidWrap()
|
||||
setCanShowSingleRun(isValid)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data._isSingleRun])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -293,9 +294,9 @@ const useOneStepRun = <T>({
|
|||
if (!isIteration && !isLoop) {
|
||||
const isStartNode = data.type === BlockEnum.Start
|
||||
const postData: Record<string, any> = {}
|
||||
if(isStartNode) {
|
||||
if (isStartNode) {
|
||||
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
|
||||
if(isChatMode)
|
||||
if (isChatMode)
|
||||
postData.conversation_id = ''
|
||||
|
||||
postData.inputs = inputs
|
||||
|
|
@ -317,7 +318,7 @@ const useOneStepRun = <T>({
|
|||
{
|
||||
onWorkflowStarted: noop,
|
||||
onWorkflowFinished: (params) => {
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
|
|
@ -396,7 +397,7 @@ const useOneStepRun = <T>({
|
|||
setIterationRunResult(newIterationRunResult)
|
||||
},
|
||||
onError: () => {
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
|
|
@ -420,7 +421,7 @@ const useOneStepRun = <T>({
|
|||
{
|
||||
onWorkflowStarted: noop,
|
||||
onWorkflowFinished: (params) => {
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
|
|
@ -500,7 +501,7 @@ const useOneStepRun = <T>({
|
|||
setLoopRunResult(newLoopRunResult)
|
||||
},
|
||||
onError: () => {
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
|
|
@ -522,7 +523,7 @@ const useOneStepRun = <T>({
|
|||
hasError = true
|
||||
invalidLastRun()
|
||||
if (!isIteration && !isLoop) {
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
|
|
@ -544,11 +545,11 @@ const useOneStepRun = <T>({
|
|||
})
|
||||
}
|
||||
}
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
|
||||
if (!isIteration && !isLoop && !hasError) {
|
||||
if(isPausedRef.current)
|
||||
if (isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
|
|
@ -587,7 +588,7 @@ const useOneStepRun = <T>({
|
|||
}
|
||||
}
|
||||
return {
|
||||
label: item.label || item.variable,
|
||||
label: (typeof item.label === 'object' ? item.label.variable : item.label) || item.variable,
|
||||
variable: item.variable,
|
||||
type: varTypeToInputVarType(originalVar.type, {
|
||||
isSelect: !!originalVar.isSelect,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const useToggleExpend = ({ ref, hasFooter = true, isInNode }: Params) => {
|
|||
useEffect(() => {
|
||||
if (!ref?.current) return
|
||||
setWrapHeight(ref.current?.clientHeight)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isExpand])
|
||||
|
||||
const wrapClassName = (() => {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ import { AssignerNodeInputType, WriteMode } from '../../types'
|
|||
import type { AssignerNodeOperation } from '../../types'
|
||||
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 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 ActionButton from '@/app/components/base/action-button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { noop } from 'lodash-es'
|
||||
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
|
|
@ -59,23 +61,27 @@ const VarList: FC<Props> = ({
|
|||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleOperationChange = useCallback((index: number) => {
|
||||
const handleOperationChange = useCallback((index: number, varType: VarType) => {
|
||||
return (item: { value: string | number }) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft[index].operation = item.value as WriteMode
|
||||
draft[index].value = '' // Clear value when operation changes
|
||||
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
|
||||
else
|
||||
}
|
||||
else {
|
||||
draft[index].input_type = AssignerNodeInputType.variable
|
||||
}
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleToAssignedVarChange = useCallback((index: number) => {
|
||||
return (value: ValueSelector | string | number) => {
|
||||
return (value: ValueSelector | string | number | boolean) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft[index].value = value as ValueSelector
|
||||
})
|
||||
|
|
@ -145,7 +151,7 @@ const VarList: FC<Props> = ({
|
|||
value={item.operation}
|
||||
placeholder='Operation'
|
||||
disabled={!item.variable_selector || item.variable_selector.length === 0}
|
||||
onSelect={handleOperationChange(index)}
|
||||
onSelect={handleOperationChange(index, assignedVarType!)}
|
||||
assignedVarType={assignedVarType}
|
||||
writeModeTypes={writeModeTypes}
|
||||
writeModeTypesArr={writeModeTypesArr}
|
||||
|
|
@ -188,6 +194,12 @@ const VarList: FC<Props> = ({
|
|||
className='w-full'
|
||||
/>
|
||||
)}
|
||||
{assignedVarType === 'boolean' && (
|
||||
<BoolValue
|
||||
value={item.value as boolean}
|
||||
onChange={value => handleToAssignedVarChange(index)(value)}
|
||||
/>
|
||||
)}
|
||||
{assignedVarType === 'object' && (
|
||||
<CodeEditor
|
||||
value={item.value as string}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const nodeDefault: NodeDefault<AssignerNodeType> = {
|
|||
if (value.operation === WriteMode.set || value.operation === WriteMode.increment
|
||||
|| value.operation === WriteMode.decrement || value.operation === WriteMode.multiply
|
||||
|| 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') })
|
||||
}
|
||||
else if (!value.value?.length) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const getOperationItems = (
|
|||
]
|
||||
}
|
||||
|
||||
if (writeModeTypes && ['string', 'object'].includes(assignedVarType || '')) {
|
||||
if (writeModeTypes && ['string', 'boolean', 'object'].includes(assignedVarType || '')) {
|
||||
return writeModeTypes.map(type => ({
|
||||
value: type,
|
||||
name: type,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
|||
})
|
||||
syncOutputKeyOrders(defaultConfig.outputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultConfig])
|
||||
|
||||
const handleCodeChange = useCallback((code: string) => {
|
||||
|
|
@ -85,7 +84,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
|||
}, [allLanguageDefault, inputs, setInputs])
|
||||
|
||||
const handleSyncFunctionSignature = useCallback(() => {
|
||||
const generateSyncSignatureCode = (code: string) => {
|
||||
const generateSyncSignatureCode = (code: string) => {
|
||||
let mainDefRe
|
||||
let newMainDef
|
||||
if (inputs.code_language === CodeLanguage.javascript) {
|
||||
|
|
@ -159,7 +158,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
|||
})
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { VarType } from '@/app/components/workflow/types'
|
|||
import cn from '@/utils/classnames'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
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 { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow'
|
||||
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
|
||||
|
|
@ -142,12 +143,12 @@ const ConditionItem = ({
|
|||
|
||||
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
|
||||
const newCondition = {
|
||||
...condition,
|
||||
value: isArrayValue ? [value] : value,
|
||||
value: isArrayValue ? [value as string] : value,
|
||||
}
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition, isArrayValue])
|
||||
|
|
@ -203,8 +204,12 @@ const ConditionItem = ({
|
|||
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
|
||||
|
||||
const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
|
||||
const {
|
||||
conversationVariables,
|
||||
} = workflowStore.getState()
|
||||
const resolvedVarType = getVarType({
|
||||
valueSelector,
|
||||
conversationVariables,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
})
|
||||
|
|
@ -212,7 +217,7 @@ const ConditionItem = ({
|
|||
const newCondition = produce(condition, (draft) => {
|
||||
draft.variable_selector = valueSelector
|
||||
draft.varType = resolvedVarType
|
||||
draft.value = ''
|
||||
draft.value = resolvedVarType === VarType.boolean ? false : ''
|
||||
draft.comparison_operator = getOperators(resolvedVarType)[0]
|
||||
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
})
|
||||
|
|
@ -220,6 +225,14 @@ const ConditionItem = ({
|
|||
setOpen(false)
|
||||
}, [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 (
|
||||
<div className={cn('mb-1 flex last-of-type:mb-0', className)}>
|
||||
<div className={cn(
|
||||
|
|
@ -273,7 +286,7 @@ const ConditionItem = ({
|
|||
/>
|
||||
</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'>
|
||||
<ConditionInput
|
||||
disabled={disabled}
|
||||
|
|
@ -285,6 +298,16 @@ const ConditionItem = ({
|
|||
</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 && (
|
||||
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type ConditionValueProps = {
|
|||
variableSelector: string[]
|
||||
labelName?: string
|
||||
operator: ComparisonOperator
|
||||
value: string | string[]
|
||||
value: string | string[] | boolean
|
||||
}
|
||||
const ConditionValue = ({
|
||||
variableSelector,
|
||||
|
|
@ -46,6 +46,9 @@ const ConditionValue = ({
|
|||
if (Array.isArray(value)) // transfer method
|
||||
return value[0]
|
||||
|
||||
if(value === true || value === false)
|
||||
return value ? 'True' : 'False'
|
||||
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BlockEnum, type NodeDefault } from '../../types'
|
||||
import { BlockEnum, type NodeDefault, VarType } from '../../types'
|
||||
import { type IfElseNodeType, LogicalOperator } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
|
|
@ -58,13 +58,13 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
|
|||
if (isEmptyRelatedOperator(c.comparison_operator!))
|
||||
return true
|
||||
|
||||
return !!c.value
|
||||
return (c.varType === VarType.boolean || c.varType === VarType.arrayBoolean) ? c.value === undefined : !!c.value
|
||||
})
|
||||
if (!isSet)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
}
|
||||
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`) })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { isEmptyRelatedOperator } from './utils'
|
|||
import type { Condition, IfElseNodeType } from './types'
|
||||
import ConditionValue from './components/condition-value'
|
||||
import ConditionFilesListValue from './components/condition-files-list-value'
|
||||
import { VarType } from '../../types'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
||||
|
|
@ -23,18 +24,14 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
|||
if (!c.comparison_operator)
|
||||
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
|
||||
}
|
||||
else {
|
||||
if (isEmptyRelatedOperator(condition.comparison_operator!))
|
||||
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'>
|
||||
|
|
@ -73,7 +70,7 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
|||
<ConditionValue
|
||||
variableSelector={condition.variable_selector!}
|
||||
operator={condition.comparison_operator!}
|
||||
value={condition.value}
|
||||
value={condition.varType === VarType.boolean ? (!condition.value ? 'False' : condition.value) : condition.value}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type Condition = {
|
|||
variable_selector?: ValueSelector
|
||||
key?: string // sub variable key
|
||||
comparison_operator?: ComparisonOperator
|
||||
value: string | string[]
|
||||
value: string | string[] | boolean
|
||||
numberVarType?: NumberVarType
|
||||
sub_variable_condition?: CaseItem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
|||
varType: varItem.type,
|
||||
variable_selector: valueSelector,
|
||||
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 : '',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -107,6 +107,11 @@ export const getOperators = (type?: VarType, file?: { key: string }) => {
|
|||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.boolean:
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
]
|
||||
case VarType.file:
|
||||
return [
|
||||
ComparisonOperator.exists,
|
||||
|
|
@ -114,6 +119,7 @@ export const getOperators = (type?: VarType, file?: { key: string }) => {
|
|||
]
|
||||
case VarType.arrayString:
|
||||
case VarType.arrayNumber:
|
||||
case VarType.arrayBoolean:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
|
|||
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/u
|
|||
import SubVariablePicker from './sub-variable-picker'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/constants'
|
||||
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 useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import cn from '@/utils/classnames'
|
||||
|
|
@ -28,8 +29,8 @@ const VAR_INPUT_SUPPORTED_KEYS: Record<string, VarType> = {
|
|||
|
||||
type Props = {
|
||||
condition: Condition
|
||||
onChange: (condition: Condition) => void
|
||||
varType: VarType
|
||||
onChange: (condition: Condition) => void
|
||||
hasSubVariable: boolean
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
|
|
@ -58,6 +59,7 @@ const FilterCondition: FC<Props> = ({
|
|||
|
||||
const isSelect = [ComparisonOperator.in, ComparisonOperator.notIn, ComparisonOperator.allOf].includes(condition.comparison_operator)
|
||||
const isArrayValue = condition.key === 'transfer_method' || condition.key === 'type'
|
||||
const isBoolean = varType === VarType.boolean
|
||||
|
||||
const selectOptions = useMemo(() => {
|
||||
if (isSelect) {
|
||||
|
|
@ -112,6 +114,12 @@ const FilterCondition: FC<Props> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
else if (isBoolean) {
|
||||
inputElement = (<BoolValue
|
||||
value={condition.value as boolean}
|
||||
onChange={handleChange('value')}
|
||||
/>)
|
||||
}
|
||||
else if (supportVariableInput) {
|
||||
inputElement = (
|
||||
<Input
|
||||
|
|
@ -162,7 +170,7 @@ const FilterCondition: FC<Props> = ({
|
|||
<div className='flex space-x-1'>
|
||||
<ConditionOperator
|
||||
className='h-8 bg-components-input-bg-normal'
|
||||
varType={expectedVarType ?? VarType.string}
|
||||
varType={expectedVarType ?? varType ?? VarType.string}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={handleChange('comparison_operator')}
|
||||
file={hasSubVariable ? { key: condition.key } : undefined}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
|||
},
|
||||
checkValid(payload: ListFilterNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variable, var_type, filter_by } = payload
|
||||
const { variable, var_type, filter_by, item_var_type } = payload
|
||||
|
||||
if (!errorMessages && !variable?.length)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.inputVar') })
|
||||
|
|
@ -51,7 +51,7 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
|||
if (!errorMessages && !filter_by.conditions[0]?.comparison_operator)
|
||||
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') })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export type Limit = {
|
|||
export type Condition = {
|
||||
key: string
|
||||
comparison_operator: ComparisonOperator
|
||||
value: string | number | string[]
|
||||
value: string | number | boolean | string[]
|
||||
}
|
||||
|
||||
export type ListFilterNodeType = CommonNodeType & {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
|
|||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
let itemVarType = varType
|
||||
let itemVarType
|
||||
switch (varType) {
|
||||
case VarType.arrayNumber:
|
||||
itemVarType = VarType.number
|
||||
|
|
@ -59,6 +59,11 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
|
|||
case VarType.arrayObject:
|
||||
itemVarType = VarType.object
|
||||
break
|
||||
case VarType.arrayBoolean:
|
||||
itemVarType = VarType.boolean
|
||||
break
|
||||
default:
|
||||
itemVarType = varType
|
||||
}
|
||||
return { varType, itemVarType }
|
||||
}, [availableNodes, getCurrentVariableType, inputs.variable, isChatMode, isInIteration, iterationNode, loopNode])
|
||||
|
|
@ -84,7 +89,7 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
|
|||
draft.filter_by.conditions = [{
|
||||
key: (isFileArray && !draft.filter_by.conditions[0]?.key) ? 'name' : '',
|
||||
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)
|
||||
draft.order_by.key = 'name'
|
||||
|
|
@ -94,7 +99,7 @@ const useConfig = (id: string, payload: ListFilterNodeType) => {
|
|||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
// 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) => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import VisualEditor from './visual-editor'
|
|||
import SchemaEditor from './schema-editor'
|
||||
import {
|
||||
checkJsonSchemaDepth,
|
||||
convertBooleanToString,
|
||||
getValidationErrorMessage,
|
||||
jsonToSchema,
|
||||
preValidateSchema,
|
||||
|
|
@ -87,7 +86,6 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||
return
|
||||
}
|
||||
convertBooleanToString(schema)
|
||||
const validationErrors = validateSchemaAgainstDraft7(schema)
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(validationErrors))
|
||||
|
|
@ -168,7 +166,6 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||
return
|
||||
}
|
||||
convertBooleanToString(schema)
|
||||
const validationErrors = validateSchemaAgainstDraft7(schema)
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(validationErrors))
|
||||
|
|
|
|||
|
|
@ -39,21 +39,19 @@ type EditCardProps = {
|
|||
const TYPE_OPTIONS = [
|
||||
{ value: Type.string, text: 'string' },
|
||||
{ value: Type.number, text: 'number' },
|
||||
// { value: Type.boolean, text: 'boolean' },
|
||||
{ value: Type.boolean, text: 'boolean' },
|
||||
{ value: Type.object, text: 'object' },
|
||||
{ value: ArrayType.string, text: 'array[string]' },
|
||||
{ value: ArrayType.number, text: 'array[number]' },
|
||||
// { value: ArrayType.boolean, text: 'array[boolean]' },
|
||||
{ value: ArrayType.object, text: 'array[object]' },
|
||||
]
|
||||
|
||||
const MAXIMUM_DEPTH_TYPE_OPTIONS = [
|
||||
{ value: Type.string, text: 'string' },
|
||||
{ value: Type.number, text: 'number' },
|
||||
// { value: Type.boolean, text: 'boolean' },
|
||||
{ value: Type.boolean, text: 'boolean' },
|
||||
{ value: ArrayType.string, text: 'array[string]' },
|
||||
{ value: ArrayType.number, text: 'array[number]' },
|
||||
// { value: ArrayType.boolean, text: 'array[boolean]' },
|
||||
]
|
||||
|
||||
const EditCard: FC<EditCardProps> = ({
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ export const getValidationErrorMessage = (errors: ValidationError[]) => {
|
|||
return message
|
||||
}
|
||||
|
||||
// Previous Not support boolean type, so transform boolean to string when paste it into schema editor
|
||||
export const convertBooleanToString = (schema: any) => {
|
||||
if (schema.type === Type.boolean)
|
||||
schema.type = Type.string
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import cn from '@/utils/classnames'
|
|||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
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'
|
||||
|
||||
|
|
@ -129,12 +130,12 @@ const ConditionItem = ({
|
|||
|
||||
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
|
||||
const newCondition = {
|
||||
...condition,
|
||||
value: isArrayValue ? [value] : value,
|
||||
value: isArrayValue ? [value as string] : value,
|
||||
}
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition, isArrayValue])
|
||||
|
|
@ -253,7 +254,7 @@ const ConditionItem = ({
|
|||
/>
|
||||
</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'>
|
||||
<ConditionInput
|
||||
disabled={disabled}
|
||||
|
|
@ -264,6 +265,14 @@ const ConditionItem = ({
|
|||
</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 && (
|
||||
<div className='border-t border-t-divider-subtle px-2 py-1 pt-[3px]'>
|
||||
|
|
|
|||
|
|
@ -18,33 +18,16 @@ import {
|
|||
ValueType,
|
||||
VarType,
|
||||
} 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 = {
|
||||
nodeId: string
|
||||
|
|
@ -83,6 +66,8 @@ const FormItem = ({
|
|||
return arrayNumberPlaceholder
|
||||
if (var_type === VarType.arrayObject)
|
||||
return arrayObjectPlaceholder
|
||||
if (var_type === VarType.arrayBoolean)
|
||||
return arrayBoolPlaceholder
|
||||
return objectPlaceholder
|
||||
}, [var_type])
|
||||
|
||||
|
|
@ -120,6 +105,14 @@ const FormItem = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.boolean && (
|
||||
<BoolValue
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant
|
||||
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
|
||||
|
|
@ -137,6 +130,15 @@ const FormItem = ({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.arrayBoolean && (
|
||||
<ArrayBoolList
|
||||
className='mt-2'
|
||||
list={value || [false]}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ValueType, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type ItemProps = {
|
||||
item: LoopVariable
|
||||
|
|
@ -42,12 +43,25 @@ const Item = ({
|
|||
handleUpdateLoopVariable(item.id, { label: e.target.value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const getDefaultValue = useCallback((varType: VarType, valueType: ValueType) => {
|
||||
if(valueType === ValueType.variable)
|
||||
return undefined
|
||||
switch (varType) {
|
||||
case VarType.boolean:
|
||||
return false
|
||||
case VarType.arrayBoolean:
|
||||
return [false]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleUpdateItemVarType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { var_type: value, value: undefined })
|
||||
handleUpdateLoopVariable(item.id, { var_type: value, value: getDefaultValue(value, item.value_type) })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValueType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value_type: value, value: undefined })
|
||||
handleUpdateLoopVariable(item.id, { value_type: value, value: getDefaultValue(item.var_type, value) })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValue = useCallback((value: any) => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue