feat(chat): auto-generate titles for chat sessions

This commit is contained in:
Rodrigo Verdiani 2026-06-12 16:44:52 -03:00
parent 4dc1493473
commit 6cbe81f425
4 changed files with 71 additions and 1 deletions

View File

@ -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.<String>builder().data(toJson(data)).build();
}

View File

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

View File

@ -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<StreamEvent> 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<StreamEvent> eventSink;

View File

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