diff --git a/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_en.json b/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_en.json index c68d0119d3..bf9e41ca3f 100644 --- a/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_en.json +++ b/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_en.json @@ -54,6 +54,11 @@ "name": "Variable Aggregation", "uniqueName": "" }, + { + "type": "variableUpdaterNodeState", + "name": "Variable Updater", + "uniqueName": "" + }, { "type": "fileExtractionNodeState", "name": "File Extraction", diff --git a/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_zh.json b/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_zh.json index 2f90ee566a..0543adeb45 100644 --- a/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_zh.json +++ b/app-builder/plugins/aipp-plugin/src/main/resources/component/basic_node_zh.json @@ -54,6 +54,11 @@ "name": "变量聚合", "uniqueName": "" }, + { + "type": "variableUpdaterNodeState", + "name": "变量更新", + "uniqueName": "" + }, { "type": "fileExtractionNodeState", "name": "文件提取", diff --git a/app-builder/plugins/aipp-variable-updater/pom.xml b/app-builder/plugins/aipp-variable-updater/pom.xml new file mode 100644 index 0000000000..50effe1bcc --- /dev/null +++ b/app-builder/plugins/aipp-variable-updater/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + + modelengine.fit.jade + app-builder-plugin-parent + 1.0.0-SNAPSHOT + + + modelengine.fit.jade.plugin + aipp-variable-updater + + + + modelengine.fit.jade.waterflow + waterflow-runtime-service + + + modelengine.fit.jade + aipp-genericable + + + modelengine.fit.jober + jober-genericable + + + modelengine.fit.jade + aipp-service + + + + + org.junit.jupiter + junit-jupiter + + + + + + + org.fitframework + fit-build-maven-plugin + ${fit.version} + + user + 5 + + + + build-plugin + + build-plugin + + + + package-plugin + + package-plugin + + + + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven.antrun.version} + + + install + + + + + + + run + + + + + + + \ No newline at end of file diff --git a/app-builder/plugins/aipp-variable-updater/src/main/java/modelengine/fit/jade/aipp/variable/updater/AippVariableUpdater.java b/app-builder/plugins/aipp-variable-updater/src/main/java/modelengine/fit/jade/aipp/variable/updater/AippVariableUpdater.java new file mode 100644 index 0000000000..b05006be1a --- /dev/null +++ b/app-builder/plugins/aipp-variable-updater/src/main/java/modelengine/fit/jade/aipp/variable/updater/AippVariableUpdater.java @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.jade.aipp.variable.updater; + +import modelengine.fit.jober.aipp.common.exception.AippErrCode; +import modelengine.fit.jober.aipp.common.exception.AippParamException; +import modelengine.fit.jober.aipp.constants.AippConst; +import modelengine.fit.jober.common.ErrorCodes; +import modelengine.fit.jober.common.exceptions.JobberException; +import modelengine.fit.waterflow.spi.FlowableService; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.annotation.Fitable; +import modelengine.fitframework.util.ObjectUtils; + +import java.util.List; +import java.util.Map; + +/** + * 变量更新算子服务。 + * + * @author 鲁为 + * @since 2025-09-26 + */ +@Component +public class AippVariableUpdater implements FlowableService { + private static final String UPDATE_VARIABLES = "updateVariables"; + private static final String INTERNAL = "_internal"; + private static final String OUTPUT_SCOPE = "outputScope"; + private static final String KEY = "key"; + private static final String VALUE = "value"; + + @Override + @Fitable("modelengine.fit.jade.aipp.variable.updater") + public List> handleTask(List> flowData) { + Map businessData = this.getBusinessData(flowData); + List> updateVariables = ObjectUtils.cast(businessData.get(UPDATE_VARIABLES)); + Map internal = ObjectUtils.cast(businessData.get(INTERNAL)); + Map outputScope = ObjectUtils.cast(internal.get(OUTPUT_SCOPE)); + for (Map variable : updateVariables) { + List path = ObjectUtils.cast(variable.get(KEY)); + Object newValue = variable.get(VALUE); + this.updateNestedMapByPath(outputScope, path, newValue); + } + return flowData; + } + + private Map getBusinessData(List> flowData) { + if (flowData.isEmpty() || !flowData.get(0).containsKey(AippConst.BS_DATA_KEY)) { + throw new JobberException(ErrorCodes.INPUT_PARAM_IS_EMPTY, AippConst.BS_DATA_KEY); + } + return ObjectUtils.cast(flowData.get(0).get(AippConst.BS_DATA_KEY)); + } + + private void updateNestedMapByPath(Map businessData, List path, Object newValue) { + if (businessData == null || path == null || path.isEmpty()) { + return; + } + + Map currentMap = businessData; + + for (int i = 0; i < path.size() - 1; i++) { + String key = path.get(i); + Object value = currentMap.get(key); + + if (value instanceof Map) { + currentMap = ObjectUtils.cast(value); + } else { + throw new AippParamException(AippErrCode.DATA_TYPE_IS_NOT_SUPPORTED); + } + } + + String finalKey = path.get(path.size() - 1); + currentMap.put(finalKey, newValue); + } +} \ No newline at end of file diff --git a/app-builder/plugins/aipp-variable-updater/src/main/resources/application.yml b/app-builder/plugins/aipp-variable-updater/src/main/resources/application.yml new file mode 100644 index 0000000000..c7c1a0daed --- /dev/null +++ b/app-builder/plugins/aipp-variable-updater/src/main/resources/application.yml @@ -0,0 +1,4 @@ +fit: + beans: + packages: + - 'modelengine.fit.jade.aipp.variable.updater' \ No newline at end of file diff --git a/app-builder/plugins/aipp-variable-updater/src/test/java/modelengine/fit/jade/aipp/variable/updater/AippVariableUpdaterTest.java b/app-builder/plugins/aipp-variable-updater/src/test/java/modelengine/fit/jade/aipp/variable/updater/AippVariableUpdaterTest.java new file mode 100644 index 0000000000..48a7223526 --- /dev/null +++ b/app-builder/plugins/aipp-variable-updater/src/test/java/modelengine/fit/jade/aipp/variable/updater/AippVariableUpdaterTest.java @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.jade.aipp.variable.updater; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import modelengine.fit.jober.aipp.common.exception.AippParamException; +import modelengine.fitframework.util.MapBuilder; +import modelengine.fitframework.util.ObjectUtils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link AippVariableUpdater}的测试方法。 + * + * @author 鲁为 + * @since 2025-09-28 + */ +@DisplayName("变量更新节点测试。") +public class AippVariableUpdaterTest { + private static final String UPDATE_VARIABLES = "updateVariables"; + private static final String INTERNAL = "_internal"; + private static final String OUTPUT_SCOPE = "outputScope"; + private static final String KEY = "key"; + private static final String VALUE = "value"; + + private final AippVariableUpdater aippVariableUpdater = new AippVariableUpdater(); + + @Test + @DisplayName("修改 businessData 中指定路径的值。") + void shouldModifySpecificPathValue() { + Map flowDataInner = + this.getFlowData(this.getContextData(List.of("trace1"), "contextId"), this.buildBusinessData()); + List> flowData = List.of(flowDataInner); + this.aippVariableUpdater.handleTask(flowData); + assertEquals(ObjectUtils.cast(ObjectUtils.>cast(ObjectUtils.>cast( + ObjectUtils.>cast(ObjectUtils.>cast(ObjectUtils.>cast( + flowData.get(0).get("businessData")).get(INTERNAL)).get(OUTPUT_SCOPE)).get( + "jade8xvdq4")) + .get("output")).get("a")), "after"); + } + + @Test + @DisplayName("当 path 不存在时,抛出异常。") + void shouldThrowExceptionWhenPathDoesNotExist() { + Map flowDataInner = + this.getFlowData(this.getContextData(List.of("trace1"), "contextId"), this.buildInvalidBusinessData()); + List> flowData = List.of(flowDataInner); + assertThrows(AippParamException.class, () -> this.aippVariableUpdater.handleTask(flowData)); + } + + private Map getFlowData(Map contextData, Map businessData) { + return MapBuilder.get(() -> new HashMap()) + .put("contextData", contextData) + .put("businessData", businessData) + .build(); + } + + private Map getContextData(List traceIds, String contextId) { + return MapBuilder.get(() -> new HashMap()) + .put("flowTraceIds", traceIds) + .put("contextId", contextId) + .build(); + } + + private Map buildBusinessData() { + Map output = MapBuilder.get().put("a", "before").build(); + Map node = MapBuilder.get().put("output", output).build(); + Map outputScope = MapBuilder.get().put("jade8xvdq4", node).build(); + return MapBuilder.get() + .put(UPDATE_VARIABLES, + List.of(MapBuilder.get() + .put(KEY, List.of("jade8xvdq4", "output", "a")) + .put(VALUE, "after") + .build())) + .put(INTERNAL, MapBuilder.get().put(OUTPUT_SCOPE, outputScope).build()) + .build(); + } + + private Map buildInvalidBusinessData() { + List output = List.of("a", "before"); + Map node = MapBuilder.get().put("output", output).build(); + Map outputScope = MapBuilder.get().put("jade8xvdq4", node).build(); + return MapBuilder.get() + .put(UPDATE_VARIABLES, + List.of(MapBuilder.get() + .put(KEY, List.of("jade8xvdq4", "output", "a")) + .put(VALUE, "after") + .build())) + .put(INTERNAL, MapBuilder.get().put(OUTPUT_SCOPE, outputScope).build()) + .build(); + } +} diff --git a/app-builder/plugins/pom.xml b/app-builder/plugins/pom.xml index 4a0ab34250..478b9c91b6 100644 --- a/app-builder/plugins/pom.xml +++ b/app-builder/plugins/pom.xml @@ -30,6 +30,7 @@ aipp-prompt-builder aipp-rewriter aipp-variable-aggregation + aipp-variable-updater aipp-websocket-plugin app-announcement app-base diff --git a/frontend/src/assets/icon.js b/frontend/src/assets/icon.js index a2ea532ea0..f98b6e8930 100644 --- a/frontend/src/assets/icon.js +++ b/frontend/src/assets/icon.js @@ -60,6 +60,7 @@ const SendIcon = (props) => ()} {...pro const ConfigurationIcon = (props) => ()} {...props} />; const HttpIcon = (props) => ()} {...props} />; const VariableAggregation = (props) => ()} {...props} />; +const VariableUpdater = (props) => ()} {...props} />; const TextToImageIcon = (props) => ()} {...props} />; const FileExtractionIcon = (props) => ()} {...props} />; const LoopIcon = (props) => ()} {...props} />; @@ -118,6 +119,7 @@ export { ConfigurationIcon, HttpIcon, VariableAggregation, + VariableUpdater, TextToImageIcon, FileExtractionIcon, LoopIcon, diff --git a/frontend/src/components/icons/base.tsx b/frontend/src/components/icons/base.tsx index b7da05fa1f..71f33a6caa 100644 --- a/frontend/src/components/icons/base.tsx +++ b/frontend/src/components/icons/base.tsx @@ -876,6 +876,23 @@ export const BaseIcons = { ), + VariableUpdater: () => ( + + + Created with Pixso. + + + + + + + + + + + + + ), TextToImage: () => ( diff --git a/frontend/src/pages/addFlow/components/basic-item.tsx b/frontend/src/pages/addFlow/components/basic-item.tsx index 14bfd651bd..dd0a58eee7 100644 --- a/frontend/src/pages/addFlow/components/basic-item.tsx +++ b/frontend/src/pages/addFlow/components/basic-item.tsx @@ -20,6 +20,7 @@ import { TextExtractionIcon, HttpIcon, VariableAggregation, + VariableUpdater, TextToImageIcon, FileExtractionIcon, LoopIcon, @@ -55,6 +56,7 @@ const BasicItems = (props: any) => { 'questionClassificationNodeCondition': , 'httpNodeState': , 'variableAggregationNodeState': , + 'variableUpdaterNodeState': , 'textToImageNodeState': , 'fileExtractionNodeState': , 'noteNode': ,