mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[GSOC] Chat UI, Conversation Management, OpenAI Chat Client Support, More monitoring tools. (#3679)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in> Co-authored-by: Calvin <zhengqiwei@apache.org> Co-authored-by: Jast <shenghang@apache.org> Co-authored-by: Duansg <siguoduan@gmail.com> Co-authored-by: DeleiGuo <deleiguo@163.com> Co-authored-by: shown <yuluo08290126@gmail.com> Co-authored-by: tomsun28 <tomsun28@outlook.com> Co-authored-by: Logic <zqr10159@dromara.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Calvin
Jast
Duansg
DeleiGuo
shown
tomsun28
Logic
Copilot
parent
1fae67282d
commit
feb43c5678
@@ -35,6 +35,10 @@
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
+39
-9
@@ -20,7 +20,10 @@ package org.apache.hertzbeat.ai.agent.adapters;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface that provides access to monitor information by retrieving monitor data
|
||||
@@ -28,14 +31,41 @@ import java.util.List;
|
||||
*/
|
||||
public interface MonitorServiceAdapter {
|
||||
Page<Monitor> getMonitors(
|
||||
List<Long> ids,
|
||||
String app,
|
||||
String search,
|
||||
Byte status,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
String labels
|
||||
List<Long> ids,
|
||||
String app,
|
||||
String search,
|
||||
Byte status,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
String labels
|
||||
);
|
||||
|
||||
/**
|
||||
* Add a new monitor
|
||||
*
|
||||
* @param monitor The monitor entity to create
|
||||
* @param params List of parameters for the monitor
|
||||
* @param collector Optional collector assignment
|
||||
* @return The created monitor ID
|
||||
*/
|
||||
Long addMonitor(Monitor monitor, List<Param> params, String collector);
|
||||
|
||||
/**
|
||||
* Get all available monitor types with their display names
|
||||
*
|
||||
* @param language Language code (e.g., "en-US", "zh-CN")
|
||||
* @return Map of monitor type key to display name
|
||||
*/
|
||||
Map<String, String> getAvailableMonitorTypes(String language);
|
||||
|
||||
/**
|
||||
* Get parameter definitions for a specific monitor type
|
||||
*
|
||||
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
|
||||
* @return List of parameter definitions for the monitor type
|
||||
*/
|
||||
List<ParamDefine> getMonitorParamDefines(String app);
|
||||
|
||||
}
|
||||
+117
-2
@@ -24,11 +24,14 @@ import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
import org.apache.hertzbeat.common.support.SpringContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
|
||||
@@ -63,7 +66,7 @@ public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
|
||||
if (pageSize == null) {
|
||||
pageSize = 8;
|
||||
}
|
||||
|
||||
|
||||
Object monitorService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject: {}", subjectSum);
|
||||
@@ -97,4 +100,116 @@ public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
|
||||
try {
|
||||
Object monitorService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for addMonitor: {}", subjectSum);
|
||||
|
||||
try {
|
||||
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert monitorService != null;
|
||||
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
|
||||
|
||||
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
|
||||
Method method = monitorService.getClass().getMethod(
|
||||
"addMonitor",
|
||||
Monitor.class, List.class, String.class,
|
||||
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
|
||||
|
||||
// Call the method with null dashboard
|
||||
method.invoke(monitorService, monitor, params, collector, null);
|
||||
|
||||
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
|
||||
return monitor.getId();
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: addMonitor", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke addMonitor via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getAvailableMonitorTypes(String language) {
|
||||
try {
|
||||
Object appService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
|
||||
|
||||
try {
|
||||
appService = SpringContextHolder.getBean("appServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert appService != null;
|
||||
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
|
||||
|
||||
// Provide default language if not specified
|
||||
if (language == null || language.trim().isEmpty()) {
|
||||
language = "en-US";
|
||||
}
|
||||
|
||||
// Call getI18nApps method: getI18nApps(String lang)
|
||||
Method method = appService.getClass().getMethod("getI18nApps", String.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
|
||||
|
||||
log.debug("Successfully retrieved {} monitor types", result.size());
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getI18nApps", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getI18nApps via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParamDefine> getMonitorParamDefines(String app) {
|
||||
try {
|
||||
Object appService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
|
||||
|
||||
try {
|
||||
appService = SpringContextHolder.getBean("appServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert appService != null;
|
||||
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
|
||||
|
||||
// Validate app parameter
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Monitor type/app parameter is required");
|
||||
}
|
||||
|
||||
// Call getAppParamDefines method: getAppParamDefines(String app)
|
||||
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
|
||||
|
||||
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getAppParamDefines", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
|
||||
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+57
@@ -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.
|
||||
*/
|
||||
|
||||
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.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)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DynamicOpenAiApiKey implements ApiKey {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
try {
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-3
@@ -20,6 +20,8 @@ package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
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.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -29,9 +31,42 @@ import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class LlmConfig {
|
||||
|
||||
/**
|
||||
* Create OpenAI API instance with dynamic API key
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
|
||||
return ChatClient.create(chatModel);
|
||||
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
|
||||
return OpenAiApi.builder()
|
||||
.apiKey(dynamicApiKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Create OpenAI Chat Options with custom settings
|
||||
*/
|
||||
@Bean
|
||||
public OpenAiChatOptions openAiChatOptions() {
|
||||
return OpenAiChatOptions.builder()
|
||||
.model("gpt-4.1-nano-2025-04-14")
|
||||
.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)
|
||||
.defaultOptions(openAiChatOptions)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
|
||||
return ChatClient.create(openAiChatModel);
|
||||
}
|
||||
|
||||
}
|
||||
+21
-4
@@ -15,12 +15,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Service interface for agent operations.
|
||||
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
|
||||
*/
|
||||
public interface AgentService {
|
||||
@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();
|
||||
}
|
||||
}
|
||||
+42
-18
@@ -31,23 +31,47 @@ public class PromptProvider {
|
||||
*/
|
||||
public static final String HERTZBEAT_MONITORING_PROMPT = """
|
||||
You are an AI assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
|
||||
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
|
||||
Your role is to help users manage and analyze their monitoring data using the available tools.
|
||||
You have access to the following HertzBeat monitoring tools:
|
||||
- list_monitors: Query monitor information with flexible filtering and pagination
|
||||
- add_monitor: Add a new monitor to the system
|
||||
When users ask questions about their monitoring setup or data, identify which tool would be most helpful
|
||||
and use it to provide relevant information. Always provide clear explanations of the monitoring data and
|
||||
suggest next steps or insights based on the results.
|
||||
For monitoring-related queries:
|
||||
1. If users want to see their monitors, use list_monitors with appropriate filters
|
||||
2. If users want to add a new monitor, use add_monitor with the necessary details
|
||||
3. If the monitoring information shows potential issues, highlight them and suggest troubleshooting steps
|
||||
For parameters that accept specific values:
|
||||
- Monitor status values: 0 (no monitor), 1 (usable), 2 (disabled), 9 (all)
|
||||
- Sort fields typically include: name, host, app, gmtCreate
|
||||
- Sort order should be 'asc' or 'desc'
|
||||
Keep responses focused on monitoring topics and HertzBeat capabilities.
|
||||
If you're unsure about specific monitoring details, ask clarifying questions before using the tools.
|
||||
|
||||
## Available HertzBeat Monitoring Tools:
|
||||
- **list_monitors**: Query monitor information with detailed output (ID, name, type, host, status)
|
||||
- **add_monitor**: Add a new monitor to the system with comprehensive configuration
|
||||
- **list_monitor_types**: List all available monitor types (linux, mysql, redis, http, etc.)
|
||||
- **get_monitor_param_defines**: Get parameter definitions required for specific monitor types
|
||||
|
||||
## HertzBeat Monitor Types:
|
||||
HertzBeat supports monitoring of:
|
||||
- **Operating Systems**: Linux, Windows, FreeBSD, macOS, etc.
|
||||
- **Databases**: MySQL, PostgreSQL, Redis, MongoDB, Oracle, SQL Server, etc.
|
||||
- **Application Services**: Tomcat, Spring Boot, Elasticsearch, Kafka, etc.
|
||||
- **Network & Infrastructure**: HTTP/HTTPS websites, DNS, ping, SSL certificates, etc.
|
||||
- **Cloud Services**: AWS, Azure, Kubernetes, Docker, etc.
|
||||
- **Custom Monitoring**: Through YAML templates and various protocols (HTTP, JDBC, SSH, JMX, SNMP, etc.)
|
||||
|
||||
## Workflow Guidelines:
|
||||
1. **For viewing monitors**: Use list_monitors with appropriate filters (by type, status, host, etc.)
|
||||
2. **For adding monitors**:
|
||||
- First use list_monitor_types to show available types
|
||||
- Then use get_monitor_param_defines to show required parameters for the chosen type
|
||||
- Finally use add_monitor with all necessary parameters
|
||||
|
||||
## Parameter Values:
|
||||
- **Monitor status**: 0 (no monitor), 1 (usable), 2 (disabled), 9 (all)
|
||||
- **Sort fields**: name, host, app, gmtCreate, gmtUpdate
|
||||
- **Sort order**: 'asc' or 'desc'
|
||||
- **Monitor intervals**: Typically 30s to 3600s (30 seconds to 1 hour)
|
||||
|
||||
## Best Practices:
|
||||
- Always validate monitor types using list_monitor_types before adding
|
||||
- Check parameter requirements using get_monitor_param_defines for each monitor type
|
||||
- Provide clear explanations of monitoring data and suggest actionable insights
|
||||
- For performance issues, recommend appropriate collection intervals
|
||||
- Explain HertzBeat's template-based YAML monitoring definitions when relevant
|
||||
|
||||
Keep responses focused on monitoring topics and HertzBeat's comprehensive monitoring capabilities.
|
||||
If you're unsure about specific monitoring requirements, use the parameter definition tools to get
|
||||
accurate information.
|
||||
""";
|
||||
|
||||
}
|
||||
}
|
||||
+161
-34
@@ -15,56 +15,183 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.controller;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.ConversationService;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
|
||||
/**
|
||||
* Controller class for handling chat-related HTTP requests.
|
||||
* Controller class for handling AI chat requests and conversation management.
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "AI Chat API")
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
|
||||
public class ChatController {
|
||||
|
||||
private final ChatClientProviderService chatClientProviderService;
|
||||
private final ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
public ChatController(@Qualifier("openAiChatClient") ChatClient openAiChatClient,
|
||||
ChatClientProviderService chatClientProviderService) {
|
||||
this.chatClientProviderService = chatClientProviderService;
|
||||
public ChatController(ConversationService conversationService) {
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get a streaming response
|
||||
*
|
||||
* @param context The chat request context containing message and optional
|
||||
* conversationId
|
||||
* @return SSE emitter for streaming response
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation details
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamChat(@RequestBody ChatRequestContext context) {
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String aiResponse = chatClientProviderService.streamChat(context);
|
||||
emitter.send(aiResponse);
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}).start();
|
||||
return emitter;
|
||||
@PostMapping(path = "/conversations")
|
||||
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
|
||||
public ResponseEntity<Message<ConversationDto>> createConversation() {
|
||||
try {
|
||||
ConversationDto conversation = conversationService.createConversation();
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating conversation: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get a streaming response with conversation tracking
|
||||
*
|
||||
* @param context The chat request context containing message and optional conversationId
|
||||
* @return Flux of ServerSentEvent for streaming response
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
|
||||
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
|
||||
try {
|
||||
// Validate message is not empty
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
log.info(subject.toString());
|
||||
McpContextHolder.setSubject(subject);
|
||||
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("Error: Message cannot be empty")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||
return conversationService.streamChat(context.getMessage(), context.getConversationId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error in stream chat endpoint: ", e);
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("An error occurred: " + e.getMessage())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations
|
||||
*
|
||||
* @return List of all conversations
|
||||
*/
|
||||
@GetMapping(path = "/conversations")
|
||||
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
|
||||
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
|
||||
try {
|
||||
List<ConversationDto> conversations = conversationService.getAllConversations();
|
||||
return ResponseEntity.ok(Message.success(conversations));
|
||||
} catch (Exception e) {
|
||||
log.error("Error listing conversations: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*
|
||||
* @param conversationId The conversation ID
|
||||
* @return Conversation details with message history
|
||||
*/
|
||||
@GetMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
|
||||
public ResponseEntity<Message<ConversationDto>> getConversation(
|
||||
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
|
||||
try {
|
||||
// Validate conversation ID
|
||||
if (conversationId == null || conversationId.trim().isEmpty()) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
|
||||
}
|
||||
|
||||
ConversationDto conversation = conversationService.getConversation(conversationId);
|
||||
|
||||
if (conversation == null) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting conversation: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId The conversation ID to delete
|
||||
* @return Success or error message
|
||||
*/
|
||||
@DeleteMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
|
||||
public ResponseEntity<Message<Void>> deleteConversation(
|
||||
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
|
||||
try {
|
||||
// Validate conversation ID
|
||||
if (conversationId == null || conversationId.trim().isEmpty()) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
|
||||
}
|
||||
|
||||
boolean deleted = conversationService.deleteConversation(conversationId);
|
||||
if (!deleted) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error deleting conversation: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration API
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
|
||||
@Tag(name = "OpenAI Configuration API")
|
||||
@Slf4j
|
||||
public class OpenAiConfigController {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@PostMapping("/openai")
|
||||
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
|
||||
try {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
// Validate API key if enabled
|
||||
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
|
||||
|
||||
if (!validationResult.isValid()) {
|
||||
log.warn("API key validation failed during save: {}", validationResult.getMessage());
|
||||
response.put("code", 1);
|
||||
response.put("msg", "API key validation failed: " + validationResult.getMessage());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
log.info("API key validation successful during save");
|
||||
}
|
||||
|
||||
// Save the configuration
|
||||
openAiConfigService.saveConfig(config);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("msg", "OpenAI configuration saved successfully");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to save configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai")
|
||||
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
|
||||
try {
|
||||
OpenAiConfigDto config = openAiConfigService.getConfig();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", config);
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai/status")
|
||||
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
|
||||
try {
|
||||
boolean configured = openAiConfigService.isConfigured();
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
boolean hasDbConfig = openAiConfigService.getConfig() != null;
|
||||
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
|
||||
|
||||
// Validate the effective configuration
|
||||
boolean validationPassed = false;
|
||||
String validationMessage = "No configuration found";
|
||||
|
||||
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
|
||||
validationPassed = validationResult.isValid();
|
||||
validationMessage = validationResult.getMessage();
|
||||
|
||||
if (!validationPassed) {
|
||||
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", Map.of(
|
||||
"configured", configured && validationPassed,
|
||||
"hasDbConfig", hasDbConfig,
|
||||
"hasYamlConfig", hasYamlConfig,
|
||||
"validationPassed", validationPassed,
|
||||
"validationMessage", validationMessage
|
||||
));
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration status", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get status: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +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;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for Message entities.
|
||||
*/
|
||||
public interface MessageDao {
|
||||
}
|
||||
+16
-4
@@ -15,11 +15,23 @@
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for Conversation entities.
|
||||
* OpenAI Agent Configuration Dao
|
||||
*/
|
||||
public interface ConversationDao {
|
||||
}
|
||||
@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);
|
||||
}
|
||||
-25
@@ -1,25 +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;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for UserPreference entities.
|
||||
*/
|
||||
public interface UserPreferenceDao {
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.entity;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* OpenAI Agent Config Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_ai_agent_config")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI Agent config entity")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class OpenAiConfig {
|
||||
|
||||
@Id
|
||||
@Schema(title = "Config type: openai, primary key", description = "Config type: openai, primary key",
|
||||
accessMode = READ_WRITE)
|
||||
@NotBlank(message = "type can not null")
|
||||
private String type;
|
||||
|
||||
@Schema(title = "Config content", description = "Config content,format json", accessMode = READ_WRITE)
|
||||
@Column(length = 8192)
|
||||
private String content;
|
||||
|
||||
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
|
||||
@CreatedBy
|
||||
private String creator;
|
||||
|
||||
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
|
||||
@LastModifiedBy
|
||||
private String modifier;
|
||||
|
||||
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
|
||||
@CreatedDate
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
|
||||
@LastModifiedDate
|
||||
private LocalDateTime gmtUpdate;
|
||||
}
|
||||
+9
-4
@@ -15,12 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.event;
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.controller;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Controller for managing conversations.
|
||||
* OpenAI configuration change event
|
||||
*/
|
||||
public class ConversationController {
|
||||
public class OpenAiConfigChangeEvent extends ApplicationEvent {
|
||||
|
||||
}
|
||||
public OpenAiConfigChangeEvent(ApplicationContext source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -19,13 +19,17 @@
|
||||
package org.apache.hertzbeat.ai.agent.pojo.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Chat request context for AI chat endpoint.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatRequestContext {
|
||||
@@ -37,4 +41,9 @@ public class ChatRequestContext {
|
||||
* Optional conversation ID for context
|
||||
*/
|
||||
private String conversationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversation history messages for context
|
||||
*/
|
||||
private List<MessageDto> conversationHistory;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Chat response DTO for AI responses.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat response")
|
||||
public class ChatResponseDto {
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "AI response message", example = "Here are your monitors...")
|
||||
private String response;
|
||||
|
||||
@Schema(description = "User message ID", example = "msg-user-123")
|
||||
private String userMessageId;
|
||||
|
||||
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
|
||||
private String assistantMessageId;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Conversation DTO for AI chat conversations.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Conversation information")
|
||||
public class ConversationDto {
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "Creation time")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "Last updated time")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Schema(description = "Messages in this conversation")
|
||||
private List<MessageDto> messages;
|
||||
|
||||
@Schema(description = "Message count")
|
||||
private Integer messageCount;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Message DTO for chat messages.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat message")
|
||||
public class MessageDto {
|
||||
|
||||
@Schema(description = "Message ID", example = "msg-123")
|
||||
private String messageId;
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "Message content", example = "List all monitors")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
|
||||
private String role;
|
||||
|
||||
@Schema(description = "Message timestamp")
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration DTO - simplified to handle only API key
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI configuration")
|
||||
public class OpenAiConfigDto {
|
||||
|
||||
/**
|
||||
* Whether to enable OpenAI, default is false
|
||||
*/
|
||||
@Schema(title = "Enable OpenAI", description = "Whether OpenAI is enabled", example = "true")
|
||||
private boolean enable = false;
|
||||
|
||||
/**
|
||||
* OpenAI API key
|
||||
*/
|
||||
@Schema(title = "API Key", description = "OpenAI API key", example = "sk-...")
|
||||
@NotBlank(message = "API Key cannot be empty when enabled")
|
||||
private String apiKey;
|
||||
}
|
||||
+9
-4
@@ -19,13 +19,18 @@
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
|
||||
*/
|
||||
public interface ChatClientProviderService {
|
||||
|
||||
String complete(String message);
|
||||
|
||||
String streamChat(ChatRequestContext context);
|
||||
}
|
||||
/**
|
||||
* Stream chat response from the LLM
|
||||
*
|
||||
* @param context Chat request context containing message and conversation history
|
||||
* @return Flux of string chunks from the LLM response
|
||||
*/
|
||||
Flux<String> streamChat(ChatRequestContext context);
|
||||
}
|
||||
+26
-17
@@ -18,34 +18,34 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service for managing chat conversations and interactions with LLM providers.
|
||||
*/
|
||||
public interface ConversationService {
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation data
|
||||
*/
|
||||
ConversationDto createConversation();
|
||||
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return SseEmitter for streaming the response
|
||||
* @return Flux of ServerSentEvent for streaming the response
|
||||
*/
|
||||
SseEmitter streamChat(String message, String conversationId);
|
||||
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
|
||||
|
||||
/**
|
||||
* Send a message and get a complete response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return Response object containing the AI's response and conversation metadata
|
||||
*/
|
||||
Map<String, Object> chat(String message, String conversationId);
|
||||
|
||||
/**
|
||||
* Get conversation history for a specific conversation
|
||||
@@ -53,19 +53,28 @@ public interface ConversationService {
|
||||
* @param conversationId Conversation ID
|
||||
* @return Conversation data including messages
|
||||
*/
|
||||
Map<String, Object> getConversation(String conversationId);
|
||||
ConversationDto getConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Get all conversations for the current user
|
||||
*
|
||||
* @return List of conversations
|
||||
*/
|
||||
List<Map<String, Object>> getAllConversations();
|
||||
List<ConversationDto> getAllConversations();
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId Conversation ID to delete
|
||||
* @return true if deleted, false if conversation not found
|
||||
*/
|
||||
void deleteConversation(String conversationId);
|
||||
}
|
||||
boolean deleteConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Check if a conversation exists
|
||||
*
|
||||
* @param conversationId Conversation ID to check
|
||||
* @return true if conversation exists, false otherwise
|
||||
*/
|
||||
boolean conversationExists(String conversationId);
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration Service
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
* Validate OpenAI API key by calling the OpenAI API
|
||||
* @param apiKey the API key to validate
|
||||
* @return validation result with success status and message
|
||||
*/
|
||||
ValidationResult validateApiKey(String apiKey);
|
||||
|
||||
/**
|
||||
* Force reload of OpenAI configuration cache
|
||||
* This method is typically called when configuration changes
|
||||
*/
|
||||
void reloadConfig();
|
||||
|
||||
/**
|
||||
* Validation result class
|
||||
*/
|
||||
class ValidationResult {
|
||||
private final boolean valid;
|
||||
private final String message;
|
||||
|
||||
private ValidationResult(boolean valid, String message) {
|
||||
this.valid = valid;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static ValidationResult success(String message) {
|
||||
return new ValidationResult(true, message);
|
||||
}
|
||||
|
||||
public static ValidationResult failure(String message) {
|
||||
return new ValidationResult(false, message);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +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 org.apache.hertzbeat.ai.agent.service.AgentService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Implementation of the AgentService interface.
|
||||
* This service provides functionality for handling AI agent operations.
|
||||
*/
|
||||
@Service
|
||||
public class AgentServiceImpl implements AgentService {
|
||||
}
|
||||
+43
-12
@@ -18,20 +18,30 @@
|
||||
|
||||
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.service.ChatClientProviderService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ChatClientProviderService}.
|
||||
* Provides functionality to interact with the ChatClient for handling chat
|
||||
* messages.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
@@ -42,11 +52,10 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
@Autowired
|
||||
public ChatClientProviderServiceImpl(@Qualifier("openAiChatClient") ChatClient openAiChatClient) {
|
||||
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
|
||||
this.chatClient = openAiChatClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String complete(String message) {
|
||||
return this.chatClient.prompt()
|
||||
.user(message)
|
||||
@@ -55,16 +64,38 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
}
|
||||
|
||||
@Override
|
||||
public String streamChat(ChatRequestContext context) {
|
||||
public Flux<String> streamChat(ChatRequestContext context) {
|
||||
try {
|
||||
return this.chatClient.prompt(PromptProvider.HERTZBEAT_MONITORING_PROMPT)
|
||||
.user(context.getMessage())
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.call()
|
||||
.content();
|
||||
} catch (Exception e) {
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
List<Message> messages = new ArrayList<>();
|
||||
|
||||
// Add conversation history if available
|
||||
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
|
||||
for (MessageDto historyMessage : context.getConversationHistory()) {
|
||||
if ("user".equals(historyMessage.getRole())) {
|
||||
messages.add(new UserMessage(historyMessage.getContent()));
|
||||
} else if ("assistant".equals(historyMessage.getRole())) {
|
||||
messages.add(new AssistantMessage(historyMessage.getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.add(new UserMessage(context.getMessage()));
|
||||
|
||||
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||
|
||||
return this.chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(PromptProvider.HERTZBEAT_MONITORING_PROMPT)
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.stream()
|
||||
.content()
|
||||
.doOnNext(chunk -> log.debug("Received chunk: {}", chunk))
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
-3
@@ -15,15 +15,262 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
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;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implementation of the ConversationService interface for managing chat conversations.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ConversationServiceImpl {
|
||||
public class ConversationServiceImpl implements ConversationService {
|
||||
|
||||
}
|
||||
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
|
||||
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private ChatClientProviderService chatClientProviderService;
|
||||
|
||||
@Autowired
|
||||
private OpenAiConfigService openAiConfigService;
|
||||
|
||||
@Override
|
||||
public ConversationDto createConversation() {
|
||||
String conversationId = createNewConversation();
|
||||
return getConversation(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
|
||||
// Validate conversation exists
|
||||
if (!conversationExists(conversationId)) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("Error: Conversation not found: " + conversationId)
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
// Check if OpenAI is properly configured
|
||||
if (!openAiConfigService.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.")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Starting streaming conversation: {}", conversationId);
|
||||
|
||||
// Add user message to conversation
|
||||
String userMessageId = addMessageToConversation(conversationId, message, "user");
|
||||
|
||||
// Get conversation history for context
|
||||
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
|
||||
List<MessageDto> conversationHistory = new ArrayList<>();
|
||||
|
||||
if (messagesList != null && messagesList.size() > 1) {
|
||||
// Get all messages except the last one (which is the current user message we just added)
|
||||
for (int i = 0; i < messagesList.size() - 1; i++) {
|
||||
Map<String, Object> msgMap = messagesList.get(i);
|
||||
conversationHistory.add(mapToMessageDto(msgMap));
|
||||
}
|
||||
}
|
||||
|
||||
ChatRequestContext context = ChatRequestContext.builder()
|
||||
.message(message)
|
||||
.conversationId(conversationId)
|
||||
.conversationHistory(conversationHistory)
|
||||
.build();
|
||||
|
||||
// Stream response from AI service
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
return chatClientProviderService.streamChat(context)
|
||||
.map(chunk -> {
|
||||
fullResponse.append(chunk);
|
||||
ChatResponseDto responseDto = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response(chunk)
|
||||
.userMessageId(userMessageId)
|
||||
.build();
|
||||
|
||||
return ServerSentEvent.builder(responseDto)
|
||||
.event("message")
|
||||
.build();
|
||||
})
|
||||
.concatWith(Flux.defer(() -> {
|
||||
// Add the complete AI response to conversation
|
||||
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
|
||||
|
||||
ChatResponseDto finalResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("")
|
||||
.userMessageId(userMessageId)
|
||||
.assistantMessageId(assistantMessageId)
|
||||
.build();
|
||||
|
||||
return Flux.just(ServerSentEvent.builder(finalResponse)
|
||||
.event("complete")
|
||||
.build());
|
||||
}))
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
|
||||
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
|
||||
.onErrorResume(error -> {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("An error occurred: " + error.getMessage())
|
||||
.userMessageId(userMessageId)
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationDto getConversation(String conversationId) {
|
||||
if (conversationId == null || conversationId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> conversation = conversations.get(conversationId);
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
|
||||
List<MessageDto> messages = messagesList != null
|
||||
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
|
||||
new ArrayList<>();
|
||||
|
||||
return ConversationDto.builder()
|
||||
.conversationId((String) conversation.get("conversationId"))
|
||||
.createdAt((LocalDateTime) conversation.get("createdAt"))
|
||||
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
|
||||
.messages(messages)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConversationDto> getAllConversations() {
|
||||
List<ConversationDto> result = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
|
||||
Map<String, Object> conv = entry.getValue();
|
||||
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
|
||||
|
||||
ConversationDto dto = ConversationDto.builder()
|
||||
.conversationId((String) conv.get("conversationId"))
|
||||
.createdAt((LocalDateTime) conv.get("createdAt"))
|
||||
.updatedAt((LocalDateTime) conv.get("updatedAt"))
|
||||
.messages(new ArrayList<>()) // Don't include messages in list view for performance
|
||||
.build();
|
||||
result.add(dto);
|
||||
}
|
||||
|
||||
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteConversation(String conversationId) {
|
||||
if (conversationId == null || conversationId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean existed = conversations.containsKey(conversationId);
|
||||
if (existed) {
|
||||
conversations.remove(conversationId);
|
||||
conversationMessages.remove(conversationId);
|
||||
log.info("Deleted conversation: {}", conversationId);
|
||||
}
|
||||
return existed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean conversationExists(String conversationId) {
|
||||
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
|
||||
}
|
||||
|
||||
private String createNewConversation() {
|
||||
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
Map<String, Object> conversation = new HashMap<>();
|
||||
conversation.put("conversationId", conversationId);
|
||||
conversation.put("createdAt", now);
|
||||
conversation.put("updatedAt", now);
|
||||
|
||||
conversations.put(conversationId, conversation);
|
||||
conversationMessages.put(conversationId, new ArrayList<>());
|
||||
|
||||
log.info("Created new conversation: {}", conversationId);
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
|
||||
return MessageDto.builder()
|
||||
.messageId((String) messageMap.get("messageId"))
|
||||
.conversationId((String) messageMap.get("conversationId"))
|
||||
.content((String) messageMap.get("content"))
|
||||
.role((String) messageMap.get("role"))
|
||||
.timestamp((LocalDateTime) messageMap.get("timestamp"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String addMessageToConversation(String conversationId, String content, String role) {
|
||||
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
|
||||
|
||||
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("messageId", messageId);
|
||||
message.put("conversationId", conversationId);
|
||||
message.put("content", content);
|
||||
message.put("role", role);
|
||||
message.put("timestamp", LocalDateTime.now());
|
||||
|
||||
messages.add(message);
|
||||
|
||||
// Update conversation timestamp
|
||||
Map<String, Object> conversation = conversations.get(conversationId);
|
||||
if (conversation != null) {
|
||||
conversation.put("updatedAt", LocalDateTime.now());
|
||||
// Auto-generate title from first user message
|
||||
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
|
||||
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
|
||||
conversation.put("title", title);
|
||||
}
|
||||
}
|
||||
|
||||
return messageId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
+43
-7
@@ -18,8 +18,6 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -27,13 +25,43 @@ import java.util.List;
|
||||
*/
|
||||
public interface MonitorTools {
|
||||
|
||||
String addMonitor(String name, ToolContext context);
|
||||
/**
|
||||
* Add a new monitor with comprehensive configuration
|
||||
*
|
||||
* @param name Monitor name
|
||||
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||
* @param host Target host (IP address or domain name)
|
||||
* @param port Target port (optional, depends on monitor type)
|
||||
* @param intervals Collection interval in seconds (default: 600)
|
||||
* @param username Username for authentication (optional)
|
||||
* @param password Password for authentication (optional)
|
||||
* @param description Monitor description (optional)
|
||||
* @return Result message with monitor ID if successful
|
||||
*/
|
||||
String addMonitor(
|
||||
String name,
|
||||
String app,
|
||||
String host,
|
||||
Integer port,
|
||||
Integer intervals,
|
||||
String username,
|
||||
String password,
|
||||
String description
|
||||
);
|
||||
|
||||
/**
|
||||
* List all available monitor types that can be added
|
||||
*
|
||||
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
|
||||
* @return Formatted string list of available monitor types with descriptions
|
||||
*/
|
||||
String listMonitorTypes(String language);
|
||||
|
||||
/**
|
||||
* Query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns results as plain JSON.
|
||||
* Returns results as plain JSON string for AI tool.
|
||||
*/
|
||||
String listMonitors(
|
||||
List<Long> ids,
|
||||
@@ -44,7 +72,15 @@ public interface MonitorTools {
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
ToolContext context);
|
||||
Integer pageSize);
|
||||
|
||||
}
|
||||
/**
|
||||
* Get parameter definitions required for a specific monitor type
|
||||
*
|
||||
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
|
||||
* @return Formatted string with parameter definitions including field names, types, and requirements
|
||||
*/
|
||||
String getMonitorParamDefines(String app);
|
||||
|
||||
|
||||
}
|
||||
+298
-15
@@ -21,7 +21,6 @@ import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -29,8 +28,13 @@ import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implementation of Monitoring Tools functionality
|
||||
@@ -47,13 +51,15 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
* Tool to query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns monitor names as string.
|
||||
* Returns detailed monitor information including ID, name, type, host, and status.
|
||||
*/
|
||||
@Override
|
||||
@Tool(name = "list_monitors", returnDirect = true, description = """
|
||||
@Tool(name = "list_monitors", description = """
|
||||
Query monitor information with flexible filtering and pagination.
|
||||
Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
|
||||
Returns results as String. When no parameters are available, pass the default value as mentioned below. If the user doesn't provide any specific parameter, the default value will be used.
|
||||
Returns detailed results including monitor ID, name, type, host, and status for easy identification and management.
|
||||
Show the long monitor id in the brackets
|
||||
When no parameters are available, pass the default value as mentioned below. If the user doesn't provide any specific parameter, the default value will be used.
|
||||
""")
|
||||
public String listMonitors(
|
||||
@ToolParam(description = "List of monitor IDs to filter (default: empty list)", required = false) List<Long> ids,
|
||||
@@ -64,24 +70,301 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
@ToolParam(description = "Sort field, e.g., 'name' (default: gmtCreate)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order, 'asc' or 'desc' (default: desc)", required = false) String order,
|
||||
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size (default: 8)", required = false) Integer pageSize,
|
||||
ToolContext context) {
|
||||
@ToolParam(description = "Page size (default: 8)", required = false) Integer pageSize) {
|
||||
try {
|
||||
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
|
||||
return result.getContent().stream().map(Monitor::getName).toList().toString();
|
||||
|
||||
// Format response to include both ID and name for better usability
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Found ").append(result.getContent().size()).append(" monitors:\n\n");
|
||||
|
||||
for (Monitor monitor : result.getContent()) {
|
||||
log.info(String.valueOf(monitor.getId()));
|
||||
response.append("ID: ").append(monitor.getId())
|
||||
.append(" | Name: ").append(monitor.getName())
|
||||
.append(" | Type: ").append(monitor.getApp())
|
||||
.append(" | Host: ").append(monitor.getHost())
|
||||
.append(" | Status: ").append(getStatusText(monitor.getStatus()))
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
if (result.getContent().isEmpty()) {
|
||||
response.append("No monitors found matching the specified criteria.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
} catch (Exception e) {
|
||||
return "error is" + e.getMessage();
|
||||
return "Error retrieving monitors: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor", description = """
|
||||
Add a new monitor to HertzBeat with comprehensive configuration.
|
||||
This tool creates a monitor for various types like linux, mysql, http, redis, etc.
|
||||
validate the the monitor type using the list_monitor_types tool.
|
||||
ALWAYS Ask the user to give all the the parameter definitions for the asked monitor type using the get_monitor_param_defines tool.
|
||||
""")
|
||||
public String addMonitor(
|
||||
@ToolParam(description = "Monitor name (required)", required = true) String name,
|
||||
@ToolParam(description = "Monitor type/application: linux, mysql, http, redis, postgresql, etc.", required = true) String app,
|
||||
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
|
||||
@ToolParam(description = "Target port (optional, depends on monitor type)", required = false) Integer port,
|
||||
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
|
||||
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
|
||||
try {
|
||||
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in add_monitor tool: {}", subjectSum);
|
||||
|
||||
// Validate required parameters
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "Error: Monitor name is required";
|
||||
}
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application is required";
|
||||
}
|
||||
if (host == null || host.trim().isEmpty()) {
|
||||
return "Error: Host is required";
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if (intervals == null || intervals < 10) {
|
||||
intervals = 600; // Default 10 minutes
|
||||
}
|
||||
|
||||
// Create Monitor entity
|
||||
Monitor monitor = Monitor.builder()
|
||||
.name(name.trim())
|
||||
.app(app.toLowerCase().trim())
|
||||
.host(host.trim())
|
||||
.intervals(intervals)
|
||||
.status((byte) 1) // Status: Up
|
||||
.type((byte) 0) // Type: Normal
|
||||
.description(description != null ? description.trim() : "")
|
||||
.build();
|
||||
|
||||
// Create parameters list
|
||||
List<Param> params = new ArrayList<>();
|
||||
|
||||
// Add host parameter (always required)
|
||||
params.add(Param.builder()
|
||||
.field("host")
|
||||
.paramValue(host.trim())
|
||||
.type((byte) 0)
|
||||
.build());
|
||||
|
||||
// Add port parameter if provided
|
||||
if (port != null && port > 0) {
|
||||
params.add(Param.builder()
|
||||
.field("port")
|
||||
.paramValue(port.toString())
|
||||
.type((byte) 0)
|
||||
.build());
|
||||
}
|
||||
|
||||
// Add authentication parameters if provided
|
||||
if (username != null && !username.trim().isEmpty()) {
|
||||
params.add(Param.builder()
|
||||
.field("username")
|
||||
.paramValue(username.trim())
|
||||
.type((byte) 1) // Type: Password
|
||||
.build());
|
||||
}
|
||||
|
||||
if (password != null && !password.trim().isEmpty()) {
|
||||
params.add(Param.builder()
|
||||
.field("password")
|
||||
.paramValue(password.trim())
|
||||
.type((byte) 1) // Type: Password
|
||||
.build());
|
||||
}
|
||||
|
||||
// Add timeout parameter (default)
|
||||
params.add(Param.builder()
|
||||
.field("timeout")
|
||||
.paramValue("6000")
|
||||
.type((byte) 0)
|
||||
.build());
|
||||
|
||||
// Call the adapter to add the monitor
|
||||
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
|
||||
|
||||
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
|
||||
return String.format("Successfully added monitor '%s' with ID: %d. Monitor type: %s, Host: %s, Interval: %d seconds",
|
||||
name, monitorId, app, host, intervals);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
|
||||
return "Error adding monitor '" + name + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor", description = "Add a new monitor")
|
||||
public String addMonitor(@ToolParam(description = "Name of the monitor") String name, ToolContext context) {
|
||||
log.debug("Adding monitor with name: {}", name);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in tool: {}", subjectSum);
|
||||
return "Monitor added: " + name;
|
||||
@Tool(name = "list_monitor_types", description = """
|
||||
List all available monitor types that can be added to HerzBeat.
|
||||
This tool shows all supported monitor types with their display names.
|
||||
Use this to see what types of monitors you can create with the add_monitor tool.
|
||||
""")
|
||||
public String listMonitorTypes(
|
||||
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
|
||||
|
||||
try {
|
||||
log.info("Listing available monitor types for language: {}", language);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
|
||||
|
||||
// Set default language if not provided
|
||||
if (language == null || language.trim().isEmpty()) {
|
||||
language = "en-US";
|
||||
}
|
||||
|
||||
// Get available monitor types from adapter
|
||||
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
|
||||
|
||||
if (monitorTypes == null || monitorTypes.isEmpty()) {
|
||||
return "No monitor types are currently available.";
|
||||
}
|
||||
|
||||
// Format the response as a nice list
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
|
||||
|
||||
// Sort monitor types alphabetically by key
|
||||
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Map.Entry<String, String> entry : sortedTypes) {
|
||||
String typeKey = entry.getKey();
|
||||
String displayName = entry.getValue();
|
||||
response.append("• ").append(typeKey)
|
||||
.append(" - ").append(displayName)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
|
||||
response.append("\nExample: add_monitor(name='my-server', app='linux', host='192.168.1.100')");
|
||||
|
||||
log.info("Successfully listed {} monitor types", monitorTypes.size());
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list monitor types: {}", e.getMessage(), e);
|
||||
return "Error retrieving monitor types: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
@Tool(name = "get_monitor_param_defines", description = """
|
||||
Get the parameter definitions required for a specific monitor type.
|
||||
This tool shows what parameters are needed when adding a monitor of the specified type,
|
||||
including field names, data types, validation rules, and whether they are required.
|
||||
Use this before adding a monitor to understand what parameters you need to provide.
|
||||
""")
|
||||
public String getMonitorParamDefines(
|
||||
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
|
||||
|
||||
try {
|
||||
log.info("Getting parameter definitions for monitor type: {}", app);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
|
||||
|
||||
// Validate required parameter
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application parameter is required";
|
||||
}
|
||||
|
||||
// Get parameter definitions from adapter
|
||||
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
|
||||
|
||||
if (paramDefines == null || paramDefines.isEmpty()) {
|
||||
return String.format("No parameter definitions found for monitor type '%s'. "
|
||||
+ "This monitor type may not exist or may not require additional parameters.", app);
|
||||
}
|
||||
|
||||
// Format the response
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
|
||||
app, paramDefines.size()));
|
||||
|
||||
for (ParamDefine paramDefine : paramDefines) {
|
||||
response.append("• Field: ").append(paramDefine.getField()).append("\n");
|
||||
|
||||
// Add display name if available
|
||||
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
|
||||
response.append(" Name: ").append(paramDefine.getName()).append("\n");
|
||||
}
|
||||
|
||||
// Add type
|
||||
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
|
||||
response.append(" Type: ").append(paramDefine.getType()).append("\n");
|
||||
}
|
||||
|
||||
// Add required status
|
||||
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
|
||||
|
||||
// Add default value if present
|
||||
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
|
||||
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
|
||||
}
|
||||
|
||||
// Add validation range if present
|
||||
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
|
||||
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
|
||||
}
|
||||
|
||||
// Add limit if present
|
||||
if (paramDefine.getLimit() != null) {
|
||||
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
|
||||
}
|
||||
|
||||
// Add placeholder text if present
|
||||
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
|
||||
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
|
||||
}
|
||||
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
|
||||
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
|
||||
|
||||
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
|
||||
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert monitor status byte to readable text
|
||||
* @param status The status byte from monitor
|
||||
* @return Human-readable status text
|
||||
*/
|
||||
private String getStatusText(Byte status) {
|
||||
if (status == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "Paused";
|
||||
case 1:
|
||||
return "Online";
|
||||
case 2:
|
||||
return "Offline";
|
||||
case 3:
|
||||
return "Unreachable";
|
||||
default:
|
||||
return "Unknown (" + status + ")";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -68,6 +68,9 @@ resourceRole:
|
||||
- /api/bulletin/**===delete===[admin]
|
||||
- /api/sse/**===get===[admin,user]
|
||||
- /api/sse/**===post===[admin,user]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin,user]
|
||||
|
||||
# config the resource restful api that need bypass auth protection
|
||||
# rule: api===method
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
id: ai_agent_chat
|
||||
title: AI Agent Chat User Guide
|
||||
sidebar_label: AI Agent Chat
|
||||
keywords: [AI, Chat, Agent, Monitoring, Assistant, OpenAI]
|
||||
---
|
||||
|
||||
> HertzBeat AI Agent Chat is an intelligent monitoring assistant that helps you manage monitors, configure alerts, and optimize your infrastructure monitoring through natural language conversation.
|
||||
|
||||
## Overview
|
||||
|
||||
The AI Agent Chat feature provides an interactive chat interface where you can:
|
||||
|
||||
- 🔍 List and manage your existing monitors
|
||||
- ➕ Add new monitors for websites, APIs, databases, and services
|
||||
- 📊 Get detailed information about available monitor types and their parameters
|
||||
- ⚡ Check monitor status and troubleshoot monitoring issues
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the AI Agent Chat, ensure:
|
||||
|
||||
1 **OpenAI Configuration**: Valid OpenAI API key must be configured
|
||||
2 **Database Connection**: HertzBeat database must be accessible for monitor operations
|
||||
|
||||
## Configuration
|
||||
|
||||
### OpenAI API Key Setup
|
||||
|
||||
The AI Agent Chat uses OpenAI's GPT models. You need to configure an OpenAI API key in one of two ways:
|
||||
|
||||
#### Method 1: Database Configuration via UI (Recommended)
|
||||
|
||||
1. Navigate to the AI Agent Chat interface
|
||||
2. If no API key is configured, you'll see a configuration dialog
|
||||
3. Enter your OpenAI API key
|
||||
4. Click **Save Configuration**
|
||||
|
||||
#### Method 2: YAML Configuration
|
||||
|
||||
Add to your `application.yml`:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
ai:
|
||||
openai:
|
||||
api-key: sk-your-openai-api-key-here
|
||||
```
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
- **Database configuration** takes priority over YAML configuration
|
||||
- Configuration changes are applied immediately without restart
|
||||
- Invalid API keys are automatically validated before saving
|
||||
|
||||
## Using the AI Agent Chat
|
||||
|
||||
### Accessing the Chat Interface
|
||||
|
||||
1. Navigate to the **AI Agent Chat** section in HertzBeat web interface
|
||||
2. You'll see the HertzBeat Monitoring Assistant welcome screen
|
||||
3. Start typing your questions in the chat input field
|
||||
|
||||
### Sample Conversations
|
||||
|
||||
#### Adding a New Monitor
|
||||
|
||||
```text
|
||||
You: Add a new HTTP monitor for my website https://example.com
|
||||
Assistant: I'll help you add an HTTP monitor for https://example.com.
|
||||
Let me create that monitor with the following configuration:
|
||||
- Name: example.com Website Monitor
|
||||
- Type: HTTP
|
||||
- Host: example.com
|
||||
- Port: 443 (HTTPS)
|
||||
- Path: /
|
||||
- Interval: 60 seconds
|
||||
|
||||
Monitor created successfully with ID: 12345
|
||||
```
|
||||
|
||||
#### Listing Existing Monitors
|
||||
|
||||
```text
|
||||
You: Show me all my current monitors
|
||||
Assistant: Here are your current monitors:
|
||||
|
||||
1. **Linux Server** (ID: 1001)
|
||||
- Status: ✅ Online
|
||||
- Host: 192.168.1.100
|
||||
- Last Check: 2 minutes ago
|
||||
|
||||
2. **MySQL Database** (ID: 1002)
|
||||
- Status: ⚠️ Warning
|
||||
- Host: db.example.com
|
||||
- Last Check: 30 seconds ago
|
||||
|
||||
3. **Website Monitor** (ID: 1003)
|
||||
- Status: ✅ Online
|
||||
- Host: https://example.com
|
||||
- Last Check: 1 minute ago
|
||||
|
||||
Would you like details on any specific monitor?
|
||||
```
|
||||
|
||||
#### Getting Monitor Type Information
|
||||
|
||||
```text
|
||||
You: What parameters do I need to monitor a PostgreSQL database?
|
||||
Assistant: To monitor a PostgreSQL database, you'll need these parameters:
|
||||
|
||||
**Required Parameters:**
|
||||
- **Host**: Database server IP/hostname
|
||||
- **Port**: PostgreSQL port (default: 5432)
|
||||
- **Database**: Database name to connect to
|
||||
- **Username**: Database username
|
||||
- **Password**: Database password
|
||||
|
||||
**Optional Parameters:**
|
||||
- **Timeout**: Connection timeout (default: 6000ms)
|
||||
- **Query Timeout**: SQL query timeout (default: 3000ms)
|
||||
- **SSL**: Enable SSL connection (default: false)
|
||||
|
||||
Would you like me to help you add a PostgreSQL monitor?
|
||||
```
|
||||
|
||||
### Chat Features
|
||||
|
||||
#### Message History
|
||||
|
||||
- All conversations are automatically saved
|
||||
- Access previous conversations from the sidebar
|
||||
- Search through conversation history
|
||||
- Export conversation logs
|
||||
|
||||
#### Real-time Responses
|
||||
|
||||
- Streaming responses for immediate feedback
|
||||
- Typing indicators show when the assistant is processing
|
||||
- Cancel ongoing requests if needed
|
||||
|
||||
#### Conversation Management
|
||||
|
||||
- Create new conversations for different topics
|
||||
- Rename conversations for better organization
|
||||
- Delete old conversations to keep things clean
|
||||
Note: Conversations are not saved in the database
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Chat Interface Not Loading
|
||||
|
||||
**Symptoms**: Chat interface shows loading spinner indefinitely
|
||||
**Solutions**:
|
||||
|
||||
1. Check browser console for JavaScript errors
|
||||
2. Ensure network connectivity to HertzBeat server
|
||||
|
||||
#### "Service Unavailable" Message
|
||||
|
||||
**Symptoms**: Chat shows "HertzBeat AI monitoring service unavailable"
|
||||
**Solutions**:
|
||||
1.Verify OpenAI API key configuration
|
||||
2.Check application logs for errors
|
||||
3.Ensure database connectivity
|
||||
|
||||
#### Invalid API Key Error
|
||||
|
||||
**Symptoms**: Configuration dialog shows "Invalid API key" error
|
||||
**Solutions**:
|
||||
|
||||
1. Verify your OpenAI API key starts with `sk-`
|
||||
2. Check API key has sufficient credits/quota
|
||||
3. Test API key directly with OpenAI API
|
||||
4. Ensure no extra spaces in the API key
|
||||
|
||||
#### Monitor Creation Failures
|
||||
|
||||
**Symptoms**: AI suggests monitor configuration but creation fails
|
||||
**Solutions**:
|
||||
|
||||
1. Verify you have permissions to create monitors
|
||||
2. Check if monitor with same name already exists
|
||||
3. Ensure target host/service is accessible
|
||||
4. Review monitor parameter validation errors
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by setting log level to DEBUG for:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
org.apache.hertzbeat.ai.agent: DEBUG
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Effective Chat Usage
|
||||
|
||||
1. **Be Specific**: "Add HTTP monitor for api.example.com port 8080" vs "add a monitor"
|
||||
2. **Provide Context**: Mention if you want production vs test monitors
|
||||
3. **Ask Follow-ups**: Request configuration details if needed
|
||||
4. **Use Natural Language**: The AI understands conversational requests
|
||||
|
||||
### Monitor Management
|
||||
|
||||
1. **Naming Convention**: Use descriptive monitor names
|
||||
2. **Documentation**: Ask the AI to document complex configurations
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **API Key Security**: Store OpenAI API keys securely
|
||||
2. **Access Control**: Limit AI chat access to authorized users
|
||||
3. **Data Privacy**: Be mindful of sensitive information in chat logs
|
||||
4. **Network Security**: Ensure secure connections to OpenAI API
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```text
|
||||
You: Add HTTP monitors for all services in my staging environment:
|
||||
- api-staging.example.com:8080
|
||||
- web-staging.example.com:80
|
||||
- admin-staging.example.com:3000
|
||||
```
|
||||
|
||||
### Integration Suggestions
|
||||
|
||||
```text
|
||||
You: What's the best way to monitor a microservices architecture with 20+ services?
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires active internet connection for OpenAI API
|
||||
- OpenAI API usage incurs costs based on token consumption
|
||||
- Complex multi-step operations may require multiple interactions
|
||||
- Some advanced configurations may need manual setup
|
||||
- Rate limiting may apply based on OpenAI plan
|
||||
|
||||
## Support
|
||||
|
||||
For issues with AI Agent Chat:
|
||||
|
||||
1. Check this documentation first
|
||||
2. Review application logs for errors
|
||||
3. Test OpenAI API connectivity independently
|
||||
4. Contact HertzBeat support with specific error messages
|
||||
|
||||
---
|
||||
@@ -63,9 +63,11 @@ After saving, reload MCP in Cursor or restart the editor.
|
||||
|
||||
### Tools available
|
||||
|
||||
- list_monitors: Returns the list of names of all configured monitors.
|
||||
- **list_monitors**: Query monitor information with flexible filtering and pagination. Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
|
||||
- **add_monitor**: Add a new monitor to HertzBeat with comprehensive configuration. Creates monitors for various types like linux, mysql, http, redis, etc.
|
||||
- **list_monitor_types**: List all available monitor types that can be added to HertzBeat. Shows all supported monitor types with their display names.
|
||||
- **get_monitor_param_defines**: Get the parameter definitions required for a specific monitor type. Shows what parameters are needed when adding a monitor.
|
||||
|
||||
More tools are coming soon to expand management and query capabilities.
|
||||
|
||||
### Notes
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@
|
||||
"help/grafana_dashboard",
|
||||
"help/mcp_sse_server",
|
||||
"help/collector",
|
||||
"help/ai_agent_chat",
|
||||
"help/ai_config",
|
||||
"help/issue"
|
||||
]
|
||||
|
||||
@@ -328,174 +328,62 @@ global-footer {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* AI Assistant Chatbot styles */
|
||||
.ai-chatbot-container {
|
||||
/* AI Chat Button styles */
|
||||
.ai-chat-button-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ai-chatbot-button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
.ai-chat-button {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background-color: @primary-color; /* Use system theme color */
|
||||
background-color: @primary-color;
|
||||
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;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ai-chatbot-button:hover {
|
||||
.ai-chat-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Robot icon styles */
|
||||
.robot-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.ai-chat-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.robot-icon svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
fill: white; /* Ensure SVG is white */
|
||||
}
|
||||
|
||||
.ai-chatbot-window {
|
||||
position: absolute;
|
||||
bottom: 70px;
|
||||
right: 0;
|
||||
width: 350px;
|
||||
height: 500px;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ai-chatbot-window.maximized {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
bottom: 10vh;
|
||||
right: 10vw;
|
||||
}
|
||||
|
||||
.chatbot-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: @primary-color; /* Use system theme color */
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chatbot-title {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chatbot-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.control-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.chatbot-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;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.user-message {
|
||||
align-self: flex-end;
|
||||
background-color: @primary-color; /* Use system theme color */
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bot-message {
|
||||
align-self: flex-start;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.streaming-message {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.2); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
|
||||
}
|
||||
|
||||
.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: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chatbot-input {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.chatbot-input input {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
/* AI Chat Modal Styles */
|
||||
:host ::ng-deep .ai-chat-modal {
|
||||
.ant-modal {
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
border-radius: 16px 16px 0 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
border-radius: 0 0 16px 16px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
top: 8px !important;
|
||||
right: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ import { I18NService } from '@core';
|
||||
import { ALAIN_I18N_TOKEN, SettingsService, User } from '@delon/theme';
|
||||
import { LayoutDefaultOptions } from '@delon/theme/layout-default';
|
||||
import { environment } from '@env/environment';
|
||||
import { Observable, Subject, of } from 'rxjs';
|
||||
import { delay, tap, finalize, catchError, takeUntil } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { CONSTANTS } from '../../shared/constants';
|
||||
import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service';
|
||||
import { AiChatModalService } from '../../shared/services/ai-chat-modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-basic',
|
||||
@@ -75,9 +74,9 @@ import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service'
|
||||
</global-footer>
|
||||
<setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
|
||||
|
||||
<!-- AI Chatbot -->
|
||||
<div class="ai-chatbot-container">
|
||||
<div class="ai-chatbot-button" (click)="toggleChatbot()" *ngIf="!isChatbotOpen">
|
||||
<!-- AI Chat Button -->
|
||||
<div class="ai-chat-button-container">
|
||||
<div class="ai-chat-button" (click)="openChatModal()">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -91,69 +90,11 @@ import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="ai-chatbot-window" *ngIf="isChatbotOpen" [class.maximized]="isChatbotMaximized">
|
||||
<div class="chatbot-header">
|
||||
<div class="chatbot-title">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="white"
|
||||
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>
|
||||
<div class="chatbot-controls">
|
||||
<span class="control-item" (click)="toggleMaximize()" title="{{ isChatbotMaximized ? '还原' : '最大化' }}">
|
||||
<i nz-icon [nzType]="isChatbotMaximized ? 'fullscreen-exit' : 'fullscreen'" nzTheme="outline"></i>
|
||||
</span>
|
||||
<span class="control-item" (click)="toggleChatbot()" title="关闭">
|
||||
<i nz-icon nzType="close" nzTheme="outline"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chatbot-messages" #chatMessagesContainer>
|
||||
<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="currentBotMessage && isLoading" class="bot-message streaming-message">
|
||||
<div class="message-content">{{ currentBotMessage.content }}</div>
|
||||
<div class="message-time">{{ currentBotMessage.timestamp | date : 'HH:mm' }}</div>
|
||||
</div>
|
||||
<div *ngIf="isLoading && !currentBotMessage" 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()"
|
||||
[disabled]="isLoading"
|
||||
/>
|
||||
<button nz-button nzType="primary" [disabled]="!currentMessage.trim() || isLoading" (click)="sendMessage()">
|
||||
{{ 'ai.bot.send' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./basic.component.less']
|
||||
})
|
||||
export class LayoutBasicComponent implements OnInit, OnDestroy {
|
||||
export class LayoutBasicComponent implements OnDestroy {
|
||||
options: LayoutDefaultOptions = {
|
||||
logoExpanded: `./assets/brand_white.svg`,
|
||||
logoCollapsed: `./assets/logo.svg`
|
||||
@@ -163,6 +104,7 @@ export class LayoutBasicComponent implements OnInit, OnDestroy {
|
||||
showSettingDrawer = !environment.production;
|
||||
version = CONSTANTS.VERSION;
|
||||
currentYear = new Date().getFullYear();
|
||||
|
||||
get user(): User {
|
||||
return this.settings.user;
|
||||
}
|
||||
@@ -177,127 +119,20 @@ export class LayoutBasicComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// AI Chatbot related properties
|
||||
isChatbotOpen = false;
|
||||
isChatbotMaximized = false;
|
||||
chatMessages: ChatMessage[] = [];
|
||||
currentMessage = '';
|
||||
isLoading = false;
|
||||
currentBotMessage: ChatMessage | null = null;
|
||||
|
||||
// For subscription cleanup
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private settings: SettingsService,
|
||||
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
|
||||
private aiBotService: AiBotService
|
||||
private aiChatModalService: AiChatModalService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize welcome message
|
||||
this.chatMessages.push({
|
||||
content: this.i18nSvc.fanyi('ai.bot.greeting'),
|
||||
isUser: false,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
console.log('AI Chatbot initialization completed');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Cancel all subscriptions when component is destroyed
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
toggleChatbot(): void {
|
||||
this.isChatbotOpen = !this.isChatbotOpen;
|
||||
|
||||
if (!this.isChatbotOpen) {
|
||||
setTimeout(() => {
|
||||
this.isChatbotMaximized = false;
|
||||
}, 300);
|
||||
} else {
|
||||
// Scroll to bottom when window opens
|
||||
setTimeout(() => this.scrollToBottom(), 100);
|
||||
}
|
||||
|
||||
console.log('Toggle chatbot status:', this.isChatbotOpen ? 'open' : 'closed');
|
||||
}
|
||||
|
||||
toggleMaximize(): void {
|
||||
setTimeout(() => {
|
||||
this.isChatbotMaximized = !this.isChatbotMaximized;
|
||||
console.log('Chat window maximize status:', this.isChatbotMaximized ? 'maximized' : 'normal');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
sendMessage(): void {
|
||||
if (!this.currentMessage.trim() || this.isLoading) return;
|
||||
|
||||
// Add user message
|
||||
this.chatMessages.push({
|
||||
content: this.currentMessage,
|
||||
isUser: true,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
const userMessage = this.currentMessage;
|
||||
this.currentMessage = '';
|
||||
this.isLoading = true;
|
||||
this.currentBotMessage = null;
|
||||
|
||||
// Ensure scrolling to bottom after message display
|
||||
setTimeout(() => this.scrollToBottom(), 100);
|
||||
|
||||
// Call AI service to get response
|
||||
this.aiBotService
|
||||
.sendMessage(userMessage)
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
finalize(() => {
|
||||
this.isLoading = false;
|
||||
|
||||
// If there is a current message, add it to chat history
|
||||
if (this.currentBotMessage) {
|
||||
this.chatMessages.push({ ...this.currentBotMessage });
|
||||
this.currentBotMessage = null;
|
||||
}
|
||||
|
||||
// Ensure scrolling to bottom after message display
|
||||
setTimeout(() => this.scrollToBottom(), 100);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: response => {
|
||||
console.log('Received AI response update:', response);
|
||||
// Update currently receiving message
|
||||
this.currentBotMessage = response;
|
||||
// Scroll to bottom in real-time
|
||||
this.scrollToBottom();
|
||||
},
|
||||
error: error => {
|
||||
console.error('AI response error:', error);
|
||||
// Add error message
|
||||
this.chatMessages.push({
|
||||
content: this.i18nSvc.fanyi('ai.bot.connect-fail'),
|
||||
isUser: false,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll to bottom of messages
|
||||
private scrollToBottom(): void {
|
||||
try {
|
||||
const chatMessages = document.querySelector('.chatbot-messages');
|
||||
if (chatMessages) {
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to scroll to bottom:', err);
|
||||
}
|
||||
openChatModal(): void {
|
||||
this.aiChatModalService.openChatModal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { Message } from '../pojo/Message';
|
||||
import { LocalStorageService } from './local-storage.service';
|
||||
|
||||
export interface ChatMessage {
|
||||
content: string;
|
||||
role: 'user' | 'assistant';
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface ConversationDto {
|
||||
conversationId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
export interface ChatRequestContext {
|
||||
message: string;
|
||||
conversationId?: string;
|
||||
}
|
||||
|
||||
const chat_uri = '/chat';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AiChatService {
|
||||
constructor(private http: HttpClient, private localStorageService: LocalStorageService) {}
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*/
|
||||
createConversation(): Observable<Message<ConversationDto>> {
|
||||
return this.http.post<Message<ConversationDto>>(`${chat_uri}/conversations`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations
|
||||
*/
|
||||
getConversations(): Observable<Message<ConversationDto[]>> {
|
||||
return this.http.get<Message<ConversationDto[]>>(`${chat_uri}/conversations`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific conversation with its history
|
||||
*/
|
||||
getConversation(conversationId: string): Observable<Message<ConversationDto>> {
|
||||
return this.http.get<Message<ConversationDto>>(`${chat_uri}/conversations/${conversationId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*/
|
||||
deleteConversation(conversationId: string): Observable<Message<void>> {
|
||||
return this.http.delete<Message<void>>(`${chat_uri}/conversations/${conversationId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get streaming response
|
||||
*/
|
||||
streamChat(message: string, conversationId?: string): Observable<ChatMessage> {
|
||||
const responseSubject = new Subject<ChatMessage>();
|
||||
|
||||
const requestBody: ChatRequestContext = { message };
|
||||
if (conversationId) {
|
||||
requestBody.conversationId = conversationId;
|
||||
}
|
||||
|
||||
const token = this.localStorageService.getAuthorizationToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/event-stream',
|
||||
'Cache-Control': 'no-cache'
|
||||
};
|
||||
|
||||
// Add Authorization header like the interceptor does
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Use fetch for SSE streaming (HttpClient doesn't support true streaming)
|
||||
fetch(`/api${chat_uri}/stream`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error('No reader available');
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
function readStream(): Promise<void> {
|
||||
if (!reader) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return reader.read().then(({ value, done }) => {
|
||||
if (done) {
|
||||
responseSubject.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
buffer += chunk;
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine.startsWith('data:')) {
|
||||
const jsonStr = trimmedLine.substring(5).trim();
|
||||
if (jsonStr && jsonStr !== '[DONE]') {
|
||||
try {
|
||||
const data = JSON.parse(jsonStr);
|
||||
if (data.response !== undefined) {
|
||||
responseSubject.next({
|
||||
content: data.response || '',
|
||||
role: 'assistant',
|
||||
timestamp: data.timestamp ? new Date(data.timestamp) : new Date()
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing SSE data:', parseError, 'Raw data:', jsonStr);
|
||||
if (jsonStr) {
|
||||
responseSubject.next({
|
||||
content: jsonStr,
|
||||
role: 'assistant',
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return readStream();
|
||||
});
|
||||
}
|
||||
|
||||
return readStream();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Chat stream error:', error);
|
||||
responseSubject.error(error);
|
||||
});
|
||||
|
||||
return responseSubject.asObservable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { DelonFormModule } from '@delon/form';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||
import { NzFormModule } from 'ng-zorro-antd/form';
|
||||
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 { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||
import { MarkdownPipe } from 'ngx-markdown';
|
||||
|
||||
import { ChatComponent } from './chat.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ChatComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
AlainThemeModule.forChild(),
|
||||
DelonFormModule,
|
||||
NzButtonModule,
|
||||
NzFormModule,
|
||||
NzIconModule,
|
||||
NzInputModule,
|
||||
NzMessageModule,
|
||||
NzModalModule,
|
||||
NzSpinModule,
|
||||
MarkdownPipe
|
||||
],
|
||||
exports: [ChatComponent]
|
||||
})
|
||||
export class AiChatModule {}
|
||||
@@ -0,0 +1,182 @@
|
||||
<!--
|
||||
* 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="chat-container">
|
||||
<!-- Sidebar with conversation list -->
|
||||
<div class="chat-sidebar" [class.collapsed]="sidebarCollapsed">
|
||||
<div class="sidebar-header">
|
||||
<h3>{{ 'menu.extras.ai.chat' | i18n }}</h3>
|
||||
<button nz-button nzType="primary" nzSize="small" (click)="createNewConversation()" [nzLoading]="isLoading">
|
||||
<i nz-icon nzType="plus"></i>
|
||||
New Chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<div
|
||||
*ngFor="let conversation of conversations"
|
||||
class="conversation-item"
|
||||
[class.active]="currentConversation?.conversationId === conversation.conversationId"
|
||||
(click)="selectConversation(conversation)"
|
||||
>
|
||||
<div class="conversation-content">
|
||||
<div class="conversation-title">
|
||||
{{ getConversationTitle(conversation) }}
|
||||
</div>
|
||||
<div class="conversation-time">
|
||||
{{ conversation.updatedAt | date : 'MMM d, HH:mm' }}
|
||||
</div>
|
||||
</div>
|
||||
<button nz-button nzType="text" nzSize="small" nzDanger (click)="deleteConversation(conversation, $event)" class="delete-btn">
|
||||
<i nz-icon nzType="delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main chat area -->
|
||||
<div class="chat-main">
|
||||
<!-- Header -->
|
||||
<div class="chat-header">
|
||||
<button nz-button nzType="text" (click)="toggleSidebar()" class="sidebar-toggle">
|
||||
<i nz-icon [nzType]="sidebarCollapsed ? 'menu-unfold' : 'menu-fold'"></i>
|
||||
</button>
|
||||
|
||||
<div class="chat-title">
|
||||
<h2 *ngIf="currentConversation">
|
||||
{{ getConversationTitle(currentConversation) }}
|
||||
</h2>
|
||||
<h2 *ngIf="!currentConversation"> HertzBeat AI Agent </h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages area -->
|
||||
<div class="messages-container" #messagesContainer>
|
||||
<!-- Loading indicator -->
|
||||
<div *ngIf="isLoading" class="loading-container">
|
||||
<nz-spin nzSimple [nzSize]="'large'">
|
||||
<ng-template #indicator>
|
||||
<i nz-icon nzType="loading" nzSpin></i>
|
||||
</ng-template>
|
||||
</nz-spin>
|
||||
<p>Loading conversation history...</p>
|
||||
</div>
|
||||
|
||||
<!-- Welcome message -->
|
||||
<div *ngIf="messages.length === 0 && !isLoading" class="welcome-message">
|
||||
<div class="welcome-content">
|
||||
<img [src]="theme === 'dark' ? 'assets/logo_white.svg' : 'assets/logo.svg'" alt="HertzBeat Logo" class="welcome-icon" />
|
||||
<h3>Welcome to HertzBeat AI Agent!</h3>
|
||||
<p>
|
||||
I'm your intelligent monitoring companion, ready to help you manage and optimize your infrastructure monitoring. You can ask me
|
||||
to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>🔍 List and manage your existing monitors</li>
|
||||
<li>➕ Add new monitors for websites, APIs, databases, and services</li>
|
||||
<li>📊 Get detailed information about available monitor types and their parameters</li>
|
||||
<li>⚡ Check monitor status and troubleshoot monitoring issues</li>
|
||||
</ul>
|
||||
<p>Ask me anything about your monitoring setup!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat messages -->
|
||||
<div *ngFor="let message of messages; let i = index" class="message" [class.user-message]="message.role === 'user'">
|
||||
<div class="message-avatar">
|
||||
<i nz-icon [nzType]="message.role === 'user' ? 'user' : 'robot'"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-header">
|
||||
<span class="message-role">{{ message.role === 'user' ? 'You' : 'AI Assistant' }}</span>
|
||||
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
|
||||
<span *ngIf="message.role === 'assistant' && isLoading && i === messages.length - 1" class="streaming-indicator">
|
||||
<nz-spin nzSize="small"></nz-spin>
|
||||
</span>
|
||||
</div>
|
||||
<div class="message-text" [innerHTML]="message.content"></div>
|
||||
<div *ngIf="message.role === 'assistant' && message.content === '' && isLoading" class="typing-indicator">
|
||||
<nz-spin nzSize="small"></nz-spin>
|
||||
<span>Starting to type...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input area -->
|
||||
<div class="input-area">
|
||||
<div class="input-container">
|
||||
<nz-textarea-count [nzMaxCharacterCount]="1000">
|
||||
<textarea
|
||||
nz-input
|
||||
[(ngModel)]="newMessage"
|
||||
(keypress)="onKeyPress($event)"
|
||||
[disabled]="isLoading"
|
||||
placeholder="Ask me anything about monitoring..."
|
||||
rows="3"
|
||||
style="resize: none"
|
||||
></textarea>
|
||||
</nz-textarea-count>
|
||||
<button
|
||||
nz-button
|
||||
nzType="primary"
|
||||
(click)="sendMessage()"
|
||||
[disabled]="!newMessage.trim() || isLoading"
|
||||
[nzLoading]="isLoading"
|
||||
class="send-button"
|
||||
>
|
||||
<i nz-icon nzType="send"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-hint">
|
||||
<small>Press Enter to send, Shift+Enter for new line</small>
|
||||
<button nz-button nzType="link" nzSize="small" (click)="onShowConfigModal()" style="float: right">
|
||||
<i nz-icon nzType="setting"></i>
|
||||
Modify API Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Configuration Modal -->
|
||||
<nz-modal
|
||||
[(nzVisible)]="showConfigModal"
|
||||
nzTitle="OpenAI Configuration"
|
||||
(nzOnCancel)="onCloseConfigModal()"
|
||||
(nzOnOk)="onSaveOpenAiConfig()"
|
||||
nzMaskClosable="false"
|
||||
[nzClosable]="false"
|
||||
nzWidth="600px"
|
||||
[nzOkLoading]="configLoading"
|
||||
nzOkText="Validate & Save"
|
||||
nzCancelText="Cancel"
|
||||
>
|
||||
<div *nzModalContent class="-inner-content">
|
||||
<form nz-form nzLayout="vertical">
|
||||
<nz-form-item>
|
||||
<nz-form-label nzRequired="true">OpenAI 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>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
</div>
|
||||
</nz-modal>
|
||||
@@ -0,0 +1,649 @@
|
||||
/*
|
||||
* 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 '@delon/theme/index';
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: 80vh; // Use viewport height for modal context
|
||||
background: @layout-body-background;
|
||||
|
||||
.chat-sidebar {
|
||||
width: 300px;
|
||||
background: #fff !important;
|
||||
border-right: 1px solid @border-color-base;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.conversation-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid @border-color-split;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: @item-hover-bg;
|
||||
|
||||
.delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: @primary-1;
|
||||
border-left: 3px solid @primary-color;
|
||||
}
|
||||
|
||||
.conversation-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.conversation-title {
|
||||
font-size: 14px;
|
||||
color: @text-color;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conversation-time {
|
||||
font-size: 12px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: @layout-body-background;
|
||||
|
||||
.chat-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.sidebar-toggle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-title h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: @heading-color;
|
||||
}
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
background: @layout-body-background;
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.welcome-content {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
|
||||
.welcome-icon {
|
||||
width: 64px;
|
||||
height: 62px;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: @heading-color;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: @text-color;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
ul {
|
||||
text-align: left;
|
||||
color: @text-color;
|
||||
margin: 16px 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
margin-bottom: 24px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
|
||||
&.user-message {
|
||||
justify-content: flex-end;
|
||||
|
||||
.message-content {
|
||||
background: #3f51b5;
|
||||
color: #ffffff;
|
||||
|
||||
.message-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
.message-role {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
order: 2;
|
||||
margin-left: 12px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background: @background-color-light;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: @box-shadow-base;
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.message-role {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
||||
.streaming-indicator {
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: @primary-color;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
line-height: 1.5;
|
||||
color: @text-color;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
|
||||
// Markdown styling
|
||||
:global {
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background: @background-color-light;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: @code-family;
|
||||
font-size: 12px;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: @background-color-light;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
border: 1px solid @border-color-split;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid @border-color-base;
|
||||
padding-left: 12px;
|
||||
margin: 8px 0;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: @text-color-secondary;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-area {
|
||||
padding: 16px;
|
||||
border-top: 1px solid @border-color-base;
|
||||
background: #fff;
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
|
||||
nz-textarea-count {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
|
||||
small {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
.chat-sidebar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&.collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
.messages-container {
|
||||
.message {
|
||||
&.user-message .message-content,
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme overrides with maximum specificity
|
||||
:host-context([data-theme='dark']) .chat-container,
|
||||
[data-theme='dark'] .chat-container,
|
||||
body[data-theme='dark'] .chat-container {
|
||||
background: #000;
|
||||
|
||||
.chat-sidebar {
|
||||
background: #000 !important;
|
||||
border-right-color: @border-color-base;
|
||||
|
||||
.sidebar-header {
|
||||
border-bottom-color: @border-color-base;
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
.conversation-item {
|
||||
border-bottom-color: @border-color-split;
|
||||
background: transparent;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
|
||||
.conversation-content .conversation-title {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: fade(@white, 5%);
|
||||
border-left: 3px solid @primary-color;
|
||||
|
||||
.conversation-content .conversation-title {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-content {
|
||||
.conversation-title {
|
||||
color: fade(@white, 65%);
|
||||
}
|
||||
|
||||
.conversation-time {
|
||||
color: fade(@white, 45%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
background: #000;
|
||||
|
||||
.chat-header {
|
||||
border-bottom-color: @border-color-base;
|
||||
background: #000;
|
||||
|
||||
.chat-title h2 {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
background: #000;
|
||||
|
||||
.loading-container {
|
||||
p {
|
||||
color: fade(@white, 65%);
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-message .welcome-content {
|
||||
h3 {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
|
||||
p, ul {
|
||||
color: fade(@white, 65%);
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
.message-avatar {
|
||||
background: fade(@white, 8%);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
i {
|
||||
color: fade(@white, 65%);
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background: #141414 !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.message-header {
|
||||
.message-role {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: fade(@white, 45%);
|
||||
}
|
||||
|
||||
.streaming-indicator {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
color: fade(@white, 45%);
|
||||
}
|
||||
}
|
||||
|
||||
&.user-message .message-content {
|
||||
background: @primary-color;
|
||||
|
||||
.message-text {
|
||||
color: @white;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
.message-role {
|
||||
color: fade(@white, 90%);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: fade(@white, 70%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-area {
|
||||
background: #000 !important;
|
||||
border-top-color: @border-color-base;
|
||||
|
||||
.input-hint small {
|
||||
color: fade(@white, 45%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Force input styling for dark theme
|
||||
[data-theme='dark'] {
|
||||
.chat-container .input-area {
|
||||
.ant-input {
|
||||
background: fade(@white, 8%) !important;
|
||||
border-color: @border-color-split !important;
|
||||
color: fade(@white, 85%) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: fade(@white, 45%) !important;
|
||||
}
|
||||
|
||||
&:focus, &:hover {
|
||||
border-color: @primary-color !important;
|
||||
box-shadow: 0 0 0 2px fade(@primary-color, 20%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-data-count {
|
||||
color: fade(@white, 45%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional high-specificity overrides for dark theme
|
||||
[data-theme="dark"] .chat-container .chat-sidebar {
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .chat-container .chat-sidebar .sidebar-header {
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .chat-container .chat-main .messages-container .message .message-content {
|
||||
background: #141414 !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .chat-container .chat-main .input-area {
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
// OpenAI Configuration Modal Styles
|
||||
.config-alert {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.validation-result {
|
||||
margin-top: 8px;
|
||||
|
||||
nz-alert {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
nz-modal {
|
||||
.ant-modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
/*
|
||||
* 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, ViewChild, ElementRef, AfterViewChecked, ChangeDetectorRef } from '@angular/core';
|
||||
import { I18NService } from '@core';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
|
||||
import { AiChatService, ChatMessage, ConversationDto } from '../../../service/ai-chat.service';
|
||||
import { OpenAiConfigService, OpenAiConfig, OpenAiConfigStatus } from '../../../service/openai-config.service';
|
||||
import { ThemeService } from '../../../service/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chat',
|
||||
templateUrl: './chat.component.html',
|
||||
styleUrls: ['./chat.component.less']
|
||||
})
|
||||
export class ChatComponent implements OnInit, AfterViewChecked {
|
||||
@ViewChild('messagesContainer') private messagesContainer!: ElementRef;
|
||||
|
||||
conversations: ConversationDto[] = [];
|
||||
currentConversation: ConversationDto | null = null;
|
||||
messages: ChatMessage[] = [];
|
||||
newMessage = '';
|
||||
isLoading = false;
|
||||
sidebarCollapsed = false;
|
||||
theme: string = 'default';
|
||||
|
||||
// OpenAI Configuration
|
||||
isOpenAiConfigured = false;
|
||||
showConfigModal = false;
|
||||
configLoading = false;
|
||||
openAiConfig: OpenAiConfig = {
|
||||
enable: false,
|
||||
apiKey: ''
|
||||
};
|
||||
|
||||
constructor(
|
||||
private aiChatService: AiChatService,
|
||||
private message: NzMessageService,
|
||||
private modal: NzModalService,
|
||||
private i18n: I18NService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private themeSvc: ThemeService,
|
||||
private openAiConfigService: OpenAiConfigService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.theme = this.themeSvc.getTheme() || 'default';
|
||||
this.checkOpenAiConfiguration();
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
if (this.conversations.length === 0) {
|
||||
this.createNewConversation();
|
||||
} else {
|
||||
// Select the most recent conversation
|
||||
this.selectConversation(this.conversations[0]);
|
||||
}
|
||||
} else {
|
||||
console.error('Error in conversations response:', response);
|
||||
this.message.error(`Failed to load conversations: ${response.msg || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error loading conversations:', error);
|
||||
this.message.error(`Failed to load conversations: ${error.status} ${error.statusText || error.message}`);
|
||||
|
||||
// Create a fallback new conversation if API is not available
|
||||
console.log('Creating fallback conversation...');
|
||||
this.createFallbackConversation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
this.selectConversation(newConversation);
|
||||
this.message.success('New conversation created');
|
||||
} else {
|
||||
console.error('Error in create conversation response:', response);
|
||||
this.message.error(`Failed to create conversation: ${response.msg || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error creating conversation:', error);
|
||||
this.message.error(`Failed to create new conversation: ${error.status} ${error.statusText || error.message}`);
|
||||
|
||||
// Create fallback conversation
|
||||
this.createFallbackConversation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fallback conversation when API is not available
|
||||
*/
|
||||
createFallbackConversation(): void {
|
||||
const fallbackConversation: ConversationDto = {
|
||||
conversationId: `fallback-${Date.now()}`,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
messages: []
|
||||
};
|
||||
|
||||
this.conversations = [fallbackConversation];
|
||||
this.selectConversation(fallbackConversation);
|
||||
|
||||
this.message.warning('AI Chat service unavailable. Running in offline mode.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a conversation and load its messages
|
||||
*/
|
||||
selectConversation(conversation: ConversationDto): void {
|
||||
this.currentConversation = conversation;
|
||||
this.loadConversationHistory(conversation.conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load conversation history from the API
|
||||
*/
|
||||
loadConversationHistory(conversationId: string): void {
|
||||
this.isLoading = true;
|
||||
|
||||
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 || [];
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
} else {
|
||||
console.error('Error loading conversation history:', response);
|
||||
// Fallback to the messages from the conversation list if API fails
|
||||
this.messages = this.currentConversation?.messages || [];
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
this.isLoading = false;
|
||||
console.error('Error loading conversation history:', error);
|
||||
// Fallback to the messages from the conversation list if API fails
|
||||
this.messages = this.currentConversation?.messages || [];
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*/
|
||||
deleteConversation(conversation: ConversationDto, event: Event): void {
|
||||
event.stopPropagation();
|
||||
|
||||
this.modal.confirm({
|
||||
nzTitle: 'Delete Conversation',
|
||||
nzContent: 'Are you sure you want to delete this conversation?',
|
||||
nzOkText: 'Delete',
|
||||
nzOkType: 'primary',
|
||||
nzOkDanger: true,
|
||||
nzOnOk: () => {
|
||||
this.aiChatService.deleteConversation(conversation.conversationId).subscribe({
|
||||
next: response => {
|
||||
if (response.code === 0) {
|
||||
// Remove from conversations list
|
||||
this.conversations = this.conversations.filter(c => c.conversationId !== conversation.conversationId);
|
||||
|
||||
// If this was the current conversation, select another or create new
|
||||
if (this.currentConversation?.conversationId === conversation.conversationId) {
|
||||
if (this.conversations.length > 0) {
|
||||
this.selectConversation(this.conversations[0]);
|
||||
} else {
|
||||
this.createNewConversation();
|
||||
}
|
||||
}
|
||||
|
||||
this.message.success('Conversation deleted');
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error deleting conversation:', error);
|
||||
this.message.error('Failed to delete conversation');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message
|
||||
*/
|
||||
sendMessage(): void {
|
||||
if (!this.newMessage.trim() || this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
content: this.newMessage.trim(),
|
||||
role: 'user',
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// Add user message to the messages list
|
||||
this.messages.push(userMessage);
|
||||
|
||||
const messageContent = this.newMessage.trim();
|
||||
this.newMessage = '';
|
||||
this.isLoading = true;
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
|
||||
// Check if this is a fallback conversation
|
||||
if (this.currentConversation?.conversationId.startsWith('fallback-')) {
|
||||
console.log('Fallback mode - showing offline message');
|
||||
setTimeout(() => {
|
||||
const offlineMessage: ChatMessage = {
|
||||
content:
|
||||
'I apologize, but the AI Chat service is currently unavailable. Please ensure the HertzBeat AI Agent module is running and try again later.',
|
||||
role: 'assistant',
|
||||
timestamp: new Date()
|
||||
};
|
||||
this.messages.push(offlineMessage);
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create empty assistant message for streaming
|
||||
const assistantMessage: ChatMessage = {
|
||||
content: '',
|
||||
role: 'assistant',
|
||||
timestamp: new Date()
|
||||
};
|
||||
this.messages.push(assistantMessage);
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
|
||||
// Send to AI service
|
||||
console.log('Sending message to AI service:', messageContent);
|
||||
this.aiChatService.streamChat(messageContent, this.currentConversation?.conversationId).subscribe({
|
||||
next: chunk => {
|
||||
console.log('Received stream chunk:', chunk);
|
||||
|
||||
// Find the last assistant message and append content
|
||||
const lastMessage = this.messages[this.messages.length - 1];
|
||||
if (lastMessage && lastMessage.role === 'assistant') {
|
||||
// Accumulate the content for streaming effect
|
||||
lastMessage.content += chunk.content;
|
||||
lastMessage.timestamp = chunk.timestamp instanceof Date ? chunk.timestamp : new Date();
|
||||
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error in chat stream:', error);
|
||||
this.message.error(`Failed to get AI response: ${error.status} ${error.statusText || error.message}`);
|
||||
|
||||
// Remove the empty assistant message and add error message
|
||||
if (
|
||||
this.messages.length > 0 &&
|
||||
this.messages[this.messages.length - 1].role === 'assistant' &&
|
||||
this.messages[this.messages.length - 1].content === ''
|
||||
) {
|
||||
this.messages.pop();
|
||||
}
|
||||
|
||||
const errorMessage: ChatMessage = {
|
||||
content: 'Sorry, there was an error processing your request. Please check if the AI Agent service is running and try again.',
|
||||
role: 'assistant',
|
||||
timestamp: new Date()
|
||||
};
|
||||
this.messages.push(errorMessage);
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
this.scrollToBottom();
|
||||
},
|
||||
complete: () => {
|
||||
console.log('Chat stream completed');
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
// Refresh current conversation to get updated data (only if not fallback)
|
||||
if (this.currentConversation && !this.currentConversation.conversationId.startsWith('fallback-')) {
|
||||
this.aiChatService.getConversation(this.currentConversation.conversationId).subscribe({
|
||||
next: response => {
|
||||
if (response.code === 0 && response.data) {
|
||||
// Update conversation in the list
|
||||
const index = this.conversations.findIndex(c => c.conversationId === response.data!.conversationId);
|
||||
if (index >= 0) {
|
||||
this.conversations[index] = response.data;
|
||||
}
|
||||
this.currentConversation = response.data;
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.log('Error refreshing conversation (non-critical):', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Enter key press
|
||||
*/
|
||||
onKeyPress(event: KeyboardEvent): void {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle sidebar
|
||||
*/
|
||||
toggleSidebar(): void {
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format conversation title
|
||||
*/
|
||||
getConversationTitle(conversation: ConversationDto): string {
|
||||
if (conversation.messages && conversation.messages.length > 0) {
|
||||
const firstUserMessage = conversation.messages.find(m => m.role === 'user');
|
||||
if (firstUserMessage) {
|
||||
return firstUserMessage.content.length > 30 ? `${firstUserMessage.content.substring(0, 30)}...` : firstUserMessage.content;
|
||||
}
|
||||
}
|
||||
return `Conversation ${conversation.conversationId.substring(0, 8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time
|
||||
*/
|
||||
formatTime(date: Date): string {
|
||||
if (!(date instanceof Date)) {
|
||||
date = new Date(date);
|
||||
}
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process message content to ensure it's properly handled
|
||||
*/
|
||||
processMessage(message: ChatMessage): void {
|
||||
// Handle Promise content
|
||||
if (typeof message.content === 'object' && message.content !== null && 'then' in message.content) {
|
||||
(message.content as Promise<any>)
|
||||
.then((content: any) => {
|
||||
message.content = String(content || '');
|
||||
this.cdr.detectChanges();
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Error processing message content:', error);
|
||||
message.content = 'Error loading message content';
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
} else {
|
||||
// Ensure content is always a string
|
||||
message.content = String(message.content || '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to bottom of messages
|
||||
*/
|
||||
private scrollToBottom(): void {
|
||||
try {
|
||||
if (this.messagesContainer) {
|
||||
const element = this.messagesContainer.nativeElement;
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error scrolling to bottom:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check OpenAI configuration status
|
||||
*/
|
||||
checkOpenAiConfiguration(): void {
|
||||
this.openAiConfigService.getOpenAiConfigStatus().subscribe({
|
||||
next: response => {
|
||||
if (response.code === 0) {
|
||||
this.isOpenAiConfigured = response.data.configured;
|
||||
if (this.isOpenAiConfigured) {
|
||||
this.loadConversations();
|
||||
} else {
|
||||
this.showOpenAiConfigDialog(response.data);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to check OpenAI configuration:', response.msg);
|
||||
this.showOpenAiConfigDialog();
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error checking OpenAI configuration:', error);
|
||||
this.showOpenAiConfigDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show OpenAI configuration dialog
|
||||
*/
|
||||
showOpenAiConfigDialog(status?: OpenAiConfigStatus): void {
|
||||
// Load existing configuration if available
|
||||
this.loadOpenAiConfig();
|
||||
|
||||
let contentMessage = `
|
||||
<div style="margin-bottom: 16px;">
|
||||
<p>To use AI Agent Chat, please configure your OpenAI API key.</p>
|
||||
`;
|
||||
|
||||
if (status && !status.validationPassed && status.validationMessage) {
|
||||
contentMessage += `
|
||||
<div style="margin-bottom: 12px; padding: 8px; background: #fff2f0; border: 1px solid #ffccc7; border-radius: 4px;">
|
||||
<strong>Configuration Issue:</strong> ${status.validationMessage}
|
||||
</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',
|
||||
nzContent: contentMessage,
|
||||
nzWidth: 600,
|
||||
nzClosable: false,
|
||||
nzMaskClosable: false,
|
||||
nzFooter: [
|
||||
{
|
||||
label: 'Configure Here',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
this.showConfigModal = true;
|
||||
modalRef.destroy();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.showConfigModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close configuration modal
|
||||
*/
|
||||
onCloseConfigModal(): void {
|
||||
this.showConfigModal = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save OpenAI configuration
|
||||
*/
|
||||
onSaveOpenAiConfig(): void {
|
||||
if (!this.openAiConfig.apiKey.trim()) {
|
||||
this.message.error('API Key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Always enable when saving an API key
|
||||
this.openAiConfig.enable = true;
|
||||
|
||||
this.configLoading = true;
|
||||
this.message.info('Validating API key...', { nzDuration: 2000 });
|
||||
|
||||
this.openAiConfigService.saveOpenAiConfig(this.openAiConfig).subscribe({
|
||||
next: response => {
|
||||
this.configLoading = false;
|
||||
if (response.code === 0) {
|
||||
this.message.success('OpenAI API key validated and saved successfully!');
|
||||
this.showConfigModal = false;
|
||||
this.isOpenAiConfigured = true;
|
||||
this.loadConversations();
|
||||
} else {
|
||||
// Check if it's a validation error
|
||||
if (response.msg.includes('validation failed')) {
|
||||
this.message.error(`❌ API Key validation failed: ${response.msg}`, { nzDuration: 5000 });
|
||||
} else {
|
||||
this.message.error(`Failed to save configuration: ${response.msg}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
this.configLoading = false;
|
||||
this.message.error(`Failed to save configuration: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { Injectable, ComponentRef } from '@angular/core';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
|
||||
import { ChatComponent } from '../components/ai-chat/chat.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AiChatModalService {
|
||||
private currentModal: any = null;
|
||||
|
||||
constructor(private modalService: NzModalService) {}
|
||||
|
||||
openChatModal(): void {
|
||||
if (this.currentModal) {
|
||||
this.currentModal.destroy();
|
||||
}
|
||||
|
||||
this.currentModal = this.modalService.create({
|
||||
nzTitle: '',
|
||||
nzContent: ChatComponent,
|
||||
nzFooter: null,
|
||||
nzWidth: '90vw',
|
||||
nzWrapClassName: 'ai-chat-modal',
|
||||
nzCentered: true,
|
||||
nzStyle: {
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
nzBodyStyle: {
|
||||
padding: '0',
|
||||
height: '80vh',
|
||||
borderRadius: '16px'
|
||||
},
|
||||
nzMaskClosable: false,
|
||||
nzClosable: true,
|
||||
nzOnCancel: () => {
|
||||
this.currentModal = null;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeChatModal(): void {
|
||||
if (this.currentModal) {
|
||||
this.currentModal.destroy();
|
||||
this.currentModal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
|
||||
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';
|
||||
import { HelpMessageShowComponent } from './components/help-message-show/help-message-show.component';
|
||||
@@ -68,7 +69,8 @@ const DIRECTIVES: Array<Type<void>> = [TimezonePipe, I18nElsePipe, ElapsedTimePi
|
||||
NzButtonModule,
|
||||
NzInputModule,
|
||||
NzIconModule.forChild(icons),
|
||||
NzSpinModule
|
||||
NzSpinModule,
|
||||
AiChatModule
|
||||
],
|
||||
declarations: [...COMPONENTS, ...DIRECTIVES, HelpMessageShowComponent],
|
||||
exports: [
|
||||
@@ -83,7 +85,8 @@ const DIRECTIVES: Array<Type<void>> = [TimezonePipe, I18nElsePipe, ElapsedTimePi
|
||||
...SHARED_ZORRO_MODULES,
|
||||
...ThirdModules,
|
||||
...COMPONENTS,
|
||||
...DIRECTIVES
|
||||
...DIRECTIVES,
|
||||
AiChatModule
|
||||
]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
||||
@@ -589,6 +589,7 @@
|
||||
"menu.dashboard": "Dashboard",
|
||||
"menu.extras": "More",
|
||||
"menu.extras.about": "About",
|
||||
"menu.extras.ai.chat": "AI Agent Chat",
|
||||
"menu.extras.help": "Help",
|
||||
"menu.extras.setting": "Setting",
|
||||
"menu.extras.settings": "Settings",
|
||||
|
||||
@@ -589,6 +589,7 @@
|
||||
"menu.dashboard": "ダッシュボード",
|
||||
"menu.extras": "その他",
|
||||
"menu.extras.about": "概要",
|
||||
"menu.extras.ai.chat": "AIアシスタント",
|
||||
"menu.extras.help": "ヘルプ",
|
||||
"menu.extras.setting": "設定",
|
||||
"menu.extras.settings": "設定",
|
||||
|
||||
@@ -630,6 +630,7 @@
|
||||
"menu.dashboard": "Painel",
|
||||
"menu.extras": "Mais",
|
||||
"menu.extras.about": "sobre",
|
||||
"menu.extras.ai.chat": "Assistente AI",
|
||||
"menu.extras.help": "Centro de ajuda",
|
||||
"menu.extras.setting": "configurar",
|
||||
"menu.extras.settings": "Configurações do sistema",
|
||||
|
||||
@@ -589,6 +589,7 @@
|
||||
"menu.dashboard": "仪表盘",
|
||||
"menu.extras": "更多",
|
||||
"menu.extras.about": "关于",
|
||||
"menu.extras.ai.chat": "AI智能助手",
|
||||
"menu.extras.help": "帮助中心",
|
||||
"menu.extras.setting": "设置",
|
||||
"menu.extras.settings": "系统设置",
|
||||
|
||||
@@ -588,6 +588,7 @@
|
||||
"menu.dashboard": "儀表盤",
|
||||
"menu.extras": "更多",
|
||||
"menu.extras.about": "關於",
|
||||
"menu.extras.ai.chat": "AI智能助手",
|
||||
"menu.extras.help": "幫助中心",
|
||||
"menu.extras.setting": "設置",
|
||||
"menu.extras.settings": "系統設置",
|
||||
|
||||
Reference in New Issue
Block a user