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]