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]

Reply via email to