From b434649c6642043df4da65fae934682854302f18 Mon Sep 17 00:00:00 2001 From: LiuTianyou Date: Sun, 21 Dec 2025 03:58:26 +0800 Subject: [PATCH 01/10] [improve] use security form collect password --- .../impl/ChatClientProviderServiceImpl.java | 10 +-- web-app/src/app/service/ai-chat.service.ts | 13 +++- .../components/ai-chat/ai-chat.module.ts | 4 +- .../components/ai-chat/chat.component.html | 30 ++++++++ .../components/ai-chat/chat.component.ts | 72 ++++++++++++++++++- web-app/src/app/shared/shared.module.ts | 6 +- 6 files changed, 121 insertions(+), 14 deletions(-) diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java index b073e0f12e0..a3bf0781cda 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java @@ -54,13 +54,13 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService private final ApplicationContext applicationContext; private final GeneralConfigDao generalConfigDao; - + @Autowired private ToolCallbackProvider toolCallbackProvider; - + private boolean isConfigured = false; - @Value("classpath:/prompt/system-message.st") + @Value("classpath:/prompt/system-message-improve.st") private Resource systemResource; @Autowired @@ -74,7 +74,7 @@ public Flux streamChat(ChatRequestContext context) { try { // Get the current (potentially refreshed) ChatClient instance ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class); - + List messages = new ArrayList<>(); // Add conversation history if available @@ -112,7 +112,7 @@ public boolean isConfigured() { if (!isConfigured) { GeneralConfig providerConfig = generalConfigDao.findByType("provider"); ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class); - isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null; + isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null; } return isConfigured; } diff --git a/web-app/src/app/service/ai-chat.service.ts b/web-app/src/app/service/ai-chat.service.ts index 37f308af8a7..d5dd066608d 100644 --- a/web-app/src/app/service/ai-chat.service.ts +++ b/web-app/src/app/service/ai-chat.service.ts @@ -28,6 +28,13 @@ export interface ChatMessage { content: string; role: 'user' | 'assistant'; gmtCreate: Date; + securityForm: SecurityForm; +} + +export interface SecurityForm { + show: Boolean; + param: string; + content: string; } export interface ChatConversation { @@ -121,7 +128,7 @@ export class AiChatService { const decoder = new TextDecoder(); let buffer = ''; - function readStream(): Promise { + const readStream = (): Promise => { if (!reader) { return Promise.resolve(); } @@ -148,6 +155,7 @@ export class AiChatService { responseSubject.next({ content: data.response || '', role: 'assistant', + securityForm: { show: false, param: '', content: '' }, gmtCreate: data.timestamp ? new Date(data.timestamp) : new Date() }); } @@ -157,6 +165,7 @@ export class AiChatService { responseSubject.next({ content: jsonStr, role: 'assistant', + securityForm: { show: false, param: '', content: '' }, gmtCreate: new Date() }); } @@ -167,7 +176,7 @@ export class AiChatService { return readStream(); }); - } + }; return readStream(); }) diff --git a/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts b/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts index a154954c7ab..13c8f3d29fc 100644 --- a/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts +++ b/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts @@ -22,6 +22,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DelonFormModule } from '@delon/form'; import { AlainThemeModule } from '@delon/theme'; +import { SharedModule } from '@shared'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzFormModule } from 'ng-zorro-antd/form'; import { NzIconModule } from 'ng-zorro-antd/icon'; @@ -52,7 +53,8 @@ import { ChatComponent } from './chat.component'; NzSelectModule, NzSpinModule, MarkdownComponent, - NzTooltipDirective + NzTooltipDirective, + SharedModule ], exports: [ChatComponent] }) diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.html b/web-app/src/app/shared/components/ai-chat/chat.component.html index a09c0a91c16..5e447da2019 100644 --- a/web-app/src/app/shared/components/ai-chat/chat.component.html +++ b/web-app/src/app/shared/components/ai-chat/chat.component.html @@ -117,6 +117,14 @@

{{ 'ai.chat.welcome.title' | i18n }}

+
{{ 'ai.chat.typing' | i18n }} @@ -229,3 +237,25 @@

{{ 'ai.chat.welcome.title' | i18n }}

+ + + +
+
+ + + {{ paramDefine.name }} + + + + + +
+
+
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.ts b/web-app/src/app/shared/components/ai-chat/chat.component.ts index 1c3ca19434d..7d699f70317 100644 --- a/web-app/src/app/shared/components/ai-chat/chat.component.ts +++ b/web-app/src/app/shared/components/ai-chat/chat.component.ts @@ -24,7 +24,8 @@ import { NzMessageService } from 'ng-zorro-antd/message'; import { NzModalService } from 'ng-zorro-antd/modal'; import { ModelProviderConfig, PROVIDER_OPTIONS, ProviderOption } from '../../../pojo/ModelProviderConfig'; -import { AiChatService, ChatMessage, ChatConversation } from '../../../service/ai-chat.service'; +import { ParamDefine } from '../../../pojo/ParamDefine'; +import { AiChatService, ChatMessage, ChatConversation, SecurityForm } from '../../../service/ai-chat.service'; import { GeneralConfigService } from '../../../service/general-config.service'; import { ThemeService } from '../../../service/theme.service'; @@ -51,9 +52,13 @@ export class ChatComponent implements OnInit, OnDestroy { isAiProviderConfigured = false; showConfigModal = false; configLoading = false; + showSecurityFormModal = false; aiProviderConfig: ModelProviderConfig = new ModelProviderConfig(); providerOptions: ProviderOption[] = PROVIDER_OPTIONS; + securityParamDefine: ParamDefine[] = []; + securityParams: any = {}; + constructor( private aiChatService: AiChatService, private message: NzMessageService, @@ -192,6 +197,12 @@ export class ChatComponent implements OnInit, OnDestroy { if (response.code === 0 && response.data) { this.messages = response.data.messages || []; + //计算所有 message 的 securityForm + this.messages.forEach(message => { + message.securityForm = this.calculateShowSecurityForm(message.content); + message.content = message.securityForm.content; + console.log('securityForm:', message.securityForm); + }); this.cdr.detectChanges(); this.scrollToBottom(); } else { @@ -253,6 +264,23 @@ export class ChatComponent implements OnInit, OnDestroy { }); } + /** + * calculate if the security form should be shown for the given message content + * + * @param content + */ + calculateShowSecurityForm(content: string): SecurityForm { + const regex = /```json\s*SecureForm:((?:.+\s)+)```/gm; + if (!content) return { show: false, param: '', content: content }; + const match = content.match(regex); + if (!match || match.length === 0) { + return { show: false, param: '', content: content }; + } + // @ts-ignore + const result = match[0].replace(/```json\s*SecureForm:/gm, '').replace(/```/, ''); + return { show: true, param: result, content: content.replace(regex, '') }; + } + /** * Send a message */ @@ -264,6 +292,7 @@ export class ChatComponent implements OnInit, OnDestroy { const userMessage: ChatMessage = { content: this.newMessage.trim(), role: 'user', + securityForm: { show: false, param: '', content: '' }, gmtCreate: new Date() }; @@ -282,6 +311,7 @@ export class ChatComponent implements OnInit, OnDestroy { const offlineMessage: ChatMessage = { content: this.i18nSvc.fanyi('ai.chat.offline.response'), role: 'assistant', + securityForm: { show: false, param: '', content: '' }, gmtCreate: new Date() }; this.messages.push(offlineMessage); @@ -296,6 +326,7 @@ export class ChatComponent implements OnInit, OnDestroy { const assistantMessage: ChatMessage = { content: '', role: 'assistant', + securityForm: { show: false, param: '', content: '' }, gmtCreate: new Date() }; this.messages.push(assistantMessage); @@ -311,7 +342,6 @@ export class ChatComponent implements OnInit, OnDestroy { // Accumulate the content for streaming effect lastMessage.content += chunk.content; lastMessage.gmtCreate = chunk.gmtCreate; - this.cdr.detectChanges(); this.scrollToBottom(); } @@ -332,6 +362,7 @@ export class ChatComponent implements OnInit, OnDestroy { const errorMessage: ChatMessage = { content: this.i18nSvc.fanyi('ai.chat.error.processing'), role: 'assistant', + securityForm: { show: false, param: '', content: '' }, gmtCreate: new Date() }; this.messages.push(errorMessage); @@ -343,6 +374,10 @@ export class ChatComponent implements OnInit, OnDestroy { this.isSendingMessage = false; this.cdr.detectChanges(); + const lastMessage = this.messages[this.messages.length - 1]; + lastMessage.securityForm = this.calculateShowSecurityForm(lastMessage.content); + console.log('securityForm:', lastMessage.securityForm); + // Refresh current conversation to get updated data (only if not fallback) if (this.currentConversation && this.currentConversation.id !== 0) { this.aiChatService.getConversation(this.currentConversation.id).subscribe({ @@ -618,4 +653,37 @@ export class ChatComponent implements OnInit, OnDestroy { this.aiProviderConfig.model = selectedProvider.defaultModel; } } + + /** + * 111 + * + * @param event + */ + onSecurityFormSubmit(): void { + this.showSecurityFormModal = false; + } + + /** + * 1111 + */ + onSecurityFormCancel(): void { + this.showSecurityFormModal = false; + } + + openSecurityForm(securityForm: SecurityForm): void { + debugger; + this.securityParamDefine = JSON.parse(securityForm.param).privateParams.map((i: any) => { + this.securityParams[i.field] = { + // Parameter type 0: number 1: string 2: encrypted string 3: json string mapped by map + type: i.type === 'number' ? 0 : i.type === 'text' || i.type === 'string' ? 1 : i.type === 'json' ? 3 : 2, + field: i.field, + paramValue: null + }; + i.name = i.name[this.i18nSvc.defaultLang]; + return i; + }); + + console.log('this.securityParamDefine:', this.securityParamDefine); + this.showSecurityFormModal = true; + } } diff --git a/web-app/src/app/shared/shared.module.ts b/web-app/src/app/shared/shared.module.ts index 1c2735e285a..72971c6a861 100644 --- a/web-app/src/app/shared/shared.module.ts +++ b/web-app/src/app/shared/shared.module.ts @@ -73,8 +73,7 @@ const DIRECTIVES: Array> = [TimezonePipe, I18nElsePipe, ElapsedTimePi NzInputModule, NzIconModule.forChild(icons), NzSpinModule, - NzCodeEditorModule, - AiChatModule + NzCodeEditorModule ], declarations: [...COMPONENTS, ...DIRECTIVES, HelpMessageShowComponent], exports: [ @@ -89,8 +88,7 @@ const DIRECTIVES: Array> = [TimezonePipe, I18nElsePipe, ElapsedTimePi ...SHARED_ZORRO_MODULES, ...ThirdModules, ...COMPONENTS, - ...DIRECTIVES, - AiChatModule + ...DIRECTIVES ] }) export class SharedModule {} From d43dd22f844fb48f8b7415f416ef252c41bb4a83 Mon Sep 17 00:00:00 2001 From: LiuTianyou Date: Sun, 21 Dec 2025 07:45:24 +0800 Subject: [PATCH 02/10] [improve] use security form collect password or other sensitive information --- .../ai/controller/ChatController.java | 33 +- .../hertzbeat/ai/pojo/dto/SecurityData.java | 14 + .../ai/service/ConversationService.java | 18 +- .../impl/ChatClientProviderServiceImpl.java | 7 +- .../service/impl/ConversationServiceImpl.java | 136 ++++---- .../hertzbeat/ai/tools/MonitorTools.java | 58 ++-- .../ai/tools/impl/MonitorToolsImpl.java | 316 ++++++++++-------- .../main/resources/prompt/system-message.st | 197 +++++++++-- .../common/entity/ai/ChatConversation.java | 5 +- web-app/src/app/service/ai-chat.service.ts | 16 +- .../components/ai-chat/chat.component.html | 17 +- .../components/ai-chat/chat.component.ts | 55 ++- web-app/src/assets/i18n/en-US.json | 6 +- web-app/src/assets/i18n/ja-JP.json | 6 +- web-app/src/assets/i18n/pt-BR.json | 6 +- web-app/src/assets/i18n/zh-CN.json | 6 +- web-app/src/assets/i18n/zh-TW.json | 6 +- 17 files changed, 606 insertions(+), 296 deletions(-) create mode 100644 hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java index 71c5d47cb58..f62fa2cdc41 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java @@ -26,6 +26,7 @@ import org.apache.hertzbeat.ai.config.McpContextHolder; import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; import org.apache.hertzbeat.ai.service.ConversationService; import org.apache.hertzbeat.common.entity.ai.ChatConversation; import org.apache.hertzbeat.common.entity.dto.Message; @@ -78,12 +79,12 @@ public Flux> streamChat(@Valid @RequestBody C McpContextHolder.setSubject(subject); if (context.getMessage() == null || context.getMessage().trim().isEmpty()) { ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(context.getConversationId()) - .response("Error: Message cannot be empty") - .build(); + .conversationId(context.getConversationId()) + .response("Error: Message cannot be empty") + .build(); return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); + .event("error") + .build()); } log.info("Received streaming chat request for conversation: {}", context.getConversationId()); @@ -92,12 +93,12 @@ public Flux> streamChat(@Valid @RequestBody C } catch (Exception e) { log.error("Error in stream chat endpoint: ", e); ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(context.getConversationId()) - .response("An error occurred: " + e.getMessage()) - .build(); + .conversationId(context.getConversationId()) + .response("An error occurred: " + e.getMessage()) + .build(); return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); + .event("error") + .build()); } } @@ -134,7 +135,7 @@ public ResponseEntity>> listConversations() { @GetMapping(path = "/conversations/{conversationId}") @Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation") public ResponseEntity> getConversation( - @Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) { + @Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) { ChatConversation conversation = conversationService.getConversation(conversationId); return ResponseEntity.ok(Message.success(conversation)); } @@ -148,8 +149,16 @@ public ResponseEntity> getConversation( @DeleteMapping(path = "/conversations/{conversationId}") @Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages") public ResponseEntity> deleteConversation( - @Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) { + @Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) { conversationService.deleteConversation(conversationId); return ResponseEntity.ok(Message.success()); } + + @PostMapping(path = "/security") + @Operation(summary = "save security data", description = "Save security data") + public ResponseEntity> commitSecurityData(@Valid @RequestBody SecurityData securityData) { + return ResponseEntity.ok(Message.success(conversationService.saveSecurityData(securityData))); + } + + } diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java new file mode 100644 index 00000000000..b09d05db886 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java @@ -0,0 +1,14 @@ +package org.apache.hertzbeat.ai.pojo.dto; + +import lombok.Data; + +/** + * security data + */ +@Data +public class SecurityData { + + private Long conversationId; + private String securityData; + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java index a9eadef0520..2e2e813c481 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java @@ -19,6 +19,7 @@ package org.apache.hertzbeat.ai.service; import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; import org.apache.hertzbeat.common.entity.ai.ChatConversation; import org.springframework.http.codec.ServerSentEvent; import reactor.core.publisher.Flux; @@ -33,7 +34,7 @@ public interface ConversationService { /** * Send a message and receive a streaming response * - * @param message The user's message + * @param message The user's message * @param conversationId Optional conversation ID for continuing a chat * @return Flux of ServerSentEvent for streaming the response */ @@ -67,4 +68,19 @@ public interface ConversationService { * @param conversationId Conversation ID to delete */ void deleteConversation(Long conversationId); + + /** + * save security data for a conversation + * + * @param securityData securityData + * @return save result + */ + Boolean saveSecurityData(SecurityData securityData); + + /** + * query security data by conversationId + * @param conversationId conversationId + * @return securityData + */ + String getSecurityData(Long conversationId); } diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java index a3bf0781cda..bbfcf950294 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java @@ -18,6 +18,8 @@ package org.apache.hertzbeat.ai.service.impl; +import java.util.HashMap; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.hertzbeat.common.entity.ai.ChatMessage; import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig; @@ -92,9 +94,12 @@ public Flux streamChat(ChatRequestContext context) { log.info("Starting streaming chat for conversation: {}", context.getConversationId()); + Map metadata = new HashMap<>(); + metadata.put("conversationId", context.getConversationId()); + log.info(SystemPromptTemplate.builder().resource(systemResource).build().createMessage(metadata).getText()); return chatClient.prompt() .messages(messages) - .system(SystemPromptTemplate.builder().resource(systemResource).build().getTemplate()) + .system(SystemPromptTemplate.builder().resource(systemResource).build().create(metadata).getContents()) .toolCallbacks(toolCallbackProvider) .stream() .content() diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java index 23e745a0509..ed10b24e5df 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java @@ -17,11 +17,13 @@ package org.apache.hertzbeat.ai.service.impl; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.apache.hertzbeat.ai.dao.ChatConversationDao; import org.apache.hertzbeat.ai.dao.ChatMessageDao; import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; import org.apache.hertzbeat.ai.service.ChatClientProviderService; import org.apache.hertzbeat.ai.service.ConversationService; import org.apache.hertzbeat.common.entity.ai.ChatConversation; @@ -62,17 +64,17 @@ public Flux> streamChat(String message, Long // Check if provider is properly configured if (!chatClientProviderService.isConfigured()) { ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(conversationId) - .response("Provider is not configured. Please configure your AI Provider.") - .build(); + .conversationId(conversationId) + .response("Provider is not configured. Please configure your AI Provider.") + .build(); return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); + .event("error") + .build()); } log.info("Starting streaming conversation: {}", conversationId); ChatConversation conversation = conversationDao.findById(conversationId) - .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); + .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); // Manually load messages for conversation history List messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId); @@ -94,58 +96,59 @@ public Flux> streamChat(String message, Long chatMessage = messageDao.save(chatMessage); ChatRequestContext context = ChatRequestContext.builder() - .message(message) - .conversationId(conversationId) - .conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null - : conversation.getMessages().subList(0, conversation.getMessages().size() - 1)) - .build(); + .message(message) + .conversationId(conversationId) + .conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null + : conversation.getMessages().subList(0, conversation.getMessages().size() - 1)) + .build(); // Stream response from AI service StringBuilder fullResponse = new StringBuilder(); ChatMessage finalChatMessage = chatMessage; return chatClientProviderService.streamChat(context) - .map(chunk -> { - fullResponse.append(chunk); - ChatResponseChunk responseChunk = ChatResponseChunk.builder() - .conversationId(conversationId) - .userMessageId(finalChatMessage.getId()) - .response(chunk) - .build(); - - return ServerSentEvent.builder(responseChunk) - .event("message") - .build(); - }) - .concatWith(Flux.defer(() -> { - // Add the complete AI response to conversation - ChatMessage assistantMessage = ChatMessage.builder() - .conversationId(conversationId) - .content(fullResponse.toString()) - .role("assistant") - .build(); - assistantMessage = messageDao.save(assistantMessage); - ChatResponseChunk finalResponse = ChatResponseChunk.builder() - .conversationId(conversationId) - .response("") - .assistantMessageId(assistantMessage.getId()) - .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 -> { - ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(conversationId) - .response("An error occurred: " + error.getMessage()) - .userMessageId(finalChatMessage.getId()) - .build(); - return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); - }); + .map(chunk -> { + fullResponse.append(chunk); + ChatResponseChunk responseChunk = ChatResponseChunk.builder() + .conversationId(conversationId) + .userMessageId(finalChatMessage.getId()) + .response(chunk) + .build(); + + return ServerSentEvent.builder(responseChunk) + .event("message") + .build(); + }) + .concatWith(Flux.defer(() -> { + // Add the complete AI response to conversation + ChatMessage assistantMessage = ChatMessage.builder() + .conversationId(conversationId) + .content(fullResponse.toString()) + .role("assistant") + .build(); + assistantMessage = messageDao.save(assistantMessage); + ChatResponseChunk finalResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("") + .assistantMessageId(assistantMessage.getId()) + .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 -> { + ChatResponseChunk errorResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("An error occurred: " + error.getMessage()) + .userMessageId(finalChatMessage.getId()) + .build(); + return Flux.just(ServerSentEvent.builder(errorResponse) + .event("error") + .build()); + }); } @Override @@ -175,13 +178,14 @@ public List getAllConversations() { return conversations; } List conversationIds = conversations.stream() - .map(ChatConversation::getId) - .toList(); + .map(ChatConversation::getId) + .toList(); List allMessages = messageDao.findByConversationIdInOrderByGmtCreateAsc(conversationIds); Map> messagesByConversationId = allMessages.stream() - .collect(Collectors.groupingBy(ChatMessage::getConversationId)); + .collect(Collectors.groupingBy(ChatMessage::getConversationId)); for (ChatConversation conversation : conversations) { - List messages = messagesByConversationId.getOrDefault(conversation.getId(), Collections.emptyList()); + List messages = messagesByConversationId.getOrDefault(conversation.getId(), + Collections.emptyList()); conversation.setMessages(messages); } return conversations; @@ -196,4 +200,22 @@ public void deleteConversation(Long conversationId) { } conversationDao.deleteById(conversationId); } + + @Override + public Boolean saveSecurityData(SecurityData securityData) { + Optional chatConversation = conversationDao.findById(securityData.getConversationId()); + if (chatConversation.isPresent()) { + ChatConversation conversation = chatConversation.get(); + conversation.setSecurityData(securityData.getSecurityData()); + conversationDao.save(conversation); + return true; + } + return false; + } + + @Override + public String getSecurityData(Long conversationId) { + Optional chatConversation = conversationDao.findById(conversationId); + return chatConversation.map(ChatConversation::getSecurityData).orElse(null); + } } diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java index c813f09d927..ab24f25368c 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java @@ -27,19 +27,20 @@ public interface MonitorTools { /** * Add a new monitor with comprehensive configuration * - * @param name Monitor name - * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') - * @param intervals Collection interval in seconds (default: 600) - * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) + * @param name Monitor name + * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') + * @param intervals Collection interval in seconds (default: 600) + * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) * @param description Monitor description (optional) * @return Result message with monitor ID if successful */ String addMonitor( - String name, - String app, - Integer intervals, - String params, - String description + Long conversationId, + String name, + String app, + Integer intervals, + String params, + String description ); /** @@ -52,29 +53,30 @@ String addMonitor( /** * Comprehensive monitor querying with flexible filtering, pagination, and specialized views - * @param ids Specific monitor IDs to retrieve (optional) - * @param app Monitor type filter (linux, mysql, http, etc.) - * @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all) - * @param search Search in monitor names or hosts (partial matching) - * @param labels Label filters, format: 'key1:value1,key2:value2' - * @param sort Sort field (name, gmtCreate, gmtUpdate, status, app) - * @param order Sort order (asc, desc) - * @param pageIndex Page number starting from 0 - * @param pageSize Items per page (1-100 recommended) + * + * @param ids Specific monitor IDs to retrieve (optional) + * @param app Monitor type filter (linux, mysql, http, etc.) + * @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all) + * @param search Search in monitor names or hosts (partial matching) + * @param labels Label filters, format: 'key1:value1,key2:value2' + * @param sort Sort field (name, gmtCreate, gmtUpdate, status, app) + * @param order Sort order (asc, desc) + * @param pageIndex Page number starting from 0 + * @param pageSize Items per page (1-100 recommended) * @param includeStats Include status statistics summary * @return Comprehensive monitor information with optional statistics */ String queryMonitors( - List ids, - String app, - Byte status, - String search, - String labels, - String sort, - String order, - Integer pageIndex, - Integer pageSize, - Boolean includeStats); + List ids, + String app, + Byte status, + String search, + String labels, + String sort, + String order, + Integer pageIndex, + Integer pageSize, + Boolean includeStats); /** * Get parameter definitions required for a specific monitor type diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java index 6364bccc905..1296798cf97 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java @@ -17,9 +17,16 @@ package org.apache.hertzbeat.ai.tools.impl; +import com.fasterxml.jackson.core.type.TypeReference; import com.usthe.sureness.subject.SubjectSum; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.ai.dao.ChatConversationDao; +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.apache.hertzbeat.common.util.JsonUtil; import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; import org.apache.hertzbeat.manager.service.MonitorService; import org.apache.hertzbeat.manager.service.AppService; @@ -44,76 +51,79 @@ @Slf4j @Service public class MonitorToolsImpl implements MonitorTools { + @Autowired private MonitorService monitorService; @Autowired private AppService appService; + @Autowired + private ChatConversationDao conversationDao; + /** - * Tool to query monitor information with flexible filtering and pagination. - * Supports filtering by monitor IDs, type, status, host, labels, sorting, and - * pagination. - * Returns detailed monitor information including ID, name, type, host, and status. + * Tool to query monitor information with flexible filtering and pagination. Supports filtering by monitor IDs, + * type, status, host, labels, sorting, and pagination. Returns detailed monitor information including ID, name, + * type, host, and status. */ @Override @Tool(name = "query_monitors", description = """ - HertzBeat: Query Existing/configured monitors in HertzBeat. - This tool retrieves monitors based on various filters and parameters. - Comprehensive monitor querying with flexible filtering, pagination, and specialized views. - - MONITOR STATUSES: - - status=1: Online/Active monitors (healthy, responding normally) - - status=2: Offline monitors (not responding, connection failed) - - status=3: Unreachable monitors (network/connectivity issues) - - status=0: Paused monitors (manually disabled/suspended) - - status=9 or null: All monitors regardless of status (default) - - COMMON USE CASES & PARAMETER COMBINATIONS: - - 1. BASIC MONITOR LISTING: - - Default: No parameters (shows all monitors, 8 per page) - - By type: app='linux' (show only Linux monitors) - - Search: search='web' (find monitors with 'web' in name/host) - - 2. STATUS-BASED QUERIES: - - Healthy monitors: status=1, pageSize=50 - - Problem monitors: status=2 or status=3, pageSize=50 - - Offline monitors only: status=2 - - Unreachable monitors only: status=3 - - Paused monitors: status=0 - - 3. MONITORING HEALTH OVERVIEW: - - All statuses with statistics: status=9, includeStats=true, pageSize=100 - - Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls) - - 4. ADVANCED FILTERING: - - Specific monitor types: app='mysql', status=1 (healthy MySQL monitors) - - Label-based: labels='env:prod,critical:true' - - Host search: search='192.168' (find by IP pattern) - - Monitor IDs: ids=[1,2,3] (specific monitors by ID) - - 5. SORTING & PAGINATION: - - Recently updated: sort='gmtUpdate', order='desc' - - Alphabetical: sort='name', order='asc' - - By creation: sort='gmtCreate', order='desc' (newest first) - - Large datasets: pageSize=50-100 for bulk operations - - RESPONSE FORMAT: - - includeStats=true: Adds status distribution summary at top - - Default: Simple list with ID, name, type, host, status - - Shows total count and pagination info - """) + HertzBeat: Query Existing/configured monitors in HertzBeat. + This tool retrieves monitors based on various filters and parameters. + Comprehensive monitor querying with flexible filtering, pagination, and specialized views. + + MONITOR STATUSES: + - status=1: Online/Active monitors (healthy, responding normally) + - status=2: Offline monitors (not responding, connection failed) + - status=3: Unreachable monitors (network/connectivity issues) + - status=0: Paused monitors (manually disabled/suspended) + - status=9 or null: All monitors regardless of status (default) + + COMMON USE CASES & PARAMETER COMBINATIONS: + + 1. BASIC MONITOR LISTING: + - Default: No parameters (shows all monitors, 8 per page) + - By type: app='linux' (show only Linux monitors) + - Search: search='web' (find monitors with 'web' in name/host) + + 2. STATUS-BASED QUERIES: + - Healthy monitors: status=1, pageSize=50 + - Problem monitors: status=2 or status=3, pageSize=50 + - Offline monitors only: status=2 + - Unreachable monitors only: status=3 + - Paused monitors: status=0 + + 3. MONITORING HEALTH OVERVIEW: + - All statuses with statistics: status=9, includeStats=true, pageSize=100 + - Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls) + + 4. ADVANCED FILTERING: + - Specific monitor types: app='mysql', status=1 (healthy MySQL monitors) + - Label-based: labels='env:prod,critical:true' + - Host search: search='192.168' (find by IP pattern) + - Monitor IDs: ids=[1,2,3] (specific monitors by ID) + + 5. SORTING & PAGINATION: + - Recently updated: sort='gmtUpdate', order='desc' + - Alphabetical: sort='name', order='asc' + - By creation: sort='gmtCreate', order='desc' (newest first) + - Large datasets: pageSize=50-100 for bulk operations + + RESPONSE FORMAT: + - includeStats=true: Adds status distribution summary at top + - Default: Simple list with ID, name, type, host, status + - Shows total count and pagination info + """) public String queryMonitors( - @ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List ids, - @ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app, - @ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status, - @ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search, - @ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels, - @ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort, - @ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order, - @ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex, - @ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize, - @ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) { + @ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List ids, + @ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app, + @ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status, + @ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search, + @ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels, + @ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort, + @ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order, + @ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex, + @ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize, + @ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) { try { // Set defaults if (pageSize == null || pageSize <= 0) { @@ -130,7 +140,7 @@ public String queryMonitors( log.debug("Current security subject: {}", subjectSum); Page result = monitorService.getMonitors( - ids, app, search, status, sort, order, pageIndex, pageSize, labels); + ids, app, search, status, sort, order, pageIndex, pageSize, labels); log.debug("MonitorService.getMonitors result: {}", result); StringBuilder response = new StringBuilder(); @@ -140,10 +150,14 @@ public String queryMonitors( // Include statistics if requested if (includeStats) { // Get status distribution by calling with different status values - long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements(); - long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements(); - long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements(); - long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements(); + long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels) + .getTotalElements(); + long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels) + .getTotalElements(); + long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, + labels).getTotalElements(); + long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels) + .getTotalElements(); response.append("STATUS OVERVIEW:\n"); response.append("- Online: ").append(onlineCount).append("\n"); @@ -160,19 +174,20 @@ public String queryMonitors( } response.append("Query Results: ").append(result.getContent().size()) - .append(" monitors (Total: ").append(result.getTotalElements()).append(")\n"); + .append(" monitors (Total: ").append(result.getTotalElements()).append(")\n"); if (result.getTotalPages() > 1) { - response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n"); + response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()) + .append("\n"); } response.append("\n"); for (Monitor monitor : result.getContent()) { response.append("ID: ").append(monitor.getId()) - .append(" | Name: ").append(monitor.getName()) - .append(" | Type: ").append(monitor.getApp()) - .append(" | Instance: ").append(monitor.getInstance()) - .append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus())); + .append(" | Name: ").append(monitor.getName()) + .append(" | Type: ").append(monitor.getApp()) + .append(" | Instance: ").append(monitor.getInstance()) + .append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus())); // Add creation date for better context if (monitor.getGmtCreate() != null) { @@ -194,44 +209,45 @@ public String queryMonitors( @Override @Tool(name = "add_monitor", description = """ - HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. - This tool dynamically handles different parameter requirements for each monitor type. - - This tool creates monitors with proper app-specific parameters. - - ********* - VERY IMPORTANT: - ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. - Use list_monitor_types tool to see available monitor type names to use here in the app parameter. - Use the information obtained from this to query user for parameters. - If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. - ********** - - Examples of natural language requests this tool handles: - - "Monitor website example.com with HTTPS on port 443" - - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" - - "Monitor Linux server health on host server.company.com via SSH" - - "Set up Redis monitoring on localhost port 6379 with password" - - PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. - The params should be a JSON string containing key-value pairs for the monitor type. - Use get_monitor_additional_params tool to see what parameters are required for each monitor type. - - PARAMS EXAMPLES: - - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} - - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} - - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} - - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} - """) + HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. + This tool dynamically handles different parameter requirements for each monitor type. + + This tool creates monitors with proper app-specific parameters. + + ********* + VERY IMPORTANT: + ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. + Use list_monitor_types tool to see available monitor type names to use here in the app parameter. + Use the information obtained from this to query user for parameters. + If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. + ********** + + Examples of natural language requests this tool handles: + - "Monitor website example.com with HTTPS on port 443" + - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" + - "Monitor Linux server health on host server.company.com via SSH" + - "Set up Redis monitoring on localhost port 6379 with password" + + PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. + The params should be a JSON string containing key-value pairs for the monitor type. + Use get_monitor_additional_params tool to see what parameters are required for each monitor type. + + PARAMS EXAMPLES: + - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} + - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} + - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} + - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} + """) public String addMonitor( - @ToolParam(description = "Monitor name (required)", required = true) String name, - @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, - @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, - @ToolParam(description = "Monitor-specific parameters as JSON string. " - + "Use get_monitor_additional_params to see required fields. " - + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", - required = true) String params, - @ToolParam(description = "Monitor description (optional)", required = false) String description) { + @ToolParam(description = "The id for current conversation", required = true) Long conversationId, + @ToolParam(description = "Monitor name (required)", required = true) String name, + @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, + @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, + @ToolParam(description = "Monitor-specific parameters as JSON string. " + + "Use get_monitor_additional_params to see required fields. " + + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", + required = true) String params, + @ToolParam(description = "Monitor description (optional)", required = false) String description) { try { log.info("Adding monitor: name={}, app={}", name, app); @@ -251,32 +267,42 @@ public String addMonitor( if (intervals == null || intervals < 10) { intervals = 600; } - // Parse params to extract host and port for instance List paramList = parseParams(params); + + // Query and add sensitive parameters + Optional chatConversation = conversationDao.findById(conversationId); + if (chatConversation.isPresent() && StringUtils.isNotEmpty(chatConversation.get().getSecurityData())) { + List securityParams = JsonUtil.fromJson(chatConversation.get().getSecurityData(), + new TypeReference>() { + }); + if (CollectionUtils.isNotEmpty(securityParams)) { + paramList.addAll(securityParams); + } + } String host = paramList.stream() - .filter(p -> "host".equals(p.getField())) - .map(Param::getParamValue) - .findFirst() - .orElse(""); + .filter(p -> "host".equals(p.getField())) + .map(Param::getParamValue) + .findFirst() + .orElse(""); String port = paramList.stream() - .filter(p -> "port".equals(p.getField())) - .map(Param::getParamValue) - .findFirst() - .orElse(null); + .filter(p -> "port".equals(p.getField())) + .map(Param::getParamValue) + .findFirst() + .orElse(null); String instance = (port != null && !port.isEmpty()) ? host.trim() + ":" + port : host.trim(); // Create Monitor entity Monitor monitor = Monitor.builder() - .name(name.trim()) - .app(app.toLowerCase().trim()) - .instance(instance) - .intervals(intervals) - .status((byte) 1) - .type((byte) 0) - .description(description != null ? description.trim() : "") - .build(); + .name(name.trim()) + .app(app.toLowerCase().trim()) + .instance(instance) + .intervals(intervals) + .status((byte) 1) + .type((byte) 0) + .description(description != null ? description.trim() : "") + .build(); // Validate that all required parameters for this monitor type are provided try { @@ -294,7 +320,7 @@ public String addMonitor( monitorService.addMonitor(monitor, paramList, null, null); log.info("Successfully added monitor '{}' with ID: {}", monitor.getName(), monitor.getId()); return String.format("Successfully added %s monitor '%s' with ID: %d (Instance: %s, Interval: %d seconds)", - app.toUpperCase(), monitor.getName(), monitor.getId(), monitor.getInstance(), monitor.getIntervals()); + app.toUpperCase(), monitor.getName(), monitor.getId(), monitor.getInstance(), monitor.getIntervals()); } catch (Exception e) { log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e); @@ -383,12 +409,12 @@ private byte determineParamType(String fieldName) { @Override @Tool(name = "list_monitor_types", description = """ - HertzBeat: List all available monitor types that can be added to HertzBeat. - 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. - """) + HertzBeat: List all available monitor types that can be added to HertzBeat. + 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) { + @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); @@ -413,18 +439,19 @@ public String listMonitorTypes( // Sort monitor types alphabetically by key List> sortedTypes = monitorTypes.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); + .sorted(Map.Entry.comparingByKey()) + .toList(); for (Map.Entry entry : sortedTypes) { String typeKey = entry.getKey(); String displayName = entry.getValue(); response.append("• ").append(typeKey) - .append(" - ").append(displayName) - .append("\n"); + .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( + "\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter."); log.info("Successfully listed {} monitor types", monitorTypes); return response.toString(); @@ -437,13 +464,13 @@ public String listMonitorTypes( @Override @Tool(name = "get_monitor_params", description = """ - HertzBeat: 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, - ALWAYS use this before adding a monitor to understand what parameters the user needs to provide. - Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool. - """) + HertzBeat: 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, + ALWAYS use this before adding a monitor to understand what parameters the user needs to provide. + Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool. + """) public String getMonitorParams( - @ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) { + @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); @@ -460,7 +487,7 @@ public String getMonitorParams( 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); + + "This monitor type may not exist or may not require additional parameters.", app); } // Format the response @@ -508,7 +535,8 @@ public String getMonitorParams( } 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)); + 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(); diff --git a/hertzbeat-ai/src/main/resources/prompt/system-message.st b/hertzbeat-ai/src/main/resources/prompt/system-message.st index a371391ea57..d789d615222 100644 --- a/hertzbeat-ai/src/main/resources/prompt/system-message.st +++ b/hertzbeat-ai/src/main/resources/prompt/system-message.st @@ -2,16 +2,23 @@ You are an AI Assistant specialized in monitoring infrastructure and application 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 monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues. + +## Security and Privacy Principles +1. **Never collect sensitive information through conversation**: Passwords, keys, tokens, and other private data must be collected via secure forms +2. **Parameter classification**: Clearly distinguish between public parameters (collectible via conversation) and private parameters (collectible only via secure forms) +3. **Secure interaction flow**: Public parameters collected through conversation, private parameters collected through secure forms + ******* VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system. If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters. -******** +BUT NEVER ASK FOR SENSITIVE PARAMETERS - use secure forms instead, If the user has completed the security form, it is considered that the user has filled in all the private parameters, and no further requests for access will be made. +******* ## Available HertzBeat Tools: ### Monitor Management Tools: - **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels) -- **add_monitor**: Add a new monitor with dynamic app-specific parameter support +- **add_monitor**: Add a new monitor with dynamic app-specific parameter support,In addition to the parameters provided by the user, a system-provided parameter, conversationId, is also required, with a value of {conversationId}. - **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.) - **get_monitor_additional_params**: Get parameter definitions required for specific monitor types @@ -33,16 +40,86 @@ If the user doesn't provide required parameters, ask them iteratively to provide - **get_historical_metrics**: Get historical time-series metrics with flexible time ranges - **get_warehouse_status**: Check metrics storage system status +## Secure Interaction Protocol + +### Parameter Classification Guide +- **Public parameters** (collectible via conversation): + - Hostnames, IP addresses, port numbers + - Monitor names, labels, tags + - Check intervals, timeout settings + - Protocol types, URLs, paths + - Threshold values, alert names + +- **Private parameters** (must be collected via secure forms): + - Passwords (password, passwd, pwd) + - Keys (key, secret, token, credential) + - Certificate files (certificate, private_key, ssl_key) + - Access tokens (access_token, api_token, bearer_token) + - Database connection strings with authentication + - API keys, secret keys + - Any parameter containing "secret", "key", "token", "credential", or "password" + +### Secure Response Patterns +Use these patterns when interacting with users: + +#### Pattern 1: Normal Parameter Collection +[Continuing Collection] +Current progress: [collected parameters] +Next parameter: [parameter name] +Please provide: [specific information] + +#### Pattern 2: Private Parameters Required +Private parameters requiring secure collection: +[private_parameter1] (description) +[private_parameter2] (description) +Please complete configuration via the secure form. + +```json +SecureForm:\{ + "showSecureForm": true, + "publicParams": \{ ... \}, + "privateParams": [ + \{ + "id": ..., + "app":..., + "name": \{ + "zh-CN": "...", + "en-US": "...", + "ja-JP": "...", + "pt-BR": "...", + "zh-TW": "..." + \}, + "field": "...", + "type": "password", + "required": ..., + "defaultValue": ..., + "placeholder": null, + "range": null, + "limit": null, + "options": nul, + "keyAlias": null, + "valueAlias": null, + "hide": false, + "depend": null + \} + ], + "monitorType": "..." +\} +``` + ## Natural Language Examples: -### Monitor Management: -- "Add a MySQL monitor for database server at 192.168.1.10 with user admin" +### Secure Monitor Creation Examples: +- "Add a MySQL monitor for database server at 192.168.1.10 with username 'admin' (password will be collected via secure form)" - "Monitor website https://example.com with SSL checking every 60 seconds" +- "Set up Redis monitoring at localhost:6379 (authentication password required via secure form)" + +### Normal Monitor Management Examples: - "Show me all Linux servers that are currently offline" - "List all Redis monitors with their connection status" ### Alert Configuration: -- ALERT RULE means when to alert a user +- ALERT RULE means when to alert a user - "Create an alert for Kafka JVM when VmName equals 'vm-w2'" - "Alert when OpenAI credit grants exceed 1000" - "Set up HBase Master alert when heap memory usage is over 80%" @@ -62,13 +139,15 @@ If the user doesn't provide required parameters, ask them iteratively to provide ## Workflow Guidelines: -1. **Adding Monitors**: - - ALWAYS use get_monitor_additional_params first to check required parameters - - Use list_monitor_types to show available types - - Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor - - Example: "To monitor MySQL, I need host, port, username, password, and database name" +### 1. Adding Monitors Securely: + - ALWAYS use `get_monitor_additional_params` first to check required parameters + - Use `list_monitor_types` to show available types + - **Parameter classification**: Identify which parameters are public (collectible via conversation) and which are private (require secure forms) + - Collect all required public parameters from the `get_monitor_additional_params` tool and ask user to provide them + - When all public parameters are collected, inform user about private parameters and trigger secure form + - Example: "To monitor MySQL, I need host, port, and database name (public). Username and password (private) will be collected via secure form." -2. **Creating Alert Rules or Alerts**: +### 2. Creating Alert Rules or Alerts: THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE, IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'. APPLY THE SAME LOGIC FOR LESS THAN OPERATOR. @@ -104,16 +183,16 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step: - Priority levels: 0=critical, 1=warning, 2=info -3. **Analyzing Performance**: - - Use get_realtime_metrics for current status - - Use get_historical_metrics for trends - - Use get_high_usage_monitors to find problems +### 3. Analyzing Performance: + - Use `get_realtime_metrics` for current status + - Use `get_historical_metrics` for trends + - Use `get_high_usage_monitors` to find problems - Provide actionable recommendations based on data -4. **Troubleshooting Alerts**: - - Use query_alerts to find current issues - - Use get_monitor_alerts for specific monitor problems - - Use get_frequent_alerts to identify recurring issues +### 4. Troubleshooting Alerts: + - Use `query_alerts` to find current issues + - Use `get_monitor_alerts` for specific monitor problems + - Use `get_frequent_alerts` to identify recurring issues - Suggest root cause analysis steps ## Parameter Guidelines: @@ -126,6 +205,7 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step: ## Best Practices: +- Never store sensitive data: Do not log, cache, or record passwords and keys in conversation - Never create alert rules without exact user input on app, metrics, and field conditions - Always validate monitor types and parameters before adding monitors - ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields @@ -138,11 +218,88 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step: - Provide clear explanations of monitoring data and actionable insights ## Avoid these common errors: -- Using Label name instead of the value from the heirarchy JSON while creating alert rules. +- Using Label name instead of the value from the hierarchy JSON while creating alert rules. - Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators - This process is to trigger alarms, when certain rule or set of rules exceed a threshold value. So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<', because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator +- **NEVER ask for sensitive parameters in conversation** (passwords, keys, tokens, credentials) +- **NEVER include sensitive information in tool call parameters** +- **NEVER assume all parameters can be collected via conversation** + +## Special Scenario Handling: + +### Scenario 1: User provides sensitive information +User: "The password is 123456" +AI: "Thank you, but for security reasons, passwords should not be transmitted through conversation. The system will display a secure form for you to enter this information safely." + +### Scenario 2: Uncertain parameter classification +If unsure whether a parameter is sensitive: +1. Assume it's sensitive +2. Guide to secure form +3. Prioritize user data protection + +### Scenario 3: Mixed parameter requirements +When both public and private parameters are needed: +1. Collect all public parameters first +2. Then guide to secure form for private parameters +3. Provide clear progress indicators + +## Tool Usage Security Specifications: + +### add_monitor tool +- Only collect public parameters through conversation +- Private parameters are added to tool calls directly by the system +- Ensure tool calls never expose sensitive data + +### Other tools +- Most tools don't involve sensitive data +- If they do, follow the same principles +- Prioritize data security in all interactions + +## Complete Monitor Addition Workflow (Secure Version): + +1. **Identify requirement**: What does the user want to monitor? +2. **Determine type**: Use `list_monitor_types` to find matching monitor type +3. **Get parameter definitions**: Use `get_monitor_additional_params` to see required parameters +4. **Parameter classification**: + - Mark which are public parameters (askable) + - Mark which are private parameters (secure form only) +5. **Collect public parameters**: + - Ask for one public parameter at a time + - Confirm each parameter value +6. **Trigger secure phase**: + - When all public parameters are complete + - Inform user about private parameters + - Instruct system to display secure form +7. **Complete creation**: System handles remaining process + +## Example Dialogue (Secure Monitor Addition): + +User: "I want to monitor my MySQL database" +AI: (Uses `list_monitor_types` and `get_monitor_additional_params`) +AI: "I can help you set up MySQL monitoring. First, I need some information: + 1. What is the database host address?" +User: "192.168.1.10" +AI: "Port number (default 3306)?" +User: "3306" +AI: "What name would you like for this monitor?" +User: "Production Database" +AI: "Check interval in seconds (recommended 60)?" +User: "60" +AI: "[Secure Form Required] + Monitor type: mysql + Public parameters collected: + - host: 192.168.1.10 + - port: 3306 + - name: Production Database + - interval: 60 + Private parameters requiring secure collection: + - username (database username) + - password (database password) + Please complete configuration via the secure form." + +--- Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities. When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration. diff --git a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java index ce6129c7569..1efbc2bcd5e 100644 --- a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java +++ b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java @@ -18,6 +18,7 @@ package org.apache.hertzbeat.common.entity.ai; import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -55,7 +56,7 @@ public class ChatConversation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - + @Schema(title = "conversation title") private String title; @@ -81,4 +82,6 @@ public class ChatConversation { @OneToMany @JoinColumn(name = "conversation_id") private List messages; + + private String securityData; } diff --git a/web-app/src/app/service/ai-chat.service.ts b/web-app/src/app/service/ai-chat.service.ts index d5dd066608d..c3162992a74 100644 --- a/web-app/src/app/service/ai-chat.service.ts +++ b/web-app/src/app/service/ai-chat.service.ts @@ -35,8 +35,16 @@ export interface SecurityForm { show: Boolean; param: string; content: string; + complete: boolean; } +export const DEFAULT_SECURITY_FORM: SecurityForm = { + show: false, + param: '', + content: '', + complete: false +}; + export interface ChatConversation { id: number; title: string; @@ -155,7 +163,7 @@ export class AiChatService { responseSubject.next({ content: data.response || '', role: 'assistant', - securityForm: { show: false, param: '', content: '' }, + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: data.timestamp ? new Date(data.timestamp) : new Date() }); } @@ -165,7 +173,7 @@ export class AiChatService { responseSubject.next({ content: jsonStr, role: 'assistant', - securityForm: { show: false, param: '', content: '' }, + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: new Date() }); } @@ -187,4 +195,8 @@ export class AiChatService { return responseSubject.asObservable(); } + + saveSecurityData(body: any): Observable> { + return this.http.post>(`${chat_uri}/security`, body); + } } diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.html b/web-app/src/app/shared/components/ai-chat/chat.component.html index 5e447da2019..5de669de66c 100644 --- a/web-app/src/app/shared/components/ai-chat/chat.component.html +++ b/web-app/src/app/shared/components/ai-chat/chat.component.html @@ -81,7 +81,7 @@

{{ 'ai.chat.title' | i18n }}

- HertzBeat Logo + HertzBeat Logo

{{ 'ai.chat.welcome.title' | i18n }}

{{ 'ai.chat.welcome.description' | i18n }} @@ -119,12 +119,13 @@

{{ 'ai.chat.welcome.title' | i18n }}

+ {{ (message.securityForm.complete ? 'ai.chat.security.form.complete' : 'ai.chat.security.form.button') | i18n }} +
{{ 'ai.chat.typing' | i18n }} @@ -198,7 +199,7 @@

{{ 'ai.chat.welcome.title' | i18n }}

{{ 'ai.chat.config.api-key' | i18n }} - +

{{ 'ai.chat.config.api-key.help' | i18n }}

@@ -208,7 +209,7 @@

{{ 'ai.chat.welcome.title' | i18n }}

{{ 'ai.chat.config.base-url' | i18n }} - +