Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -34,7 +34,6 @@
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;

import java.util.Collections;
Expand Down Expand Up @@ -99,8 +98,7 @@ public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.conversationHistory(messages)
.build();

// Stream response from AI service
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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.service.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
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.service.ChatClientProviderService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;

/**
* Tests multi-turn conversation context handling in {@link ConversationServiceImpl}.
*/
@ExtendWith(MockitoExtension.class)
class ConversationServiceImplTest {

private static final long CONVERSATION_ID = 1L;

@Mock
private ChatConversationDao conversationDao;

@Mock
private ChatMessageDao messageDao;

@Mock
private ChatClientProviderService chatClientProviderService;

@InjectMocks
private ConversationServiceImpl conversationService;

@Test
void streamChatShouldKeepCompleteConversationHistory() {
ChatConversation conversation = ChatConversation.builder()
.id(CONVERSATION_ID)
.title("已命名会话")
.build();
List<ChatMessage> history = List.of(
ChatMessage.builder()
.id(11L)
.conversationId(CONVERSATION_ID)
.role("user")
.content("上一轮问题")
.build(),
ChatMessage.builder()
.id(12L)
.conversationId(CONVERSATION_ID)
.role("assistant")
.content("上一轮回答")
.build());
AtomicLong messageId = new AtomicLong(20L);

when(chatClientProviderService.isConfigured()).thenReturn(true);
when(conversationDao.findById(CONVERSATION_ID)).thenReturn(Optional.of(conversation));
when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID)).thenReturn(history);
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("本轮问题", CONVERSATION_ID)
.collectList()
.block();

assertNotNull(events);
assertEquals(2, events.size());
ArgumentCaptor<ChatRequestContext> contextCaptor = ArgumentCaptor.forClass(ChatRequestContext.class);
verify(chatClientProviderService).streamChat(contextCaptor.capture());
assertEquals(history, contextCaptor.getValue().getConversationHistory());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.hertzbeat.common.entity.ai;

import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -61,12 +62,13 @@ public class ChatMessage {
private Long id;

@Schema(title = "conversation id")
@Column(name = "conversation_id", insertable = false, updatable = false)
@Column(name = "conversation_id")
private Long conversationId;

@JsonIgnore
@Schema(title = "conversation", hidden = true)
@ManyToOne
@JoinColumn(name = "conversation_id")
@JoinColumn(name = "conversation_id", insertable = false, updatable = false)
private ChatConversation conversation;

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

package org.apache.hertzbeat.common.entity.ai;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.List;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.Test;

/**
* Tests AI conversation message serialization.
*/
class ChatMessageTest {

@Test
void serializationShouldNotRecurseThroughConversation() {
ChatConversation conversation = ChatConversation.builder()
.id(1L)
.title("序列化测试会话")
.build();
ChatMessage message = ChatMessage.builder()
.id(2L)
.conversationId(conversation.getId())
.conversation(conversation)
.role("assistant")
.content("序列化测试消息")
.build();
conversation.setMessages(List.of(message));

String json = JsonUtil.toJson(conversation);

assertNotNull(json);
assertFalse(json.contains("\"conversation\""));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.startup.dao;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.annotation.Resource;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.startup.AbstractSpringIntegrationTest;
import org.junit.jupiter.api.Test;
import org.springframework.transaction.annotation.Transactional;

/**
* Tests persistence mapping for AI conversation messages.
*/
@Transactional
class ChatMessageDaoTest extends AbstractSpringIntegrationTest {

@Resource
private ChatConversationDao conversationDao;

@Resource
private ChatMessageDao messageDao;

@PersistenceContext
private EntityManager entityManager;

@Test
void saveMessageShouldPersistConversationId() {
ChatConversation conversation = conversationDao.saveAndFlush(
ChatConversation.builder().title("映射测试会话").build());
messageDao.saveAndFlush(ChatMessage.builder()
.conversationId(conversation.getId())
.role("user")
.content("映射测试消息")
.build());
entityManager.clear();

List<ChatMessage> messages = messageDao
.findByConversationIdOrderByGmtCreateAsc(conversation.getId());

assertEquals(1, messages.size());
assertEquals(conversation.getId(), messages.getFirst().getConversationId());
}
}
Loading