From 6cbe81f4258f0df4a2c159d39e941b32bd54d07c Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Fri, 12 Jun 2026 16:44:52 -0300 Subject: [PATCH] feat(chat): auto-generate titles for chat sessions --- .../ai/controller/ChatController.java | 2 + .../decompile/ai/model/dto/StreamEvent.java | 5 +- .../ai/decompile/ai/service/ChatService.java | 53 +++++++++++++++++++ .../ai/service/ChatSessionService.java | 12 +++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main/java/ai/decompile/ai/controller/ChatController.java b/src/main/java/ai/decompile/ai/controller/ChatController.java index 260201c..650dd9b 100644 --- a/src/main/java/ai/decompile/ai/controller/ChatController.java +++ b/src/main/java/ai/decompile/ai/controller/ChatController.java @@ -143,6 +143,8 @@ public class ChatController { oldName, "newName", (Object) newName); + case StreamEvent.TitleGenerated(var title, var sessionId) -> + Map.of("type", "title_generated", "title", title, "sessionId", sessionId); }; return ServerSentEvent.builder().data(toJson(data)).build(); } diff --git a/src/main/java/ai/decompile/ai/model/dto/StreamEvent.java b/src/main/java/ai/decompile/ai/model/dto/StreamEvent.java index 1e7fee1..2965ed5 100644 --- a/src/main/java/ai/decompile/ai/model/dto/StreamEvent.java +++ b/src/main/java/ai/decompile/ai/model/dto/StreamEvent.java @@ -10,7 +10,8 @@ public sealed interface StreamEvent StreamEvent.Done, StreamEvent.Error, StreamEvent.RenameSuggestion, - StreamEvent.FunctionRenamed { + StreamEvent.FunctionRenamed, + StreamEvent.TitleGenerated { record Chunk(String content) implements StreamEvent {} @@ -28,4 +29,6 @@ public sealed interface StreamEvent implements StreamEvent {} record FunctionRenamed(UUID functionId, String oldName, String newName) implements StreamEvent {} + + record TitleGenerated(String title, UUID sessionId) implements StreamEvent {} } diff --git a/src/main/java/ai/decompile/ai/service/ChatService.java b/src/main/java/ai/decompile/ai/service/ChatService.java index 85744ad..7deefa7 100644 --- a/src/main/java/ai/decompile/ai/service/ChatService.java +++ b/src/main/java/ai/decompile/ai/service/ChatService.java @@ -21,6 +21,7 @@ import lombok.extern.log4j.Log4j2; import org.jspecify.annotations.NonNull; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.support.ToolCallbacks; @@ -86,6 +87,12 @@ public class ChatService { () -> { log.debug("Chat completed for session {}", sessionId); saveAssistantMessage(session, assistantContent.toString()); + generateAndSetTitle( + session, + chatModel, + request.content(), + assistantContent.toString(), + eventSink); eventSink.tryEmitComplete(); }) .doOnError( @@ -131,6 +138,52 @@ public class ChatService { } } + private void generateAndSetTitle( + ChatSession session, + ChatModel chatModel, + String userMessage, + String assistantResponse, + Sinks.Many eventSink) { + + if (Objects.isNull(session.getTitle()) || !session.getTitle().endsWith(" — Chat")) { + return; + } + + try { + var truncatedResponse = AiUtils.truncate(assistantResponse, 500); + var prompt = + String.format( + """ + Generate a very short title for a chat conversation + (maximum 6 words, 50 characters). Output ONLY the title text. + No quotes, no punctuation, no explanation. + + User message: %s + + Assistant response: %s + + Title:""", + userMessage, truncatedResponse); + + var response = chatModel.call(new Prompt(new UserMessage(prompt))); + var title = response.getResult().getOutput().getText(); + + if (Objects.nonNull(title) && !title.isBlank()) { + title = title.trim().replaceAll("^[\"'\\[\\]]+|[\"'\\[\\]]+$", ""); + + if (title.length() > 50) { + title = title.substring(0, 50); + } + + sessionService.updateTitle(session.getId(), title); + eventSink.tryEmitNext(new StreamEvent.TitleGenerated(title, session.getId())); + log.info("Auto-generated title '{}' for session {}", title, session.getId()); + } + } catch (Exception e) { + log.warn("Failed to auto-generate title for session {}: {}", session.getId(), e.getMessage()); + } + } + private class ObservedToolCallback implements ToolCallback { private final ToolCallback delegate; private final Sinks.Many eventSink; diff --git a/src/main/java/ai/decompile/ai/service/ChatSessionService.java b/src/main/java/ai/decompile/ai/service/ChatSessionService.java index 9098479..88a7a3c 100644 --- a/src/main/java/ai/decompile/ai/service/ChatSessionService.java +++ b/src/main/java/ai/decompile/ai/service/ChatSessionService.java @@ -96,4 +96,16 @@ public class ChatSessionService { messageRepository.save(msg); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void updateTitle(UUID sessionId, String title) { + var session = + sessionRepository + .findById(sessionId) + .orElseThrow( + () -> new NotFoundException("Chat session with id '" + sessionId + "' not found")); + + session.setTitle(title); + sessionRepository.save(session); + } }