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 5ecc7b6b76 feat: add SkillTools for AI-driven diagnostic skill 
execution
5ecc7b6b76 is described below

commit 5ecc7b6b76fad36cb413ab69df3984030b90163b
Author: TJxiaobao <[email protected]>
AuthorDate: Thu Jan 29 22:25:40 2026 +0800

    feat: add SkillTools for AI-driven diagnostic skill execution
    
    - Add SkillTools interface with listSkills/executeSkill for AI discovery
    - Add [[SKILL_REPORT]] marker for direct report output to users
    - Update system-message.st with dynamic skill discovery workflow
    - Add front-end support for skill report rendering in chat
---
 .../ai/service/impl/McpServerServiceImpl.java      |  12 +-
 .../org/apache/hertzbeat/ai/tools/SkillTools.java  |  39 +++++
 .../hertzbeat/ai/tools/impl/DatabaseToolsImpl.java |  10 +-
 .../hertzbeat/ai/tools/impl/SkillToolsImpl.java    | 174 +++++++++++++++++++++
 .../src/main/resources/prompt/system-message.st    |  18 +++
 .../src/main/resources/skills/daily_inspection.yml |   4 +-
 .../skills/mysql_slow_query_diagnosis.yml          |   8 +-
 web-app/src/app/service/ai-chat.service.ts         |   4 +-
 .../shared/components/ai-chat/chat.component.ts    |  33 +++-
 9 files changed, 288 insertions(+), 14 deletions(-)

diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
index cdb2418397..02fc30c004 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
@@ -23,6 +23,7 @@ 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.tools.SkillTools;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.ai.tool.ToolCallbackProvider;
@@ -45,9 +46,18 @@ public class McpServerServiceImpl implements 
McpServerService {
     private MetricsTools metricsTools;
     @Autowired
     private AlertDefineTools alertDefineTools;
+    @Autowired
+    private SkillTools skillTools;
 
     @Bean
     public ToolCallbackProvider hertzbeatTools() {
-        return MethodToolCallbackProvider.builder().toolObjects(monitorTools, 
alertTools, alertDefineTools, metricsTools).build();
+        // Note: DatabaseTools is NOT exposed to AI directly.
+        // AI should use Skills (via skillTools) to perform database 
diagnostics.
+        return MethodToolCallbackProvider.builder()
+                .toolObjects(monitorTools, alertTools, alertDefineTools, 
metricsTools, 
+                             skillTools)
+                .build();
     }
 }
+
+
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/SkillTools.java 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/SkillTools.java
new file mode 100644
index 0000000000..8341619c21
--- /dev/null
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/SkillTools.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Interface for AI Skill execution tools.
+ * Provides methods to list available skills and execute them.
+ */
+public interface SkillTools {
+
+    /**
+     * List all available diagnostic skills.
+     * @return JSON string containing skill metadata (name, description, 
parameters)
+     */
+    String listSkills();
+
+    /**
+     * Execute a diagnostic skill.
+     * @param skillName Name of the skill to execute
+     * @param paramsJson JSON string containing skill parameters (e.g., 
{"monitorId": 123})
+     * @return Skill execution result. For report-type skills, returns 
"[[SKILL_REPORT]]\n{content}"
+     */
+    String executeSkill(String skillName, String paramsJson);
+}
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
index 87a1fd5da9..285731c0ab 100644
--- 
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
@@ -59,7 +59,7 @@ public class DatabaseToolsImpl implements DatabaseTools {
     }
 
     @Override
