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]


Reply via email to