From e40630d7967daaaa7e64960da80eb881ec3e7aa8 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Wed, 8 May 2024 10:11:13 +1200 Subject: [PATCH] feat: Implement Ollama as a high-level service (#510) * Initial implementation of Ollama as a service * Fix model selector in tool window * Enable image attachment * Rewrite OllamaSettingsForm in Kt * Create OllamaInlineCompletionModel and use it for building completion template * Add support for blocking code completion on models that we don't know support it * Allow disabling code completion settings * Disable code completion settings when an unsupported model is entered * Track FIM template in settings as a derived state * Update llm-client * Initial implementation of model combo box * Add Ollama icon and display models as list * Make OllamaSettingsState immutable & convert OllamaSettings to Kotlin * Add refresh models button * Distinguish between empty/needs refresh/loading * Avoid storing any model if the combo box is empty * Fix icon size * Back to mutable settings There were some bugs with immutable settings * Store available models in settings state * Expose available models in model dropdown * Add dark icon * Cleanups for CompletionRequestProvider * Fix checkstyle issues * refactor: migrate to SimplePersistentStateComponent * fix: add code completion stop tokens * fix: display only one item in the model popup action group * fix: add back multi model selection --------- Co-authored-by: Carl-Robert Linnupuu --- .../java/ee/carlrobert/codegpt/Icons.java | 1 + .../completions/CompletionClientProvider.java | 13 ++ .../CompletionRequestProvider.java | 83 ++++++++- .../completions/CompletionRequestService.java | 29 +++- .../conversations/ConversationService.java | 9 +- .../codegpt/settings/GeneralSettings.java | 9 + .../settings/GeneralSettingsComponent.java | 11 ++ .../settings/GeneralSettingsConfigurable.java | 10 +- .../codegpt/settings/service/ServiceType.java | 3 +- .../service/llama/form/LlamaSettingsForm.java | 3 +- .../service/openai/OpenAISettingsForm.java | 3 +- .../chat/ui/textarea/ModelComboBoxAction.java | 43 +++++ .../chat/ui/textarea/UserPromptTextArea.java | 2 + .../CodeCompletionFeatureToggleActions.kt | 29 +++- .../CodeCompletionRequestFactory.kt | 33 +++- .../CodeGPTInlineCompletionProvider.kt | 2 + .../CodeCompletionConfigurationForm.kt | 61 ++++++- .../settings/service/ollama/OllamaSettings.kt | 20 +++ .../service/ollama/OllamaSettingsForm.kt | 163 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + src/main/resources/icons/ollama.svg | 7 + src/main/resources/icons/ollama_dark.svg | 7 + .../resources/messages/codegpt.properties | 2 + 23 files changed, 505 insertions(+), 39 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt create mode 100644 src/main/resources/icons/ollama.svg create mode 100644 src/main/resources/icons/ollama_dark.svg diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index 6dbeaf12..0e210996 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -16,6 +16,7 @@ public final class Icons { public static final Icon Sparkle = IconLoader.getIcon("/icons/sparkle.svg", Icons.class); public static final Icon You = IconLoader.getIcon("/icons/you.svg", Icons.class); public static final Icon YouSmall = IconLoader.getIcon("/icons/you_small.png", Icons.class); + public static final Icon Ollama = IconLoader.getIcon("/icons/ollama.svg", Icons.class); public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class); public static final Icon Upload = IconLoader.getIcon("/icons/upload.svg", Icons.class); } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java index ee1506d4..0e74e9bd 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionClientProvider.java @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.completions; +import com.intellij.openapi.application.ApplicationManager; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.completions.you.YouUserManager; import ee.carlrobert.codegpt.credentials.CredentialsStore; @@ -8,11 +9,13 @@ import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.anthropic.ClaudeClient; import ee.carlrobert.llm.client.azure.AzureClient; import ee.carlrobert.llm.client.azure.AzureCompletionRequestParams; import ee.carlrobert.llm.client.llama.LlamaClient; +import ee.carlrobert.llm.client.ollama.OllamaClient; import ee.carlrobert.llm.client.openai.OpenAIClient; import ee.carlrobert.llm.client.you.UTMParameters; import ee.carlrobert.llm.client.you.YouClient; @@ -92,6 +95,16 @@ public static LlamaClient getLlamaClient() { return builder.build(getDefaultClientBuilder()); } + public static OllamaClient getOllamaClient() { + var host = ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .getHost(); + return new OllamaClient.Builder() + .setHost(host) + .build(getDefaultClientBuilder()); + } + public static OkHttpClient.Builder getDefaultClientBuilder() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); var advancedSettings = AdvancedSettings.getCurrentState(); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index a5dc6280..04f0d39c 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -26,8 +26,8 @@ import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceChatCompletionSettingsState; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceState; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.you.YouSettings; import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration; @@ -41,6 +41,8 @@ import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageImageContent; import ee.carlrobert.llm.client.anthropic.completion.ClaudeMessageTextContent; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; +import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionMessage; +import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionDetailedMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; @@ -56,6 +58,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -140,7 +143,8 @@ public static Request buildCustomOpenAICompletionRequest(String context, String public static Request buildCustomOpenAILookupCompletionRequest(String context) { return buildCustomOpenAIChatCompletionRequest( - ApplicationManager.getApplication().getService(CustomServiceState.class) + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() .getChatCompletionSettings(), List.of( new OpenAIChatCompletionStandardMessage( @@ -210,7 +214,7 @@ public OpenAIChatCompletionRequest buildOpenAIChatCompletionRequest( @Nullable String model, CallParameters callParameters) { var configuration = ConfigurationSettings.getCurrentState(); - return new OpenAIChatCompletionRequest.Builder(buildMessages(model, callParameters)) + return new OpenAIChatCompletionRequest.Builder(buildOpenAIMessages(model, callParameters)) .setModel(model) .setMaxTokens(configuration.getMaxTokens()) .setStream(true) @@ -222,7 +226,7 @@ public Request buildCustomOpenAIChatCompletionRequest( CallParameters callParameters) { return buildCustomOpenAIChatCompletionRequest( settings, - buildMessages(callParameters), + buildOpenAIMessages(callParameters), true); } @@ -307,7 +311,68 @@ public ClaudeCompletionRequest buildAnthropicChatCompletionRequest( return request; } - private List buildMessages(CallParameters callParameters) { + public OllamaChatCompletionRequest buildOllamaChatCompletionRequest( + CallParameters callParameters + ) { + var settings = ApplicationManager.getApplication().getService(OllamaSettings.class).getState(); + return new OllamaChatCompletionRequest + .Builder(settings.getModel(), buildOllamaMessages(callParameters)) + .build(); + } + + private List buildOllamaMessages(CallParameters callParameters) { + var message = callParameters.getMessage(); + var messages = new ArrayList(); + if (callParameters.getConversationType() == ConversationType.DEFAULT) { + String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt(); + messages.add(new OllamaChatCompletionMessage("system", systemPrompt, null)); + } + if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { + messages.add( + new OllamaChatCompletionMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT, null) + ); + } + + for (var prevMessage : conversation.getMessages()) { + if (callParameters.isRetry() && prevMessage.getId().equals(message.getId())) { + break; + } + var prevMessageImageFilePath = prevMessage.getImageFilePath(); + if (prevMessageImageFilePath != null && !prevMessageImageFilePath.isEmpty()) { + try { + var imageFilePath = Path.of(prevMessageImageFilePath); + var imageBytes = Files.readAllBytes(imageFilePath); + var imageBase64 = Base64.getEncoder().encodeToString(imageBytes); + messages.add( + new OllamaChatCompletionMessage( + "user", prevMessage.getPrompt(), List.of(imageBase64) + ) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + messages.add( + new OllamaChatCompletionMessage("user", prevMessage.getPrompt(), null) + ); + } + messages.add( + new OllamaChatCompletionMessage("assistant", prevMessage.getResponse(), null) + ); + } + + if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { + var imageBase64 = Base64.getEncoder().encodeToString(callParameters.getImageData()); + messages.add( + new OllamaChatCompletionMessage("user", message.getPrompt(), List.of(imageBase64)) + ); + } else { + messages.add(new OllamaChatCompletionMessage("user", message.getPrompt(), null)); + } + return messages; + } + + private List buildOpenAIMessages(CallParameters callParameters) { var message = callParameters.getMessage(); var messages = new ArrayList(); if (callParameters.getConversationType() == ConversationType.DEFAULT) { @@ -339,7 +404,9 @@ private List buildMessages(CallParameters callParam } else { messages.add(new OpenAIChatCompletionStandardMessage("user", prevMessage.getPrompt())); } - messages.add(new OpenAIChatCompletionStandardMessage("assistant", prevMessage.getResponse())); + messages.add( + new OpenAIChatCompletionStandardMessage("assistant", prevMessage.getResponse()) + ); } if (callParameters.getImageMediaType() != null && callParameters.getImageData().length > 0) { @@ -355,10 +422,10 @@ private List buildMessages(CallParameters callParam return messages; } - private List buildMessages( + private List buildOpenAIMessages( @Nullable String model, CallParameters callParameters) { - var messages = buildMessages(callParameters); + var messages = buildOpenAIMessages(callParameters); if (model == null || GeneralSettings.getCurrentState().getSelectedService() == ServiceType.YOU) { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 9d98e100..6edfe92d 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -21,11 +21,14 @@ import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.DeserializationUtil; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionRequest; import ee.carlrobert.llm.client.anthropic.completion.ClaudeCompletionStandardMessage; import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest; +import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionMessage; +import ee.carlrobert.llm.client.ollama.completion.request.OllamaChatCompletionRequest; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionEventSourceListener; import ee.carlrobert.llm.client.openai.completion.OpenAITextCompletionEventSourceListener; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; @@ -104,6 +107,9 @@ public EventSource getChatCompletionAsync( callParameters.getMessage(), callParameters.getConversationType()), eventListener); + case OLLAMA -> CompletionClientProvider.getOllamaClient().getChatCompletionAsync( + requestProvider.buildOllamaChatCompletionRequest(callParameters), + eventListener); }; } @@ -123,6 +129,9 @@ public EventSource getCodeCompletionAsync( .getInfillAsync( CodeCompletionRequestFactory.buildLlamaRequest(requestDetails), eventListener); + case OLLAMA -> CompletionClientProvider.getOllamaClient().getCompletionAsync( + CodeCompletionRequestFactory.INSTANCE.buildOllamaRequest(requestDetails), + eventListener); default -> throw new IllegalArgumentException("Code completion not supported for selected service"); }; @@ -189,6 +198,20 @@ public void generateCommitMessageAsync( .setRepeat_penalty(settings.getRepeatPenalty()) .build(), eventListener); break; + case OLLAMA: + var model = ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .getModel(); + var request = new OllamaChatCompletionRequest.Builder( + model, + List.of( + new OllamaChatCompletionMessage("system", systemPrompt, null), + new OllamaChatCompletionMessage("user", gitDiff, null) + ) + ).build(); + CompletionClientProvider.getOllamaClient().getChatCompletionAsync(request, eventListener); + break; default: LOG.debug("Unknown service: {}", selectedService); break; @@ -228,9 +251,9 @@ public static boolean isRequestAllowed(ServiceType serviceType) { case OPENAI -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.OPENAI_API_KEY); case AZURE -> CredentialsStore.INSTANCE.isCredentialSet( AzureSettings.getCurrentState().isUseAzureApiKeyAuthentication() - ? CredentialKey.AZURE_OPENAI_API_KEY - : CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN); - case CUSTOM_OPENAI, ANTHROPIC, LLAMA_CPP -> true; + ? CredentialKey.AZURE_OPENAI_API_KEY + : CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN); + case CUSTOM_OPENAI, ANTHROPIC, LLAMA_CPP, OLLAMA -> true; case YOU -> false; }; } diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java index 8e5757fd..fcded8e6 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/ConversationService.java @@ -9,6 +9,7 @@ import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import java.time.LocalDateTime; import java.util.ArrayList; @@ -195,9 +196,13 @@ private static String getModelForSelectedService(ServiceType serviceType) { case LLAMA_CPP -> { var llamaSettings = LlamaSettings.getCurrentState(); yield llamaSettings.isUseCustomModel() - ? llamaSettings.getCustomLlamaModelPath() - : llamaSettings.getHuggingFaceModel().getCode(); + ? llamaSettings.getCustomLlamaModelPath() + : llamaSettings.getHuggingFaceModel().getCode(); } + case OLLAMA -> ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .getModel(); }; } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java index 56d9ba3c..423fcc06 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java @@ -11,6 +11,7 @@ import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import org.jetbrains.annotations.NotNull; @@ -69,6 +70,9 @@ public void sync(Conversation conversation) { if ("you.chat.completion".equals(clientCode)) { state.setSelectedService(ServiceType.YOU); } + if ("ollama.chat.completion".equals(clientCode)) { + state.setSelectedService(ServiceType.OLLAMA); + } } public String getModel() { @@ -98,6 +102,11 @@ public String getModel() { llamaModel.getLabel(), huggingFaceModel.getParameterSize(), huggingFaceModel.getQuantization()); + case OLLAMA: + return ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .getModel(); default: return "Unknown"; } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java index d5fea919..9583040b 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java @@ -4,6 +4,7 @@ import static ee.carlrobert.codegpt.settings.service.ServiceType.AZURE; import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; +import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; @@ -20,6 +21,8 @@ import ee.carlrobert.codegpt.settings.service.custom.CustomServiceForm; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsForm; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; import ee.carlrobert.codegpt.settings.service.you.YouSettings; @@ -45,6 +48,7 @@ public class GeneralSettingsComponent { private final AzureSettingsForm azureSettingsForm; private final YouSettingsForm youSettingsForm; private final LlamaSettingsForm llamaSettingsForm; + private final OllamaSettingsForm ollamaSettingsForm; public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings settings) { displayNameField = new JBTextField(settings.getState().getDisplayName(), 20); @@ -54,6 +58,7 @@ public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings set azureSettingsForm = new AzureSettingsForm(AzureSettings.getCurrentState()); youSettingsForm = new YouSettingsForm(YouSettings.getCurrentState(), parentDisposable); llamaSettingsForm = new LlamaSettingsForm(LlamaSettings.getCurrentState()); + ollamaSettingsForm = new OllamaSettingsForm(); var cardLayout = new DynamicCardLayout(); var cards = new JPanel(cardLayout); @@ -63,6 +68,7 @@ public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings set cards.add(azureSettingsForm.getForm(), AZURE.getCode()); cards.add(youSettingsForm, YOU.getCode()); cards.add(llamaSettingsForm, LLAMA_CPP.getCode()); + cards.add(ollamaSettingsForm.getForm(), OLLAMA.getCode()); var serviceComboBoxModel = new DefaultComboBoxModel(); serviceComboBoxModel.addAll(Arrays.stream(ServiceType.values()).toList()); serviceComboBox = new ComboBox<>(serviceComboBoxModel); @@ -106,6 +112,10 @@ public YouSettingsForm getYouSettingsForm() { return youSettingsForm; } + public OllamaSettingsForm getOllamaSettingsForm() { + return ollamaSettingsForm; + } + public ServiceType getSelectedService() { return serviceComboBox.getItem(); } @@ -137,6 +147,7 @@ public void resetForms() { azureSettingsForm.resetForm(); youSettingsForm.resetForm(); llamaSettingsForm.resetForm(); + ollamaSettingsForm.resetForm(); } static class DynamicCardLayout extends CardLayout { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index 3d12089a..17a5fe8e 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -20,6 +20,8 @@ import ee.carlrobert.codegpt.settings.service.custom.CustomServiceForm; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsForm; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; import ee.carlrobert.codegpt.settings.service.you.YouSettings; @@ -68,7 +70,8 @@ public boolean isModified() { || AnthropicSettings.getInstance().isModified(component.getAnthropicSettingsForm()) || AzureSettings.getInstance().isModified(component.getAzureSettingsForm()) || YouSettings.getInstance().isModified(component.getYouSettingsForm()) - || LlamaSettings.getInstance().isModified(component.getLlamaSettingsForm()); + || LlamaSettings.getInstance().isModified(component.getLlamaSettingsForm()) + || component.getOllamaSettingsForm().isModified(); } @Override @@ -84,6 +87,7 @@ public void apply() { applyAzureSettings(component.getAzureSettingsForm()); applyYouSettings(component.getYouSettingsForm()); applyLlamaSettings(component.getLlamaSettingsForm()); + component.getOllamaSettingsForm().applyChanges(); var serviceChanged = component.getSelectedService() != settings.getSelectedService(); var modelChanged = !OpenAISettings.getCurrentState().getModel() @@ -133,6 +137,10 @@ private void applyAzureSettings(AzureSettingsForm form) { form.getActiveDirectoryToken()); } + private void applyOllamaSettings(OllamaSettingsForm form) { + form.applyChanges(); + } + @Override public void reset() { var settings = GeneralSettings.getCurrentState(); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java index 8ee130cd..d8c6425b 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceType.java @@ -8,7 +8,8 @@ public enum ServiceType { ANTHROPIC("ANTHROPIC", "service.anthropic.title", "anthropic.chat.completion"), AZURE("AZURE", "service.azure.title", "azure.chat.completion"), YOU("YOU", "service.you.title", "you.chat.completion"), - LLAMA_CPP("LLAMA_CPP", "service.llama.title", "llama.chat.completion"); + LLAMA_CPP("LLAMA_CPP", "service.llama.title", "llama.chat.completion"), + OLLAMA("OLLAMA", "service.ollama.title", "ollama.chat.completion"); private final String code; private final String label; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java index d5260b24..da415871 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaSettingsForm.java @@ -22,7 +22,8 @@ public LlamaSettingsForm(LlamaSettingsState settings) { llamaRequestPreferencesForm = new LlamaRequestPreferencesForm(settings); codeCompletionConfigurationForm = new CodeCompletionConfigurationForm( settings.isCodeCompletionsEnabled(), - settings.getCodeCompletionMaxTokens()); + settings.getCodeCompletionMaxTokens(), + null); init(); } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java index f0bdc78b..d0f4b4dc 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/openai/OpenAISettingsForm.java @@ -36,7 +36,8 @@ public OpenAISettingsForm(OpenAISettingsState settings) { OpenAIChatCompletionModel.findByCode(settings.getModel())); codeCompletionConfigurationForm = new CodeCompletionConfigurationForm( settings.isCodeCompletionsEnabled(), - settings.getCodeCompletionMaxTokens()); + settings.getCodeCompletionMaxTokens(), + null); } public JPanel getForm() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index 71aef244..38457966 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; +import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; import static java.lang.String.format; @@ -23,6 +24,8 @@ import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettingsState; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsState; import ee.carlrobert.codegpt.settings.service.you.YouSettings; @@ -41,12 +44,16 @@ public class ModelComboBoxAction extends ComboBoxAction { private final GeneralSettingsState settings; private final OpenAISettingsState openAISettings; private final YouSettingsState youSettings; + private final OllamaSettingsState ollamaSettings; public ModelComboBoxAction(Runnable onModelChange, ServiceType selectedService) { this.onModelChange = onModelChange; settings = GeneralSettings.getCurrentState(); openAISettings = OpenAISettings.getCurrentState(); youSettings = YouSettings.getCurrentState(); + ollamaSettings = ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState(); updateTemplatePresentation(selectedService); subscribeToYouSignedOutTopic(ApplicationManager.getApplication().getMessageBus().connect()); @@ -103,6 +110,9 @@ public JComponent createCustomComponent( getLlamaCppPresentationText(), Icons.Llama, presentation)); + actionGroup.addSeparator("Ollama"); + ollamaSettings.getAvailableModels().forEach(model -> + actionGroup.add(createOllamaModelAction(model, presentation))); if (YouUserManager.getInstance().isSubscribed()) { actionGroup.addSeparator("You.com"); @@ -179,7 +189,12 @@ private void updateTemplatePresentation(ServiceType selectedService) { templatePresentation.setText(getLlamaCppPresentationText()); templatePresentation.setIcon(Icons.Llama); break; + case OLLAMA: + templatePresentation.setIcon(Icons.Ollama); + templatePresentation.setText(ollamaSettings.getModel()); + break; default: + break; } } @@ -235,6 +250,34 @@ private void handleModelChange( onModelChange.run(); } + private AnAction createOllamaModelAction( + String model, + Presentation comboBoxPresentation + ) { + return new DumbAwareAction(model, "", Icons.Ollama) { + @Override + public void update(@NotNull AnActionEvent event) { + var presentation = event.getPresentation(); + presentation.setEnabled(!presentation.getText().equals(comboBoxPresentation.getText())); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + ollamaSettings.setModel(model); + handleModelChange( + OLLAMA, + model, + Icons.Ollama, + comboBoxPresentation); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + }; + } + private AnAction createOpenAIModelAction( OpenAIChatCompletionModel model, Presentation comboBoxPresentation) { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java index e8cedd82..e7bc6862 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC; +import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW; @@ -192,6 +193,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { })); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); if (selectedService == ANTHROPIC + || selectedService == OLLAMA || (selectedService == OPENAI && GPT_4_VISION_PREVIEW.getCode().equals(OpenAISettings.getCurrentState().getModel()))) { iconsPanel.add(new IconActionButton(new AttachImageAction())); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index be2d0b66..9faf9666 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -9,34 +9,44 @@ import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.ServiceType.* import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings abstract class CodeCompletionFeatureToggleActions( private val enableFeatureAction: Boolean ) : DumbAwareAction() { - override fun actionPerformed(e: AnActionEvent) { when (GeneralSettings.getCurrentState().selectedService) { - OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction - LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction - CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled = - enableFeatureAction + OPENAI -> + OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + + LLAMA_CPP -> + LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + + OLLAMA -> service().state.codeCompletionsEnabled = enableFeatureAction + CUSTOM_OPENAI -> service().state + .codeCompletionSettings + .codeCompletionsEnabled = enableFeatureAction + ANTHROPIC, AZURE, YOU, - null -> { /* no-op for these services */ } + null -> { /* no-op for these services */ + } } } override fun update(e: AnActionEvent) { val selectedService = GeneralSettings.getCurrentState().selectedService val codeCompletionEnabled = isCodeCompletionsEnabled(selectedService) - e.presentation.isEnabled = codeCompletionEnabled != enableFeatureAction - e.presentation.isVisible = when (selectedService) { + e.presentation.isVisible = codeCompletionEnabled != enableFeatureAction + e.presentation.isEnabled = when (selectedService) { OPENAI, CUSTOM_OPENAI, - LLAMA_CPP -> true + LLAMA_CPP, + OLLAMA -> true + ANTHROPIC, AZURE, YOU, @@ -53,6 +63,7 @@ abstract class CodeCompletionFeatureToggleActions( OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled LLAMA_CPP -> LlamaSettings.isCodeCompletionsPossible() + OLLAMA -> service().state.codeCompletionsEnabled ANTHROPIC, AZURE, YOU -> false diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index d335144a..aadc7d36 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -10,9 +10,12 @@ import ee.carlrobert.codegpt.settings.configuration.Placeholder import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettingsState +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest import ee.carlrobert.llm.client.llama.completion.LlamaInfillRequest +import ee.carlrobert.llm.client.ollama.completion.request.OllamaCompletionRequest +import ee.carlrobert.llm.client.ollama.completion.request.OllamaParameters import ee.carlrobert.llm.client.openai.completion.request.OpenAITextCompletionRequest import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request @@ -82,11 +85,29 @@ object CodeCompletionRequestFactory { fun buildLlamaRequest(details: InfillRequestDetails): LlamaInfillRequest { val settings = LlamaSettings.getCurrentState() val promptTemplate = getLlamaInfillPromptTemplate(settings) - return LlamaInfillRequest(LlamaCompletionRequest.Builder(null) - .setN_predict(settings.codeCompletionMaxTokens) - .setStream(true) - .setTemperature(0.4) - .setStop(promptTemplate.stopTokens), details.prefix, details.suffix) + return LlamaInfillRequest( + LlamaCompletionRequest.Builder(null) + .setN_predict(settings.codeCompletionMaxTokens) + .setStream(true) + .setTemperature(0.4) + .setStop(promptTemplate.stopTokens), details.prefix, details.suffix + ) + } + + fun buildOllamaRequest(details: InfillRequestDetails): OllamaCompletionRequest { + val settings = service().state + return OllamaCompletionRequest.Builder( + settings.model, + settings.fimTemplate.buildPrompt(details.prefix, details.suffix) + ) + .setOptions( + OllamaParameters.Builder() + .stop(settings.fimTemplate.stopTokens) + .numPredict(settings.codeCompletionMaxTokens) + .build() + ) + .setRaw(true) + .build() } private fun getLlamaInfillPromptTemplate(settings: LlamaSettingsState): InfillPromptTemplate { @@ -112,4 +133,4 @@ object CodeCompletionRequestFactory { else -> value } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index 32811531..27f3d43e 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -12,6 +12,7 @@ import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings +import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings import ee.carlrobert.codegpt.ui.OverlayUtil.showNotification import ee.carlrobert.llm.client.openai.completion.ErrorDetails @@ -70,6 +71,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled ServiceType.CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled + ServiceType.OLLAMA -> service().state.codeCompletionsEnabled ServiceType.ANTHROPIC, ServiceType.AZURE, ServiceType.YOU, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt index 1fddb3fa..ddf60b53 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/CodeCompletionConfigurationForm.kt @@ -1,13 +1,26 @@ package ee.carlrobert.codegpt.settings.service +import com.intellij.icons.AllIcons.General +import com.intellij.ide.HelpTooltip +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.panel.ComponentPanelBuilder +import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.fields.IntegerField import com.intellij.util.ui.FormBuilder import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate +import org.apache.commons.text.StringEscapeUtils +import java.awt.FlowLayout +import javax.swing.Box import javax.swing.JPanel -class CodeCompletionConfigurationForm(codeCompletionsEnabled: Boolean, maxTokens: Int) { +class CodeCompletionConfigurationForm( + codeCompletionsEnabled: Boolean, + maxTokens: Int, + fimTemplate: InfillPromptTemplate? +) { private val codeCompletionsEnabledCheckBox = JBCheckBox( CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), @@ -18,15 +31,33 @@ class CodeCompletionConfigurationForm(codeCompletionsEnabled: Boolean, maxTokens columns = 12 value = maxTokens } + private val promptTemplateComboBox = + ComboBox(EnumComboBoxModel(InfillPromptTemplate::class.java)).apply { + item = fimTemplate + addItemListener { + updatePromptTemplateHelpTooltip(it.item as InfillPromptTemplate) + } + } + private val promptTemplateHelpText = JBLabel(General.ContextHelp) fun getForm(): JPanel { - return FormBuilder.createFormBuilder() + val formBuilder = FormBuilder.createFormBuilder() .addComponent(codeCompletionsEnabledCheckBox) - .addVerticalGap(4) - .addLabeledComponent( - CodeGPTBundle.get("codeCompletionsForm.maxTokensLabel"), - codeCompletionMaxTokensField - ) + .addVerticalGap(4); + if (fimTemplate != null) { + formBuilder.addVerticalGap(4) + .addLabeledComponent( + "FIM template:", + JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { + add(promptTemplateComboBox) + add(Box.createHorizontalStrut(4)) + add(promptTemplateHelpText) + }) + } + return formBuilder.addLabeledComponent( + CodeGPTBundle.get("codeCompletionsForm.maxTokensLabel"), + codeCompletionMaxTokensField + ) .addComponentToRightColumn( ComponentPanelBuilder.createCommentComponent( CodeGPTBundle.get("codeCompletionsForm.maxTokensComment"), true, 48, true @@ -46,4 +77,20 @@ class CodeCompletionConfigurationForm(codeCompletionsEnabled: Boolean, maxTokens set(maxTokens) { codeCompletionMaxTokensField.value = maxTokens } + + var fimTemplate: InfillPromptTemplate? + get() = promptTemplateComboBox.item + set(template) { + promptTemplateComboBox.item = template + } + + private fun updatePromptTemplateHelpTooltip(template: InfillPromptTemplate) { + promptTemplateHelpText.setToolTipText(null) + + val description = StringEscapeUtils.escapeHtml4(template.buildPrompt("PREFIX", "SUFFIX")) + HelpTooltip() + .setTitle(template.toString()) + .setDescription("

$description

") + .installOn(promptTemplateHelpText) + } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt new file mode 100644 index 00000000..9743c808 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettings.kt @@ -0,0 +1,20 @@ +package ee.carlrobert.codegpt.settings.service.ollama + +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate + +@State(name = "CodeGPT_OllamaSettings_210", storages = [Storage("CodeGPT_OllamaSettings_210.xml")]) +class OllamaSettings : + SimplePersistentStateComponent(OllamaSettingsState()) + +class OllamaSettingsState : BaseState() { + var host by string("http://localhost:11434") + var model by string() + var codeCompletionsEnabled by property(true) + var codeCompletionMaxTokens by property(128) + var fimTemplate by enum(InfillPromptTemplate.CODE_LLAMA) + var availableModels by list() +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt new file mode 100644 index 00000000..e9ac10c7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt @@ -0,0 +1,163 @@ +package ee.carlrobert.codegpt.settings.service.ollama + +import com.intellij.notification.NotificationType +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.util.whenTextChangedFromUi +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.settings.service.CodeCompletionConfigurationForm +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.codegpt.ui.UIUtil +import ee.carlrobert.llm.client.ollama.OllamaClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import java.awt.BorderLayout +import java.net.ConnectException +import javax.swing.ComboBoxModel +import javax.swing.DefaultComboBoxModel +import javax.swing.JButton +import javax.swing.JPanel + +class OllamaSettingsForm { + + private val refreshModelsButton = + JButton(CodeGPTBundle.get("settingsConfigurable.service.ollama.models.refresh")) + private val hostField: JBTextField + private val modelComboBox: ComboBox + private val codeCompletionConfigurationForm: CodeCompletionConfigurationForm + + companion object { + private val logger = thisLogger() + } + + init { + val settings = service().state + codeCompletionConfigurationForm = CodeCompletionConfigurationForm( + settings.codeCompletionsEnabled, + settings.codeCompletionMaxTokens, + settings.fimTemplate + ) + val emptyModelsComboBoxModel = + DefaultComboBoxModel(arrayOf("Hit refresh to see models for this host")) + modelComboBox = ComboBox(emptyModelsComboBoxModel).apply { + isEnabled = false + } + hostField = JBTextField().apply { + text = settings.host + whenTextChangedFromUi { + modelComboBox.model = emptyModelsComboBoxModel + modelComboBox.isEnabled = false + } + } + refreshModelsButton.addActionListener { refreshModels() } + refreshModels() + } + + fun getForm(): JPanel = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator(CodeGPTBundle.get("shared.configuration"))) + .addComponent( + FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.shared.baseHost.label"), + hostField + ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.shared.model.label"), + JPanel(BorderLayout(8, 0)).apply { + add(modelComboBox, BorderLayout.CENTER) + add(refreshModelsButton, BorderLayout.EAST) + } + ) + .panel + ) + .addComponent(TitledSeparator(CodeGPTBundle.get("shared.codeCompletions"))) + .addComponent(UIUtil.withEmptyLeftBorder(codeCompletionConfigurationForm.getForm())) + .panel + + fun getModel(): String { + return if (modelComboBox.isEnabled) { + modelComboBox.item + } else { + "" + } + } + + fun resetForm() { + service().state.run { + hostField.text = host + modelComboBox.item = model + codeCompletionConfigurationForm.isCodeCompletionsEnabled = codeCompletionsEnabled + codeCompletionConfigurationForm.maxTokens = codeCompletionMaxTokens + codeCompletionConfigurationForm.fimTemplate = fimTemplate + } + } + + fun applyChanges() { + service().state.run { + host = hostField.text + model = modelComboBox.item + codeCompletionsEnabled = codeCompletionConfigurationForm.isCodeCompletionsEnabled + codeCompletionMaxTokens = codeCompletionConfigurationForm.maxTokens + fimTemplate = codeCompletionConfigurationForm.fimTemplate!! + } + } + + fun isModified() = service().state.run { + hostField.text != host + || modelComboBox.item != model + || codeCompletionConfigurationForm.isCodeCompletionsEnabled != codeCompletionsEnabled + || codeCompletionConfigurationForm.maxTokens != codeCompletionMaxTokens + || codeCompletionConfigurationForm.fimTemplate != fimTemplate + } + + private fun disableModelComboBoxWithPlaceholder(placeholderModel: ComboBoxModel) { + invokeLater { + modelComboBox.apply { + model = placeholderModel + isEnabled = false + } + } + } + + private fun refreshModels() { + disableModelComboBoxWithPlaceholder(DefaultComboBoxModel(arrayOf("Loading"))) + try { + val models = runBlocking(Dispatchers.IO) { + OllamaClient.Builder() + .setHost(hostField.text) + .build() + .modelTags + .models + .map { it.name } + } + service().state.availableModels = models.toMutableList() + invokeLater { + modelComboBox.apply { + if (models.isNotEmpty()) { + model = DefaultComboBoxModel(models.toTypedArray()) + isEnabled = true + } else { + model = DefaultComboBoxModel(arrayOf("No models")) + } + } + } + } catch (ex: RuntimeException) { + logger.error(ex) + if (ex.cause is ConnectException) { + OverlayUtil.showNotification( + "Unable to connect to Ollama server", + NotificationType.ERROR + ) + } else { + OverlayUtil.showNotification(ex.message, NotificationType.ERROR) + } + disableModelComboBoxWithPlaceholder(DefaultComboBoxModel(arrayOf("Unable to load models"))) + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7c64684a..2c883574 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -36,6 +36,7 @@ + diff --git a/src/main/resources/icons/ollama.svg b/src/main/resources/icons/ollama.svg new file mode 100644 index 00000000..87079a82 --- /dev/null +++ b/src/main/resources/icons/ollama.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/ollama_dark.svg b/src/main/resources/icons/ollama_dark.svg new file mode 100644 index 00000000..b090a840 --- /dev/null +++ b/src/main/resources/icons/ollama_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 40764742..b4ae54fd 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -116,6 +116,7 @@ settingsConfigurable.service.custom.openai.url.label=URL: settingsConfigurable.service.custom.openai.linkToDocs=Link to API docs settingsConfigurable.service.custom.openai.connectionSuccess=Connection successful. settingsConfigurable.service.custom.openai.connectionFailed=Connection failed. +settingsConfigurable.service.ollama.models.refresh=Refresh Models configurationConfigurable.section.commitMessage.title=Commit Message Template configurationConfigurable.section.commitMessage.systemPromptField.label=Prompt template: configurationConfigurable.section.inlineCompletion.title=Inline Completion @@ -173,6 +174,7 @@ service.anthropic.title=Anthropic Service service.azure.title=Azure Service service.you.title=You.com Service (Free, Cloud) service.llama.title=LLaMA C/C++ Port (Free, Local) +service.ollama.title=Ollama (Free, Local) validation.error.fieldRequired=This field is required. validation.error.invalidEmail=The email you entered is invalid. validation.error.mustBeNumber=Value must be number.