feat: add flatten_output configuration to iteration node (#27502)

This commit is contained in:
Novice 2025-10-27 16:04:24 +08:00 committed by GitHub
parent 43bcf40f80
commit b6e0abadab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 649 additions and 0 deletions

View File

@ -23,6 +23,7 @@ class IterationNodeData(BaseIterationNodeData):
is_parallel: bool = False # open the parallel mode or not is_parallel: bool = False # open the parallel mode or not
parallel_nums: int = 10 # the numbers of parallel parallel_nums: int = 10 # the numbers of parallel
error_handle_mode: ErrorHandleMode = ErrorHandleMode.TERMINATED # how to handle the error error_handle_mode: ErrorHandleMode = ErrorHandleMode.TERMINATED # how to handle the error
flatten_output: bool = True # whether to flatten the output array if all elements are lists
class IterationStartNodeData(BaseNodeData): class IterationStartNodeData(BaseNodeData):

View File

@ -98,6 +98,7 @@ class IterationNode(LLMUsageTrackingMixin, Node):
"is_parallel": False, "is_parallel": False,
"parallel_nums": 10, "parallel_nums": 10,
"error_handle_mode": ErrorHandleMode.TERMINATED, "error_handle_mode": ErrorHandleMode.TERMINATED,
"flatten_output": True,
}, },
} }
@ -411,7 +412,14 @@ class IterationNode(LLMUsageTrackingMixin, Node):
""" """
Flatten the outputs list if all elements are lists. Flatten the outputs list if all elements are lists.
This maintains backward compatibility with version 1.8.1 behavior. This maintains backward compatibility with version 1.8.1 behavior.
If flatten_output is False, returns outputs as-is (nested structure).
If flatten_output is True (default), flattens the list if all elements are lists.
""" """
# If flatten_output is disabled, return outputs as-is
if not self._node_data.flatten_output:
return outputs
if not outputs: if not outputs:
return outputs return outputs

View File

@ -0,0 +1,258 @@
app:
description: 'This workflow tests the iteration node with flatten_output=False.
It processes [1, 2, 3], outputs [item, item*2] for each iteration.
With flatten_output=False, it should output nested arrays:
```
{"output": [[1, 2], [2, 4], [3, 6]]}
```'
icon: 🤖
icon_background: '#FFEAD5'
mode: workflow
name: test_iteration_flatten_disabled
use_icon_as_answer_icon: false
dependencies: []
kind: app
version: 0.3.1
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
enabled: false
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
graph:
edges:
- data:
isInIteration: false
isInLoop: false
sourceType: start
targetType: code
id: start-source-code-target
source: start_node
sourceHandle: source
target: code_node
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: code
targetType: iteration
id: code-source-iteration-target
source: code_node
sourceHandle: source
target: iteration_node
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: true
isInLoop: false
iteration_id: iteration_node
sourceType: iteration-start
targetType: code
id: iteration-start-source-code-inner-target
source: iteration_nodestart
sourceHandle: source
target: code_inner_node
targetHandle: target
type: custom
zIndex: 1002
- data:
isInIteration: false
isInLoop: false
sourceType: iteration
targetType: end
id: iteration-source-end-target
source: iteration_node
sourceHandle: source
target: end_node
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: Start
type: start
variables: []
height: 54
id: start_node
position:
x: 80
y: 282
positionAbsolute:
x: 80
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
code: "\ndef main() -> dict:\n return {\n \"result\": [1, 2, 3],\n\
\ }\n"
code_language: python3
desc: ''
outputs:
result:
children: null
type: array[number]
selected: false
title: Generate Array
type: code
variables: []
height: 54
id: code_node
position:
x: 384
y: 282
positionAbsolute:
x: 384
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
error_handle_mode: terminated
flatten_output: false
height: 178
is_parallel: false
iterator_input_type: array[number]
iterator_selector:
- code_node
- result
output_selector:
- code_inner_node
- result
output_type: array[array[number]]
parallel_nums: 10
selected: false
start_node_id: iteration_nodestart
title: Iteration with Flatten Disabled
type: iteration
width: 388
height: 178
id: iteration_node
position:
x: 684
y: 282
positionAbsolute:
x: 684
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 388
zIndex: 1
- data:
desc: ''
isInIteration: true
selected: false
title: ''
type: iteration-start
draggable: false
height: 48
id: iteration_nodestart
parentId: iteration_node
position:
x: 24
y: 68
positionAbsolute:
x: 708
y: 350
selectable: false
sourcePosition: right
targetPosition: left
type: custom-iteration-start
width: 44
zIndex: 1002
- data:
code: "\ndef main(arg1: int) -> dict:\n return {\n \"result\": [arg1,\
\ arg1 * 2],\n }\n"
code_language: python3
desc: ''
isInIteration: true
isInLoop: false
iteration_id: iteration_node
outputs:
result:
children: null
type: array[number]
selected: false
title: Generate Pair
type: code
variables:
- value_selector:
- iteration_node
- item
value_type: number
variable: arg1
height: 54
id: code_inner_node
parentId: iteration_node
position:
x: 128
y: 68
positionAbsolute:
x: 812
y: 350
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
zIndex: 1002
- data:
desc: ''
outputs:
- value_selector:
- iteration_node
- output
value_type: array[array[number]]
variable: output
selected: false
title: End
type: end
height: 90
id: end_node
position:
x: 1132
y: 282
positionAbsolute:
x: 1132
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: -476
y: 3
zoom: 1

