Skip to content

Commit 16a960b

Browse files
authored
[app-builder] add new feature variable-updater-node (#452)
1 parent f59e398 commit 16a960b

File tree

10 files changed

+304
-0
lines changed

10 files changed

+304
-0
lines changed

app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
"name": "Variable Aggregation",
5555
"uniqueName": ""
5656
},
57+
{
58+
"type": "variableUpdaterNodeState",
59+
"name": "Variable Updater",
60+
"uniqueName": ""
61+
},
5762
{
5863
"type": "fileExtractionNodeState",
5964
"name": "File Extraction",

app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_zh.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
"name": "变量聚合",
5555
"uniqueName": ""
5656
},
57+
{
58+
"type": "variableUpdaterNodeState",
59+
"name": "变量更新",
60+
"uniqueName": ""
61+
},
5762
{
5863
"type": "fileExtractionNodeState",
5964
"name": "文件提取",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>modelengine.fit.jade</groupId>
8+
<artifactId>app-builder-plugin-parent</artifactId>
9+
<version>1.0.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<groupId>modelengine.fit.jade.plugin</groupId>
13+
<artifactId>aipp-variable-updater</artifactId>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>modelengine.fit.jade.waterflow</groupId>
18+
<artifactId>waterflow-runtime-service</artifactId>
19+
</dependency>
20+
<dependency>
21+
<groupId>modelengine.fit.jade</groupId>
22+
<artifactId>aipp-genericable</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>modelengine.fit.jober</groupId>
26+
<artifactId>jober-genericable</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>modelengine.fit.jade</groupId>
30+
<artifactId>aipp-service</artifactId>
31+
</dependency>
32+
33+
<!-- Test -->
34+
<dependency>
35+
<groupId>org.junit.jupiter</groupId>
36+
<artifactId>junit-jupiter</artifactId>
37+
</dependency>
38+
</dependencies>
39+
40+
<build>
41+
<plugins>
42+
<plugin>
43+
<groupId>org.fitframework</groupId>
44+
<artifactId>fit-build-maven-plugin</artifactId>
45+
<version>${fit.version}</version>
46+
<configuration>
47+
<category>user</category>
48+
<level>5</level>
49+
</configuration>
50+
<executions>
51+
<execution>
52+
<id>build-plugin</id>
53+
<goals>
54+
<goal>build-plugin</goal>
55+
</goals>
56+
</execution>
57+
<execution>
58+
<id>package-plugin</id>
59+
<goals>
60+
<goal>package-plugin</goal>
61+
</goals>
62+
</execution>
63+
</executions>
64+
</plugin>
65+
<plugin>
66+
<groupId>org.apache.maven.plugins</groupId>
67+
<artifactId>maven-antrun-plugin</artifactId>
68+
<version>${maven.antrun.version}</version>
69+
<executions>
70+
<execution>
71+
<phase>install</phase>
72+
<configuration>
73+
<target>
74+
<copy file="${project.build.directory}/${project.build.finalName}.jar"
75+
todir="../../../build/plugins"/>
76+
</target>
77+
</configuration>
78+
<goals>
79+
<goal>run</goal>
80+
</goals>
81+
</execution>
82+
</executions>
83+
</plugin>
84+
</plugins>
85+
</build>
86+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fit.jade.aipp.variable.updater;
8+
9+
import modelengine.fit.jober.aipp.common.exception.AippErrCode;
10+
import modelengine.fit.jober.aipp.common.exception.AippParamException;
11+
import modelengine.fit.jober.aipp.constants.AippConst;
12+
import modelengine.fit.jober.common.ErrorCodes;
13+
import modelengine.fit.jober.common.exceptions.JobberException;
14+
import modelengine.fit.waterflow.spi.FlowableService;
15+
import modelengine.fitframework.annotation.Component;
16+
import modelengine.fitframework.annotation.Fitable;
17+
import modelengine.fitframework.util.ObjectUtils;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
/**
23+
* 变量更新算子服务。
24+
*
25+
* @author 鲁为
26+
* @since 2025-09-26
27+
*/
28+
@Component
29+
public class AippVariableUpdater implements FlowableService {
30+
private static final String UPDATE_VARIABLES = "updateVariables";
31+
private static final String INTERNAL = "_internal";
32+
private static final String OUTPUT_SCOPE = "outputScope";
33+
private static final String KEY = "key";
34+
private static final String VALUE = "value";
35+
36+
@Override
37+
@Fitable("modelengine.fit.jade.aipp.variable.updater")
38+
public List<Map<String, Object>> handleTask(List<Map<String, Object>> flowData) {
39+
Map<String, Object> businessData = this.getBusinessData(flowData);
40+
List<Map<String, Object>> updateVariables = ObjectUtils.cast(businessData.get(UPDATE_VARIABLES));
41+
Map<String, Object> internal = ObjectUtils.cast(businessData.get(INTERNAL));
42+
Map<String, Object> outputScope = ObjectUtils.cast(internal.get(OUTPUT_SCOPE));
43+
for (Map<String, Object> variable : updateVariables) {
44+
List<String> path = ObjectUtils.cast(variable.get(KEY));
45+
Object newValue = variable.get(VALUE);
46+
this.updateNestedMapByPath(outputScope, path, newValue);
47+
}
48+
return flowData;
49+
}
50+
51+
private Map<String, Object> getBusinessData(List<Map<String, Object>> flowData) {
52+
if (flowData.isEmpty() || !flowData.get(0).containsKey(AippConst.BS_DATA_KEY)) {
53+
throw new JobberException(ErrorCodes.INPUT_PARAM_IS_EMPTY, AippConst.BS_DATA_KEY);
54+
}
55+
return ObjectUtils.cast(flowData.get(0).get(AippConst.BS_DATA_KEY));
56+
}
57+
58+
private void updateNestedMapByPath(Map<String, Object> businessData, List<String> path, Object newValue) {
59+
if (businessData == null || path == null || path.isEmpty()) {
60+
return;
61+
}
62+
63+
Map<String, Object> currentMap = businessData;
64+
65+
for (int i = 0; i < path.size() - 1; i++) {
66+
String key = path.get(i);
67+
Object value = currentMap.get(key);
68+
69+
if (value instanceof Map) {
70+
currentMap = ObjectUtils.cast(value);
71+
} else {
72+
throw new AippParamException(AippErrCode.DATA_TYPE_IS_NOT_SUPPORTED);
73+
}
74+
}
75+
76+
String finalKey = path.get(path.size() - 1);
77+
currentMap.put(finalKey, newValue);
78+
}
79+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fit:
2+
beans:
3+
packages:
4+
- 'modelengine.fit.jade.aipp.variable.updater'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fit.jade.aipp.variable.updater;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
12+
import modelengine.fit.jober.aipp.common.exception.AippParamException;
13+
import modelengine.fitframework.util.MapBuilder;
14+
import modelengine.fitframework.util.ObjectUtils;
15+
16+
import org.junit.jupiter.api.DisplayName;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
/**
24+
* {@link AippVariableUpdater}的测试方法。
25+
*
26+
* @author 鲁为
27+
* @since 2025-09-28
28+
*/
29+
@DisplayName("变量更新节点测试。")
30+
public class AippVariableUpdaterTest {
31+
private static final String UPDATE_VARIABLES = "updateVariables";
32+
private static final String INTERNAL = "_internal";
33+
private static final String OUTPUT_SCOPE = "outputScope";
34+
private static final String KEY = "key";
35+
private static final String VALUE = "value";
36+
37+
private final AippVariableUpdater aippVariableUpdater = new AippVariableUpdater();
38+
39+
@Test
40+
@DisplayName("修改 businessData 中指定路径的值。")
41+
void shouldModifySpecificPathValue() {
42+
Map<String, Object> flowDataInner =
43+
this.getFlowData(this.getContextData(List.of("trace1"), "contextId"), this.buildBusinessData());
44+
List<Map<String, Object>> flowData = List.of(flowDataInner);
45+
this.aippVariableUpdater.handleTask(flowData);
46+
assertEquals(ObjectUtils.<String>cast(ObjectUtils.<Map<String, Object>>cast(ObjectUtils.<Map<String, Object>>cast(
47+
ObjectUtils.<Map<String, Object>>cast(ObjectUtils.<Map<String, Object>>cast(ObjectUtils.<Map<String, Object>>cast(
48+
flowData.get(0).get("businessData")).get(INTERNAL)).get(OUTPUT_SCOPE)).get(
49+
"jade8xvdq4"))
50+
.get("output")).get("a")), "after");
51+
}
52+
53+
@Test
54+
@DisplayName("当 path 不存在时,抛出异常。")
55+
void shouldThrowExceptionWhenPathDoesNotExist() {
56+
Map<String, Object> flowDataInner =
57+
this.getFlowData(this.getContextData(List.of("trace1"), "contextId"), this.buildInvalidBusinessData());
58+
List<Map<String, Object>> flowData = List.of(flowDataInner);
59+
assertThrows(AippParamException.class, () -> this.aippVariableUpdater.handleTask(flowData));
60+
}
61+
62+
private Map<String, Object> getFlowData(Map<String, Object> contextData, Map<String, Object> businessData) {
63+
return MapBuilder.get(() -> new HashMap<String, Object>())
64+
.put("contextData", contextData)
65+
.put("businessData", businessData)
66+
.build();
67+
}
68+
69+
private Map<String, Object> getContextData(List<String> traceIds, String contextId) {
70+
return MapBuilder.get(() -> new HashMap<String, Object>())
71+
.put("flowTraceIds", traceIds)
72+
.put("contextId", contextId)
73+
.build();
74+
}
75+
76+
private Map<String, Object> buildBusinessData() {
77+
Map<Object, Object> output = MapBuilder.get().put("a", "before").build();
78+
Map<Object, Object> node = MapBuilder.get().put("output", output).build();
79+
Map<Object, Object> outputScope = MapBuilder.get().put("jade8xvdq4", node).build();
80+
return MapBuilder.<String, Object>get()
81+
.put(UPDATE_VARIABLES,
82+
List.of(MapBuilder.get()
83+
.put(KEY, List.of("jade8xvdq4", "output", "a"))
84+
.put(VALUE, "after")
85+
.build()))
86+
.put(INTERNAL, MapBuilder.get().put(OUTPUT_SCOPE, outputScope).build())
87+
.build();
88+
}
89+
90+
private Map<String, Object> buildInvalidBusinessData() {
91+
List<Object> output = List.of("a", "before");
92+
Map<Object, Object> node = MapBuilder.get().put("output", output).build();
93+
Map<Object, Object> outputScope = MapBuilder.get().put("jade8xvdq4", node).build();
94+
return MapBuilder.<String, Object>get()
95+
.put(UPDATE_VARIABLES,
96+
List.of(MapBuilder.get()
97+
.put(KEY, List.of("jade8xvdq4", "output", "a"))
98+
.put(VALUE, "after")
99+
.build()))
100+
.put(INTERNAL, MapBuilder.get().put(OUTPUT_SCOPE, outputScope).build())
101+
.build();
102+
}
103+
}

app-builder/plugins/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<module>aipp-prompt-builder</module>
3232
<module>aipp-rewriter</module>
3333
<module>aipp-variable-aggregation</module>
34+
<module>aipp-variable-updater</module>
3435
<module>aipp-websocket-plugin</module>
3536
<module>app-announcement</module>
3637
<module>app-base</module>

frontend/src/assets/icon.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const SendIcon = (props) => <Icon component={() => (<BaseIcons.Send />)} {...pro
6060
const ConfigurationIcon = (props) => <Icon component={() => (<BaseIcons.Configuration />)} {...props} />;
6161
const HttpIcon = (props) => <Icon component={() => (<BaseIcons.Http />)} {...props} />;
6262
const VariableAggregation = (props) => <Icon component={() => (<BaseIcons.VariableAggregation />)} {...props} />;
63+
const VariableUpdater = (props) => <Icon component={() => (<BaseIcons.VariableUpdater />)} {...props} />;
6364
const TextToImageIcon = (props) => <Icon component={() => (<BaseIcons.TextToImage />)} {...props} />;
6465
const FileExtractionIcon = (props) => <Icon component={() => (<BaseIcons.FileExtraction />)} {...props} />;
6566
const LoopIcon = (props) => <Icon component={() => (<BaseIcons.Loop />)} {...props} />;
@@ -118,6 +119,7 @@ export {
118119
ConfigurationIcon,
119120
HttpIcon,
120121
VariableAggregation,
122+
VariableUpdater,
121123
TextToImageIcon,
122124
FileExtractionIcon,
123125
LoopIcon,

frontend/src/components/icons/base.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,23 @@ export const BaseIcons = {
876876
</g>
877877
</svg>
878878
),
879+
VariableUpdater: () => (
880+
<svg width="28.000000" height="28.000000" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
881+
<desc>
882+
Created with Pixso.
883+
</desc>
884+
<defs>
885+
<clipPath id="clip10488_209423">
886+
<rect id="转移 (5)" rx="14.000000" width="28.000000" height="28.000000" fill="white" fill-opacity="0"/>
887+
</clipPath>
888+
</defs>
889+
<rect id="转移 (5)" rx="14.000000" width="28.000000" height="28.000000" fill="#39C295" fill-opacity="1.000000"/>
890+
<g clip-path="url(#clip10488_209423)">
891+
<path id="减去顶层" d="M19.7134 5L7.32031 5C6.57568 5 6 5.5874 6 6.34717L6 21.6528C6 22.3779 6.57568 23 7.32031 23L14.9995 23C13.7852 22.0879 13 20.6357 13 19C13 16.2388 15.2388 14 18 14C19.1255 14 20.1646 14.3721 21 14.9995L21 6.34717C21 5.5874 20.4243 5 19.7134 5ZM9.75 9.63086C9.8667 9.44922 9.92529 9.20068 9.92529 8.88574L9.92529 8.63867C9.92529 8.38379 9.96924 8.20654 10.0566 8.10742C10.1445 8.0083 10.2915 7.9585 10.4985 7.9585L10.6787 7.9585L10.6787 7L10.271 7C9.93018 7 9.65381 7.03711 9.44141 7.11084C9.33691 7.14697 9.24463 7.19775 9.16504 7.26367L9.16504 7.26367C9.0835 7.33105 9.01514 7.41406 8.95947 7.5127C8.84912 7.70654 8.79395 7.98096 8.79395 8.33496L8.79395 8.72266C8.79395 8.90381 8.77002 9.04492 8.72168 9.14502C8.70801 9.17334 8.69141 9.19922 8.67236 9.22363C8.62451 9.28369 8.56006 9.33105 8.479 9.36475C8.36621 9.41211 8.20703 9.44434 8 9.46143L8 10.5371C8.20215 10.5552 8.35986 10.5864 8.47217 10.6318C8.58545 10.6772 8.66748 10.7495 8.71826 10.8486C8.76855 10.9478 8.79395 11.0903 8.79395 11.2759L8.79395 11.665C8.79395 12.019 8.84912 12.2935 8.95947 12.4873C9.06934 12.6816 9.22998 12.8154 9.44141 12.8892C9.65381 12.9629 9.93018 13 10.271 13L10.6787 13L10.6787 12.04L10.4985 12.04C10.2915 12.04 10.1445 11.9902 10.0566 11.8911C9.96924 11.792 9.92529 11.6147 9.92529 11.3594L9.92529 11.1143C9.92529 10.7993 9.8667 10.5508 9.75 10.3691C9.73438 10.3452 9.71729 10.3218 9.69922 10.2998L9.69922 10.2998C9.58154 10.1558 9.41064 10.0557 9.18701 9.99902C9.44531 9.93506 9.6333 9.81201 9.75 9.63086ZM17.0728 8.63867C17.0728 8.38379 17.0293 8.20654 16.9414 8.10742C16.855 8.0083 16.7075 7.9585 16.5 7.9585L16.3213 7.9585L16.3213 7L16.7275 7C17.0679 7 17.3428 7.03711 17.5518 7.11084C17.7622 7.18359 17.9224 7.31738 18.0322 7.5127C18.1436 7.70654 18.1992 7.98096 18.1992 8.33496L18.1992 8.72266C18.1992 8.90381 18.2231 9.04492 18.2715 9.14502C18.3198 9.24414 18.4014 9.31738 18.5161 9.36475C18.6318 9.41211 18.793 9.44434 19 9.46143L19 10.5371C18.793 10.5552 18.6328 10.5864 18.5195 10.6318C18.4072 10.6772 18.3257 10.7505 18.2749 10.8521C18.2246 10.9531 18.1992 11.0942 18.1992 11.2759L18.1992 11.665C18.1992 12.019 18.1436 12.2935 18.0322 12.4873C17.9224 12.6816 17.7622 12.8154 17.5518 12.8892C17.3428 12.9629 17.0679 13 16.7275 13L16.3213 13L16.3213 12.04L16.5 12.04C16.7075 12.04 16.855 11.9902 16.9414 11.8911C17.0293 11.792 17.0728 11.6147 17.0728 11.3594L17.0728 11.1143C17.0728 10.7993 17.1313 10.5508 17.2485 10.3691C17.3662 10.188 17.5518 10.0645 17.8047 9.99902C17.5518 9.93506 17.3662 9.81201 17.2485 9.63086C17.1313 9.44922 17.0728 9.20068 17.0728 8.88574L17.0728 8.63867ZM14.1797 9.83789L15.9482 7.5376L14.5879 7.5376L13.5308 8.96289L12.4888 7.5376L11.0439 7.5376L12.8125 9.86328L10.9546 12.2803L12.3086 12.2803L13.4551 10.7378L14.5811 12.2803L16.0376 12.2803L14.1797 9.83789Z" clip-rule="evenodd" fill="#FFFFFF" fill-opacity="0.800000" fill-rule="evenodd"/>
892+
<path id="减去顶层" d="M18 15C15.791 15 14 16.791 14 19C14 21.209 15.791 23 18 23C20.209 23 22 21.209 22 19C22 16.791 20.209 15 18 15ZM18.707 19.7676C18.5088 19.9658 18.5088 20.2769 18.707 20.4746C18.9048 20.6729 19.2158 20.6729 19.4141 20.4746L20.4746 19.4141C20.5234 19.3652 20.5601 19.3115 20.5845 19.2524C20.6089 19.1934 20.6211 19.1294 20.6211 19.0605C20.6211 18.9224 20.5723 18.8047 20.4746 18.707L19.4141 17.6465C19.2158 17.4482 18.9048 17.4482 18.707 17.6465C18.5088 17.8442 18.5088 18.1553 18.707 18.3535L18.8535 18.5L16 18.5L16 18C16 17.7202 15.7798 17.5 15.5 17.5C15.2197 17.5 15 17.7202 15 18L15 19C15 19.0693 15.0122 19.1333 15.0366 19.1919C15.061 19.251 15.0977 19.3047 15.1465 19.3535C15.1953 19.4023 15.249 19.439 15.3081 19.4634C15.3667 19.4878 15.4312 19.5 15.5 19.5L18.9746 19.5L18.707 19.7676Z" clip-rule="evenodd" fill="#FFFFFF" fill-opacity="1.000000" fill-rule="evenodd"/>
893+
</g>
894+
</svg>
895+
),
879896
TextToImage: () => (
880897
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" fill="none">
881898
<defs>

frontend/src/pages/addFlow/components/basic-item.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
TextExtractionIcon,
2121
HttpIcon,
2222
VariableAggregation,
23+
VariableUpdater,
2324
TextToImageIcon,
2425
FileExtractionIcon,
2526
LoopIcon,
@@ -55,6 +56,7 @@ const BasicItems = (props: any) => {
5556
'questionClassificationNodeCondition': <ClassificationIcon />,
5657
'httpNodeState': <HttpIcon />,
5758
'variableAggregationNodeState': <VariableAggregation />,
59+
'variableUpdaterNodeState': <VariableUpdater />,
5860
'textToImageNodeState': <TextToImageIcon />,
5961
'fileExtractionNodeState': <FileExtractionIcon />,
6062
'noteNode': <ClassificationIcon />,

0 commit comments

Comments
 (0)