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 e0f92ccda1 feat: add AI SOP engine for workflow automation
e0f92ccda1 is described below
commit e0f92ccda1ff5151984c5ffecbc14855029292eb
Author: TJxiaobao <[email protected]>
AuthorDate: Wed Jan 28 23:55:48 2026 +0800
feat: add AI SOP engine for workflow automation
- Add YAML-based SOP definition and execution engine
- Support tool/llm step types for workflow orchestration
- Implement unified SopResult with i18n support (zh/en)
- Add sync/stream/ai-friendly API endpoints
- Include daily_inspection skill as example
---
hertzbeat-ai/pom.xml | 4 +
.../apache/hertzbeat/ai/config/SopI18nConfig.java | 39 +++
.../hertzbeat/ai/controller/SopController.java | 180 +++++++++++++
.../impl/ChatClientProviderServiceImpl.java | 2 +
.../apache/hertzbeat/ai/sop/engine/SopEngine.java | 46 ++++
.../hertzbeat/ai/sop/engine/SopEngineImpl.java | 283 +++++++++++++++++++++
.../hertzbeat/ai/sop/executor/LlmExecutor.java | 110 ++++++++
.../hertzbeat/ai/sop/executor/SopExecutor.java | 43 ++++
.../hertzbeat/ai/sop/executor/ToolExecutor.java | 258 +++++++++++++++++++
.../hertzbeat/ai/sop/model/OutputConfig.java | 91 +++++++
.../apache/hertzbeat/ai/sop/model/OutputType.java | 44 ++++
.../hertzbeat/ai/sop/model/SopDefinition.java | 65 +++++
.../hertzbeat/ai/sop/model/SopParameter.java | 58 +++++
.../apache/hertzbeat/ai/sop/model/SopResult.java | 197 ++++++++++++++
.../org/apache/hertzbeat/ai/sop/model/SopStep.java | 65 +++++
.../apache/hertzbeat/ai/sop/model/StepResult.java | 73 ++++++
.../hertzbeat/ai/sop/registry/SkillRegistry.java | 103 ++++++++
.../hertzbeat/ai/sop/registry/SopToolCallback.java | 100 ++++++++
.../hertzbeat/ai/sop/registry/SopYamlLoader.java | 75 ++++++
.../hertzbeat/ai/sop/util/SopMessageUtil.java | 88 +++++++
.../hertzbeat/ai/tools/impl/MonitorToolsImpl.java | 8 +-
.../src/main/resources/i18n/messages.properties | 16 ++
.../src/main/resources/i18n/messages_en.properties | 16 ++
.../src/main/resources/i18n/messages_zh.properties | 16 ++
hertzbeat-ai/src/main/resources/skills/README.md | 70 +++++
.../src/main/resources/skills/README_ZH.md | 81 ++++++
.../src/main/resources/skills/daily_inspection.yml | 66 +++++
.../manager/service/impl/MonitorServiceImpl.java | 5 +-
28 files changed, 2197 insertions(+), 5 deletions(-)
diff --git a/hertzbeat-ai/pom.xml b/hertzbeat-ai/pom.xml
index d026a98b3b..302df8f20f 100644
--- a/hertzbeat-ai/pom.xml
+++ b/hertzbeat-ai/pom.xml
@@ -78,6 +78,10 @@
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.dataformat</groupId>
+ <artifactId>jackson-dataformat-yaml</artifactId>
+ </dependency>
</dependencies>
<dependencyManagement>
<dependencies>
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/SopI18nConfig.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/SopI18nConfig.java
new file mode 100644
index 0000000000..dc75aeafb1
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/SopI18nConfig.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.config;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.ResourceBundleMessageSource;
+
+/**
+ * Configuration for SOP internationalization.
+ */
+@Configuration
+public class SopI18nConfig {
+
+ @Bean("sopMessageSource")
+ public MessageSource sopMessageSource() {
+ ResourceBundleMessageSource messageSource = new
ResourceBundleMessageSource();
+ messageSource.setBasename("i18n/messages");
+ messageSource.setDefaultEncoding("UTF-8");
+ messageSource.setUseCodeAsDefaultMessage(true);
+ return messageSource;
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopController.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopController.java
new file mode 100644
index 0000000000..d2b0f31029
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopController.java
@@ -0,0 +1,180 @@
+/*
+ * 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.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+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.SopDefinition;
+import org.apache.hertzbeat.ai.sop.model.SopResult;
+import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+/**
+ * Controller for AI SOP (Standard Operating Procedure) execution.
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/ai/sop")
+@Tag(name = "AI SOP", description = "AI Standard Operating Procedure
Management")
+public class SopController {
+
+ private final SkillRegistry skillRegistry;
+ private final SopEngine sopEngine;
+
+ @Autowired
+ public SopController(SkillRegistry skillRegistry, SopEngine sopEngine) {
+ this.skillRegistry = skillRegistry;
+ this.sopEngine = sopEngine;
+ }
+
+ /**
+ * List all available SOP skills.
+ */
+ @GetMapping("/skills")
+ @Operation(summary = "List available SOP skills",
+ description = "Get all registered SOP skill definitions")
+ public ResponseEntity<List<Map<String, String>>> listSkills() {
+ List<Map<String, String>> skills =
skillRegistry.getAllSkills().stream()
+ .map(skill -> {
+ Map<String, String> info = new HashMap<>();
+ info.put("name", skill.getName());
+ info.put("description", skill.getDescription());
+ info.put("version", skill.getVersion());
+ return info;
+ })
+ .collect(Collectors.toList());
+ return ResponseEntity.ok(skills);
+ }
+
+ /**
+ * Get details of a specific SOP skill.
+ */
+ @GetMapping("/skills/{skillName}")
+ @Operation(summary = "Get SOP skill details",
+ description = "Get detailed definition of a specific SOP skill")
+ public ResponseEntity<SopDefinition> getSkillDetails(
+ @Parameter(description = "Name of the SOP skill")
+ @PathVariable String skillName) {
+ SopDefinition skill = skillRegistry.getSkill(skillName);
+ if (skill == null) {
+ return ResponseEntity.notFound().build();
+ }
+ return ResponseEntity.ok(skill);
+ }
+
+ /**
+ * Execute a SOP skill with streaming output (SSE mode).
+ * Use this for real-time progress updates in UI.
+ */
+ @PostMapping(value = "/execute/{skillName}", produces =
MediaType.TEXT_EVENT_STREAM_VALUE)
+ @Operation(summary = "Execute SOP skill (streaming)",
+ description = "Execute a SOP skill with streaming output for
real-time progress")
+ public Flux<String> executeSopStream(
+ @Parameter(description = "Name of the SOP skill to execute")
+ @PathVariable String skillName,
+ @Parameter(description = "Input parameters for the SOP")
+ @RequestBody(required = false) Map<String, Object> params) {
+
+ log.info("Executing SOP skill (stream): {} with params: {}",
skillName, params);
+
+ SopDefinition skill = skillRegistry.getSkill(skillName);
+ if (skill == null) {
+ return Flux.just("Error: SOP skill not found: " + skillName);
+ }
+
+ Map<String, Object> inputParams = params != null ? params : new
HashMap<>();
+
+ return sopEngine.execute(skill, inputParams)
+ .doOnNext(msg -> log.debug("SOP output: {}", msg))
+ .doOnError(e -> log.error("SOP execution error: {}",
e.getMessage()))
+ .doOnComplete(() -> log.info("SOP {} execution completed",
skillName));
+ }
+
+ /**
+ * Execute a SOP skill synchronously and return unified result.
+ * Use this for AI tool calls and programmatic access.
+ */
+ @PostMapping(value = "/execute/{skillName}/sync", produces =
MediaType.APPLICATION_JSON_VALUE)
+ @Operation(summary = "Execute SOP skill (sync)",
+ description = "Execute a SOP skill synchronously and return
unified result")
+ public ResponseEntity<SopResult> executeSopSync(
+ @Parameter(description = "Name of the SOP skill to execute")
+ @PathVariable String skillName,
+ @Parameter(description = "Input parameters for the SOP")
+ @RequestBody(required = false) Map<String, Object> params) {
+
+ log.info("Executing SOP skill (sync): {} with params: {}", skillName,
params);
+
+ SopDefinition skill = skillRegistry.getSkill(skillName);
+ if (skill == null) {
+ SopResult errorResult = SopResult.builder()
+ .sopName(skillName)
+ .status("FAILED")
+ .error("SOP skill not found: " + skillName)
+ .build();
+ return ResponseEntity.notFound().build();
+ }
+
+ Map<String, Object> inputParams = params != null ? params : new
HashMap<>();
+
+ SopResult result = sopEngine.executeSync(skill, inputParams);
+ return ResponseEntity.ok(result);
+ }
+
+ /**
+ * Execute a SOP skill and return AI-friendly text response.
+ * Use this when SOP is called as a tool by AI.
+ */
+ @PostMapping(value = "/execute/{skillName}/ai", produces =
MediaType.TEXT_PLAIN_VALUE)
+ @Operation(summary = "Execute SOP skill (AI format)",
+ description = "Execute a SOP and return AI-friendly text
response")
+ public ResponseEntity<String> executeSopForAi(
+ @Parameter(description = "Name of the SOP skill to execute")
+ @PathVariable String skillName,
+ @Parameter(description = "Input parameters for the SOP")
+ @RequestBody(required = false) Map<String, Object> params) {
+
+ log.info("Executing SOP skill (AI): {} with params: {}", skillName,
params);
+
+ SopDefinition skill = skillRegistry.getSkill(skillName);
+ if (skill == null) {
+ return ResponseEntity.ok("Error: SOP skill not found: " +
skillName);
+ }
+
+ Map<String, Object> inputParams = params != null ? params : new
HashMap<>();
+
+ SopResult result = sopEngine.executeSync(skill, inputParams);
+ return ResponseEntity.ok(result.toAiResponse());
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
index b073e0f12e..f624d70943 100644
---
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
@@ -36,6 +36,7 @@ import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
@@ -56,6 +57,7 @@ public class ChatClientProviderServiceImpl implements
ChatClientProviderService
private final GeneralConfigDao generalConfigDao;
@Autowired
+ @Qualifier("hertzbeatTools")
private ToolCallbackProvider toolCallbackProvider;
private boolean isConfigured = false;
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngine.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngine.java
new file mode 100644
index 0000000000..4fccb2a283
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngine.java
@@ -0,0 +1,46 @@
+/*
+ * 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.engine;
+
+import java.util.Map;
+import org.apache.hertzbeat.ai.sop.model.SopDefinition;
+import org.apache.hertzbeat.ai.sop.model.SopResult;
+import reactor.core.publisher.Flux;
+
+/**
+ * Engine for executing AI SOPs.
+ */
+public interface SopEngine {
+
+ /**
+ * Execute an SOP with the given input parameters (streaming mode).
+ * @param definition The SOP definition to execute.
+ * @param inputParams Input parameters for the SOP.
+ * @return A stream of execution logs/results.
+ */
+ Flux<String> execute(SopDefinition definition, Map<String, Object>
inputParams);
+
+ /**
+ * Execute an SOP synchronously and return a unified result.
+ * Used for AI tool calls and programmatic access.
+ * @param definition The SOP definition to execute.
+ * @param inputParams Input parameters for the SOP.
+ * @return Unified SOP execution result.
+ */
+ SopResult executeSync(SopDefinition definition, Map<String, Object>
inputParams);
+}
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
new file mode 100644
index 0000000000..d307fb5d2a
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/engine/SopEngineImpl.java
@@ -0,0 +1,283 @@
+/*
+ * 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.engine;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+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.SopResult;
+import org.apache.hertzbeat.ai.sop.model.SopStep;
+import org.apache.hertzbeat.ai.sop.model.StepResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+
+/**
+ * Implementation of the SopEngine.
+ */
+@Slf4j
+@Service
+public class SopEngineImpl implements SopEngine {
+
+ // Thread-local context for each execution
+ private static final ThreadLocal<Map<String, Object>> CONTEXT_BUS =
ThreadLocal.withInitial(HashMap::new);
+
+ private final List<SopExecutor> executors;
+
+ @Autowired
+ public SopEngineImpl(List<SopExecutor> executors) {
+ this.executors = executors;
+ }
+
+ @Override
+ public Flux<String> execute(SopDefinition definition, Map<String, Object>
inputParams) {
+ return Flux.create(sink -> {
+ try {
+ log.info("Starting execution of SOP: {}",
definition.getName());
+ sink.next("Starting SOP: " + definition.getName() + " (v" +
definition.getVersion() + ")");
+
+ // Initialize context with input parameters
+ Map<String, Object> context = CONTEXT_BUS.get();
+ context.clear();
+ context.putAll(inputParams);
+
+ // Add language configuration to context
+ OutputConfig outputConfig = definition.getOutput();
+ if (outputConfig != null) {
+ context.put("_language", outputConfig.getLanguageCode());
+ } else {
+ context.put("_language", "zh");
+ }
+
+ for (SopStep step : definition.getSteps()) {
+ sink.next("Executing step [" + step.getId() + "]: " +
step.getType());
+
+ // Find appropriate executor
+ SopExecutor executor = findExecutor(step.getType());
+ if (executor == null) {
+ String error = "No executor found for step type: " +
step.getType();
+ log.error(error);
+ sink.error(new IllegalArgumentException(error));
+ return;
+ }
+
+ // Execute the step
+ try {
+ Object result = executor.execute(step, context);
+ context.put(step.getId(), result);
+
+ // For LLM steps, output the result (the report)
+ if ("llm".equalsIgnoreCase(step.getType()) && result
!= null) {
+ sink.next("--- Report from " + step.getId() + "
---");
+ sink.next(String.valueOf(result));
+ sink.next("--- End of Report ---");
+ }
+
+ sink.next("Step " + step.getId() + " completed");
+ } catch (Exception e) {
+ log.error("Error executing step {}: {}", step.getId(),
e.getMessage(), e);
+ sink.error(e);
+ return;
+ }
+ }
+
+ sink.next("SOP " + definition.getName() + " completed
successfully.");
+ sink.complete();
+ } catch (Exception e) {
+ log.error("Error executing SOP {}: {}", definition.getName(),
e.getMessage(), e);
+ sink.error(e);
+ } finally {
+ CONTEXT_BUS.remove();
+ }
+ });
+ }
+
+ @Override
+ public SopResult executeSync(SopDefinition definition, Map<String, Object>
inputParams) {
+ long startTime = System.currentTimeMillis();
+ List<StepResult> stepResults = new ArrayList<>();
+ Map<String, Object> context = new HashMap<>(inputParams);
+
+ // Get output configuration first
+ OutputConfig outputConfig = definition.getOutput();
+ if (outputConfig == null) {
+ outputConfig = OutputConfig.builder()
+ .type("simple")
+ .format("text")
+ .language("zh")
+ .build();
+ }
+
+ // Add language configuration to context
+ context.put("_language", outputConfig.getLanguageCode());
+
+ SopResult.SopResultBuilder resultBuilder = SopResult.builder()
+ .sopName(definition.getName())
+ .sopVersion(definition.getVersion())
+ .startTime(startTime);
+
+ resultBuilder.outputType(outputConfig.getOutputType());
+ resultBuilder.outputFormat(outputConfig.getFormat() != null ?
outputConfig.getFormat() : "text");
+ resultBuilder.language(outputConfig.getLanguageCode());
+
+ try {
+ log.info("Starting sync execution of SOP: {}",
definition.getName());
+
+ for (SopStep step : definition.getSteps()) {
+ long stepStartTime = System.currentTimeMillis();
+ StepResult.StepResultBuilder stepBuilder = StepResult.builder()
+ .stepId(step.getId())
+ .type(step.getType())
+ .startTime(stepStartTime);
+
+ // Find appropriate executor
+ SopExecutor executor = findExecutor(step.getType());
+ if (executor == null) {
+ String error = "No executor found for step type: " +
step.getType();
+ log.error(error);
+ stepBuilder.status("FAILED")
+ .error(error)
+ .endTime(System.currentTimeMillis());
+ stepResults.add(stepBuilder.build());
+
+ return buildFailedResult(resultBuilder, stepResults,
error, startTime);
+ }
+
+ // Execute the step
+ try {
+ Object result = executor.execute(step, context);
+ context.put(step.getId(), result);
+
+ long stepEndTime = System.currentTimeMillis();
+ stepBuilder.status("SUCCESS")
+ .output(result)
+ .endTime(stepEndTime)
+ .duration(stepEndTime - stepStartTime);
+ stepResults.add(stepBuilder.build());
+
+ } catch (Exception e) {
+ log.error("Error executing step {}: {}", step.getId(),
e.getMessage(), e);
+ long stepEndTime = System.currentTimeMillis();
+ stepBuilder.status("FAILED")
+ .error(e.getMessage())
+ .endTime(stepEndTime)
+ .duration(stepEndTime - stepStartTime);
+ stepResults.add(stepBuilder.build());
+
+ return buildFailedResult(resultBuilder, stepResults,
e.getMessage(), startTime);
+ }
+ }
+
+ // Build successful result
+ long endTime = System.currentTimeMillis();
+ resultBuilder.status("SUCCESS")
+ .endTime(endTime)
+ .duration(endTime - startTime)
+ .steps(stepResults)
+ .data(context);
+
+ // Extract summary and content based on output configuration
+ extractOutputContent(resultBuilder, context, outputConfig,
stepResults);
+
+ return resultBuilder.build();
+
+ } catch (Exception e) {
+ log.error("Error executing SOP {}: {}", definition.getName(),
e.getMessage(), e);
+ return buildFailedResult(resultBuilder, stepResults,
e.getMessage(), startTime);
+ }
+ }
+
+ private SopResult buildFailedResult(SopResult.SopResultBuilder builder,
+ List<StepResult> stepResults, String error, long startTime) {
+ long endTime = System.currentTimeMillis();
+ return builder.status("FAILED")
+ .endTime(endTime)
+ .duration(endTime - startTime)
+ .steps(stepResults)
+ .error(error)
+ .summary("SOP execution failed: " + error)
+ .build();
+ }
+
+ private void extractOutputContent(SopResult.SopResultBuilder builder,
+ Map<String, Object> context, OutputConfig outputConfig,
List<StepResult> stepResults) {
+
+ // Extract content from specified step or last LLM step
+ String contentStepId = outputConfig.getContentStep();
+ String summaryStepId = outputConfig.getSummaryStep();
+
+ // Find content
+ if (contentStepId != null && context.containsKey(contentStepId)) {
+ builder.content(String.valueOf(context.get(contentStepId)));
+ } else {
+ // Default: use the last LLM step's output as content
+ for (int i = stepResults.size() - 1; i >= 0; i--) {
+ StepResult step = stepResults.get(i);
+ if ("llm".equalsIgnoreCase(step.getType()) && step.getOutput()
!= null) {
+ builder.content(String.valueOf(step.getOutput()));
+ break;
+ }
+ }
+ }
+
+ // Generate summary based on output type
+ OutputType outputType = outputConfig.getOutputType();
+ switch (outputType) {
+ case REPORT:
+ builder.summary("Report generated successfully");
+ break;
+ case DATA:
+ builder.summary("Data retrieved: " + context.size() + "
items");
+ break;
+ case ACTION:
+ builder.summary("Action pending confirmation");
+ break;
+ default:
+ builder.summary("Operation completed successfully");
+ }
+
+ // Override with custom summary step if specified
+ if (summaryStepId != null && context.containsKey(summaryStepId)) {
+ String summaryContent = String.valueOf(context.get(summaryStepId));
+ // Take first line as summary
+ int newlineIndex = summaryContent.indexOf('\n');
+ if (newlineIndex > 0 && newlineIndex < 200) {
+ builder.summary(summaryContent.substring(0, newlineIndex));
+ } else if (summaryContent.length() > 200) {
+ builder.summary(summaryContent.substring(0, 200) + "...");
+ } else {
+ builder.summary(summaryContent);
+ }
+ }
+ }
+
+ private SopExecutor findExecutor(String type) {
+ for (SopExecutor executor : executors) {
+ if (executor.support(type)) {
+ return executor;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
new file mode 100644
index 0000000000..0e58c385c6
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
@@ -0,0 +1,110 @@
+/*
+ * 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.executor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
+import org.apache.hertzbeat.ai.service.ChatClientProviderService;
+import org.apache.hertzbeat.ai.sop.model.SopStep;
+import org.apache.hertzbeat.ai.sop.util.SopMessageUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * Executor for 'llm' type steps.
+ * Calls the AI model for reasoning or summarization.
+ * Reuses the existing ChatClientProviderService for LLM interaction.
+ */
+@Slf4j
+@Component
+public class LlmExecutor implements SopExecutor {
+
+ private final ChatClientProviderService chatClientProviderService;
+
+ @Autowired
+ public LlmExecutor(ChatClientProviderService chatClientProviderService) {
+ this.chatClientProviderService = chatClientProviderService;
+ }
+
+ @Override
+ public boolean support(String type) {
+ return "llm".equalsIgnoreCase(type);
+ }
+
+ @Override
+ public Object execute(SopStep step, Map<String, Object> context) {
+ String prompt = step.getPrompt();
+ if (prompt == null || prompt.trim().isEmpty()) {
+ throw new IllegalArgumentException("LLM step must have a 'prompt'
field");
+ }
+
+ // Check if LLM is configured
+ if (!chatClientProviderService.isConfigured()) {
+ log.warn("LLM provider is not configured, returning mock
response");
+ return "Mock LLM response: Provider not configured";
+ }
+
+ // Resolve variables in prompt from context
+ String resolvedPrompt = resolvePrompt(prompt, context);
+
+ // Add language instruction based on configuration
+ String language = (String) context.getOrDefault("_language", "zh");
+ resolvedPrompt = addLanguageInstruction(resolvedPrompt, language);
+
+ log.info("Executing LLM step with prompt length: {}",
resolvedPrompt.length());
+
+ try {
+ // Build chat request context
+ ChatRequestContext chatContext = ChatRequestContext.builder()
+ .message(resolvedPrompt)
+ .build();
+
+ // Use existing service to get response (collect stream to string)
+ StringBuilder responseBuilder = new StringBuilder();
+ chatClientProviderService.streamChat(chatContext)
+ .doOnNext(responseBuilder::append)
+ .blockLast();
+
+ String response = responseBuilder.toString();
+ log.debug("LLM response length: {}", response.length());
+ return response;
+ } catch (Exception e) {
+ log.error("Failed to execute LLM step: {}", e.getMessage());
+ throw new RuntimeException("LLM execution failed", e);
+ }
+ }
+
+ private String addLanguageInstruction(String prompt, String language) {
+ String langInstruction =
SopMessageUtil.getMessage("sop.llm.language.instruction", language);
+ return "\n\n[" + langInstruction + "]\n\n" + prompt;
+ }
+
+ private String resolvePrompt(String prompt, Map<String, Object> context) {
+ // Simple variable replacement: ${variable} -> context.get("variable")
+ String result = prompt;
+ for (Map.Entry<String, Object> entry : context.entrySet()) {
+ String placeholder = "${" + entry.getKey() + "}";
+ if (result.contains(placeholder)) {
+ result = result.replace(placeholder,
String.valueOf(entry.getValue()));
+ }
+ }
+ return result;
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/SopExecutor.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/SopExecutor.java
new file mode 100644
index 0000000000..5826ad2089
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/SopExecutor.java
@@ -0,0 +1,43 @@
+/*
+ * 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.executor;
+
+import org.apache.hertzbeat.ai.sop.model.SopStep;
+
+import java.util.Map;
+
+/**
+ * Interface for SOP step executors.
+ */
+public interface SopExecutor {
+
+ /**
+ * Check if this executor supports the given step type.
+ * @param type Step type.
+ * @return true if supported.
+ */
+ boolean support(String type);
+
+ /**
+ * Execute the given step.
+ * @param step The step to execute.
+ * @param context Current execution context.
+ * @return Execution result.
+ */
+ Object execute(SopStep step, Map<String, Object> context);
+}
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
new file mode 100644
index 0000000000..e1aba900fa
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/ToolExecutor.java
@@ -0,0 +1,258 @@
+/*
+ * 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.executor;
+
+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.springframework.beans.factory.annotation.Autowired;
+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.
+ */
+@Slf4j
+@Component
+public class ToolExecutor implements SopExecutor {
+
+ private final MonitorTools monitorTools;
+ private final AlertTools alertTools;
+ private final AlertDefineTools alertDefineTools;
+ private final MetricsTools metricsTools;
+
+ @Autowired
+ public ToolExecutor(MonitorTools monitorTools, AlertTools alertTools,
+ AlertDefineTools alertDefineTools, MetricsTools
metricsTools) {
+ this.monitorTools = monitorTools;
+ this.alertTools = alertTools;
+ this.alertDefineTools = alertDefineTools;
+ this.metricsTools = metricsTools;
+ }
+
+ @Override
+ public boolean support(String type) {
+ return "tool".equalsIgnoreCase(type);
+ }
+
+ @Override
+ public Object execute(SopStep step, Map<String, Object> context) {
+ String toolName = step.getTool();
+ log.info("Executing tool step: {}", toolName);
+
+ Map<String, Object> args = resolveArgs(step.getArgs(), context);
+
+ try {
+ String result = invokeTool(toolName, args);
+ log.debug("Tool {} returned result length: {}", toolName,
result.length());
+ return result;
+ } catch (Exception e) {
+ log.error("Failed to execute tool {}: {}", toolName,
e.getMessage());
+ throw new RuntimeException("Tool execution failed: " + toolName,
e);
+ }
+ }
+
+ 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<>();
+ }
+
+ Map<String, Object> resolved = new HashMap<>();
+ 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);
+ } else {
+ resolved.put(entry.getKey(), value);
+ }
+ }
+ 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/model/OutputConfig.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java
new file mode 100644
index 0000000000..e236d444e9
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java
@@ -0,0 +1,91 @@
+/*
+ * 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Output configuration for SOP definition.
+ * Defines how the SOP result should be formatted and presented.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OutputConfig {
+
+ /**
+ * Output type: report, simple, data, action
+ */
+ private String type;
+
+ /**
+ * Output format: markdown, json, text
+ */
+ private String format;
+
+ /**
+ * Which step's output should be used as the final summary
+ */
+ private String summaryStep;
+
+ /**
+ * Which step's output should be used as the main content
+ */
+ private String contentStep;
+
+ /**
+ * Language for LLM responses and SOP output: zh (Chinese), en (English)
+ * Default is zh (Chinese)
+ */
+ private String language;
+
+ /**
+ * Get language code, default to zh (Chinese)
+ */
+ public String getLanguageCode() {
+ if (language == null || language.isEmpty()) {
+ return "zh";
+ }
+ return language.toLowerCase();
+ }
+
+ /**
+ * Check if the language is Chinese
+ */
+ public boolean isChinese() {
+ return "zh".equals(getLanguageCode()) ||
"chinese".equalsIgnoreCase(language);
+ }
+
+ /**
+ * Get the OutputType enum from string
+ */
+ public OutputType getOutputType() {
+ if (type == null) {
+ return OutputType.SIMPLE;
+ }
+ try {
+ return OutputType.valueOf(type.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return OutputType.SIMPLE;
+ }
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputType.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputType.java
new file mode 100644
index 0000000000..f8eb05d581
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputType.java
@@ -0,0 +1,44 @@
+/*
+ * 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.model;
+
+/**
+ * SOP output type enumeration.
+ * Defines different types of SOP execution results.
+ */
+public enum OutputType {
+ /**
+ * Report type - generates a detailed report (e.g., daily inspection,
fault analysis)
+ */
+ REPORT,
+
+ /**
+ * Simple type - returns simple success/failure result (e.g., restart,
clear cache)
+ */
+ SIMPLE,
+
+ /**
+ * Data type - returns structured data (e.g., query resources, statistics)
+ */
+ DATA,
+
+ /**
+ * Action type - returns pending actions that require human confirmation
+ */
+ ACTION
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopDefinition.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopDefinition.java
new file mode 100644
index 0000000000..24e0b36879
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopDefinition.java
@@ -0,0 +1,65 @@
+/*
+ * 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Model representing an AI SOP (Standard Operating Procedure) definition.
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SopDefinition {
+
+ /**
+ * Unique name of the SOP skill.
+ */
+ private String name;
+
+ /**
+ * Description of what this SOP does (used by AI for discovery).
+ */
+ private String description;
+
+ /**
+ * Version of the SOP.
+ */
+ private String version;
+
+ /**
+ * Input parameters required by this SOP.
+ */
+ private List<SopParameter> parameters;
+
+ /**
+ * Ordered list of steps to execute.
+ */
+ private List<SopStep> steps;
+
+ /**
+ * Output configuration for this SOP.
+ */
+ private OutputConfig output;
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopParameter.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopParameter.java
new file mode 100644
index 0000000000..55eb75fac1
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopParameter.java
@@ -0,0 +1,58 @@
+/*
+ * 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Model representing an input parameter for an AI SOP.
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SopParameter {
+
+ /**
+ * Name of the parameter.
+ */
+ private String name;
+
+ /**
+ * Type of the parameter (string, long, boolean, etc.).
+ */
+ private String type;
+
+ /**
+ * Description of the parameter.
+ */
+ private String description;
+
+ /**
+ * Whether the parameter is required.
+ */
+ private boolean required;
+
+ /**
+ * Default value if not provided.
+ */
+ private String defaultValue;
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java
new file mode 100644
index 0000000000..1facf238a5
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java
@@ -0,0 +1,197 @@
+/*
+ * 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.model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.ai.sop.util.SopMessageUtil;
+
+/**
+ * Unified SOP execution result.
+ * Contains all information about a SOP execution including metadata, output,
and step details.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SopResult {
+
+ // ===== Metadata =====
+
+ /**
+ * SOP name
+ */
+ private String sopName;
+
+ /**
+ * SOP version
+ */
+ private String sopVersion;
+
+ /**
+ * Execution status: SUCCESS, FAILED, PARTIAL
+ */
+ private String status;
+
+ /**
+ * Start timestamp in milliseconds
+ */
+ private long startTime;
+
+ /**
+ * End timestamp in milliseconds
+ */
+ private long endTime;
+
+ /**
+ * Duration in milliseconds
+ */
+ private long duration;
+
+ // ===== Output Type =====
+
+ /**
+ * Output type: REPORT, SIMPLE, DATA, ACTION
+ */
+ private OutputType outputType;
+
+ /**
+ * Output format: markdown, json, text
+ */
+ private String outputFormat;
+
+ /**
+ * Language code: zh (Chinese), en (English)
+ */
+ @Builder.Default
+ private String language = "zh";
+
+ // ===== Output Content =====
+
+ /**
+ * Short summary (one line)
+ */
+ private String summary;
+
+ /**
+ * Main content (Markdown report / JSON data / etc.)
+ */
+ private String content;
+
+ /**
+ * Structured data (for programmatic processing)
+ */
+ @Builder.Default
+ private Map<String, Object> data = new HashMap<>();
+
+ // ===== Step Details =====
+
+ /**
+ * Execution result of each step
+ */
+ @Builder.Default
+ private List<StepResult> steps = new ArrayList<>();
+
+ /**
+ * Error message if failed
+ */
+ private String error;
+
+ /**
+ * Get localized message using i18n.
+ */
+ private String msg(String code) {
+ return SopMessageUtil.getMessage(code, language);
+ }
+
+ /**
+ * Convert to AI-friendly response format.
+ * Used when SOP is called as a tool by AI.
+ */
+ public String toAiResponse() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(msg("sop.result.title")).append(":
").append(status).append("\n");
+ sb.append(msg("sop.result.name")).append(": ").append(sopName)
+ .append(" (v").append(sopVersion).append(")\n");
+ sb.append(msg("sop.result.duration")).append(":
").append(duration).append("ms\n");
+
+ if (summary != null && !summary.isEmpty()) {
+ sb.append(msg("sop.result.summary")).append(":
").append(summary).append("\n");
+ }
+
+ if ("FAILED".equals(status) && error != null) {
+ sb.append(msg("sop.result.error")).append(":
").append(error).append("\n");
+ return sb.toString();
+ }
+
+ if (outputType == OutputType.REPORT && content != null) {
+ sb.append("\n---
").append(msg("sop.result.report.title")).append(" ---\n");
+ sb.append(content);
+ } else if (outputType == OutputType.DATA && !data.isEmpty()) {
+ sb.append("\n").append(msg("sop.result.data.title")).append(":\n");
+ for (Map.Entry<String, Object> entry : data.entrySet()) {
+ sb.append("- ").append(entry.getKey()).append(":
").append(entry.getValue()).append("\n");
+ }
+ } else if (outputType == OutputType.SIMPLE) {
+ sb.append(msg("sop.result.simple.complete")).append("\n");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Convert to SSE stream format for real-time updates.
+ */
+ public String toSseFormat() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("event: sop_result\n");
+ sb.append("data: {");
+ sb.append("\"sopName\":\"").append(sopName).append("\",");
+ sb.append("\"status\":\"").append(status).append("\",");
+ sb.append("\"duration\":").append(duration).append(",");
+ sb.append("\"outputType\":\"").append(outputType).append("\",");
+ sb.append("\"language\":\"").append(language).append("\",");
+ if (summary != null) {
+
sb.append("\"summary\":\"").append(escapeJson(summary)).append("\",");
+ }
+ if (content != null) {
+
sb.append("\"content\":\"").append(escapeJson(content)).append("\",");
+ }
+ sb.append("\"stepsCount\":").append(steps.size());
+ sb.append("}\n\n");
+ return sb.toString();
+ }
+
+ private String escapeJson(String text) {
+ if (text == null) {
+ return "";
+ }
+ return text.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopStep.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopStep.java
new file mode 100644
index 0000000000..95f5e69494
--- /dev/null
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopStep.java
@@ -0,0 +1,65 @@
+/*
+ * 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+/**
+ * Model representing a single step in an AI SOP.
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SopStep {
+
+ /**
+ * Unique ID of the step within the SOP.
+ */
+ private String id;
+
+ /**
+ * Type of executor: 'tool', 'llm', 'script', etc.
+ */
+ private String type;
+
+ /**
+ * Name of the tool to call (if type is 'tool').
+ */
+ private String tool;
+
+ /**
+ * Arguments for the step (supports ${var} expressions).
+ */
+ private Map<String, Object> args;
+
+ /**
+ * Prompt template (if type is 'llm').
+ */
+ private String prompt;
+
+ /**
+ * Condition to execute this step (optional).
+ */
+ private String condition;
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/StepResult.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/StepResult.java
new file mode 100644
index 0000000000..a3c43fb266
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/StepResult.java
@@ -0,0 +1,73 @@
+/*
+ * 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.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Represents the result of a single SOP step execution.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StepResult {
+
+ /**
+ * Step ID
+ */
+ private String stepId;
+
+ /**
+ * Step type (tool, llm, condition, etc.)
+ */
+ private String type;
+
+ /**
+ * Execution status: SUCCESS, FAILED, SKIPPED
+ */
+ private String status;
+
+ /**
+ * Start timestamp in milliseconds
+ */
+ private long startTime;
+
+ /**
+ * End timestamp in milliseconds
+ */
+ private long endTime;
+
+ /**
+ * Duration in milliseconds
+ */
+ private long duration;
+
+ /**
+ * Step output/result
+ */
+ private Object output;
+
+ /**
+ * Error message if failed
+ */
+ private String error;
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SkillRegistry.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SkillRegistry.java
new file mode 100644
index 0000000000..f3301f709f
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SkillRegistry.java
@@ -0,0 +1,103 @@
+/*
+ * 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 lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.sop.engine.SopEngine;
+import org.apache.hertzbeat.ai.sop.model.SopDefinition;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Registry for AI SOP skills.
+ * Manages the lifecycle of SOPs and exposes them as Spring AI tools.
+ */
+@Slf4j
+@Service
+@Configuration
+public class SkillRegistry implements ToolCallbackProvider {
+
+ private final SopYamlLoader yamlLoader;
+ private final SopEngine sopEngine;
+ private final Map<String, SopDefinition> skillMap = new
ConcurrentHashMap<>();
+
+ @Autowired
+ public SkillRegistry(SopYamlLoader yamlLoader, SopEngine sopEngine) {
+ this.yamlLoader = yamlLoader;
+ this.sopEngine = sopEngine;
+ }
+
+ @PostConstruct
+ public void init() {
+ refreshSkills();
+ }
+
+ /**
+ * Reload all skills from YAML files.
+ */
+ public void refreshSkills() {
+ List<SopDefinition> loadedSkills = yamlLoader.loadAllSkills();
+ skillMap.clear();
+ for (SopDefinition skill : loadedSkills) {
+ skillMap.put(skill.getName(), skill);
+ }
+ log.info("SkillRegistry initialized with {} skills", skillMap.size());
+ }
+
+ /**
+ * Get a specific SOP definition by name.
+ * @param name Skill name.
+ * @return SopDefinition or null if not found.
+ */
+ public SopDefinition getSkill(String name) {
+ return skillMap.get(name);
+ }
+
+ /**
+ * Get all registered SOP definitions.
+ * @return Collection of all SopDefinitions.
+ */
+ public List<SopDefinition> getAllSkills() {
+ return new ArrayList<>(skillMap.values());
+ }
+
+ /**
+ * Provides the SOP skills as ToolCallbacks for Spring AI.
+ * @return Array of ToolCallbacks.
+ */
+ @Override
+ public ToolCallback[] getToolCallbacks() {
+ List<ToolCallback> callbacks = new ArrayList<>();
+
+ for (SopDefinition skill : skillMap.values()) {
+ log.debug("Registering SOP as tool: {}", skill.getName());
+ callbacks.add(new SopToolCallback(skill, sopEngine));
+ }
+
+ return callbacks.toArray(new ToolCallback[0]);
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopToolCallback.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopToolCallback.java
new file mode 100644
index 0000000000..90ce21e7c2
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopToolCallback.java
@@ -0,0 +1,100 @@
+/*
+ * 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 org.apache.hertzbeat.ai.sop.engine.SopEngine;
+import org.apache.hertzbeat.ai.sop.model.SopDefinition;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+
+/**
+ * ToolCallback implementation for executing AI SOPs.
+ * Wraps a SOP definition as a Spring AI tool.
+ */
+public class SopToolCallback implements ToolCallback {
+
+ private final SopDefinition definition;
+ private final SopEngine sopEngine;
+ private final ToolDefinition toolDefinition;
+
+ public SopToolCallback(SopDefinition definition, SopEngine sopEngine) {
+ this.definition = definition;
+ this.sopEngine = sopEngine;
+ this.toolDefinition = ToolDefinition.builder()
+ .name(definition.getName())
+ .description(definition.getDescription())
+ .inputSchema(buildInputSchema())
+ .build();
+ }
+
+ @Override
+ public ToolDefinition getToolDefinition() {
+ return toolDefinition;
+ }
+
+ @Override
+ public String call(String arguments) {
+ // TODO: Parse arguments and execute SOP
+ return "SOP " + definition.getName() + " execution started.";
+ }
+
+ private String buildInputSchema() {
+ // Build JSON Schema for SOP parameters
+ StringBuilder schema = new StringBuilder();
+ schema.append("{\"type\":\"object\",\"properties\":{");
+
+ if (definition.getParameters() != null &&
!definition.getParameters().isEmpty()) {
+ boolean first = true;
+ for (var param : definition.getParameters()) {
+ if (!first) {
+ schema.append(",");
+ }
+ schema.append("\"").append(param.getName()).append("\":");
+
schema.append("{\"type\":\"").append(mapType(param.getType())).append("\"");
+ if (param.getDescription() != null) {
+
schema.append(",\"description\":\"").append(param.getDescription()).append("\"");
+ }
+ schema.append("}");
+ first = false;
+ }
+ }
+
+ schema.append("}}");
+ return schema.toString();
+ }
+
+ private String mapType(String type) {
+ if (type == null) {
+ return "string";
+ }
+ switch (type.toLowerCase()) {
+ case "boolean":
+ return "boolean";
+ case "integer":
+ case "int":
+ case "long":
+ return "integer";
+ case "number":
+ case "double":
+ case "float":
+ return "number";
+ default:
+ return "string";
+ }
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopYamlLoader.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopYamlLoader.java
new file mode 100644
index 0000000000..9b773dd97a
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/registry/SopYamlLoader.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.sop.model.SopDefinition;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loader for AI SOP definitions from YAML files.
+ */
+@Slf4j
+@Service
+public class SopYamlLoader {
+
+ private static final String SKILLS_PATH_PATTERN = "classpath:skills/*.yml";
+ private final ObjectMapper yamlMapper;
+
+ public SopYamlLoader() {
+ this.yamlMapper = new ObjectMapper(new YAMLFactory());
+ }
+
+ /**
+ * Load all SOP definitions from the classpath.
+ * @return List of loaded SopDefinition objects.
+ */
+ public List<SopDefinition> loadAllSkills() {
+ List<SopDefinition> skills = new ArrayList<>();
+ PathMatchingResourcePatternResolver resolver = new
PathMatchingResourcePatternResolver();
+
+ try {
+ Resource[] resources = resolver.getResources(SKILLS_PATH_PATTERN);
+ log.info("Found {} SOP definition files", resources.length);
+
+ for (Resource resource : resources) {
+ try {
+ SopDefinition definition =
yamlMapper.readValue(resource.getInputStream(), SopDefinition.class);
+ if (definition != null) {
+ skills.add(definition);
+ log.info("Loaded SOP skill: {}", definition.getName());
+ }
+ } catch (Exception e) {
+ log.error("Failed to parse SOP definition from {}: {}",
resource.getFilename(), e.getMessage());
+ }
+ }
+ } catch (IOException e) {
+ log.error("Failed to scan for SOP definition files: {}",
e.getMessage());
+ }
+
+ return skills;
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/util/SopMessageUtil.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/util/SopMessageUtil.java
new file mode 100644
index 0000000000..9b38a48441
--- /dev/null
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/util/SopMessageUtil.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util;
+
+import java.util.Locale;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.stereotype.Component;
+
+/**
+ * Utility class for SOP internationalization messages.
+ * Provides static access to MessageSource for POJOs like SopResult.
+ */
+@Component
+public class SopMessageUtil {
+
+ private static MessageSource messageSource;
+
+ public SopMessageUtil(@Qualifier("sopMessageSource") MessageSource
messageSource) {
+ SopMessageUtil.messageSource = messageSource;
+ }
+
+ /**
+ * Get message by code using current locale.
+ */
+ public static String getMessage(String code) {
+ return getMessage(code, null, null);
+ }
+
+ /**
+ * Get message by code for specific language.
+ * @param code message code
+ * @param language language code: "zh", "en"
+ */
+ public static String getMessage(String code, String language) {
+ return getMessage(code, null, language);
+ }
+
+ /**
+ * Get message by code with arguments for specific language.
+ */
+ public static String getMessage(String code, Object[] args, String
language) {
+ if (messageSource == null) {
+ return code;
+ }
+
+ Locale locale;
+ if ("en".equalsIgnoreCase(language) ||
"english".equalsIgnoreCase(language)) {
+ locale = Locale.ENGLISH;
+ } else if ("zh".equalsIgnoreCase(language) ||
"chinese".equalsIgnoreCase(language)) {
+ locale = Locale.CHINESE;
+ } else {
+ locale = LocaleContextHolder.getLocale();
+ }
+
+ try {
+ return messageSource.getMessage(code, args, code, locale);
+ } catch (Exception e) {
+ return code;
+ }
+ }
+
+ /**
+ * Get locale from language code.
+ */
+ public static Locale getLocale(String language) {
+ if ("en".equalsIgnoreCase(language) ||
"english".equalsIgnoreCase(language)) {
+ return Locale.ENGLISH;
+ }
+ return Locale.CHINESE;
+ }
+}
diff --git
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
index 6364bccc90..7495cf83eb 100644
---
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
+++
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java
@@ -140,10 +140,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status
values
- long onlineCount = monitorService.getMonitors(null, app,
search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
- long offlineCount = monitorService.getMonitors(null, app,
search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
- long unreachableCount = monitorService.getMonitors(null, app,
search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
- long pausedCount = monitorService.getMonitors(null, app,
search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
+ long onlineCount = monitorService.getMonitors(null, app,
search, (byte) 1, "id", "asc", 0, 1, labels).getTotalElements();
+ long offlineCount = monitorService.getMonitors(null, app,
search, (byte) 2, "id", "asc", 0, 1, labels).getTotalElements();
+ long unreachableCount = monitorService.getMonitors(null, app,
search, (byte) 3, "id", "asc", 0, 1, labels).getTotalElements();
+ long pausedCount = monitorService.getMonitors(null, app,
search, (byte) 0, "id", "asc", 0, 1, labels).getTotalElements();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
diff --git a/hertzbeat-ai/src/main/resources/i18n/messages.properties
b/hertzbeat-ai/src/main/resources/i18n/messages.properties
new file mode 100644
index 0000000000..7a97ff01cf
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/i18n/messages.properties
@@ -0,0 +1,16 @@
+# SOP Result Messages - Default (Chinese)
+sop.result.title=SOP执行结果
+sop.result.name=SOP名称
+sop.result.duration=耗时
+sop.result.summary=摘要
+sop.result.error=错误
+sop.result.report.title=详细报告
+sop.result.data.title=返回数据
+sop.result.simple.complete=操作已完成。
+sop.result.report.success=报告生成成功
+sop.result.data.success=数据获取成功
+sop.result.action.pending=等待确认操作
+sop.result.operation.success=操作已完成
+
+# LLM Language Instructions
+sop.llm.language.instruction=重要:请使用中文回复。
diff --git a/hertzbeat-ai/src/main/resources/i18n/messages_en.properties
b/hertzbeat-ai/src/main/resources/i18n/messages_en.properties
new file mode 100644
index 0000000000..f358ade8e1
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/i18n/messages_en.properties
@@ -0,0 +1,16 @@
+# SOP Result Messages - English
+sop.result.title=SOP Execution Result
+sop.result.name=SOP Name
+sop.result.duration=Duration
+sop.result.summary=Summary
+sop.result.error=Error
+sop.result.report.title=Detailed Report
+sop.result.data.title=Returned Data
+sop.result.simple.complete=Operation completed successfully.
+sop.result.report.success=Report generated successfully
+sop.result.data.success=Data retrieved successfully
+sop.result.action.pending=Action pending confirmation
+sop.result.operation.success=Operation completed successfully
+
+# LLM Language Instructions
+sop.llm.language.instruction=IMPORTANT: Please respond in English.
diff --git a/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties
b/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties
new file mode 100644
index 0000000000..56c8232fc8
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties
@@ -0,0 +1,16 @@
+# SOP Result Messages - Chinese
+sop.result.title=SOP执行结果
+sop.result.name=SOP名称
+sop.result.duration=耗时
+sop.result.summary=摘要
+sop.result.error=错误
+sop.result.report.title=详细报告
+sop.result.data.title=返回数据
+sop.result.simple.complete=操作已完成。
+sop.result.report.success=报告生成成功
+sop.result.data.success=数据获取成功
+sop.result.action.pending=等待确认操作
+sop.result.operation.success=操作已完成
+
+# LLM Language Instructions
+sop.llm.language.instruction=重要:请使用中文回复。
diff --git a/hertzbeat-ai/src/main/resources/skills/README.md
b/hertzbeat-ai/src/main/resources/skills/README.md
new file mode 100644
index 0000000000..a699073dd9
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/skills/README.md
@@ -0,0 +1,70 @@
+# AI SOP Engine
+
+The AI SOP (Standard Operating Procedure) Engine enables defining and
executing automated workflows through YAML configuration.
+
+## Features
+
+- **YAML-based Definition**: Define workflows declaratively
+- **Multiple Step Types**: `tool` (call HertzBeat tools), `llm` (AI reasoning)
+- **Unified Output**: Consistent result structure with `SopResult`
+- **I18n Support**: Multi-language output (zh/en)
+- **Multiple APIs**: Streaming (SSE), Sync (JSON), AI-friendly (Text)
+
+## Quick Start
+
+### 1. Define a Skill
+
+Create `skills/my_skill.yml`:
+
+```yaml
+name: my_skill
+description: "My custom skill"
+version: "1.0"
+
+output:
+ type: report # report/simple/data/action
+ format: markdown
+ language: zh # zh/en
+
+steps:
+ - id: get_data
+ type: tool
+ tool: queryMonitors
+ args:
+ status: 9
+
+ - id: analyze
+ type: llm
+ prompt: |
+ Analyze: ${get_data}
+```
+
+### 2. Execute
+
+```bash
+# Streaming (SSE)
+POST /api/ai/sop/execute/{skillName}
+
+# Sync (JSON)
+POST /api/ai/sop/execute/{skillName}/sync
+
+# AI Format (Text)
+POST /api/ai/sop/execute/{skillName}/ai
+```
+
+## Output Types
+
+| Type | Use Case |
+|------|----------|
+| `report` | Daily inspection, analysis |
+| `simple` | Restart, clear cache |
+| `data` | Query, statistics |
+| `action` | Pending confirmation |
+
+## Architecture
+
+```
+YAML Definition → SkillRegistry → SopEngine → Executors → SopResult
+ ↓
+ ToolExecutor / LlmExecutor
+```
diff --git a/hertzbeat-ai/src/main/resources/skills/README_ZH.md
b/hertzbeat-ai/src/main/resources/skills/README_ZH.md
new file mode 100644
index 0000000000..aa5116e2ea
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/skills/README_ZH.md
@@ -0,0 +1,81 @@
+# AI SOP 引擎
+
+AI SOP(标准操作流程)引擎支持通过 YAML 配置定义和执行自动化工作流。
+
+## 功能特性
+
+- **YAML 声明式定义**:通过配置文件定义工作流
+- **多步骤类型**:`tool`(调用 HertzBeat 工具)、`llm`(AI 推理)
+- **统一输出**:使用 `SopResult` 统一结果结构
+- **国际化支持**:多语言输出(中文/英文)
+- **多种 API**:流式(SSE)、同步(JSON)、AI 友好(纯文本)
+
+## 快速开始
+
+### 1. 定义技能
+
+创建 `skills/my_skill.yml`:
+
+```yaml
+name: my_skill
+description: "我的自定义技能"
+version: "1.0"
+
+output:
+ type: report # report/simple/data/action
+ format: markdown
+ language: zh # zh/en
+
+steps:
+ - id: get_data
+ type: tool
+ tool: queryMonitors
+ args:
+ status: 9
+
+ - id: analyze
+ type: llm
+ prompt: |
+ 分析以下数据: ${get_data}
+```
+
+### 2. 执行
+
+```bash
+# 流式输出(SSE)
+POST /api/ai/sop/execute/{skillName}
+
+# 同步返回(JSON)
+POST /api/ai/sop/execute/{skillName}/sync
+
+# AI 格式(纯文本)
+POST /api/ai/sop/execute/{skillName}/ai
+```
+
+## 输出类型
+
+| 类型 | 使用场景 |
+|------|---------|
+| `report` | 日常巡检、故障分析 |
+| `simple` | 重启服务、清理缓存 |
+| `data` | 查询资源、统计信息 |
+| `action` | 需要人工确认的操作 |
+
+## 架构
+
+```
+YAML 定义 → SkillRegistry → SopEngine → Executors → SopResult
+ ↓
+ ToolExecutor / LlmExecutor
+```
+
+## 配置说明
+
+### output 配置
+
+| 字段 | 说明 | 可选值 |
+|-----|------|-------|
+| type | 输出类型 | report/simple/data/action |
+| format | 格式 | markdown/json/text |
+| language | 语言 | zh(中文)/en(英文) |
+| contentStep | 内容步骤 | 步骤 ID |
diff --git a/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
new file mode 100644
index 0000000000..1a726dbf31
--- /dev/null
+++ b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
@@ -0,0 +1,66 @@
+name: daily_inspection
+description: "Execute daily health inspection on all monitors and generate a
comprehensive report"
+version: "1.0"
+
+# Output configuration
+output:
+ type: report # report / simple / data / action
+ format: markdown # markdown / json / text
+ language: zh # zh (Chinese) / en (English) - controls LLM response
language
+ contentStep: generate_report # Which step's output is the main content
+
+parameters:
+ - name: includeMetrics
+ type: boolean
+ description: "Whether to include metrics data in the report"
+ required: false
+ defaultValue: "false"
+
+steps:
+ - id: get_monitor_summary
+ type: tool
+ tool: queryMonitors
+ args:
+ status: 9
+ pageSize: 100
+ pageIndex: 0
+ sort: "gmtCreate"
+ order: "desc"
+ includeStats: true
+
+ # TODO: Enable when alert tables are initialized
+ # - id: get_alert_summary
+ # type: tool
+ # tool: getAlertsSummary
+
+ # - id: get_firing_alerts
+ # type: tool
+ # tool: queryAlerts
+ # args:
+ # status: "firing"
+ # pageSize: 20
+ # pageIndex: 0
+ # sort: "startAt"
+ # order: "desc"
+
+ - id: get_storage_status
+ type: tool
+ tool: getWarehouseStatus
+
+ - id: generate_report
+ type: llm
+ prompt: |
+ You are an expert IT operations engineer. Based on the following
monitoring data, generate a concise daily inspection report.
+
+ ## Monitor Status Summary
+ ${get_monitor_summary}
+
+ ## Storage Status
+ ${get_storage_status}
+
+ Please provide a report with:
+ 1. Overall Health Summary
+ 2. Critical Issues Requiring Immediate Attention
+ 3. Recommendations
+
+ Format the report in markdown. Be concise but thorough.
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
index 4c97f91432..3d946f88ad 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
@@ -614,7 +614,10 @@ public class MonitorServiceImpl implements MonitorService {
}
};
// Pagination is a must
- Sort sortExp = Sort.by(new
Sort.Order(Sort.Direction.fromString(order), sort));
+ // Handle null sort/order parameters with defaults
+ String effectiveSort = (sort == null || sort.isEmpty()) ? "id" : sort;
+ String effectiveOrder = (order == null || order.isEmpty()) ? "desc" :
order;
+ Sort sortExp = Sort.by(new
Sort.Order(Sort.Direction.fromString(effectiveOrder), effectiveSort));
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp);
return monitorDao.findAll(specification, pageRequest);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]