mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
refactor: refactor ai feature (#3823)
Signed-off-by: tomsun28 <tomsun28@outlook.com> Co-authored-by: Calvin <zhengqiwei@apache.org>
This commit is contained in:
@@ -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
-19
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-43
@@ -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();
|
||||
}
|
||||
}
|
||||
-160
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-37
@@ -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);
|
||||
}
|
||||
+23
-11
@@ -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;
|
||||
}
|
||||
+2
-42
@@ -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
|
||||
*/
|
||||
+6
@@ -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();
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -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")
|
||||
|
||||
-200
@@ -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();
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -55,5 +55,10 @@ public enum GeneralConfigTypeEnum {
|
||||
/**
|
||||
* system store config
|
||||
*/
|
||||
oss
|
||||
oss,
|
||||
|
||||
/**
|
||||
* model llm provider
|
||||
*/
|
||||
provider
|
||||
}
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
|
||||
+69
@@ -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 name:glm-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 name:glm-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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user