refactor: refactor ai feature (#3823)

Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
This commit is contained in:
Tomsun28
2025-10-25 12:39:53 +08:00
committed by GitHub
co-authored by Calvin
parent 897d5d4220
commit e073bfee91
40 changed files with 662 additions and 1255 deletions
+12 -2
View File
@@ -26,7 +26,7 @@
<artifactId>hertzbeat-ai-agent</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.0.1</spring-ai.version>
<spring-ai.version>1.0.3</spring-ai.version>
<java.version>17</java.version>
</properties>
@@ -51,12 +51,22 @@
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common</artifactId>
</dependency>
<!-- common -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
@@ -18,40 +18,39 @@
package org.apache.hertzbeat.ai.agent.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.apache.hertzbeat.ai.agent.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 OpenAI API Key implementation that retrieves the API key
* from our configuration service (database first, then YAML fallback)
* Dynamic LLM Provider API Key implementation that retrieves the API key
*/
@Slf4j
@Component
public class DynamicOpenAiApiKey implements ApiKey {
private final OpenAiConfigService openAiConfigService;
private final GeneralConfigDao generalConfigDao;
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
public DynamicOpenAiApiKey(GeneralConfigDao generalConfigDao) {
this.generalConfigDao = generalConfigDao;
}
@NotNull
@Override
public String getValue() {
try {
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null) {
log.debug("Retrieved OpenAI API key from configuration service");
return effectiveConfig.getApiKey();
} else {
log.warn("No valid OpenAI API key found in configuration");
return null;
}
} catch (Exception e) {
log.error("Error retrieving OpenAI API key from configuration", e);
return null;
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 "";
}
}
}
@@ -18,59 +18,144 @@
package org.apache.hertzbeat.ai.agent.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.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.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
@Slf4j
public class LlmConfig {
@Value("${spring.ai.openai.chat.options.model}")
private String model;
private final GeneralConfigDao generalConfigDao;
/**
* Create OpenAI API instance with dynamic API key
*/
@Bean
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
return OpenAiApi.builder()
.apiKey(dynamicApiKey)
.build();
private ApplicationContext applicationContext;
public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext applicationContext) {
this.generalConfigDao = generalConfigDao;
this.applicationContext = applicationContext;
}
/**
* Create OpenAI Chat Options with custom settings
* Create ChatClient bean with all dependencies created internally
*/
@Bean
public OpenAiChatOptions openAiChatOptions() {
return OpenAiChatOptions.builder()
.model(model)
public ChatClient openAiChatClient() {
return createChatClient();
}
/**
* Create ChatClient with all necessary components
*/
private ChatClient createChatClient() {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
if (providerConfig == null || providerConfig.getContent() == null) {
log.warn("LLM Provider is not set, ChatClient bean will not be created");
return null;
}
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) {
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
return null;
}
if (modelProviderConfig.getBaseUrl() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4");
} else {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
}
}
if (modelProviderConfig.getModel() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("gpt-5");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else {
modelProviderConfig.setModel("gpt-5");
}
}
OpenAiApi.Builder builder = new OpenAiApi.Builder();
builder.baseUrl(modelProviderConfig.getBaseUrl());
builder.apiKey(modelProviderConfig.getApiKey());
builder.completionsPath("/chat/completions");
// Create Chat Options
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.model(modelProviderConfig.getModel())
.temperature(0.3)
.build();
}
/**
* Create OpenAI Chat Model with custom API configuration
*/
@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions openAiChatOptions) {
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
// Create Chat Model
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(builder.build())
.defaultOptions(openAiChatOptions)
.build();
}
@Bean
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
// Create and return ChatClient
return ChatClient.create(openAiChatModel);
}
/**
* AI configuration change event listener
* Uses ApplicationContext to unregister and re-register the ChatClient bean
*/
@EventListener(AiProviderConfigChangeEvent.class)
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
log.info("Provider configuration change event received, refreshing ChatClient bean");
try {
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
// Remove the existing ChatClient bean
if (beanFactory.containsSingleton("openAiChatClient")) {
beanFactory.destroySingleton("openAiChatClient");
log.info("Existing ChatClient bean destroyed");
}
// Create new ChatClient with updated configuration
ChatClient newChatClient = createChatClient();
// Register the new ChatClient bean
beanFactory.registerSingleton("openAiChatClient", newChatClient);
log.info("ChatClient bean refreshed successfully with new AI provider configuration");
} catch (Exception e) {
log.error("Failed to refresh ChatClient bean after configuration change", e);
}
}
}
@@ -1,43 +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.agent.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
*/
@Data
@Component
@ConfigurationProperties(prefix = "spring.ai.openai")
public class OpenAiYamlConfig {
/**
* OpenAI API key from spring.ai.openai.api-key
*/
private String apiKey;
/**
* Check if OpenAI is enabled (has API key)
*/
public boolean isEnable() {
return apiKey != null && !apiKey.trim().isEmpty();
}
}
@@ -1,160 +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.agent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.http.ResponseEntity;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* OpenAI Configuration API
*/
@RestController
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
@Tag(name = "OpenAI Configuration API")
@Slf4j
public class OpenAiConfigController {
private final OpenAiConfigService openAiConfigService;
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
}
@PostMapping("/openai")
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
try {
Map<String, Object> response = new HashMap<>();
// Validate API key if enabled
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
if (!validationResult.isValid()) {
log.warn("API key validation failed during save: {}", validationResult.getMessage());
response.put("code", 1);
response.put("msg", "API key validation failed: " + validationResult.getMessage());
return ResponseEntity.ok(response);
}
log.info("API key validation successful during save");
}
// Save the configuration
openAiConfigService.saveConfig(config);
response.put("code", 0);
response.put("msg", "OpenAI configuration saved successfully");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to save OpenAI configuration", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to save configuration: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
@GetMapping("/openai")
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
try {
OpenAiConfigDto config = openAiConfigService.getConfig();
Map<String, Object> response = new HashMap<>();
response.put("code", 0);
response.put("data", config);
response.put("msg", "Success");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get OpenAI configuration", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to get configuration: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
@GetMapping("/openai/status")
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
try {
boolean configured = openAiConfigService.isConfigured();
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
boolean hasDbConfig = openAiConfigService.getConfig() != null;
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
// Validate the effective configuration
boolean validationPassed = false;
String validationMessage = "No configuration found";
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
validationPassed = validationResult.isValid();
validationMessage = validationResult.getMessage();
if (!validationPassed) {
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
}
}
Map<String, Object> response = new HashMap<>();
response.put("code", 0);
response.put("data", Map.of(
"configured", configured && validationPassed,
"hasDbConfig", hasDbConfig,
"hasYamlConfig", hasYamlConfig,
"validationPassed", validationPassed,
"validationMessage", validationMessage
));
response.put("msg", "Success");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get OpenAI configuration status", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to get status: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
}
@@ -1,37 +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.agent.dao;
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* OpenAI Agent Configuration Dao
*/
@Repository
public interface OpenAiConfigDao extends JpaRepository<OpenAiConfig, String>, JpaSpecificationExecutor<OpenAiConfig> {
/**
* Query by type
* @param type type
* @return Return the queried configuration information
*/
OpenAiConfig findByType(String type);
}
@@ -24,24 +24,36 @@ import lombok.Data;
import lombok.NoArgsConstructor;
/**
* OpenAI Configuration DTO - simplified to handle only API key
* Model Provider Configuration
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "OpenAI configuration")
public class OpenAiConfigDto {
@Schema(description = "LLM Model Provider configuration")
public class ModelProviderConfig {
/**
* Whether to enable OpenAI, default is false
*/
@Schema(title = "Enable OpenAI", description = "Whether OpenAI is enabled", example = "true")
@Schema(title = "Enable Provider", description = "Whether Provider is enabled", example = "true")
private boolean enable = false;
/**
* OpenAI API key
*/
@Schema(title = "API Key", description = "OpenAI API key", example = "sk-...")
@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;
@Schema(title = "Model Provider code, like openai, zai, bigmodel")
private String code;
@Schema(title = "custom the provider server base url")
private String baseUrl;
@Schema(title = "use the model id name, eg: gpt-5, glm-4.6")
private String model;
@Schema(title = "API Key", description = "API key", example = "sk-...")
@NotBlank(message = "API Key cannot be empty when enabled")
private String apiKey;
}
@@ -17,38 +17,10 @@
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
/**
* OpenAI Configuration Service
* Consolidated service for OpenAI configuration, validation, and client factory management
* Ai Configuration Service
*/
public interface OpenAiConfigService {
/**
* Save OpenAI configuration
* @param config OpenAI configuration
*/
void saveConfig(OpenAiConfigDto config);
/**
* Get OpenAI configuration
* @return OpenAI configuration
*/
OpenAiConfigDto getConfig();
/**
* Check if OpenAI is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
/**
* Get effective OpenAI configuration (DB first, then YAML fallback)
* @return effective configuration or null if not configured
*/
OpenAiConfigDto getEffectiveConfig();
public interface AiConfigService {
/**
* Validate OpenAI API key by calling the OpenAI API
@@ -57,18 +29,6 @@ public interface OpenAiConfigService {
*/
ValidationResult validateApiKey(String apiKey);
/**
* Force reload of OpenAI configuration cache
* This method is typically called when configuration changes
*/
void reloadConfig();
/**
* Handle OpenAI configuration change events
* @param event OpenAI configuration change event
*/
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
/**
* Validation result class
*/
@@ -33,4 +33,10 @@ public interface ChatClientProviderService {
* @return Flux of string chunks from the LLM response
*/
Flux<String> streamChat(ChatRequestContext context);
/**
* Check if provider is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
}
@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.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);
}
}
}
}
@@ -21,7 +21,11 @@ package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
@@ -31,6 +35,7 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
@@ -45,19 +50,25 @@ import java.util.List;
@Service
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
private final ChatClient chatClient;
private final ApplicationContext applicationContext;
private final GeneralConfigDao generalConfigDao;
@Qualifier("hertzbeatTools")
@Autowired
private ToolCallbackProvider toolCallbackProvider;
private boolean isConfigured = false;
@Autowired
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
this.chatClient = openAiChatClient;
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
this.applicationContext = applicationContext;
this.generalConfigDao = generalConfigDao;
}
public String complete(String message) {
return this.chatClient.prompt()
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
return chatClient.prompt()
.user(message)
.call()
.content();
@@ -66,6 +77,9 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
// Get the current (potentially refreshed) ChatClient instance
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
List<Message> messages = new ArrayList<>();
// Add conversation history if available
@@ -83,7 +97,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
return this.chatClient.prompt()
return chatClient.prompt()
.messages(messages)
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
.toolCallbacks(toolCallbackProvider)
@@ -97,4 +111,14 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
return Flux.error(e);
}
}
@Override
public boolean isConfigured() {
if (!isConfigured) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.isStatus();
}
return isConfigured;
}
}
@@ -24,7 +24,6 @@ import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
@@ -52,9 +51,6 @@ public class ConversationServiceImpl implements ConversationService {
@Autowired
private ChatClientProviderService chatClientProviderService;
@Autowired
private OpenAiConfigService openAiConfigService;
@Override
public ConversationDto createConversation() {
String conversationId = createNewConversation();
@@ -75,10 +71,10 @@ public class ConversationServiceImpl implements ConversationService {
}
// Check if OpenAI is properly configured
if (!openAiConfigService.isConfigured()) {
if (!chatClientProviderService.isConfigured()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("OpenAI is not configured. Please configure your OpenAI API key in the settings or application.yml file.")
.response("Provider is not configured. Please configure your AI Provider.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
@@ -1,200 +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.agent.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.OpenAiYamlConfig;
import org.apache.hertzbeat.ai.agent.dao.OpenAiConfigDao;
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
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.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* OpenAI Configuration Service Implementation
* Consolidated service for OpenAI configuration, validation, and client factory management
*/
@Slf4j
@Service
public class OpenAiConfigServiceImpl implements OpenAiConfigService {
private static final String CONFIG_TYPE = "openai";
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
private final OpenAiConfigDao openAiConfigDao;
private final ObjectMapper objectMapper;
private final ApplicationContext applicationContext;
private final OpenAiYamlConfig yamlConfig;
private final RestTemplate restTemplate;
// Client factory cache
private volatile OpenAiConfigDto currentConfig;
public OpenAiConfigServiceImpl(OpenAiConfigDao openAiConfigDao,
ObjectMapper objectMapper,
ApplicationContext applicationContext,
OpenAiYamlConfig yamlConfig) {
this.openAiConfigDao = openAiConfigDao;
this.objectMapper = objectMapper;
this.applicationContext = applicationContext;
this.yamlConfig = yamlConfig;
this.restTemplate = new RestTemplate();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveConfig(OpenAiConfigDto config) {
try {
String contentJson = objectMapper.writeValueAsString(config);
OpenAiConfig openAiConfig = OpenAiConfig.builder()
.type(CONFIG_TYPE)
.content(contentJson)
.build();
openAiConfigDao.save(openAiConfig);
log.info("OpenAI configuration saved successfully");
applicationContext.publishEvent(new OpenAiConfigChangeEvent(applicationContext));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to save OpenAI configuration: " + e.getMessage());
}
}
@Override
public OpenAiConfigDto getConfig() {
OpenAiConfig config = openAiConfigDao.findByType(CONFIG_TYPE);
if (config == null || !StringUtils.hasText(config.getContent())) {
return null;
}
try {
return objectMapper.readValue(config.getContent(), OpenAiConfigDto.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse OpenAI configuration: " + e.getMessage());
}
}
@Override
public boolean isConfigured() {
OpenAiConfigDto effective = getEffectiveConfig();
return effective != null && effective.isEnable() && StringUtils.hasText(effective.getApiKey());
}
@Override
public OpenAiConfigDto getEffectiveConfig() {
OpenAiConfigDto dbConfig = getConfig();
if (dbConfig != null && dbConfig.isEnable() && StringUtils.hasText(dbConfig.getApiKey())) {
log.debug("Using database OpenAI configuration");
return dbConfig;
}
if (yamlConfig != null && yamlConfig.isEnable() && StringUtils.hasText(yamlConfig.getApiKey())) {
log.debug("Using YAML OpenAI configuration from spring.ai.openai.api-key");
OpenAiConfigDto yamlDto = new OpenAiConfigDto();
yamlDto.setEnable(true);
yamlDto.setApiKey(yamlConfig.getApiKey());
return yamlDto;
}
log.debug("No valid OpenAI configuration found");
return null;
}
@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);
}
}
}
@Override
public void reloadConfig() {
synchronized (this) {
currentConfig = null; // Force reload
}
}
/**
* OpenAI configuration change event listener
*/
@EventListener(OpenAiConfigChangeEvent.class)
public void onOpenAiConfigChange(OpenAiConfigChangeEvent event) {
log.info("[OpenAiConfigService] OpenAI configuration change event received");
reloadConfig();
}
}
@@ -55,5 +55,10 @@ public enum GeneralConfigTypeEnum {
/**
* system store config
*/
oss
oss,
/**
* model llm provider
*/
provider
}
@@ -15,17 +15,17 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.event;
package org.apache.hertzbeat.common.support.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
/**
* OpenAI configuration change event
* Ai Provider configuration change event
*/
public class OpenAiConfigChangeEvent extends ApplicationEvent {
public class AiProviderConfigChangeEvent extends ApplicationEvent {
public OpenAiConfigChangeEvent(ApplicationContext source) {
public AiProviderConfigChangeEvent(ApplicationContext source) {
super(source);
}
}
@@ -51,11 +51,11 @@ import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Alert sender Configuration API
* Generate Configuration API
*/
@RestController
@RequestMapping(value = "/api/config", produces = {APPLICATION_JSON_VALUE})
@Tag(name = "Alert sender Configuration API")
@Tag(name = "Generate Configuration API")
@Slf4j
public class GeneralConfigController {
@@ -0,0 +1,69 @@
/*
* 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.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Type;
import org.apache.hertzbeat.ai.agent.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
/**
* llm model provider config service impl
*/
@Service
public class ModelProviderConfigServiceImpl extends AbstractGeneralConfigServiceImpl<ModelProviderConfig> {
private final ApplicationContext applicationContext;
/**
*
* <p>Constructor, passing in GeneralConfigDao, ObjectMapper and type.</p>
*
* @param generalConfigDao ConfigDao object
* @param objectMapper JSON tool object
*/
public ModelProviderConfigServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao, ObjectMapper objectMapper) {
super(generalConfigDao, objectMapper);
this.applicationContext = applicationContext;
}
@Override
public String type() {
return GeneralConfigTypeEnum.provider.name();
}
@Override
public void handler(ModelProviderConfig config) {
applicationContext.publishEvent(new AiProviderConfigChangeEvent(applicationContext));
}
@Override
public TypeReference<ModelProviderConfig> getTypeReference() {
return new TypeReference<>() {
@Override
public Type getType() {
return ModelProviderConfig.class;
}
};
}
}
@@ -189,16 +189,3 @@ grafana:
expose-url: http://127.0.0.1:3000
username: admin
password: admin
# See the documentation for details : https://hertzbeat.apache.org/zh-cn/docs/help/aiConfig
ai:
# AI Type:zhiPu、alibabaAi、kimiAi、sparkDesk、ollama、openRouter
type:
# Model nameglm-4、qwen-turboo、moonshot-v1-8k、generalv3.5
model:
# api key
api-key:
#At present, only IFLYTEK large model needs to be filled in
api-secret:
# The URL of the ollama AI service
api-url:
@@ -37,14 +37,6 @@ spring:
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
@@ -321,16 +313,3 @@ grafana:
expose-url: http://127.0.0.1:3000
username: admin
password: admin
# See the documentation for details : https://hertzbeat.apache.org/zh-cn/docs/help/aiConfig
ai:
# AI Type:zhiPu、alibabaAi、kimiAi、sparkDesk、ollama、openRouter
type:
# Model nameglm-4、qwen-turboo、moonshot-v1-8k、generalv3.5
model:
# api key
api-key:
#At present, only IFLYTEK large model needs to be filled in
api-secret:
# The URL of the ollama AI service
api-url:
+1 -1
View File
@@ -22,7 +22,7 @@ Previous releases of HertzBeat may be affected by security issues, please use th
| Version | Date | Download | Release |
|---------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| v1.7.3 | 2025.09.05 | [apache-hertzbeat-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-src.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-docker-compose.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.3) |
| v1.7.3 | 2025.09.05 | [apache-hertzbeat-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-src.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-docker-compose.tar.gz](https://github.com/apache/hertzbeat/releases/download/v1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.3) |
## Release Docker Image
@@ -22,7 +22,7 @@ sidebar_label: Download
| 版本 | 日期 | 下载 | Release |
|--------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| v1.7.3 | 2025.09.05 | [apache-hertzbeat-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-src.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-docker-compose.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.3) |
| v1.7.3 | 2025.09.05 | [apache-hertzbeat-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-src.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-docker-compose.tar.gz](https://github.com/apache/hertzbeat/releases/download/v1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.3) |
## Docker 镜像版本
@@ -38,14 +38,6 @@ spring:
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -38,14 +38,6 @@ spring:
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -38,14 +38,6 @@ spring:
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -38,14 +38,6 @@ spring:
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -1,30 +0,0 @@
<div class="alain-default__content">
<router-outlet></router-outlet>
</div>
<div class="ai-chatbot-container">
<div class="ai-chatbot-button" (click)="toggleChatbot()">
<span *ngIf="!isChatbotOpen">AI</span>
<span *ngIf="isChatbotOpen">X</span>
</div>
<div class="ai-chatbot-window" *ngIf="isChatbotOpen">
<div class="chatbot-header">
<div class="chatbot-title">{{ 'ai.bot.title' | i18n }}</div>
<div class="chatbot-close" (click)="toggleChatbot()">X</div>
</div>
<div class="chatbot-messages">
<div *ngFor="let message of chatMessages" [class.user-message]="message.isUser" [class.bot-message]="!message.isUser" class="message">
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.timestamp | date : 'HH:mm' }}</div>
</div>
<div *ngIf="isLoading" class="bot-message loading-message">
<nz-spin nzSimple></nz-spin>
</div>
</div>
<div class="chatbot-input">
<input nz-input placeholder="{{ 'ai.bot.input.placeholder' | i18n }}" [(ngModel)]="currentMessage" (keyup.enter)="sendMessage()" />
<button nz-button nzType="primary" [disabled]="!currentMessage.trim()" (click)="sendMessage()"> {{ 'ai.bot.send' | i18n }} </button>
</div>
</div>
</div>
@@ -338,9 +338,9 @@ global-footer {
}
.ai-chat-button {
width: 56px;
height: 56px;
border-radius: 50%;
width: 44px;
height: 44px;
border-radius: 8px;
background-color: @primary-color;
color: white;
display: flex;
@@ -83,7 +83,7 @@ import { AiChatModalService } from '../../shared/services/ai-chat-modal.service'
width="28"
height="28"
fill="white"
style="min-width:28px; min-height:28px;"
style="min-width:36px; min-height:36px;"
>
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
@@ -0,0 +1,57 @@
/*
* 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.
*/
export class ModelProviderConfig {
enable: boolean = false;
status: boolean = true;
error!: string;
type!: string;
code: string = 'openai';
baseUrl: string = '';
model: string = '';
apiKey!: string;
}
export interface ProviderOption {
value: string;
label: string;
defaultBaseUrl: string;
defaultModel: string;
}
export const PROVIDER_OPTIONS: ProviderOption[] = [
{
value: 'openai',
label: 'OpenAI',
defaultBaseUrl: 'https://api.openai.com/v1',
defaultModel: 'gpt-4'
},
{
value: 'zai',
label: 'ZAI',
defaultBaseUrl: 'https://api.z.ai/api/paas/v4',
defaultModel: 'glm-4.6'
},
{
value: 'zhipu',
label: 'ZhiPu',
defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
defaultModel: 'glm-4.6'
}
];
@@ -22,6 +22,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Message } from '../pojo/Message';
import { ModelProviderConfig } from '../pojo/ModelProviderConfig';
const general_config_uri = '/config';
@@ -39,6 +40,14 @@ export class GeneralConfigService {
return this.http.get<Message<any>>(`${general_config_uri}/${type}`);
}
public saveModelProviderConfig(body: ModelProviderConfig): Observable<Message<any>> {
return this.http.post<Message<any>>(`${general_config_uri}/provider`, body);
}
public getModelProviderConfig(): Observable<Message<ModelProviderConfig>> {
return this.http.get<Message<ModelProviderConfig>>(`${general_config_uri}/provider`);
}
public updateAppTemplateConfig(body: any, app: string): Observable<Message<void>> {
return this.http.put<Message<void>>(`${general_config_uri}/template/${app}`, body);
}
@@ -1,58 +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.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Message } from '../pojo/Message';
export interface OpenAiConfig {
enable: boolean;
apiKey: string;
}
export interface OpenAiConfigStatus {
configured: boolean;
hasDbConfig: boolean;
hasYamlConfig: boolean;
validationPassed: boolean;
validationMessage: string;
}
const openai_config_uri = '/ai-agent/config';
@Injectable({
providedIn: 'root'
})
export class OpenAiConfigService {
constructor(private http: HttpClient) {}
public saveOpenAiConfig(config: OpenAiConfig): Observable<Message<any>> {
return this.http.post<Message<any>>(`${openai_config_uri}/openai`, config);
}
public getOpenAiConfig(): Observable<Message<OpenAiConfig>> {
return this.http.get<Message<OpenAiConfig>>(`${openai_config_uri}/openai`);
}
public getOpenAiConfigStatus(): Observable<Message<OpenAiConfigStatus>> {
return this.http.get<Message<OpenAiConfigStatus>>(`${openai_config_uri}/openai/status`);
}
}
@@ -1,67 +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.
-->
<div class="ai-bot-container">
<div class="ai-bot-icon" (click)="toggleChat()" [class.active]="isOpen">
<div *ngIf="!isOpen" class="robot-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
/>
</svg>
</div>
<div *ngIf="isOpen" class="close-icon">X</div>
</div>
<div class="ai-bot-chat-window" *ngIf="isOpen">
<div class="chat-header">
<div class="chat-title">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="20"
height="20"
fill="currentColor"
style="margin-right: 6px; vertical-align: middle"
>
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
/>
</svg>
{{ 'ai.bot.title' | i18n }}
</div>
<span class="close-btn" (click)="toggleChat()">X</span>
</div>
<div class="chat-messages">
<div *ngFor="let message of messages" class="message" [class.user-message]="message.isUser" [class.bot-message]="!message.isUser">
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.timestamp | date : 'HH:mm' }}</div>
</div>
<div *ngIf="isLoading" class="bot-message loading-message">
<nz-spin nzSimple></nz-spin>
</div>
</div>
<div class="chat-input">
<input nz-input placeholder="{{ 'ai.bot.input.placeholder' | i18n }}" [(ngModel)]="currentMessage" (keyup.enter)="sendMessage()" />
<button nz-button nzType="primary" [disabled]="!currentMessage.trim()" (click)="sendMessage()"> {{ 'ai.bot.send' | i18n }} </button>
</div>
</div>
</div>
@@ -1,147 +0,0 @@
.ai-bot-container {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 10000;
}
.ai-bot-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
color: white;
font-weight: bold;
font-size: 18px;
&:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
&.active {
background-color: #ff4d4f;
}
}
.ai-bot-icon-text {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
color: white;
font-weight: bold;
font-size: 14px;
text-align: center;
&:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
}
.ai-bot-chat-window {
position: absolute;
bottom: 80px;
right: 0;
width: 350px;
height: 500px;
background-color: white;
border-radius: 8px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 10000;
}
.chat-header {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #1890ff;
color: white;
.chat-title {
font-weight: 500;
font-size: 16px;
}
.close-btn {
cursor: pointer;
font-size: 16px;
color: white;
font-weight: bold;
}
}
.chat-messages {
flex: 1;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
background-color: #f5f5f5;
}
.message {
max-width: 80%;
padding: 10px 12px;
border-radius: 8px;
position: relative;
.message-content {
word-break: break-word;
}
.message-time {
font-size: 11px;
margin-top: 4px;
opacity: 0.7;
text-align: right;
}
}
.bot-message {
align-self: flex-start;
background-color: white;
border: 1px solid #e8e8e8;
&.loading-message {
padding: 16px;
display: flex;
justify-content: center;
}
}
.user-message {
align-self: flex-end;
background-color: #e6f7ff;
color: #0050b3;
}
.chat-input {
padding: 12px;
border-top: 1px solid #f0f0f0;
display: flex;
gap: 8px;
input {
flex: 1;
}
}
@@ -1,158 +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.
.ai-bot-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.ai-bot-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #1890ff;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.ai-bot-icon:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.ai-bot-icon.active {
background-color: #f5222d;
}
/* 机器人图标样式 */
.robot-icon {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.robot-icon svg {
width: 100%;
height: 100%;
fill: white; /* 确保SVG是白色 */
}
/* 关闭图标样式 */
.close-icon {
font-weight: bold;
font-size: 18px;
}
.ai-bot-chat-window {
position: absolute;
bottom: 70px;
right: 0;
width: 320px;
height: 450px;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #1890ff;
color: white;
}
.chat-title {
font-weight: 500;
display: flex;
align-items: center;
}
.close-btn {
cursor: pointer;
font-weight: bold;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
background-color: #f5f5f5;
}
.message {
max-width: 80%;
padding: 8px 12px;
border-radius: 12px;
}
.user-message {
align-self: flex-end;
background-color: #1890ff;
color: white;
}
.bot-message {
align-self: flex-start;
background-color: white;
color: #333;
border: 1px solid #e8e8e8;
}
.message-content {
word-break: break-word;
white-space: pre-wrap;
}
.message-time {
font-size: 12px;
margin-top: 4px;
opacity: 0.7;
text-align: right;
}
.loading-message {
padding: 8px;
text-align: center;
}
.chat-input {
display: flex;
padding: 12px;
border-top: 1px solid #e8e8e8;
}
.chat-input input {
flex: 1;
margin-right: 8px;
}
@@ -1,72 +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.
*/
import { Component, OnInit } from '@angular/core';
import { I18NService } from '@core';
import { AiBotService, ChatMessage } from '../../services/ai-bot.service';
@Component({
selector: 'app-ai-bot',
templateUrl: './ai-bot.component.html',
styleUrls: ['./ai-bot.component.less']
})
export class AiBotComponent implements OnInit {
isOpen = false;
messages: ChatMessage[] = [];
currentMessage = '';
isLoading = false;
constructor(private aiBotService: AiBotService, private i18nSvc: I18NService) {}
ngOnInit(): void {
// add greeting message
this.messages.push({
content: this.i18nSvc.fanyi('ai.bot.greeting'),
isUser: false,
timestamp: new Date()
});
console.log('The AI assistant component has loaded.');
}
toggleChat(): void {
this.isOpen = !this.isOpen;
}
sendMessage(): void {
if (!this.currentMessage.trim()) return;
// add user message
this.messages.push({
content: this.currentMessage,
isUser: true,
timestamp: new Date()
});
const userMessage = this.currentMessage;
this.currentMessage = '';
this.isLoading = true;
// call service to fetch AI response
this.aiBotService.sendMessage(userMessage).subscribe(response => {
this.messages.push(response);
this.isLoading = false;
});
}
}
@@ -28,6 +28,7 @@ import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageModule } from 'ng-zorro-antd/message';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { MarkdownPipe } from 'ngx-markdown';
@@ -47,6 +48,7 @@ import { ChatComponent } from './chat.component';
NzInputModule,
NzMessageModule,
NzModalModule,
NzSelectModule,
NzSpinModule,
MarkdownPipe
],
@@ -160,23 +160,73 @@
<!-- OpenAI Configuration Modal -->
<nz-modal
[(nzVisible)]="showConfigModal"
nzTitle="OpenAI Configuration"
nzTitle="AI Provider Configuration"
(nzOnCancel)="onCloseConfigModal()"
(nzOnOk)="onSaveOpenAiConfig()"
(nzOnOk)="onSaveAiProviderConfig()"
nzMaskClosable="false"
[nzClosable]="false"
nzWidth="600px"
nzWidth="700px"
[nzOkLoading]="configLoading"
nzOkText="Validate & Save"
nzCancelText="Cancel"
>
<div *nzModalContent class="-inner-content">
<form nz-form nzLayout="vertical">
<!-- Provider Selection -->
<nz-form-item>
<nz-form-label nzRequired="true">OpenAI API Key</nz-form-label>
<nz-form-label nzRequired="true">AI Provider</nz-form-label>
<nz-form-control nzErrorTip="Please select an AI provider">
<nz-select
[(ngModel)]="aiProviderConfig.code"
name="provider"
nzPlaceHolder="Select AI Provider"
(ngModelChange)="onProviderChange($event)"
style="width: 100%"
>
<nz-option *ngFor="let option of providerOptions" [nzValue]="option.value" [nzLabel]="option.label"></nz-option>
</nz-select>
<p class="form-help">Choose your AI provider (OpenAI, ZhiPu, or ZAI)</p>
</nz-form-control>
</nz-form-item>
<!-- API Key -->
<nz-form-item>
<nz-form-label nzRequired="true">API Key</nz-form-label>
<nz-form-control nzErrorTip="API Key is required">
<input nz-input [(ngModel)]="openAiConfig.apiKey" name="apiKey" type="password" placeholder="sk-..." required />
<p class="form-help">Your OpenAI API key (starts with sk-). The key will be validated when saved.</p>
<input nz-input [(ngModel)]="aiProviderConfig.apiKey" name="apiKey" type="password" placeholder="sk-..." required />
<p class="form-help">Your Provider API key. The key will be validated when saved.</p>
</nz-form-control>
</nz-form-item>
<!-- Base URL -->
<nz-form-item>
<nz-form-label>Base URL</nz-form-label>
<nz-form-control>
<nz-input-group [nzSuffix]="resetBtn">
<input nz-input [(ngModel)]="aiProviderConfig.baseUrl" name="baseUrl" placeholder="https://api.openai.com/v1" />
<ng-template #resetBtn>
<button
nz-button
nzType="link"
nzSize="small"
(click)="resetToDefaults()"
nzTooltipTitle="Reset to default values"
nz-tooltip
>
<i nz-icon nzType="reload"></i>
</button>
</ng-template>
</nz-input-group>
<p class="form-help">Custom API endpoint URL. Leave empty to use default for selected provider.</p>
</nz-form-control>
</nz-form-item>
<!-- Model -->
<nz-form-item>
<nz-form-label>Model</nz-form-label>
<nz-form-control>
<input nz-input [(ngModel)]="aiProviderConfig.model" name="model" placeholder="gpt-4" />
<p class="form-help">Model name to use. Leave empty to use default for selected provider.</p>
</nz-form-control>
</nz-form-item>
</form>
@@ -634,6 +634,41 @@ body[data-theme='dark'] .chat-container {
}
}
// Provider configuration form styles
.provider-config-form {
.provider-select-row {
display: flex;
align-items: center;
gap: 8px;
nz-select {
flex: 1;
}
}
.baseurl-input-row {
display: flex;
align-items: center;
gap: 8px;
nz-input-group {
flex: 1;
}
.reset-btn {
flex-shrink: 0;
}
}
.form-item-with-help {
margin-bottom: 8px;
+ .form-help {
margin-bottom: 16px;
}
}
}
nz-modal {
.ant-modal-body {
padding: 24px;
@@ -22,8 +22,9 @@ import { I18NService } from '@core';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
import { ModelProviderConfig, PROVIDER_OPTIONS, ProviderOption } from '../../../pojo/ModelProviderConfig';
import { AiChatService, ChatMessage, ConversationDto } from '../../../service/ai-chat.service';
import { OpenAiConfigService, OpenAiConfig, OpenAiConfigStatus } from '../../../service/openai-config.service';
import { GeneralConfigService } from '../../../service/general-config.service';
import { ThemeService } from '../../../service/theme.service';
@Component({
@@ -46,10 +47,8 @@ export class ChatComponent implements OnInit, AfterViewChecked {
isOpenAiConfigured = false;
showConfigModal = false;
configLoading = false;
openAiConfig: OpenAiConfig = {
enable: false,
apiKey: ''
};
aiProviderConfig: ModelProviderConfig = new ModelProviderConfig();
providerOptions: ProviderOption[] = PROVIDER_OPTIONS;
constructor(
private aiChatService: AiChatService,
@@ -58,12 +57,12 @@ export class ChatComponent implements OnInit, AfterViewChecked {
private i18n: I18NService,
private cdr: ChangeDetectorRef,
private themeSvc: ThemeService,
private openAiConfigService: OpenAiConfigService
private generalConfigSvc: GeneralConfigService
) {}
ngOnInit(): void {
this.theme = this.themeSvc.getTheme() || 'default';
this.checkOpenAiConfiguration();
this.checkAiConfiguration();
}
ngAfterViewChecked(): void {
@@ -74,10 +73,8 @@ export class ChatComponent implements OnInit, AfterViewChecked {
* Load all conversations
*/
loadConversations(): void {
console.log('Loading conversations...');
this.aiChatService.getConversations().subscribe({
next: response => {
console.log('Conversations response:', response);
if (response.code === 0 && response.data) {
this.conversations = response.data;
// If no current conversation, create a new one
@@ -107,10 +104,8 @@ export class ChatComponent implements OnInit, AfterViewChecked {
* Create a new conversation
*/
createNewConversation(): void {
console.log('Creating new conversation...');
this.aiChatService.createConversation().subscribe({
next: response => {
console.log('Create conversation response:', response);
if (response.code === 0 && response.data) {
const newConversation = response.data;
this.conversations.unshift(newConversation);
@@ -165,7 +160,6 @@ export class ChatComponent implements OnInit, AfterViewChecked {
this.aiChatService.getConversation(conversationId).subscribe({
next: response => {
this.isLoading = false;
console.log('Conversation history response:', response);
if (response.code === 0 && response.data) {
this.messages = response.data.messages || [];
@@ -322,7 +316,6 @@ export class ChatComponent implements OnInit, AfterViewChecked {
this.scrollToBottom();
},
complete: () => {
console.log('Chat stream completed');
this.isLoading = false;
this.cdr.detectChanges();
@@ -428,61 +421,68 @@ export class ChatComponent implements OnInit, AfterViewChecked {
}
/**
* Check OpenAI configuration status
* Check provider configuration status
*/
checkOpenAiConfiguration(): void {
this.openAiConfigService.getOpenAiConfigStatus().subscribe({
checkAiConfiguration(): void {
this.generalConfigSvc.getModelProviderConfig().subscribe({
next: response => {
if (response.code === 0) {
this.isOpenAiConfigured = response.data.configured;
if (this.isOpenAiConfigured) {
if (response.code === 0 && response.data) {
this.aiProviderConfig = response.data;
// Ensure default values are set if not present
if (!this.aiProviderConfig.code) {
this.aiProviderConfig.code = 'openai';
}
if (!this.aiProviderConfig.baseUrl) {
const defaultProvider = this.providerOptions.find(p => p.value === this.aiProviderConfig.code);
if (defaultProvider) {
this.aiProviderConfig.baseUrl = defaultProvider.defaultBaseUrl;
}
}
if (!this.aiProviderConfig.model) {
const defaultProvider = this.providerOptions.find(p => p.value === this.aiProviderConfig.code);
if (defaultProvider) {
this.aiProviderConfig.model = defaultProvider.defaultModel;
}
}
if (response.data.enable) {
this.loadConversations();
} else {
this.showOpenAiConfigDialog(response.data);
this.showAiProviderConfigDialog(response.data.error);
}
} else {
console.error('Failed to check OpenAI configuration:', response.msg);
this.showOpenAiConfigDialog();
// Initialize with default values if no config exists
this.aiProviderConfig = new ModelProviderConfig();
this.showAiProviderConfigDialog();
}
},
error: error => {
console.error('Error checking OpenAI configuration:', error);
this.showOpenAiConfigDialog();
console.error('Failed to load model provider config:', error);
this.aiProviderConfig = new ModelProviderConfig();
this.showAiProviderConfigDialog();
}
});
}
/**
* Show OpenAI configuration dialog
* Show ai configuration dialog
*/
showOpenAiConfigDialog(status?: OpenAiConfigStatus): void {
// Load existing configuration if available
this.loadOpenAiConfig();
showAiProviderConfigDialog(error?: string): void {
let contentMessage = `
<div style="margin-bottom: 16px;">
<p>To use AI Agent Chat, please configure your OpenAI API key.</p>
<p>To use AI Agent Chat, please configure your Model Provider.</p>
`;
if (status && !status.validationPassed && status.validationMessage) {
if (error) {
contentMessage += `
<div style="margin-bottom: 12px; padding: 8px; background: #fff2f0; border: 1px solid #ffccc7; border-radius: 4px;">
<strong>Configuration Issue:</strong> ${status.validationMessage}
<strong>Configuration Issue:</strong> ${error}
</div>
`;
}
contentMessage += `
<p>You can either:</p>
<ul>
<li>Configure it here (stored in database)</li>
<li>Add it to your application.yml file under <code>spring.ai.openai.api-key</code></li>
</ul>
</div>
`;
const modalRef = this.modal.create({
nzTitle: 'OpenAI Configuration Required',
nzTitle: 'Ai Model Provider Configuration Required',
nzContent: contentMessage,
nzWidth: 600,
nzClosable: false,
@@ -500,27 +500,11 @@ export class ChatComponent implements OnInit, AfterViewChecked {
});
}
/**
* Load OpenAI configuration
*/
loadOpenAiConfig(): void {
this.openAiConfigService.getOpenAiConfig().subscribe({
next: response => {
if (response.code === 0 && response.data) {
this.openAiConfig = { ...this.openAiConfig, ...response.data };
}
},
error: error => {
console.error('Failed to load OpenAI config:', error);
}
});
}
/**
* Show configuration modal
*/
onShowConfigModal(): void {
this.loadOpenAiConfig();
this.checkAiConfiguration();
this.showConfigModal = true;
}
@@ -534,23 +518,39 @@ export class ChatComponent implements OnInit, AfterViewChecked {
/**
* Save OpenAI configuration
*/
onSaveOpenAiConfig(): void {
if (!this.openAiConfig.apiKey.trim()) {
this.message.error('API Key is required');
onSaveAiProviderConfig(): void {
if (!this.aiProviderConfig.apiKey?.trim()) {
this.message.error('Please enter API Key');
return;
}
if (!this.aiProviderConfig.code?.trim()) {
this.message.error('Please select a provider');
return;
}
if (!this.aiProviderConfig.baseUrl?.trim()) {
this.message.error('Please enter Base URL');
return;
}
if (!this.aiProviderConfig.model?.trim()) {
this.message.error('Please enter Model');
return;
}
// Always enable when saving an API key
this.openAiConfig.enable = true;
this.aiProviderConfig.enable = true;
this.aiProviderConfig.status = true;
this.configLoading = true;
this.message.info('Validating API key...', { nzDuration: 2000 });
this.openAiConfigService.saveOpenAiConfig(this.openAiConfig).subscribe({
this.generalConfigSvc.saveModelProviderConfig(this.aiProviderConfig).subscribe({
next: response => {
this.configLoading = false;
if (response.code === 0) {
this.message.success('OpenAI API key validated and saved successfully!');
this.message.success('Model Provider configuration saved successfully!');
this.showConfigModal = false;
this.isOpenAiConfigured = true;
this.loadConversations();
@@ -569,4 +569,32 @@ export class ChatComponent implements OnInit, AfterViewChecked {
}
});
}
/**
* Handle provider selection change
*/
onProviderChange(provider: string): void {
const selectedProvider = this.providerOptions.find(p => p.value === provider);
if (selectedProvider) {
this.aiProviderConfig.code = provider;
// Auto-fill default values if current values are empty
if (!this.aiProviderConfig.baseUrl) {
this.aiProviderConfig.baseUrl = selectedProvider.defaultBaseUrl;
}
if (!this.aiProviderConfig.model) {
this.aiProviderConfig.model = selectedProvider.defaultModel;
}
}
}
/**
* Reset to default values for selected provider
*/
resetToDefaults(): void {
const selectedProvider = this.providerOptions.find(p => p.value === this.aiProviderConfig.code);
if (selectedProvider) {
this.aiProviderConfig.baseUrl = selectedProvider.defaultBaseUrl;
this.aiProviderConfig.model = selectedProvider.defaultModel;
}
}
}
-2
View File
@@ -20,7 +20,6 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
// Icon to be used for registration
const icons: IconDefinition[] = [RobotOutline, CloseOutline, SendOutline];
import { AiBotComponent } from './components/ai-bot/ai-bot.component';
import { AiChatModule } from './components/ai-chat/ai-chat.module';
import { ConfigurableFieldComponent } from './components/configurable-field/configurable-field.component';
import { FormFieldComponent } from './components/form-field/form-field.component';
@@ -43,7 +42,6 @@ const COMPONENTS: Array<Type<void>> = [
ConfigurableFieldComponent,
FormFieldComponent,
MonitorSelectMenuComponent,
AiBotComponent,
LabelSelectorComponent
];
const DIRECTIVES: Array<Type<void>> = [TimezonePipe, I18nElsePipe, ElapsedTimePipe];