This is an automated email from the ASF dual-hosted git repository.

gaoxingcun pushed a commit to branch feature/ai-sop-workflow
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/feature/ai-sop-workflow by 
this push:
     new b14391c7aa feat: add MySQL slow query diagnosis SOP and refactor 
ToolExecutor
b14391c7aa is described below

commit b14391c7aaaebb89e637c06029c5f8a502a8d463
Author: TJxiaobao <[email protected]>
AuthorDate: Thu Jan 29 16:51:32 2026 +0800

    feat: add MySQL slow query diagnosis SOP and refactor ToolExecutor
    
    - Add mysql_slow_query_diagnosis.yml skill for MySQL diagnostics
    - Add DatabaseTools interface and implementation
    - Add ToolRegistry to auto-discover all @Tool annotated methods
---
 .../hertzbeat/ai/sop/engine/SopEngineImpl.java     |  10 +
 .../hertzbeat/ai/sop/executor/ToolExecutor.java    | 190 +------------
 .../hertzbeat/ai/sop/registry/ToolRegistry.java    | 282 +++++++++++++++++++
 .../apache/hertzbeat/ai/tools/DatabaseTools.java   |  50 ++++
 .../hertzbeat/ai/tools/impl/DatabaseToolsImpl.java | 300 +++++++++++++++++++++
 hertzbeat-ai/src/main/resources/skills/README.md   |  19 +-
 .../src/main/resources/skills/README_ZH.md         |  15 ++
 .../skills/mysql_slow_query_diagnosis.yml          |  95 +++++++
 8 files changed, 780 insertions(+), 181 deletions(-)

diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java
index d307fb5d2a..44ac80da12 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java
@@ -26,6 +26,7 @@ import org.apache.hertzbeat.ai.sop.executor.SopExecutor;
 import org.apache.hertzbeat.ai.sop.model.OutputConfig;
 import org.apache.hertzbeat.ai.sop.model.OutputType;
 import org.apache.hertzbeat.ai.sop.model.SopDefinition;
+import org.apache.hertzbeat.ai.sop.model.SopParameter;
 import org.apache.hertzbeat.ai.sop.model.SopResult;
 import org.apache.hertzbeat.ai.sop.model.SopStep;
 import org.apache.hertzbeat.ai.sop.model.StepResult;
