Compare commits

...
Author SHA1 Message Date
tomsun28 5d3ab01c56 [doc] update 2025-11-01 17:27:29 +08:00
tomsun28 8310a9abd4 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 17:22:00 +08:00
tomsun28 40178765a6 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 17:14:21 +08:00
tomsun28 01ef70baf5 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:56:26 +08:00
tomsun28 a7cc0870c1 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:44:27 +08:00
tomsun28 4693b0a296 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:33:46 +08:00
tomsun28 271b94693a [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:27:52 +08:00
tomsun28 13691cb09b [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 15:26:51 +08:00
tomsun28 d1c42d4bad [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 15:23:48 +08:00
tomsun28 35b8d87045 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 14:46:07 +08:00
tomsun28 b131061fc0 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 14:23:42 +08:00
tomsun28 04a9335a3b [doc] update 2025-11-01 12:25:42 +08:00
Tomsun28 0f86d32afb Merge branch 'master' into update-ai-with 2025-11-01 12:21:17 +08:00
tomsun28 faf2399b0a refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 00:48:17 +08:00
tomsun28 76539fb6f1 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 22:26:31 +08:00
tomsun28 49bb1f4652 Merge branch 'master' into update-ai-with 2025-10-31 21:20:11 +08:00
tomsun28 27c68e31c6 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 21:14:36 +08:00
tomsun28 a68956c34c [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 21:00:01 +08:00
tomsun28 908ae61f0a [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:47:11 +08:00
tomsun28 065fcf48bf [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:44:04 +08:00
tomsun28 41fec4149c refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:25:17 +08:00
tomsun28 48d0f5e1a8 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:21:59 +08:00
tomsun28 212852fe9b refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:17:47 +08:00
tomsun28 dc98713c0f refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:13:06 +08:00
aias00 dd59aedbef Merge branch 'master' into update-ai-with 2025-10-31 10:16:23 +08:00
aias00 83630a1331 Merge branch 'master' into update-ai-with 2025-10-31 09:36:49 +08:00
tomsun28 5e488d4a4a refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 01:08:37 +08:00
tomsun28 b781a0bcbe [doc] update 2025-10-31 01:05:13 +08:00
tomsun28 be31129428 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:59:38 +08:00
tomsun28 6a54bdfbb0 [doc] update 2025-10-31 00:52:30 +08:00
tomsun28 7846f3276d refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:49:49 +08:00
tomsun28 a3cfaf2be6 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:48:37 +08:00
tomsun28 7f5513ae56 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:37:02 +08:00
tomsun28 22d6ad7ec0 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:14:40 +08:00
tomsun28 cf7efb09f8 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:52:41 +08:00
tomsun28 f7428c25c3 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:42:30 +08:00
tomsun28 7b92448172 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:04:32 +08:00
tomsun28 fd7d9980ee refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:00:36 +08:00
tomsun28 776d806826 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 22:48:33 +08:00
tomsun28 4a63eb5cc0 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 22:38:45 +08:00
tomsun28 3b58e3dcdd fix bug
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 22:11:59 +08:00
125 changed files with 1830 additions and 4389 deletions
+4 -1
View File
@@ -65,12 +65,15 @@
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
@@ -1,81 +0,0 @@
/*
* 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.adapters;
import org.apache.hertzbeat.ai.pojo.dto.Hierarchy;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.data.domain.Page;
import java.util.List;
/**
* Interface that provides access to alert definition information by retrieving data
* through the underlying alert define service.
*/
public interface AlertDefineServiceAdapter {
/**
* Add a new alert rule definition
* @param alertDefine Alert definition to add
* @return Created alert definition with ID
*/
AlertDefine addAlertDefine(AlertDefine alertDefine);
/**
* Get alert definitions with filtering and pagination
* @param search Search term
* @param app Monitor type filter
* @param enabled Enabled status filter
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of alert definitions
*/
Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize);
/**
* Get alert definition by ID
* @param id Alert definition ID
* @return Alert definition if found
*/
AlertDefine getAlertDefine(Long id);
/**
* Enable or disable alert definition
* @param id Alert definition ID
* @param enabled Whether to enable
*/
void toggleAlertDefineStatus(Long id, boolean enabled);
/**
* Modify/update an existing alert definition
* @param alertDefine Alert definition to update
* @return Updated alert definition
*/
AlertDefine modifyAlertDefine(AlertDefine alertDefine);
/**
* Get specific app hierarchy structure
* @param app App type
* @param lang Language for localization
* @return List of hierarchy objects for specific app
*/
List<Hierarchy> getAppHierarchy(String app, String lang);
}
@@ -1,60 +0,0 @@
/*
* 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.adapters;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.data.domain.Page;
/**
* Interface that provides access to alert information by retrieving alert data
* through the underlying alert service.
*/
public interface AlertServiceAdapter {
/**
* Get single alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of single alerts
*/
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get group alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of group alerts
*/
Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information
*/
AlertSummary getAlertsSummary();
}
@@ -1,55 +0,0 @@
/*
* 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.adapters;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
/**
* Interface that provides access to metrics information by retrieving metrics data
* through the underlying metrics service.
*/
public interface MetricsServiceAdapter {
/**
* Check warehouse storage server status
* @return true if warehouse is available, false otherwise
*/
Boolean getWarehouseStorageServerStatus();
/**
* Query real-time metrics data
* @param monitorId Monitor ID
* @param metrics Metrics name
* @return Real-time metrics data
*/
MetricsData getMetricsData(Long monitorId, String metrics);
/**
* Query historical metrics data
* @param monitorId Monitor ID
* @param app Monitor type
* @param metrics Metrics name
* @param metric Metric field name
* @param label Label filter
* @param history Query historical time period
* @param interval Whether to aggregate data
* @return Historical metrics data
*/
MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
}
@@ -1,71 +0,0 @@
/*
* 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.adapters;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.List;
import java.util.Map;
/**
* Interface that provides access to monitor information by retrieving monitor data
* through the underlying monitor service.
*/
public interface MonitorServiceAdapter {
Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels
);
/**
* Add a new monitor
*
* @param monitor The monitor entity to create
* @param params List of parameters for the monitor
* @param collector Optional collector assignment
* @return The created monitor ID
*/
Long addMonitor(Monitor monitor, List<Param> params, String collector);
/**
* Get all available monitor types with their display names
*
* @param language Language code (e.g., "en-US", "zh-CN")
* @return Map of monitor type key to display name
*/
Map<String, String> getAvailableMonitorTypes(String language);
/**
* Get parameter definitions for a specific monitor type
*
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
* @return List of parameter definitions for the monitor type
*/
List<ParamDefine> getMonitorParamDefines(String app);
}
@@ -1,257 +0,0 @@
/*
* 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.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
/**
* Implementation of the AlertDefineServiceAdapter interface that provides access to alert definition information
* through reflection by invoking the underlying alert define service implementation.
*/
@Slf4j
@Component
public class AlertDefineServiceAdapterImpl implements AlertDefineServiceAdapter {
@Override
public AlertDefine addAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("addAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully added alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke addAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke addAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod(
"getAlertDefines",
List.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<AlertDefine> result = (Page<AlertDefine>) method.invoke(
alertDefineService, null, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} alert defines", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefines via adapter", e);
throw new RuntimeException("Failed to invoke getAlertDefines via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine getAlertDefine(Long id) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine result = (AlertDefine) method.invoke(alertDefineService, id);
log.debug("Successfully retrieved alert define with ID: {}", id);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefine via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke getAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public void toggleAlertDefineStatus(Long id, boolean enabled) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
// First get the existing AlertDefine
Method getMethod = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine alertDefine = (AlertDefine) getMethod.invoke(alertDefineService, id);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + id + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
Method modifyMethod = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
modifyMethod.invoke(alertDefineService, alertDefine);
log.debug("Successfully toggled alert define status for ID: {} to enabled: {}", id, enabled);
} catch (Exception e) {
log.error("Failed to invoke toggleAlertDefineStatus via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke toggleAlertDefineStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine modifyAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully modified alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: modifyAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke modifyAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke modifyAlertDefine via adapter: " + e.getMessage(), e);
}
}
/**
* Retrieves the application hierarchy for a given app and language.
* Uses reflection to call the underlying app service method.
*
* @param app The application name
* @param lang The language code (optional, defaults to "en-US")
* @return List of Hierarchy objects representing the app hierarchy
*/
@Override
public List<Hierarchy> getAppHierarchy(String app, String lang) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAppHierarchy: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (lang == null || lang.trim().isEmpty()) {
lang = "en-US";
}
// Call getAppHierarchy method: getAppHierarchy(String app, String lang)
Method method = appService.getClass().getMethod("getAppHierarchy", String.class, String.class);
List<?> managerHierarchies = (List<?>) method.invoke(appService, app, lang);
// Convert manager DTOs to ai-agent DTOs
List<Hierarchy> result = UtilityClass.convertToAgentHierarchies(managerHierarchies);
log.debug("Successfully retrieved and converted {} hierarchies for app '{}'", result.size(), app);
return result;
} catch (Exception e) {
log.error("Failed to get app hierarchy for app '{}': {}", app, e.getMessage(), e);
throw new RuntimeException("Failed to get app hierarchy for " + app, e);
}
}
}
@@ -1,141 +0,0 @@
/*
* 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.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the AlertServiceAdapter interface that provides access to alert information
* through reflection by invoking the underlying alert service implementation.
*/
@Slf4j
@Component
public class AlertServiceAdapterImpl implements AlertServiceAdapter {
@Override
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getSingleAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getSingleAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<SingleAlert> result = (Page<SingleAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} single alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getSingleAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getSingleAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getSingleAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getGroupAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getGroupAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<GroupAlert> result = (Page<GroupAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} group alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getGroupAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getGroupAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getGroupAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertSummary getAlertsSummary() {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertsSummary: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod("getAlertsSummary");
AlertSummary result = (AlertSummary) method.invoke(alertService);
log.debug("Successfully retrieved alerts summary");
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertsSummary", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertsSummary via adapter", e);
throw new RuntimeException("Failed to invoke getAlertsSummary via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,136 +0,0 @@
/*
* 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.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the MetricsServiceAdapter interface that provides access to metrics information
* through reflection by invoking the underlying metrics service implementation.
*/
@Slf4j
@Component
public class MetricsServiceAdapterImpl implements MetricsServiceAdapter {
@Override
public Boolean getWarehouseStorageServerStatus() {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod("getWarehouseStorageServerStatus");
Boolean result = (Boolean) method.invoke(metricsDataService);
log.debug("Successfully retrieved warehouse storage server status: {}", result);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getWarehouseStorageServerStatus", e);
} catch (Exception e) {
log.error("Failed to invoke getWarehouseStorageServerStatus via adapter", e);
throw new RuntimeException("Failed to invoke getWarehouseStorageServerStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsData getMetricsData(Long monitorId, String metrics) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricsData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricsData",
Long.class, String.class);
MetricsData result = (MetricsData) method.invoke(metricsDataService, monitorId, metrics);
log.debug("Successfully retrieved metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricsData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricsData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricsData via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricHistoryData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricHistoryData",
Long.class, String.class, String.class, String.class, String.class, String.class, Boolean.class);
MetricsHistoryData result = (MetricsHistoryData) method.invoke(
metricsDataService, monitorId, app, metrics, metric, label, history, interval);
log.debug("Successfully retrieved historical metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricHistoryData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricHistoryData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricHistoryData via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,214 +0,0 @@
/*
* 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.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
* through reflection by invoking the underlying monitor service implementation.
*/
@Slf4j
@Component
public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
@Override
public Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels) {
try {
// Provide default values for all nullable parameters
if (sort == null || sort.trim().isEmpty()) {
sort = "gmtCreate";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 8;
}
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found: {}", monitorService.getClass().getSimpleName());
Method method = monitorService.getClass().getMethod(
"getMonitors",
List.class, String.class, String.class, Byte.class,
String.class, String.class, int.class, int.class, String.class);
@SuppressWarnings("unchecked")
Page<Monitor> result = (Page<Monitor>) method.invoke(
monitorService,
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result.getContent());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMonitors", e);
} catch (Exception e) {
log.debug("Failed to invoke getMonitors via adapter", e);
throw new RuntimeException("Failed to invoke getMonitors via adapter", e);
}
}
@Override
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
try {
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
Method method = monitorService.getClass().getMethod(
"addMonitor",
Monitor.class, List.class, String.class,
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
// Call the method with null dashboard
method.invoke(monitorService, monitor, params, collector, null);
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
return monitor.getId();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addMonitor", e);
} catch (Exception e) {
log.error("Failed to invoke addMonitor via adapter", e);
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
}
}
@Override
public Map<String, String> getAvailableMonitorTypes(String language) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Call getI18nApps method: getI18nApps(String lang)
Method method = appService.getClass().getMethod("getI18nApps", String.class);
@SuppressWarnings("unchecked")
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
log.debug("Successfully retrieved {} monitor types", result.size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getI18nApps", e);
} catch (Exception e) {
log.error("Failed to invoke getI18nApps via adapter", e);
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
}
}
@Override
public List<ParamDefine> getMonitorParamDefines(String app) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
// Validate app parameter
if (app == null || app.trim().isEmpty()) {
throw new IllegalArgumentException("Monitor type/app parameter is required");
}
// Call getAppParamDefines method: getAppParamDefines(String app)
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
@SuppressWarnings("unchecked")
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAppParamDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,56 +0,0 @@
/*
* 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 lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.ai.model.ApiKey;
import org.springframework.stereotype.Component;
/**
* Dynamic LLM Provider API Key implementation that retrieves the API key
*/
@Slf4j
@Component
public class DynamicOpenAiApiKey implements ApiKey {
private final GeneralConfigDao generalConfigDao;
public DynamicOpenAiApiKey(GeneralConfigDao generalConfigDao) {
this.generalConfigDao = generalConfigDao;
}
@NotNull
@Override
public String getValue() {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (modelProviderConfig != null && modelProviderConfig.isEnable() && modelProviderConfig.isStatus()) {
log.debug("Retrieved {} API key from configuration service", modelProviderConfig.getCode());
return modelProviderConfig.getApiKey();
} else {
log.warn("No valid LLM Provider API key found in configuration");
return "";
}
}
}
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.ai.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.ai.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
@@ -72,12 +72,7 @@ public class LlmConfig {
}
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (!modelProviderConfig.isEnable() || !modelProviderConfig.isStatus()) {
log.warn("LLM Provider is not enabled or status is not valid, ChatClient bean will not be created");
return null;
}
if (modelProviderConfig.getApiKey() == null) {
if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) {
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
return null;
}
@@ -25,9 +25,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -63,23 +63,6 @@ public class ChatController {
this.conversationService = conversationService;
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ConversationDto>> createConversation() {
try {
ConversationDto conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error creating conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
}
}
/**
* Send a message and get a streaming response with conversation tracking
*
@@ -88,14 +71,13 @@ public class ChatController {
*/
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
try {
// Validate message is not empty
SubjectSum subject = SurenessContextHolder.getBindSubject();
log.info(subject.toString());
McpContextHolder.setSubject(subject);
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("Error: Message cannot be empty")
.build();
@@ -109,7 +91,7 @@ public class ChatController {
} catch (Exception e) {
log.error("Error in stream chat endpoint: ", e);
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("An error occurred: " + e.getMessage())
.build();
@@ -119,6 +101,18 @@ public class ChatController {
}
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ChatConversation>> createConversation() {
ChatConversation conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
}
/**
* Get all conversations
*
@@ -126,14 +120,9 @@ public class ChatController {
*/
@GetMapping(path = "/conversations")
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
try {
List<ConversationDto> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
} catch (Exception e) {
log.error("Error listing conversations: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
}
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
List<ChatConversation> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
}
/**
@@ -144,26 +133,10 @@ public class ChatController {
*/
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ConversationDto>> getConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
ConversationDto conversation = conversationService.getConversation(conversationId);
if (conversation == null) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error getting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
}
public ResponseEntity<Message<ChatConversation>> getConversation(
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
ChatConversation conversation = conversationService.getConversation(conversationId);
return ResponseEntity.ok(Message.success(conversation));
}
/**
@@ -175,23 +148,8 @@ public class ChatController {
@DeleteMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
public ResponseEntity<Message<Void>> deleteConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
boolean deleted = conversationService.deleteConversation(conversationId);
if (!deleted) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
} catch (Exception e) {
log.error("Error deleting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
}
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
conversationService.deleteConversation(conversationId);
return ResponseEntity.ok(Message.success());
}
}
@@ -15,13 +15,15 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service;
package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.manager.service.ai.ZhiPuServiceImpl;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* test case for {@link ZhiPuServiceImpl}
* Repository interface for Conversation entities
*/
class ZhiPuServiceTest {
@Repository
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
}
@@ -15,13 +15,16 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service;
package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.manager.service.ai.AlibabaAiServiceImpl;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* test case for {@link AlibabaAiServiceImpl}
* Repository interface for Message entities
*/
class AlibabaAiServiceTest {
@Repository
public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> {
}
@@ -24,6 +24,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
/**
* Chat request context for AI chat endpoint.
@@ -40,10 +41,10 @@ public class ChatRequestContext {
/**
* Optional conversation ID for context
*/
private String conversationId;
private Long conversationId;
/**
* Conversation history messages for context
*/
private List<MessageDto> conversationHistory;
private List<ChatMessage> conversationHistory;
}
@@ -31,17 +31,18 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat response")
public class ChatResponseDto {
public class ChatResponseChunk {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Conversation ID", example = "123")
private Long conversationId;
@Schema(description = "AI response message", example = "Here are your monitors...")
private String response;
@Schema(description = "User message ID", example = "msg-user-123")
private String userMessageId;
@Schema(description = "User message ID", example = "123")
private Long userMessageId;
@Schema(description = "Assistant message ID", example = "123")
private Long assistantMessageId;
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
private String assistantMessageId;
}
@@ -1,53 +0,0 @@
/*
* 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.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Conversation DTO for AI chat conversations.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Conversation information")
public class ConversationDto {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Creation time")
private LocalDateTime createdAt;
@Schema(description = "Last updated time")
private LocalDateTime updatedAt;
@Schema(description = "Messages in this conversation")
private List<MessageDto> messages;
@Schema(description = "Message count")
private Integer messageCount;
}
@@ -1,87 +0,0 @@
/*
* 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.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
/**
* Hierarchical structure
* eg: Monitoring Type metrics Information Hierarchy Relationship
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Schema(description = "Monitor Hierarchy")
public class Hierarchy {
/**
* Category value
*/
@Schema(description = "Category Value", example = "os", accessMode = READ_WRITE)
String category;
/**
* Attribute value
*/
@Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE)
String value;
/**
* Attribute internationalization tag
*/
@Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE)
String label;
/**
* Is it a leaf node
*/
@Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE)
Boolean isLeaf = false;
/**
* Is hide this app type in main menus layout
*/
@Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true")
Boolean hide = true;
/**
* For leaf metric
* metric type 0-number: number 1-string: string
*/
@Schema(description = "metric type 0-number: number 1-string: string")
private Byte type;
/**
* metric unit
*/
@Schema(description = "metric unit")
private String unit;
/**
* Next level of association
*/
@Schema(description = "Next Hierarchy", accessMode = READ_WRITE)
private List<Hierarchy> children;
}
@@ -1,52 +0,0 @@
/*
* 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.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Message DTO for chat messages.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat message")
public class MessageDto {
@Schema(description = "Message ID", example = "msg-123")
private String messageId;
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Message content", example = "List all monitors")
private String content;
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
private String role;
@Schema(description = "Message timestamp")
private LocalDateTime timestamp;
}
@@ -1,60 +0,0 @@
/*
* 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.service;
/**
* Ai Configuration Service
*/
public interface AiConfigService {
/**
* Validate OpenAI API key by calling the OpenAI API
* @param apiKey the API key to validate
* @return validation result with success status and message
*/
ValidationResult validateApiKey(String apiKey);
/**
* Validation result class
*/
class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult success(String message) {
return new ValidationResult(true, message);
}
public static ValidationResult failure(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}
@@ -18,8 +18,8 @@
package org.apache.hertzbeat.ai.service;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
@@ -30,13 +30,6 @@ import java.util.List;
*/
public interface ConversationService {
/**
* Create a new conversation
*
* @return Created conversation data
*/
ConversationDto createConversation();
/**
* Send a message and receive a streaming response
*
@@ -44,8 +37,14 @@ public interface ConversationService {
* @param conversationId Optional conversation ID for continuing a chat
* @return Flux of ServerSentEvent for streaming the response
*/
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
/**
* Create a new conversation
*
* @return Created conversation data
*/
ChatConversation createConversation();
/**
* Get conversation history for a specific conversation
@@ -53,28 +52,19 @@ public interface ConversationService {
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
ConversationDto getConversation(String conversationId);
ChatConversation getConversation(Long conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<ConversationDto> getAllConversations();
List<ChatConversation> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
* @return true if deleted, false if conversation not found
*/
boolean deleteConversation(String conversationId);
/**
* Check if a conversation exists
*
* @param conversationId Conversation ID to check
* @return true if conversation exists, false otherwise
*/
boolean conversationExists(String conversationId);
void deleteConversation(Long conversationId);
}
@@ -1,100 +0,0 @@
/*
* 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.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.service.AiConfigService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* Ai Configuration Service Implementation
*/
@Slf4j
@Service
public class AiConfigServiceImpl implements AiConfigService {
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
private final RestTemplate restTemplate;
private final GeneralConfigDao generalConfigDao;
public AiConfigServiceImpl(GeneralConfigDao generalConfigDao, RestTemplate restTemplate) {
this.restTemplate = restTemplate;
this.generalConfigDao = generalConfigDao;
}
@Override
public ValidationResult validateApiKey(String apiKey) {
if (!StringUtils.hasText(apiKey)) {
return ValidationResult.failure("API key cannot be empty");
}
if (!apiKey.startsWith("sk-")) {
return ValidationResult.failure("Invalid API key format. OpenAI API keys should start with 'sk-'");
}
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.set("Content-Type", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);
log.debug("Validating OpenAI API key by calling models endpoint");
ResponseEntity<String> response = restTemplate.exchange(
OPENAI_MODELS_ENDPOINT,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
log.info("OpenAI API key validation successful");
return ValidationResult.success("API key is valid");
} else {
log.warn("OpenAI API key validation failed with status: {}", response.getStatusCode());
return ValidationResult.failure("API key validation failed: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Error validating OpenAI API key", e);
String errorMessage = e.getMessage();
// Parse common error messages
if (errorMessage.contains("401")) {
return ValidationResult.failure("Invalid API key - authentication failed");
} else if (errorMessage.contains("403")) {
return ValidationResult.failure("API key does not have permission to access models");
} else if (errorMessage.contains("429")) {
return ValidationResult.failure("Rate limit exceeded - please try again later");
} else if (errorMessage.contains("timeout") || errorMessage.contains("connect")) {
return ValidationResult.failure("Network error - unable to connect to OpenAI API");
} else {
return ValidationResult.failure("API key validation failed: " + errorMessage);
}
}
}
}
@@ -20,8 +20,8 @@ package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.PromptProvider;
import org.apache.hertzbeat.ai.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -66,14 +66,6 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
this.generalConfigDao = generalConfigDao;
}
public String complete(String message) {
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
return chatClient.prompt()
.user(message)
.call()
.content();
}
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
@@ -84,7 +76,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
// Add conversation history if available
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
for (MessageDto historyMessage : context.getConversationHistory()) {
for (ChatMessage historyMessage : context.getConversationHistory()) {
if ("user".equals(historyMessage.getRole())) {
messages.add(new UserMessage(historyMessage.getContent()));
} else if ("assistant".equals(historyMessage.getRole())) {
@@ -117,7 +109,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
if (!isConfigured) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.isStatus();
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
}
return isConfigured;
}
@@ -18,25 +18,23 @@
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Implementation of the ConversationService interface for managing chat conversations.
@@ -44,35 +42,22 @@ import java.util.stream.Collectors;
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
@Autowired
private ChatConversationDao conversationDao;
@Autowired
private ChatMessageDao messageDao;
@Autowired
private ChatClientProviderService chatClientProviderService;
@Override
public ConversationDto createConversation() {
String conversationId = createNewConversation();
return getConversation(conversationId);
}
@Override
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
// Validate conversation exists
if (!conversationExists(conversationId)) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("Error: Conversation not found: " + conversationId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
// Check if OpenAI is properly configured
// Check if provider is properly configured
if (!chatClientProviderService.isConfigured()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("Provider is not configured. Please configure your AI Provider.")
.build();
@@ -84,50 +69,50 @@ public class ConversationServiceImpl implements ConversationService {
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
String userMessageId = addMessageToConversation(conversationId, message, "user");
// Get conversation history for context
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> conversationHistory = new ArrayList<>();
if (messagesList != null && messagesList.size() > 1) {
// Get all messages except the last one (which is the current user message we just added)
for (int i = 0; i < messagesList.size() - 1; i++) {
Map<String, Object> msgMap = messagesList.get(i);
conversationHistory.add(mapToMessageDto(msgMap));
}
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
.content(message).role("user").build();
chatMessage = messageDao.save(chatMessage);
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
if (conversation.getTitle().startsWith("conversation")) {
// Auto-generate title from first user message
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
conversation.setTitle(title);
conversationDao.save(conversation);
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(conversationHistory)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
ChatMessage finalChatMessage = chatMessage;
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseDto responseDto = ChatResponseDto.builder()
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
.conversationId(conversationId)
.userMessageId(finalChatMessage.getId())
.response(chunk)
.userMessageId(userMessageId)
.build();
return ServerSentEvent.builder(responseDto)
return ServerSentEvent.builder(responseChunk)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
ChatResponseDto finalResponse = ChatResponseDto.builder()
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
.content(fullResponse.toString()).role("assistant").build();
assistantMessage = messageDao.save(assistantMessage);
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("")
.userMessageId(userMessageId)
.assistantMessageId(assistantMessageId)
.assistantMessageId(assistantMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
@@ -137,10 +122,10 @@ public class ConversationServiceImpl implements ConversationService {
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(userMessageId)
.userMessageId(finalChatMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
@@ -149,124 +134,27 @@ public class ConversationServiceImpl implements ConversationService {
}
@Override
public ConversationDto getConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
public ChatConversation createConversation() {
ChatConversation conversation = new ChatConversation();
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
return conversationDao.save(conversation);
}
@Override
public ChatConversation getConversation(Long conversationId) {
if (conversationId == null) {
return null;
}
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation == null) {
return null;
}
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> messages = messagesList != null
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
new ArrayList<>();
return ConversationDto.builder()
.conversationId((String) conversation.get("conversationId"))
.createdAt((LocalDateTime) conversation.get("createdAt"))
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
.messages(messages)
.build();
return conversationDao.getReferenceById(conversationId);
}
@Override
public List<ConversationDto> getAllConversations() {
List<ConversationDto> result = new ArrayList<>();
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
Map<String, Object> conv = entry.getValue();
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
ConversationDto dto = ConversationDto.builder()
.conversationId((String) conv.get("conversationId"))
.createdAt((LocalDateTime) conv.get("createdAt"))
.updatedAt((LocalDateTime) conv.get("updatedAt"))
.messages(new ArrayList<>()) // Don't include messages in list view for performance
.build();
result.add(dto);
}
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
return result;
public List<ChatConversation> getAllConversations() {
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
@Override
public boolean deleteConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return false;
}
boolean existed = conversations.containsKey(conversationId);
if (existed) {
conversations.remove(conversationId);
conversationMessages.remove(conversationId);
log.info("Deleted conversation: {}", conversationId);
}
return existed;
public void deleteConversation(Long conversationId) {
conversationDao.deleteById(conversationId);
}
@Override
public boolean conversationExists(String conversationId) {
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
}
private String createNewConversation() {
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
LocalDateTime now = LocalDateTime.now();
Map<String, Object> conversation = new HashMap<>();
conversation.put("conversationId", conversationId);
conversation.put("createdAt", now);
conversation.put("updatedAt", now);
conversations.put(conversationId, conversation);
conversationMessages.put(conversationId, new ArrayList<>());
log.info("Created new conversation: {}", conversationId);
return conversationId;
}
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
return MessageDto.builder()
.messageId((String) messageMap.get("messageId"))
.conversationId((String) messageMap.get("conversationId"))
.content((String) messageMap.get("content"))
.role((String) messageMap.get("role"))
.timestamp((LocalDateTime) messageMap.get("timestamp"))
.build();
}
private String addMessageToConversation(String conversationId, String content, String role) {
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, Object> message = new HashMap<>();
message.put("messageId", messageId);
message.put("conversationId", conversationId);
message.put("content", content);
message.put("role", role);
message.put("timestamp", LocalDateTime.now());
messages.add(message);
// Update conversation timestamp
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation != null) {
conversation.put("updatedAt", LocalDateTime.now());
// Auto-generate title from first user message
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
conversation.put("title", title);
}
}
return messageId;
}
}
@@ -22,12 +22,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
@@ -46,7 +47,9 @@ import java.util.Map;
@Service
public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AlertDefineServiceAdapter alertDefineServiceAdapter;
private AlertDefineService alertDefineService;
@Autowired
private AppService appService;
@Override
@@ -240,17 +243,19 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
.datasource(datasource)
.enable(enable)
.build();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
AlertDefine createdAlertDefine = alertDefineServiceAdapter.addAlertDefine(alertDefine);
alertDefineService.addAlertDefine(alertDefine);
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
log.info("Successfully created alert rule '{}' with ID: {}", name, createdAlertDefine.getId());
log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId());
StringBuilder response = new StringBuilder();
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
type, name, createdAlertDefine.getId()));
type, name, alertDefine.getId()));
response.append(String.format("Expression: %s\n", expr));
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
response.append(String.format("Trigger after: %d consecutive violations\n", times));
@@ -296,8 +301,11 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
pageSize = 10;
}
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
Page<AlertDefine> result = alertDefineService.getAlertDefines(null,
search, "gmtCreate", "desc", pageIndex, pageSize);
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size())
@@ -345,7 +353,21 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
// First get the existing AlertDefine
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + ruleId + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
alertDefineService.modifyAlertDefine(alertDefine);
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
return String.format("Successfully %s alert rule ID: %d",
@@ -369,7 +391,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Getting alert rule details for ID: {}", ruleId);
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
return "Alert rule with ID " + ruleId + " not found";
}
@@ -441,7 +466,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
List<Hierarchy> hierarchies;
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US");
ObjectMapper mapper = new ObjectMapper();
@@ -459,10 +484,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
result.put("message", "No hierarchy data available");
}
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
log.info("Hierarchy JSON: {}", jsonResult);
return jsonResult;
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
} catch (Exception e) {
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
@@ -494,7 +516,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
// Get the existing alert rule
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId);
if (existingRule == null) {
return String.format("Error: Alert rule with ID %d not found", ruleId);
}
@@ -571,7 +595,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
@@ -608,7 +634,8 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
@@ -633,7 +660,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
// Get hierarchy for the specified app
List<Hierarchy> hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.toLowerCase(), "en-US");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US");
if (hierarchies == null || hierarchies.isEmpty()) {
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
@@ -20,11 +20,11 @@ package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,7 +41,7 @@ import org.springframework.stereotype.Service;
@Service
public class AlertToolsImpl implements AlertTools {
@Autowired
private AlertServiceAdapter alertServiceAdapter;
private AlertService alertService;
@Override
@Tool(name = "query_alerts", description = """
@@ -116,7 +116,7 @@ public class AlertToolsImpl implements AlertTools {
// Handle different alert types
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
Page<SingleAlert> singleResult = alertServiceAdapter.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("SINGLE ALERTS:\n");
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
@@ -154,7 +154,7 @@ public class AlertToolsImpl implements AlertTools {
response.append("\n");
}
Page<GroupAlert> groupResult = alertServiceAdapter.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("GROUP ALERTS:\n");
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
@@ -205,7 +205,7 @@ public class AlertToolsImpl implements AlertTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
AlertSummary summary = alertService.getAlertsSummary();
StringBuilder response = new StringBuilder();
response.append("ALERTS SUMMARY\n");
@@ -20,9 +20,8 @@ package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
@@ -44,9 +43,7 @@ import java.util.Map;
@Service
public class MetricsToolsImpl implements MetricsTools {
@Autowired
private MetricsServiceAdapter metricsServiceAdapter;
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
private MetricsDataService metricsDataService;
@Override
@Tool(name = "query_realtime_metrics", description = """
@@ -78,7 +75,7 @@ public class MetricsToolsImpl implements MetricsTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics);
if (metricsData == null) {
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
@@ -173,7 +170,7 @@ public class MetricsToolsImpl implements MetricsTools {
interval = true;
}
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
@@ -235,8 +232,10 @@ public class MetricsToolsImpl implements MetricsTools {
public String getWarehouseStatus() {
try {
log.info("Checking warehouse storage status");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
StringBuilder response = new StringBuilder();
response.append("METRICS WAREHOUSE STATUS\n");
@@ -19,8 +19,9 @@ package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
@@ -43,7 +44,9 @@ import java.util.Map;
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
private MonitorService monitorService;
@Autowired
private AppService appService;
/**
@@ -123,8 +126,12 @@ public class MonitorToolsImpl implements MonitorTools {
includeStats = false;
}
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
Page<Monitor> result = monitorService.getMonitors(
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorService.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
@@ -133,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 = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
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();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
@@ -266,7 +273,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Validate that all required parameters for this monitor type are provided
try {
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
@@ -290,8 +300,12 @@ public class MonitorToolsImpl implements MonitorTools {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call adapter - it handles all the complexity (validation, defaults, app-specific logic)
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
monitorService.addMonitor(monitor, params, null, null);
Long monitorId = monitor.getId();
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
@@ -361,14 +375,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
// Set default language if not provided
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Get available monitor types from adapter
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
// Get available monitor types from app service
Map<String, String> monitorTypes = appService.getI18nApps(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
@@ -416,14 +430,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
// Validate required parameter
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application parameter is required";
}
// Get parameter definitions from adapter
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
@@ -22,9 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.pojo.dto.Hierarchy;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -33,6 +31,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
/**
* Utility class providing helper methods for field expression validation, parsing,
@@ -227,51 +226,6 @@ public class UtilityClass {
return "VALID";
}
/**
* Helper method to validate operator
*/
public boolean isValidOperator(String operator) {
return operator != null && (operator.equals(">") || operator.equals("<")
|| operator.equals(">=") || operator.equals("<=")
|| operator.equals("==") || operator.equals("!="));
}
/**
* Helper method to validate priority
*/
public boolean isValidPriority(String priority) {
return priority != null && (priority.equalsIgnoreCase("critical")
|| priority.equalsIgnoreCase("warning") || priority.equalsIgnoreCase("info"));
}
/**
* Helper method to build expression
*/
public String buildExpression(String metric, String operator, String threshold) {
return String.format("%s %s %s", metric, operator, threshold);
}
/**
* Helper method to parse existing expression into components
*/
public String[] parseExpression(String expression) {
if (expression == null || expression.trim().isEmpty()) {
return null;
}
// Simple parsing for basic expressions like "metric > value"
String[] operators = {">", "<", ">=", "<=", "==", "!="};
for (String op : operators) {
if (expression.contains(" " + op + " ")) {
String[] parts = expression.split(" " + op + " ");
if (parts.length == 2) {
return new String[]{parts[0].trim(), op, parts[1].trim()};
}
}
}
return null;
}
/**
* Helper method to parse key-value pairs from a string
* Format: "key1:value1, key2:value2, ..."
@@ -473,19 +427,6 @@ public class UtilityClass {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Parse time range string to milliseconds
*/
public long parseTimeRangeToMillis(String timeRange) {
return switch (timeRange.toLowerCase()) {
case "1h" -> 60 * 60 * 1000L;
case "6h" -> 6 * 60 * 60 * 1000L;
case "24h" -> 24 * 60 * 60 * 1000L;
case "7d" -> 7 * 24 * 60 * 60 * 1000L;
default -> 24 * 60 * 60 * 1000L; // default to 24h
};
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
@@ -504,143 +445,6 @@ public class UtilityClass {
};
}
/**
* Helper method to get metrics name for a metric type
*/
public String getMetricsNameForType(String metricType) {
return switch (metricType.toLowerCase()) {
case "cpu" -> "cpu";
case "memory" -> "memory";
case "disk" -> "disk";
case "network" -> "network";
default -> "system";
};
}
/**
* Helper method to check if a field represents usage for a metric type
*/
public boolean isUsageField(String field, String metricType) {
if (field == null) return false;
String fieldLower = field.toLowerCase();
String typeLower = metricType.toLowerCase();
return fieldLower.contains("usage")
|| fieldLower.contains("percent")
|| fieldLower.contains("util")
|| (typeLower.equals("cpu") && (fieldLower.contains("cpu") || fieldLower.contains("idle")))
|| (typeLower.equals("memory") && fieldLower.contains("memory"))
|| (typeLower.equals("disk") && fieldLower.contains("disk"));
}
/**
* Convert manager module Hierarchy objects to ai-agent module Hierarchy objects
* This handles the cross-module DTO conversion to avoid ClassCastException
*/
public List<Hierarchy> convertToAgentHierarchies(List<?> managerHierarchies) {
List<Hierarchy> agentHierarchies = new ArrayList<>();
for (Object managerHierarchy : managerHierarchies) {
Hierarchy agentHierarchy = convertToAgentHierarchy(managerHierarchy);
agentHierarchies.add(agentHierarchy);
}
return agentHierarchies;
}
/**
* Convert a single manager Hierarchy object to ai-agent Hierarchy object using reflection
*/
public Hierarchy convertToAgentHierarchy(Object managerHierarchy) {
try {
Hierarchy agentHierarchy = new Hierarchy();
// Use reflection to copy properties from manager DTO to agent DTO
Class<?> managerClass = managerHierarchy.getClass();
// Copy basic properties
agentHierarchy.setCategory(getStringField(managerHierarchy, managerClass, "category"));
agentHierarchy.setValue(getStringField(managerHierarchy, managerClass, "value"));
agentHierarchy.setLabel(getStringField(managerHierarchy, managerClass, "label"));
agentHierarchy.setIsLeaf(getBooleanField(managerHierarchy, managerClass, "isLeaf"));
agentHierarchy.setHide(getBooleanField(managerHierarchy, managerClass, "hide"));
agentHierarchy.setType(getByteField(managerHierarchy, managerClass, "type"));
agentHierarchy.setUnit(getStringField(managerHierarchy, managerClass, "unit"));
// Handle children recursively
List<?> managerChildren = getListField(managerHierarchy, managerClass, "children");
if (managerChildren != null && !managerChildren.isEmpty()) {
List<Hierarchy> agentChildren = convertToAgentHierarchies(managerChildren);
agentHierarchy.setChildren(agentChildren);
}
return agentHierarchy;
} catch (Exception e) {
log.error("Failed to convert manager hierarchy to agent hierarchy: {}", e.getMessage(), e);
throw new RuntimeException("Failed to convert hierarchy", e);
}
}
public String getStringField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value != null ? value.toString() : null;
} catch (Exception e) {
log.debug("Could not get string field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public Boolean getBooleanField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e) {
try {
// Try alternative getter pattern for boolean fields
Method isGetter = clazz.getMethod("is" + capitalize(fieldName));
Object value = isGetter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e2) {
log.debug("Could not get boolean field '{}': {}", fieldName, e.getMessage());
return null;
}
}
}
public Byte getByteField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Byte ? (Byte) value : null;
} catch (Exception e) {
log.debug("Could not get byte field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public List<?> getListField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof List ? (List<?>) value : null;
} catch (Exception e) {
log.debug("Could not get list field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
/**
* Extract existing monitor IDs from the alert expression
@@ -24,7 +24,7 @@ import java.util.Properties;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
@@ -151,4 +151,4 @@ public class SmsClientFactory {
break;
}
}
}
}
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -236,4 +236,4 @@ public class AlibabaSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
}
}
}
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.constants.SmsConstants;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import jakarta.xml.bind.DatatypeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TencentSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TencentSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -168,4 +168,4 @@ public class TwilioSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
}
}
}
@@ -18,7 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -190,4 +190,4 @@ public class UniSmsClientImpl implements SmsClient {
return true;
}
}
}
@@ -27,7 +27,7 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Properties;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -19,8 +19,8 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -89,4 +89,4 @@ public class SmsClientFactoryTest {
assertNull(smsClientFactory.getSmsClient());
}
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -61,4 +61,4 @@ class TwilioSmsClientImplTest {
assertEquals(expected, twilioSmsClient.checkConfig());
}
}
}
@@ -18,7 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
@@ -70,4 +70,4 @@ public class SmsLocalSmsClientImplTest {
() -> smsLocalSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service.impl;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
@@ -82,4 +82,4 @@ public class UniSmsClientImplTest {
}
}
}
@@ -0,0 +1,84 @@
/*
* 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.common.entity.ai;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Entity for storing AI chat conversation metadata
*/
@Data
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "hzb_ai_conversation")
@AllArgsConstructor
@NoArgsConstructor
public class ChatConversation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Schema(title = "conversation title")
private String title;
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
@CreatedBy
private String creator;
@Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY)
@LastModifiedBy
private String modifier;
@Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY)
@CreatedDate
private LocalDateTime gmtCreate;
@Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY)
@LastModifiedDate
private LocalDateTime gmtUpdate;
/**
* List of messages in this conversation (one-to-many relationship)
*/
@OneToMany
@JoinColumn(name = "conversation_id")
private List<ChatMessage> messages;
}
@@ -15,66 +15,74 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.entity;
package org.apache.hertzbeat.common.entity.ai;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* OpenAI Agent Config Entity
* Entity for storing individual chat messages in conversations
*/
@Entity
@Table(name = "hzb_ai_agent_config")
@Data
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "hzb_ai_message", indexes = {
@Index(name = "idx_message_conversation_id", columnList = "conversation_id")
})
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "OpenAI Agent config entity")
@EntityListeners(AuditingEntityListener.class)
public class OpenAiConfig {
public class ChatMessage {
@Id
@Schema(title = "Config type: openai, primary key", description = "Config type: openai, primary key",
accessMode = READ_WRITE)
@NotBlank(message = "type can not null")
private String type;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Schema(title = "conversation id")
private Long conversationId;
@Schema(title = "Config content", description = "Config contentformat json", accessMode = READ_WRITE)
@Column(length = 8192)
@Schema(title = "message content")
@Lob
@NotBlank
private String content;
@Schema(title = "message role: user, system")
private String role;
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
@CreatedBy
private String creator;
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
@Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY)
@LastModifiedBy
private String modifier;
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
@Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY)
@CreatedDate
private LocalDateTime gmtCreate;
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
@Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY)
@LastModifiedDate
private LocalDateTime gmtUpdate;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
package org.apache.hertzbeat.common.entity.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.pojo.dto;
package org.apache.hertzbeat.common.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@@ -32,15 +32,6 @@ import lombok.NoArgsConstructor;
@Schema(description = "LLM Model Provider configuration")
public class ModelProviderConfig {
@Schema(title = "Enable Provider", description = "Whether Provider is enabled", example = "true")
private boolean enable = false;
@Schema(title = "Check the provider available status")
private boolean status = false;
@Schema(title = "The error message when provider status check failed")
private String error;
@Schema(title = "Model type, text-generate, vision")
private String type;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -74,4 +74,4 @@ public class SmsConfig {
* Smslocal SMS configuration
*/
private SmslocalSmsProperties smslocal;
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import lombok.AllArgsConstructor;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -58,4 +58,4 @@ public class TencentSmsProperties {
*/
@NotBlank(message = "templateId cannot be null")
private String templateId;
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -46,4 +46,4 @@ public class TwilioSmsProperties {
*/
@NotBlank(message = "Twilio Phone Number cannot be empty")
private String twilioPhoneNumber;
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
package org.apache.hertzbeat.common.entity.dto.sms;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -57,4 +57,4 @@ public class UniSmsProperties {
*/
@NotBlank(message = "authMode cannot be null")
private String authMode = "simple";
}
}
-5
View File
@@ -212,11 +212,6 @@
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-memory-netty</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-ai</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
@@ -1,60 +0,0 @@
/*
* 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.manager.config;
import lombok.Data;
import org.apache.hertzbeat.common.constants.ConfigConstants;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* AiProperties
*/
@Data
@Configuration
@ConfigurationProperties(prefix =
ConfigConstants.FunctionModuleConstants.AI)
public class AiProperties {
/**
* AI Type: zhiPu, alibabaAi, kimiAi, sparkDesk
*/
private String type;
/**
* Model name: glm-4, qwen-turboo, moonshot-v1-8k, generalv3.5
*/
private String model;
/**
* API key
*/
private String apiKey;
/**
* At present, only IFLYTEK large model needs to be filled in
*/
private String apiSecret;
/**
* API URL for the Ollama AI service.
*/
private String apiUrl;
}
@@ -1,68 +0,0 @@
/*
* 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.manager.controller;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiControllerRequestParam;
import org.apache.hertzbeat.manager.service.ai.AiService;
import org.apache.hertzbeat.manager.service.ai.factory.AiServiceFactoryImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.util.Assert;
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;
/**
* AI Management API
*/
@Tag(name = "AI Manage API")
@RestController
@RequestMapping(value = "/api/ai")
public class AiController {
/**
* AI beanFactory
*/
@Autowired(required = false)
private AiServiceFactoryImpl aiServiceFactory;
@Autowired
private AiProperties aiProperties;
/**
* request AI
* @param requestParam request text
* @return AI response
*/
@PostMapping(path = "/get", produces = {TEXT_EVENT_STREAM_VALUE})
@Operation(summary = "Artificial intelligence questions and Answers",
description = "Artificial intelligence questions and Answers")
public Flux<ServerSentEvent<String>> requestAi(@Parameter(description = "Request text", example = "Who are you")
@RequestBody AiControllerRequestParam requestParam) {
Assert.notNull(aiServiceFactory, "please check that your type value is consistent with the documentation on the website");
AiService aiServiceImplBean = aiServiceFactory.getAiServiceImplBean(aiProperties.getType());
return aiServiceImplBean.requestAi(requestParam.getText());
}
}
@@ -1,70 +0,0 @@
/*
* 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.manager.pojo.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* openAi Request param
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class OpenAiRequestParamDTO {
/**
* ai version
*/
private String model;
/**
* request message
*/
private List<AiMessage> messages;
/**
* The sampling temperature, which controls the randomness of the output, must be positive
* The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95.
* The larger the value, the more random and creative the output will be. The smaller the value, the more stable or certain the output will be
* You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time
*/
private float temperature;
/**
* The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 1024
*/
@JsonProperty("max_tokens")
private Integer maxTokens;
/**
* stream response
*/
private Boolean stream = Boolean.FALSE;
}
@@ -1,115 +0,0 @@
/*
* 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.manager.pojo.dto;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.codec.ServerSentEvent;
/**
* openAi Response
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class OpenAiResponse {
/**
* Task order number generated by the AI open platform. Use this order number when invoking the request result interface
*/
private String id;
/**
* The request creation time is a Unix timestamp in seconds
*/
private Long created;
/**
* response message
*/
private List<Choice> choices;
/**
* Returns the number of tokens invoked by the model at the end.
*/
private Tokens usage;
/**
* Choice
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Choice {
private int index;
private AiMessage delta;
}
/**
* Tokens
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Tokens {
/**
* The number of tokens entered by users
*/
@JsonProperty("prompt_tokens")
private Integer promptTokens;
/**
* The number of tokens that the model outputs
*/
@JsonProperty("completion_tokens")
private Integer completionTokens;
/**
* Total number of tokens
*/
@JsonProperty("total_tokens")
private Integer totalTokens;
}
public static ServerSentEvent<String> convertToResponse(String aiRes) {
try {
OpenAiResponse openAiResponse = JSON.parseObject(aiRes, OpenAiResponse.class);
if (Objects.nonNull(openAiResponse)) {
OpenAiResponse.Choice choice = openAiResponse.getChoices().get(0);
String content = choice.getDelta().getContent();
return ServerSentEvent.<String>builder()
.data(content)
.build();
}
} catch (Exception e) {
log.info("convertToResponse Exception:{}", e.toString());
throw e;
}
return ServerSentEvent.<String>builder().build();
}
}
@@ -51,6 +51,7 @@ public class TemplateConfig {
/**
* Is hide this app in main menus layout, only for app type, default true
*/
@Builder.Default
private boolean hide = true;
}
}
@@ -1,44 +0,0 @@
/*
* 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.manager.service.ai;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
/**
* AI Service
*/
public interface AiService {
/**
* get AI type
* @return type
*/
AiTypeEnum getType();
/**
* AI response
* @param text text
* @return AI response
*/
Flux<ServerSentEvent<String>> requestAi(String text);
}
@@ -1,118 +0,0 @@
/*
* 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.manager.service.ai;
import java.util.List;
import java.util.Objects;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.AiConstants;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiMessage;
import org.apache.hertzbeat.manager.pojo.dto.AliAiRequestParamDTO;
import org.apache.hertzbeat.manager.pojo.dto.AliAiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
/**
* alibaba Ai
*/
@Service("AlibabaAiServiceImpl")
@ConditionalOnProperty(prefix = "ai", name = "type", havingValue = "alibabaAi")
@Slf4j
public class AlibabaAiServiceImpl implements AiService {
@Autowired
private AiProperties aiProperties;
private WebClient webClient;
@PostConstruct
private void init() {
this.webClient = WebClient.builder()
.baseUrl(AiConstants.AliAiConstants.URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + aiProperties.getApiKey())
//sse
.defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE)
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(item -> item.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
}
@Override
public AiTypeEnum getType() {
return AiTypeEnum.alibabaAi;
}
@Override
public Flux<ServerSentEvent<String>> requestAi(String text) {
checkParam(text, aiProperties.getModel(), aiProperties.getApiKey());
AliAiRequestParamDTO aliAiRequestParamDTO = AliAiRequestParamDTO.builder()
.model(aiProperties.getModel())
.input(AliAiRequestParamDTO.Input.builder()
.messages(List.of(new AiMessage(AiConstants.AliAiConstants.REQUEST_ROLE, text)))
.build())
.parameters(AliAiRequestParamDTO.Parameters.builder()
.maxTokens(AiConstants.AliAiConstants.MAX_TOKENS)
.temperature(AiConstants.AliAiConstants.TEMPERATURE)
.enableSearch(true)
.resultFormat("message")
.incrementalOutput(true)
.build())
.build();
return webClient.post()
.body(BodyInserters.fromValue(aliAiRequestParamDTO))
.retrieve()
.bodyToFlux(AliAiResponse.class)
.map(aliAiResponse -> {
if (Objects.nonNull(aliAiResponse)) {
List<AliAiResponse.Choice> choices = aliAiResponse.getOutput().getChoices();
if (CollectionUtils.isEmpty(choices)) {
return ServerSentEvent.<String>builder().build();
}
String content = choices.get(0).getMessage().getContent();
return ServerSentEvent.<String>builder()
.data(content)
.build();
}
return ServerSentEvent.<String>builder().build();
})
.doOnError(error -> log.info("AlibabaAiServiceImpl.requestAi exception:{}", error.getMessage()));
}
private void checkParam(String param, String apiKey, String model) {
Assert.notNull(param, "text is null");
Assert.notNull(model, "model is null");
Assert.notNull(apiKey, "ai.api-key is null");
}
}
@@ -1,96 +0,0 @@
/*
* 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.manager.service.ai;
import java.util.List;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.AiConstants;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiMessage;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiRequestParamDTO;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
/**
* Kimi Ai
*/
@Service("KimiAiServiceImpl")
@ConditionalOnProperty(prefix = "ai", name = "type", havingValue = "kimiAi")
@Slf4j
public class KimiAiServiceImpl implements AiService {
@Autowired
private AiProperties aiProperties;
private WebClient webClient;
@PostConstruct
private void init() {
this.webClient = WebClient.builder()
.baseUrl(AiConstants.KimiAiConstants.URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + aiProperties.getApiKey())
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(item -> item.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
}
@Override
public AiTypeEnum getType() {
return AiTypeEnum.kimiAi;
}
@Override
public Flux<ServerSentEvent<String>> requestAi(String text) {
checkParam(text, aiProperties.getModel(), aiProperties.getApiKey());
OpenAiRequestParamDTO zhiPuRequestParamDTO = OpenAiRequestParamDTO.builder()
.model(aiProperties.getModel())
.stream(Boolean.TRUE)
.maxTokens(AiConstants.KimiAiConstants.MAX_TOKENS)
.temperature(AiConstants.KimiAiConstants.TEMPERATURE)
.messages(List.of(new AiMessage(AiConstants.KimiAiConstants.REQUEST_ROLE, text)))
.build();
return webClient.post()
.body(BodyInserters.fromValue(zhiPuRequestParamDTO))
.retrieve()
.bodyToFlux(String.class)
.filter(aiResponse -> !"[DONE]".equals(aiResponse))
.map(OpenAiResponse::convertToResponse)
.doOnError(error -> log.info("KimiAiServiceImpl.requestAi exception:{}", error.getMessage()));
}
private void checkParam(String param, String model, String apiKey) {
Assert.notNull(param, "text is null");
Assert.notNull(model, "model is null");
Assert.notNull(apiKey, "ai.api-key is null");
}
}
@@ -1,95 +0,0 @@
/*
* 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.manager.service.ai;
import io.jsonwebtoken.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.AiConstants;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiMessage;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiRequestParamDTO;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* Ollama service
*/
@Service("OllamaServiceImpl")
@ConditionalOnProperty(prefix = "ai", name = "type", havingValue = "ollama")
@Slf4j
public class OllamaAiService implements AiService{
@Autowired
private AiProperties aiProperties;
private WebClient webClient;
@PostConstruct
private void init() {
Assert.notNull(aiProperties.getApiUrl(), "Ollama API URL is null");
this.webClient = WebClient.builder()
.baseUrl(aiProperties.getApiUrl())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(item -> item.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
}
@Override
public AiTypeEnum getType() {
return AiTypeEnum.ollama;
}
@Override
public Flux<ServerSentEvent<String>> requestAi(String text) {
checkParam(text, aiProperties.getModel());
OpenAiRequestParamDTO ollamaParam = OpenAiRequestParamDTO.builder()
.model(aiProperties.getModel())
.stream(Boolean.TRUE)
.maxTokens(AiConstants.OllamaConstants.MAX_TOKENS)
.temperature(AiConstants.OllamaConstants.TEMPERATURE)
.messages(List.of(new AiMessage(AiConstants.OllamaConstants.REQUEST_ROLE, text)))
.build();
return webClient.post()
.body(BodyInserters.fromValue(ollamaParam))
.retrieve()
.bodyToFlux(String.class)
.filter(aiResponse -> !"[DONE]".equals(aiResponse))
.map(OpenAiResponse::convertToResponse)
.doOnError(error -> log.info("OllamaAiService.requestAi exception:{}", error.getMessage()));
}
private void checkParam(String param, String model) {
Assert.notNull(param, "text is null");
Assert.notNull(model, "model is null");
}
}
@@ -1,97 +0,0 @@
/*
* 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.manager.service.ai;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.AiConstants;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiMessage;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiRequestParamDTO;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* OpenRouter service
*/
@Service("OpenRouterServiceImpl")
@ConditionalOnProperty(prefix = "ai", name = "type", havingValue = "openRouter")
@Slf4j
public class OpenRouterServiceImpl implements AiService {
@Autowired
private AiProperties aiProperties;
private WebClient webClient;
@PostConstruct
private void init() {
this.webClient = WebClient.builder()
.baseUrl(AiConstants.OpenRouterConstants.URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + aiProperties.getApiKey())
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(item -> item.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
}
@Override
public AiTypeEnum getType() {
return AiTypeEnum.openRouter;
}
@Override
public Flux<ServerSentEvent<String>> requestAi(String text) {
checkParam(text, aiProperties.getModel(), aiProperties.getApiKey());
OpenAiRequestParamDTO openRouterParam = OpenAiRequestParamDTO.builder()
.model(aiProperties.getModel())
.stream(Boolean.TRUE)
.maxTokens(AiConstants.OpenRouterConstants.MAX_TOKENS)
.temperature(AiConstants.OpenRouterConstants.TEMPERATURE)
.messages(List.of(new AiMessage(AiConstants.OpenRouterConstants.REQUEST_ROLE, text)))
.build();
return webClient.post()
.body(BodyInserters.fromValue(openRouterParam))
.retrieve()
.bodyToFlux(String.class)
.filter(aiResponse -> !"[DONE]".equals(aiResponse))
.map(OpenAiResponse::convertToResponse)
.doOnError(error -> log.info("OpenRouterAiServiceImpl.requestAi exception:{}", error.getMessage()));
}
private void checkParam(String param, String model, String apiKey) {
Assert.notNull(param, "text is null");
Assert.notNull(model, "model is null");
Assert.notNull(apiKey, "ai.api-key is null");
}
}
@@ -1,104 +0,0 @@
/*
* 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.manager.service.ai;
import java.util.List;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.AiConstants;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiMessage;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiRequestParamDTO;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
/**
* sparkDesk AI
*/
@Service("SparkDeskAiServiceImpl")
@ConditionalOnProperty(prefix = "ai", name = "type", havingValue = "sparkDesk")
@Slf4j
public class SparkDeskAiServiceImpl implements AiService {
@Autowired
private AiProperties aiProperties;
private WebClient webClient;
@PostConstruct
private void init() {
StringBuilder sb = new StringBuilder();
String bearer = sb.append("Bearer ")
.append(aiProperties.getApiKey())
.append(":").append(aiProperties.getApiSecret()).toString();
this.webClient = WebClient.builder()
.baseUrl(AiConstants.SparkDeskConstants.SPARK_ULTRA_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.AUTHORIZATION, bearer)
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(item -> item.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
}
@Override
public AiTypeEnum getType() {
return AiTypeEnum.sparkDesk;
}
@Override
public Flux<ServerSentEvent<String>> requestAi(String text) {
checkParam(text, aiProperties.getApiKey(), aiProperties.getModel());
OpenAiRequestParamDTO zhiPuRequestParamDTO = OpenAiRequestParamDTO.builder()
.model(aiProperties.getModel())
//sse
.stream(Boolean.TRUE)
.maxTokens(AiConstants.SparkDeskConstants.MAX_TOKENS)
.temperature(AiConstants.SparkDeskConstants.TEMPERATURE)
.messages(List.of(new AiMessage(AiConstants.SparkDeskConstants.REQUEST_ROLE, text)))
.build();
return webClient.post()
.body(BodyInserters.fromValue(zhiPuRequestParamDTO))
.retrieve()
.bodyToFlux(String.class)
.filter(aiResponse -> !"[DONE]".equals(aiResponse))
.map(OpenAiResponse::convertToResponse)
.doOnError(error -> log.info("SparkDeskAiServiceImpl.requestAi exception:{}", error.getMessage()));
}
private void checkParam(String param, String apiKey, String model) {
Assert.notNull(param, "text is null");
Assert.notNull(model, "model is null");
Assert.notNull(apiKey, "ai.api-key is null");
}
}
@@ -1,100 +0,0 @@
/*
* 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.manager.service.ai;
import java.util.List;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.AiConstants;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.pojo.dto.AiMessage;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiRequestParamDTO;
import org.apache.hertzbeat.manager.pojo.dto.OpenAiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
/**
* ZhiPu AI
*/
@Service("ZhiPuServiceImpl")
@ConditionalOnProperty(prefix = "ai", name = "type", havingValue = "zhiPu")
@Slf4j
public class ZhiPuServiceImpl implements AiService {
@Autowired
private AiProperties aiProperties;
private WebClient webClient;
@PostConstruct
private void init() {
this.webClient = WebClient.builder()
.baseUrl(AiConstants.ZhiPuConstants.URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + aiProperties.getApiKey())
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(item -> item.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
}
@Override
public AiTypeEnum getType() {
return AiTypeEnum.zhiPu;
}
@Override
public Flux<ServerSentEvent<String>> requestAi(String text) {
checkParam(text, aiProperties.getModel(), aiProperties.getApiKey());
OpenAiRequestParamDTO zhiPuRequestParamDTO = OpenAiRequestParamDTO.builder()
.model(aiProperties.getModel())
//sse
.stream(Boolean.TRUE)
.maxTokens(AiConstants.ZhiPuConstants.MAX_TOKENS)
.temperature(AiConstants.ZhiPuConstants.TEMPERATURE)
.messages(List.of(new AiMessage(AiConstants.ZhiPuConstants.REQUEST_ROLE, text)))
.build();
return webClient.post()
.body(BodyInserters.fromValue(zhiPuRequestParamDTO))
.retrieve()
.bodyToFlux(String.class)
.filter(aiResponse -> !"[DONE]".equals(aiResponse))
.map(OpenAiResponse::convertToResponse)
.doOnError(error -> log.info("ZhiPuServiceImpl.requestAi exception:{}", error.getMessage()));
}
private void checkParam(String param, String model, String apiKey) {
Assert.notNull(param, "text is null");
Assert.notNull(model, "model is null");
Assert.notNull(apiKey, "ai.api-key is null");
}
}
@@ -1,64 +0,0 @@
/*
* 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.manager.service.ai.factory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.service.ai.AiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* AI bean factory
*/
@Component
@ConditionalOnProperty(prefix = "ai", name = "type")
public class AiServiceFactoryImpl {
@Autowired(required = false)
private List<AiService> aiService;
private Map<AiTypeEnum, AiService> aiServiceFactoryMap = new HashMap<>();
@PostConstruct
public void init() {
if (CollectionUtils.isEmpty(aiService)) {
return;
}
aiServiceFactoryMap = aiService.stream()
.collect(Collectors.toMap(AiService::getType, Function.identity()));
}
public AiService getAiServiceImplBean(String type) {
Assert.notNull(type, "type is null");
AiTypeEnum typeByName = AiTypeEnum.getTypeByName(type);
Assert.notNull(typeByName, "The current type is not supported,please check that your type value is consistent with the documentation on the website");
AiService aiServiceImpl = aiServiceFactoryMap.get(typeByName);
Assert.notNull(aiServiceImpl, "No bean for current type found");
return aiServiceImpl;
}
}
@@ -22,7 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Type;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.springframework.stereotype.Service;
/**
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.manager.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Type;
import org.apache.hertzbeat.ai.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
@@ -22,9 +22,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Type;
import jakarta.annotation.Resource;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@@ -1,88 +0,0 @@
/*
* 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.manager.controller;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.apache.hertzbeat.manager.config.AiProperties;
import org.apache.hertzbeat.manager.service.ai.AiService;
import org.apache.hertzbeat.manager.service.ai.factory.AiServiceFactoryImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import reactor.core.publisher.Flux;
/**
* test case for {@link AiController}
*/
@ExtendWith(MockitoExtension.class)
class AiControllerTest {
private MockMvc mockMvc;
@Mock
private AiServiceFactoryImpl aiServiceFactory;
@Mock
private AiProperties aiProperties;
@Mock
private AiService aiService;
@InjectMocks
private AiController aiController;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.standaloneSetup(aiController).build();
}
@Test
public void testRequestAi() throws Exception {
String responseText = "response";
Flux<ServerSentEvent<String>> responseFlux = Flux.just(ServerSentEvent.builder(responseText).build());
when(aiServiceFactory.getAiServiceImplBean(anyString())).thenReturn(aiService);
when(aiService.requestAi(anyString())).thenReturn(responseFlux);
when(aiProperties.getType()).thenReturn("alibabaAi");
String requestBody = "{\"text\":\"Who are you\"}";
mockMvc.perform((MockMvcRequestBuilders.post("/api/ai/get")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_EVENT_STREAM_VALUE))
.andExpect(content().string("data:response\n\n"));
}
}
@@ -252,4 +252,4 @@ class MetricsFavoriteControllerTest {
verify(metricsFavoriteService, never()).addMetricsFavorite(anyString(), anyLong(), anyString());
}
}
}
}
@@ -1,125 +0,0 @@
/*
* 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.manager.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.hertzbeat.common.constants.AiTypeEnum;
import org.apache.hertzbeat.manager.service.ai.AiService;
import org.apache.hertzbeat.manager.service.ai.factory.AiServiceFactoryImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
/**
* test case for {@link AiServiceFactoryImpl}
*/
@ExtendWith(MockitoExtension.class)
class AiServiceFactoryTest {
@Mock
private List<AiService> aiService;
@Mock
private AiService aiService1;
@Mock
private AiService aiService2;
@InjectMocks
private AiServiceFactoryImpl aiServiceFactory;
@BeforeEach
public void setup() {
when(aiService1.getType()).thenReturn(AiTypeEnum.alibabaAi);
when(aiService2.getType()).thenReturn(AiTypeEnum.zhiPu);
aiService = Arrays.asList(aiService1, aiService2);
ReflectionTestUtils.setField(aiServiceFactory, "aiService", aiService);
aiServiceFactory.init();
}
@Test
public void testInit() {
Map<AiTypeEnum, AiService> expectedMap = aiService.stream()
.collect(Collectors.toMap(AiService::getType, Function.identity()));
Map<AiTypeEnum, AiService> actualMap = (Map<AiTypeEnum, AiService>) ReflectionTestUtils.getField(aiServiceFactory, "aiServiceFactoryMap");
assertEquals(expectedMap, actualMap);
}
@Test
public void testGetAiServiceImplBean_Success() {
AiService service = aiServiceFactory.getAiServiceImplBean(AiTypeEnum.alibabaAi + "");
assertNotNull(service);
assertEquals(aiService1, service);
service = aiServiceFactory.getAiServiceImplBean(AiTypeEnum.zhiPu + "");
assertNotNull(service);
assertEquals(aiService2, service);
}
@Test
public void testGetAiServiceImplBeanTypeNotFound() {
Exception exception = assertThrows(
IllegalArgumentException.class,
() -> aiServiceFactory.getAiServiceImplBean("InvalidType")
);
assertTrue(exception.getMessage().contains("The current type is not supported"));
}
@Test
public void testGetAiServiceImplBeanNoBean() {
aiServiceFactory.init();
when(aiService1.getType()).thenReturn(AiTypeEnum.kimiAi);
List<AiService> singleServiceList = Collections.singletonList(aiService1);
ReflectionTestUtils.setField(aiServiceFactory, "aiService", singleServiceList);
aiServiceFactory.init();
Exception exception = assertThrows(
IllegalArgumentException.class,
() -> aiServiceFactory.getAiServiceImplBean(AiTypeEnum.sparkDesk + "")
);
assertTrue(exception.getMessage().contains("No bean for current type found"));
}
}
@@ -27,7 +27,7 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.hertzbeat.base.service.GeneralConfigService;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.manager.pojo.dto.ObjectStoreDTO;
import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig;
import org.apache.hertzbeat.manager.service.impl.ConfigServiceImpl;
@@ -1,28 +0,0 @@
/*
* 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.manager.service;
import org.apache.hertzbeat.manager.service.ai.KimiAiServiceImpl;
/**
* test case for {@link KimiAiServiceImpl}
*/
class KimiAiServiceTest {
}
@@ -22,7 +22,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.manager.service.impl.MailGeneralConfigServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -21,9 +21,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.manager.service.impl.SmsGeneralConfigServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -1,27 +0,0 @@
/*
* 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.manager.service;
import org.apache.hertzbeat.manager.service.ai.SparkDeskAiServiceImpl;
/**
* test case for {@link SparkDeskAiServiceImpl}
*/
class SparkDeskAiServiceTest {
}
+6
View File
@@ -87,6 +87,12 @@
<artifactId>hertzbeat-remoting</artifactId>
</dependency>
<!-- HertzBeat ai -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-ai</artifactId>
</dependency>
<!-- Jackson -->
<dependency>
+3 -3
View File
@@ -60,9 +60,9 @@ This command generates static content into the `build` directory and can be serv
## Deployment
```console
GIT_USER=<Your GitHub username> USE_SSH=true pnpm deploy
GIT_USER=<Your GitHub username> USE_SSH=true pnpm run deploy
OR
USE_SSH=true pnpm deploy
USE_SSH=true pnpm run deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
@@ -70,7 +70,7 @@ If you are using GitHub pages for hosting, this command is a convenient way to b
### Deployment to Gitee
```console
GITHUB_HOST=gitee.com USE_SSH=true pnpm deploy
GITHUB_HOST=gitee.com USE_SSH=true pnpm run deploy
```
### Archive Version
@@ -1,15 +1,15 @@
---
id: ai_agent_chat
title: AI Agent Chat User Guide
sidebar_label: AI Agent Chat
id: ai_agent
title: HertzBeat AI Agent User Guide
sidebar_label: HertzBeat AI
keywords: [AI, Chat, Agent, Monitoring, AI Agent, OpenAI]
---
> HertzBeat AI Agent Chat is an intelligent monitoring AI Agent that helps you manage monitors, configure alerts, and optimize your infrastructure monitoring through natural language conversation.
> HertzBeat AI Agent is an intelligent monitoring AI Agent that helps you manage monitors, configure alerts, and optimize your infrastructure monitoring through natural language conversation.
## Overview
The AI Agent Chat feature provides an interactive chat interface where you can:
The HertzBeat AI Agent feature provides an interactive chat interface where you can:
**Monitor Management:**
@@ -40,7 +40,7 @@ The AI Agent Chat feature provides an interactive chat interface where you can:
## Prerequisites
Before using the AI Agent Chat, ensure:
Before using the AI Agent, ensure:
1 **OpenAI Configuration**: Valid OpenAI API key must be configured. Optionally, you can change the model in `application.yml` under `spring.ai.openai.chat.options.model` (default is `gpt-4.1-nano`).
2 **Database Connection**: HertzBeat database must be accessible for monitor operations
@@ -49,37 +49,26 @@ Before using the AI Agent Chat, ensure:
### OpenAI API Key Setup
The AI Agent Chat uses OpenAI's GPT models. You need to configure an OpenAI API key in one of two ways:
The AI Agent uses OpenAI's GPT models. You need to configure an OpenAI API key in this ways:
#### Method 1: Database Configuration via UI (Recommended)
#### Database Configuration via UI
1. Navigate to the AI Agent Chat interface
1. Navigate to the AI Agent interface
2. If no API key is configured, you'll see a configuration dialog
3. Enter your OpenAI API key
4. Click **Save Configuration**
#### Method 2: YAML Configuration
Add to your `application.yml`:
```yaml
spring:
ai:
openai:
api-key: sk-your-openai-api-key-here
```
### Configuration Priority
- **Database configuration** takes priority over YAML configuration
- Configuration changes are applied immediately without restart
- Invalid API keys are automatically validated before saving
## Using the AI Agent Chat
## Using the AI Agent
### Accessing the Chat Interface
1. Navigate to the **AI Agent Chat** section in HertzBeat web interface
1. Navigate to the **AI Agent** section in HertzBeat web interface
2. You'll see the HertzBeat Monitoring AI Agent welcome screen
3. Start typing your questions in the chat input field
@@ -361,7 +350,7 @@ You: Add HTTP monitors for all services in my staging environment:
## Support
For issues with AI Agent Chat:
For issues with AI Agent:
1. Check this documentation first
2. Review application logs for errors
-81
View File
@@ -1,81 +0,0 @@
---
id: ai_config
title: AI QuickStart
sidebar_label: AI QuickStart
keywords: [AI]
---
> The dialogue with artificial intelligence is achieved by configuring aiConfig in the application.yml file
### Configuration parameter description
| Name of the parameter | Parameter help description |
|-----------------------|-----------------------------------------------------------------------|
| type | Choose a large AI model (such as Zhipu, Tongyi thousand questions...) |
| model | Select the model, which defaults to GLM-4 |
| api-key | Gets the api_key, without which you cannot talk to the large model |
### Large model options and configuration details
#### ZhiPu AI
| Name of the parameter | Example | Link |
|-----------------------|-----------------------------------------------------|-----------------------------------------------------------------|
| type | zhiPu (must be exactly the same as example) | |
| model | glm-4-0520、glm-4 、glm-4-air、glm-4-airx、 glm-4-flash | |
| api-key | xxxxx.xxxxxx | <https://open.bigmodel.cn/login?redirect=%2Fusercenter%2Fapikeys> |
#### Alibaba AI
| Name of the parameter | Example | Link |
|-----------------------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| type | alibabaAi (must be exactly the same as example) | |
| model | qwen-turbo、qwen-plus、qwen-max、qwen-max-0428、qwen-max-0403、qwen-max-0107、qwen-max-longcontext | <https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction?spm=a2c4g.11186623.0.0.4e0246c1RQFKMH> |
| api-key | xxxxxxxxxxx | <https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key?spm=a2c4g.11186623.0.i10> |
#### Kimi AI
| Name of the parameter | Example | Link |
|-----------------------|-------------------------------------------------|-----------------------------------------------|
| type | kimiAi (must be exactly the same as example) | |
| model | moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k | |
| api-key | xxxxxxxxxxx | <https://platform.moonshot.cn/console/api-keys> |
#### sparkDesk AI
QuickStart: <https://www.xfyun.cn/doc/platform/quickguide.html>
| Name of the parameter | Example | Link |
|-----------------------|--------------------------------------------------|---------------------------------------|
| type | sparkDesk (must be exactly the same as example) | |
| model | general、generalv2、generalv3、generalv3.5、4.0Ultra | |
| api-key | xxxxxxxxxxx | <https://console.xfyun.cn/services/cbm> |
| api-secret | xxxxxxxxxxx | <https://console.xfyun.cn/services/cbm> |
| sparkDesk version | model |
|-------------------|-------------|
| Spark4.0 Ultra | 4.0Ultra |
| Spark Max | generalv3.5 |
| Spark Pro | generalv3 |
| Spark V2.0 | generalv2 |
| Spark Lite(free) | general |
#### Ollama AI
QuickStart: <https://github.com/ollama/ollama/tree/main/docs>
| Name of the parameter | Example | Link |
|-----------------------|-------------------------------------------------|-------------------------------|
| type | ollama (must be exactly the same as example) | |
| model | deepseek-r1:latest、qwen3:latest、llama4:16x17b | <https://ollama.com/search> |
| api-url | <http://127.0.0.1:11434/v1/chat/completions> | |
#### OpenRouter
QuickStart: <https://openrouter.ai/docs/quickstart>
| Name of the parameter | Example | Link |
|-----------------------|------------------------------------------------|-----------------------------------------|
| type | openRouter (must be exactly the same as example) | |
| model | openai/gpt-4o, anthropic/claude-sonnet-4 | <https://openrouter.ai/models> |
| api-key | xxxxxxxxxxx | <https://openrouter.ai/settings/provisioning-keys> |
+4 -2
View File
@@ -1,7 +1,7 @@
---
id: mcp_sse_server
title: MCP SSE Server
sidebar_label: MCP SSE Server
title: MCP Server
sidebar_label: MCP Server
keywords: [MCP, SSE, streaming, server]
---
@@ -26,6 +26,8 @@ You must authenticate each request using one of the following methods:
- Header: `Authorization: Bearer <your-jwt-token>`
Note: You can generate a JWT token from the HertzBeat web UI under the Log Integration or Alarm Integration Page.
- Basic authentication
- Header: `Authorization: Basic <base64(username:password)>`
@@ -0,0 +1,357 @@
---
id: ai_agent
title: HertzBeat AI 智能助手用户指南
sidebar_label: HertzBeat AI
keywords: [AI, 聊天, 智能助手, 监控, AI Agent, OpenAI]
---
> HertzBeat AI 智能助手是一个智能监控 AI 助手,通过自然语言对话帮助您管理监控器、配置告警并优化基础设施监控。
## 概述
HertzBeat AI 智能助手功能提供了一个交互式聊天界面,您可以:
**监控器管理:**
- 🔍 按状态、类型、主机和标签查询和过滤现有监控器
- ➕ 为网站、API、数据库和服务添加新的监控器
- 📊 获取可用监控器类型及其参数的详细信息
- ⚡ 检查监控器状态并排除监控问题
**告警管理:**
- 🚨 通过综合过滤(类型、状态、搜索)查询活跃告警
- 📈 获取告警摘要统计和分布
- 🔔 查看单个和分组告警
- 📋 分析告警模式和趋势
**指标分析:**
- 📊 检索任何监控器的实时指标数据
- 📈 访问可自定义时间范围的历史指标
- 💾 检查仓库存储系统状态
- 🔍 查询特定指标字段和标签
**告警规则管理:**
- ⚙️ 配置告警规则和阈值
- 📝 管理不同监控器类型的告警定义
- 🎯 设置自定义告警条件
## 先决条件
在使用 AI 智能助手之前,请确保:
1. **OpenAI 配置**:必须配置有效的 OpenAI API 密钥。可选地,您可以在 `application.yml` 中的 `spring.ai.openai.chat.options.model` 下更改模型(默认为 `gpt-4.1-nano`)。
2. **数据库连接**:HertzBeat 数据库必须可访问以进行监控器操作
## 配置
### OpenAI API 密钥设置
AI 智能助手使用 OpenAI 的 GPT 模型。您需要通过以下方式配置 OpenAI API 密钥:
#### 通过 UI 进行数据库配置
1. 导航到 AI 智能助手界面
2. 如果未配置 API 密钥,您将看到配置对话框
3. 输入您的 OpenAI API 密钥
4. 点击 **保存配置**
### 配置优先级
- **数据库配置** 优先于 YAML 配置
- 配置更改立即生效,无需重启
- 保存前会自动验证无效的 API 密钥
## 使用 AI 智能助手
### 访问聊天界面
1. 在 HertzBeat Web 界面中导航到 **AI 智能助手** 部分
2. 您将看到 HertzBeat 监控 AI 助手欢迎屏幕
3. 在聊天输入框中开始输入您的问题
### 示例对话
#### 添加新监控器
```text
您:为我的网站 https://example.com 添加一个新的 HTTP 监控器
AI 助手:我将帮助您为 https://example.com 添加一个 HTTP 监控器。
让我使用以下配置创建该监控器:
- 名称:example.com 网站监控器
- 类型:HTTP
- 主机:example.com
- 端口:443 (HTTPS)
- 路径:/
- 间隔:60 秒
监控器创建成功,ID12345
```
#### 列出现有监控器
```text
您:显示我所有当前的监控器
AI 助手:以下是您当前的监控器:
1. **Linux 服务器** (ID: 1001)
- 状态:✅ 在线
- 主机:192.168.1.100
- 最后检查:2 分钟前
2. **MySQL 数据库** (ID: 1002)
- 状态:⚠️ 警告
- 主机:db.example.com
- 最后检查:30 秒前
3. **网站监控器** (ID: 1003)
- 状态:✅ 在线
- 主机:https://example.com
- 最后检查:1 分钟前
您想了解任何特定监控器的详细信息吗?
```
#### 获取监控器类型信息
```text
您:监控 PostgreSQL 数据库需要哪些参数?
AI 助手:要监控 PostgreSQL 数据库,您需要这些参数:
**必需参数:**
- **主机**:数据库服务器 IP/主机名
- **端口**PostgreSQL 端口(默认:5432
- **数据库**:要连接的数据库名称
- **用户名**:数据库用户名
- **密码**:数据库密码
**可选参数:**
- **超时**:连接超时(默认:6000ms)
- **查询超时**:SQL 查询超时(默认:3000ms
- **SSL**:启用 SSL 连接(默认:false)
您想让我帮助您添加一个 PostgreSQL 监控器吗?
```
#### 告警管理
```text
您:显示过去 24 小时内的所有严重告警
AI 助手:以下是过去 24 小时内的严重告警:
🚨 **活跃告警(3 个触发中)**
1. **高 CPU 使用率** - Linux 服务器 (ID: 1001)
- 状态:🔥 触发中
- 触发时间:2 小时前
- 严重程度:严重
- 消息:CPU 使用率 95% > 阈值 90%
2. **数据库连接失败** - MySQL 数据库 (ID: 1002)
- 状态:🔥 触发中
- 触发时间:45 分钟前
- 严重程度:严重
- 消息:10 秒后连接超时
3. **网站响应时间** - 网站监控器 (ID: 1003)
- 状态:⚠️ 警告
- 触发时间:15 分钟前
- 严重程度:警告
- 消息:响应时间 5.2s > 阈值 3s
**告警摘要:**
- 总告警数:12
- 触发中:3(2 个严重,1 个警告)
- 已解决:9
您想让我帮助调查任何特定的告警吗?
```
#### 指标分析
```text
您:显示我的 Linux 服务器过去 6 小时的 CPU 使用率
AI 助手:以下是您的 Linux 服务器(监控器 ID:1001)过去 6 小时的 CPU 使用率数据:
📊 **CPU 使用率指标 - 过去 6 小时**
时间 | 使用率 (%) | 负载平均 | 进程数
14:00-15:00 | 45.2% | 1.2 | 156
15:00-16:00 | 67.8% | 2.1 | 178
16:00-17:00 | 89.4% | 3.8 | 195
17:00-18:00 | 92.1% | 4.2 | 203
18:00-19:00 | 94.7% | 4.8 | 210
19:00-20:00 | 88.3% | 3.5 | 188
**分析:**
- 峰值使用率:19:00 时 94.7%
- 平均使用率:79.6%
- 趋势:高使用率持续 3+ 小时
- 当前状态:下降中(88.3%
**实时指标:**
- 当前 CPU76.2%
- 内存使用率:82.1%
- 磁盘 I/O:正常
您想让我检查历史数据进行比较或调查高使用率的原因吗?
```
#### 告警规则配置
```text
您:当我的网站响应时间超过 5 秒时设置告警规则
AI 助手:我将帮助您创建网站响应时间监控的告警规则。
**告警规则配置:**
- **监控器类型**HTTP/网站
- **指标**:响应时间
- **条件**:大于 5 秒
- **严重程度**:警告
- **评估**:每 60 秒检查一次
- **通知**:邮件 + 仪表板
**建议规则:**
```yaml
name: "网站响应时间告警"
condition: "response_time > 5000ms"
severity: "warning"
description: "当网站响应时间超过 5 秒时告警"
evaluation_interval: 60s
for: 2m # 条件持续 2 分钟后告警
```
此规则将:
1. 每分钟监控响应时间
2. 如果响应时间 > 5s 持续 2+ 分钟则触发告警
3. 通过配置的渠道发送通知
4. 在仪表板中显示警告状态
您想让我应用此配置还是修改任何设置?
``` text
### 聊天功能
#### 消息历史
- 所有对话都会自动保存
- 从侧边栏访问以前的对话
- 搜索对话历史
- 导出对话日志
#### 实时响应
- 流式响应提供即时反馈
- 输入指示器显示助手正在处理时
- 如需要可取消正在进行的请求
#### 对话管理
- 为不同主题创建新对话
- 重命名对话以便更好地组织
- 删除旧对话以保持整洁
注意:对话不会保存在数据库中
## 故障排除
### 常见问题
#### 聊天界面无法加载
**症状**:聊天界面无限显示加载旋转器
**解决方案**
1. 检查浏览器控制台中的 JavaScript 错误
2. 确保与 HertzBeat 服务器的网络连接
#### "服务不可用"消息
**症状**:聊天显示"HertzBeat AI 监控服务不可用"
**解决方案**
1. 验证 OpenAI API 密钥配置
2. 检查应用程序日志中的错误
3. 确保数据库连接
#### 无效 API 密钥错误
**症状**:配置对话框显示"无效 API 密钥"错误
**解决方案**
1. 验证您的 OpenAI API 密钥以 `sk-` 开头
2. 检查 API 密钥是否有足够的积分/配额
3. 直接使用 OpenAI API 测试 API 密钥
4. 确保 API 密钥中没有多余的空格
#### 监控器创建失败
**症状**:AI 建议监控器配置但创建失败
**解决方案**
1. 验证您有创建监控器的权限
2. 检查是否已存在同名监控器
3. 确保目标主机/服务可访问
4. 查看监控器参数验证错误
### 调试模式
通过设置日志级别为 DEBUG 来启用调试日志:
```yaml
logging:
level:
org.apache.hertzbeat.ai.agent: DEBUG
```
## 最佳实践
### 有效的聊天使用
1. **具体明确**"为 api.example.com 端口 8080 添加 HTTP 监控器" vs "添加一个监控器"
2. **提供上下文**:说明您想要生产环境还是测试环境监控器
3. **询问后续问题**:如需要请求配置详细信息
4. **使用自然语言**:AI 理解对话式请求
### 监控器管理
1. **命名约定**:使用描述性的监控器名称
2. **文档记录**:要求 AI 记录复杂配置
### 安全考虑
1. **API 密钥安全**:安全存储 OpenAI API 密钥
2. **访问控制**:限制 AI 聊天访问给授权用户
3. **数据隐私**:注意聊天日志中的敏感信息
4. **网络安全**:确保与 OpenAI API 的安全连接
## 高级功能
### 批量操作
```text
您:为我的预发布环境中的所有服务添加 HTTP 监控器:
- api-staging.example.com:8080
- web-staging.example.com:80
- admin-staging.example.com:3000
```
## 限制
- 需要活跃的互联网连接访问 OpenAI API
- OpenAI API 使用会根据令牌消耗产生费用
- 复杂的多步骤操作可能需要多次交互
- 某些高级配置可能需要手动设置
- 可能会根据 OpenAI 计划应用速率限制
## 支持
对于 AI 智能助手的问题:
1. 首先查看此文档
2. 查看应用程序日志中的错误
3. 独立测试 OpenAI API 连接
4. 联系 HertzBeat 支持并提供具体错误消息
---
@@ -1,81 +0,0 @@
---
id: ai_config
title: AI 快速入门
sidebar_label: AI 快速入门
keywords: [人工智能 AI]
---
> 通过配置application.yml文件里面的aiConfig实现与人工智能的对话
### 配置参数说明
| 参数名称 | 参数帮助描述 |
|---------|------------------------------|
| type | 选择AI大模型(如智普、通义千问...) |
| model | 选择模型,默认为GLM-4 |
| api-key | 获取api_key,如果没有该配置,无法与大模型进行对话 |
### 大模型选项与配置详解
#### 智普AI
| 参数名称 | 示例 | 链接 |
|---------|-----------------------------------------------------|-----------------------------------------------------------------|
| type | zhiPu(必须和示例完全相同) | 无 |
| model | glm-4-0520、glm-4 、glm-4-air、glm-4-airx、 glm-4-flash | 无 |
| api-key | xxxxx.xxxxxx | <https://open.bigmodel.cn/login?redirect=%2Fusercenter%2Fapikeys> |
#### 阿里巴巴AI
| 参数名称 | 示例 | 链接 |
|---------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| type | alibabaAi(必须和示例完全相同) | 无 |
| model | qwen-turbo、qwen-plus、qwen-max、qwen-max-0428、qwen-max-0403、qwen-max-0107、qwen-max-longcontext | <https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction?spm=a2c4g.11186623.0.0.4e0246c1RQFKMH> |
| api-key | xxxxxxxxxxx | <https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key?spm=a2c4g.11186623.0.i10> |
#### 月之暗面AI
| 参数名称 | 示例 | 链接 |
|---------|-------------------------------------------------|-----------------------------------------------|
| type | kimiAi(必须和示例完全相同) | 无 |
| model | moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k | 无 |
| api-key | xxxxxxxxxxx | <https://platform.moonshot.cn/console/api-keys> |
#### 科大讯飞AI
快速入门:<https://www.xfyun.cn/doc/platform/quickguide.html>
| 参数名称 | 示例 | 链接 |
|------------|--------------------------------------------------|---------------------------------------|
| type | sparkDesk (must be exactly the same as example) | |
| model | general、generalv2、generalv3、generalv3.5、4.0Ultra | |
| api-key | xxxxxxxxxxx | <https://console.xfyun.cn/services/cbm> |
| api-secret | xxxxxxxxxxx | <https://console.xfyun.cn/services/cbm> |
| 模型版本 | 模型类型(application.yml的model参数) |
|-----------------|-------------------------------|
| Spark4.0 Ultra | 4.0Ultra |
| Spark Max | generalv3.5 |
| Spark Pro | generalv3 |
| Spark V2.0 | generalv2 |
| Spark Lite(免费版) | general |
#### Ollama AI
快速入门: <https://github.com/ollama/ollama/tree/main/docs>
| 参数名称 | 示例 | 链接 |
|-----------------------|--------------------------------------------------|---------------------------------------|
| type | ollama (必须和示例完全相同) | |
| model | deepseek-r1:latest、qwen3:latest、llama4:16x17b | <https://ollama.com/search> |
| api-url | <http://127.0.0.1:11434/v1/chat/completions> | |
#### OpenRouter
快速入门: <https://openrouter.ai/docs/quickstart>
| 参数名称 | 示例 | 链接 |
|---------|------------------------------------------|----------------------------------------------------|
| type | openRouter (必须和示例完全相同) | |
| model | openai/gpt-4o, anthropic/claude-sonnet-4 | <https://openrouter.ai/models> |
| api-key | xxxxxxxxxxx | <https://openrouter.ai/settings/provisioning-keys> |
@@ -0,0 +1,97 @@
---
id: mcp_sse_server
title: MCP 服务器
sidebar_label: MCP 服务器
keywords: [MCP, SSE, 流式传输, 服务器]
---
本页面介绍如何连接到 HertzBeat MCP SSE 服务器。当您启动 HertzBeat 服务器时,MCP 服务器会自动在默认端口 1157 上启动。
### 概述
- 提供用于工具调用的服务器发送事件 (SSE) 流。
- 专为 MCP 集成和消费流式事件的客户端而设计。
### 连接到 MCP 服务器
确保 HertzBeat 服务器已启动并正在运行。如果您使用的端口不是 1157,请相应地替换以下内容:
- URL: `http://localhost:1157/api/sse`
### 身份验证
您必须使用以下方法之一对每个请求进行身份验证:
- JWT Bearer 令牌
- 请求头: `Authorization: Bearer <your-jwt-token>`
注意:您可以在 HertzBeat Web UI 的日志集成或告警集成页面点击生成 JWT 令牌。
- 基本身份验证
- 请求头: `Authorization: Basic <base64(username:password)>`
### Cursor MCP 配置
在您的主目录或项目根目录中创建或编辑 `.cursor/mcp.json`
基本身份验证:
```json
{
"Hertzbeat-MCP": {
"url": "http://localhost:1157/api/sse",
"headers": {
"Authorization": "Basic <base64(username:password)>"
}
}
}
```
JWT Bearer
```json
{
"Hertzbeat-MCP": {
"url": "http://localhost:1157/api/sse",
"headers": {
"Authorization": "Bearer <your-jwt-token>"
}
}
}
```
保存后,在 Cursor 中重新加载 MCP 或重启编辑器。
### 可用工具
#### 监控管理工具
- **query_monitors**: 查询现有/已配置的监控器,支持全面的过滤、分页和状态概览。支持按 ID、类型、状态、主机、标签进行过滤和排序。
- **add_monitor**: 向 HertzBeat 添加新的监控目标,支持全面配置。处理每种监控器类型的不同参数要求。
- **list_monitor_types**: 列出所有可添加到 HertzBeat 的监控器类型。显示所有支持的监控器类型及其显示名称。
- **get_monitor_additional_params**: 获取特定监控器类型所需的参数定义。显示添加监控器时需要的参数。
#### 指标数据工具
- **query_realtime_metrics**: 获取特定监控器的实时指标数据。返回当前指标值,包括 CPU、内存、磁盘使用率等。
- **get_historical_metrics**: 获取用于分析和趋势的历史指标数据。返回指定时间范围内指定指标的时间序列数据。
- **get_warehouse_status**: 检查指标存储仓库系统的状态。返回指标存储是否可操作和可访问。
#### 告警管理工具
- **query_alerts**: 查询告警,支持全面的过滤和分页选项。支持按告警类型(单个/组)、状态(触发/已解决)、搜索词进行过滤和排序。
- **get_alerts_summary**: 获取告警摘要统计信息,包括总数、状态分布和所有监控器的优先级分解。
#### 告警规则定义工具
- **create_alert_rule**: 基于应用层次结构和用户需求创建 HertzBeat 告警规则。支持阈值、字段条件和全面的告警配置。
- **list_alert_rules**: 列出现有的告警规则,支持过滤选项。显示已配置的阈值和告警定义,支持搜索和分页。
- **get_alert_rule_details**: 获取特定告警规则的详细信息。显示完整的阈值配置和规则设置。
- **toggle_alert_rule**: 启用或禁用告警规则。允许激活或停用特定规则的阈值监控。
- **get_apps_metrics_hierarchy**: 获取所有可用应用及其指标的层次结构,用于告警规则创建。返回带有字段参数的结构化 JSON 数据。
- **bind_monitors_to_alert_rule**: 将监控器绑定到告警规则。将特定监控器与告警规则关联以启用监控和告警。
### 注意事项
- 如果连接断开,请使用相同的请求头重新连接。
+2 -3
View File
@@ -66,6 +66,8 @@
"items": [
"help/guide",
"help/security_model",
"help/ai_agent",
"help/mcp_sse_server",
{
"type": "category",
"label": "use-case",
@@ -287,10 +289,7 @@
"help/plugin",
"help/time_expression",
"help/grafana_dashboard",
"help/mcp_sse_server",
"help/collector",
"help/ai_agent_chat",
"help/ai_config",
"help/risc-v",
"help/issue"
]
+6
View File
@@ -202,6 +202,12 @@
<artifactId>hertzbeat-manager</artifactId>
<version>${hertzbeat.version}</version>
</dependency>
<!-- ai -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-ai</artifactId>
<version>${hertzbeat.version}</version>
</dependency>
<!-- data warehouse -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
+65 -3
View File
@@ -19,6 +19,25 @@ spring:
name: ${HOSTNAME:@hertzbeat@}${PID}
profiles:
active: prod
ai:
mcp:
server:
enabled: true
stdio: false
name: sse-mcp-server
version: 1.0.0
resource-change-notification: true
tool-change-notification: true
prompt-change-notification: true
sse-endpoint: /api/sse
sse-message-endpoint: /api/mcp/message
type: SYNC
capabilities:
tool: true
resource: true
prompt: true
completion: true
mvc:
static-path-pattern: /**
jackson:
@@ -38,6 +57,7 @@ spring:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
mail:
@@ -124,13 +144,26 @@ spring:
common:
queue:
# memory or kafka
# memory, kafka, redis
type: memory
# properties when queue type is kafka
kafka:
servers: 127.0.0.1:9092
metrics-data-topic: async-metrics-data
service-discovery-data-topic: service-discovery-data
alerts-data-topic: async-alerts-data
log-entry-data-topic: async-log-entry-data
log-entry-data-to-storage-topic: log-entry-data-to-storage-topic
redis:
redis-host: 127.0.0.1
redis-port: 6379
metrics-data-queue-name-for-service-discovery: service_discovery
metrics-data-queue-name-to-persistent-storage: metrics:to_persistent_storage
metrics-data-queue-name-to-alerter: metrics:to_alerter
metrics-data-queue-name-to-real-time-storage: metrics:to_realtime_storage
alerts-data-queue-name: alerts
log-entry-queue-name: log:to_alerter
log-entry-to-storage-queue-name: log:to_storage
warehouse:
store:
@@ -143,10 +176,28 @@ warehouse:
# (please set this configuration reasonably as history records can affect performance when it is large)
max-history-record-num: 6000
victoria-metrics:
# Standalone mode toggle — must be set to false when using cluster mode
enabled: false
url: http://localhost:8428
username: root
password: root
insert:
buffer-size: 100
flush-interval: 3
compression:
enabled: false
cluster:
enabled: false
select:
url: http://localhost:8481
username: root
password: root
insert:
url: http://localhost:8480
username: root
password: root
buffer-size: 1000
flush-interval: 3
td-engine:
enabled: false
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
@@ -161,6 +212,11 @@ warehouse:
database: public
username: greptime
password: greptime
questdb:
enabled: false
url: localhost:9000
username: admin
password: quest
iot-db:
enabled: false
host: 127.0.0.1
@@ -244,10 +300,16 @@ alerter:
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
port: 1158
grafana:
enabled: false
url: http://127.0.0.1:3000
expose-url: http://127.0.0.1:3000
username: admin
password: admin
+1 -1
View File
@@ -60,7 +60,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
@@ -61,7 +61,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
@@ -61,7 +61,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
@@ -61,7 +61,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
@@ -60,7 +60,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
@@ -61,7 +61,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
+1 -1
View File
@@ -60,7 +60,7 @@ http://maven.apache.org/ASSEMBLY/2.0.0 ">
<!-- Configuration files under the src/main/resources/define directory are packaged to the define directory -->
<fileSet>
<directory>src/main/resources</directory>
<directory>../hertzbeat-manager/src/main/resources</directory>
<includes>
<include>define/**</include>
</includes>
@@ -240,10 +240,3 @@ grafana:
url: http://127.0.0.1:3000
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:
@@ -238,10 +238,3 @@ grafana:
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:
@@ -240,10 +240,3 @@ grafana:
url: http://127.0.0.1:3000
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:

Some files were not shown because too many files have changed in this diff Show More