This is an automated email from the ASF dual-hosted git repository. yuluo-yx pushed a commit to branch 0812-yuluo/feat in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit 95fe36445bd94884836815666fb2f51cc078db0e Author: yuluo-yx <[email protected]> AuthorDate: Wed Aug 12 21:13:39 2026 +0800 fix(ai): create conversation when conversation ID is missing Signed-off-by: yuluo-yx <[email protected]> --- .../ai/service/impl/ConversationServiceImpl.java | 40 +++++++++++++-------- .../service/impl/ConversationServiceImplTest.java | 42 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 14 deletions(-) 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 c674617dea..bc48bbaa8b 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 @@ -78,24 +78,32 @@ public class ConversationServiceImpl implements ConversationService { .build()); } - log.info("Starting streaming conversation: {}", conversationId); - ChatConversation conversation = conversationDao.findById(conversationId) - .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); + ChatConversation conversation; + if (conversationId == null) { + // 接口约定 conversationId 可选,首次聊天时按需创建会话。 + conversation = new ChatConversation(); + conversation.setTitle(buildConversationTitle(message)); + conversation = conversationDao.save(conversation); + } else { + conversation = conversationDao.findById(conversationId) + .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); + } + Long currentConversationId = conversation.getId(); + log.info("Starting streaming conversation: {}", currentConversationId); // Manually load messages for conversation history - List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId); + List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(currentConversationId); conversation.setMessages(messages); if (conversation.getTitle().startsWith("conversation")) { // Auto-generate title from first user message - String title = message.length() > 30 ? message.substring(0, 27) + "..." : message; - conversation.setTitle(title); + conversation.setTitle(buildConversationTitle(message)); conversationDao.save(conversation); } // Add user message to conversation ChatMessage chatMessage = ChatMessage.builder() - .conversationId(conversationId) + .conversationId(currentConversationId) .content(message) .role("user") .build(); @@ -103,7 +111,7 @@ public class ConversationServiceImpl implements ConversationService { ChatRequestContext context = ChatRequestContext.builder() .message(message) - .conversationId(conversationId) + .conversationId(currentConversationId) .conversationHistory(messages) .build(); @@ -116,7 +124,7 @@ public class ConversationServiceImpl implements ConversationService { .map(chunk -> { fullResponse.append(chunk); ChatResponseChunk responseChunk = ChatResponseChunk.builder() - .conversationId(conversationId) + .conversationId(currentConversationId) .userMessageId(finalChatMessage.getId()) .response(chunk) .build(); @@ -128,13 +136,13 @@ public class ConversationServiceImpl implements ConversationService { .concatWith(Flux.defer(() -> { // Add the complete AI response to conversation ChatMessage assistantMessage = ChatMessage.builder() - .conversationId(conversationId) + .conversationId(currentConversationId) .content(fullResponse.toString()) .role("assistant") .build(); assistantMessage = messageDao.save(assistantMessage); ChatResponseChunk finalResponse = ChatResponseChunk.builder() - .conversationId(conversationId) + .conversationId(currentConversationId) .response("") .assistantMessageId(assistantMessage.getId()) .build(); @@ -143,12 +151,12 @@ public class ConversationServiceImpl implements ConversationService { .event("complete") .build()); })) - .doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId)) - .doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, + .doOnComplete(() -> log.info("Streaming completed for conversation: {}", currentConversationId)) + .doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", currentConversationId, error.getMessage(), error)) .onErrorResume(error -> { ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(conversationId) + .conversationId(currentConversationId) .response("An error occurred: " + error.getMessage()) .userMessageId(finalChatMessage.getId()) .build(); @@ -165,6 +173,10 @@ public class ConversationServiceImpl implements ConversationService { return conversationDao.save(conversation); } + private String buildConversationTitle(String message) { + return message.length() > 30 ? message.substring(0, 27) + "..." : message; + } + @Override public ChatConversation getConversation(Long conversationId) { if (conversationId == null) { diff --git a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImplTest.java b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImplTest.java index b192073e45..cfe7d8c217 100644 --- a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImplTest.java +++ b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImplTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import com.usthe.sureness.subject.SubjectSum; @@ -123,6 +124,47 @@ class ConversationServiceImplTest { assertEquals(subject, contextCaptor.getValue().getSubject()); } + /** + * 客户端省略可选会话 ID 时,服务端应创建会话并在整个流式响应中返回新 ID。 + */ + @Test + void streamChatShouldCreateConversationWhenConversationIdIsMissing() { + AtomicLong messageId = new AtomicLong(20L); + when(chatClientProviderService.isConfigured()).thenReturn(true); + when(conversationDao.save(any(ChatConversation.class))).thenAnswer(invocation -> { + ChatConversation savedConversation = invocation.getArgument(0); + savedConversation.setId(CONVERSATION_ID); + return savedConversation; + }); + when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID)).thenReturn(List.of()); + when(messageDao.save(any(ChatMessage.class))).thenAnswer(invocation -> { + ChatMessage savedMessage = invocation.getArgument(0); + savedMessage.setId(messageId.getAndIncrement()); + return savedMessage; + }); + when(chatClientProviderService.streamChat(any(ChatRequestContext.class))) + .thenReturn(Flux.just("本轮回答")); + + List<ServerSentEvent<ChatResponseChunk>> events = conversationService + .streamChat("本轮问题", null) + .collectList() + .block(); + + assertNotNull(events); + assertEquals(2, events.size()); + assertEquals(CONVERSATION_ID, events.get(0).data().getConversationId()); + assertEquals(CONVERSATION_ID, events.get(1).data().getConversationId()); + + ArgumentCaptor<ChatRequestContext> contextCaptor = ArgumentCaptor.forClass(ChatRequestContext.class); + verify(chatClientProviderService).streamChat(contextCaptor.capture()); + assertEquals(CONVERSATION_ID, contextCaptor.getValue().getConversationId()); + assertEquals(List.of(), contextCaptor.getValue().getConversationHistory()); + ArgumentCaptor<ChatConversation> conversationCaptor = ArgumentCaptor.forClass(ChatConversation.class); + verify(conversationDao).save(conversationCaptor.capture()); + assertEquals("本轮问题", conversationCaptor.getValue().getTitle()); + verifyNoMoreInteractions(conversationDao); + } + /** * Deleting a conversation must remove its schedules before they can push more messages. */ --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
