From d4690e979661f60594c002e7e0cbdd3b1654ced8 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Fri, 12 Apr 2024 18:01:21 +0300 Subject: [PATCH 01/24] fix: remove exclusion of okhttp dependency from gradle-intellij-plugin (required for publishPlugin task) --- buildSrc/build.gradle.kts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 85e1e93d..006b09c9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,9 +8,6 @@ repositories { } dependencies { - implementation(libs.gradle.intellij.plugin) { - // vulnerable transitive dependency okhttp 3.14.9 in gradle-intellij-plugin 1.17.3 - exclude(group = "com.squareup.okhttp3", module = "okhttp") - } + implementation(libs.gradle.intellij.plugin) implementation(libs.kotlin.gradle.plugin) } From 0dfaa128b7823d694a3012c740c072221574c5be Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Fri, 12 Apr 2024 18:04:51 +0300 Subject: [PATCH 02/24] 2.6.1 --- CHANGELOG.md | 5 ++++- gradle.properties | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ca8bd0..442d9c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.6.1-233] - 2024-04-12 + ### Fixed - EncodingManager error handling for invalid inputs (#444) @@ -423,7 +425,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OPENAI_API_KEY` persistence, key is saved in the OS password safe from now on -[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.0-233...HEAD +[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.1-233...HEAD +[2.6.1-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.0-233...v2.6.1-233 [2.6.0-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.1...v2.6.0-233 [2.5.1]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.0...v2.5.1 [2.5.0]: https://github.com/carlrobertoh/CodeGPT/compare/v2.4.0...v2.5.0 diff --git a/gradle.properties b/gradle.properties index 4d6d468e..47102bf5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = ee.carlrobert pluginName = CodeGPT pluginRepositoryUrl = https://github.com/carlrobertoh/CodeGPT # SemVer format -> https://semver.org -pluginVersion = 2.6.0 +pluginVersion = 2.6.1 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 233 From 5f16213bd1c18ec092d9bcc569d81c900bdcb060 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:42:42 +0200 Subject: [PATCH 03/24] fix: Use System Prompt from user configuration (#454) (#455) --- .../CompletionRequestProvider.java | 25 +++++++++-------- .../configuration/ConfigurationComponent.java | 2 +- .../configuration/ConfigurationSettings.java | 4 +++ .../chat/ui/textarea/TotalTokensDetails.java | 3 +-- .../CompletionRequestProviderTest.kt | 11 +++++--- .../DefaultCompletionRequestHandlerTest.kt | 10 ++++--- .../chat/ChatToolWindowTabPanelTest.kt | 27 +++++++++---------- 7 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index fa26cfac..23562a49 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -1,5 +1,7 @@ package ee.carlrobert.codegpt.completions; +import static ee.carlrobert.codegpt.completions.ConversationType.DEFAULT; +import static ee.carlrobert.codegpt.completions.ConversationType.FIX_COMPILE_ERRORS; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; import static ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent; import static java.lang.String.format; @@ -57,6 +59,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -74,6 +77,8 @@ public class CompletionRequestProvider { public static final String FIX_COMPILE_ERRORS_SYSTEM_PROMPT = getResourceContent( "/prompts/fix-compile-errors.txt"); + private static final Set OPENAI_SYSTEM_CONVERSATION_TYPES = Set.of( + DEFAULT, FIX_COMPILE_ERRORS); private final EncodingManager encodingManager = EncodingManager.getInstance(); private final Conversation conversation; @@ -151,10 +156,8 @@ public class CompletionRequestProvider { promptTemplate = settings.getRemoteModelPromptTemplate(); } - var systemPrompt = COMPLETION_SYSTEM_PROMPT; - if (conversationType == ConversationType.FIX_COMPILE_ERRORS) { - systemPrompt = FIX_COMPILE_ERRORS_SYSTEM_PROMPT; - } + var systemPrompt = conversationType == FIX_COMPILE_ERRORS + ? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : ConfigurationSettings.getSystemPrompt(); var prompt = promptTemplate.buildPrompt( systemPrompt, @@ -257,7 +260,7 @@ public class CompletionRequestProvider { request.setModel(settings.getModel()); request.setMaxTokens(configuration.getMaxTokens()); request.setStream(true); - request.setSystem(COMPLETION_SYSTEM_PROMPT); + request.setSystem(ConfigurationSettings.getSystemPrompt()); List messages = conversation.getMessages().stream() .filter(prevMessage -> prevMessage.getResponse() != null && !prevMessage.getResponse().isEmpty()) @@ -284,14 +287,10 @@ public class CompletionRequestProvider { private List buildMessages(CallParameters callParameters) { var message = callParameters.getMessage(); var messages = new ArrayList(); - if (callParameters.getConversationType() == ConversationType.DEFAULT) { - messages.add(new OpenAIChatCompletionStandardMessage( - "system", - ConfigurationSettings.getCurrentState().getSystemPrompt())); - } - if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { - messages.add( - new OpenAIChatCompletionStandardMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); + if (OPENAI_SYSTEM_CONVERSATION_TYPES.contains(callParameters.getConversationType())) { + String content = DEFAULT == callParameters.getConversationType() + ? ConfigurationSettings.getSystemPrompt() : FIX_COMPILE_ERRORS_SYSTEM_PROMPT; + messages.add(new OpenAIChatCompletionStandardMessage("system", content)); } for (var prevMessage : conversation.getMessages()) { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index 8c07cac7..24607c63 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -94,7 +94,7 @@ public class ConfigurationComponent { maxTokensField.setValue(configuration.getMaxTokens()); systemPromptTextArea = new JTextArea(); - if (configuration.getSystemPrompt().isEmpty()) { + if (configuration.getSystemPrompt().isBlank()) { // for backward compatibility systemPromptTextArea.setText(COMPLETION_SYSTEM_PROMPT); } else { diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.java index abcce780..3d2d7f1c 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.java @@ -31,4 +31,8 @@ public class ConfigurationSettings implements PersistentStateComponent Date: Mon, 15 Apr 2024 15:51:12 +0300 Subject: [PATCH 04/24] chore(deps): bump llm-client --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec946d45..165d019f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ jsoup = "1.17.2" jtokkit = "1.0.0" junit = "5.10.2" kotlin = "1.9.23" -llm-client = "0.7.1" +llm-client = "0.7.2" okio = "3.9.0" tree-sitter = "0.22.2" From f6a5113216aaba0c74cba7947f639aa727732b8b Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Mon, 15 Apr 2024 16:03:37 +0300 Subject: [PATCH 05/24] 2.6.2 --- CHANGELOG.md | 9 ++++++++- gradle.properties | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 442d9c27..9859d871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.6.2-233] - 2024-04-15 + +### Fixed + +- Text rendering anomalies upon streaming + ## [2.6.1-233] - 2024-04-12 ### Fixed @@ -425,7 +431,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `OPENAI_API_KEY` persistence, key is saved in the OS password safe from now on -[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.1-233...HEAD +[Unreleased]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.2-233...HEAD +[2.6.2-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.1-233...v2.6.2-233 [2.6.1-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.6.0-233...v2.6.1-233 [2.6.0-233]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.1...v2.6.0-233 [2.5.1]: https://github.com/carlrobertoh/CodeGPT/compare/v2.5.0...v2.5.1 diff --git a/gradle.properties b/gradle.properties index 47102bf5..cc4e57a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = ee.carlrobert pluginName = CodeGPT pluginRepositoryUrl = https://github.com/carlrobertoh/CodeGPT # SemVer format -> https://semver.org -pluginVersion = 2.6.1 +pluginVersion = 2.6.2 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 233 From 2221d72430d4912784b5a1bf3b065b808a70df38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Wed, 17 Apr 2024 10:41:21 +0200 Subject: [PATCH 06/24] feat: add support for placeholders in prompts (#458) * fixes #432 adds support for Placeholders in Prompts - activate gradle plugin Git4Idea - adds PlaceholderUtil - adds DATE_ISO_8601 PlaceholderReplacer - adds BRANCH_NAME PlaceholderReplacer * convert to kotlin, improve ui and add int. test * fix: do not reuse projects from previous test runs --------- Co-authored-by: Carl-Robert Linnupuu --- build.gradle.kts | 2 +- .../GenerateGitCommitMessageAction.java | 6 ++- .../CompletionRequestProvider.java | 17 +++--- .../completions/CompletionRequestService.java | 19 ++++--- .../configuration/ConfigurationComponent.java | 38 +++++++------ .../configuration/CommitMessageTemplate.kt | 41 ++++++++++++++ .../settings/configuration/Placeholder.kt | 36 +++++++++++++ .../resources/messages/codegpt.properties | 5 +- .../CommitMessageTemplateTest.kt | 30 +++++++++++ src/test/kotlin/testsupport/VcsTestCase.kt | 54 +++++++++++++++++++ 10 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt create mode 100644 src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt create mode 100644 src/test/kotlin/testsupport/VcsTestCase.kt diff --git a/build.gradle.kts b/build.gradle.kts index d875ee67..0f4d4fca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,7 +42,7 @@ intellij { pluginName.set(properties("pluginName")) version.set(properties("platformVersion")) type.set(properties("platformType")) - plugins.set(listOf("java")) + plugins.set(listOf("java", "Git4Idea")) } changelog { diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java index 1b7faa0b..79d40759 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -28,6 +28,7 @@ import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.CompletionRequestService; import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.configuration.CommitMessageTemplate; import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; import ee.carlrobert.llm.completion.CompletionEventListener; @@ -94,7 +95,10 @@ public class GenerateGitCommitMessageAction extends AnAction { if (editor != null) { ((EditorEx) editor).setCaretVisible(false); CompletionRequestService.getInstance() - .generateCommitMessageAsync(gitDiff, getEventListener(project, editor.getDocument())); + .generateCommitMessageAsync( + project.getService(CommitMessageTemplate.class).getSystemPrompt(), + gitDiff, + getEventListener(project, editor.getDocument())); } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 23562a49..a4cde93b 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -1,6 +1,5 @@ package ee.carlrobert.codegpt.completions; -import static ee.carlrobert.codegpt.completions.ConversationType.DEFAULT; import static ee.carlrobert.codegpt.completions.ConversationType.FIX_COMPILE_ERRORS; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; import static ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent; @@ -59,7 +58,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -77,8 +75,6 @@ public class CompletionRequestProvider { public static final String FIX_COMPILE_ERRORS_SYSTEM_PROMPT = getResourceContent( "/prompts/fix-compile-errors.txt"); - private static final Set OPENAI_SYSTEM_CONVERSATION_TYPES = Set.of( - DEFAULT, FIX_COMPILE_ERRORS); private final EncodingManager encodingManager = EncodingManager.getInstance(); private final Conversation conversation; @@ -157,7 +153,7 @@ public class CompletionRequestProvider { } var systemPrompt = conversationType == FIX_COMPILE_ERRORS - ? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : ConfigurationSettings.getSystemPrompt(); + ? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : ConfigurationSettings.getSystemPrompt(); var prompt = promptTemplate.buildPrompt( systemPrompt, @@ -287,10 +283,13 @@ public class CompletionRequestProvider { private List buildMessages(CallParameters callParameters) { var message = callParameters.getMessage(); var messages = new ArrayList(); - if (OPENAI_SYSTEM_CONVERSATION_TYPES.contains(callParameters.getConversationType())) { - String content = DEFAULT == callParameters.getConversationType() - ? ConfigurationSettings.getSystemPrompt() : FIX_COMPILE_ERRORS_SYSTEM_PROMPT; - messages.add(new OpenAIChatCompletionStandardMessage("system", content)); + if (callParameters.getConversationType() == ConversationType.DEFAULT) { + String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt(); + messages.add(new OpenAIChatCompletionStandardMessage("system", systemPrompt)); + } + if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { + messages.add( + new OpenAIChatCompletionStandardMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); } for (var prevMessage : conversation.getMessages()) { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index f4e3a17b..6f34310f 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -93,7 +93,6 @@ public final class CompletionRequestService { callParameters.getMessage(), callParameters.getConversationType()), eventListener); - default -> throw new IllegalArgumentException(); }; } @@ -115,13 +114,13 @@ public final class CompletionRequestService { } public void generateCommitMessageAsync( - String prompt, + String systemPrompt, + String gitDiff, CompletionEventListener eventListener) { var configuration = ConfigurationSettings.getCurrentState(); - var commitMessagePrompt = configuration.getCommitMessagePrompt(); var openaiRequest = new OpenAIChatCompletionRequest.Builder(List.of( - new OpenAIChatCompletionStandardMessage("system", commitMessagePrompt), - new OpenAIChatCompletionStandardMessage("user", prompt))) + new OpenAIChatCompletionStandardMessage("system", systemPrompt), + new OpenAIChatCompletionStandardMessage("user", gitDiff))) .setModel(OpenAISettings.getCurrentState().getModel()) .build(); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); @@ -134,18 +133,18 @@ public final class CompletionRequestService { var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); EventSources.createFactory(httpClient).newEventSource( CompletionRequestProvider.buildCustomOpenAICompletionRequest( - commitMessagePrompt, - prompt), + systemPrompt, + gitDiff), new OpenAIChatCompletionEventSourceListener(eventListener)); break; case ANTHROPIC: var anthropicSettings = AnthropicSettings.getCurrentState(); var claudeRequest = new ClaudeCompletionRequest(); - claudeRequest.setSystem(commitMessagePrompt); + claudeRequest.setSystem(systemPrompt); claudeRequest.setStream(true); claudeRequest.setMaxTokens(configuration.getMaxTokens()); claudeRequest.setModel(anthropicSettings.getModel()); - claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", prompt))); + claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", gitDiff))); CompletionClientProvider.getClaudeClient() .getCompletionAsync(claudeRequest, eventListener); break; @@ -164,7 +163,7 @@ public final class CompletionRequestService { } else { promptTemplate = settings.getRemoteModelPromptTemplate(); } - var finalPrompt = promptTemplate.buildPrompt(commitMessagePrompt, prompt, List.of()); + var finalPrompt = promptTemplate.buildPrompt(systemPrompt, gitDiff, List.of()); CompletionClientProvider.getLlamaClient().getChatCompletionAsync( new LlamaCompletionRequest.Builder(finalPrompt) .setN_predict(configuration.getMaxTokens()) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index 24607c63..0020137a 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -16,7 +16,6 @@ import com.intellij.ui.TitledSeparator; import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; -import com.intellij.ui.components.JBTextArea; import com.intellij.ui.components.JBTextField; import com.intellij.ui.components.fields.IntegerField; import com.intellij.ui.table.JBTable; @@ -29,6 +28,7 @@ import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.Dimension; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JComponent; @@ -93,7 +93,7 @@ public class ConfigurationComponent { maxTokensField.setColumns(12); maxTokensField.setValue(configuration.getMaxTokens()); - systemPromptTextArea = new JTextArea(); + systemPromptTextArea = new JTextArea(3, 60); if (configuration.getSystemPrompt().isBlank()) { // for backward compatibility systemPromptTextArea.setText(COMPLETION_SYSTEM_PROMPT); @@ -101,13 +101,12 @@ public class ConfigurationComponent { systemPromptTextArea.setText(configuration.getSystemPrompt()); } systemPromptTextArea.setLineWrap(true); + systemPromptTextArea.setWrapStyleWord(true); systemPromptTextArea.setBorder(JBUI.Borders.empty(8, 4)); - systemPromptTextArea.setColumns(60); - systemPromptTextArea.setRows(3); - commitMessagePromptTextArea = new JBTextArea(configuration.getCommitMessagePrompt(), - 3, 60); + commitMessagePromptTextArea = new JTextArea(configuration.getCommitMessagePrompt(), 3, 60); commitMessagePromptTextArea.setLineWrap(true); + commitMessagePromptTextArea.setWrapStyleWord(true); commitMessagePromptTextArea.setBorder(JBUI.Borders.empty(8, 4)); checkForPluginUpdatesCheckBox = new JBCheckBox( @@ -247,20 +246,19 @@ public class ConfigurationComponent { } private JPanel createCommitMessageConfigurationForm() { - var formBuilder = FormBuilder.createFormBuilder(); - addAssistantFormLabeledComponent( - formBuilder, - "configurationConfigurable.section.commitMessage.systemPromptField.label", - "configurationConfigurable.section.commitMessage.systemPromptField.comment", - JBUI.Panels - .simplePanel(commitMessagePromptTextArea) - .withBorder(JBUI.Borders.customLine( - JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()))); - formBuilder.addVerticalGap(8); - - var form = formBuilder.getPanel(); - form.setBorder(JBUI.Borders.emptyLeft(16)); - return form; + return FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addLabeledComponent( + new JBLabel(CodeGPTBundle.get( + "configurationConfigurable.section.commitMessage.systemPromptField.label")) + .withBorder(JBUI.Borders.emptyLeft(2)), + UI.PanelFactory.panel(commitMessagePromptTextArea) + .resizeX(false) + .withComment(CommitMessageTemplate.Companion.getHtmlDescription()) + .createPanel(), + true + ) + .getPanel(); } private ComponentValidator createTemperatureInputValidator( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt new file mode 100644 index 00000000..f074c2c9 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt @@ -0,0 +1,41 @@ +package ee.carlrobert.codegpt.settings.configuration + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.Service.Level.PROJECT +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.settings.configuration.Placeholder.BRANCH_NAME +import ee.carlrobert.codegpt.settings.configuration.Placeholder.DATE_ISO_8601 + +@Service(PROJECT) +class CommitMessageTemplate private constructor(project: Project) { + + companion object { + fun getHtmlDescription(): String { + val placeholderDescriptions = listOf(BRANCH_NAME, DATE_ISO_8601).joinToString("\n") { + "
  • ${it.name}: ${it.description}
  • " + } + + return buildString { + append("\n") + append("\n") + append("

    Template for generating commit messages. Use the following placeholders to insert dynamic values:

    \n") + append("
      $placeholderDescriptions
    \n") + append("\n") + append("") + } + } + } + + private val placeholderStrategyMapping: Map = mapOf( + BRANCH_NAME to BranchNamePlaceholderStrategy(project), + DATE_ISO_8601 to DatePlaceholderStrategy() + ) + + fun getSystemPrompt(): String = + service().state.commitMessagePrompt.let { template -> + placeholderStrategyMapping.entries.fold(template) { acc, (placeholder, strategy) -> + acc.replace("{${placeholder.name}}", strategy.getReplacementValue()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt new file mode 100644 index 00000000..9a940907 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt @@ -0,0 +1,36 @@ +package ee.carlrobert.codegpt.settings.configuration + +import com.intellij.openapi.project.Project +import git4idea.GitUtil +import git4idea.branch.GitBranchUtil +import java.time.LocalDate + +enum class Placeholder(val description: String) { + DATE_ISO_8601("Current date in ISO 8601 format, e.g. 2021-01-01."), + BRANCH_NAME("The name of the current branch") +} + +interface PlaceholderStrategy { + fun getReplacementValue(): String +} + +class DatePlaceholderStrategy : PlaceholderStrategy { + override fun getReplacementValue(): String { + return LocalDate.now().toString() + } +} + +class BranchNamePlaceholderStrategy(val project: Project) : PlaceholderStrategy { + override fun getReplacementValue(): String { + return try { + val repositories = GitUtil.getRepositoryManager(project).repositories + if (repositories.isEmpty() || repositories.size != 1) { + return "BRANCH-UNKNOWN" + } + + GitBranchUtil.getBranchNameOrRev(repositories[0]) + } catch (ignore: Exception) { + "BRANCH-UNKNOWN" + } + } +} \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index c31b2b00..a2250f20 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -113,9 +113,8 @@ 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. -configurationConfigurable.section.commitMessage.title=Commit Message -configurationConfigurable.section.commitMessage.systemPromptField.label=Prompt: -configurationConfigurable.section.commitMessage.systemPromptField.comment=Custom system prompt used for commit message generation. +configurationConfigurable.section.commitMessage.title=Commit Message Template +configurationConfigurable.section.commitMessage.systemPromptField.label=Prompt template: configurationConfigurable.section.inlineCompletion.title=Inline Completion configurationConfigurable.section.inlineCompletion.systemPromptField.label=Prompt: configurationConfigurable.section.inlineCompletion.systemPromptField.comment=Custom system prompt used for inline code generation (Fill in the Middle (FIM) template).
    The {pre}, {suf} and {mid} are replaced depending on the used Model's FIM template. diff --git a/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt new file mode 100644 index 00000000..fe57cc8a --- /dev/null +++ b/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt @@ -0,0 +1,30 @@ +package ee.carlrobert.codegpt.settings.configuration + +import com.intellij.openapi.components.service +import git4idea.commands.GitCommand +import org.assertj.core.api.Assertions.assertThat +import testsupport.VcsTestCase +import java.time.LocalDate + +class CommitMessageTemplateTest : VcsTestCase() { + + fun `test commit message system prompt construction`() { + git(GitCommand.INIT) + git(GitCommand.CHECKOUT, listOf("-b", "feature/my-cool-feature")) + registerRepository() + service().state.commitMessagePrompt = buildString { + append("Branch: {BRANCH_NAME}\n") + append("Date: {DATE_ISO_8601}") + } + + val systemPrompt = project.service().getSystemPrompt() + + assertThat(systemPrompt).isEqualTo( + buildString { + append("Branch: feature/my-cool-feature\n") + append("Date: ${LocalDate.now()}") + } + ) + } +} + diff --git a/src/test/kotlin/testsupport/VcsTestCase.kt b/src/test/kotlin/testsupport/VcsTestCase.kt new file mode 100644 index 00000000..bc3f84e5 --- /dev/null +++ b/src/test/kotlin/testsupport/VcsTestCase.kt @@ -0,0 +1,54 @@ +package testsupport + +import com.intellij.openapi.components.service +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.VcsDirectoryMapping +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.testFramework.HeavyPlatformTestCase +import git4idea.GitVcs +import git4idea.commands.Git +import git4idea.commands.GitCommand +import git4idea.commands.GitLineHandler +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert +import java.nio.file.Files +import java.nio.file.Path + +open class VcsTestCase : HeavyPlatformTestCase() { + + private lateinit var projectDir: Path + + @Throws(Exception::class) + override fun setUp() { + super.setUp() + projectDir = tempDir.createDir() + } + + fun git(command: GitCommand, parameters: List = emptyList()) { + val checkoutHandler = GitLineHandler(project, projectDir.toFile(), command) + checkoutHandler.addParameters(parameters) + service().runCommand(checkoutHandler).throwOnError() + } + + fun registerRepository(): GitRepository = + ProjectLevelVcsManager.getInstance(project).run { + directoryMappings = listOf(VcsDirectoryMapping(projectDir.toString(), GitVcs.NAME)) + Files.createDirectories(projectDir) + Assert.assertFalse( + "There are no VCS roots. Active VCSs: $allActiveVcss", + allVcsRoots.isEmpty() + ) + val file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(projectDir) + + runBlocking(Dispatchers.IO) { + val repository = project.service().getRepositoryForRoot(file) + assertThat(repository).describedAs("Couldn't find repository for root $projectDir") + .isNotNull() + repository!! + } + } +} \ No newline at end of file From 7d075f69056fe1f0444580236d0762e938857b42 Mon Sep 17 00:00:00 2001 From: Simon Svensson Date: Wed, 17 Apr 2024 11:04:40 +0200 Subject: [PATCH 07/24] Persist credentials back into the PasswordSafe (#465) --- .../codegpt/credentials/CredentialsStore.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt index 1a97fc62..284c6084 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/credentials/CredentialsStore.kt @@ -12,14 +12,23 @@ object CredentialsStore { CredentialKey.values().forEach { val credentialAttributes = CredentialAttributes(generateServiceName("CodeGPT", it.name)) val password = PasswordSafe.instance.getPassword(credentialAttributes) - setCredential(it, password) + + // Avoid calling setCredential here since it will persist + // the password back into the PasswordSafe unnecessarily. + credentialsMap[it] = password } } fun getCredential(key: CredentialKey): String? = credentialsMap[key] fun setCredential(key: CredentialKey, password: String?) { + val prevPassword = credentialsMap[key] credentialsMap[key] = password + + if (prevPassword != password) { + val credentialAttributes = CredentialAttributes(generateServiceName("CodeGPT", key.name)) + PasswordSafe.instance.setPassword(credentialAttributes, password) + } } fun isCredentialSet(key: CredentialKey): Boolean = !getCredential(key).isNullOrEmpty() From b2d9442eba7f1680f14fd6b7320d736bee562f24 Mon Sep 17 00:00:00 2001 From: Simon Svensson Date: Wed, 17 Apr 2024 11:46:21 +0200 Subject: [PATCH 08/24] fix: custom OpenAI service settings sync (#472) --- .../carlrobert/codegpt/settings/GeneralSettings.java | 3 +++ .../codegpt/settings/state/GeneralSettingsTest.kt | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java index d6ca6070..56d9ba3c 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettings.java @@ -51,6 +51,9 @@ public class GeneralSettings implements PersistentStateComponent Date: Wed, 17 Apr 2024 19:34:37 +0800 Subject: [PATCH 09/24] feat: cancel completions early on newline (#461) * Stream completion results and cancel early on newline * Rename 'suggestion, needCancel' to 'message, cancel' * Replace cancelCurrentCall() with eventSource.cancel() for simplicity * remove isStreaming variable and onComplete() method * fix: do not trigger completed callbacks during streaming --------- Co-authored-by: lichuang Co-authored-by: Carl-Robert Linnupuu --- .../CodeGPTInlineCompletionProvider.kt | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt index bb899597..91f59474 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -43,11 +43,12 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { currentCall.set( CompletionRequestService.getInstance().getCodeCompletionAsync( infillRequest, - CodeCompletionEventListener(infillRequest) { - request.editor.putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, it) + CodeCompletionEventListener { + val inlineText = it.takeWhile { message -> message != '\n' }.toString() + request.editor.putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, inlineText) launch { try { - trySend(InlineCompletionGrayTextElement(it)) + trySend(InlineCompletionGrayTextElement(inlineText)) } catch (e: Exception) { LOG.error("Failed to send inline completion suggestion", e) } @@ -74,24 +75,21 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { } class CodeCompletionEventListener( - private val requestDetails: InfillRequestDetails, - private val completed: (String) -> Unit + private val completed: (StringBuilder) -> Unit ) : CompletionEventListener { + override fun onMessage(message: String?, eventSource: EventSource?) { + if (message != null && message.contains('\n')) { + eventSource?.cancel() + } + } + override fun onComplete(messageBuilder: StringBuilder) { - // TODO: https://youtrack.jetbrains.com/issue/CPP-38312/CLion-crashes-around-every-10-minutes-of-work - /*val processedOutput = CodeCompletionParserFactory - .getParserForFileExtension(requestDetails.fileExtension) - .parse( - requestDetails.prefix, - requestDetails.suffix, - messageBuilder.toString() - )*/ - val output = - if (messageBuilder.contains("\n")) - messageBuilder.substring(0, messageBuilder.indexOf("\n")) - else messageBuilder.toString() - completed(output) + completed(messageBuilder) + } + + override fun onCancelled(messageBuilder: StringBuilder) { + completed(messageBuilder) } } } \ No newline at end of file From 92d9d5ee20bfc66e69635d44e3b3db326e7ec48e Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 17 Apr 2024 16:33:04 +0300 Subject: [PATCH 10/24] fix: file watcher disposable by making it project-level service --- .../codegpt/CodeGPTProjectActivity.kt | 14 ++++++-------- .../ee/carlrobert/codegpt/FileWatcher.kt | 18 +++++++++--------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt index 13317571..736ae2c9 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt @@ -3,9 +3,9 @@ package ee.carlrobert.codegpt import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity -import com.intellij.openapi.util.Disposer import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil import ee.carlrobert.codegpt.completions.you.YouUserManager import ee.carlrobert.codegpt.completions.you.auth.AuthenticationHandler @@ -34,14 +34,12 @@ class CodeGPTProjectActivity : ProjectActivity { if (!ApplicationManager.getApplication().isUnitTestMode && ConfigurationSettings.getCurrentState().isCheckForNewScreenshots ) { - val pathToWatch = Paths.get(System.getProperty("user.home"), "Desktop") - val fileWatcher = FileWatcher(pathToWatch) - fileWatcher.watch { - if (listOf("jpg", "jpeg", "png").contains(it.extension)) { - showImageAttachmentNotification(project, it.absolutePath) + project.service() + .watch(Paths.get(System.getProperty("user.home"), "Desktop").toFile()) { + if (listOf("jpg", "jpeg", "png").contains(it.extension)) { + showImageAttachmentNotification(project, it.absolutePath) + } } - } - Disposer.register(project, fileWatcher) } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt index d1c20669..8d7a9123 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt @@ -1,29 +1,29 @@ package ee.carlrobert.codegpt import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service import org.apache.commons.io.monitor.FileAlterationListenerAdaptor import org.apache.commons.io.monitor.FileAlterationMonitor import org.apache.commons.io.monitor.FileAlterationObserver import java.io.File -import java.nio.file.Path -class FileWatcher(private val pathToWatch: Path) : Disposable { +@Service(Service.Level.PROJECT) +class FileWatcher : Disposable { - private val fileMonitor = - FileAlterationMonitor(500, FileAlterationObserver(pathToWatch.toFile())) + private var fileMonitor: FileAlterationMonitor? = null - fun watch(onFileCreated: (File) -> Unit) { - val observer = FileAlterationObserver(pathToWatch.toFile()) + fun watch(pathToWatch: File, onFileCreated: (File) -> Unit) { + val observer = FileAlterationObserver(pathToWatch) observer.addListener(object : FileAlterationListenerAdaptor() { override fun onFileCreate(file: File) { onFileCreated(file) } }) - fileMonitor.addObserver(observer) - fileMonitor.start() + fileMonitor = FileAlterationMonitor(500, observer) + fileMonitor?.start() } override fun dispose() { - fileMonitor.stop() + fileMonitor?.stop() } } \ No newline at end of file From b202d46984568b10189dd5a2be486e2df248c3c0 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:36:49 +0200 Subject: [PATCH 11/24] fix: High CPU usage in new files check (#446) (#474) * fix: High CPU usage in new files check (#446) * Resolve absolute path --- .../configuration/ConfigurationState.java | 11 +++--- .../codegpt/CodeGPTProjectActivity.kt | 13 ++++--- .../ee/carlrobert/codegpt/FileWatcher.kt | 35 +++++++++++-------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java index dbf9bb6e..4704acec 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java @@ -123,13 +123,13 @@ public class ConfigurationState { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof ConfigurationState that)) { return false; } - ConfigurationState that = (ConfigurationState) o; return maxTokens == that.maxTokens - && Double.compare(that.temperature, temperature) == 0 + && Double.compare(temperature, that.temperature) == 0 && checkForPluginUpdates == that.checkForPluginUpdates + && checkForNewScreenshots == that.checkForNewScreenshots && createNewChatOnEachAction == that.createNewChatOnEachAction && ignoreGitCommitTokenLimit == that.ignoreGitCommitTokenLimit && methodNameGenerationEnabled == that.methodNameGenerationEnabled @@ -143,7 +143,8 @@ public class ConfigurationState { @Override public int hashCode() { return Objects.hash(systemPrompt, commitMessagePrompt, maxTokens, temperature, - checkForPluginUpdates, createNewChatOnEachAction, ignoreGitCommitTokenLimit, - methodNameGenerationEnabled, captureCompileErrors, autoFormattingEnabled, tableData); + checkForPluginUpdates, checkForNewScreenshots, createNewChatOnEachAction, + ignoreGitCommitTokenLimit, methodNameGenerationEnabled, captureCompileErrors, + autoFormattingEnabled, tableData); } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt index 736ae2c9..02f7dce2 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt @@ -19,10 +19,14 @@ import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.codegpt.settings.service.you.YouSettings import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier import ee.carlrobert.codegpt.ui.OverlayUtil +import io.ktor.util.* import java.nio.file.Paths +import kotlin.io.path.absolutePathString class CodeGPTProjectActivity : ProjectActivity { + private val watchExtensions = listOf("jpg", "jpeg", "png") + override suspend fun execute(project: Project) { EditorActionsUtil.refreshActions() CredentialsStore.loadAll() @@ -34,10 +38,11 @@ class CodeGPTProjectActivity : ProjectActivity { if (!ApplicationManager.getApplication().isUnitTestMode && ConfigurationSettings.getCurrentState().isCheckForNewScreenshots ) { + val desktopPath = Paths.get(System.getProperty("user.home"), "Desktop") project.service() - .watch(Paths.get(System.getProperty("user.home"), "Desktop").toFile()) { - if (listOf("jpg", "jpeg", "png").contains(it.extension)) { - showImageAttachmentNotification(project, it.absolutePath) + .watch(desktopPath) { + if (watchExtensions.contains(it.extension.lowercase())) { + showImageAttachmentNotification(project, desktopPath.resolve(it).absolutePathString()) } } } @@ -95,4 +100,4 @@ class CodeGPTProjectActivity : ProjectActivity { }) .notify(project) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt index 8d7a9123..a698f4e9 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt @@ -2,28 +2,33 @@ package ee.carlrobert.codegpt import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service -import org.apache.commons.io.monitor.FileAlterationListenerAdaptor -import org.apache.commons.io.monitor.FileAlterationMonitor -import org.apache.commons.io.monitor.FileAlterationObserver -import java.io.File +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE +import java.nio.file.WatchKey +import kotlin.concurrent.thread + @Service(Service.Level.PROJECT) class FileWatcher : Disposable { - private var fileMonitor: FileAlterationMonitor? = null + private var fileMonitor: Thread? = null - fun watch(pathToWatch: File, onFileCreated: (File) -> Unit) { - val observer = FileAlterationObserver(pathToWatch) - observer.addListener(object : FileAlterationListenerAdaptor() { - override fun onFileCreate(file: File) { - onFileCreated(file) + fun watch(pathToWatch: Path, onFileCreated: (Path) -> Unit) { + val watchService = FileSystems.getDefault().newWatchService() + pathToWatch.register(watchService, ENTRY_CREATE) // watch for new files + fileMonitor = thread { + var key: WatchKey + while ((watchService.take().also { key = it }) != null) { + for (event in key.pollEvents()) { + onFileCreated(event.context() as Path) + } + key.reset() } - }) - fileMonitor = FileAlterationMonitor(500, observer) - fileMonitor?.start() + } } override fun dispose() { - fileMonitor?.stop() + fileMonitor?.interrupt() } -} \ No newline at end of file +} From 29b36c52f84524736c3b47c4c2720a76a965c5aa Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:01:55 +0200 Subject: [PATCH 12/24] chore: Convert utils to Kotlin (#473) * chore: Convert utils to Kotlin * Remove nullable operators --- .../codegpt/util/ApplicationUtil.java | 43 ---- .../codegpt/util/BaseConverter.java | 38 ---- .../carlrobert/codegpt/util/EditorUtil.java | 152 ------------- .../carlrobert/codegpt/util/MapConverter.java | 11 - .../carlrobert/codegpt/util/MarkdownUtil.java | 47 ---- .../file/FileExtensionLanguageDetails.java | 23 -- .../codegpt/util/file/FileUtil.java | 195 ---------------- .../file/LanguageFileExtensionDetails.java | 34 --- .../codegpt/util/ApplicationUtil.kt | 37 +++ .../carlrobert/codegpt/util/BaseConverter.kt | 30 +++ .../ee/carlrobert/codegpt/util/EditorUtil.kt | 152 +++++++++++++ .../carlrobert/codegpt/util/MapConverter.kt | 5 + .../carlrobert/codegpt/util/MarkdownUtil.kt | 42 ++++ .../util/file/FileExtensionLanguageDetails.kt | 4 + .../carlrobert/codegpt/util/file/FileUtil.kt | 213 ++++++++++++++++++ .../util/file/LanguageFileExtensionDetails.kt | 4 + 16 files changed, 487 insertions(+), 543 deletions(-) delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/ApplicationUtil.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/BaseConverter.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/EditorUtil.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/MapConverter.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/MarkdownUtil.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.java create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/ApplicationUtil.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/BaseConverter.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/MapConverter.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/MarkdownUtil.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.kt diff --git a/src/main/java/ee/carlrobert/codegpt/util/ApplicationUtil.java b/src/main/java/ee/carlrobert/codegpt/util/ApplicationUtil.java deleted file mode 100644 index 96dfae01..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/ApplicationUtil.java +++ /dev/null @@ -1,43 +0,0 @@ -package ee.carlrobert.codegpt.util; - -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.openapi.wm.IdeFrame; -import org.jetbrains.annotations.Nullable; - -public class ApplicationUtil { - - private ApplicationUtil() { - } - - public static boolean isUnitTestingMode() { - Application app = ApplicationManager.getApplication(); - return app != null && app.isUnitTestMode(); - } - - @Nullable - public static Project findCurrentProject() { - IdeFrame frame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame(); - Project project = frame != null ? frame.getProject() : null; - if (isValidProject(project)) { - return project; - } - return findProjectFromOpenProjects(); - } - - private static Project findProjectFromOpenProjects() { - for (Project project : ProjectManager.getInstance().getOpenProjects()) { - if (isValidProject(project)) { - return project; - } - } - return null; - } - - private static boolean isValidProject(@Nullable Project project) { - return project != null && !project.isDisposed() && !project.isDefault(); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/BaseConverter.java b/src/main/java/ee/carlrobert/codegpt/util/BaseConverter.java deleted file mode 100644 index 0a4f2950..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/BaseConverter.java +++ /dev/null @@ -1,38 +0,0 @@ -package ee.carlrobert.codegpt.util; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.intellij.util.xmlb.Converter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public abstract class BaseConverter extends Converter { - - private final TypeReference typeReference; - private final ObjectMapper objectMapper = new ObjectMapper() - .registerModule(new Jdk8Module()) - .registerModule(new JavaTimeModule()); - - public BaseConverter(TypeReference typeReference) { - this.typeReference = typeReference; - } - - public @Nullable T fromString(@NotNull String value) { - try { - return objectMapper.readValue(value, typeReference); - } catch (JsonProcessingException e) { - throw new RuntimeException("Unable to deserialize conversations", e); - } - } - - public @Nullable String toString(@NotNull T value) { - try { - return objectMapper.writeValueAsString(value); - } catch (JsonProcessingException e) { - throw new RuntimeException("Unable to serialize conversations", e); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/EditorUtil.java b/src/main/java/ee/carlrobert/codegpt/util/EditorUtil.java deleted file mode 100644 index 8f2c1744..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/EditorUtil.java +++ /dev/null @@ -1,152 +0,0 @@ -package ee.carlrobert.codegpt.util; - -import static java.lang.String.format; - -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.PathManager; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorFactory; -import com.intellij.openapi.editor.EditorKind; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.fileEditor.FileEditor; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.TextEditor; -import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.testFramework.LightVirtualFile; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class EditorUtil { - - private EditorUtil() { - } - - public static Editor createEditor(@NotNull Project project, String fileExtension, String code) { - var timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); - var fileName = "temp_" + timestamp + fileExtension; - var lightVirtualFile = new LightVirtualFile( - format("%s/%s", PathManager.getTempPath(), fileName), - code); - var existingDocument = FileDocumentManager.getInstance().getDocument(lightVirtualFile); - var document = existingDocument != null - ? existingDocument - : EditorFactory.getInstance().createDocument(code); - - disableHighlighting(project, document); - - return EditorFactory.getInstance().createEditor( - document, - project, - lightVirtualFile, - true, - EditorKind.MAIN_EDITOR); - } - - public static void updateEditorDocument(Editor editor, String content) { - var document = editor.getDocument(); - var application = ApplicationManager.getApplication(); - Runnable updateDocumentRunnable = () -> application.runWriteAction(() -> - WriteCommandAction.runWriteCommandAction(editor.getProject(), () -> { - document.replaceString(0, document.getTextLength(), content); - editor.getComponent().repaint(); - editor.getComponent().revalidate(); - })); - - if (application.isUnitTestMode()) { - application.invokeAndWait(updateDocumentRunnable); - } else { - application.invokeLater(updateDocumentRunnable); - } - } - - public static boolean hasSelection(@Nullable Editor editor) { - return editor != null && editor.getSelectionModel().hasSelection(); - } - - public static @Nullable Editor getSelectedEditor(@NotNull Project project) { - FileEditorManager editorManager = FileEditorManager.getInstance(project); - return editorManager != null ? editorManager.getSelectedTextEditor() : null; - } - - public static @Nullable String getSelectedEditorSelectedText(@NotNull Project project) { - var selectedEditor = EditorUtil.getSelectedEditor(project); - if (selectedEditor != null) { - return selectedEditor.getSelectionModel().getSelectedText(); - } - return null; - } - - public static boolean isSelectedEditor(Editor editor) { - Project project = editor.getProject(); - if (project != null && !project.isDisposed()) { - FileEditorManager editorManager = FileEditorManager.getInstance(project); - if (editorManager == null) { - return false; - } - if (editorManager instanceof FileEditorManagerImpl) { - Editor current = ((FileEditorManagerImpl) editorManager).getSelectedTextEditor(true); - return current != null && current.equals(editor); - } - FileEditor current = editorManager.getSelectedEditor(); - return current instanceof TextEditor && editor.equals(((TextEditor) current).getEditor()); - } - return false; - } - - public static boolean isMainEditorTextSelected(@NotNull Project project) { - return hasSelection(getSelectedEditor(project)); - } - - public static void replaceMainEditorSelection(@NotNull Project project, @NotNull String text) { - var application = ApplicationManager.getApplication(); - application.invokeLater(() -> - application.runWriteAction(() -> WriteCommandAction.runWriteCommandAction(project, () -> { - var editor = getSelectedEditor(project); - if (editor != null) { - var selectionModel = editor.getSelectionModel(); - int startOffset = selectionModel.getSelectionStart(); - int endOffset = selectionModel.getSelectionEnd(); - var document = editor.getDocument(); - - document.replaceString(startOffset, endOffset, text); - - if (ConfigurationSettings.getCurrentState().isAutoFormattingEnabled()) { - reformatDocument(project, document, startOffset, endOffset); - } - - editor.getContentComponent().requestFocus(); - selectionModel.removeSelection(); - } - }))); - } - - public static void reformatDocument( - @NotNull Project project, - @NotNull Document document, - int startOffset, - int endOffset) { - var psiDocumentManager = PsiDocumentManager.getInstance(project); - psiDocumentManager.commitDocument(document); - var psiFile = psiDocumentManager.getPsiFile(document); - if (psiFile != null) { - CodeStyleManager.getInstance(project) - .reformatText(psiFile, startOffset, endOffset); - } - } - - public static void disableHighlighting(@NotNull Project project, Document document) { - var psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document); - if (psiFile != null) { - DaemonCodeAnalyzer.getInstance(project).setHighlightingEnabled(psiFile, false); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/MapConverter.java b/src/main/java/ee/carlrobert/codegpt/util/MapConverter.java deleted file mode 100644 index e7de065d..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/MapConverter.java +++ /dev/null @@ -1,11 +0,0 @@ -package ee.carlrobert.codegpt.util; - -import com.fasterxml.jackson.core.type.TypeReference; -import java.util.Map; - -public class MapConverter extends BaseConverter> { - - public MapConverter() { - super(new TypeReference<>() {}); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/MarkdownUtil.java b/src/main/java/ee/carlrobert/codegpt/util/MarkdownUtil.java deleted file mode 100644 index 1d7ced58..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/MarkdownUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -package ee.carlrobert.codegpt.util; - -import com.vladsch.flexmark.html.HtmlRenderer; -import com.vladsch.flexmark.parser.Parser; -import com.vladsch.flexmark.util.data.MutableDataSet; -import ee.carlrobert.codegpt.toolwindow.chat.ResponseNodeRenderer; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class MarkdownUtil { - - private MarkdownUtil() { - } - - /** - * Splits a given string into a list of strings where each element is either a code block - * surrounded by triple backticks or a non-code block text. - * - * @param inputMarkdown The input markdown formatted string to be split. - * @return A list of strings where each element is a code block or a non-code block text from the - * input string. - */ - public static List splitCodeBlocks(String inputMarkdown) { - List result = new ArrayList<>(); - Pattern pattern = Pattern.compile("(?s)```.*?```"); - Matcher matcher = pattern.matcher(inputMarkdown); - int start = 0; - while (matcher.find()) { - result.add(inputMarkdown.substring(start, matcher.start())); - result.add(matcher.group()); - start = matcher.end(); - } - result.add(inputMarkdown.substring(start)); - return result.stream().filter(item -> !item.isBlank()).toList(); - } - - public static String convertMdToHtml(String message) { - MutableDataSet options = new MutableDataSet(); - var document = Parser.builder(options).build().parse(message); - return HtmlRenderer.builder(options) - .nodeRendererFactory(new ResponseNodeRenderer.Factory()) - .build() - .render(document); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.java b/src/main/java/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.java deleted file mode 100644 index 412d0f20..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.java +++ /dev/null @@ -1,23 +0,0 @@ -package ee.carlrobert.codegpt.util.file; - -public class FileExtensionLanguageDetails { - - private String extension; - private String value; - - public String getExtension() { - return extension; - } - - public void setExtension(String extension) { - this.extension = extension; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java b/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java deleted file mode 100644 index 13df0f54..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/file/FileUtil.java +++ /dev/null @@ -1,195 +0,0 @@ -package ee.carlrobert.codegpt.util.file; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.vfs.VirtualFile; -import ee.carlrobert.codegpt.CodeGPTPlugin; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.Writer; -import java.net.URL; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.text.DecimalFormat; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.jetbrains.annotations.NotNull; - -public class FileUtil { - - private FileUtil() { - } - - private static final Logger LOG = Logger.getInstance(FileUtil.class); - - public static File createFile(String directoryPath, String fileName, String fileContent) { - try { - tryCreateDirectory(directoryPath); - return Files.writeString( - Path.of(directoryPath, fileName), - fileContent, - StandardOpenOption.CREATE).toFile(); - } catch (IOException e) { - throw new RuntimeException("Failed to create file", e); - } - } - - public static void copyFileWithProgress( - String fileName, - URL url, - long[] bytesRead, - long fileSize, - ProgressIndicator indicator) throws IOException { - FileUtil.tryCreateDirectory(CodeGPTPlugin.getLlamaModelsPath()); - - try ( - var readableByteChannel = Channels.newChannel(url.openStream()); - var fileOutputStream = new FileOutputStream( - CodeGPTPlugin.getLlamaModelsPath() + File.separator + fileName)) { - var buffer = ByteBuffer.allocateDirect(1024 * 10); - - while (readableByteChannel.read(buffer) != -1) { - if (indicator.isCanceled()) { - readableByteChannel.close(); - break; - } - buffer.flip(); - bytesRead[0] += fileOutputStream.getChannel().write(buffer); - buffer.clear(); - indicator.setFraction((double) bytesRead[0] / fileSize); - } - } - } - - public static VirtualFile getEditorFile(@NotNull Editor editor) { - return FileDocumentManager.getInstance().getFile(editor.getDocument()); - } - - public static void tryCreateDirectory(String directoryPath) { - try { - if (!com.intellij.openapi.util.io.FileUtil.exists(directoryPath)) { - if (!com.intellij.openapi.util.io.FileUtil.createDirectory( - Path.of(directoryPath).toFile())) { - throw new IOException("Failed to create directory: " + directoryPath); - } - } - } catch (IOException e) { - throw new RuntimeException("Failed to create directory", e); - } - } - - - public static String getFileExtension(String filename) { - Pattern pattern = Pattern.compile("[^.]+$"); - Matcher matcher = pattern.matcher(filename); - - if (matcher.find()) { - return matcher.group(); - } - return ""; - } - - public static Map.Entry findLanguageExtensionMapping(String language) { - var defaultValue = Map.entry("Text", ".txt"); - var mapper = new ObjectMapper(); - - List extensionToLanguageMappings; - List languageToExtensionMappings; - try { - extensionToLanguageMappings = mapper.readValue( - getResourceContent("/fileExtensionLanguageMappings.json"), new TypeReference<>() { - }); - languageToExtensionMappings = mapper.readValue( - getResourceContent("/languageFileExtensionMappings.json"), new TypeReference<>() { - }); - } catch (JsonProcessingException e) { - LOG.error("Unable to extract file extension", e); - return defaultValue; - } - - return findFirstExtension(languageToExtensionMappings, language) - .or(() -> extensionToLanguageMappings.stream() - .filter(it -> it.getExtension().equalsIgnoreCase(language)) - .findFirst() - .flatMap(it -> findFirstExtension(languageToExtensionMappings, it.getValue())) - ).orElse(defaultValue); - } - - public static boolean isUtf8File(String filePath) { - var path = Paths.get(filePath); - try (var reader = Files.newBufferedReader(path)) { - int c = reader.read(); - if (c >= 0) { - reader.transferTo(Writer.nullWriter()); - } - return true; - } catch (Exception e) { - return false; - } - } - - public static String getImageMediaType(String fileName) { - var fileExtension = getFileExtension(fileName); - return switch (fileExtension) { - case "png" -> "image/png"; - case "jpg", "jpeg" -> "image/jpeg"; - default -> throw new IllegalArgumentException("Unsupported image type: " + fileExtension); - }; - } - - public static String getResourceContent(String name) { - try (var stream = Objects.requireNonNull(FileUtil.class.getResourceAsStream(name))) { - return new String(stream.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException("Unable to read resource", e); - } - } - - public static String convertFileSize(long fileSizeInBytes) { - String[] units = {"B", "KB", "MB", "GB"}; - int unitIndex = 0; - double fileSize = fileSizeInBytes; - - while (fileSize >= 1024 && unitIndex < units.length - 1) { - fileSize /= 1024; - unitIndex++; - } - - return new DecimalFormat("#.##").format(fileSize) + " " + units[unitIndex]; - } - - public static String convertLongValue(long value) { - if (value >= 1_000_000) { - return value / 1_000_000 + "M"; - } - if (value >= 1_000) { - return value / 1_000 + "K"; - } - - return String.valueOf(value); - } - - private static Optional> findFirstExtension( - List languageFileExtensionMappings, - String language) { - return languageFileExtensionMappings.stream() - .filter(item -> language.equalsIgnoreCase(item.getName())) - .findFirst() - .map(it -> Map.entry(it.getName(), it.getExtensions().get(0))); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.java b/src/main/java/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.java deleted file mode 100644 index b5574be7..00000000 --- a/src/main/java/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.java +++ /dev/null @@ -1,34 +0,0 @@ -package ee.carlrobert.codegpt.util.file; - -import java.util.List; - -public class LanguageFileExtensionDetails { - - private String name; - private String type; - private List extensions; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public List getExtensions() { - return extensions; - } - - public void setExtensions(List extensions) { - this.extensions = extensions; - } -} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/ApplicationUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/ApplicationUtil.kt new file mode 100644 index 00000000..b024e3ff --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/ApplicationUtil.kt @@ -0,0 +1,37 @@ +package ee.carlrobert.codegpt.util + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.wm.IdeFocusManager + +object ApplicationUtil { + @JvmStatic + fun isUnitTestingMode(): Boolean { + val app = ApplicationManager.getApplication() + return app != null && app.isUnitTestMode + } + + @JvmStatic + fun findCurrentProject(): Project? { + val frame = IdeFocusManager.getGlobalInstance().lastFocusedFrame + val project = frame?.project + if (isValidProject(project)) { + return project + } + return findProjectFromOpenProjects() + } + + private fun findProjectFromOpenProjects(): Project? { + for (project in ProjectManager.getInstance().openProjects) { + if (isValidProject(project)) { + return project + } + } + return null + } + + private fun isValidProject(project: Project?): Boolean { + return project != null && !project.isDisposed && !project.isDefault + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/BaseConverter.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/BaseConverter.kt new file mode 100644 index 00000000..cec7fbfb --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/BaseConverter.kt @@ -0,0 +1,30 @@ +package ee.carlrobert.codegpt.util + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.intellij.util.xmlb.Converter + +abstract class BaseConverter protected constructor(private val typeReference: TypeReference) : Converter() { + private val objectMapper: ObjectMapper = ObjectMapper() + .registerModule(Jdk8Module()) + .registerModule(JavaTimeModule()) + + override fun fromString(value: String): T? { + try { + return objectMapper.readValue(value, typeReference) + } catch (e: JsonProcessingException) { + throw RuntimeException("Unable to deserialize conversations", e) + } + } + + override fun toString(value: T & Any): String? { + try { + return objectMapper.writeValueAsString(value) + } catch (e: JsonProcessingException) { + throw RuntimeException("Unable to serialize conversations", e) + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt new file mode 100644 index 00000000..71471f6b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt @@ -0,0 +1,152 @@ +package ee.carlrobert.codegpt.util + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.EditorKind +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.testFramework.LightVirtualFile +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +object EditorUtil { + @JvmStatic + fun createEditor(project: Project, fileExtension: String, code: String): Editor { + val timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()) + val fileName = "temp_$timestamp$fileExtension" + val lightVirtualFile = LightVirtualFile( + String.format("%s/%s", PathManager.getTempPath(), fileName), + code + ) + val existingDocument = FileDocumentManager.getInstance().getDocument(lightVirtualFile) + val document = existingDocument ?: EditorFactory.getInstance().createDocument(code) + + disableHighlighting(project, document) + + return EditorFactory.getInstance().createEditor( + document, + project, + lightVirtualFile, + true, + EditorKind.MAIN_EDITOR + ) + } + + @JvmStatic + fun updateEditorDocument(editor: Editor, content: String) { + val document = editor.document + val application = ApplicationManager.getApplication() + val updateDocumentRunnable = Runnable { + application.runWriteAction { + WriteCommandAction.runWriteCommandAction(editor.project) { + document.replaceString(0, document.textLength, content) + editor.component.repaint() + editor.component.revalidate() + } + } + } + + if (application.isUnitTestMode) { + application.invokeAndWait(updateDocumentRunnable) + } else { + application.invokeLater(updateDocumentRunnable) + } + } + + @JvmStatic + fun hasSelection(editor: Editor?): Boolean { + return editor?.selectionModel?.hasSelection() == true + } + + @JvmStatic + fun getSelectedEditor(project: Project): Editor? { + val editorManager = FileEditorManager.getInstance(project) + return editorManager?.selectedTextEditor + } + + @JvmStatic + fun getSelectedEditorSelectedText(project: Project): String? { + val selectedEditor = getSelectedEditor(project) + return selectedEditor?.selectionModel?.selectedText + } + + @JvmStatic + fun isSelectedEditor(editor: Editor): Boolean { + val project = editor.project + if (project != null && !project.isDisposed) { + val editorManager = FileEditorManager.getInstance(project) ?: return false + if (editorManager is FileEditorManagerImpl) { + return editor == editorManager.getSelectedTextEditor(true) + } + val current = editorManager.selectedEditor + return (current is TextEditor) && editor == current.editor + } + return false + } + + @JvmStatic + fun isMainEditorTextSelected(project: Project): Boolean { + return hasSelection(getSelectedEditor(project)) + } + + @JvmStatic + fun replaceMainEditorSelection(project: Project, text: String) { + val application = ApplicationManager.getApplication() + application.invokeLater { + application.runWriteAction { + WriteCommandAction.runWriteCommandAction(project) { + val editor = getSelectedEditor(project) + editor?.let { + val selectionModel = editor.selectionModel + val startOffset = selectionModel.selectionStart + val endOffset = selectionModel.selectionEnd + val document = editor.document + + document.replaceString(startOffset, endOffset, text) + + if (ConfigurationSettings.getCurrentState().isAutoFormattingEnabled) { + reformatDocument(project, document, startOffset, endOffset) + } + + editor.contentComponent.requestFocus() + selectionModel.removeSelection() + } + } + } + } + } + + @JvmStatic + fun reformatDocument( + project: Project, + document: Document, + startOffset: Int, + endOffset: Int + ) { + val psiDocumentManager = PsiDocumentManager.getInstance(project) + psiDocumentManager.commitDocument(document) + val psiFile = psiDocumentManager.getPsiFile(document) + psiFile?.let { + CodeStyleManager.getInstance(project).reformatText(psiFile, startOffset, endOffset) + } + } + + @JvmStatic + fun disableHighlighting(project: Project, document: Document) { + val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) + psiFile?.let { + DaemonCodeAnalyzer.getInstance(project).setHighlightingEnabled(psiFile, false) + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/MapConverter.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/MapConverter.kt new file mode 100644 index 00000000..52972b90 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/MapConverter.kt @@ -0,0 +1,5 @@ +package ee.carlrobert.codegpt.util + +import com.fasterxml.jackson.core.type.TypeReference + +class MapConverter : BaseConverter>(object : TypeReference>() {}) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/MarkdownUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/MarkdownUtil.kt new file mode 100644 index 00000000..e0907289 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/MarkdownUtil.kt @@ -0,0 +1,42 @@ +package ee.carlrobert.codegpt.util + +import com.vladsch.flexmark.html.HtmlRenderer +import com.vladsch.flexmark.parser.Parser +import com.vladsch.flexmark.util.data.MutableDataSet +import ee.carlrobert.codegpt.toolwindow.chat.ResponseNodeRenderer +import java.util.regex.Pattern + +object MarkdownUtil { + /** + * Splits a given string into a list of strings where each element is either a code block + * surrounded by triple backticks or a non-code block text. + * + * @param inputMarkdown The input markdown formatted string to be split. + * @return A list of strings where each element is a code block or a non-code block text from the + * input string. + */ + @JvmStatic + fun splitCodeBlocks(inputMarkdown: String): List { + val result: MutableList = ArrayList() + val pattern = Pattern.compile("(?s)```.*?```") + val matcher = pattern.matcher(inputMarkdown) + var start = 0 + while (matcher.find()) { + result.add(inputMarkdown.substring(start, matcher.start())) + result.add(matcher.group()) + start = matcher.end() + } + result.add(inputMarkdown.substring(start)) + return result.stream().filter(String::isNotBlank).toList() + } + + @JvmStatic + fun convertMdToHtml(message: String?): String { + val options = MutableDataSet() + val document = Parser.builder(options).build().parse(message!!) + return HtmlRenderer.builder(options) + .nodeRendererFactory(ResponseNodeRenderer.Factory()) + .build() + .render(document) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.kt new file mode 100644 index 00000000..06c1a5ef --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileExtensionLanguageDetails.kt @@ -0,0 +1,4 @@ +package ee.carlrobert.codegpt.util.file + +@JvmRecord +data class FileExtensionLanguageDetails(val extension: String, val value: String) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt new file mode 100644 index 00000000..b55c704b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt @@ -0,0 +1,213 @@ +package ee.carlrobert.codegpt.util.file + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.vfs.VirtualFile +import ee.carlrobert.codegpt.CodeGPTPlugin +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.Writer +import java.net.URL +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.text.DecimalFormat +import java.util.Objects +import java.util.Optional +import java.util.regex.Pattern + +object FileUtil { + private val LOG = Logger.getInstance(FileUtil::class.java) + + @JvmStatic + fun createFile(directoryPath: String, fileName: String?, fileContent: String?): File { + try { + tryCreateDirectory(directoryPath) + return Files.writeString( + Path.of(directoryPath, fileName), + fileContent, + StandardOpenOption.CREATE + ).toFile() + } catch (e: IOException) { + throw RuntimeException("Failed to create file", e) + } + } + + @JvmStatic + @Throws(IOException::class) + fun copyFileWithProgress( + fileName: String, + url: URL, + bytesRead: LongArray, + fileSize: Long, + indicator: ProgressIndicator + ) { + tryCreateDirectory(CodeGPTPlugin.getLlamaModelsPath()) + + Channels.newChannel(url.openStream()).use { readableByteChannel -> + FileOutputStream( + CodeGPTPlugin.getLlamaModelsPath() + File.separator + fileName + ).use { fileOutputStream -> + val buffer = ByteBuffer.allocateDirect(1024 * 10) + while (readableByteChannel.read(buffer) != -1) { + if (indicator.isCanceled) { + readableByteChannel.close() + break + } + buffer.flip() + bytesRead[0] += fileOutputStream.channel.write(buffer).toLong() + buffer.clear() + indicator.fraction = bytesRead[0].toDouble() / fileSize + } + } + } + } + + @JvmStatic + fun getEditorFile(editor: Editor): VirtualFile? { + return FileDocumentManager.getInstance().getFile(editor.document) + } + + private fun tryCreateDirectory(directoryPath: String) { + try { + if (!com.intellij.openapi.util.io.FileUtil.exists(directoryPath)) { + if (!com.intellij.openapi.util.io.FileUtil.createDirectory( + Path.of(directoryPath).toFile() + ) + ) { + throw IOException("Failed to create directory: $directoryPath") + } + } + } catch (e: IOException) { + throw RuntimeException("Failed to create directory", e) + } + } + + + @JvmStatic + fun getFileExtension(filename: String?): String { + val pattern = Pattern.compile("[^.]+$") + val matcher = filename?.let { pattern.matcher(it) } + + if (matcher?.find() == true) { + return matcher.group() + } + return "" + } + + @JvmStatic + fun findLanguageExtensionMapping(language: String): Map.Entry { + val defaultValue = mapOf("Text" to ".txt").entries.first() + val mapper = ObjectMapper() + + val extensionToLanguageMappings: List + val languageToExtensionMappings: List + try { + extensionToLanguageMappings = mapper.readValue( + getResourceContent("/fileExtensionLanguageMappings.json"), + object : TypeReference>() { + }) + languageToExtensionMappings = mapper.readValue( + getResourceContent("/languageFileExtensionMappings.json"), + object : TypeReference>() { + }) + } catch (e: JsonProcessingException) { + LOG.error("Unable to extract file extension", e) + return defaultValue + } + + return findFirstExtension(languageToExtensionMappings, language) + .or { + extensionToLanguageMappings.stream() + .filter { it.extension.equals(language, ignoreCase = true) } + .findFirst() + .flatMap { findFirstExtension(languageToExtensionMappings, it.value) } + }.orElse(defaultValue) + } + + fun isUtf8File(filePath: String?): Boolean { + val path = filePath?.let { Paths.get(it) } + try { + Files.newBufferedReader(path).use { reader -> + val c = reader.read() + if (c >= 0) { + reader.transferTo(Writer.nullWriter()) + } + return true + } + } catch (e: Exception) { + return false + } + } + + @JvmStatic + fun getImageMediaType(fileName: String?): String { + return when (val fileExtension = getFileExtension(fileName)) { + "png" -> "image/png" + "jpg", "jpeg" -> "image/jpeg" + else -> throw IllegalArgumentException("Unsupported image type: $fileExtension") + } + } + + @JvmStatic + fun getResourceContent(name: String?): String { + try { + Objects.requireNonNull(name?.let { FileUtil::class.java.getResourceAsStream(it) }).use { stream -> + return String(stream.readAllBytes(), StandardCharsets.UTF_8) + } + } catch (e: IOException) { + throw RuntimeException("Unable to read resource", e) + } + } + + @JvmStatic + fun convertFileSize(fileSizeInBytes: Long): String { + val units = arrayOf("B", "KB", "MB", "GB") + var unitIndex = 0 + var fileSize = fileSizeInBytes.toDouble() + + while (fileSize >= 1024 && unitIndex < units.size - 1) { + fileSize /= 1024.0 + unitIndex++ + } + + return DecimalFormat("#.##").format(fileSize) + " " + units[unitIndex] + } + + @JvmStatic + fun convertLongValue(value: Long): String { + if (value >= 1000000) { + return (value / 1000000).toString() + "M" + } + if (value >= 1000) { + return (value / 1000).toString() + "K" + } + + return value.toString() + } + + @JvmStatic + fun findFirstExtension( + languageFileExtensionMappings: List, + language: String + ): Optional> { + return languageFileExtensionMappings.stream() + .filter { language.equals(it.name, ignoreCase = true) + && it.extensions != null + && it.extensions.stream().anyMatch(String::isNotBlank) } + .findFirst() + .map { java.util.Map.entry(it.name, + it.extensions?.stream()?.filter(String::isNotBlank)?.findFirst()?.orElse("") ?: "" + ) } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.kt new file mode 100644 index 00000000..07f770b7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/file/LanguageFileExtensionDetails.kt @@ -0,0 +1,4 @@ +package ee.carlrobert.codegpt.util.file + +@JvmRecord +data class LanguageFileExtensionDetails(val name: String, val type: String, val extensions: List?) From 9666590cb1fe8f0899cb75018ce507c5a2a27165 Mon Sep 17 00:00:00 2001 From: Phil <39240633+PhilKes@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:49:04 +0200 Subject: [PATCH 13/24] feat: add include file in context to editor context menu (#475) * feat: add include file in context to editor context menu * fix: custom title for IncludeFilesInContextAction in editor context menu --- .../codegpt/actions/IncludeFilesInContextAction.java | 9 ++++----- .../codegpt/actions/editor/EditorActionsUtil.java | 3 +++ src/main/resources/messages/codegpt.properties | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java index 3735a31b..63cdc1e9 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java @@ -52,6 +52,10 @@ public class IncludeFilesInContextAction extends AnAction { super(CodeGPTBundle.get("action.includeFilesInContext.title")); } + public IncludeFilesInContextAction(String customTitleKey) { + super(CodeGPTBundle.get(customTitleKey)); + } + @Override public void actionPerformed(@NotNull AnActionEvent e) { var project = e.getProject(); @@ -93,11 +97,6 @@ public class IncludeFilesInContextAction extends AnAction { } private @Nullable FileCheckboxTree getCheckboxTree(DataContext dataContext) { - var psiElement = CommonDataKeys.PSI_ELEMENT.getData(dataContext); - if (psiElement != null) { - return new PsiElementCheckboxTree(psiElement); - } - var selectedVirtualFiles = VIRTUAL_FILE_ARRAY.getData(dataContext); if (selectedVirtualFiles != null) { return new VirtualFileCheckboxTree(selectedVirtualFiles); diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java index 78685f25..39e5f8c6 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -11,6 +11,7 @@ import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.ReferencedFile; +import ee.carlrobert.codegpt.actions.IncludeFilesInContextAction; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; @@ -75,6 +76,8 @@ public class EditorActionsUtil { }; group.add(action); }); + group.addSeparator(); + group.add(new IncludeFilesInContextAction("action.includeFileInContext.title")); } } diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index a2250f20..b8be890a 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -5,6 +5,7 @@ action.generateCommitMessage.description=Generate commit message action.generateCommitMessage.serviceWarning=Messages can only be generated with OpenAI or Azure service action.generateCommitMessage.missingCredentials=Credentials not provided action.includeFilesInContext.title=Include In Context... +action.includeFileInContext.title=Include File In Context... action.includeFilesInContext.dialog.title=Include In Context action.includeFilesInContext.dialog.description=Choose the files that you wish to include in the final prompt action.includeFilesInContext.dialog.repeatableContext.label=Repeatable context: From 67dc425a94890eb29c3278c0e309e51288d700e2 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:06:37 +0200 Subject: [PATCH 14/24] fix: Telemetry can't serialize traits anymore (#477) * fix: Telemetry can't serialize traits anymore * Add tests --- build.gradle.kts | 1 + codegpt-telemetry/build.gradle.kts | 1 + .../segment/IdentifyTraitsPersistence.java | 40 +++++------ gradle/libs.versions.toml | 2 + .../actions/IncludeFilesInContextAction.java | 2 +- .../segment/IdentifyTraitsPersistenceTest.kt | 67 +++++++++++++++++++ 6 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 src/test/kotlin/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistenceTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0f4d4fca..77ff0325 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(libs.jsoup) implementation(libs.commons.text) implementation(libs.jtokkit) + testImplementation(kotlin("test")) } tasks.register("updateSubmodules") { diff --git a/codegpt-telemetry/build.gradle.kts b/codegpt-telemetry/build.gradle.kts index 60546d9e..a808a377 100644 --- a/codegpt-telemetry/build.gradle.kts +++ b/codegpt-telemetry/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { implementation(libs.analytics) + implementation(libs.gson) } diff --git a/codegpt-telemetry/src/main/java/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistence.java b/codegpt-telemetry/src/main/java/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistence.java index ab20ce07..bca14b31 100644 --- a/codegpt-telemetry/src/main/java/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistence.java +++ b/codegpt-telemetry/src/main/java/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistence.java @@ -28,9 +28,10 @@ public class IdentifyTraitsPersistence { public static final IdentifyTraitsPersistence INSTANCE = new IdentifyTraitsPersistence(); private static final Logger LOGGER = Logger.getInstance(IdentifyTraitsPersistence.class); - private static final Path FILE = Directories.PATH.resolve("segment-identify-traits.json"); + static Path FILE = Directories.PATH.resolve("segment-identify-traits.json"); - private IdentifyTraits identifyTraits = null; + IdentifyTraits identifyTraits = null; + private Gson gson = new Gson(); protected IdentifyTraitsPersistence() {} @@ -41,36 +42,25 @@ public class IdentifyTraitsPersistence { return identifyTraits; } - public synchronized void set(IdentifyTraits identifyTraits) { + public synchronized boolean set(IdentifyTraits identifyTraits) { if (Objects.equals(identifyTraits, this.identifyTraits)) { - return; + return true; } this.identifyTraits = identifyTraits; - String string = null; - if (identifyTraits != null) { - string = serialize(identifyTraits); - } - save(string, FILE); + return save(serialize(identifyTraits), FILE); } - private String serialize(IdentifyTraits identifyTraits) { - if (identifyTraits == null) { - return null; - } - return new Gson().toJson(identifyTraits); + String serialize(IdentifyTraits identifyTraits) { + return identifyTraits == null ? null : gson.toJson(identifyTraits); } - private IdentifyTraits deserialize(String identity) { - if (identity == null) { - return null; - } - return new Gson().fromJson(identity, IdentifyTraits.class); + IdentifyTraits deserialize(String identity) { + return identity == null ? null : gson.fromJson(identity, IdentifyTraits.class); } - private String load(Path file) { - String event = null; + String load(Path file) { try(Stream lines = getLines(file)) { - event = lines + return lines .filter(l -> !l.isBlank()) .findAny() .map(String::trim) @@ -78,7 +68,7 @@ public class IdentifyTraitsPersistence { } catch (IOException e) { LOGGER.warn("Could not read identity file at " + file.toAbsolutePath(), e); } - return event; + return null; } /* for testing purposes */ @@ -86,13 +76,15 @@ public class IdentifyTraitsPersistence { return Files.lines(file); } - private void save(String event, Path file) { + boolean save(String event, Path file) { try { createFileAndParent(file); writeFile(event, file); + return true; } catch (IOException e) { LOGGER.warn("Could not write identity to file at " + FILE.toAbsolutePath(), e); } + return false; } /* for testing purposes */ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 165d019f..49c0c4c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ checkstyle = "10.15.0" commons-text = "1.11.0" flexmark = "0.64.8" gradle-intellij-plugin-version = "1.17.3" +gson = "2.10.1" jackson = "2.17.0" jsoup = "1.17.2" jtokkit = "1.0.0" @@ -21,6 +22,7 @@ assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } flexmark-all = { module = "com.vladsch.flexmark:flexmark-all", version.ref = "flexmark" } gradle-intellij-plugin = { module = "org.jetbrains.intellij.plugins:gradle-intellij-plugin", version.ref = "gradle-intellij-plugin-version" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } jtokkit = { module = "com.knuddels:jtokkit", version.ref = "jtokkit" } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java index 63cdc1e9..924d24ec 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java @@ -49,7 +49,7 @@ public class IncludeFilesInContextAction extends AnAction { private static final Logger LOG = Logger.getInstance(IncludeFilesInContextAction.class); public IncludeFilesInContextAction() { - super(CodeGPTBundle.get("action.includeFilesInContext.title")); + this("action.includeFilesInContext.title"); } public IncludeFilesInContextAction(String customTitleKey) { diff --git a/src/test/kotlin/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistenceTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistenceTest.kt new file mode 100644 index 00000000..689f784a --- /dev/null +++ b/src/test/kotlin/ee/carlrobert/codegpt/telemetry/core/service/segment/IdentifyTraitsPersistenceTest.kt @@ -0,0 +1,67 @@ +package ee.carlrobert.codegpt.telemetry.core.service.segment + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.intellij.util.io.write +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.io.path.Path +import kotlin.io.path.createTempFile +import kotlin.io.path.readText +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull + +private const val NOT_JSON = "}NOT]:JSON{" + +class IdentifyTraitsPersistenceTest { + private val gson = Gson() + private val persistence = IdentifyTraitsPersistence.INSTANCE + private val identifyTraits = IdentifyTraits("locale", "timezone", "os", "version", "distribution") + + @BeforeEach + fun setUp() { + persistence.identifyTraits = null + IdentifyTraitsPersistence.FILE = createTempFile() + } + + @Test + fun `get returns null when file does not exist`() { + IdentifyTraitsPersistence.FILE = Path(" ") + assertNull(persistence.get()) + } + + @Test + fun `get throws JsonSyntaxException when file contains malformed JSON`() { + IdentifyTraitsPersistence.FILE.write(NOT_JSON) + assertFailsWith { + persistence.get() + } + } + + @Test + fun `set saves the event to the file overwriting it`() { + IdentifyTraitsPersistence.FILE.write(NOT_JSON) + persistence.set(identifyTraits) + assertEquals(IdentifyTraitsPersistence.FILE.readText(), gson.toJson(identifyTraits)) + } + + @Test + fun `set saves the event to the file when file does not exist`() { + persistence.set(identifyTraits) + assertEquals(IdentifyTraitsPersistence.FILE.readText(), gson.toJson(identifyTraits)) + } + + @Test + fun `get returns the deserialized event`() { + IdentifyTraitsPersistence.FILE.write(gson.toJson(identifyTraits)) + assertEquals(identifyTraits, persistence.get()) + } + + @Test + fun `set throws IOException when file cannot be written and returns false`() { + IdentifyTraitsPersistence.FILE = IdentifyTraitsPersistence.FILE.resolve(" xyz ") + assertEquals(persistence.set(identifyTraits), false) + } + +} From c8181a62e48be234388a74665ca0b56d23fbc7cd Mon Sep 17 00:00:00 2001 From: Phil <39240633+PhilKes@users.noreply.github.com> Date: Sat, 20 Apr 2024 22:18:43 +0200 Subject: [PATCH 15/24] feat: add input field for llama server build parameters and improve error handling (#481) --- build.gradle.kts | 1 + .../completions/llama/LlamaServerAgent.java | 86 ++++++++++++++----- .../llama/LlamaServerStartupParams.java | 3 +- .../service/llama/LlamaSettingsState.java | 14 ++- .../form/LlamaServerPreferencesForm.java | 50 ++++++++--- .../service/llama/form/LlamaSettingsForm.java | 1 + .../llama/form/ServerProgressPanel.java | 19 ++-- .../ee/carlrobert/codegpt/ui/OverlayUtil.java | 9 ++ .../resources/messages/codegpt.properties | 4 +- 9 files changed, 138 insertions(+), 49 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 77ff0325..e8105050 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -149,6 +149,7 @@ tasks { runIde { enabled = true environment("ENVIRONMENT", "LOCAL") + autoReloadPlugins.set(false) // is triggered when building llama server } test { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java index 6e3ac219..a837150a 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java @@ -14,14 +14,17 @@ import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.util.Key; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.form.ServerProgressPanel; +import ee.carlrobert.codegpt.ui.OverlayUtil; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,65 +35,94 @@ public final class LlamaServerAgent implements Disposable { private @Nullable OSProcessHandler makeProcessHandler; private @Nullable OSProcessHandler startServerProcessHandler; + private ServerProgressPanel activeServerProgressPanel; + private boolean stoppedByUser; public void startAgent( LlamaServerStartupParams params, ServerProgressPanel serverProgressPanel, Runnable onSuccess, - Runnable onServerTerminated) { + Consumer onServerTerminated) { + this.activeServerProgressPanel = serverProgressPanel; ApplicationManager.getApplication().invokeLater(() -> { try { - serverProgressPanel.updateText( + stoppedByUser = false; + serverProgressPanel.displayText( CodeGPTBundle.get("llamaServerAgent.buildingProject.description")); - makeProcessHandler = new OSProcessHandler(getMakeCommandLinde()); + makeProcessHandler = new OSProcessHandler( + getMakeCommandLine(params.additionalBuildParameters())); makeProcessHandler.addProcessListener( - getMakeProcessListener(params, serverProgressPanel, onSuccess, onServerTerminated)); + getMakeProcessListener(params, onSuccess, onServerTerminated)); makeProcessHandler.startNotify(); } catch (ExecutionException e) { - throw new RuntimeException(e); + showServerError(e.getMessage(), onServerTerminated); } }); } public void stopAgent() { + stoppedByUser = true; + if (makeProcessHandler != null) { + makeProcessHandler.destroyProcess(); + } if (startServerProcessHandler != null) { startServerProcessHandler.destroyProcess(); } } public boolean isServerRunning() { - return startServerProcessHandler != null + return (makeProcessHandler != null + && makeProcessHandler.isStartNotified() + && !makeProcessHandler.isProcessTerminated()) + || (startServerProcessHandler != null && startServerProcessHandler.isStartNotified() - && !startServerProcessHandler.isProcessTerminated(); + && !startServerProcessHandler.isProcessTerminated()); } private ProcessListener getMakeProcessListener( LlamaServerStartupParams params, - ServerProgressPanel serverProgressPanel, Runnable onSuccess, - Runnable onServerTerminated) { + Consumer onServerTerminated) { LOG.info("Building llama project"); return new ProcessAdapter() { + + private final List errorLines = new CopyOnWriteArrayList<>(); + @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + if (ProcessOutputType.isStderr(outputType)) { + errorLines.add(event.getText()); + return; + } LOG.info(event.getText()); } @Override public void processTerminated(@NotNull ProcessEvent event) { + int exitCode = event.getExitCode(); + LOG.info(format("Server build exited with code %d", exitCode)); + if (stoppedByUser) { + onServerTerminated.accept(activeServerProgressPanel); + return; + } + if (exitCode != 0) { + showServerError(String.join(",", errorLines), onServerTerminated); + return; + } + try { LOG.info("Booting up llama server"); - serverProgressPanel.updateText( + activeServerProgressPanel.displayText( CodeGPTBundle.get("llamaServerAgent.serverBootup.description")); startServerProcessHandler = new OSProcessHandler.Silent(getServerCommandLine(params)); startServerProcessHandler.addProcessListener( - getProcessListener(params.port(), onSuccess, onServerTerminated)); + getProcessListener(params.port(), onSuccess, + onServerTerminated)); startServerProcessHandler.startNotify(); } catch (ExecutionException ex) { - LOG.error("Unable to start llama server", ex); - throw new RuntimeException(ex); + showServerError(ex.getMessage(), onServerTerminated); } } }; @@ -99,27 +131,25 @@ public final class LlamaServerAgent implements Disposable { private ProcessListener getProcessListener( int port, Runnable onSuccess, - Runnable onServerTerminated) { + Consumer onServerTerminated) { return new ProcessAdapter() { private final ObjectMapper objectMapper = new ObjectMapper(); private final List errorLines = new CopyOnWriteArrayList<>(); @Override public void processTerminated(@NotNull ProcessEvent event) { - if (errorLines.isEmpty()) { - LOG.info(format("Server terminated with code %d", event.getExitCode())); + LOG.info(format("Server terminated with code %d", event.getExitCode())); + if (stoppedByUser) { + onServerTerminated.accept(activeServerProgressPanel); } else { - LOG.info(String.join("", errorLines)); + showServerError(String.join(",", errorLines), onServerTerminated); } - - onServerTerminated.run(); } @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { if (ProcessOutputType.isStderr(outputType)) { errorLines.add(event.getText()); - return; } if (ProcessOutputType.isStdout(outputType)) { @@ -141,11 +171,18 @@ public final class LlamaServerAgent implements Disposable { }; } - private static GeneralCommandLine getMakeCommandLinde() { + private void showServerError(String errorText, Consumer onServerTerminated) { + onServerTerminated.accept(activeServerProgressPanel); + LOG.info("Unable to start llama server:\n" + errorText); + OverlayUtil.showClosableBalloon(errorText, MessageType.ERROR, activeServerProgressPanel); + } + + private static GeneralCommandLine getMakeCommandLine(List additionalCompileParameters) { GeneralCommandLine commandLine = new GeneralCommandLine().withCharset(StandardCharsets.UTF_8); commandLine.setExePath("make"); commandLine.withWorkDirectory(CodeGPTPlugin.getLlamaSourcePath()); commandLine.addParameters("-j"); + commandLine.addParameters(additionalCompileParameters); commandLine.setRedirectErrorStream(false); return commandLine; } @@ -159,11 +196,16 @@ public final class LlamaServerAgent implements Disposable { "-c", String.valueOf(params.contextLength()), "--port", String.valueOf(params.port()), "-t", String.valueOf(params.threads())); - commandLine.addParameters(params.additionalParameters()); + commandLine.addParameters(params.additionalRunParameters()); commandLine.setRedirectErrorStream(false); return commandLine; } + public void setActiveServerProgressPanel( + ServerProgressPanel activeServerProgressPanel) { + this.activeServerProgressPanel = activeServerProgressPanel; + } + @Override public void dispose() { if (makeProcessHandler != null && !makeProcessHandler.isProcessTerminated()) { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerStartupParams.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerStartupParams.java index 7b259a1f..d15c274f 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerStartupParams.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerStartupParams.java @@ -3,5 +3,6 @@ package ee.carlrobert.codegpt.completions.llama; import java.util.List; public record LlamaServerStartupParams(String modelPath, int contextLength, int threads, int port, - List additionalParameters) { + List additionalRunParameters, + List additionalBuildParameters) { } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java index cb81c722..fc3ca47b 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/LlamaSettingsState.java @@ -23,6 +23,7 @@ public class LlamaSettingsState { private int contextSize = 2048; private int threads = 8; private String additionalParameters = ""; + private String additionalBuildParameters = ""; private int topK = 40; private double topP = 0.9; private double minP = 0.05; @@ -138,6 +139,14 @@ public class LlamaSettingsState { this.additionalParameters = additionalParameters; } + public String getAdditionalBuildParameters() { + return additionalBuildParameters; + } + + public void setAdditionalBuildParameters(String additionalBuildParameters) { + this.additionalBuildParameters = additionalBuildParameters; + } + public int getTopK() { return topK; } @@ -220,6 +229,7 @@ public class LlamaSettingsState { && Objects.equals(baseHost, that.baseHost) && Objects.equals(serverPort, that.serverPort) && Objects.equals(additionalParameters, that.additionalParameters) + && Objects.equals(additionalBuildParameters, that.additionalBuildParameters) && codeCompletionsEnabled == that.codeCompletionsEnabled && codeCompletionMaxTokens == that.codeCompletionMaxTokens; } @@ -229,7 +239,7 @@ public class LlamaSettingsState { return Objects.hash(runLocalServer, useCustomModel, customLlamaModelPath, huggingFaceModel, localModelPromptTemplate, remoteModelPromptTemplate, localModelInfillPromptTemplate, remoteModelInfillPromptTemplate, baseHost, serverPort, contextSize, threads, - additionalParameters, topK, topP, minP, repeatPenalty, codeCompletionsEnabled, - codeCompletionMaxTokens); + additionalParameters, additionalBuildParameters, topK, topP, minP, repeatPenalty, + codeCompletionsEnabled, codeCompletionMaxTokens); } } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaServerPreferencesForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaServerPreferencesForm.java index 1935c74e..cbd2bae3 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaServerPreferencesForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/LlamaServerPreferencesForm.java @@ -57,6 +57,7 @@ public class LlamaServerPreferencesForm { private final IntegerField maxTokensField; private final IntegerField threadsField; private final JBTextField additionalParametersField; + private final JBTextField additionalBuildParametersField; private final ChatPromptTemplatePanel remotePromptTemplatePanel; private final InfillPromptTemplatePanel infillPromptTemplatePanel; @@ -79,6 +80,9 @@ public class LlamaServerPreferencesForm { additionalParametersField = new JBTextField(settings.getAdditionalParameters(), 30); additionalParametersField.setEnabled(!serverRunning); + additionalBuildParametersField = new JBTextField(settings.getAdditionalBuildParameters(), 30); + additionalBuildParametersField.setEnabled(!serverRunning); + baseHostField = new JBTextField(settings.getBaseHost(), 30); apiKeyField = new JBPasswordField(); apiKeyField.setColumns(30); @@ -124,6 +128,7 @@ public class LlamaServerPreferencesForm { maxTokensField.setValue(state.getContextSize()); threadsField.setValue(state.getThreads()); additionalParametersField.setText(state.getAdditionalParameters()); + additionalBuildParametersField.setText(state.getAdditionalBuildParameters()); remotePromptTemplatePanel.setPromptTemplate(state.getRemoteModelPromptTemplate()); // ? infillPromptTemplatePanel.setPromptTemplate(state.getRemoteModelInfillPromptTemplate()); apiKeyField.setText(CredentialsStore.INSTANCE.getCredential(LLAMA_API_KEY)); @@ -184,9 +189,17 @@ public class LlamaServerPreferencesForm { createComment("settingsConfigurable.service.llama.threads.comment")) .addLabeledComponent( CodeGPTBundle.get("settingsConfigurable.service.llama.additionalParameters.label"), - additionalParametersField) - .addComponentToRightColumn( - createComment("settingsConfigurable.service.llama.additionalParameters.comment")) + additionalParametersField) + .addComponentToRightColumn( + createComment( + "settingsConfigurable.service.llama.additionalParameters.comment")) + .addLabeledComponent( + CodeGPTBundle.get( + "settingsConfigurable.service.llama.additionalBuildParameters.label"), + additionalBuildParametersField) + .addComponentToRightColumn( + createComment( + "settingsConfigurable.service.llama.additionalBuildParameters.comment")) .addVerticalGap(4) .addComponentFillVertically(new JPanel(), 0) .getPanel())) @@ -196,6 +209,7 @@ public class LlamaServerPreferencesForm { private JButton getServerButton( LlamaServerAgent llamaServerAgent, ServerProgressPanel serverProgressPanel) { + llamaServerAgent.setActiveServerProgressPanel(serverProgressPanel); var serverRunning = llamaServerAgent.isServerRunning(); var serverButton = new JButton(); serverButton.setText(serverRunning @@ -218,7 +232,9 @@ public class LlamaServerPreferencesForm { getContextSize(), getThreads(), getServerPort(), - getListOfAdditionalParameters()), + getListOfAdditionalParameters(), + getListOfAdditionalBuildParameters() + ), serverProgressPanel, () -> { setFormEnabled(false); @@ -227,12 +243,12 @@ public class LlamaServerPreferencesForm { Actions.Checked, SwingConstants.LEADING)); }, - () -> { + (activeServerProgressPanel) -> { setFormEnabled(true); serverButton.setText( CodeGPTBundle.get("settingsConfigurable.service.llama.startServer.label")); serverButton.setIcon(Actions.Execute); - serverProgressPanel.displayComponent(new JBLabel( + activeServerProgressPanel.displayComponent(new JBLabel( CodeGPTBundle.get("settingsConfigurable.service.llama.progress.serverTerminated"), Actions.Cancel, SwingConstants.LEADING)); @@ -282,7 +298,7 @@ public class LlamaServerPreferencesForm { serverButton.setText( CodeGPTBundle.get("settingsConfigurable.service.llama.startServer.label")); serverButton.setIcon(Actions.Execute); - progressPanel.updateText( + progressPanel.displayText( CodeGPTBundle.get("settingsConfigurable.service.llama.progress.stoppingServer")); } @@ -291,7 +307,7 @@ public class LlamaServerPreferencesForm { serverButton.setText( CodeGPTBundle.get("settingsConfigurable.service.llama.stopServer.label")); serverButton.setIcon(Actions.Suspend); - progressPanel.startProgress( + progressPanel.displayText( CodeGPTBundle.get("settingsConfigurable.service.llama.progress.startingServer")); } @@ -301,6 +317,7 @@ public class LlamaServerPreferencesForm { maxTokensField.setEnabled(enabled); threadsField.setEnabled(enabled); additionalParametersField.setEnabled(enabled); + additionalBuildParametersField.setEnabled(enabled); } public boolean isRunLocalServer() { @@ -337,9 +354,20 @@ public class LlamaServerPreferencesForm { public List getListOfAdditionalParameters() { return Arrays.stream(additionalParametersField.getText().split(",")) - .map(String::trim) - .filter(s -> !s.isBlank()) - .toList(); + .map(String::trim) + .filter(s -> !s.isBlank()) + .toList(); + } + + public String getAdditionalBuildParameters() { + return additionalBuildParametersField.getText(); + } + + public List getListOfAdditionalBuildParameters() { + return Arrays.stream(additionalBuildParametersField.getText().split(",")) + .map(String::trim) + .filter(s -> !s.isBlank()) + .toList(); } public PromptTemplate getPromptTemplate() { 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 7147eceb..d5260b24 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 @@ -41,6 +41,7 @@ public class LlamaSettingsForm extends JPanel { state.setContextSize(llamaServerPreferencesForm.getContextSize()); state.setThreads(llamaServerPreferencesForm.getThreads()); state.setAdditionalParameters(llamaServerPreferencesForm.getAdditionalParameters()); + state.setAdditionalBuildParameters(llamaServerPreferencesForm.getAdditionalBuildParameters()); var modelPreferencesForm = llamaServerPreferencesForm.getLlamaModelPreferencesForm(); state.setCustomLlamaModelPath(modelPreferencesForm.getCustomLlamaModelPath()); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/ServerProgressPanel.java b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/ServerProgressPanel.java index 9a755003..4d348865 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/ServerProgressPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/llama/form/ServerProgressPanel.java @@ -8,20 +8,15 @@ import javax.swing.JPanel; public class ServerProgressPanel extends JPanel { private final JBLabel label = new JBLabel(); + private final AsyncProcessIcon loadingSpinner = new AsyncProcessIcon("sign_in_spinner"); - public ServerProgressPanel() { - setVisible(false); - add(new AsyncProcessIcon("sign_in_spinner")); - add(label); - } - - public void startProgress(String text) { - setVisible(true); - updateText(text); - } - - public void updateText(String text) { + public void displayText(String text) { label.setText(text); + removeAll(); + add(loadingSpinner); + add(label); + revalidate(); + repaint(); } public void displayComponent(JComponent component) { diff --git a/src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java b/src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java index bc16cbb0..394da503 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java @@ -149,4 +149,13 @@ public class OverlayUtil { .createBalloon() .show(RelativePoint.getSouthOf(component), Position.below); } + + public static void showClosableBalloon(String content, MessageType messageType, + JComponent component) { + JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(content, messageType, null) + .setCloseButtonEnabled(true) + .createBalloon() + .show(RelativePoint.getSouthOf(component), Position.below); + } } diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index b8be890a..2b0aa0fe 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -63,6 +63,8 @@ settingsConfigurable.service.llama.threads.label=Threads: settingsConfigurable.service.llama.threads.comment=The number of threads available to execute the model. It is not recommended to specify a number greater than the number of processor cores. settingsConfigurable.service.llama.additionalParameters.label=Additional parameters: settingsConfigurable.service.llama.additionalParameters.comment=Additional command-line parameters for the server startup process, separated by commas. See the full list of options.

    Example: "--n-gpu-layers, 1, --no-mmap, --mlock"

    +settingsConfigurable.service.llama.additionalBuildParameters.label=Additional build parameters: +settingsConfigurable.service.llama.additionalBuildParameters.comment=Additional command-line parameters for the server build process, separated by commas. See the full list of build options.

    Example: "LLAMA_CUBLAS=1,CUDA_DOCKER_ARCH=all"

    settingsConfigurable.service.llama.baseHost.label=Base host: settingsConfigurable.service.llama.baseHost.comment=URL to existing LLama server settingsConfigurable.service.llama.startServer.label=Start server @@ -179,7 +181,7 @@ validation.error.mustBeGreaterThanZero=Value must be greater than 0 checkForUpdatesTask.title=Checking for CodeGPT update... checkForUpdatesTask.notification.message=An update for CodeGPT is available. checkForUpdatesTask.notification.installButton=Install update -llamaServerAgent.buildingProject.description=Building llama.cpp... +llamaServerAgent.buildingProject.description=Building server... llamaServerAgent.serverBootup.description=Booting up server... notification.compilationError.description=CodeGPT has detected a compilation error. Would you like assistance in resolving it? notification.compilationError.okLabel=Resolve errors From 14f325491340b3ec3b8b3e8b9213877b1446bf56 Mon Sep 17 00:00:00 2001 From: Simon Svensson Date: Sat, 20 Apr 2024 22:23:08 +0200 Subject: [PATCH 16/24] feat: code completion for "Custom OpenAI Service" (#476) * Add code completion setting states for custom service * Add settings for code completion in Custom OpenAI service * Move code completion section to the bottom * Create test testFetchCodeCompletionCustomService * Add Custom OpenAI to the "Enable/Disable Completion" actions * New configuration UI separating /v1/chat/completions from /v1/completions * Code completion for Custom Service * Formatting fixes * Move prefix and suffix to templates in body * Message updates * New tabbed UI for Chat and Code Completions * convert to kotlin, improve ui and other minor changes * fix test connection for chat completions * add help tooltips * allow backward compatibility * support prefix and suffix placeholders * fix initial state loading --------- Co-authored-by: Jack Boswell (boswelja) Co-authored-by: Carl-Robert Linnupuu --- README.md | 2 +- .../CompletionRequestProvider.java | 31 ++- .../completions/CompletionRequestService.java | 23 ++- .../settings/GeneralSettingsComponent.java | 78 ++++++-- .../settings/GeneralSettingsConfigurable.java | 34 ++-- .../service/ServiceSelectionForm.java | 68 ------- .../service/custom/CustomServiceForm.java | 175 ----------------- .../custom/CustomServiceFormTabbedPane.java | 11 +- .../service/custom/CustomServiceSettings.java | 45 ----- .../custom/CustomServiceSettingsState.java | 69 ------- .../service/custom/CustomServiceTemplate.java | 158 --------------- .../chat/ui/textarea/ModelComboBoxAction.java | 13 +- .../CodeCompletionFeatureToggleActions.kt | 14 +- .../CodeCompletionRequestFactory.kt | 56 ++++++ .../CodeGPTInlineCompletionProvider.kt | 22 ++- .../codecompletions/InfillRequestDetails.kt | 10 +- .../settings/configuration/Placeholder.kt | 5 +- .../custom/CustomServiceChatCompletionForm.kt | 101 ++++++++++ .../CustomServiceChatCompletionTemplate.kt | 124 ++++++++++++ .../custom/CustomServiceCodeCompletionForm.kt | 182 ++++++++++++++++++ .../CustomServiceCodeCompletionTemplate.kt | 73 +++++++ .../service/custom/CustomServiceForm.kt | 148 ++++++++++++++ .../service/custom/CustomServiceSettings.kt | 74 +++++++ .../service/custom/CustomServiceTemplate.kt | 69 +++++++ src/main/resources/META-INF/plugin.xml | 5 +- .../resources/messages/codegpt.properties | 3 +- 26 files changed, 1001 insertions(+), 592 deletions(-) delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java delete mode 100644 src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt diff --git a/README.md b/README.md index a9635aa0..033bc485 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Receive single-line or whole-function autocomplete suggestions as you type. ![Code Completions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/new/inline-completion.png?raw=true) -> **Note**: Currently supported only on GPT-3.5 and locally-hosted models. +> **Note**: Currently only supported with OpenAI, Custom OpenAI, or LLaMA. ### Chat (with Vision) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index a4cde93b..160d7189 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -4,6 +4,7 @@ import static ee.carlrobert.codegpt.completions.ConversationType.FIX_COMPILE_ERR import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; import static ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent; import static java.lang.String.format; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -23,8 +24,9 @@ import ee.carlrobert.codegpt.settings.IncludedFilesSettings; import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; 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.CustomServiceSettingsState; +import ee.carlrobert.codegpt.settings.service.custom.CustomServiceState; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.codegpt.settings.service.you.YouSettings; @@ -114,16 +116,27 @@ public class CompletionRequestProvider { public static Request buildCustomOpenAICompletionRequest(String system, String context) { return buildCustomOpenAIChatCompletionRequest( - CustomServiceSettings.getCurrentState(), + ApplicationManager.getApplication().getService(CustomServiceState.class) + .getChatCompletionSettings(), List.of( new OpenAIChatCompletionStandardMessage("system", system), new OpenAIChatCompletionStandardMessage("user", context)), true); } + public static Request buildCustomOpenAICompletionRequest(String input) { + return buildCustomOpenAIChatCompletionRequest( + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getChatCompletionSettings(), + List.of(new OpenAIChatCompletionStandardMessage("user", input)), + true); + } + public static Request buildCustomOpenAILookupCompletionRequest(String context) { return buildCustomOpenAIChatCompletionRequest( - CustomServiceSettings.getCurrentState(), + ApplicationManager.getApplication().getService(CustomServiceState.class) + .getChatCompletionSettings(), List.of( new OpenAIChatCompletionStandardMessage( "system", @@ -199,21 +212,21 @@ public class CompletionRequestProvider { } public Request buildCustomOpenAIChatCompletionRequest( - CustomServiceSettingsState customConfiguration, + CustomServiceChatCompletionSettingsState settings, CallParameters callParameters) { return buildCustomOpenAIChatCompletionRequest( - customConfiguration, + settings, buildMessages(callParameters), true); } private static Request buildCustomOpenAIChatCompletionRequest( - CustomServiceSettingsState customConfiguration, + CustomServiceChatCompletionSettingsState settings, List messages, boolean streamRequest) { - var requestBuilder = new Request.Builder().url(customConfiguration.getUrl().trim()); + var requestBuilder = new Request.Builder().url(requireNonNull(settings.getUrl()).trim()); var credential = CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY); - for (var entry : customConfiguration.getHeaders().entrySet()) { + for (var entry : settings.getHeaders().entrySet()) { String value = entry.getValue(); if (credential != null && value.contains("$CUSTOM_SERVICE_API_KEY")) { value = value.replace("$CUSTOM_SERVICE_API_KEY", credential); @@ -221,7 +234,7 @@ public class CompletionRequestProvider { requestBuilder.addHeader(entry.getKey(), value); } - var body = customConfiguration.getBody().entrySet().stream() + var body = settings.getBody().entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index 6f34310f..edacf655 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -29,6 +29,7 @@ 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.openai.completion.OpenAIChatCompletionEventSourceListener; +import ee.carlrobert.llm.client.openai.completion.OpenAITextCompletionEventSourceListener; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage; import ee.carlrobert.llm.client.openai.completion.response.OpenAIChatCompletionResponse; @@ -55,6 +56,15 @@ public final class CompletionRequestService { return ApplicationManager.getApplication().getService(CompletionRequestService.class); } + public EventSource getCustomOpenAICompletionAsync( + Request customRequest, + CompletionEventListener eventListener) { + var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); + return EventSources.createFactory(httpClient).newEventSource( + customRequest, + new OpenAITextCompletionEventSourceListener(eventListener)); + } + public EventSource getCustomOpenAIChatCompletionAsync( Request customRequest, CompletionEventListener eventListener) { @@ -76,7 +86,10 @@ public final class CompletionRequestService { eventListener); case CUSTOM_OPENAI -> getCustomOpenAIChatCompletionAsync( requestProvider.buildCustomOpenAIChatCompletionRequest( - CustomServiceSettings.getCurrentState(), + ApplicationManager.getApplication() + .getService(CustomServiceSettings.class) + .getState() + .getChatCompletionSettings(), callParameters), eventListener); case ANTHROPIC -> CompletionClientProvider.getClaudeClient().getCompletionAsync( @@ -99,14 +112,18 @@ public final class CompletionRequestService { public EventSource getCodeCompletionAsync( InfillRequestDetails requestDetails, CompletionEventListener eventListener) { + var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); return switch (GeneralSettings.getCurrentState().getSelectedService()) { case OPENAI -> CompletionClientProvider.getOpenAIClient() .getCompletionAsync( - CodeCompletionRequestFactory.INSTANCE.buildOpenAIRequest(requestDetails), + CodeCompletionRequestFactory.buildOpenAIRequest(requestDetails), eventListener); + case CUSTOM_OPENAI -> EventSources.createFactory(httpClient).newEventSource( + CodeCompletionRequestFactory.buildCustomRequest(requestDetails), + new OpenAITextCompletionEventSourceListener(eventListener)); case LLAMA_CPP -> CompletionClientProvider.getLlamaClient() .getChatCompletionAsync( - CodeCompletionRequestFactory.INSTANCE.buildLlamaRequest(requestDetails), + CodeCompletionRequestFactory.buildLlamaRequest(requestDetails), eventListener); default -> throw new IllegalArgumentException("Code completion not supported for selected service"); diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java index 74377c2e..d5fea919 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsComponent.java @@ -12,8 +12,18 @@ import com.intellij.openapi.ui.ComboBox; import com.intellij.ui.components.JBTextField; import com.intellij.util.ui.FormBuilder; import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.settings.service.ServiceSelectionForm; import ee.carlrobert.codegpt.settings.service.ServiceType; +import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; +import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettingsForm; +import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; +import ee.carlrobert.codegpt.settings.service.azure.AzureSettingsForm; +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.openai.OpenAISettings; +import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; +import ee.carlrobert.codegpt.settings.service.you.YouSettings; +import ee.carlrobert.codegpt.settings.service.you.YouSettingsForm; import java.awt.CardLayout; import java.awt.Component; import java.awt.Container; @@ -29,21 +39,30 @@ public class GeneralSettingsComponent { private final JPanel mainPanel; private final JBTextField displayNameField; private final ComboBox serviceComboBox; - private final ServiceSelectionForm serviceSelectionForm; + private final OpenAISettingsForm openAISettingsForm; + private final CustomServiceForm customConfigurationSettingsForm; + private final AnthropicSettingsForm anthropicSettingsForm; + private final AzureSettingsForm azureSettingsForm; + private final YouSettingsForm youSettingsForm; + private final LlamaSettingsForm llamaSettingsForm; public GeneralSettingsComponent(Disposable parentDisposable, GeneralSettings settings) { displayNameField = new JBTextField(settings.getState().getDisplayName(), 20); - serviceSelectionForm = new ServiceSelectionForm(parentDisposable); + openAISettingsForm = new OpenAISettingsForm(OpenAISettings.getCurrentState()); + customConfigurationSettingsForm = new CustomServiceForm(); + anthropicSettingsForm = new AnthropicSettingsForm(AnthropicSettings.getCurrentState()); + azureSettingsForm = new AzureSettingsForm(AzureSettings.getCurrentState()); + youSettingsForm = new YouSettingsForm(YouSettings.getCurrentState(), parentDisposable); + llamaSettingsForm = new LlamaSettingsForm(LlamaSettings.getCurrentState()); + var cardLayout = new DynamicCardLayout(); var cards = new JPanel(cardLayout); - cards.add(serviceSelectionForm.getOpenAISettingsForm().getForm(), OPENAI.getCode()); - cards.add( - serviceSelectionForm.getCustomConfigurationSettingsForm().getForm(), - CUSTOM_OPENAI.getCode()); - cards.add(serviceSelectionForm.getAnthropicSettingsForm().getForm(), ANTHROPIC.getCode()); - cards.add(serviceSelectionForm.getAzureSettingsForm().getForm(), AZURE.getCode()); - cards.add(serviceSelectionForm.getYouSettingsForm(), YOU.getCode()); - cards.add(serviceSelectionForm.getLlamaSettingsForm(), LLAMA_CPP.getCode()); + cards.add(openAISettingsForm.getForm(), OPENAI.getCode()); + cards.add(customConfigurationSettingsForm.getForm(), CUSTOM_OPENAI.getCode()); + cards.add(anthropicSettingsForm.getForm(), ANTHROPIC.getCode()); + cards.add(azureSettingsForm.getForm(), AZURE.getCode()); + cards.add(youSettingsForm, YOU.getCode()); + cards.add(llamaSettingsForm, LLAMA_CPP.getCode()); var serviceComboBoxModel = new DefaultComboBoxModel(); serviceComboBoxModel.addAll(Arrays.stream(ServiceType.values()).toList()); serviceComboBox = new ComboBox<>(serviceComboBoxModel); @@ -63,6 +82,30 @@ public class GeneralSettingsComponent { .getPanel(); } + public OpenAISettingsForm getOpenAISettingsForm() { + return openAISettingsForm; + } + + public CustomServiceForm getCustomConfigurationSettingsForm() { + return customConfigurationSettingsForm; + } + + public AnthropicSettingsForm getAnthropicSettingsForm() { + return anthropicSettingsForm; + } + + public AzureSettingsForm getAzureSettingsForm() { + return azureSettingsForm; + } + + public LlamaSettingsForm getLlamaSettingsForm() { + return llamaSettingsForm; + } + + public YouSettingsForm getYouSettingsForm() { + return youSettingsForm; + } + public ServiceType getSelectedService() { return serviceComboBox.getItem(); } @@ -79,10 +122,6 @@ public class GeneralSettingsComponent { return displayNameField; } - public ServiceSelectionForm getServiceSelectionForm() { - return serviceSelectionForm; - } - public String getDisplayName() { return displayNameField.getText(); } @@ -91,6 +130,15 @@ public class GeneralSettingsComponent { displayNameField.setText(displayName); } + public void resetForms() { + openAISettingsForm.resetForm(); + customConfigurationSettingsForm.resetForm(); + anthropicSettingsForm.resetForm(); + azureSettingsForm.resetForm(); + youSettingsForm.resetForm(); + llamaSettingsForm.resetForm(); + } + static class DynamicCardLayout extends CardLayout { @Override diff --git a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java index b9d781bc..3d12089a 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/GeneralSettingsConfigurable.java @@ -18,7 +18,6 @@ import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettingsForm; import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; import ee.carlrobert.codegpt.settings.service.azure.AzureSettingsForm; import ee.carlrobert.codegpt.settings.service.custom.CustomServiceForm; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; @@ -61,17 +60,15 @@ public class GeneralSettingsConfigurable implements Configurable { @Override public boolean isModified() { var settings = GeneralSettings.getCurrentState(); - var serviceSelectionForm = component.getServiceSelectionForm(); + return !component.getDisplayName().equals(settings.getDisplayName()) || component.getSelectedService() != settings.getSelectedService() - || OpenAISettings.getInstance().isModified(serviceSelectionForm.getOpenAISettingsForm()) - || CustomServiceSettings.getInstance() - .isModified(serviceSelectionForm.getCustomConfigurationSettingsForm()) - || AnthropicSettings.getInstance() - .isModified(serviceSelectionForm.getAnthropicSettingsForm()) - || AzureSettings.getInstance().isModified(serviceSelectionForm.getAzureSettingsForm()) - || YouSettings.getInstance().isModified(serviceSelectionForm.getYouSettingsForm()) - || LlamaSettings.getInstance().isModified(serviceSelectionForm.getLlamaSettingsForm()); + || OpenAISettings.getInstance().isModified(component.getOpenAISettingsForm()) + || component.getCustomConfigurationSettingsForm().isModified() + || AnthropicSettings.getInstance().isModified(component.getAnthropicSettingsForm()) + || AzureSettings.getInstance().isModified(component.getAzureSettingsForm()) + || YouSettings.getInstance().isModified(component.getYouSettingsForm()) + || LlamaSettings.getInstance().isModified(component.getLlamaSettingsForm()); } @Override @@ -80,14 +77,13 @@ public class GeneralSettingsConfigurable implements Configurable { settings.setDisplayName(component.getDisplayName()); settings.setSelectedService(component.getSelectedService()); - var serviceSelectionForm = component.getServiceSelectionForm(); - var openAISettingsForm = serviceSelectionForm.getOpenAISettingsForm(); + var openAISettingsForm = component.getOpenAISettingsForm(); applyOpenAISettings(openAISettingsForm); - applyCustomOpenAISettings(serviceSelectionForm.getCustomConfigurationSettingsForm()); - applyAnthropicSettings(serviceSelectionForm.getAnthropicSettingsForm()); - applyAzureSettings(serviceSelectionForm.getAzureSettingsForm()); - applyYouSettings(serviceSelectionForm.getYouSettingsForm()); - applyLlamaSettings(serviceSelectionForm.getLlamaSettingsForm()); + applyCustomOpenAISettings(component.getCustomConfigurationSettingsForm()); + applyAnthropicSettings(component.getAnthropicSettingsForm()); + applyAzureSettings(component.getAzureSettingsForm()); + applyYouSettings(component.getYouSettingsForm()); + applyLlamaSettings(component.getLlamaSettingsForm()); var serviceChanged = component.getSelectedService() != settings.getSelectedService(); var modelChanged = !OpenAISettings.getCurrentState().getModel() @@ -109,7 +105,7 @@ public class GeneralSettingsConfigurable implements Configurable { private void applyCustomOpenAISettings(CustomServiceForm form) { CredentialsStore.INSTANCE.setCredential(CUSTOM_SERVICE_API_KEY, form.getApiKey()); - CustomServiceSettings.getInstance().loadState(form.getCurrentState()); + form.applyChanges(); } private void applyLlamaSettings(LlamaSettingsForm form) { @@ -142,7 +138,7 @@ public class GeneralSettingsConfigurable implements Configurable { var settings = GeneralSettings.getCurrentState(); component.setDisplayName(settings.getDisplayName()); component.setSelectedService(settings.getSelectedService()); - component.getServiceSelectionForm().resetForms(); + component.resetForms(); } @Override diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java deleted file mode 100644 index 3aa63e75..00000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java +++ /dev/null @@ -1,68 +0,0 @@ -package ee.carlrobert.codegpt.settings.service; - -import com.intellij.openapi.Disposable; -import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettings; -import ee.carlrobert.codegpt.settings.service.anthropic.AnthropicSettingsForm; -import ee.carlrobert.codegpt.settings.service.azure.AzureSettings; -import ee.carlrobert.codegpt.settings.service.azure.AzureSettingsForm; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceForm; -import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings; -import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; -import ee.carlrobert.codegpt.settings.service.llama.form.LlamaSettingsForm; -import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; -import ee.carlrobert.codegpt.settings.service.openai.OpenAISettingsForm; -import ee.carlrobert.codegpt.settings.service.you.YouSettings; -import ee.carlrobert.codegpt.settings.service.you.YouSettingsForm; - -public class ServiceSelectionForm { - - private final OpenAISettingsForm openAISettingsForm; - private final CustomServiceForm customServiceForm; - private final AnthropicSettingsForm anthropicSettingsForm; - private final AzureSettingsForm azureSettingsForm; - private final LlamaSettingsForm llamaSettingsForm; - private final YouSettingsForm youSettingsForm; - - public ServiceSelectionForm(Disposable parentDisposable) { - openAISettingsForm = new OpenAISettingsForm(OpenAISettings.getCurrentState()); - customServiceForm = new CustomServiceForm( - CustomServiceSettings.getCurrentState()); - anthropicSettingsForm = new AnthropicSettingsForm(AnthropicSettings.getCurrentState()); - azureSettingsForm = new AzureSettingsForm(AzureSettings.getCurrentState()); - youSettingsForm = new YouSettingsForm(YouSettings.getCurrentState(), parentDisposable); - llamaSettingsForm = new LlamaSettingsForm(LlamaSettings.getCurrentState()); - } - - public OpenAISettingsForm getOpenAISettingsForm() { - return openAISettingsForm; - } - - public CustomServiceForm getCustomConfigurationSettingsForm() { - return customServiceForm; - } - - public AnthropicSettingsForm getAnthropicSettingsForm() { - return anthropicSettingsForm; - } - - public AzureSettingsForm getAzureSettingsForm() { - return azureSettingsForm; - } - - public YouSettingsForm getYouSettingsForm() { - return youSettingsForm; - } - - public LlamaSettingsForm getLlamaSettingsForm() { - return llamaSettingsForm; - } - - public void resetForms() { - openAISettingsForm.resetForm(); - customServiceForm.resetForm(); - anthropicSettingsForm.resetForm(); - azureSettingsForm.resetForm(); - youSettingsForm.resetForm(); - llamaSettingsForm.resetForm(); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java deleted file mode 100644 index f05be3cd..00000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.java +++ /dev/null @@ -1,175 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; -import static ee.carlrobert.codegpt.ui.UIUtil.withEmptyLeftBorder; - -import com.intellij.icons.AllIcons.General; -import com.intellij.ide.HelpTooltip; -import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.ui.MessageType; -import com.intellij.ui.EnumComboBoxModel; -import com.intellij.ui.TitledSeparator; -import com.intellij.ui.components.JBLabel; -import com.intellij.ui.components.JBPasswordField; -import com.intellij.ui.components.JBTextField; -import com.intellij.util.ui.FormBuilder; -import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.completions.CallParameters; -import ee.carlrobert.codegpt.completions.CompletionRequestProvider; -import ee.carlrobert.codegpt.completions.CompletionRequestService; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.credentials.CredentialsStore; -import ee.carlrobert.codegpt.ui.OverlayUtil; -import ee.carlrobert.codegpt.ui.UIUtil; -import ee.carlrobert.llm.client.openai.completion.ErrorDetails; -import ee.carlrobert.llm.completion.CompletionEventListener; -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import java.net.MalformedURLException; -import java.net.URL; -import javax.swing.Box; -import javax.swing.JButton; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import okhttp3.sse.EventSource; -import org.jetbrains.annotations.Nullable; - -public class CustomServiceForm { - - private final JBPasswordField apiKeyField; - private final JBTextField urlField; - private final CustomServiceFormTabbedPane tabbedPane; - private final JButton testConnectionButton; - private final JBLabel templateHelpText; - private final ComboBox templateComboBox; - - public CustomServiceForm(CustomServiceSettingsState settings) { - apiKeyField = new JBPasswordField(); - apiKeyField.setColumns(30); - apiKeyField.setText(CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); - urlField = new JBTextField(settings.getUrl(), 30); - tabbedPane = new CustomServiceFormTabbedPane(settings); - testConnectionButton = new JButton(CodeGPTBundle.get( - "settingsConfigurable.service.custom.openai.testConnection.label")); - testConnectionButton.addActionListener(e -> testConnection(getCurrentState())); - templateHelpText = new JBLabel(General.ContextHelp); - templateComboBox = new ComboBox<>( - new EnumComboBoxModel<>(CustomServiceTemplate.class)); - templateComboBox.setSelectedItem(settings.getTemplate()); - templateComboBox.addItemListener(e -> { - var template = (CustomServiceTemplate) e.getItem(); - updateTemplateHelpTextTooltip(template); - urlField.setText(template.getUrl()); - tabbedPane.setHeaders(template.getHeaders()); - tabbedPane.setBody(template.getBody()); - }); - updateTemplateHelpTextTooltip(settings.getTemplate()); - } - - public JPanel getForm() { - var urlPanel = new JPanel(new BorderLayout(8, 0)); - urlPanel.add(urlField, BorderLayout.CENTER); - urlPanel.add(testConnectionButton, BorderLayout.EAST); - - var templateComboBoxWrapper = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); - templateComboBoxWrapper.add(templateComboBox); - templateComboBoxWrapper.add(Box.createHorizontalStrut(8)); - templateComboBoxWrapper.add(templateHelpText); - - var form = FormBuilder.createFormBuilder() - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.presetTemplate.label"), - templateComboBoxWrapper) - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), - apiKeyField) - .addComponentToRightColumn( - UIUtil.createComment("settingsConfigurable.service.custom.openai.apiKey.comment")) - .addLabeledComponent( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.url.label"), - urlPanel) - .addComponent(tabbedPane) - .getPanel(); - - return FormBuilder.createFormBuilder() - .addComponent(new TitledSeparator(CodeGPTBundle.get("shared.configuration"))) - .addComponent(withEmptyLeftBorder(form)) - .addComponentFillVertically(new JPanel(), 0) - .getPanel(); - } - - public @Nullable String getApiKey() { - var apiKey = new String(apiKeyField.getPassword()); - return apiKey.isEmpty() ? null : apiKey; - } - - public CustomServiceSettingsState getCurrentState() { - var state = new CustomServiceSettingsState(); - state.setUrl(urlField.getText()); - state.setTemplate(templateComboBox.getItem()); - state.setHeaders(tabbedPane.getHeaders()); - state.setBody(tabbedPane.getBody()); - return state; - } - - public void resetForm() { - var state = CustomServiceSettings.getCurrentState(); - apiKeyField.setText(CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); - urlField.setText(state.getUrl()); - templateComboBox.setSelectedItem(state.getTemplate()); - tabbedPane.setHeaders(state.getHeaders()); - tabbedPane.setBody(state.getBody()); - } - - private void updateTemplateHelpTextTooltip(CustomServiceTemplate template) { - templateHelpText.setToolTipText(null); - try { - new HelpTooltip() - .setTitle(template.getName()) - .setBrowserLink( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.linkToDocs"), - new URL(template.getDocsUrl())) - .installOn(templateHelpText); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - private void testConnection(CustomServiceSettingsState customConfiguration) { - var conversation = new Conversation(); - var request = new CompletionRequestProvider(conversation) - .buildCustomOpenAIChatCompletionRequest( - customConfiguration, - new CallParameters(conversation, new Message("Hello!"))); - CompletionRequestService.getInstance() - .getCustomOpenAIChatCompletionAsync(request, new TestConnectionEventListener()); - } - - class TestConnectionEventListener implements CompletionEventListener { - - @Override - public void onMessage(String value, EventSource eventSource) { - if (value != null && !value.isEmpty()) { - SwingUtilities.invokeLater(() -> { - OverlayUtil.showBalloon( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), - MessageType.INFO, - testConnectionButton); - eventSource.cancel(); - }); - } - } - - @Override - public void onError(ErrorDetails error, Throwable ex) { - SwingUtilities.invokeLater(() -> - OverlayUtil.showBalloon( - CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") - + "\n\n" - + error.getMessage(), - MessageType.ERROR, - testConnectionButton)); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java index b318bef7..2b5d828f 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceFormTabbedPane.java @@ -18,12 +18,13 @@ class CustomServiceFormTabbedPane extends JBTabbedPane { private final JBTable headersTable; private final JBTable bodyTable; - CustomServiceFormTabbedPane(CustomServiceSettingsState customConfiguration) { + CustomServiceFormTabbedPane(Map headers, Map body) { headersTable = new JBTable( - new DefaultTableModel(toArray(customConfiguration.getHeaders()), + new DefaultTableModel(toArray(headers), new Object[]{"Key", "Value"})); + bodyTable = new JBTable( - new DefaultTableModel(toArray(customConfiguration.getBody()), + new DefaultTableModel(toArray(body), new Object[]{"Key", "Value"})); setTabComponentInsets(JBUI.insetsTop(8)); @@ -46,11 +47,11 @@ class CustomServiceFormTabbedPane extends JBTabbedPane { .collect(toMap(Entry::getKey, entry -> (String) entry.getValue())); } - public void setBody(Map body) { + public void setBody(Map body) { setTableData(bodyTable, body); } - public Map getBody() { + public Map getBody() { return getTableData(bodyTable); } diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java deleted file mode 100644 index f65fa3c5..00000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.java +++ /dev/null @@ -1,45 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import ee.carlrobert.codegpt.credentials.CredentialsStore; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; - -@State( - name = "CodeGPT_CustomServiceSettings", - storages = @Storage("CodeGPT_CustomServiceSettings.xml")) -public class CustomServiceSettings implements PersistentStateComponent { - - private CustomServiceSettingsState state = new CustomServiceSettingsState(); - - @Override - @NotNull - public CustomServiceSettingsState getState() { - return state; - } - - @Override - public void loadState(@NotNull CustomServiceSettingsState state) { - this.state = state; - } - - public static CustomServiceSettingsState getCurrentState() { - return getInstance().getState(); - } - - public static CustomServiceSettings getInstance() { - return ApplicationManager.getApplication().getService(CustomServiceSettings.class); - } - - public boolean isModified(CustomServiceForm form) { - return !form.getCurrentState().equals(state) - || !StringUtils.equals( - form.getApiKey(), - CredentialsStore.INSTANCE.getCredential(CUSTOM_SERVICE_API_KEY)); - } -} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java deleted file mode 100644 index cc93872e..00000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettingsState.java +++ /dev/null @@ -1,69 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import static ee.carlrobert.codegpt.settings.service.custom.CustomServiceTemplate.OPENAI; - -import com.intellij.util.xmlb.annotations.OptionTag; -import ee.carlrobert.codegpt.util.MapConverter; -import java.util.Map; -import java.util.Objects; - -public class CustomServiceSettingsState { - - private String url = OPENAI.getUrl(); - private Map headers = OPENAI.getHeaders(); - @OptionTag(converter = MapConverter.class) - private Map body = OPENAI.getBody(); - private CustomServiceTemplate template = OPENAI; - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public Map getHeaders() { - return headers; - } - - public void setHeaders(Map headers) { - this.headers = headers; - } - - public Map getBody() { - return body; - } - - public void setBody(Map body) { - this.body = body; - } - - public CustomServiceTemplate getTemplate() { - return template; - } - - public void setTemplate(CustomServiceTemplate template) { - this.template = template; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CustomServiceSettingsState that = (CustomServiceSettingsState) o; - return Objects.equals(url, that.url) - && Objects.equals(headers, that.headers) - && Objects.equals(body, that.body) - && template == that.template; - } - - @Override - public int hashCode() { - return Objects.hash(url, headers, body, template); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java b/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java deleted file mode 100644 index 49226a4c..00000000 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.java +++ /dev/null @@ -1,158 +0,0 @@ -package ee.carlrobert.codegpt.settings.service.custom; - -import java.util.HashMap; -import java.util.Map; - -public enum CustomServiceTemplate { - - // Cloud providers - ANYSCALE( - "Anyscale", - "https://docs.endpoints.anyscale.com/", - "https://api.endpoints.anyscale.com/v1/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "mistralai/Mixtral-8x7B-Instruct-v0.1", - "max_tokens", 1024))), - AZURE( - "Azure OpenAI", - "https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions", - "https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/chat/completions?api-version=2023-05-15", - getDefaultHeaders("api-key", "$CUSTOM_SERVICE_API_KEY"), - getDefaultBodyParams(Map.of())), - DEEP_INFRA( - "DeepInfra", - "https://deepinfra.com/docs/advanced/openai_api", - "https://api.deepinfra.com/v1/openai/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "meta-llama/Llama-2-70b-chat-hf", - "max_tokens", 1024))), - FIREWORKS( - "Fireworks", - "https://readme.fireworks.ai/reference/createchatcompletion", - "https://api.fireworks.ai/inference/v1/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "accounts/fireworks/models/llama-v2-7b-chat", - "max_tokens", 1024))), - GROQ( - "Groq", - "https://docs.api.groq.com/md/openai.oas.html", - "https://api.groq.com/openai/v1/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "codellama-34b", - "max_tokens", 1024))), - OPENAI( - "OpenAI", - "https://platform.openai.com/docs/api-reference/chat", - "https://api.openai.com/v1/chat/completions", - getDefaultHeaders("Authorization", "Bearer $CUSTOM_SERVICE_API_KEY"), - getDefaultBodyParams(Map.of( - "model", "gpt-4", - "max_tokens", 1024))), - PERPLEXITY( - "Perplexity AI", - "https://docs.perplexity.ai/reference/post_chat_completions", - "https://api.perplexity.ai/chat/completions", - getDefaultHeadersWithAuthentication(), - getDefaultBodyParams(Map.of( - "model", "codellama", - "max_tokens", 1024))), - TOGETHER( - "Together AI", - "https://docs.together.ai/docs/openai-api-compatibility", - "https://api.together.xyz/v1/chat/completions", - getDefaultHeaders("Authorization", "Bearer $CUSTOM_SERVICE_API_KEY"), - getDefaultBodyParams(Map.of( - "model", "deepseek-ai/deepseek-coder-33b-instruct", - "max_tokens", 1024))), - - // Local providers - OLLAMA( - "Ollama", - "https://github.com/ollama/ollama/blob/main/docs/openai.md", - "http://localhost:11434/v1/chat/completions", - getDefaultHeaders(), - getDefaultBodyParams(Map.of("model", "codellama"))), - LLAMA_CPP( - "LLaMA C/C++", - "https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md", - "http://localhost:8080/v1/chat/completions", - getDefaultHeaders(), - getDefaultBodyParams(Map.of())); - - private final String name; - private final String docsUrl; - private final String url; - private final Map headers; - private final Map body; - - CustomServiceTemplate( - String name, - String docsUrl, - String url, - Map headers, - Map body) { - this.name = name; - this.docsUrl = docsUrl; - this.url = url; - this.headers = headers; - this.body = body; - } - - public String getName() { - return name; - } - - public String getDocsUrl() { - return docsUrl; - } - - public String getUrl() { - return url; - } - - public Map getHeaders() { - return headers; - } - - public Map getBody() { - return body; - } - - @Override - public String toString() { - return name; - } - - private static Map getDefaultHeadersWithAuthentication() { - return getDefaultHeaders("Authorization", "Bearer $CUSTOM_SERVICE_API_KEY"); - } - - private static Map getDefaultHeaders() { - return getDefaultHeaders(Map.of()); - } - - private static Map getDefaultHeaders(String key, String value) { - return getDefaultHeaders(Map.of(key, value)); - } - - private static Map getDefaultHeaders(Map additionalHeaders) { - var defaultHeaders = new HashMap<>(Map.of( - "Content-Type", "application/json", - "X-LLM-Application-Tag", "codegpt")); - defaultHeaders.putAll(additionalHeaders); - return defaultHeaders; - } - - private static Map getDefaultBodyParams(Map additionalParams) { - var defaultParams = new HashMap(Map.of( - "stream", true, - "messages", "$OPENAI_MESSAGES", - "temperature", 0.1)); - defaultParams.putAll(additionalParams); - return defaultParams; - } -} 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 1d1b14b5..71aef244 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 @@ -82,7 +82,10 @@ public class ModelComboBoxAction extends ComboBoxAction { actionGroup.addSeparator("Custom OpenAI Service"); actionGroup.add(createModelAction( CUSTOM_OPENAI, - CustomServiceSettings.getCurrentState().getTemplate().getName(), + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getTemplate() + .getProviderName(), Icons.OpenAI, presentation)); actionGroup.addSeparator(); @@ -150,9 +153,11 @@ public class ModelComboBoxAction extends ComboBoxAction { break; case CUSTOM_OPENAI: templatePresentation.setIcon(Icons.OpenAI); - templatePresentation.setText(CustomServiceSettings.getCurrentState() - .getTemplate() - .getName()); + templatePresentation.setText( + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getTemplate() + .getProviderName()); break; case ANTHROPIC: templatePresentation.setIcon(Icons.Anthropic); diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index c9dc223e..bf5d7145 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -2,11 +2,12 @@ package ee.carlrobert.codegpt.actions import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.service.ServiceType -import ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP -import ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI +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.openai.OpenAISettings @@ -14,12 +15,16 @@ abstract class CodeCompletionFeatureToggleActions( private val enableFeatureAction: Boolean ) : DumbAwareAction() { + override fun actionPerformed(e: AnActionEvent) { GeneralSettings.getCurrentState().selectedService - .takeIf { it in listOf(OPENAI, LLAMA_CPP) } + .takeIf { it in listOf(OPENAI, CUSTOM_OPENAI, LLAMA_CPP) } ?.also { selectedService -> if (OPENAI == selectedService) { OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction + } else if (CUSTOM_OPENAI == selectedService) { + service().state.codeCompletionSettings.codeCompletionsEnabled = + enableFeatureAction } else { LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction } @@ -31,7 +36,7 @@ abstract class CodeCompletionFeatureToggleActions( val codeCompletionEnabled = isCodeCompletionsEnabled(selectedService) e.presentation.isEnabled = codeCompletionEnabled != enableFeatureAction e.presentation.isVisible = - e.presentation.isEnabled && listOf(OPENAI, LLAMA_CPP).contains( + e.presentation.isEnabled && listOf(OPENAI, CUSTOM_OPENAI, LLAMA_CPP).contains( selectedService ) } @@ -43,6 +48,7 @@ abstract class CodeCompletionFeatureToggleActions( private fun isCodeCompletionsEnabled(serviceType: ServiceType): Boolean { return when (serviceType) { OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled else -> false } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt index 26c35063..d7723f0d 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionRequestFactory.kt @@ -1,13 +1,26 @@ package ee.carlrobert.codegpt.codecompletions +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.intellij.openapi.components.service import ee.carlrobert.codegpt.completions.llama.LlamaModel +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey +import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +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.openai.OpenAISettings import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest import ee.carlrobert.llm.client.openai.completion.request.OpenAITextCompletionRequest +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.nio.charset.StandardCharsets object CodeCompletionRequestFactory { + + @JvmStatic fun buildOpenAIRequest(details: InfillRequestDetails): OpenAITextCompletionRequest { return OpenAITextCompletionRequest.Builder(details.prefix) .setSuffix(details.suffix) @@ -17,6 +30,35 @@ object CodeCompletionRequestFactory { .build() } + @JvmStatic + fun buildCustomRequest(details: InfillRequestDetails): Request { + val settings = service().state.codeCompletionSettings + val requestBuilder = Request.Builder().url(settings.url!!) + val credential = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + for (entry in settings.headers.entries) { + var value = entry.value + if (credential != null && value.contains("\$CUSTOM_SERVICE_API_KEY")) { + value = value.replace("\$CUSTOM_SERVICE_API_KEY", credential) + } + requestBuilder.addHeader(entry.key, value) + } + val transformedBody = settings.body.entries.associate { (key, value) -> + key to transformValue(value, settings.infillTemplate, details) + } + + try { + val requestBody = ObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(transformedBody) + .toByteArray(StandardCharsets.UTF_8) + .toRequestBody("application/json".toMediaType()) + return requestBuilder.post(requestBody).build() + } catch (e: JsonProcessingException) { + throw RuntimeException(e) + } + } + + @JvmStatic fun buildLlamaRequest(details: InfillRequestDetails): LlamaCompletionRequest { val settings = LlamaSettings.getCurrentState() val promptTemplate = getLlamaInfillPromptTemplate(settings) @@ -38,4 +80,18 @@ object CodeCompletionRequestFactory { } return LlamaModel.findByHuggingFaceModel(settings.huggingFaceModel).infillPromptTemplate } + + private fun transformValue( + value: Any, + template: InfillPromptTemplate, + details: InfillRequestDetails + ): Any { + if (value !is String) return value + return when (value) { + "$" + Placeholder.FIM_PROMPT -> template.buildPrompt(details.prefix, details.suffix) + "$" + Placeholder.PREFIX -> details.prefix + "$" + Placeholder.SUFFIX -> details.suffix + 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 91f59474..9812902f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeGPTInlineCompletionProvider.kt @@ -2,14 +2,19 @@ package ee.carlrobert.codegpt.codecompletions import com.intellij.codeInsight.inline.completion.* import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement +import com.intellij.notification.NotificationType import com.intellij.openapi.application.EDT -import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.completions.CompletionRequestService 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.openai.OpenAISettings +import ee.carlrobert.codegpt.ui.OverlayUtil.showNotification +import ee.carlrobert.llm.client.openai.completion.ErrorDetails import ee.carlrobert.llm.completion.CompletionEventListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose @@ -20,9 +25,8 @@ import okhttp3.sse.EventSource import java.util.concurrent.atomic.AtomicReference class CodeGPTInlineCompletionProvider : InlineCompletionProvider { - companion object { - private val LOG = Logger.getInstance(CodeGPTInlineCompletionProvider::class.java) + private val logger = thisLogger() } private val currentCall = AtomicReference(null) @@ -32,7 +36,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion { if (request.editor.project == null) { - LOG.error("Could not find project") + logger.error("Could not find project") return InlineCompletionSuggestion.empty() } @@ -50,7 +54,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { try { trySend(InlineCompletionGrayTextElement(inlineText)) } catch (e: Exception) { - LOG.error("Failed to send inline completion suggestion", e) + logger.error("Failed to send inline completion suggestion", e) } } } @@ -64,6 +68,7 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { val selectedService = GeneralSettings.getCurrentState().selectedService val codeCompletionsEnabled = when (selectedService) { ServiceType.OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled + ServiceType.CUSTOM_OPENAI -> service().state.codeCompletionSettings.codeCompletionsEnabled ServiceType.LLAMA_CPP -> LlamaSettings.getCurrentState().isCodeCompletionsEnabled else -> false } @@ -91,5 +96,12 @@ class CodeGPTInlineCompletionProvider : InlineCompletionProvider { override fun onCancelled(messageBuilder: StringBuilder) { completed(messageBuilder) } + + override fun onError(error: ErrorDetails, ex: Throwable) { + if (ex.message == null || (ex.message != null && ex.message != "Canceled")) { + showNotification(error.message, NotificationType.ERROR) + logger.error(error.message, ex) + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt index 3b207f90..7dd46664 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.kt @@ -4,11 +4,10 @@ import com.intellij.codeInsight.inline.completion.InlineCompletionRequest import com.intellij.openapi.editor.Document import com.intellij.openapi.util.TextRange import ee.carlrobert.codegpt.EncodingManager -import ee.carlrobert.codegpt.util.file.FileUtil import kotlin.math.max import kotlin.math.min -class InfillRequestDetails(val prefix: String, val suffix: String, val fileExtension: String) { +class InfillRequestDetails(val prefix: String, val suffix: String) { companion object { private const val MAX_OFFSET = 10_000 private const val MAX_PROMPT_TOKENS = 128 @@ -17,18 +16,16 @@ class InfillRequestDetails(val prefix: String, val suffix: String, val fileExten return fromDocumentWithMaxOffset( request.editor.document, request.editor.caretModel.offset, - FileUtil.getFileExtension(request.file.name) ) } private fun fromDocumentWithMaxOffset( document: Document, caretOffset: Int, - fileExtension: String ): InfillRequestDetails { val start = max(0, (caretOffset - MAX_OFFSET)) val end = min(document.textLength, (caretOffset + MAX_OFFSET)) - return fromDocumentWithCustomRange(document, caretOffset, start, end, fileExtension) + return fromDocumentWithCustomRange(document, caretOffset, start, end) } private fun fromDocumentWithCustomRange( @@ -36,11 +33,10 @@ class InfillRequestDetails(val prefix: String, val suffix: String, val fileExten caretOffset: Int, start: Int, end: Int, - fileExtension: String ): InfillRequestDetails { val prefix: String = truncateText(document, start, caretOffset, false) val suffix: String = truncateText(document, caretOffset, end, true) - return InfillRequestDetails(prefix, suffix, fileExtension) + return InfillRequestDetails(prefix, suffix) } private fun truncateText( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt index 9a940907..20f8ce15 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt @@ -7,7 +7,10 @@ import java.time.LocalDate enum class Placeholder(val description: String) { DATE_ISO_8601("Current date in ISO 8601 format, e.g. 2021-01-01."), - BRANCH_NAME("The name of the current branch") + BRANCH_NAME("The name of the current branch."), + PREFIX("Code before the cursor."), + SUFFIX("Code after the cursor."), + FIM_PROMPT("Prebuilt Fill-In-The-Middle (FIM) prompt using the specified template."), } interface PlaceholderStrategy { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt new file mode 100644 index 00000000..7bf46f95 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionForm.kt @@ -0,0 +1,101 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.openapi.ui.MessageType +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.completions.CompletionRequestProvider +import ee.carlrobert.codegpt.completions.CompletionRequestService +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.llm.client.openai.completion.ErrorDetails +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource +import java.awt.BorderLayout +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class CustomServiceChatCompletionForm(state: CustomServiceChatCompletionSettingsState) { + + private val urlField = JBTextField(state.url, 30) + private val tabbedPane = CustomServiceFormTabbedPane(state.headers, state.body) + private val testConnectionButton = JButton( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label") + ) + + init { + testConnectionButton.addActionListener { testConnection() } + } + + var url: String + get() = urlField.text + set(url) { + urlField.text = url + } + + var headers: MutableMap + get() = tabbedPane.headers + set(value) { + tabbedPane.headers = value + } + + var body: MutableMap + get() = tabbedPane.body + set(value) { + tabbedPane.body = value + } + + val form: JPanel + get() = FormBuilder.createFormBuilder() + .addVerticalGap(8) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.url.label"), + JPanel(BorderLayout(8, 0)).apply { + add(urlField, BorderLayout.CENTER) + add(testConnectionButton, BorderLayout.EAST) + } + ) + .addComponent(tabbedPane) + .addComponentFillVertically(JPanel(), 0) + .panel + + fun resetForm(settings: CustomServiceChatCompletionSettingsState) { + urlField.text = settings.url + tabbedPane.headers = settings.headers + tabbedPane.body = settings.body + } + + private fun testConnection() { + CompletionRequestService.getInstance().getCustomOpenAIChatCompletionAsync( + CompletionRequestProvider.buildCustomOpenAICompletionRequest("Hello!"), + TestConnectionEventListener() + ) + } + + internal inner class TestConnectionEventListener : CompletionEventListener { + override fun onMessage(value: String?, eventSource: EventSource) { + if (!value.isNullOrEmpty()) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), + MessageType.INFO, + testConnectionButton + ) + eventSource.cancel() + } + } + } + + override fun onError(error: ErrorDetails, ex: Throwable) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") + + "\n\n" + + error.message, + MessageType.ERROR, + testConnectionButton + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt new file mode 100644 index 00000000..f6d39271 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceChatCompletionTemplate.kt @@ -0,0 +1,124 @@ +package ee.carlrobert.codegpt.settings.service.custom + +enum class CustomServiceChatCompletionTemplate( + val url: String, + val headers: MutableMap, + val body: MutableMap +) { + ANYSCALE( + "https://api.endpoints.anyscale.com/v1/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "mistralai/Mixtral-8x7B-Instruct-v0.1", + "max_tokens" to 1024 + ) + ) + ), + AZURE( + "https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/chat/completions?api-version=2023-05-15", + getDefaultHeaders("api-key", "\$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams(emptyMap()) + ), + DEEP_INFRA( + "https://api.deepinfra.com/v1/openai/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "meta-llama/Llama-2-70b-chat-hf", + "max_tokens" to 1024 + ) + ) + ), + FIREWORKS( + "https://api.fireworks.ai/inference/v1/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "accounts/fireworks/models/llama-v2-7b-chat", + "max_tokens" to 1024 + ) + ) + ), + GROQ( + "https://api.groq.com/openai/v1/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "codellama-34b", + "max_tokens" to 1024 + ) + ) + ), + OPENAI( + "https://api.openai.com/v1/chat/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams( + mapOf( + "model" to "gpt-4", + "max_tokens" to 1024 + ) + ) + ), + PERPLEXITY( + "https://api.perplexity.ai/chat/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams( + mapOf( + "model" to "codellama", + "max_tokens" to 1024 + ) + ) + ), + TOGETHER( + "https://api.together.xyz/v1/chat/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams( + mapOf( + "model" to "deepseek-ai/deepseek-coder-33b-instruct", + "max_tokens" to 1024 + ) + ) + ), + OLLAMA( + "http://localhost:11434/v1/chat/completions", + getDefaultHeaders(), + getDefaultBodyParams(mapOf("model" to "codellama")) + ), + LLAMA_CPP( + "http://localhost:8080/v1/chat/completions", + getDefaultHeaders(), + getDefaultBodyParams(emptyMap()) + ); +} + +private fun getDefaultHeadersWithAuthentication(): MutableMap { + return getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY") +} + +private fun getDefaultHeaders(): MutableMap { + return getDefaultHeaders(emptyMap()) +} + +private fun getDefaultHeaders(key: String, value: String): MutableMap { + return getDefaultHeaders(mapOf(key to value)) +} + +private fun getDefaultHeaders(additionalHeaders: Map): MutableMap { + val defaultHeaders = mutableMapOf( + "Content-Type" to "application/json", + "X-LLM-Application-Tag" to "codegpt" + ) + defaultHeaders.putAll(additionalHeaders) + return defaultHeaders +} + +private fun getDefaultBodyParams(additionalParams: Map): MutableMap { + val defaultParams = mutableMapOf( + "stream" to true, + "messages" to "\$OPENAI_MESSAGES", + "temperature" to 0.1 + ) + defaultParams.putAll(additionalParams) + return defaultParams +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt new file mode 100644 index 00000000..fcac639b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionForm.kt @@ -0,0 +1,182 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.icons.AllIcons.General +import com.intellij.ide.HelpTooltip +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.MessageType +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.JBTextField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestFactory +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate +import ee.carlrobert.codegpt.codecompletions.InfillRequestDetails +import ee.carlrobert.codegpt.completions.CompletionRequestService +import ee.carlrobert.codegpt.settings.configuration.Placeholder +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.llm.client.openai.completion.ErrorDetails +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource +import org.apache.commons.text.StringEscapeUtils +import java.awt.BorderLayout +import java.awt.FlowLayout +import javax.swing.Box +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class CustomServiceCodeCompletionForm(state: CustomServiceCodeCompletionSettingsState) { + + private val featureEnabledCheckBox = JBCheckBox( + CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), + state.codeCompletionsEnabled + ) + private val promptTemplateComboBox = + ComboBox(EnumComboBoxModel(InfillPromptTemplate::class.java)).apply { + selectedItem = state.infillTemplate + setSelectedItem(InfillPromptTemplate.LLAMA) + addItemListener { + updatePromptTemplateHelpTooltip(it.item as InfillPromptTemplate) + } + } + private val promptTemplateHelpText = JBLabel(General.ContextHelp) + private val urlField = JBTextField(state.url, 30) + private val tabbedPane = CustomServiceFormTabbedPane(state.headers, state.body) + private val testConnectionButton = JButton( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.testConnection.label") + ) + + init { + testConnectionButton.addActionListener { testConnection() } + updatePromptTemplateHelpTooltip(state.infillTemplate) + } + + var codeCompletionsEnabled: Boolean + get() = featureEnabledCheckBox.isSelected + set(enabled) { + featureEnabledCheckBox.isSelected = enabled + } + + var infillTemplate: InfillPromptTemplate + get() = promptTemplateComboBox.item + set(template) { + promptTemplateComboBox.selectedItem = template + } + + var url: String + get() = urlField.text + set(url) { + urlField.text = url + } + + var headers: MutableMap + get() = tabbedPane.headers + set(value) { + tabbedPane.headers = value + } + + var body: MutableMap + get() = tabbedPane.body + set(value) { + tabbedPane.body = value + } + + val form: JPanel + get() = FormBuilder.createFormBuilder() + .addVerticalGap(8) + .addComponent(featureEnabledCheckBox) + .addVerticalGap(4) + .addLabeledComponent( + "FIM template:", + JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { + add(promptTemplateComboBox) + add(Box.createHorizontalStrut(4)) + add(promptTemplateHelpText) + }) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.url.label"), + JPanel(BorderLayout(8, 0)).apply { + add(urlField, BorderLayout.CENTER) + add(testConnectionButton, BorderLayout.EAST) + } + ) + .addComponent(tabbedPane) + .addComponent(ComponentPanelBuilder.createCommentComponent(getHtmlDescription(), true, 100)) + .addComponentFillVertically(JPanel(), 0) + .panel + + private fun getHtmlDescription(): String { + val placeholderDescriptions = listOf( + Placeholder.FIM_PROMPT, + Placeholder.PREFIX, + Placeholder.SUFFIX + ).joinToString("\n") { + "
  • \$${it.name}: ${it.description}
  • " + } + + return buildString { + append("\n") + append("\n") + append("

    Use the following placeholders to insert dynamic values:

    \n") + append("
      $placeholderDescriptions
    \n") + append("\n") + append("") + } + } + + fun resetForm(settings: CustomServiceCodeCompletionSettingsState) { + featureEnabledCheckBox.isSelected = settings.codeCompletionsEnabled + promptTemplateComboBox.selectedItem = settings.infillTemplate + urlField.text = settings.url + tabbedPane.headers = settings.headers + tabbedPane.body = settings.body + updatePromptTemplateHelpTooltip(settings.infillTemplate) + } + + private fun testConnection() { + CompletionRequestService.getInstance().getCustomOpenAICompletionAsync( + CodeCompletionRequestFactory.buildCustomRequest(InfillRequestDetails("Hello", "!")), + TestConnectionEventListener() + ) + } + + internal inner class TestConnectionEventListener : CompletionEventListener { + override fun onMessage(value: String?, eventSource: EventSource) { + if (!value.isNullOrEmpty()) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), + MessageType.INFO, + testConnectionButton + ) + eventSource.cancel() + } + } + } + + override fun onError(error: ErrorDetails, ex: Throwable) { + SwingUtilities.invokeLater { + OverlayUtil.showBalloon( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") + + "\n\n" + + error.message, + MessageType.ERROR, + testConnectionButton + ) + } + } + } + + private fun updatePromptTemplateHelpTooltip(template: InfillPromptTemplate) { + promptTemplateHelpText.setToolTipText(null) + + val description = StringEscapeUtils.escapeHtml4(template.buildPrompt("PREFIX", "SUFFIX")) + HelpTooltip() + .setTitle(template.toString()) + .setDescription("

    $description

    ") + .installOn(promptTemplateHelpText) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt new file mode 100644 index 00000000..98ca97ab --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceCodeCompletionTemplate.kt @@ -0,0 +1,73 @@ +package ee.carlrobert.codegpt.settings.service.custom + +enum class CustomServiceCodeCompletionTemplate( + val url: String, + val headers: MutableMap, + val body: MutableMap +) { + ANYSCALE( + "https://api.endpoints.anyscale.com/v1/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams(mapOf("model" to "codellama/CodeLlama-70b-Instruct-hf")) + ), + AZURE( + "https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}/completions?api-version=2023-05-15", + getDefaultHeaders("api-key", "\$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams(emptyMap()) + ), + DEEP_INFRA( + "https://api.deepinfra.com/v1/inference/codellama/CodeLlama-70b-Instruct-hf", + getDefaultHeadersWithAuthentication(), + mutableMapOf("input" to "\$FIM_PROMPT") + ), + FIREWORKS( + "https://api.fireworks.ai/inference/v1/completions", + getDefaultHeadersWithAuthentication(), + getDefaultBodyParams(mapOf("model" to "accounts/fireworks/models/starcoder-16b")) + ), + OPENAI( + "https://api.openai.com/v1/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + mutableMapOf( + "stream" to true, + "prompt" to "\$PREFIX", + "suffix" to "\$SUFFIX", + "model" to "gpt-3.5-turbo-instruct", + "temperature" to 0.2, + "max_tokens" to 24 + ) + ), + TOGETHER( + "https://api.together.xyz/v1/completions", + getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY"), + getDefaultBodyParams(mapOf("model" to "codellama/CodeLlama-70b-hf")) + ) +} + +private fun getDefaultHeadersWithAuthentication(): MutableMap { + return getDefaultHeaders("Authorization", "Bearer \$CUSTOM_SERVICE_API_KEY") +} + +private fun getDefaultHeaders(key: String, value: String): MutableMap { + return getDefaultHeaders(mapOf(key to value)) +} + +private fun getDefaultHeaders(additionalHeaders: Map): MutableMap { + val defaultHeaders = mutableMapOf( + "Content-Type" to "application/json", + "X-LLM-Application-Tag" to "codegpt" + ) + defaultHeaders.putAll(additionalHeaders) + return defaultHeaders +} + +private fun getDefaultBodyParams(additionalParams: Map): MutableMap { + val defaultParams = mutableMapOf( + "stream" to true, + "prompt" to "\$FIM_PROMPT", + "temperature" to 0.2, + "max_tokens" to 24 + ) + defaultParams.putAll(additionalParams) + return defaultParams +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt new file mode 100644 index 00000000..b5c8344d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceForm.kt @@ -0,0 +1,148 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.icons.AllIcons.General +import com.intellij.ide.HelpTooltip +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPasswordField +import com.intellij.util.ui.FormBuilder +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey +import ee.carlrobert.codegpt.credentials.CredentialsStore.getCredential +import ee.carlrobert.codegpt.ui.UIUtil +import java.awt.FlowLayout +import java.net.MalformedURLException +import java.net.URL +import javax.swing.Box +import javax.swing.JPanel +import javax.swing.JTabbedPane + +class CustomServiceForm { + + private val apiKeyField = JBPasswordField().apply { + columns = 30 + text = getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + } + private val templateHelpText = JBLabel(General.ContextHelp) + private val templateComboBox = ComboBox(EnumComboBoxModel(CustomServiceTemplate::class.java)) + private val chatCompletionsForm: CustomServiceChatCompletionForm + private val codeCompletionsForm: CustomServiceCodeCompletionForm + private val tabbedPane: JTabbedPane + + init { + val state = service().state + chatCompletionsForm = CustomServiceChatCompletionForm(state.chatCompletionSettings) + codeCompletionsForm = CustomServiceCodeCompletionForm(state.codeCompletionSettings) + tabbedPane = JTabbedPane().apply { + add(CodeGPTBundle.get("shared.chatCompletions"), chatCompletionsForm.form) + add(CodeGPTBundle.get("shared.codeCompletions"), codeCompletionsForm.form) + } + templateComboBox.selectedItem = state.template + templateComboBox.addItemListener { + val template = it.item as CustomServiceTemplate + updateTemplateHelpTextTooltip(template) + chatCompletionsForm.run { + url = template.chatCompletionTemplate.url + headers = template.chatCompletionTemplate.headers + body = template.chatCompletionTemplate.body + } + if (template.codeCompletionTemplate != null) { + codeCompletionsForm.run { + url = template.codeCompletionTemplate.url + headers = template.codeCompletionTemplate.headers + body = template.codeCompletionTemplate.body + } + tabbedPane.setEnabledAt(1, true) + } else { + tabbedPane.selectedIndex = 0 + tabbedPane.setEnabledAt(1, false) + } + } + updateTemplateHelpTextTooltip(state.template) + } + + fun getForm(): JPanel = FormBuilder.createFormBuilder() + .addComponent(TitledSeparator(CodeGPTBundle.get("shared.configuration"))) + .addComponent( + FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.presetTemplate.label"), + JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)).apply { + add(templateComboBox) + add(Box.createHorizontalStrut(8)) + add(templateHelpText) + } + ) + .addLabeledComponent( + CodeGPTBundle.get("settingsConfigurable.shared.apiKey.label"), + apiKeyField + ) + .addComponentToRightColumn( + UIUtil.createComment("settingsConfigurable.service.custom.openai.apiKey.comment") + ) + .addVerticalGap(4) + .addComponent(tabbedPane) + .panel + ) + .panel + + fun getApiKey() = String(apiKeyField.password).ifEmpty { null } + + fun isModified() = service().state.run { + templateComboBox.selectedItem != template + || chatCompletionsForm.url != chatCompletionSettings.url + || chatCompletionsForm.headers != chatCompletionSettings.headers + || chatCompletionsForm.body != chatCompletionSettings.body + || codeCompletionsForm.codeCompletionsEnabled != codeCompletionSettings.codeCompletionsEnabled + || codeCompletionsForm.infillTemplate != codeCompletionSettings.infillTemplate + || codeCompletionsForm.url != codeCompletionSettings.url + || codeCompletionsForm.headers != codeCompletionSettings.headers + || codeCompletionsForm.body != codeCompletionSettings.body + || getApiKey() != getCredential(CredentialKey.CUSTOM_SERVICE_API_KEY) + } + + fun applyChanges() { + service().state.run { + template = templateComboBox.item + chatCompletionSettings = CustomServiceChatCompletionSettingsState().apply { + url = chatCompletionsForm.url + headers = chatCompletionsForm.headers + body = chatCompletionsForm.body + } + codeCompletionSettings = CustomServiceCodeCompletionSettingsState().apply { + codeCompletionsEnabled = codeCompletionsForm.codeCompletionsEnabled + infillTemplate = codeCompletionsForm.infillTemplate + url = codeCompletionsForm.url + headers = codeCompletionsForm.headers + body = codeCompletionsForm.body + } + } + } + + fun resetForm() { + service().state.run { + templateComboBox.item = template + chatCompletionsForm.resetForm(chatCompletionSettings) + codeCompletionsForm.resetForm(codeCompletionSettings) + } + } + + private fun updateTemplateHelpTextTooltip(template: CustomServiceTemplate) { + templateHelpText.toolTipText = null + try { + HelpTooltip() + .setTitle(template.providerName) + .setBrowserLink( + CodeGPTBundle.get("settingsConfigurable.service.custom.openai.linkToDocs"), + URL(template.docsUrl) + ) + .installOn(templateHelpText) + } catch (e: MalformedURLException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt new file mode 100644 index 00000000..aa9556d6 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt @@ -0,0 +1,74 @@ +package ee.carlrobert.codegpt.settings.service.custom + +import com.intellij.openapi.components.* +import com.intellij.util.xmlb.annotations.OptionTag +import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate +import ee.carlrobert.codegpt.util.MapConverter + +@Service +@State( + name = "CodeGPT_CustomServiceSettings", + storages = [Storage("CodeGPT_CustomServiceSettings.xml")] +) +class CustomServiceSettings : + SimplePersistentStateComponent(CustomServiceState()) { + + override fun loadState(state: CustomServiceState) { + this.state.run { + // Migrate old settings + if (state.url != null || state.body.isNotEmpty() || state.headers.isNotEmpty()) { + template = state.template + chatCompletionSettings.url = state.url + chatCompletionSettings.body = state.body + chatCompletionSettings.headers = state.headers + url = null + body = mutableMapOf() + headers = mutableMapOf() + } + } + } +} + +class CustomServiceState : BaseState() { + var template by enum(CustomServiceTemplate.OPENAI) + var chatCompletionSettings by property(CustomServiceChatCompletionSettingsState()) + var codeCompletionSettings by property(CustomServiceCodeCompletionSettingsState()) + + @Deprecated("", ReplaceWith("this.chatCompletionSettings.url")) + var url by string() + + @Deprecated("", ReplaceWith("this.chatCompletionSettings.headers")) + var headers by map() + + @get:OptionTag(converter = MapConverter::class) + @Deprecated("", ReplaceWith("this.chatCompletionSettings.body")) + var body by map() +} + +class CustomServiceChatCompletionSettingsState : BaseState() { + var url by string(CustomServiceChatCompletionTemplate.OPENAI.url) + var headers by map() + + @get:OptionTag(converter = MapConverter::class) + var body by map() + + init { + headers.putAll(CustomServiceChatCompletionTemplate.OPENAI.headers) + body.putAll(CustomServiceChatCompletionTemplate.OPENAI.body) + } +} + +class CustomServiceCodeCompletionSettingsState : BaseState() { + var codeCompletionsEnabled by property(true) + var infillTemplate by enum(InfillPromptTemplate.OPENAI) + var url by string(CustomServiceCodeCompletionTemplate.OPENAI.url) + var headers by map() + + @get:OptionTag(converter = MapConverter::class) + var body by map() + + init { + headers.putAll(CustomServiceCodeCompletionTemplate.OPENAI.headers) + body.putAll(CustomServiceCodeCompletionTemplate.OPENAI.body) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt new file mode 100644 index 00000000..2d837d4b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceTemplate.kt @@ -0,0 +1,69 @@ +package ee.carlrobert.codegpt.settings.service.custom + +enum class CustomServiceTemplate( + val providerName: String, + val docsUrl: String, + val chatCompletionTemplate: CustomServiceChatCompletionTemplate, + val codeCompletionTemplate: CustomServiceCodeCompletionTemplate? = null +) { + ANYSCALE( + "Anyscale", + "https://docs.endpoints.anyscale.com/", + CustomServiceChatCompletionTemplate.ANYSCALE, + CustomServiceCodeCompletionTemplate.ANYSCALE, + ), + AZURE( + "Azure OpenAI", + "https://learn.microsoft.com/en-us/azure/ai-services/openai/reference", + CustomServiceChatCompletionTemplate.AZURE, + CustomServiceCodeCompletionTemplate.AZURE + ), + DEEP_INFRA( + "DeepInfra", + "https://deepinfra.com/docs/advanced/openai_api", + CustomServiceChatCompletionTemplate.DEEP_INFRA, + CustomServiceCodeCompletionTemplate.DEEP_INFRA + ), + FIREWORKS( + "Fireworks", + "https://readme.fireworks.ai/reference/createchatcompletion", + CustomServiceChatCompletionTemplate.FIREWORKS, + CustomServiceCodeCompletionTemplate.FIREWORKS + ), + GROQ( + "Groq", + "https://docs.api.groq.com/md/openai.oas.html", + CustomServiceChatCompletionTemplate.GROQ + ), + OPENAI( + "OpenAI", + "https://platform.openai.com/docs/api-reference/chat", + CustomServiceChatCompletionTemplate.OPENAI, + CustomServiceCodeCompletionTemplate.OPENAI + ), + PERPLEXITY( + "Perplexity AI", + "https://docs.perplexity.ai/reference/post_chat_completions", + CustomServiceChatCompletionTemplate.PERPLEXITY + ), + TOGETHER( + "Together AI", + "https://docs.together.ai/docs/openai-api-compatibility", + CustomServiceChatCompletionTemplate.TOGETHER, + CustomServiceCodeCompletionTemplate.TOGETHER + ), + OLLAMA( + "Ollama", + "https://github.com/ollama/ollama/blob/main/docs/openai.md", + CustomServiceChatCompletionTemplate.OLLAMA + ), + LLAMA_CPP( + "LLaMA C/C++", + "https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md", + CustomServiceChatCompletionTemplate.LLAMA_CPP + ); + + override fun toString(): String { + return providerName + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 554d81b9..5c355356 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,8 +9,8 @@ - + @@ -33,7 +33,6 @@ - diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 2b0aa0fe..40764742 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -2,7 +2,7 @@ project.label=CodeGPT notification.group.name=CodeGPT notification group action.generateCommitMessage.title=Generate Message action.generateCommitMessage.description=Generate commit message -action.generateCommitMessage.serviceWarning=Messages can only be generated with OpenAI or Azure service +action.generateCommitMessage.serviceWarning=Messages can only be generated with OpenAI, Custom OpenAI, or Azure service action.generateCommitMessage.missingCredentials=Credentials not provided action.includeFilesInContext.title=Include In Context... action.includeFileInContext.title=Include File In Context... @@ -200,6 +200,7 @@ action.attachImage=Attach Image action.attachImageDescription=Attach an image imageFileChooser.title=Select Image imageAccordion.title=Attached image +shared.chatCompletions=Chat Completions shared.codeCompletions=Code Completions codeCompletionsForm.enableFeatureText=Enable code completions codeCompletionsForm.maxTokensLabel=Max tokens: From bcb33aeeeb59b9231de5c05edf134aee4f23898a Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 21 Apr 2024 00:32:41 +0300 Subject: [PATCH 17/24] docs: update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 033bc485..53b8e31a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,12 @@ Stuck on naming a method or variable? CodeGPT offers context-aware suggestions, ![Name Suggestions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/method-name-suggestions.png?raw=true) +### OpenAI Compatibility + +Interested in trying out 800t/s or getting access to new models as soon as they're released? We provide integration with most cloud providers that are OpenAI-compatible, such as Together.ai, Grok, Anyscale, and others, as well as the option to customize your own setup. + +![OpenAI Compatibility](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/openai-compatibility.png?raw=true) + ### Offline Development Support CodeGPT supports a completely offline development workflow by allowing you to connect to a locally hosted language model. This ensures that your code and data remain private and secure within your local environment, eliminating the need for an internet connection or sharing sensitive information with third-party servers. From 6e6a4991055da8cc31f91d2228afc3b18c5b7661 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Sun, 21 Apr 2024 00:12:13 +0200 Subject: [PATCH 18/24] feat: Support Llama 3 model (#479) * feat: Support Llama 3 model (#478) * Use new InfillPrompt * Switch to lmstudio-community * Use new Prompt * llama.cpp removed the BOS token https://github.com/ggerganov/llama.cpp/pull/6751/commits/a55d8a9348fc9e9215229bf03f96ecff4dcc7c91 * Add tests * I would prefer a stream based solution * Add 70B models * Add tests for skipping blank system prompt * Remove InfillPrompt for now --- .../codegpt/completions/HuggingFaceModel.java | 34 +++++++-- .../codegpt/completions/llama/LlamaModel.java | 19 ++++- .../completions/llama/PromptTemplate.java | 35 +++++++++ .../codecompletions/InfillPromptTemplate.kt | 2 +- .../codegpt/completions/PromptTemplateTest.kt | 72 ++++++++++++++++++- 5 files changed, 155 insertions(+), 7 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/HuggingFaceModel.java b/src/main/java/ee/carlrobert/codegpt/completions/HuggingFaceModel.java index e95c44f1..fc9f06b8 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/HuggingFaceModel.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/HuggingFaceModel.java @@ -43,16 +43,31 @@ public enum HuggingFaceModel { WIZARD_CODER_PYTHON_13B_Q5(13, 5, "WizardCoder-Python-13B-V1.0-GGUF"), WIZARD_CODER_PYTHON_34B_Q3(34, 3, "WizardCoder-Python-34B-V1.0-GGUF"), WIZARD_CODER_PYTHON_34B_Q4(34, 4, "WizardCoder-Python-34B-V1.0-GGUF"), - WIZARD_CODER_PYTHON_34B_Q5(34, 5, "WizardCoder-Python-34B-V1.0-GGUF"); + WIZARD_CODER_PYTHON_34B_Q5(34, 5, "WizardCoder-Python-34B-V1.0-GGUF"), + + LLAMA_3_8B_IQ3_M(8, 3, "Meta-Llama-3-8B-Instruct-IQ3_M.gguf", "lmstudio-community"), + LLAMA_3_8B_Q4_K_M(8, 4, "Meta-Llama-3-8B-Instruct-Q4_K_M.gguf", "lmstudio-community"), + LLAMA_3_8B_Q5_K_M(8, 5, "Meta-Llama-3-8B-Instruct-Q5_K_M.gguf", "lmstudio-community"), + LLAMA_3_8B_Q6_K(8, 6, "Meta-Llama-3-8B-Instruct-Q6_K.gguf", "lmstudio-community"), + LLAMA_3_8B_Q8_0(8, 8, "Meta-Llama-3-8B-Instruct-Q8_0.gguf", "lmstudio-community"), + LLAMA_3_70B_IQ1(70, 1, "Meta-Llama-3-70B-Instruct-IQ1_M.gguf", "lmstudio-community"), + LLAMA_3_70B_IQ2_XS(70, 2, "Meta-Llama-3-70B-Instruct-IQ2_XS.gguf", "lmstudio-community"), + LLAMA_3_70B_Q4_K_M(70, 4, "Meta-Llama-3-70B-Instruct-Q4_K_M.gguf", "lmstudio-community"); private final int parameterSize; private final int quantization; private final String modelName; + private final String user; HuggingFaceModel(int parameterSize, int quantization, String modelName) { + this(parameterSize, quantization, modelName, "TheBloke"); + } + + HuggingFaceModel(int parameterSize, int quantization, String modelName, String user) { this.parameterSize = parameterSize; this.quantization = quantization; this.modelName = modelName; + this.user = user; } public int getParameterSize() { @@ -68,13 +83,16 @@ public enum HuggingFaceModel { } public String getFileName() { - return modelName.toLowerCase().replace("-gguf", format(".Q%d_K_M.gguf", quantization)); + if ("TheBloke".equals(user)) { + return modelName.toLowerCase().replace("-gguf", format(".Q%d_K_M.gguf", quantization)); + } + return modelName; } public URL getFileURL() { try { return new URL( - format("https://huggingface.co/TheBloke/%s/resolve/main/%s", modelName, getFileName())); + "https://huggingface.co/%s/%s/resolve/main/%s".formatted(user, getDirectory(), getFileName())); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } @@ -82,12 +100,20 @@ public enum HuggingFaceModel { public URL getHuggingFaceURL() { try { - return new URL("https://huggingface.co/TheBloke/" + modelName); + return new URL("https://huggingface.co/%s/%s".formatted(user, getDirectory())); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } } + private String getDirectory() { + if ("lmstudio-community".equals(user)) { + // Meta-Llama-3-8B-Instruct-Q4_K_M.gguf -> Meta-Llama-3-8B-Instruct-GGUF + return modelName.replaceFirst("-[^.-]+\\.gguf$", "-GGUF"); + } + return modelName; + } + @Override public String toString() { return format("%d-bit precision", quantization); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaModel.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaModel.java index f596c62f..dc1288ba 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaModel.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaModel.java @@ -82,7 +82,24 @@ public enum LlamaModel { HuggingFaceModel.WIZARD_CODER_PYTHON_13B_Q5, HuggingFaceModel.WIZARD_CODER_PYTHON_34B_Q3, HuggingFaceModel.WIZARD_CODER_PYTHON_34B_Q4, - HuggingFaceModel.WIZARD_CODER_PYTHON_34B_Q5)); + HuggingFaceModel.WIZARD_CODER_PYTHON_34B_Q5)), + LLAMA_3( + "Llama 3", + "Llama 3 is a family of large language models (LLMs), a collection of pretrained and " + + "instruction tuned generative text models in 8 and 70B sizes. The Llama 3 instruction " + + "tuned models are optimized for dialogue use cases and outperform many of the available" + + " open source chat models on common industry benchmarks. Further, in developing these " + + "models, we took great care to optimize helpfulness and safety.", + PromptTemplate.LLAMA_3, + List.of( + HuggingFaceModel.LLAMA_3_8B_IQ3_M, + HuggingFaceModel.LLAMA_3_8B_Q4_K_M, + HuggingFaceModel.LLAMA_3_8B_Q5_K_M, + HuggingFaceModel.LLAMA_3_8B_Q6_K, + HuggingFaceModel.LLAMA_3_8B_Q8_0, + HuggingFaceModel.LLAMA_3_70B_IQ1, + HuggingFaceModel.LLAMA_3_70B_IQ2_XS, + HuggingFaceModel.LLAMA_3_70B_Q4_K_M)); private final String label; private final String description; diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java index a1509752..584539db 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java @@ -1,7 +1,11 @@ package ee.carlrobert.codegpt.completions.llama; +import static java.util.stream.Stream.concat; + import ee.carlrobert.codegpt.conversations.message.Message; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; public enum PromptTemplate { @@ -55,6 +59,26 @@ public enum PromptTemplate { .toString(); } }, + LLAMA_3("Llama 3") { + @Override + public String buildPrompt(String systemPrompt, String userPrompt, List history) { + return concat(concat(Stream.ofNullable(systemPrompt) + .filter(s -> !s.isBlank()) + .flatMap(system -> Stream.of( + "<|start_header_id|>system<|end_header_id|>\n\n", + system, + "<|eot_id|>")), + history.stream().flatMap(message -> mapMessage( + message, + "<|start_header_id|>user<|end_header_id|>\n\n", + "<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", + "<|eot_id|>"))), Stream.of( + "<|start_header_id|>user<|end_header_id|>\n\n", + userPrompt, + "<|eot_id|>")) + .collect(Collectors.joining()); + } + }, MIXTRAL_INSTRUCT("Mixtral Instruct") { @Override public String buildPrompt(String systemPrompt, String userPrompt, List history) { @@ -171,4 +195,15 @@ public enum PromptTemplate { public String toString() { return label; } + + private static Stream mapMessage(Message message, + String prefix, String infix, String suffix) { + return Stream.of( + prefix, + message.getPrompt(), + infix, + message.getResponse(), + suffix + ); + } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillPromptTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillPromptTemplate.kt index 9236adb6..ea68aa40 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillPromptTemplate.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillPromptTemplate.kt @@ -28,4 +28,4 @@ enum class InfillPromptTemplate(val label: String, val stopTokens: List? override fun toString(): String { return label } -} \ No newline at end of file +} diff --git a/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt index 8c7ff407..5e54468b 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt @@ -3,10 +3,14 @@ package ee.carlrobert.codegpt.completions import ee.carlrobert.codegpt.completions.llama.PromptTemplate.ALPACA import ee.carlrobert.codegpt.completions.llama.PromptTemplate.CHAT_ML import ee.carlrobert.codegpt.completions.llama.PromptTemplate.LLAMA +import ee.carlrobert.codegpt.completions.llama.PromptTemplate.LLAMA_3 import ee.carlrobert.codegpt.completions.llama.PromptTemplate.TORA import ee.carlrobert.codegpt.conversations.message.Message import org.assertj.core.api.Assertions.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.NullAndEmptySource +import org.junit.jupiter.params.provider.ValueSource class PromptTemplateTest { @@ -34,6 +38,72 @@ class PromptTemplateTest { """.trimIndent()) } + @Test + fun shouldBuildLlama3PromptWithoutHistory() { + val prompt = LLAMA_3.buildPrompt(SYSTEM_PROMPT, USER_PROMPT, listOf()) + + assertThat(prompt).isEqualTo(""" + <|start_header_id|>system<|end_header_id|> + + TEST_SYSTEM_PROMPT<|eot_id|><|start_header_id|>user<|end_header_id|> + + TEST_USER_PROMPT<|eot_id|>""".trimIndent() + ) + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = [" ", "\t", "\n"]) + fun shouldBuildLlama3PromptWithoutHistorySkippingBlankSystemPrompt(systemPrompt: String?) { + val prompt = LLAMA_3.buildPrompt(systemPrompt, USER_PROMPT, listOf()) + + assertThat(prompt).isEqualTo(""" + <|start_header_id|>user<|end_header_id|> + + TEST_USER_PROMPT<|eot_id|>""".trimIndent() + ) + } + + @Test + fun shouldBuildLlama3PromptWithHistory() { + val prompt = LLAMA_3.buildPrompt(SYSTEM_PROMPT, USER_PROMPT, HISTORY) + + assertThat(prompt).isEqualTo(""" + <|start_header_id|>system<|end_header_id|> + + TEST_SYSTEM_PROMPT<|eot_id|><|start_header_id|>user<|end_header_id|> + + TEST_PREV_PROMPT_1<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + TEST_PREV_RESPONSE_1<|eot_id|><|start_header_id|>user<|end_header_id|> + + TEST_PREV_PROMPT_2<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + TEST_PREV_RESPONSE_2<|eot_id|><|start_header_id|>user<|end_header_id|> + + TEST_USER_PROMPT<|eot_id|>""".trimIndent()) + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = [" ", "\t", "\n"]) + fun shouldBuildLlama3PromptWithHistorySkippingBlankSystemPrompt(systemPrompt: String?) { + val prompt = LLAMA_3.buildPrompt(systemPrompt, USER_PROMPT, HISTORY) + + assertThat(prompt).isEqualTo(""" + <|start_header_id|>user<|end_header_id|> + + TEST_PREV_PROMPT_1<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + TEST_PREV_RESPONSE_1<|eot_id|><|start_header_id|>user<|end_header_id|> + + TEST_PREV_PROMPT_2<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + TEST_PREV_RESPONSE_2<|eot_id|><|start_header_id|>user<|end_header_id|> + + TEST_USER_PROMPT<|eot_id|>""".trimIndent()) + } + @Test fun shouldBuildAlpacaPromptWithHistory() { val prompt = ALPACA.buildPrompt(SYSTEM_PROMPT, USER_PROMPT, HISTORY) From 39679d9ee949770a458efa1cae6f0b3eec6e3d6d Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 21 Apr 2024 01:39:26 +0300 Subject: [PATCH 19/24] fix: custom service settings sync --- .../settings/service/custom/CustomServiceSettings.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt index aa9556d6..baa46482 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/CustomServiceSettings.kt @@ -14,9 +14,9 @@ class CustomServiceSettings : SimplePersistentStateComponent(CustomServiceState()) { override fun loadState(state: CustomServiceState) { - this.state.run { - // Migrate old settings - if (state.url != null || state.body.isNotEmpty() || state.headers.isNotEmpty()) { + if (state.url != null || state.body.isNotEmpty() || state.headers.isNotEmpty()) { + super.loadState(this.state.apply { + // Migrate old settings template = state.template chatCompletionSettings.url = state.url chatCompletionSettings.body = state.body @@ -24,7 +24,9 @@ class CustomServiceSettings : url = null body = mutableMapOf() headers = mutableMapOf() - } + }) + } else { + super.loadState(state) } } } From a10b5f791a9413079b5d62afad8113a333d3490c Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Sun, 21 Apr 2024 16:12:14 +0200 Subject: [PATCH 20/24] feat: Upgrade submodule for Llama 3 support (#483) --- src/main/cpp/llama.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/cpp/llama.cpp b/src/main/cpp/llama.cpp index 594fca3f..7dbdba56 160000 --- a/src/main/cpp/llama.cpp +++ b/src/main/cpp/llama.cpp @@ -1 +1 @@ -Subproject commit 594fca3fefe27b8e95cfb1656eb0e160ad15a793 +Subproject commit 7dbdba5690ca61b3ee8c92cfac8e7e251042e787 From e8002a116c9feeaedb7bdee2848daa63ec371f0a Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 21 Apr 2024 17:38:57 +0300 Subject: [PATCH 21/24] docs: update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9859d871..2fca33de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for Llama 3 model via llama.cpp port (#479) +- Code completion for "Custom OpenAI Service" (#476) +- Support for configuring llama.cpp server build parameters (#481) +- "Include file in context" to editor context menu (#475) +- Support for placeholders in the commit message system prompt (#458) + +### Fixed + +- High CPU usage during new files check (#474) +- Persistence of credentials back into the PasswordSafe (#465) + ## [2.6.2-233] - 2024-04-15 ### Fixed From 62f0fa43bca7052005b9335731b3d16b8a61aaef Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 21 Apr 2024 18:00:15 +0300 Subject: [PATCH 22/24] docs: update plugin description --- DESCRIPTION.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 23b201ef..e9885289 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -42,6 +42,12 @@ Stuck on naming a method or variable? CodeGPT offers context-aware suggestions, ![Name Suggestions](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/method-name-suggestions.png?raw=true) +### OpenAI Compatibility + +Interested in trying out 800t/s or getting access to new models as soon as they're released? We provide integration with most cloud providers that are OpenAI-compatible, such as Together.ai, Grok, Anyscale, and others, as well as the option to customize your own setup. + +![OpenAI Compatibility](https://github.com/carlrobertoh/CodeGPT-docs/blob/main/images/openai-compatibility.png?raw=true) + ### Offline Development Support CodeGPT supports a completely offline development workflow by allowing you to connect to a locally hosted language model. This ensures that your code and data remain private and secure within your local environment, eliminating the need for an internet connection or sharing sensitive information with third-party servers. From 7899429d4fd5b2364746d124b2c14800dd248cb8 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 21 Apr 2024 23:01:33 +0300 Subject: [PATCH 23/24] fix: llama3 prompt --- .../CompletionRequestProvider.java | 1 + .../completions/llama/PromptTemplate.java | 70 ++++++++++--------- .../codegpt/completions/PromptTemplateTest.kt | 16 ++--- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 160d7189..58f28c3a 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -180,6 +180,7 @@ public class CompletionRequestProvider { .setTop_p(settings.getTopP()) .setMin_p(settings.getMinP()) .setRepeat_penalty(settings.getRepeatPenalty()) + .setStop(promptTemplate.getStopTokens()) .build(); } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java index 584539db..7101587a 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/PromptTemplate.java @@ -1,11 +1,9 @@ package ee.carlrobert.codegpt.completions.llama; -import static java.util.stream.Stream.concat; +import static java.util.Collections.emptyList; import ee.carlrobert.codegpt.conversations.message.Message; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; public enum PromptTemplate { @@ -59,24 +57,31 @@ public enum PromptTemplate { .toString(); } }, - LLAMA_3("Llama 3") { + LLAMA_3("Llama 3", List.of("<|eot_id|>")) { @Override public String buildPrompt(String systemPrompt, String userPrompt, List history) { - return concat(concat(Stream.ofNullable(systemPrompt) - .filter(s -> !s.isBlank()) - .flatMap(system -> Stream.of( - "<|start_header_id|>system<|end_header_id|>\n\n", - system, - "<|eot_id|>")), - history.stream().flatMap(message -> mapMessage( - message, - "<|start_header_id|>user<|end_header_id|>\n\n", - "<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", - "<|eot_id|>"))), Stream.of( - "<|start_header_id|>user<|end_header_id|>\n\n", - userPrompt, - "<|eot_id|>")) - .collect(Collectors.joining()); + var prompt = new StringBuilder("<|begin_of_text|>"); + if (systemPrompt != null && !systemPrompt.isBlank()) { + prompt + .append("<|start_header_id|>system<|end_header_id|>\n\n") + .append(systemPrompt) + .append("<|eot_id|>"); + } + + for (var message : history) { + prompt + .append("<|start_header_id|>user<|end_header_id|>\n\n") + .append(message.getPrompt()) + .append("<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n") + .append(message.getResponse()) + .append("<|eot_id|>"); + } + + return prompt + .append("<|start_header_id|>user<|end_header_id|>\n\n") + .append(userPrompt) + .append("<|eot_id|><|start_header_id|>assistant<|end_header_id|>") + .toString(); } }, MIXTRAL_INSTRUCT("Mixtral Instruct") { @@ -126,10 +131,10 @@ public enum PromptTemplate { StringBuilder prompt = new StringBuilder(); prompt.append(""" - Below is an instruction that describes a task. \ - Write a response that appropriately completes the request. + Below is an instruction that describes a task. \ + Write a response that appropriately completes the request. - """); + """); for (Message message : history) { prompt.append("### Instruction\n") @@ -184,26 +189,25 @@ public enum PromptTemplate { }; private final String label; + private final List stopTokens; PromptTemplate(String label) { + this(label, emptyList()); + } + + PromptTemplate(String label, List stopTokens) { this.label = label; + this.stopTokens = stopTokens; } public abstract String buildPrompt(String systemPrompt, String userPrompt, List history); + public List getStopTokens() { + return stopTokens; + } + @Override public String toString() { return label; } - - private static Stream mapMessage(Message message, - String prefix, String infix, String suffix) { - return Stream.of( - prefix, - message.getPrompt(), - infix, - message.getResponse(), - suffix - ); - } } diff --git a/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt index 5e54468b..84dad146 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/completions/PromptTemplateTest.kt @@ -43,11 +43,11 @@ class PromptTemplateTest { val prompt = LLAMA_3.buildPrompt(SYSTEM_PROMPT, USER_PROMPT, listOf()) assertThat(prompt).isEqualTo(""" - <|start_header_id|>system<|end_header_id|> + <|begin_of_text|><|start_header_id|>system<|end_header_id|> TEST_SYSTEM_PROMPT<|eot_id|><|start_header_id|>user<|end_header_id|> - TEST_USER_PROMPT<|eot_id|>""".trimIndent() + TEST_USER_PROMPT<|eot_id|><|start_header_id|>assistant<|end_header_id|>""".trimIndent() ) } @@ -58,9 +58,9 @@ class PromptTemplateTest { val prompt = LLAMA_3.buildPrompt(systemPrompt, USER_PROMPT, listOf()) assertThat(prompt).isEqualTo(""" - <|start_header_id|>user<|end_header_id|> + <|begin_of_text|><|start_header_id|>user<|end_header_id|> - TEST_USER_PROMPT<|eot_id|>""".trimIndent() + TEST_USER_PROMPT<|eot_id|><|start_header_id|>assistant<|end_header_id|>""".trimIndent() ) } @@ -69,7 +69,7 @@ class PromptTemplateTest { val prompt = LLAMA_3.buildPrompt(SYSTEM_PROMPT, USER_PROMPT, HISTORY) assertThat(prompt).isEqualTo(""" - <|start_header_id|>system<|end_header_id|> + <|begin_of_text|><|start_header_id|>system<|end_header_id|> TEST_SYSTEM_PROMPT<|eot_id|><|start_header_id|>user<|end_header_id|> @@ -81,7 +81,7 @@ class PromptTemplateTest { TEST_PREV_RESPONSE_2<|eot_id|><|start_header_id|>user<|end_header_id|> - TEST_USER_PROMPT<|eot_id|>""".trimIndent()) + TEST_USER_PROMPT<|eot_id|><|start_header_id|>assistant<|end_header_id|>""".trimIndent()) } @ParameterizedTest @@ -91,7 +91,7 @@ class PromptTemplateTest { val prompt = LLAMA_3.buildPrompt(systemPrompt, USER_PROMPT, HISTORY) assertThat(prompt).isEqualTo(""" - <|start_header_id|>user<|end_header_id|> + <|begin_of_text|><|start_header_id|>user<|end_header_id|> TEST_PREV_PROMPT_1<|eot_id|><|start_header_id|>assistant<|end_header_id|> @@ -101,7 +101,7 @@ class PromptTemplateTest { TEST_PREV_RESPONSE_2<|eot_id|><|start_header_id|>user<|end_header_id|> - TEST_USER_PROMPT<|eot_id|>""".trimIndent()) + TEST_USER_PROMPT<|eot_id|><|start_header_id|>assistant<|end_header_id|>""".trimIndent()) } @Test From ed9397c3dde571cf5703f7be4032e82a3613bd49 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sun, 21 Apr 2024 23:25:53 +0300 Subject: [PATCH 24/24] fix: llama server success callback trigger --- .../carlrobert/codegpt/completions/llama/LlamaServerAgent.java | 3 ++- .../codegpt/completions/llama/LlamaServerMessage.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java index a837150a..ee3854ed 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java @@ -157,7 +157,8 @@ public final class LlamaServerAgent implements Disposable { try { var serverMessage = objectMapper.readValue(event.getText(), LlamaServerMessage.class); - if ("HTTP server listening".equals(serverMessage.message())) { + // hack + if ("HTTP server listening".equals(serverMessage.msg())) { LOG.info("Server up and running!"); LlamaSettings.getCurrentState().setServerPort(port); diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerMessage.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerMessage.java index 0eec2be9..b3f683ab 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerMessage.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerMessage.java @@ -3,5 +3,5 @@ package ee.carlrobert.codegpt.completions.llama; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) -public record LlamaServerMessage(String level, String message) { +public record LlamaServerMessage(String level, String msg) { }