This is an automated email from the ASF dual-hosted git repository. zqr10159 pushed a commit to branch 2.0.0 in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit cc46063beaea5625d500dde75968aeca936dccbe Author: Logic <[email protected]> AuthorDate: Wed Aug 26 14:55:27 2026 +0800 Preserve typed AI workflow arguments --- .../hertzbeat/ai/sop/executor/ToolExecutor.java | 36 ++++++++++--- .../ai/sop/executor/ToolExecutorTest.java | 63 ++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java index 4e7c0ce29f..903fc170fa 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java @@ -19,6 +19,8 @@ package org.apache.hertzbeat.ai.sop.executor; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; import org.apache.hertzbeat.ai.sop.model.SopStep; import org.apache.hertzbeat.ai.sop.registry.ToolRegistry; @@ -34,6 +36,9 @@ import org.springframework.stereotype.Component; @Component public class ToolExecutor implements SopExecutor { + private static final Pattern FULL_PLACEHOLDER = Pattern.compile("^\\$\\{([^{}]+)}$"); + private static final Pattern INLINE_PLACEHOLDER = Pattern.compile("\\$\\{([^{}]+)}"); + private final ToolRegistry toolRegistry; @Autowired @@ -73,18 +78,33 @@ public class ToolExecutor implements SopExecutor { for (Map.Entry<String, Object> entry : args.entrySet()) { Object value = entry.getValue(); if (value instanceof String) { - String strValue = (String) value; - for (Map.Entry<String, Object> ctxEntry : context.entrySet()) { - String placeholder = "${" + ctxEntry.getKey() + "}"; - if (strValue.contains(placeholder)) { - strValue = strValue.replace(placeholder, String.valueOf(ctxEntry.getValue())); - } - } - resolved.put(entry.getKey(), strValue); + resolved.put(entry.getKey(), resolveStringArg((String) value, context)); } else { resolved.put(entry.getKey(), value); } } return resolved; } + + private Object resolveStringArg(String value, Map<String, Object> context) { + Matcher fullPlaceholder = FULL_PLACEHOLDER.matcher(value); + if (fullPlaceholder.matches()) { + return context.get(fullPlaceholder.group(1)); + } + + Matcher inlinePlaceholder = INLINE_PLACEHOLDER.matcher(value); + if (!inlinePlaceholder.find()) { + return value; + } + + StringBuffer resolved = new StringBuffer(); + do { + String key = inlinePlaceholder.group(1); + Object replacement = context.get(key); + String replacementText = replacement == null ? "" : String.valueOf(replacement); + inlinePlaceholder.appendReplacement(resolved, Matcher.quoteReplacement(replacementText)); + } while (inlinePlaceholder.find()); + inlinePlaceholder.appendTail(resolved); + return resolved.toString(); + } } diff --git a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutorTest.java b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutorTest.java new file mode 100644 index 0000000000..5692612707 --- /dev/null +++ b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutorTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0. + */ + +package org.apache.hertzbeat.ai.sop.executor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; +import org.apache.hertzbeat.ai.sop.model.SopStep; +import org.apache.hertzbeat.ai.sop.registry.ToolRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ToolExecutorTest { + + @Mock + private ToolRegistry toolRegistry; + + @Test + void shouldPreservePlaceholderTypesAndResolveMissingOptionalValues() { + ToolExecutor executor = new ToolExecutor(toolRegistry); + SopStep step = SopStep.builder() + .id("typed_tool") + .type("tool") + .tool("typed_tool") + .args(Map.of( + "host", "${host}", + "port", "${port}", + "timeoutMs", "${timeoutMs}", + "command", "echo ${service}" + )) + .build(); + Map<String, Object> context = new HashMap<>(); + context.put("host", "127.0.0.1"); + context.put("port", 22); + context.put("service", "demo"); + when(toolRegistry.invoke(eq("typed_tool"), anyMap())).thenReturn("{}"); + + executor.execute(step, context); + + @SuppressWarnings("unchecked") + ArgumentCaptor<Map<String, Object>> argsCaptor = ArgumentCaptor.forClass(Map.class); + verify(toolRegistry).invoke(eq("typed_tool"), argsCaptor.capture()); + Map<String, Object> resolvedArgs = argsCaptor.getValue(); + assertThat(resolvedArgs.get("host")).isEqualTo("127.0.0.1"); + assertThat(resolvedArgs.get("port")).isEqualTo(22); + assertThat(resolvedArgs.get("timeoutMs")).isNull(); + assertThat(resolvedArgs.get("command")).isEqualTo("echo demo"); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