-    @Tool(name = "getMySqlSlowQueries", description = "Get MySQL slow query 
statistics from performance_schema. "
+    @Tool(name = "get_mysql_slow_queries", 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(
@@ -84,7 +84,7 @@ public class DatabaseToolsImpl implements DatabaseTools {
     }
 
     @Override
-    @Tool(name = "getMySqlProcessList", description = "Get MySQL current 
process list. "
+    @Tool(name = "get_mysql_process_list", 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(
@@ -101,7 +101,7 @@ public class DatabaseToolsImpl implements DatabaseTools {
     }
 
     @Override
-    @Tool(name = "getMySqlLockWaits", description = "Get MySQL lock wait 
information. "
+    @Tool(name = "get_mysql_lock_waits", description = "Get MySQL lock wait 
information. "
             + "Shows current lock waits and blocking transactions. "
             + "Useful for diagnosing deadlocks and lock contention issues.")
     public String getMySqlLockWaits(
@@ -126,7 +126,7 @@ public class DatabaseToolsImpl implements DatabaseTools {
     }
 
     @Override
-    @Tool(name = "getMySqlGlobalStatus", description = "Get MySQL global 
status variables. "
+    @Tool(name = "get_mysql_global_status", 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(
@@ -143,7 +143,7 @@ public class DatabaseToolsImpl implements DatabaseTools {
     }
 
     @Override
-    @Tool(name = "explainQuery", description = "Explain a SELECT query for 
performance analysis. "
+    @Tool(name = "explain_query", 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(
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/SkillToolsImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/SkillToolsImpl.java
new file mode 100644
index 0000000000..2e13dc699a
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/SkillToolsImpl.java
@@ -0,0 +1,174 @@
+/*
+ * 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.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.sop.engine.SopEngine;
+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.registry.SkillRegistry;
+import org.apache.hertzbeat.ai.tools.SkillTools;
+import org.apache.hertzbeat.common.util.JsonUtil;
+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.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+/**
+ * Implementation of SkillTools for AI-driven skill execution.
+ * Provides a unified interface for AI to discover and execute diagnostic 
skills.
+ */
+@Slf4j
+@Service
+public class SkillToolsImpl implements SkillTools {
+
+    /**
+     * Marker prefix for report-type outputs that should be displayed directly 
to users.
+     * When AI returns content with this prefix, frontend should render it 
directly
+     * without waiting for AI to process it further.
+     */
+    public static final String SKILL_REPORT_MARKER = "[[SKILL_REPORT]]";
+
+    private final SkillRegistry skillRegistry;
+    private final SopEngine sopEngine;
+
+    @Autowired
+    public SkillToolsImpl(@Lazy SkillRegistry skillRegistry, @Lazy SopEngine 
sopEngine) {
+        this.skillRegistry = skillRegistry;
+        this.sopEngine = sopEngine;
+    }
+
+    @Override
+    @Tool(name = "listSkills",
+          description = "List all available diagnostic skills. Returns skill 
names, descriptions, "
+                  + "and required parameters. Use this to discover what 
diagnostic capabilities are available "
+                  + "before executing a skill with executeSkill.")
+    public String listSkills() {
+        List<SopDefinition> skills = skillRegistry.getAllSkills();
+        
+        if (skills.isEmpty()) {
+            return "No diagnostic skills available.";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("Available Diagnostic Skills:\n\n");
+
+        for (SopDefinition skill : skills) {
+            sb.append("- **").append(skill.getName()).append("**\n");
+            sb.append("  Description: 
").append(skill.getDescription()).append("\n");
+            sb.append("  Version: ").append(skill.getVersion()).append("\n");
+            
+            // List required parameters
+            if (skill.getParameters() != null && 
!skill.getParameters().isEmpty()) {
+                sb.append("  Parameters:\n");
+                for (SopParameter param : skill.getParameters()) {
+                    sb.append("    - ").append(param.getName());
+                    if (param.isRequired()) {
+                        sb.append(" (required)");
+                    }
+                    sb.append(": 
").append(param.getDescription()).append("\n");
+                }
+            }
+            sb.append("\n");
+        }
+
+        sb.append("Usage: Call executeSkill with skillName and params (JSON 
format).\n");
+        sb.append("Example: executeSkill(\"mysql_slow_query_diagnosis\", 
\"{\\\"monitorId\\\": 123}\")");
+
+        return sb.toString();
+    }
+
+
+    @Override
+    @Tool(name = "executeSkill",
+          description = "Execute a diagnostic skill. For skills requiring 
monitorId, first use queryMonitors "
+                  + "to find the target monitor's ID. Report-type skills will 
return results directly to the user "
+                  + "without further AI processing. Use listSkills to see 
available skills.")
+    public String executeSkill(
+            @ToolParam(description = "Name of the skill to execute (e.g., 
mysql_slow_query_diagnosis)", 
+                       required = true) String skillName,
+            @ToolParam(description = "JSON string with skill parameters (e.g., 
{\"monitorId\": 123})", 
+                       required = false) String paramsJson) {
+
+        log.info("Executing skill: {} with params: {}", skillName, paramsJson);
+
+        // Get skill definition
+        SopDefinition skill = skillRegistry.getSkill(skillName);
+        if (skill == null) {
+            String available = skillRegistry.getAllSkills().stream()
+                    .map(SopDefinition::getName)
+                    .collect(Collectors.joining(", "));
+            return "Error: Skill '" + skillName + "' not found. Available 
skills: " + available;
+        }
+
+        // Parse parameters
+        Map<String, Object> params = parseParams(paramsJson);
+
+        // Validate required parameters
+        if (skill.getParameters() != null) {
+            for (SopParameter paramDef : skill.getParameters()) {
+                if (paramDef.isRequired()) {
+                    if (!params.containsKey(paramDef.getName()) || 
params.get(paramDef.getName()) == null) {
+                        return "Error: Required parameter '" + 
paramDef.getName() + "' is missing. "
+                                + "Description: " + paramDef.getDescription();
+                    }
+                }
+            }
+        }
+
+        try {
+            // Execute the skill
+            SopResult result = sopEngine.executeSync(skill, params);
+
+            // Check output type
+            if (result.getOutputType() == OutputType.REPORT) {
+                // Report type: return with marker for direct display to user
+                log.info("Skill {} returned report-type output, marking for 
direct display", skillName);
+                return SKILL_REPORT_MARKER + "\n" + result.getContent();
+            }
+
+            // For other types, return AI-friendly format for further 
processing
+            return result.toAiResponse();
+
+        } catch (Exception e) {
+            log.error("Failed to execute skill {}: {}", skillName, 
e.getMessage(), e);
+            return "Error executing skill '" + skillName + "': " + 
e.getMessage();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> parseParams(String paramsJson) {
+        if (paramsJson == null || paramsJson.trim().isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            return JsonUtil.fromJson(paramsJson, Map.class);
+        } catch (Exception e) {
+            log.warn("Failed to parse params JSON: {}, returning empty map", 
paramsJson);
+            return new HashMap<>();
+        }
+    }
+}
diff --git a/hertzbeat-ai/src/main/resources/prompt/system-message.st 
b/hertzbeat-ai/src/main/resources/prompt/system-message.st
index a371391ea5..772340c374 100644
--- a/hertzbeat-ai/src/main/resources/prompt/system-message.st
+++ b/hertzbeat-ai/src/main/resources/prompt/system-message.st
@@ -33,6 +33,24 @@ If the user doesn't provide required parameters, ask them 
iteratively to provide
 - **get_historical_metrics**: Get historical time-series metrics with flexible 
time ranges
 - **get_warehouse_status**: Check metrics storage system status
 
+### Diagnostic Skills (SOP):
+Skills are dynamically loaded workflows. ALWAYS call listSkills first to 
discover available skills.
+
+- **listSkills**: List all available diagnostic skills with descriptions and 
parameters.
+  IMPORTANT: Always call this first before executing any skill to get the 
current list.
+- **executeSkill**: Execute a skill by name. Parameters are passed as JSON.
+  - For skills requiring monitorId, first use query_monitors to find the 
target monitor's ID
+  - If multiple monitors match, ask the user which one to diagnose
+  - Report-type skills return results directly to the user (no need to 
summarize)
+
+Example workflow for diagnostics:
+1. User: "Help me check the system health" or "Diagnose MySQL slow queries"
+2. Call listSkills() to see available skills
+3. Match user intent to a skill (e.g., "daily_inspection" or 
"mysql_slow_query_diagnosis")
+4. If skill needs monitorId, call query_monitors to find it
+5. Call executeSkill(skillName, paramsJson)
+
+
 ## Natural Language Examples:
 
 ### Monitor Management:
diff --git a/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml 
b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
index 1a726dbf31..eba0795104 100644
--- a/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
+++ b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
@@ -19,7 +19,7 @@ parameters:
 steps:
   - id: get_monitor_summary
     type: tool
-    tool: queryMonitors
+    tool: query_monitors
     args:
       status: 9
       pageSize: 100
@@ -45,7 +45,7 @@ steps:
 
   - id: get_storage_status
     type: tool
-    tool: getWarehouseStatus
+    tool: get_warehouse_status
 
   - id: generate_report
     type: llm
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
index 90c9c6468b..c51c30818e 100644
--- a/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml
+++ b/hertzbeat-ai/src/main/resources/skills/mysql_slow_query_diagnosis.yml
@@ -24,7 +24,7 @@ steps:
   # Step 1: Get slow query statistics
   - id: get_slow_queries
     type: tool
-    tool: getMySqlSlowQueries
+    tool: get_mysql_slow_queries
     args:
       monitorId: "${monitorId}"
       limit: "${slowQueryLimit}"
@@ -32,21 +32,21 @@ steps:
   # Step 2: Get current process list
   - id: get_process_list
     type: tool
-    tool: getMySqlProcessList
+    tool: get_mysql_process_list
     args:
       monitorId: "${monitorId}"
 
   # Step 3: Get lock wait information
   - id: get_lock_waits
     type: tool
-    tool: getMySqlLockWaits
+    tool: get_mysql_lock_waits
     args:
       monitorId: "${monitorId}"
 
   # Step 4: Get global status for slow query metrics
   - id: get_slow_status
     type: tool
-    tool: getMySqlGlobalStatus
+    tool: get_mysql_global_status
     args:
       monitorId: "${monitorId}"
       pattern: "Slow%"
diff --git a/web-app/src/app/service/ai-chat.service.ts 
b/web-app/src/app/service/ai-chat.service.ts
index 37f308af8a..96e54c0ae9 100644
--- a/web-app/src/app/service/ai-chat.service.ts
+++ b/web-app/src/app/service/ai-chat.service.ts
@@ -28,6 +28,8 @@ export interface ChatMessage {
   content: string;
   role: 'user' | 'assistant';
   gmtCreate: Date;
+  /** Flag indicating this message is a skill report that should be displayed 
directly */
+  isSkillReport?: boolean;
 }
 
 export interface ChatConversation {
@@ -49,7 +51,7 @@ const chat_uri = '/chat';
   providedIn: 'root'
 })
 export class AiChatService {
-  constructor(private http: HttpClient, private localStorageService: 
LocalStorageService) {}
+  constructor(private http: HttpClient, private localStorageService: 
LocalStorageService) { }
 
   /**
    * Create a new conversation
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.ts 
b/web-app/src/app/shared/components/ai-chat/chat.component.ts
index 1c3ca19434..482cb13399 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.ts
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.ts
@@ -34,8 +34,15 @@ import { ThemeService } from 
'../../../service/theme.service';
   styleUrls: ['./chat.component.less']
 })
 export class ChatComponent implements OnInit, OnDestroy {
+  /**
+   * Marker for skill report content that should be displayed directly to users
+   * without further AI processing.
+   */
+  private readonly SKILL_REPORT_MARKER = '[[SKILL_REPORT]]';
+
   @ViewChild('messagesContainer') private messagesContainer!: ElementRef;
 
+
   conversations: ChatConversation[] = [];
   currentConversation: ChatConversation | null = null;
   messages: ChatMessage[] = [];
@@ -62,7 +69,7 @@ export class ChatComponent implements OnInit, OnDestroy {
     private cdr: ChangeDetectorRef,
     private themeSvc: ThemeService,
     private generalConfigSvc: GeneralConfigService
-  ) {}
+  ) { }
 
   ngOnInit(): void {
     this.theme = this.themeSvc.getTheme() || 'default';
@@ -312,6 +319,12 @@ export class ChatComponent implements OnInit, OnDestroy {
           lastMessage.content += chunk.content;
           lastMessage.gmtCreate = chunk.gmtCreate;
 
+          // Check for skill report marker and process it
+          if (lastMessage.content.includes(this.SKILL_REPORT_MARKER)) {
+            lastMessage.content = 
this.processSkillReportContent(lastMessage.content);
+            lastMessage.isSkillReport = true;
+          }
+
           this.cdr.detectChanges();
           this.scrollToBottom();
         }
@@ -431,6 +444,24 @@ export class ChatComponent implements OnInit, OnDestroy {
     }
   }
 
+  /**
+   * Process skill report content by removing the marker and extracting the 
report.
+   * Skill reports are directly displayed to users without further AI 
processing.
+   */
+  private processSkillReportContent(content: string): string {
+    // Remove the marker and return the report content
+    const markerIndex = content.indexOf(this.SKILL_REPORT_MARKER);
+    if (markerIndex !== -1) {
+      // Extract content after the marker
+      let reportContent = content.substring(markerIndex + 
this.SKILL_REPORT_MARKER.length);
+      // Remove leading newlines
+      reportContent = reportContent.replace(/^\n+/, '');
+      return reportContent;
+    }
+    return content;
+  }
+
+
   /**
    * Scroll to bottom of messages with smooth animation
    */


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

Reply via email to