Create ChatEntity to group chat messages under a parent entity
This commit is contained in:
parent
5450d97abc
commit
3844734794
@ -46,14 +46,14 @@ public class ChatRestController {
|
||||
}
|
||||
|
||||
@PostMapping(produces = "application/json")
|
||||
public ResponseEntity<String> newChat() {
|
||||
return ResponseEntity.ok(UUID.randomUUID().toString());
|
||||
public ResponseEntity<ChatIdentity> newChat() {
|
||||
return ResponseEntity.ok(chatUseCase.createChat());
|
||||
}
|
||||
|
||||
@PutMapping(value = "{chatId}", consumes = "application/x-www-form-urlencoded", produces = "application/json")
|
||||
public ResponseEntity<ChatMessage> handleChat(@PathVariable("chatId") String chatId, @RequestParam("prompt") String prompt) {
|
||||
if (ObjectUtils.isEmpty(chatId)) {
|
||||
throw new IllegalArgumentException("Chat id cannot be empty");
|
||||
throw new IllegalArgumentException("Chat uuid cannot be empty");
|
||||
}
|
||||
ChatMessage reply;
|
||||
try {
|
||||
|
@ -13,9 +13,9 @@ public class PromptTemplates {
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Loads and returns the full prompts string for the given profile name.
|
||||
* Loads and returns the full prompts string for the given profile resume.
|
||||
*
|
||||
* @param profileName name of the prompts profile, without extension (e.g. "default")
|
||||
* @param profileName resume of the prompts profile, without extension (e.g. "default")
|
||||
* @return full system prompts as String
|
||||
*/
|
||||
public static String get(String profileName) {
|
||||
@ -25,7 +25,7 @@ public class PromptTemplates {
|
||||
/**
|
||||
* Loads and returns the PromptDefinition for the given profile.
|
||||
*
|
||||
* @param profileName prompts profile name (e.g. "developer", "minimal")
|
||||
* @param profileName prompts profile resume (e.g. "developer", "minimal")
|
||||
* @return PromptDefinition object
|
||||
*/
|
||||
public static PromptDefinition load(String profileName) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.pablotj.ia.chat.boot.application.session;
|
||||
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatIdentity;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
|
||||
import com.pablotj.ia.chat.boot.domain.port.ChatMessageStore;
|
||||
import java.util.ArrayList;
|
||||
@ -16,8 +17,12 @@ public class ChatSessionManager {
|
||||
this.chatMessageStore = chatMessageStore;
|
||||
}
|
||||
|
||||
public List<ChatMessage> getMessages(String chatId) {
|
||||
List<ChatMessage> messages = chatMessageStore.getMessages(chatId);
|
||||
public ChatIdentity createChat() {
|
||||
return chatMessageStore.createChat();
|
||||
}
|
||||
|
||||
public List<ChatMessage> getMessages(String chatUuid) {
|
||||
List<ChatMessage> messages = chatMessageStore.getMessages(chatUuid);
|
||||
List<ChatMessage> filteredMessages = new ArrayList<>(messages);
|
||||
|
||||
if (ObjectUtils.isEmpty(filteredMessages)) {
|
||||
@ -28,8 +33,10 @@ public class ChatSessionManager {
|
||||
return filteredMessages;
|
||||
}
|
||||
|
||||
public void setMessages(String chatId, List<ChatMessage> messages) {
|
||||
public void setMessages(String chatUuid, List<ChatMessage> messages) {
|
||||
messages.removeIf(m -> ObjectUtils.isEmpty(m.text()));
|
||||
chatMessageStore.saveMessages(chatId, messages);
|
||||
chatMessageStore.saveMessages(chatUuid, messages);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ package com.pablotj.ia.chat.boot.application.usecase;
|
||||
import com.pablotj.ia.chat.boot.application.prompt.PromptBuilder;
|
||||
import com.pablotj.ia.chat.boot.application.prompt.PromptTemplates;
|
||||
import com.pablotj.ia.chat.boot.application.session.ChatSessionManager;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatIdentity;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
|
||||
import com.pablotj.ia.chat.boot.infraestructure.llm.LlmModelClient;
|
||||
import java.util.Date;
|
||||
@ -24,6 +25,10 @@ public class ChatUseCase {
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public ChatIdentity createChat() {
|
||||
return sessionManager.createChat();
|
||||
}
|
||||
|
||||
public List<ChatMessage> getMessages(String chatId) {
|
||||
return sessionManager.getMessages(chatId);
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ package com.pablotj.ia.chat.boot.domain.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public record ChatIdentity(String id, String name) implements Serializable {
|
||||
public record ChatIdentity(String uuid, String resume) implements Serializable {
|
||||
}
|
@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
public record ChatMessage(String chatId, String role, String text, Date date) implements Serializable {
|
||||
public record ChatMessage(String chatUuid, String role, String text, Date date) implements Serializable {
|
||||
|
||||
@Override
|
||||
@JsonFormat(pattern = "dd/MM/yyyy HH:mm", timezone = "Europe/Madrid")
|
||||
|
@ -3,10 +3,11 @@ package com.pablotj.ia.chat.boot.domain.port;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatIdentity;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
|
||||
import java.util.List;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
public interface ChatMessageStore {
|
||||
ChatIdentity createChat();
|
||||
List<ChatIdentity> getChats();
|
||||
|
||||
List<ChatMessage> getMessages(String chatId);
|
||||
void saveMessages(String sessionId, List<ChatMessage> messages);
|
||||
List<ChatMessage> getMessages(String chatUuid);
|
||||
void saveMessages(String chatUuid, List<ChatMessage> messages);
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
package com.pablotj.ia.chat.boot.persistence;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "chat")
|
||||
public class ChatEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String uuid;
|
||||
|
||||
private String resume;
|
||||
|
||||
@CreatedDate
|
||||
private Date createdDate;
|
||||
|
||||
@OneToMany(mappedBy = "chat", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<ChatMessageEntity> messages = new ArrayList<>();
|
||||
|
||||
// Getters y Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public String getResume() {
|
||||
return resume;
|
||||
}
|
||||
|
||||
public void setResume(String resume) {
|
||||
this.resume = resume;
|
||||
}
|
||||
|
||||
public Date getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(Date createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public List<ChatMessageEntity> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(List<ChatMessageEntity> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.pablotj.ia.chat.boot.persistence;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ChatJpaRepository extends JpaRepository<ChatEntity, Long> {
|
||||
|
||||
Optional<ChatEntity> findOneByUuid(String chatUuid);
|
||||
|
||||
List<ChatEntity> findAllByOrderByCreatedDateDesc();
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.pablotj.ia.chat.boot.persistence;
|
||||
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatIdentity;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
|
||||
|
||||
public class ChatMapper {
|
||||
|
||||
private ChatMapper() throws IllegalAccessException {
|
||||
throw new IllegalAccessException("Private access to ChatMapper");
|
||||
}
|
||||
|
||||
public static ChatIdentity toDomain(ChatEntity entity) {
|
||||
return new ChatIdentity(entity.getUuid(), entity.getResume());
|
||||
}
|
||||
}
|
@ -11,7 +11,8 @@ public class ChatMessageEntity {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String sessionId;
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
private ChatEntity chat;
|
||||
|
||||
@Column(length = 4000)
|
||||
private String text;
|
||||
@ -25,8 +26,8 @@ public class ChatMessageEntity {
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSessionId() { return sessionId; }
|
||||
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
|
||||
public ChatEntity getChat() { return chat; }
|
||||
public void setChat(ChatEntity chat) { this.chat = chat; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
@ -2,23 +2,10 @@ package com.pablotj.ia.chat.boot.persistence;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
|
||||
public interface ChatMessageJpaRepository extends JpaRepository<ChatMessageEntity, Long> {
|
||||
|
||||
@Query(value = """
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT *, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY id ASC) AS rn
|
||||
FROM chat_messages
|
||||
) sub
|
||||
WHERE rn = 1
|
||||
ORDER BY id desc
|
||||
""", nativeQuery = true)
|
||||
List<ChatMessageEntity> findFirstMessagePerSession();
|
||||
List<ChatMessageEntity> findByChatUuidOrderByIdAsc(String chatUuid);
|
||||
|
||||
List<ChatMessageEntity> findBySessionIdOrderByIdAsc(String chatId);
|
||||
|
||||
void deleteBySessionId(String chatId);
|
||||
void deleteByChatUuid(String chatUuid);
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package com.pablotj.ia.chat.boot.persistence;
|
||||
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatIdentity;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
|
||||
|
||||
public class ChatMessageMapper {
|
||||
@ -9,9 +8,9 @@ public class ChatMessageMapper {
|
||||
throw new IllegalAccessException("Private access to ChatMessageMapper");
|
||||
}
|
||||
|
||||
public static ChatMessageEntity toEntity(String chatId, ChatMessage message) {
|
||||
public static ChatMessageEntity toEntity(ChatEntity chat, ChatMessage message) {
|
||||
ChatMessageEntity entity = new ChatMessageEntity();
|
||||
entity.setSessionId(chatId);
|
||||
entity.setChat(chat);
|
||||
entity.setRole(message.role());
|
||||
entity.setText(message.text());
|
||||
entity.setDate(message.date());
|
||||
@ -19,10 +18,6 @@ public class ChatMessageMapper {
|
||||
}
|
||||
|
||||
public static ChatMessage toDomain(ChatMessageEntity entity) {
|
||||
return new ChatMessage(entity.getSessionId(), entity.getRole(), entity.getText(), entity.getDate());
|
||||
}
|
||||
|
||||
public static ChatIdentity toEntityId(ChatMessageEntity m) {
|
||||
return new ChatIdentity(m.getSessionId(), m.getText().length() > 35 ? m.getText().substring(0, 35).concat("...") : m.getText());
|
||||
return new ChatMessage(entity.getChat().getUuid(), entity.getRole(), entity.getText(), entity.getDate());
|
||||
}
|
||||
}
|
@ -3,29 +3,44 @@ package com.pablotj.ia.chat.boot.persistence;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatIdentity;
|
||||
import com.pablotj.ia.chat.boot.domain.model.ChatMessage;
|
||||
import com.pablotj.ia.chat.boot.domain.port.ChatMessageStore;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Repository
|
||||
public class SqliteChatMessageStore implements ChatMessageStore {
|
||||
|
||||
private final ChatMessageJpaRepository repository;
|
||||
private final ChatJpaRepository chatJpaRepository;
|
||||
private final ChatMessageJpaRepository chatMessageJpaRepository;
|
||||
|
||||
public SqliteChatMessageStore(ChatMessageJpaRepository repository) {
|
||||
this.repository = repository;
|
||||
public SqliteChatMessageStore(ChatJpaRepository chatJpaRepository, ChatMessageJpaRepository chatMessageJpaRepository) {
|
||||
this.chatJpaRepository = chatJpaRepository;
|
||||
this.chatMessageJpaRepository = chatMessageJpaRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ChatIdentity createChat() {
|
||||
ChatEntity chat = new ChatEntity();
|
||||
chat.setUuid(UUID.randomUUID().toString());
|
||||
chat.setResume("Nuevo chat");
|
||||
chat.setCreatedDate(new Date());
|
||||
chat = chatJpaRepository.save(chat);
|
||||
return ChatMapper.toDomain(chat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatIdentity> getChats() {
|
||||
return repository.findFirstMessagePerSession().stream()
|
||||
.map(ChatMessageMapper::toEntityId)
|
||||
return chatJpaRepository.findAllByOrderByCreatedDateDesc().stream()
|
||||
.map(ChatMapper::toDomain)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getMessages(String chatId) {
|
||||
return repository.findBySessionIdOrderByIdAsc(chatId)
|
||||
public List<ChatMessage> getMessages(String chatUuid) {
|
||||
return chatMessageJpaRepository.findByChatUuidOrderByIdAsc(chatUuid)
|
||||
.stream()
|
||||
.map(ChatMessageMapper::toDomain)
|
||||
.toList();
|
||||
@ -33,11 +48,31 @@ public class SqliteChatMessageStore implements ChatMessageStore {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void saveMessages(String chatId, List<ChatMessage> messages) {
|
||||
repository.deleteBySessionId(chatId);
|
||||
List<ChatMessageEntity> entities = messages.stream()
|
||||
.map(m -> ChatMessageMapper.toEntity(chatId, m))
|
||||
.toList();
|
||||
repository.saveAll(entities);
|
||||
public void saveMessages(String chatUuid, List<ChatMessage> messages) {
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChatEntity chat = chatJpaRepository.findOneByUuid(chatUuid).orElse(null);
|
||||
|
||||
if (chat == null) {
|
||||
chat = new ChatEntity();
|
||||
chat.setUuid(chatUuid != null ? chatUuid : UUID.randomUUID().toString());
|
||||
chat.setResume(createResume(messages.getFirst()));
|
||||
chat.setCreatedDate(new Date());
|
||||
chat = chatJpaRepository.save(chat);
|
||||
} else if (chat.getMessages().isEmpty()) {
|
||||
chat.setResume(createResume(messages.getFirst()));
|
||||
chat = chatJpaRepository.save(chat);
|
||||
}
|
||||
|
||||
ChatEntity finalChat = chat;
|
||||
messages.forEach(msg -> finalChat.getMessages().add(ChatMessageMapper.toEntity(finalChat, msg)));
|
||||
|
||||
chatJpaRepository.save(finalChat);
|
||||
}
|
||||
|
||||
private String createResume(ChatMessage chatMessage) {
|
||||
return chatMessage.text().length() > 35 ? chatMessage.text().substring(0, 35).concat("...") : chatMessage.text();
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<!-- Menú lateral izquierdo -->
|
||||
<ChatSidebar
|
||||
:chats="chats"
|
||||
:current-chat-id="chatId"
|
||||
:current-chat-id="chatUuid"
|
||||
@select-chat="selectChat"
|
||||
@create-chat="createNewChat"
|
||||
/>
|
||||
@ -25,7 +25,7 @@ import { chatService } from '../services/chatService.ts'
|
||||
import { dateUtils } from '../utils/dateUtils.ts'
|
||||
|
||||
// Estado reactivo
|
||||
const chatId = ref('')
|
||||
const chatUuid = ref('')
|
||||
const chats = ref([])
|
||||
const messages = ref([])
|
||||
const isLoading = ref(false)
|
||||
@ -37,9 +37,9 @@ const loadHistory = async () => {
|
||||
chats.value = data
|
||||
|
||||
// Autoabrir primer chat si no hay chatId activo
|
||||
if (!chatId.value && data.length > 0) {
|
||||
chatId.value = data[0].id
|
||||
await loadMessages(chatId.value)
|
||||
if (!chatUuid.value && data.length > 0) {
|
||||
chatUuid.value = data[0].uuid
|
||||
await loadMessages(chatUuid.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cargando historial:", error)
|
||||
@ -58,19 +58,19 @@ const loadMessages = async (selectedChatId) => {
|
||||
|
||||
// Seleccionar un chat
|
||||
const selectChat = async (selectedId) => {
|
||||
if (selectedId !== chatId.value) {
|
||||
chatId.value = selectedId
|
||||
await loadMessages(chatId.value)
|
||||
if (selectedId !== chatUuid.value) {
|
||||
chatUuid.value = selectedId
|
||||
await loadMessages(chatUuid.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Crear nuevo chat
|
||||
const createNewChat = async () => {
|
||||
try {
|
||||
const newChatId = await chatService.createChat()
|
||||
chatId.value = newChatId
|
||||
const response = await chatService.createChat()
|
||||
chatUuid.value = response.uuid;
|
||||
await loadHistory()
|
||||
await loadMessages(chatId.value)
|
||||
await loadMessages(chatUuid.value)
|
||||
} catch (error) {
|
||||
console.error("Error al crear nuevo chat:", error)
|
||||
}
|
||||
@ -79,7 +79,7 @@ const createNewChat = async () => {
|
||||
// Enviar mensaje
|
||||
const sendMessage = async (prompt) => {
|
||||
// Crear nuevo chat si no existe
|
||||
if (!chatId.value) {
|
||||
if (!chatUuid.value) {
|
||||
await createNewChat()
|
||||
}
|
||||
|
||||
@ -94,8 +94,8 @@ const sendMessage = async (prompt) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const data = await chatService.sendMessage(chatId.value, prompt)
|
||||
chatId.value = data.chatId
|
||||
const data = await chatService.sendMessage(chatUuid.value, prompt)
|
||||
chatUuid.value = data.chatId
|
||||
|
||||
// Agregar respuesta del bot
|
||||
const botMessage = {
|
||||
|
@ -7,11 +7,11 @@
|
||||
<ul class="chat-list">
|
||||
<li
|
||||
v-for="chat in chats"
|
||||
:key="chat.id"
|
||||
:class="{ active: chat.id === currentChatId }"
|
||||
@click="$emit('select-chat', chat.id)"
|
||||
:key="chat.uuid"
|
||||
:class="{ active: chat.uuid === currentChatId }"
|
||||
@click="$emit('select-chat', chat.uuid)"
|
||||
>
|
||||
{{ chat.name }}
|
||||
{{ chat.resume }}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
@ -31,7 +31,7 @@ class ChatService {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
return await response.text()
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error("Error creating chat:", error)
|
||||
toast.error("Could not create chat")
|
||||
|
Loading…
x
Reference in New Issue
Block a user