Skip to content

Commit

Permalink
Feat #35 Conversation history
Browse files Browse the repository at this point in the history
  • Loading branch information
stephanj committed Sep 3, 2024
1 parent 7c30879 commit 9532149
Show file tree
Hide file tree
Showing 25 changed files with 530 additions and 29 deletions.
16 changes: 16 additions & 0 deletions src/main/java/com/devoxx/genie/model/conversation/ChatMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.devoxx.genie.model.conversation;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class ChatMessage {
private boolean isUser;
private String content;
private String timestamp;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.devoxx.genie.model.conversation;


import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@NoArgsConstructor
@Setter
@Getter
public class Conversation {
private String id;
private String timestamp;
private String title;
private String llmProvider;
private String modelName;
private Boolean apiKeyUsed;
private Long inputCost;
private Long outputCost;
private Integer contextWindow;
private long executionTimeMs;
private List<ChatMessage> messages = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public void executePrompt(@NotNull ChatMessageContext chatMessageContext,
Runnable enableButtons) {

isRunning = true;

new Task.Backgroundable(chatMessageContext.getProject(), "Working...", true) {
@Override
public void run(@NotNull ProgressIndicator progressIndicator) {
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/devoxx/genie/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.devoxx.genie.service;

import com.devoxx.genie.model.conversation.ChatMessage;
import com.devoxx.genie.model.conversation.Conversation;
import com.devoxx.genie.model.request.ChatMessageContext;
import com.devoxx.genie.ui.listener.ConversationEventListener;
import com.devoxx.genie.ui.topic.AppTopics;
import com.intellij.openapi.application.ApplicationManager;
import org.jetbrains.annotations.NotNull;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class ChatService implements ConversationEventListener {

private final ConversationStorageService storageService;

public ChatService(ConversationStorageService storageService) {
this.storageService = storageService;

ApplicationManager.getApplication().getMessageBus()
.connect()
.subscribe(AppTopics.CONVERSATION_TOPIC, this);
}

@Override
public void onNewConversation(@NotNull ChatMessageContext chatMessageContext) {
saveConversation(chatMessageContext);
}

private void saveConversation(@NotNull ChatMessageContext chatMessageContext) {
Conversation conversation = new Conversation();
conversation.setId(UUID.randomUUID().toString());
conversation.setTitle(generateTitle(chatMessageContext.getUserPrompt()));
conversation.setTimestamp(LocalDateTime.now().toString());
conversation.setModelName(chatMessageContext.getLanguageModel().getModelName());
conversation.setExecutionTimeMs(chatMessageContext.getExecutionTimeMs());
conversation.setApiKeyUsed(chatMessageContext.getLanguageModel().isApiKeyUsed());
conversation.setLlmProvider(chatMessageContext.getLanguageModel().getProvider().name());

conversation.getMessages().add(new ChatMessage(true, chatMessageContext.getUserPrompt(), LocalDateTime.now().toString()));
conversation.getMessages().add(new ChatMessage(false, chatMessageContext.getAiMessage().text(), LocalDateTime.now().toString()));

storageService.addConversation(conversation);
}

private String generateTitle(@NotNull String userPrompt) {
return userPrompt.length() > 30 ? userPrompt.substring(0, 30) + "..." : userPrompt;
}

public void startNewConversation(String title) {
Conversation conversation = new Conversation();
conversation.setTitle(title);
conversation.setTimestamp(LocalDateTime.now().toString());
conversation.setMessages(new ArrayList<>());
storageService.addConversation(conversation);
}

// TODO: Implement the loadConversations method
public List<Conversation> loadConversations() {
return storageService.getConversations();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.devoxx.genie.service;

import com.devoxx.genie.model.conversation.Conversation;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.ArrayList;
import java.util.List;

@Service
@State(name = "DevoxxGenieConversationStorage", storages = @Storage("devoxxgenie-conversations.xml"))
public final class ConversationStorageService implements PersistentStateComponent<ConversationStorageService.State> {

private final State myState = new State();

public static ConversationStorageService getInstance() {
return ApplicationManager.getApplication().getService(ConversationStorageService.class);
}

@Override
public State getState() {
return myState;
}

@Override
public void loadState(@NotNull State state) {
XmlSerializerUtil.copyBean(state, myState);
}

public void addConversation(Conversation conversation) {
myState.conversations.add(conversation);
saveState();
}

public @NotNull List<Conversation> getConversations() {
return new ArrayList<>(myState.conversations);
}

public void removeConversation(Conversation conversation) {
myState.conversations.remove(conversation);
saveState();
}

public void clearAllConversations() {
myState.conversations.clear();
saveState();
}

private void saveState() {
ApplicationManager.getApplication().invokeLater(() ->
ApplicationManager.getApplication().saveSettings()
);
}

@XmlAccessorType(XmlAccessType.FIELD)
public static class State {
public List<Conversation> conversations = new ArrayList<>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.devoxx.genie.error.ErrorHandler;
import com.devoxx.genie.model.request.ChatMessageContext;
import com.devoxx.genie.ui.panel.PromptOutputPanel;
import com.devoxx.genie.ui.topic.AppTopics;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;

Expand Down Expand Up @@ -41,6 +43,11 @@ public void execute(ChatMessageContext chatMessageContext,
// Set token usage and cost
chatMessageContext.setTokenUsageAndCost(response.tokenUsage());

// Add the conversation to the chat service
ApplicationManager.getApplication().getMessageBus()
.syncPublisher(AppTopics.CONVERSATION_TOPIC)
.onNewConversation(chatMessageContext);

promptOutputPanel.addChatResponse(chatMessageContext);
} else if (isCancelled) {
LOG.debug(">>>> Prompt execution cancelled");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@
public class PromptExecutionService {

private static final Logger LOG = Logger.getInstance(PromptExecutionService.class);

@NotNull
static PromptExecutionService getInstance() {
return ApplicationManager.getApplication().getService(PromptExecutionService.class);
}

private final ExecutorService queryExecutor = Executors.newSingleThreadExecutor();
private CompletableFuture<Response<AiMessage>> queryFuture = null;

Expand All @@ -37,6 +31,11 @@ static PromptExecutionService getInstance() {

private final ReentrantLock queryLock = new ReentrantLock();

@NotNull
static PromptExecutionService getInstance() {
return ApplicationManager.getApplication().getService(PromptExecutionService.class);
}

/**
* Execute the query with the given language text pair and chat language model.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.devoxx.genie.ui.component.ExpandablePanel;
import com.devoxx.genie.ui.panel.ChatStreamingResponsePanel;
import com.devoxx.genie.ui.panel.PromptOutputPanel;
import com.devoxx.genie.ui.topic.AppTopics;
import com.intellij.openapi.application.ApplicationManager;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import org.jetbrains.annotations.NotNull;
Expand All @@ -17,7 +19,9 @@ public class StreamingResponseHandler implements dev.langchain4j.model.Streaming
private final ChatStreamingResponsePanel streamingChatResponsePanel;
private final PromptOutputPanel promptOutputPanel;
private volatile boolean isStopped = false;
private long startTime;
private final long startTime;
private final StringBuilder responseBuilder = new StringBuilder();

public StreamingResponseHandler(ChatMessageContext chatMessageContext,
@NotNull PromptOutputPanel promptOutputPanel,
Runnable enableButtons) {
Expand Down Expand Up @@ -48,7 +52,13 @@ public void onComplete(@NotNull Response<AiMessage> response) {
}

private void finalizeResponse(@NotNull Response<AiMessage> response) {

chatMessageContext.setAiMessage(response.content());

ApplicationManager.getApplication().getMessageBus()
.syncPublisher(AppTopics.CONVERSATION_TOPIC)
.onNewConversation(chatMessageContext);

ChatMemoryService.getInstance().add(response.content());
enableButtons.run();
}
Expand Down
34 changes: 23 additions & 11 deletions src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,14 @@
import com.devoxx.genie.model.Constant;
import com.devoxx.genie.model.LanguageModel;
import com.devoxx.genie.model.enumarations.ModelProvider;
import com.devoxx.genie.service.ChatMemoryService;
import com.devoxx.genie.service.FileListManager;
import com.devoxx.genie.service.LLMProviderService;
import com.devoxx.genie.model.request.ChatMessageContext;
import com.devoxx.genie.service.*;
import com.devoxx.genie.ui.component.PromptInputArea;
import com.devoxx.genie.ui.listener.ConversationEventListener;
import com.devoxx.genie.ui.listener.CustomPromptChangeListener;
import com.devoxx.genie.ui.listener.LLMSettingsChangeListener;
import com.devoxx.genie.ui.listener.SettingsChangeListener;
import com.devoxx.genie.ui.panel.ActionButtonsPanel;
import com.devoxx.genie.ui.panel.ConversationPanel;
import com.devoxx.genie.ui.panel.PromptContextFileListPanel;
import com.devoxx.genie.ui.panel.PromptOutputPanel;
import com.devoxx.genie.ui.panel.*;
import com.devoxx.genie.ui.renderer.ModelInfoRenderer;
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
import com.devoxx.genie.ui.topic.AppTopics;
Expand Down Expand Up @@ -48,7 +45,8 @@
public class DevoxxGenieToolWindowContent implements SettingsChangeListener,
LLMSettingsChangeListener,
ConversationStarter,
CustomPromptChangeListener {
CustomPromptChangeListener,
ConversationEventListener {

private static final Logger LOG = Logger.getInstance(DevoxxGenieToolWindowContent.class);

Expand All @@ -74,6 +72,8 @@ public class DevoxxGenieToolWindowContent implements SettingsChangeListener,

private String lastSelectedProvider = null;
private String lastSelectedLanguageModel = null;
private final ChatService chatService;
private final ConversationStorageService storageService = ConversationStorageService.getInstance();

/**
* The Devoxx Genie Tool Window Content constructor.
Expand All @@ -83,8 +83,11 @@ public class DevoxxGenieToolWindowContent implements SettingsChangeListener,
public DevoxxGenieToolWindowContent(@NotNull ToolWindow toolWindow) {
project = toolWindow.getProject();

DevoxxGenieStateService.getInstance().addLoadListener(this::onStateLoaded);
DevoxxGenieStateService.getInstance().loadState(DevoxxGenieStateService.getInstance());
DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance();
stateService.addLoadListener(this::onStateLoaded);
stateService.loadState(DevoxxGenieStateService.getInstance());

chatService = new ChatService(storageService);

setupMessageBusConnection(toolWindow);
}
Expand Down Expand Up @@ -143,6 +146,7 @@ private void setupMessageBusConnection(@NotNull ToolWindow toolWindow) {
MessageBusConnection messageBusConnection = project.getMessageBus().connect();
messageBusConnection.subscribe(AppTopics.LLM_SETTINGS_CHANGED_TOPIC, this);
messageBusConnection.subscribe(AppTopics.CUSTOM_PROMPT_CHANGED_TOPIC, this);
messageBusConnection.subscribe(AppTopics.CONVERSATION_TOPIC, this);
Disposer.register(toolWindow.getDisposable(), messageBusConnection);
}

Expand Down Expand Up @@ -171,7 +175,7 @@ private void initializeComponents() {
promptInputArea = new PromptInputArea(resourceBundle);
promptOutputPanel = new PromptOutputPanel(resourceBundle);
promptContextFileListPanel = new PromptContextFileListPanel(project);
conversationPanel = new ConversationPanel(project, this);
conversationPanel = new ConversationPanel(project, this, storageService, promptOutputPanel);
}

private void setupLayout() {
Expand Down Expand Up @@ -325,6 +329,9 @@ public void startNewConversation() {
FileListManager.getInstance().clear();
ChatMemoryService.getInstance().clear();

// TODO Set title based on first question
chatService.startNewConversation("");

SwingUtilities.invokeLater(() -> {
conversationPanel.updateNewConversationLabel();
promptInputArea.clear();
Expand Down Expand Up @@ -442,4 +449,9 @@ public void onCustomPromptsChanged() {
}
});
}

@Override
public void onNewConversation(ChatMessageContext chatMessageContext) {
conversationPanel.loadConversationHistory();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devoxx.genie.ui.listener;

import com.devoxx.genie.model.request.ChatMessageContext;

public interface ConversationEventListener {
void onNewConversation(ChatMessageContext chatMessageContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devoxx.genie.ui.listener;

import com.devoxx.genie.model.conversation.Conversation;

public interface ConversationSelectionListener {
void onConversationSelected(Conversation conversation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ public ActionButtonsPanel(Project project,
this.llmProvidersComboBox.addActionListener(e -> updateAddProjectButtonVisibility());
this.tokenCalculationService = new TokenCalculationService();


messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect();

messageBusConnection.subscribe(AppTopics.SETTINGS_CHANGED_TOPIC, this);
messageBusConnection.subscribe(AppTopics.PROMPT_SUBMISSION_TOPIC_TOPIC, this);

Expand Down
Loading

0 comments on commit 9532149

Please sign in to comment.