View File

@ -0,0 +1,258 @@
app:
description: 'This workflow tests the iteration node with flatten_output=True.
It processes [1, 2, 3], outputs [item, item*2] for each iteration.
With flatten_output=True (default), it should output:
```
{"output": [1, 2, 2, 4, 3, 6]}
```'
icon: 🤖
icon_background: '#FFEAD5'
mode: workflow
name: test_iteration_flatten_enabled
use_icon_as_answer_icon: false
dependencies: []
kind: app
version: 0.3.1
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
enabled: false
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
graph:
edges:
- data:
isInIteration: false
isInLoop: false
sourceType: start
targetType: code
id: start-source-code-target
source: start_node
sourceHandle: source
target: code_node
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: code
targetType: iteration
id: code-source-iteration-target
source: code_node
sourceHandle: source
target: iteration_node
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: true
isInLoop: false
iteration_id: iteration_node
sourceType: iteration-start
targetType: code
id: iteration-start-source-code-inner-target
source: iteration_nodestart
sourceHandle: source
target: code_inner_node
targetHandle: target
type: custom
zIndex: 1002
- data:
isInIteration: false
isInLoop: false
sourceType: iteration
targetType: end
id: iteration-source-end-target
source: iteration_node
sourceHandle: source
target: end_node
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: Start
type: start
variables: []
height: 54
id: start_node
position:
x: 80
y: 282
positionAbsolute:
x: 80
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
code: "\ndef main() -> dict:\n return {\n \"result\": [1, 2, 3],\n\
\ }\n"
code_language: python3
desc: ''
outputs:
result:
children: null
type: array[number]
selected: false
title: Generate Array
type: code
variables: []
height: 54
id: code_node
position:
x: 384
y: 282
positionAbsolute:
x: 384
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
error_handle_mode: terminated
flatten_output: true
height: 178
is_parallel: false
iterator_input_type: array[number]
iterator_selector:
- code_node
- result
output_selector:
- code_inner_node
- result
output_type: array[array[number]]
parallel_nums: 10
selected: false
start_node_id: iteration_nodestart
title: Iteration with Flatten Enabled
type: iteration
width: 388
height: 178
id: iteration_node
position:
x: 684
y: 282
positionAbsolute:
x: 684
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 388
zIndex: 1
- data:
desc: ''
isInIteration: true
selected: false
title: ''
type: iteration-start
draggable: false
height: 48
id: iteration_nodestart
parentId: iteration_node
position:
x: 24
y: 68
positionAbsolute:
x: 708
y: 350
selectable: false
sourcePosition: right
targetPosition: left
type: custom-iteration-start
width: 44
zIndex: 1002
- data:
code: "\ndef main(arg1: int) -> dict:\n return {\n \"result\": [arg1,\
\ arg1 * 2],\n }\n"
code_language: python3
desc: ''
isInIteration: true
isInLoop: false
iteration_id: iteration_node
outputs:
result:
children: null
type: array[number]
selected: false
title: Generate Pair
type: code
variables:
- value_selector:
- iteration_node
- item
value_type: number
variable: arg1
height: 54
id: code_inner_node
parentId: iteration_node
position:
x: 128
y: 68
positionAbsolute:
x: 812
y: 350
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
zIndex: 1002
- data:
desc: ''
outputs:
- value_selector:
- iteration_node
- output
value_type: array[number]
variable: output
selected: false
title: End
type: end
height: 90
id: end_node
position:
x: 1132
y: 282
positionAbsolute:
x: 1132
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: -476
y: 3
zoom: 1

