diff --git a/api/core/workflow/nodes/code/code_node.py b/api/core/workflow/nodes/code/code_node.py index 8034f4e55d..bfdec73199 100644 --- a/api/core/workflow/nodes/code/code_node.py +++ b/api/core/workflow/nodes/code/code_node.py @@ -153,11 +153,13 @@ class CodeNode(BaseNode): raise ValueError(f'{variable} in input form is out of range.') if isinstance(value, float): - value = round(value, MAX_PRECISION) + # raise error if precision is too high + if len(str(value).split('.')[1]) > MAX_PRECISION: + raise ValueError(f'{variable} in output form has too high precision.') return value - def _transform_result(self, result: dict, output_schema: dict[str, CodeNodeData.Output], + def _transform_result(self, result: dict, output_schema: Optional[dict[str, CodeNodeData.Output]], prefix: str = '', depth: int = 1) -> dict: """ @@ -170,6 +172,47 @@ class CodeNode(BaseNode): raise ValueError("Depth limit reached, object too deep.") transformed_result = {} + if output_schema is None: + # validate output thought instance type + for output_name, output_value in result.items(): + if isinstance(output_value, dict): + self._transform_result( + result=output_value, + output_schema=None, + prefix=f'{prefix}.{output_name}' if prefix else output_name, + depth=depth + 1 + ) + elif isinstance(output_value, (int, float)): + self._check_number( + value=output_value, + variable=f'{prefix}.{output_name}' if prefix else output_name + ) + elif isinstance(output_value, str): + self._check_string( + value=output_value, + variable=f'{prefix}.{output_name}' if prefix else output_name + ) + elif isinstance(output_value, list): + if all(isinstance(value, (int, float)) for value in output_value): + for value in output_value: + self._check_number( + value=value, + variable=f'{prefix}.{output_name}' if prefix else output_name + ) + elif all(isinstance(value, str) for value in output_value): + for value in output_value: + self._check_string( + value=value, + variable=f'{prefix}.{output_name}' if prefix else output_name + ) + else: + raise ValueError(f'Output {prefix}.{output_name} is not a valid array. make sure all elements are of the same type.') + else: + raise ValueError(f'Output {prefix}.{output_name} is not a valid type.') + + return result + + parameters_validated = {} for output_name, output_config in output_schema.items(): if output_config.type == 'object': # check if output is object @@ -236,6 +279,12 @@ class CodeNode(BaseNode): ] else: raise ValueError(f'Output type {output_config.type} is not supported.') + + parameters_validated[output_name] = True + + # check if all output parameters are validated + if len(parameters_validated) != len(result): + raise ValueError('Not all output parameters are validated.') return transformed_result diff --git a/api/core/workflow/nodes/code/entities.py b/api/core/workflow/nodes/code/entities.py index 6a18d181cb..ec3e3fe530 100644 --- a/api/core/workflow/nodes/code/entities.py +++ b/api/core/workflow/nodes/code/entities.py @@ -1,4 +1,4 @@ -from typing import Literal, Union +from typing import Literal, Optional from pydantic import BaseModel @@ -12,7 +12,7 @@ class CodeNodeData(BaseNodeData): """ class Output(BaseModel): type: Literal['string', 'number', 'object', 'array[string]', 'array[number]'] - children: Union[None, dict[str, 'Output']] + children: Optional[dict[str, 'Output']] variables: list[VariableSelector] answer: str diff --git a/api/tests/integration_tests/workflow/nodes/test_code.py b/api/tests/integration_tests/workflow/nodes/test_code.py index 2885b9f458..0b7217b053 100644 --- a/api/tests/integration_tests/workflow/nodes/test_code.py +++ b/api/tests/integration_tests/workflow/nodes/test_code.py @@ -1,8 +1,9 @@ import pytest +from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.variable_pool import VariablePool from core.workflow.nodes.code.code_node import CodeNode -from models.workflow import WorkflowNodeExecutionStatus, WorkflowRunStatus +from models.workflow import WorkflowNodeExecutionStatus from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock @pytest.mark.parametrize('setup_code_executor_mock', [['none']], indirect=True) @@ -15,30 +16,37 @@ def test_execute_code(setup_code_executor_mock): ''' # trim first 4 spaces at the beginning of each line code = '\n'.join([line[4:] for line in code.split('\n')]) - node = CodeNode(config={ - 'id': '1', - 'data': { - 'outputs': { - 'result': { - 'type': 'number', + node = CodeNode( + tenant_id='1', + app_id='1', + workflow_id='1', + user_id='1', + user_from=InvokeFrom.WEB_APP, + config={ + 'id': '1', + 'data': { + 'outputs': { + 'result': { + 'type': 'number', + }, }, - }, - 'title': '123', - 'variables': [ - { - 'variable': 'args1', - 'value_selector': ['1', '123', 'args1'], - }, - { - 'variable': 'args2', - 'value_selector': ['1', '123', 'args2'] - } - ], - 'answer': '123', - 'code_language': 'python3', - 'code': code + 'title': '123', + 'variables': [ + { + 'variable': 'args1', + 'value_selector': ['1', '123', 'args1'], + }, + { + 'variable': 'args2', + 'value_selector': ['1', '123', 'args2'] + } + ], + 'answer': '123', + 'code_language': 'python3', + 'code': code + } } - }) + ) # construct variable pool pool = VariablePool(system_variables={}, user_inputs={}) @@ -61,30 +69,37 @@ def test_execute_code_output_validator(setup_code_executor_mock): ''' # trim first 4 spaces at the beginning of each line code = '\n'.join([line[4:] for line in code.split('\n')]) - node = CodeNode(config={ - 'id': '1', - 'data': { - "outputs": { - "result": { - "type": "string", + node = CodeNode( + tenant_id='1', + app_id='1', + workflow_id='1', + user_id='1', + user_from=InvokeFrom.WEB_APP, + config={ + 'id': '1', + 'data': { + "outputs": { + "result": { + "type": "string", + }, }, - }, - 'title': '123', - 'variables': [ - { - 'variable': 'args1', - 'value_selector': ['1', '123', 'args1'], - }, - { - 'variable': 'args2', - 'value_selector': ['1', '123', 'args2'] - } - ], - 'answer': '123', - 'code_language': 'python3', - 'code': code + 'title': '123', + 'variables': [ + { + 'variable': 'args1', + 'value_selector': ['1', '123', 'args1'], + }, + { + 'variable': 'args2', + 'value_selector': ['1', '123', 'args2'] + } + ], + 'answer': '123', + 'code_language': 'python3', + 'code': code + } } - }) + ) # construct variable pool pool = VariablePool(system_variables={}, user_inputs={}) @@ -108,60 +123,67 @@ def test_execute_code_output_validator_depth(): ''' # trim first 4 spaces at the beginning of each line code = '\n'.join([line[4:] for line in code.split('\n')]) - node = CodeNode(config={ - 'id': '1', - 'data': { - "outputs": { - "string_validator": { - "type": "string", - }, - "number_validator": { - "type": "number", - }, - "number_array_validator": { - "type": "array[number]", - }, - "string_array_validator": { - "type": "array[string]", - }, - "object_validator": { - "type": "object", - "children": { - "result": { - "type": "number", - }, - "depth": { - "type": "object", - "children": { - "depth": { - "type": "object", - "children": { - "depth": { - "type": "number", + node = CodeNode( + tenant_id='1', + app_id='1', + workflow_id='1', + user_id='1', + user_from=InvokeFrom.WEB_APP, + config={ + 'id': '1', + 'data': { + "outputs": { + "string_validator": { + "type": "string", + }, + "number_validator": { + "type": "number", + }, + "number_array_validator": { + "type": "array[number]", + }, + "string_array_validator": { + "type": "array[string]", + }, + "object_validator": { + "type": "object", + "children": { + "result": { + "type": "number", + }, + "depth": { + "type": "object", + "children": { + "depth": { + "type": "object", + "children": { + "depth": { + "type": "number", + } } } } } } + }, + }, + 'title': '123', + 'variables': [ + { + 'variable': 'args1', + 'value_selector': ['1', '123', 'args1'], + }, + { + 'variable': 'args2', + 'value_selector': ['1', '123', 'args2'] } - }, - }, - 'title': '123', - 'variables': [ - { - 'variable': 'args1', - 'value_selector': ['1', '123', 'args1'], - }, - { - 'variable': 'args2', - 'value_selector': ['1', '123', 'args2'] - } - ], - 'answer': '123', - 'code_language': 'python3', - 'code': code + ], + 'answer': '123', + 'code_language': 'python3', + 'code': code + } } - }) + ) # construct result result = {