Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b434649
[improve] use security form collect password
LiuTianyou Dec 20, 2025
d43dd22
[improve] use security form collect password or other sensitive infor…
LiuTianyou Dec 20, 2025
0c673d4
[improve] use security form collect password or other sensitive infor…
LiuTianyou Dec 21, 2025
2f68a6e
Merge branch 'master' into improve-ai
LiuTianyou Dec 21, 2025
3483cae
Merge remote-tracking branch 'origin/improve-ai' into improve-ai
LiuTianyou Dec 21, 2025
fc12017
[improve] use security form collect password or other sensitive infor…
LiuTianyou Dec 21, 2025
c2c670d
[improve] use security form collect password or other sensitive infor…
LiuTianyou Dec 21, 2025
decfc7d
[improve] use security form collect password or other sensitive infor…
LiuTianyou Dec 21, 2025
e946fa4
[improve] use security form collect password or other sensitive infor…
LiuTianyou Dec 23, 2025
153c24a
Merge branch 'master' into improve-ai
lynx009 Dec 24, 2025
f75283d
Merge branch 'master' into improve-ai
Aias00 Dec 28, 2025
3855bb0
Merge branch 'master' into improve-ai
yuluo-yx Dec 28, 2025
c932a64
Merge branch 'master' into improve-ai
Duansg Dec 30, 2025
ecbbd22
Merge branch 'master' into improve-ai
Duansg Dec 31, 2025
e90766c
Merge branch 'master' into improve-ai
Duansg Jan 2, 2026
08f03cf
Update web-app/src/app/shared/components/ai-chat/chat.component.ts
Aias00 Jan 5, 2026
21646ec
Update web-app/src/app/shared/components/ai-chat/chat.component.ts
Aias00 Jan 5, 2026
0a11d49
Merge branch 'master' into improve-ai
Aias00 Jan 5, 2026
1c3731f
Merge branch 'master' into improve-ai
Duansg Jan 7, 2026
685871f
[improve] provide additional supplementary prompts for protected mode…
LiuTianyou Jan 8, 2026
1d87712
Merge remote-tracking branch 'origin/improve-ai' into improve-ai
LiuTianyou Jan 8, 2026
1909187
Merge branch 'master' into improve-ai
LiuTianyou Feb 27, 2026
1abcc34
Merge branch 'master' into improve-ai
LiuTianyou Mar 1, 2026
da55e1a
merge code and resolve conflicts
LiuTianyou Mar 2, 2026
c5c903c
Merge branch 'master' into improve-ai
LiuTianyou Mar 2, 2026
6471b6b
Merge branch 'master' into improve-ai
LiuTianyou Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,12 +79,12 @@ public Flux<ServerSentEvent<ChatResponseChunk>> 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());
Expand All @@ -92,12 +93,12 @@ public Flux<ServerSentEvent<ChatResponseChunk>> 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());
}
}

Expand Down Expand Up @@ -134,7 +135,7 @@ public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ChatConversation>> 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));
}
Expand All @@ -148,8 +149,21 @@ public ResponseEntity<Message<ChatConversation>> getConversation(
@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 = "2345678") @PathVariable("conversationId") Long conversationId) {
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
conversationService.deleteConversation(conversationId);
return ResponseEntity.ok(Message.success());
}

/**
* Save data submitted by secure form
* @param securityData security data
* @return save result
*/
@PostMapping(path = "/security")
@Operation(summary = "save security data", description = "Save security data")
public ResponseEntity<Message<Boolean>> commitSecurityData(@Valid @RequestBody SecurityData securityData) {
return ResponseEntity.ok(Message.success(conversationService.saveSecurityData(securityData)));
}
Comment on lines +162 to +166
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation. The SecurityData object should be validated for null conversationId and empty/null securityData before processing. Add validation annotations like @NotNull on the conversationId field and @notblank on securityData field in the SecurityData class, or add explicit validation in the controller method.

Copilot uses AI. Check for mistakes.


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.hertzbeat.ai.pojo.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

