Create ChatEntity to group chat messages under a parent entity

This commit is contained in:
2025-07-20 10:47:16 +02:00
parent 5450d97abc
commit 3844734794
18 changed files with 207 additions and 74 deletions

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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 {
}

View File

@@ -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")

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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; }

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}