View File

@ -0,0 +1,96 @@
"""
Test cases for the Iteration node's flatten_output functionality.
This module tests the iteration node's ability to:
1. Flatten array outputs when flatten_output=True (default)
2. Preserve nested array structure when flatten_output=False
"""
from .test_table_runner import TableTestRunner, WorkflowTestCase
def test_iteration_with_flatten_output_enabled():
"""
Test iteration node with flatten_output=True (default behavior).
The fixture implements an iteration that:
1. Iterates over [1, 2, 3]
2. For each item, outputs [item, item*2]
3. With flatten_output=True, should output [1, 2, 2, 4, 3, 6]
"""
runner = TableTestRunner()
test_case = WorkflowTestCase(
fixture_path="iteration_flatten_output_enabled_workflow",
inputs={},
expected_outputs={"output": [1, 2, 2, 4, 3, 6]},
description="Iteration with flatten_output=True flattens nested arrays",
use_auto_mock=False, # Run code nodes directly
)
result = runner.run_test_case(test_case)
assert result.success, f"Test failed: {result.error}"
assert result.actual_outputs is not None, "Should have outputs"
assert result.actual_outputs == {"output": [1, 2, 2, 4, 3, 6]}, (
f"Expected flattened output [1, 2, 2, 4, 3, 6], got {result.actual_outputs}"
)
def test_iteration_with_flatten_output_disabled():
"""
Test iteration node with flatten_output=False.
The fixture implements an iteration that:
1. Iterates over [1, 2, 3]
2. For each item, outputs [item, item*2]
3. With flatten_output=False, should output [[1, 2], [2, 4], [3, 6]]
"""
runner = TableTestRunner()
test_case = WorkflowTestCase(
fixture_path="iteration_flatten_output_disabled_workflow",
inputs={},
expected_outputs={"output": [[1, 2], [2, 4], [3, 6]]},
description="Iteration with flatten_output=False preserves nested structure",
use_auto_mock=False, # Run code nodes directly
)
result = runner.run_test_case(test_case)
assert result.success, f"Test failed: {result.error}"
assert result.actual_outputs is not None, "Should have outputs"
assert result.actual_outputs == {"output": [[1, 2], [2, 4], [3, 6]]}, (
f"Expected nested output [[1, 2], [2, 4], [3, 6]], got {result.actual_outputs}"
)
def test_iteration_flatten_output_comparison():
"""
Run both flatten_output configurations in parallel to verify the difference.
"""
runner = TableTestRunner()
test_cases = [
WorkflowTestCase(
fixture_path="iteration_flatten_output_enabled_workflow",
inputs={},
expected_outputs={"output": [1, 2, 2, 4, 3, 6]},
description="flatten_output=True: Flattened output",
use_auto_mock=False, # Run code nodes directly
),
WorkflowTestCase(
fixture_path="iteration_flatten_output_disabled_workflow",
inputs={},
expected_outputs={"output": [[1, 2], [2, 4], [3, 6]]},
description="flatten_output=False: Nested output",
use_auto_mock=False, # Run code nodes directly
),
]
suite_result = runner.run_table_tests(test_cases, parallel=True)
# Assert all tests passed
assert suite_result.passed_tests == 2, f"Expected 2 passed tests, got {suite_result.passed_tests}"
assert suite_result.failed_tests == 0, f"Expected 0 failed tests, got {suite_result.failed_tests}"
assert suite_result.success_rate == 100.0, f"Expected 100% success rate, got {suite_result.success_rate}"