/**
* security data
*/
@Data
public class SecurityData {

@NotNull
@Schema(description = "Conversation ID", example = "123")
private Long conversationId;

@NotNull
@Schema(description = "security data", example = "{\"password\":\"xxxxx\"}")
private String securityData;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*/
Expand Down Expand Up @@ -67,4 +68,13 @@ 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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

package org.apache.hertzbeat.ai.service.impl;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
import org.apache.hertzbeat.ai.sop.model.SopParameter;
Expand All @@ -27,9 +31,12 @@
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
Expand All @@ -44,14 +51,12 @@
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
* Implementation of the {@link ChatClientProviderService}.
* Provides functionality to interact with the ChatClient for handling chat
* messages.
* Implementation of the {@link ChatClientProviderService}. Provides functionality to interact with the ChatClient for
* handling chat messages.
*/
@Slf4j
@Service
Expand All @@ -64,21 +69,28 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService

private final GeneralConfigDao generalConfigDao;

private ModelProviderConfig modelProviderConfig;


private final SkillRegistry skillRegistry;

@Autowired
@Qualifier("hertzbeatTools")
private ToolCallbackProvider toolCallbackProvider;

private boolean isConfigured = false;

@Value("classpath:/prompt/system-message.st")
private Resource systemResource;

@Value("classpath:/prompt/extra-message-protected.st")
private Resource extraResourceProtected;


@Autowired
public ChatClientProviderServiceImpl(ApplicationContext applicationContext,
GeneralConfigDao generalConfigDao,
@Lazy SkillRegistry skillRegistry) {
public ChatClientProviderServiceImpl(ApplicationContext applicationContext,
GeneralConfigDao generalConfigDao,
@Lazy SkillRegistry skillRegistry) {
this.applicationContext = applicationContext;
this.generalConfigDao = generalConfigDao;
this.skillRegistry = skillRegistry;
Expand All @@ -89,7 +101,7 @@ public Flux<String> streamChat(ChatRequestContext context) {
try {
// Get the current (potentially refreshed) ChatClient instance
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);

List<Message> messages = new ArrayList<>();

// Add conversation history if available
Expand All @@ -112,13 +124,13 @@ public Flux<String> streamChat(ChatRequestContext context) {
String systemPrompt = buildSystemPrompt(context.getConversationId());

return chatClient.prompt()
.messages(messages)
.system(systemPrompt)
.toolCallbacks(toolCallbackProvider)
.stream()
.content()
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
.messages(messages)
.system(systemPrompt)
.toolCallbacks(toolCallbackProvider)
.stream()
.content()
.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);
Expand All @@ -133,30 +145,43 @@ private String buildSystemPrompt(Long conversationId) {
try {
String template = systemResource.getContentAsString(StandardCharsets.UTF_8);
String skillsList = generateSkillsList();
return template
.replace(SKILLS_PLACEHOLDER, skillsList)
.replace(CONVERSATION_ID_PLACEHOLDER, String.valueOf(conversationId));
template = template
.replace(SKILLS_PLACEHOLDER, skillsList)
.replace(CONVERSATION_ID_PLACEHOLDER, String.valueOf(conversationId));

// add extra prompt for protected model to guide it to use protected tools
if (Objects.equals(modelProviderConfig.getParticipationModel(), "PROTECTED")) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("conversationId", conversationId);
return template + SystemPromptTemplate.builder().resource(extraResourceProtected).build()
.create(metadata)
.getContents();
} else {
return template;
}

} catch (IOException e) {
log.error("Failed to read system prompt template: {}", e.getMessage());
return "";
}
}


/**
* Generate a formatted list of available skills for the system prompt.
*/
private String generateSkillsList() {
List<SopDefinition> skills = skillRegistry.getAllSkills();

if (skills.isEmpty()) {
return "No skills currently available. Use listSkills tool to refresh.";
}

StringBuilder sb = new StringBuilder();
for (SopDefinition skill : skills) {
sb.append("- **").append(skill.getName()).append("**: ");
sb.append(skill.getDescription());

// Add parameter hints
if (skill.getParameters() != null && !skill.getParameters().isEmpty()) {
sb.append(" (requires: ");
Expand All @@ -171,16 +196,24 @@ private String generateSkillsList() {
}
sb.append("\n");
}

return sb.toString();
}

@EventListener(AiProviderConfigChangeEvent.class)
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
this.modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
}

@Override
public boolean isConfigured() {
if (!isConfigured) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(),
ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
this.modelProviderConfig = modelProviderConfig;
}
return isConfigured;
}
Expand Down
Loading
Loading