diff --git a/README.md b/README.md index 9dd8100..cff40e4 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ plugin and configure a LLM API client in plugin's settings: Settings - Open AI - Ollama - Qianfan (Ernie) +- GigaChat The plugin is implemented in a generic way and uses [langchain4j](https://github.com/langchain4j/langchain4j) for creating LLM API clients. If you would like to use some other LLM model that is supported by langchain4j, please make a feature request in GitHub issues. diff --git a/build.gradle.kts b/build.gradle.kts index 0c70eed..28d8776 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { implementation("dev.langchain4j:langchain4j-github-models") implementation("dev.langchain4j:langchain4j-mistral-ai") implementation("dev.langchain4j:langchain4j-bedrock") + implementation("chat.giga:langchain4j-gigachat:0.1.14") implementation(platform("dev.langchain4j:langchain4j-community-bom:1.7.1-beta14")) // The Baidu Qianfan Large Model Platform, including the ERNIE series, can be accessed at https://docs.langchain4j.dev/integrations/language-models/qianfan/. diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt index 12e2313..e675e6e 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt @@ -29,6 +29,7 @@ object Icons { val GITHUB = AICommitsIcon("/icons/github15bright.svg", "/icons/github15dark.svg") val MISTRAL = AICommitsIcon("/icons/mistral.svg", null) val AMAZON_BEDROCK = AICommitsIcon("/icons/amazonBedrock15.svg", "/icons/amazonBedrock15.svg") + val GIGACHAT = AICommitsIcon("/icons/gigachat15.svg", "/icons/gigachat15.svg") object Process { val STOP = AICommitsIcon("/icons/stop.svg", "/icons/stop_dark.svg") diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt index e01ea96..44531db 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt @@ -10,6 +10,7 @@ import com.github.blarc.ai.commits.intellij.plugin.settings.clients.anthropic.An import com.github.blarc.ai.commits.intellij.plugin.settings.clients.azureOpenAi.AzureOpenAiClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiGoogle.GeminiGoogleClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiVertex.GeminiClientConfiguration +import com.github.blarc.ai.commits.intellij.plugin.settings.clients.gigachat.GigachatClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.githubModels.GitHubModelsClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.huggingface.HuggingFaceClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.mistral.MistralAIClientConfiguration @@ -67,7 +68,8 @@ class AppSettings2 : PersistentStateComponent { HuggingFaceClientConfiguration::class, GitHubModelsClientConfiguration::class, MistralAIClientConfiguration::class, - AmazonBedrockClientConfiguration::class + AmazonBedrockClientConfiguration::class, + GigachatClientConfiguration::class ], style = XCollection.Style.v2 ) diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientService.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientService.kt index 510e30e..a164268 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientService.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientService.kt @@ -8,7 +8,6 @@ import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings2 import com.github.blarc.ai.commits.intellij.plugin.settings.ProjectSettings -import com.github.blarc.ai.commits.intellij.plugin.settings.clients.mistral.MistralAIClientSharedState import com.github.blarc.ai.commits.intellij.plugin.wrap import com.intellij.icons.AllIcons import com.intellij.openapi.application.EDT @@ -54,7 +53,7 @@ abstract class LLMClientService(private val cs: Coro makeRequestWithTryCatch(function = { val availableModels = getAvailableModels(client); - MistralAIClientSharedState.getInstance().modelIds.addAll(availableModels) + client.getSharedState().modelIds.addAll(availableModels) withContext(Dispatchers.EDT) { label.text = message("settings.refreshModels.success") diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt index a1e09ee..075a3d7 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt @@ -8,6 +8,7 @@ import com.github.blarc.ai.commits.intellij.plugin.settings.clients.anthropic.An import com.github.blarc.ai.commits.intellij.plugin.settings.clients.azureOpenAi.AzureOpenAiClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiGoogle.GeminiGoogleClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiVertex.GeminiClientConfiguration +import com.github.blarc.ai.commits.intellij.plugin.settings.clients.gigachat.GigachatClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.githubModels.GitHubModelsClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.huggingface.HuggingFaceClientConfiguration import com.github.blarc.ai.commits.intellij.plugin.settings.clients.mistral.MistralAIClientConfiguration @@ -159,7 +160,8 @@ class LLMClientTable { HuggingFaceClientConfiguration(), GitHubModelsClientConfiguration(), MistralAIClientConfiguration(), - AmazonBedrockClientConfiguration() + AmazonBedrockClientConfiguration(), + GigachatClientConfiguration() ).sortedBy { it.getClientName() } } else { listOf(newLLMClientConfiguration) diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientConfiguration.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientConfiguration.kt new file mode 100644 index 0000000..dadb2ba --- /dev/null +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientConfiguration.kt @@ -0,0 +1,93 @@ +package com.github.blarc.ai.commits.intellij.plugin.settings.clients.gigachat + +import chat.giga.model.Scope +import com.github.blarc.ai.commits.intellij.plugin.Icons +import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientConfiguration +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.Converter +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.OptionTag +import com.intellij.util.xmlb.annotations.Transient +import com.intellij.vcs.commit.AbstractCommitWorkflowHandler +import kotlinx.coroutines.Job +import javax.swing.Icon + +class GigachatClientConfiguration : LLMClientConfiguration ( + "Gigachat", + "GigaChat-2-Max", + "0.7" +) { + + @Attribute + var apiUrl: String = "https://gigachat.devices.sberbank.ru/api/v1" + @Attribute + var authUrl: String = "https://ngw.devices.sberbank.ru:9443/api/v2" + @Attribute + var timeout: Int = 60 + @Attribute + var tokenIsStored: Boolean = false + @Transient + var token: String? = null + + @OptionTag(converter = ScopeConverter::class) + var scope: Scope? = null + + companion object { + const val CLIENT_NAME = "Gigachat" + } + + override fun getClientName(): String { + return CLIENT_NAME + } + + override fun getClientIcon(): Icon { + return Icons.GIGACHAT.getThemeBasedIcon() + } + + override fun getSharedState(): GigachatClientSharedState { + return GigachatClientSharedState.getInstance() + } + + override fun generateCommitMessage(commitWorkflowHandler: AbstractCommitWorkflowHandler<*, *>, project: Project) { + return GigachatClientService.getInstance().generateCommitMessage(this, commitWorkflowHandler, project) + } + + override fun getGenerateCommitMessageJob(): Job? { + return GigachatClientService.getInstance().generateCommitMessageJob + } + + fun getAuthUrls(): Set { + return getSharedState().authUrls + } + + fun addAuthUrl(authUrl: String) { + getSharedState().authUrls.add(authUrl) + } + + override fun clone(): LLMClientConfiguration { + val copy = GigachatClientConfiguration() + copy.id = id + copy.name = name + copy.tokenIsStored = tokenIsStored + copy.apiUrl = apiUrl + copy.authUrl = authUrl + copy.scope = scope + copy.timeout = timeout + copy.modelId = modelId + copy.temperature = temperature + return copy + } + + override fun panel() = GigachatClientPanel(this) + + + class ScopeConverter : Converter() { + override fun toString(value: Scope): String? { + return value.toString() + } + + override fun fromString(value: String): Scope? { + return Scope.values().find { it.name == value } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientPanel.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientPanel.kt new file mode 100644 index 0000000..63ad587 --- /dev/null +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientPanel.kt @@ -0,0 +1,107 @@ +package com.github.blarc.ai.commits.intellij.plugin.settings.clients.gigachat + +import chat.giga.model.Scope +import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message +import com.github.blarc.ai.commits.intellij.plugin.emptyText +import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientPanel +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.components.JBPasswordField +import com.intellij.ui.dsl.builder.* +import org.jetbrains.kotlin.fir.resolve.scopeSessionKey + +class GigachatClientPanel(private val clientConfiguration: GigachatClientConfiguration) : + LLMClientPanel(clientConfiguration) { + private val tokenPasswordField = JBPasswordField() + + private val authUrlComboBox = ComboBox(clientConfiguration.getAuthUrls().toTypedArray()) + + private val scopeComboBox = ComboBox(Scope.values()) + + override fun create() = panel { + nameRow() + hostRow(clientConfiguration::apiUrl.toNullableProperty(), "settings.gigachat.host") + authUrlRow(clientConfiguration::authUrl.toNullableProperty()) + timeoutRow(clientConfiguration::timeout) + tokenRow() + modelIdRow() + scopeRow() + temperatureRow() + verifyRow() + } + + open fun Panel.scopeRow(labelKey: String = "settings.llmClient.modelId") { + row { + label(message("settings.gigachat.scope")) + .widthGroup("label") + cell(scopeComboBox) + .applyToComponent { + isEditable = false + } + .bindItem( + { clientConfiguration.scope }, + { + if (it != null) { + clientConfiguration.scope = it + } + }) + .align(Align.FILL) + .onApply { clientConfiguration.scope = scopeComboBox.item } + } + } + + private fun Panel.authUrlRow(property: MutableProperty, labelKey: String = "settings.gigachat.authUrl") { + row { + label(message("settings.gigachat.authUrl")) + .widthGroup("label") + cell(authUrlComboBox) + .applyToComponent { + isEditable = true + } + .bindItem(property) + .align(Align.FILL) + .onApply { clientConfiguration.addAuthUrl(authUrlComboBox.item) } + } + } + + override fun getRefreshModelsFunction() = fun() { + clientConfiguration.apiUrl = hostComboBox.item + clientConfiguration.authUrl = authUrlComboBox.item + clientConfiguration.modelId = modelComboBox.item + clientConfiguration.token = String(tokenPasswordField.password) + clientConfiguration.scope = scopeComboBox.item + GigachatClientService.getInstance().refreshModels(clientConfiguration, modelComboBox, verifyLabel) + } + + private fun Panel.tokenRow() { + row { + label(message("settings.llmClient.token")) + .widthGroup("label") + cell(tokenPasswordField) + .bindText(getter = { "" }, setter = { + GigachatClientService.getInstance().saveToken(clientConfiguration, it) + }) + .emptyText( + if (clientConfiguration.tokenIsStored) message("settings.llmClient.token.stored") else message( + "settings.gigachat.token.example" + ) + ) + .resizableColumn() + .align(Align.FILL) + // maxLineLength was eye-balled, but prevents the dialog getting wider + .comment(message("settings.gigachat.token.comment"), 50) + } + } + + override fun verifyConfiguration() { + + clientConfiguration.apiUrl = hostComboBox.item + clientConfiguration.authUrl = authUrlComboBox.item + clientConfiguration.timeout = socketTimeoutTextField.text.toInt() + clientConfiguration.modelId = modelComboBox.item + clientConfiguration.temperature = temperatureTextField.text + clientConfiguration.token = String(tokenPasswordField.password) + clientConfiguration.scope = scopeComboBox.item + + GigachatClientService.getInstance().verifyConfiguration(clientConfiguration, verifyLabel) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientService.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientService.kt new file mode 100644 index 0000000..f63548a --- /dev/null +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientService.kt @@ -0,0 +1,116 @@ +package com.github.blarc.ai.commits.intellij.plugin.settings.clients.gigachat + +import chat.giga.client.GigaChatClientImpl +import chat.giga.client.auth.AuthClient +import chat.giga.client.auth.AuthClientBuilder +import chat.giga.client.auth.AuthClientBuilder.OAuthBuilder +import chat.giga.langchain4j.GigaChatChatModel +import chat.giga.langchain4j.GigaChatChatRequestParameters +import chat.giga.langchain4j.GigaChatStreamingChatModel +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.getCredentialAttributes +import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification +import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification +import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientService +import com.intellij.ide.passwordSafe.PasswordSafe +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import dev.langchain4j.model.chat.ChatModel +import dev.langchain4j.model.chat.StreamingChatModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Service(Service.Level.APP) +class GigachatClientService(private val cs: CoroutineScope) : LLMClientService(cs) { + + companion object { + @JvmStatic + fun getInstance(): GigachatClientService = service() + } + + override suspend fun buildChatModel(client: GigachatClientConfiguration): ChatModel { + return GigaChatChatModel.builder() + .apiUrl(client.apiUrl) + .defaultChatRequestParameters( + GigaChatChatRequestParameters.builder() + .modelName(client.modelId) + .build() + ) + .authClient( + AuthClient.builder() + .withOAuth( + OAuthBuilder.builder() + .scope(client.scope) + .authKey(client.token) + .authApiUrl(client.authUrl) + .verifySslCerts(false) + .build() + ) + .build() + ) + .logRequests(true) + .logResponses(true) + .build() + } + + override suspend fun buildStreamingChatModel(client: GigachatClientConfiguration): StreamingChatModel? { + return GigaChatStreamingChatModel.builder() + .apiUrl(client.apiUrl) + .defaultChatRequestParameters( + GigaChatChatRequestParameters.builder() + .modelName(client.modelId) + .temperature(client.temperature.toDouble()) + .build() + ) + .authClient( + AuthClient.builder() + .withOAuth( + OAuthBuilder.builder() + .scope(client.scope) + .authKey(client.token) + .authApiUrl(client.authUrl) + .build() + ) + .build() + ) + .logRequests(true) + .logResponses(true) + .build() + } + + fun saveToken(client: GigachatClientConfiguration, token: String) { + cs.launch(Dispatchers.Default) { + try { + PasswordSafe.instance.setPassword(getCredentialAttributes(client.id), token) + client.tokenIsStored = true + } catch (e: Exception) { + sendNotification(Notification.unableToSaveToken(e.message)) + } + } + } + + override suspend fun getAvailableModels(client: GigachatClientConfiguration): List { + val gigaChatClient1 = GigaChatClientImpl.builder() + .apiUrl(client.apiUrl) + .authClient( + AuthClientBuilder.builder() + .withOAuth( + OAuthBuilder.builder() + .scope(client.scope) + .authKey(client.token) + .authApiUrl(client.authUrl) + .build() + ) + .build() + ) + .build() + + val availableModels = withContext(Dispatchers.IO) { + gigaChatClient1.models().data() + } + return availableModels + .filter { it.type().equals("chat") } + .map { it.id() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientSharedState.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientSharedState.kt new file mode 100644 index 0000000..3102b9f --- /dev/null +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/gigachat/GigachatClientSharedState.kt @@ -0,0 +1,36 @@ +package com.github.blarc.ai.commits.intellij.plugin.settings.clients.gigachat + +import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientSharedState +import com.intellij.openapi.components.* +import com.intellij.util.xmlb.annotations.XCollection + +@Service(Service.Level.APP) +@State(name = "GigachatClientSharedState", storages = [Storage("AICommitsGigachat.xml")]) +class GigachatClientSharedState : PersistentStateComponent, LLMClientSharedState { + + companion object { + @JvmStatic + fun getInstance(): GigachatClientSharedState = service() + + val MODELS: List = listOf("GigaChat", "GigaChat-2", "GigaChat-Pro", "GigaChat-2-Pro", "GigaChat-Max", "GigaChat-2-Max") + + } + + @XCollection(style = XCollection.Style.v2) + override val hosts = mutableSetOf("https://gigachat.devices.sberbank.ru/api/v1") + + @XCollection(style = XCollection.Style.v2) + val authUrls = mutableSetOf("https://ngw.devices.sberbank.ru:9443/api/v2") + + @XCollection(style = XCollection.Style.v2) + override val modelIds = MODELS.toMutableSet() + + override fun getState(): GigachatClientSharedState = this + + override fun loadState(state: GigachatClientSharedState) { + // Add all model IDs from enum in case they are not stored in xml + modelIds += state.modelIds + hosts += state.hosts + authUrls += state.authUrls + } +} \ No newline at end of file diff --git a/src/main/resources/icons/gigachat15.svg b/src/main/resources/icons/gigachat15.svg new file mode 100644 index 0000000..7072cd1 --- /dev/null +++ b/src/main/resources/icons/gigachat15.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/messages/AiCommitsBundle.properties b/src/main/resources/messages/AiCommitsBundle.properties index aa58b9e..6997d7b 100644 --- a/src/main/resources/messages/AiCommitsBundle.properties +++ b/src/main/resources/messages/AiCommitsBundle.properties @@ -104,6 +104,11 @@ settings.openAI.token.example=sk-ABCdefgHIjKlxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx settings.openAi.token.comment=You can get your token here. settings.openAi.organizationId=Organization ID +settings.gigachat.token.example=sk-ABCdefgHIjKlxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +settings.gigachat.token.comment=You can get your token here. +settings.gigachat.authUrl=Auth URL +settings.gigachat.scope=Scope + settings.ollama.numCtx=Num ctx settings.ollama.numCtx.comment=This controls how many tokens the LLM can use as context to generate the next token. settings.ollama.numPredict=Num predict