View File

@ -22,6 +22,7 @@ const nodeDefault: NodeDefault<IterationNodeType> = {
is_parallel: false, is_parallel: false,
parallel_nums: 10, parallel_nums: 10,
error_handle_mode: ErrorHandleMode.Terminated, error_handle_mode: ErrorHandleMode.Terminated,
flatten_output: true,
}, },
checkValid(payload: IterationNodeType, t: any) { checkValid(payload: IterationNodeType, t: any) {
let errorMessages = '' let errorMessages = ''

View File

@ -46,6 +46,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
changeParallel, changeParallel,
changeErrorResponseMode, changeErrorResponseMode,
changeParallelNums, changeParallelNums,
changeFlattenOutput,
} = useConfig(id, data) } = useConfig(id, data)
return ( return (
@ -117,6 +118,18 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false} /> <Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false} />
</Field> </Field>
</div> </div>
<Split />
<div className='px-4 py-2'>
<Field
title={t(`${i18nPrefix}.flattenOutput`)}
tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.flattenOutputDesc`)}</div>}
inline
>
<Switch defaultValue={inputs.flatten_output} onChange={changeFlattenOutput} />
</Field>
</div>
</div> </div>
) )
} }

View File

@ -17,5 +17,6 @@ export type IterationNodeType = CommonNodeType & {
is_parallel: boolean // open the parallel mode or not is_parallel: boolean // open the parallel mode or not
parallel_nums: number // the numbers of parallel parallel_nums: number // the numbers of parallel
error_handle_mode: ErrorHandleMode // how to handle error in the iteration error_handle_mode: ErrorHandleMode // how to handle error in the iteration
flatten_output: boolean // whether to flatten the output array if all elements are lists
_isShowTips: boolean // when answer node in parallel mode iteration show tips _isShowTips: boolean // when answer node in parallel mode iteration show tips
} }

View File

@ -98,6 +98,14 @@ const useConfig = (id: string, payload: IterationNodeType) => {
}) })
setInputs(newInputs) setInputs(newInputs)
}, [inputs, setInputs]) }, [inputs, setInputs])
const changeFlattenOutput = useCallback((value: boolean) => {
const newInputs = produce(inputs, (draft) => {
draft.flatten_output = value
})
setInputs(newInputs)
}, [inputs, setInputs])
return { return {
readOnly, readOnly,
inputs, inputs,
@ -109,6 +117,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
changeParallel, changeParallel,
changeErrorResponseMode, changeErrorResponseMode,
changeParallelNums, changeParallelNums,
changeFlattenOutput,
} }
} }

View File

@ -788,6 +788,8 @@ const translation = {
removeAbnormalOutput: 'Remove Abnormal Output', removeAbnormalOutput: 'Remove Abnormal Output',
}, },
answerNodeWarningDesc: 'Parallel mode warning: Answer nodes, conversation variable assignments, and persistent read/write operations within iterations may cause exceptions.', answerNodeWarningDesc: 'Parallel mode warning: Answer nodes, conversation variable assignments, and persistent read/write operations within iterations may cause exceptions.',
flattenOutput: 'Flatten Output',
flattenOutputDesc: 'When enabled, if all iteration outputs are arrays, they will be flattened into a single array. When disabled, outputs will maintain a nested array structure.',
}, },
loop: { loop: {
deleteTitle: 'Delete Loop Node?', deleteTitle: 'Delete Loop Node?',

View File

@ -788,6 +788,8 @@ const translation = {
removeAbnormalOutput: '移除错误输出', removeAbnormalOutput: '移除错误输出',
}, },
answerNodeWarningDesc: '并行模式警告:在迭代中,回答节点、会话变量赋值和工具持久读/写操作可能会导致异常。', answerNodeWarningDesc: '并行模式警告:在迭代中,回答节点、会话变量赋值和工具持久读/写操作可能会导致异常。',
flattenOutput: '扁平化输出',
flattenOutputDesc: '启用时,如果所有迭代输出都是数组,它们将被扁平化为单个数组。禁用时,输出将保持嵌套数组结构。',
}, },
loop: { loop: {
deleteTitle: '删除循环节点?', deleteTitle: '删除循环节点?',