This is an automated email from the ASF dual-hosted git repository.
yuluo-yx pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 2cea398910 fix(ai): create conversation when conversation ID is
missing (#4315)
2cea398910 is described below
commit 2cea398910e5971ad24f46e0f27ae2b31af618bd
Author: shown <[email protected]>
AuthorDate: Thu Aug 13 00:03:24 2026 +0800
fix(ai): create conversation when conversation ID is missing (#4315)
Co-authored-by: Duansg <[email protected]>
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..b66b839e08 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) {
+ // The API contract makes conversationId optional, so create a
conversation for the first message.
+ 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..43cdf88d8f 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());
}
+ /**
+ * The service should create a conversation and return its ID when the
client omits the optional conversation 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]