@@ -119,6 +120,15 @@ public class SopEngineImpl implements SopEngine {
         List<StepResult> stepResults = new ArrayList<>();
         Map<String, Object> context = new HashMap<>(inputParams);
         
+        // Apply default values for parameters that are not provided
+        if (definition.getParameters() != null) {
+            for (SopParameter param : definition.getParameters()) {
+                if (!context.containsKey(param.getName()) && 
param.getDefaultValue() != null) {
+                    context.put(param.getName(), param.getDefaultValue());
+                }
+            }
+        }
+        
         // Get output configuration first
         OutputConfig outputConfig = definition.getOutput();
         if (outputConfig == null) {
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 e1aba900fa..4e7c0ce29f 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
@@ -17,40 +17,28 @@
 
 package org.apache.hertzbeat.ai.sop.executor;
 
+import java.util.HashMap;
+import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.ai.sop.model.SopStep;
-import org.apache.hertzbeat.ai.tools.AlertDefineTools;
-import org.apache.hertzbeat.ai.tools.AlertTools;
-import org.apache.hertzbeat.ai.tools.MetricsTools;
-import org.apache.hertzbeat.ai.tools.MonitorTools;
+import org.apache.hertzbeat.ai.sop.registry.ToolRegistry;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 /**
  * Executor for 'tool' type steps.
- * Calls existing MCP tools registered in the system.
+ * Uses ToolRegistry to dynamically discover and invoke @Tool annotated 
methods.
  */
 @Slf4j
 @Component
 public class ToolExecutor implements SopExecutor {
 
-    private final MonitorTools monitorTools;
-    private final AlertTools alertTools;
-    private final AlertDefineTools alertDefineTools;
-    private final MetricsTools metricsTools;
+    private final ToolRegistry toolRegistry;
 
     @Autowired
-    public ToolExecutor(MonitorTools monitorTools, AlertTools alertTools,
-                        AlertDefineTools alertDefineTools, MetricsTools 
metricsTools) {
-        this.monitorTools = monitorTools;
-        this.alertTools = alertTools;
-        this.alertDefineTools = alertDefineTools;
-        this.metricsTools = metricsTools;
+    public ToolExecutor(@Lazy ToolRegistry toolRegistry) {
+        this.toolRegistry = toolRegistry;
     }
 
     @Override
@@ -66,8 +54,9 @@ public class ToolExecutor implements SopExecutor {
         Map<String, Object> args = resolveArgs(step.getArgs(), context);
 
         try {
-            String result = invokeTool(toolName, args);
-            log.debug("Tool {} returned result length: {}", toolName, 
result.length());
+            String result = toolRegistry.invoke(toolName, args);
+            log.debug("Tool {} returned result length: {}", toolName, 
+                    result != null ? result.length() : 0);
             return result;
         } catch (Exception e) {
             log.error("Failed to execute tool {}: {}", toolName, 
e.getMessage());
@@ -75,92 +64,6 @@ public class ToolExecutor implements SopExecutor {
         }
     }
 
-    private String invokeTool(String toolName, Map<String, Object> args) {
-        switch (toolName) {
-            // MonitorTools
-            case "queryMonitors":
-                return monitorTools.queryMonitors(
-                        getListArg(args, "ids"),
-                        getStringArg(args, "app"),
-                        getByteArg(args, "status"),
-                        getStringArg(args, "search"),
-                        getStringArg(args, "labels"),
-                        getStringArg(args, "sort"),
-                        getStringArg(args, "order"),
-                        getIntArg(args, "pageIndex"),
-                        getIntArg(args, "pageSize"),
-                        getBoolArg(args, "includeStats")
-                );
-            case "listMonitorTypes":
-                return monitorTools.listMonitorTypes(getStringArg(args, 
"language"));
-            case "getMonitorParams":
-                return monitorTools.getMonitorParams(getStringArg(args, 
"app"));
-            case "addMonitor":
-                return monitorTools.addMonitor(
-                        getStringArg(args, "name"),
-                        getStringArg(args, "app"),
-                        getIntArg(args, "intervals"),
-                        getStringArg(args, "params"),
-                        getStringArg(args, "description")
-                );
-
-            // AlertTools
-            case "queryAlerts":
-                return alertTools.queryAlerts(
-                        getStringArg(args, "alertType"),
-                        getStringArg(args, "status"),
-                        getStringArg(args, "search"),
-                        getStringArg(args, "sort"),
-                        getStringArg(args, "order"),
-                        getIntArg(args, "pageIndex"),
-                        getIntArg(args, "pageSize")
-                );
-            case "getAlertsSummary":
-                return alertTools.getAlertsSummary();
-
-            // MetricsTools
-            case "getRealtimeMetrics":
-                return metricsTools.getRealtimeMetrics(
-                        getLongArg(args, "monitorId"),
-                        getStringArg(args, "metrics")
-                );
-            case "getHistoricalMetrics":
-                return metricsTools.getHistoricalMetrics(
-                        getStringArg(args, "instance"),
-                        getStringArg(args, "app"),
-                        getStringArg(args, "metrics"),
-                        getStringArg(args, "metric"),
-                        getStringArg(args, "label"),
-                        getStringArg(args, "history"),
-                        getBoolArg(args, "interval")
-                );
-            case "getWarehouseStatus":
-                return metricsTools.getWarehouseStatus();
-
-            // AlertDefineTools
-            case "listAlertRules":
-                return alertDefineTools.listAlertRules(
-                        getStringArg(args, "search"),
-                        getStringArg(args, "monitorType"),
-                        getBoolArg(args, "enabled"),
-                        getIntArg(args, "pageIndex"),
-                        getIntArg(args, "pageSize")
-                );
-            case "getAlertRuleDetails":
-                return alertDefineTools.getAlertRuleDetails(getLongArg(args, 
"ruleId"));
-            case "toggleAlertRule":
-                return alertDefineTools.toggleAlertRule(
-                        getLongArg(args, "ruleId"),
-                        getBoolArg(args, "enabled")
-                );
-            case "getAppsMetricsHierarchy":
-                return 
alertDefineTools.getAppsMetricsHierarchy(getStringArg(args, "app"));
-
-            default:
-                throw new IllegalArgumentException("Unknown tool: " + 
toolName);
-        }
-    }
-
     private Map<String, Object> resolveArgs(Map<String, Object> args, 
Map<String, Object> context) {
         if (args == null) {
             return new HashMap<>();
@@ -184,75 +87,4 @@ public class ToolExecutor implements SopExecutor {
         }
         return resolved;
     }
-
-    // Helper methods for argument extraction
-    private String getStringArg(Map<String, Object> args, String key) {
-        Object value = args.get(key);
-        return value != null ? String.valueOf(value) : null;
-    }
-
-    private Integer getIntArg(Map<String, Object> args, String key) {
-        Object value = args.get(key);
-        if (value == null) {
-            return null;
-        }
-        if (value instanceof Number) {
-            return ((Number) value).intValue();
-        }
-        return Integer.valueOf(String.valueOf(value));
-    }
-
-    private Long getLongArg(Map<String, Object> args, String key) {
-        Object value = args.get(key);
-        if (value == null) {
-            return null;
-        }
-        if (value instanceof Number) {
-            return ((Number) value).longValue();
-        }
-        return Long.valueOf(String.valueOf(value));
-    }
-
-    private Byte getByteArg(Map<String, Object> args, String key) {
-        Object value = args.get(key);
-        if (value == null) {
-            return null;
-        }
-        if (value instanceof Number) {
-            return ((Number) value).byteValue();
-        }
-        return Byte.valueOf(String.valueOf(value));
-    }
-
-    private Boolean getBoolArg(Map<String, Object> args, String key) {
-        Object value = args.get(key);
-        if (value == null) {
-            return null;
-        }
-        if (value instanceof Boolean) {
-            return (Boolean) value;
-        }
-        return Boolean.valueOf(String.valueOf(value));
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<Long> getListArg(Map<String, Object> args, String key) {
-        Object value = args.get(key);
-        if (value == null) {
-            return null;
-        }
-        if (value instanceof List) {
-            return (List<Long>) value;
-        }
-        // Parse comma-separated string
-        String str = String.valueOf(value);
-        if (str.isEmpty()) {
-            return new ArrayList<>();
-        }
-        List<Long> result = new ArrayList<>();
-        for (String s : str.split(",")) {
-            result.add(Long.valueOf(s.trim()));
-        }
-        return result;
-    }
 }
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/ToolRegistry.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/ToolRegistry.java
new file mode 100644
index 0000000000..77f3a68bfa
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/ToolRegistry.java
@@ -0,0 +1,282 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.sop.registry;
+
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+/**
+ * Registry for all @Tool annotated methods.
+ * Automatically discovers and registers tool methods on first use.
+ */
+@Slf4j
+@Component
+public class ToolRegistry {
+
+    private final ApplicationContext applicationContext;
+    private final Map<String, ToolMethod> tools = new HashMap<>();
+    private volatile boolean initialized = false;
+
+    @Autowired
+    public ToolRegistry(ApplicationContext applicationContext) {
+        this.applicationContext = applicationContext;
+    }
+
+    /**
+     * Initialize the registry by scanning all beans for @Tool methods.
+     * Uses double-checked locking for thread safety.
+     */
+    private void ensureInitialized() {
+        if (!initialized) {
+            synchronized (this) {
+                if (!initialized) {
+                    log.info("Scanning for @Tool annotated methods...");
+                    scanAllBeans();
+                    initialized = true;
+                    log.info("Registered {} tool methods: {}", tools.size(), 
tools.keySet());
+                }
+            }
+        }
+    }
+
+    private void scanAllBeans() {
+        String[] beanNames = applicationContext.getBeanDefinitionNames();
+        for (String beanName : beanNames) {
+            try {
+                Object bean = applicationContext.getBean(beanName);
+                scanBeanForTools(bean);
+            } catch (Exception e) {
+                // Skip beans that cannot be instantiated
+                log.trace("Skipping bean {}: {}", beanName, e.getMessage());
+            }
+        }
+    }
+
+    private void scanBeanForTools(Object bean) {
+        Class<?> clazz = bean.getClass();
+        
+        // Handle Spring proxies
+        if (clazz.getName().contains("$$")) {
+            clazz = clazz.getSuperclass();
+        }
+        
+        for (Method method : clazz.getMethods()) {
+            Tool toolAnnotation = method.getAnnotation(Tool.class);
+            if (toolAnnotation != null) {
+                String toolName = toolAnnotation.name();
+                if (toolName.isEmpty()) {
+                    toolName = method.getName();
+                }
+                
+                ToolMethod toolMethod = new ToolMethod(bean, method, 
toolAnnotation);
+                tools.put(toolName, toolMethod);
+                log.debug("Registered tool: {} -> {}.{}", 
+                        toolName, clazz.getSimpleName(), method.getName());
+            }
+        }
+    }
+
+    /**
+     * Invoke a tool by name with the given arguments.
+     */
+    public String invoke(String toolName, Map<String, Object> args) {
+        ensureInitialized();
+        ToolMethod toolMethod = tools.get(toolName);
+        if (toolMethod == null) {
+            throw new IllegalArgumentException("Unknown tool: " + toolName 
+                    + ". Available tools: " + tools.keySet());
+        }
+        return toolMethod.invoke(args);
+    }
+
+    /**
+     * Check if a tool exists.
+     */
+    public boolean hasMethod(String toolName) {
+        ensureInitialized();
+        return tools.containsKey(toolName);
+    }
+
+    /**
+     * Get all registered tool names.
+     */
+    public Set<String> getToolNames() {
+        ensureInitialized();
+        return tools.keySet();
+    }
+
+    /**
+     * Get tool method info.
+     */
+    public ToolMethod getToolMethod(String toolName) {
+        ensureInitialized();
+        return tools.get(toolName);
+    }
+
+    /**
+     * Represents a registered tool method.
+     */
+    public static class ToolMethod {
+        private final Object bean;
+        private final Method method;
+        private final Tool annotation;
+        private final List<ParamInfo> paramInfos;
+
+        public ToolMethod(Object bean, Method method, Tool annotation) {
+            this.bean = bean;
+            this.method = method;
+            this.annotation = annotation;
+            this.paramInfos = extractParamInfos(method);
+        }
+
+        private List<ParamInfo> extractParamInfos(Method method) {
+            List<ParamInfo> infos = new ArrayList<>();
+            Parameter[] parameters = method.getParameters();
+            
+            for (Parameter param : parameters) {
+                ToolParam toolParam = param.getAnnotation(ToolParam.class);
+                String name = (toolParam != null && 
!toolParam.description().isEmpty()) 
+                        ? param.getName() : param.getName();
+                boolean required = toolParam != null && toolParam.required();
+                
+                infos.add(new ParamInfo(name, param.getType(), required));
+            }
+            
+            return infos;
+        }
+
+        /**
+         * Invoke this tool method with the given arguments.
+         */
+        public String invoke(Map<String, Object> args) {
+            try {
+                Object[] methodArgs = new Object[paramInfos.size()];
+                
+                for (int i = 0; i < paramInfos.size(); i++) {
+                    ParamInfo paramInfo = paramInfos.get(i);
+                    Object value = args.get(paramInfo.name);
+                    methodArgs[i] = convertValue(value, paramInfo.type);
+                }
+                
+                Object result = method.invoke(bean, methodArgs);
+                return result != null ? result.toString() : "";
+                
+            } catch (Exception e) {
+                log.error("Failed to invoke tool {}: {}", annotation.name(), 
e.getMessage(), e);
+                throw new RuntimeException("Tool invocation failed: " + 
annotation.name(), e);
+            }
+        }
+
+        private Object convertValue(Object value, Class<?> targetType) {
+            if (value == null) {
+                return null;
+            }
+            
+            if (targetType.isAssignableFrom(value.getClass())) {
+                return value;
+            }
+            
+            String strValue = String.valueOf(value);
+            
+            if (targetType == String.class) {
+                return strValue;
+            } else if (targetType == Integer.class || targetType == int.class) 
{
+                return strValue.isEmpty() ? null : Integer.valueOf(strValue);
+            } else if (targetType == Long.class || targetType == long.class) {
+                return strValue.isEmpty() ? null : Long.valueOf(strValue);
+            } else if (targetType == Boolean.class || targetType == 
boolean.class) {
+                return Boolean.valueOf(strValue);
+            } else if (targetType == Byte.class || targetType == byte.class) {
+                return strValue.isEmpty() ? null : Byte.valueOf(strValue);
+            } else if (targetType == Double.class || targetType == 
double.class) {
+                return strValue.isEmpty() ? null : Double.valueOf(strValue);
+            } else if (targetType == Float.class || targetType == float.class) 
{
+                return strValue.isEmpty() ? null : Float.valueOf(strValue);
+            } else if (targetType == List.class) {
+                return parseList(strValue);
+            }
+            
+            return value;
+        }
+
+        @SuppressWarnings("unchecked")
+        private List<Long> parseList(String value) {
+            if (value == null || value.isEmpty()) {
+                return new ArrayList<>();
+            }
+            List<Long> result = new ArrayList<>();
+            for (String s : value.split(",")) {
+                if (!s.trim().isEmpty()) {
+                    result.add(Long.valueOf(s.trim()));
+                }
+            }
+            return result;
+        }
+
+        public String getName() {
+            return annotation.name();
+        }
+
+        public String getDescription() {
+            return annotation.description();
+        }
+
+        public List<ParamInfo> getParamInfos() {
+            return paramInfos;
+        }
+    }
+
+    /**
+     * Parameter information for a tool method.
+     */
+    public static class ParamInfo {
+        private final String name;
+        private final Class<?> type;
+        private final boolean required;
+
+        public ParamInfo(String name, Class<?> type, boolean required) {
+            this.name = name;
+            this.type = type;
+            this.required = required;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public Class<?> getType() {
+            return type;
+        }
+
+        public boolean isRequired() {
+            return required;
+        }
+    }
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/DatabaseTools.java 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/DatabaseTools.java
new file mode 100644
index 0000000000..272a4f3677
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/DatabaseTools.java
@@ -0,0 +1,50 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.tools;
+
+/**
+ * Database Tools interface for AI-powered database diagnostics.
+ * Provides predefined safe queries for database health checks and 
troubleshooting.
+ */
+public interface DatabaseTools {
+    
+    /**
+     * Get MySQL slow query statistics from performance_schema.
+     */
+    String getMySqlSlowQueries(Long monitorId, Integer limit);
+    
+    /**
+     * Get MySQL current process list.
+     */
+    String getMySqlProcessList(Long monitorId);
+    
+    /**
+     * Get MySQL lock wait information.
+     */
+    String getMySqlLockWaits(Long monitorId);
+    
+    /**
+     * Get MySQL global status variables.
+     */
+    String getMySqlGlobalStatus(Long monitorId, String pattern);
+    
+    /**
+     * Explain a SELECT query for performance analysis.
+     */
+    String explainQuery(Long monitorId, String query);
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/DatabaseToolsImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/DatabaseToolsImpl.java
new file mode 100644
index 0000000000..87a1fd5da9
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/DatabaseToolsImpl.java
@@ -0,0 +1,300 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.tools.impl;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Objects;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.tools.DatabaseTools;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.Param;
+import org.apache.hertzbeat.common.util.AesUtil;
+import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
+import org.apache.hertzbeat.manager.service.MonitorService;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * Implementation of Database Tools for AI-powered database diagnostics.
+ * Provides predefined safe queries - does NOT allow arbitrary SQL execution.
+ */
+@Slf4j
+@Service
+public class DatabaseToolsImpl implements DatabaseTools {
+
+    private static final int QUERY_TIMEOUT_SECONDS = 10;
+    private static final int MAX_ROWS = 100;
+    
+    // Allowed database types
+    private static final List<String> SUPPORTED_PLATFORMS = List.of("mysql", 
"mariadb");
+
+    private final MonitorService monitorService;
+
+    @Autowired
+    public DatabaseToolsImpl(MonitorService monitorService) {
+        this.monitorService = monitorService;
+    }
+
+    @Override
+    @Tool(name = "getMySqlSlowQueries", description = "Get MySQL slow query 
statistics from performance_schema. "
+            + "Returns top N slow queries sorted by average execution time. "
+            + "Requires the monitor to be a MySQL type with proper 
credentials.")
+    public String getMySqlSlowQueries(
+            @ToolParam(description = "Monitor ID of the MySQL instance", 
required = true) Long monitorId,
+            @ToolParam(description = "Maximum number of slow queries to return 
(default: 10, max: 50)", required = false) Integer limit) {
+        
+        int queryLimit = (limit == null || limit <= 0) ? 10 : Math.min(limit, 
50);
+        
+        String sql = "SELECT SCHEMA_NAME as db, "
+                + "DIGEST_TEXT as query, "
+                + "COUNT_STAR as exec_count, "
+                + "ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_time_ms, "
+                + "ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_ms, "
+                + "SUM_ROWS_EXAMINED as rows_examined, "
+                + "SUM_ROWS_SENT as rows_sent "
+                + "FROM performance_schema.events_statements_summary_by_digest 
"
+                + "WHERE SCHEMA_NAME IS NOT NULL "
+                + "ORDER BY AVG_TIMER_WAIT DESC "
+                + "LIMIT " + queryLimit;
+        
+        return executeQuery(monitorId, sql, "MySQL Slow Query Statistics");
+    }
+
+    @Override
+    @Tool(name = "getMySqlProcessList", description = "Get MySQL current 
process list. "
+            + "Shows all active connections and their current state. "
+            + "Useful for identifying blocking queries or connection issues.")
+    public String getMySqlProcessList(
+            @ToolParam(description = "Monitor ID of the MySQL instance", 
required = true) Long monitorId) {
+        
+        String sql = "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, "
+                + "LEFT(INFO, 200) as QUERY "
+                + "FROM information_schema.PROCESSLIST "
+                + "WHERE COMMAND != 'Sleep' "
+                + "ORDER BY TIME DESC "
+                + "LIMIT 50";
+        
+        return executeQuery(monitorId, sql, "MySQL Process List");
+    }
+
+    @Override
+    @Tool(name = "getMySqlLockWaits", description = "Get MySQL lock wait 
information. "
+            + "Shows current lock waits and blocking transactions. "
+            + "Useful for diagnosing deadlocks and lock contention issues.")
+    public String getMySqlLockWaits(
+            @ToolParam(description = "Monitor ID of the MySQL instance", 
required = true) Long monitorId) {
+        
+        // Use performance_schema for MySQL 8.0+ (innodb_lock_waits was 
removed)
+        String sql = "SELECT "
+                + "r.ENGINE_TRANSACTION_ID as waiting_trx_id, "
+                + "r.THREAD_ID as waiting_thread, "
+                + "r.OBJECT_SCHEMA as waiting_schema, "
+                + "r.OBJECT_NAME as waiting_table, "
+                + "r.LOCK_TYPE as waiting_lock_type, "
+                + "r.LOCK_MODE as waiting_lock_mode, "
+                + "b.ENGINE_TRANSACTION_ID as blocking_trx_id, "
+                + "b.THREAD_ID as blocking_thread "
+                + "FROM performance_schema.data_lock_waits w "
+                + "JOIN performance_schema.data_locks b ON b.ENGINE_LOCK_ID = 
w.BLOCKING_ENGINE_LOCK_ID "
+                + "JOIN performance_schema.data_locks r ON r.ENGINE_LOCK_ID = 
w.REQUESTING_ENGINE_LOCK_ID "
+                + "LIMIT 20";
+        
+        return executeQuery(monitorId, sql, "MySQL Lock Waits");
+    }
+
+    @Override
+    @Tool(name = "getMySqlGlobalStatus", description = "Get MySQL global 
status variables. "
+            + "Returns server status matching the given pattern. "
+            + "Examples: 'Slow%' for slow query stats, 'Threads%' for thread 
stats.")
+    public String getMySqlGlobalStatus(
+            @ToolParam(description = "Monitor ID of the MySQL instance", 
required = true) Long monitorId,
+            @ToolParam(description = "Pattern to filter status variables 
(e.g., 'Slow%', 'Threads%')", required = false) String pattern) {
+        
+        String filterPattern = (pattern == null || pattern.isEmpty()) ? "%" : 
pattern;
+        // Sanitize pattern to prevent SQL injection
+        filterPattern = filterPattern.replaceAll("[^a-zA-Z0-9_%]", "");
+        
+        String sql = "SHOW GLOBAL STATUS LIKE '" + filterPattern + "'";
+        
+        return executeQuery(monitorId, sql, "MySQL Global Status");
+    }
+
+    @Override
+    @Tool(name = "explainQuery", description = "Explain a SELECT query for 
performance analysis. "
+            + "ONLY SELECT queries are allowed for security reasons. "
+            + "Returns the execution plan to help optimize the query.")
+    public String explainQuery(
+            @ToolParam(description = "Monitor ID of the MySQL instance", 
required = true) Long monitorId,
+            @ToolParam(description = "SELECT query to explain (must start with 
SELECT)", required = true) String query) {
+        
+        if (query == null || query.trim().isEmpty()) {
+            return "Error: Query cannot be empty";
+        }
+        
+        String trimmedQuery = query.trim();
+        
+        // Security check: only allow SELECT statements
+        if (!trimmedQuery.toUpperCase().startsWith("SELECT")) {
+            return "Error: Only SELECT queries are allowed for EXPLAIN. "
+                    + "The query must start with 'SELECT'.";
+        }
+        
+        // Additional security checks
+        String lowerQuery = trimmedQuery.toLowerCase();
+        if (lowerQuery.contains(";") || lowerQuery.contains("--") 
+                || lowerQuery.contains("/*") || lowerQuery.contains("*/")) {
+            return "Error: Query contains invalid characters. "
+                    + "Semicolons and comments are not allowed.";
+        }
+        
+        String sql = "EXPLAIN " + trimmedQuery;
+        
+        return executeQuery(monitorId, sql, "Query Execution Plan");
+    }
+
+    /**
+     * Execute a predefined safe query against the database.
+     */
+    private String executeQuery(Long monitorId, String sql, String title) {
+        log.info("Executing database query for monitor {}: {}", monitorId, 
title);
+        
+        try {
+            // Get monitor DTO which includes both monitor and params
+            MonitorDto monitorDto = monitorService.getMonitorDto(monitorId);
+            if (monitorDto == null || monitorDto.getMonitor() == null) {
+                return "Error: Monitor not found with ID: " + monitorId;
+            }
+            
+            Monitor monitor = monitorDto.getMonitor();
+            
+            if (!SUPPORTED_PLATFORMS.contains(monitor.getApp().toLowerCase())) 
{
+                return "Error: This tool only supports MySQL/MariaDB. "
+                        + "Current monitor type: " + monitor.getApp();
+            }
+            
+            // Get connection parameters from MonitorDto
+            List<Param> params = monitorDto.getParams();
+            if (params == null || params.isEmpty()) {
+                return "Error: Monitor has no connection parameters 
configured.";
+            }
+            
+            String host = getParamValue(params, "host");
+            String port = getParamValue(params, "port");
+            String database = getParamValue(params, "database");
+            String username = getParamValue(params, "username");
+            String password = getParamValue(params, "password");
+            
+            // Decrypt password if it's encrypted
+            if (password != null && AesUtil.isCiphertext(password)) {
+                password = AesUtil.aesDecode(password);
+            }
+            
+            if (host == null || username == null) {
+                return "Error: Monitor connection parameters incomplete. "
+                        + "Ensure host and username are configured.";
+            }
+            
+            // Build connection URL
+            String url = buildJdbcUrl(monitor.getApp(), host, port, database);
+            
+            // Execute query
+            return executeAndFormat(url, username, password, sql, title);
+            
+        } catch (Exception e) {
+            log.error("Error executing database query: {}", e.getMessage(), e);
+            return "Error executing query: " + e.getMessage();
+        }
+    }
+    
+    private String getParamValue(List<Param> params, String field) {
+        return params.stream()
+                .filter(p -> field.equals(p.getField()))
+                .map(Param::getParamValue)
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElse(null);
+    }
+    
+    private String buildJdbcUrl(String platform, String host, String port, 
String database) {
+        String effectivePort = (port == null || port.isEmpty()) ? "3306" : 
port;
+        String effectiveDb = (database == null || database.isEmpty()) ? "" : 
database;
+        
+        return "jdbc:mysql://" + host + ":" + effectivePort + "/" + effectiveDb
+                + "?useUnicode=true&characterEncoding=utf-8&useSSL=false"
+                + "&allowPublicKeyRetrieval=true&connectTimeout=5000";
+    }
+    
+    private String executeAndFormat(String url, String username, String 
password, 
+            String sql, String title) throws Exception {
+        
+        StringBuilder result = new StringBuilder();
+        result.append("=== ").append(title).append(" ===\n\n");
+        
+        try (Connection conn = DriverManager.getConnection(url, username, 
password);
+             Statement stmt = conn.createStatement()) {
+            
+            stmt.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
+            stmt.setMaxRows(MAX_ROWS);
+            
+            try (ResultSet rs = stmt.executeQuery(sql)) {
+                ResultSetMetaData meta = rs.getMetaData();
+                int columnCount = meta.getColumnCount();
+                
+                // Build header
+                StringBuilder header = new StringBuilder();
+                StringBuilder separator = new StringBuilder();
+                for (int i = 1; i <= columnCount; i++) {
+                    String columnName = meta.getColumnLabel(i);
+                    header.append(String.format("%-20s", columnName));
+                    separator.append("-".repeat(20));
+                }
+                result.append(header).append("\n");
+                result.append(separator).append("\n");
+                
+                // Build rows
+                int rowCount = 0;
+                while (rs.next()) {
+                    StringBuilder row = new StringBuilder();
+                    for (int i = 1; i <= columnCount; i++) {
+                        String value = rs.getString(i);
+                        if (value == null) {
+                            value = "NULL";
+                        } else if (value.length() > 50) {
+                            value = value.substring(0, 47) + "...";
+                        }
+                        row.append(String.format("%-20s", value));
+                    }
+                    result.append(row).append("\n");
+                    rowCount++;
+                }
+                
+                result.append("\n").append("Total: 
").append(rowCount).append(" rows");
+            }
+        }
+        
+        return result.toString();
+    }
+}
diff --git a/hertzbeat-ai/src/main/resources/skills/README.md 
b/hertzbeat-ai/src/main/resources/skills/README.md
index a699073dd9..46616c7323 100644
--- a/hertzbeat-ai/src/main/resources/skills/README.md
+++ b/hertzbeat-ai/src/main/resources/skills/README.md
@@ -65,6 +65,21 @@ POST /api/ai/sop/execute/{skillName}/ai
 
 ```
 YAML Definition → SkillRegistry → SopEngine → Executors → SopResult
-                                     ↓
-                              ToolExecutor / LlmExecutor
+                                      ↓
+                               ToolExecutor / LlmExecutor
+                                      ↓
+                               ToolRegistry (auto-discover @Tool methods)
 ```
+
+## Adding New Tools
+
+Just add `@Tool` annotation to your method - no other code changes needed:
+
+```java
+@Tool(name = "myNewTool", description = "...")
+public String myNewTool(@ToolParam(...) Long param) {
+    // implementation
+}
+```
+
+ToolRegistry auto-discovers all `@Tool` methods at runtime.
diff --git a/hertzbeat-ai/src/main/resources/skills/README_ZH.md 
b/hertzbeat-ai/src/main/resources/skills/README_ZH.md
index aa5116e2ea..dfad433185 100644
--- a/hertzbeat-ai/src/main/resources/skills/README_ZH.md
+++ b/hertzbeat-ai/src/main/resources/skills/README_ZH.md
@@ -67,8 +67,23 @@ POST /api/ai/sop/execute/{skillName}/ai
 YAML 定义 → SkillRegistry → SopEngine → Executors → SopResult
                                 ↓
                          ToolExecutor / LlmExecutor
+                                ↓
+                         ToolRegistry(自动发现 @Tool 方法)
+```
+
+## 添加新工具
+
+只需添加 `@Tool` 注解,无需修改其他代码:
+
+```java
+@Tool(name = "myNewTool", description = "...")
+public String myNewTool(@ToolParam(...) Long param) {
+    // 实现逻辑
+}
 ```
 
+ToolRegistry 会在运行时自动发现所有 `@Tool` 方法。
+
 ## 配置说明
 
 ### output 配置
diff --git 
a/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml 
b/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml
new file mode 100644
index 0000000000..90c9c6468b
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml
@@ -0,0 +1,95 @@
+name: mysql_slow_query_diagnosis
+description: "Diagnose MySQL slow queries and provide optimization 
recommendations"
+version: "1.0"
+
+# Output configuration
+output:
+  type: report
+  format: markdown
+  language: zh
+  contentStep: generate_diagnosis
+
+parameters:
+  - name: monitorId
+    type: long
+    description: "MySQL monitor ID to diagnose"
+    required: true
+  - name: slowQueryLimit
+    type: integer
+    description: "Number of slow queries to analyze (default: 10)"
+    required: false
+    defaultValue: "10"
+
+steps:
+  # Step 1: Get slow query statistics
+  - id: get_slow_queries
+    type: tool
+    tool: getMySqlSlowQueries
+    args:
+      monitorId: "${monitorId}"
+      limit: "${slowQueryLimit}"
+
+  # Step 2: Get current process list
+  - id: get_process_list
+    type: tool
+    tool: getMySqlProcessList
+    args:
+      monitorId: "${monitorId}"
+
+  # Step 3: Get lock wait information
+  - id: get_lock_waits
+    type: tool
+    tool: getMySqlLockWaits
+    args:
+      monitorId: "${monitorId}"
+
+  # Step 4: Get global status for slow query metrics
+  - id: get_slow_status
+    type: tool
+    tool: getMySqlGlobalStatus
+    args:
+      monitorId: "${monitorId}"
+      pattern: "Slow%"
+
+  # Step 5: LLM analyze and generate diagnosis report
+  - id: generate_diagnosis
+    type: llm
+    prompt: |
+      You are a senior MySQL DBA expert. Based on the following diagnostic 
data, generate a comprehensive slow query diagnosis report.
+      
+      ## Slow Query Statistics (Top N by average execution time)
+      ${get_slow_queries}
+      
+      ## Current Process List (Active queries)
+      ${get_process_list}
+      
+      ## Lock Wait Information
+      ${get_lock_waits}
+      
+      ## Server Slow Query Status
+      ${get_slow_status}
+      
+      Please provide a diagnosis report with the following sections:
+      
+      1. **Problem Summary**
+         - Overall slow query situation
+         - Most critical issues found
+      
+      2. **Slow Query Analysis**
+         - Top slow queries and their characteristics
+         - Common patterns (full table scans, missing indexes, etc.)
+      
+      3. **Lock & Concurrency Issues**
+         - Any lock contention found
+         - Long-running transactions
+      
+      4. **Optimization Recommendations**
+         - Index recommendations
+         - Query optimization suggestions
+         - Configuration tuning tips
+      
+      5. **Urgent Actions**
+         - Queries that need immediate attention
+         - Recommended immediate steps
+      
+      Format the report in markdown. Be specific with SQL examples where 
applicable.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to