From d04e5bc967907b7372fc9f48d2fc1aae7c683124 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:22:01 +0100 Subject: [PATCH] Add Claude provider and templates for chat and code (#55) * feat: Add provider settings * feat: Add Claude provider * feat: Add Claude templates * refactor: Setting input sensitivity * fix: Back text after read code block * fix: Add missing system message for ollama fim --- CMakeLists.txt | 2 + ChatView/ClientInterface.cpp | 2 +- CodeHandler.cpp | 13 ++ LLMClientInterface.cpp | 2 +- QodeAssistClient.cpp | 84 ++++++---- llmcore/MessageBuilder.cpp | 1 + llmcore/MessageBuilder.hpp | 2 +- llmcore/Provider.hpp | 5 +- llmcore/RequestHandler.cpp | 12 +- llmcore/RequestHandler.hpp | 1 - providers/ClaudeProvider.cpp | 238 ++++++++++++++++++++++++++++ providers/ClaudeProvider.hpp | 44 +++++ providers/LMStudioProvider.cpp | 10 ++ providers/LMStudioProvider.hpp | 2 + providers/OllamaProvider.cpp | 10 ++ providers/OllamaProvider.hpp | 2 + providers/OpenAICompatProvider.cpp | 16 ++ providers/OpenAICompatProvider.hpp | 2 + providers/OpenRouterAIProvider.cpp | 7 + providers/OpenRouterAIProvider.hpp | 1 + providers/Providers.hpp | 2 + settings/CMakeLists.txt | 1 + settings/CodeCompletionSettings.cpp | 12 +- settings/ProviderSettings.cpp | 136 ++++++++++++++++ settings/ProviderSettings.hpp | 47 ++++++ settings/SettingsConstants.hpp | 11 ++ templates/Claude.hpp | 71 +++++++++ templates/Templates.hpp | 3 + 28 files changed, 683 insertions(+), 56 deletions(-) create mode 100644 providers/ClaudeProvider.cpp create mode 100644 providers/ClaudeProvider.hpp create mode 100644 settings/ProviderSettings.cpp create mode 100644 settings/ProviderSettings.hpp create mode 100644 templates/Claude.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 324c9c0..ea2af50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,10 +53,12 @@ add_qtc_plugin(QodeAssist templates/Alpaca.hpp templates/Llama2.hpp providers/Providers.hpp + templates/Claude.hpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp + providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp QodeAssist.qrc LSPCompletion.hpp LLMSuggestion.hpp LLMSuggestion.cpp diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index c58e485..5e87b70 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -126,7 +126,7 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint()); config.providerRequest = providerRequest; config.multiLineCompletion = false; - config.apiKey = Settings::chatAssistantSettings().apiKey(); + config.apiKey = provider->apiKey(); QJsonObject request; request["id"] = QUuid::createUuid().toString(); diff --git a/CodeHandler.cpp b/CodeHandler.cpp index 29cca2f..3b1b22c 100644 --- a/CodeHandler.cpp +++ b/CodeHandler.cpp @@ -64,6 +64,19 @@ QString CodeHandler::processText(QString text) } } + if (!pendingComments.isEmpty()) { + QStringList commentLines = pendingComments.split('\n'); + QString commentPrefix = getCommentPrefix(currentLanguage); + + for (const QString &commentLine : commentLines) { + if (!commentLine.trimmed().isEmpty()) { + result += commentPrefix + " " + commentLine.trimmed() + "\n"; + } else { + result += "\n"; + } + } + } + return result; } diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index d660ab2..1a796fe 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -176,7 +176,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) Settings::generalSettings().ccUrl(), promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint() : provider->chatEndpoint())); - config.apiKey = Settings::codeCompletionSettings().apiKey(); + config.apiKey = provider->apiKey(); config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 9d17ea9..f83f8e1 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -71,48 +71,62 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) return; Client::openDocument(document); - connect(document, - &TextDocument::contentsChangedWithPosition, - this, - [this, document](int position, int charsRemoved, int charsAdded) { - Q_UNUSED(charsRemoved) - if (!Settings::codeCompletionSettings().autoCompletion()) - return; + connect( + document, + &TextDocument::contentsChangedWithPosition, + this, + [this, document](int position, int charsRemoved, int charsAdded) { + if (!Settings::codeCompletionSettings().autoCompletion()) + return; - auto project = ProjectManager::projectForFile(document->filePath()); - if (!isEnabled(project)) - return; + auto project = ProjectManager::projectForFile(document->filePath()); + if (!isEnabled(project)) + return; - auto textEditor = BaseTextEditor::currentTextEditor(); - if (!textEditor || textEditor->document() != document) - return; + auto textEditor = BaseTextEditor::currentTextEditor(); + if (!textEditor || textEditor->document() != document) + return; - if (Settings::codeCompletionSettings().useProjectChangesCache()) - ChangesManager::instance().addChange(document, - position, - charsRemoved, - charsAdded); + if (Settings::codeCompletionSettings().useProjectChangesCache()) + ChangesManager::instance().addChange(document, position, charsRemoved, charsAdded); - TextEditorWidget *widget = textEditor->editorWidget(); - if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) - return; - const int cursorPosition = widget->textCursor().position(); - if (cursorPosition < position || cursorPosition > position + charsAdded) - return; + TextEditorWidget *widget = textEditor->editorWidget(); + if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) + return; - m_recentCharCount += charsAdded; + const int cursorPosition = widget->textCursor().position(); + if (cursorPosition < position || cursorPosition > position + charsAdded) + return; - if (m_typingTimer.elapsed() - > Settings::codeCompletionSettings().autoCompletionTypingInterval()) { - m_recentCharCount = charsAdded; - m_typingTimer.restart(); - } + if (charsRemoved > 0 || charsAdded <= 0) { + m_recentCharCount = 0; + m_typingTimer.restart(); + return; + } - if (m_recentCharCount - > Settings::codeCompletionSettings().autoCompletionCharThreshold()) { - scheduleRequest(widget); - } - }); + QTextCursor cursor = widget->textCursor(); + cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); + QString lastChar = cursor.selectedText(); + + if (lastChar.isEmpty() || lastChar[0].isPunct()) { + m_recentCharCount = 0; + m_typingTimer.restart(); + return; + } + + m_recentCharCount += charsAdded; + + if (m_typingTimer.elapsed() + > Settings::codeCompletionSettings().autoCompletionTypingInterval()) { + m_recentCharCount = charsAdded; + m_typingTimer.restart(); + } + + if (m_recentCharCount + > Settings::codeCompletionSettings().autoCompletionCharThreshold()) { + scheduleRequest(widget); + } + }); } bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project) diff --git a/llmcore/MessageBuilder.cpp b/llmcore/MessageBuilder.cpp index b4902cf..9c65840 100644 --- a/llmcore/MessageBuilder.cpp +++ b/llmcore/MessageBuilder.cpp @@ -72,6 +72,7 @@ void QodeAssist::LLMCore::MessageBuilder::saveTo(QJsonObject &request, Providers if (api == ProvidersApi::Ollama) { if (m_promptTemplate->type() == TemplateType::Fim) { + request["system"] = m_systemMessage; m_promptTemplate->prepareRequest(request, context); } else { QJsonArray messages; diff --git a/llmcore/MessageBuilder.hpp b/llmcore/MessageBuilder.hpp index fef5a78..8b0b734 100644 --- a/llmcore/MessageBuilder.hpp +++ b/llmcore/MessageBuilder.hpp @@ -32,7 +32,7 @@ enum class MessageRole { System, User, Assistant }; enum class OllamaFormat { Messages, Completions }; -enum class ProvidersApi { Ollama, OpenAI }; +enum class ProvidersApi { Ollama, OpenAI, Claude }; static const QString ROLE_SYSTEM = "system"; static const QString ROLE_USER = "user"; diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp index ae13019..9ff1451 100644 --- a/llmcore/Provider.hpp +++ b/llmcore/Provider.hpp @@ -19,8 +19,9 @@ #pragma once -#include #include +#include +#include #include "PromptTemplate.hpp" #include "RequestType.hpp" @@ -45,6 +46,8 @@ public: virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0; virtual QList getInstalledModels(const QString &url) = 0; virtual QList validateRequest(const QJsonObject &request, TemplateType type) = 0; + virtual QString apiKey() const = 0; + virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0; }; } // namespace QodeAssist::LLMCore diff --git a/llmcore/RequestHandler.cpp b/llmcore/RequestHandler.cpp index 3855e6d..b57d349 100644 --- a/llmcore/RequestHandler.cpp +++ b/llmcore/RequestHandler.cpp @@ -38,7 +38,7 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject & QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented)))); QNetworkRequest networkRequest(config.url); - prepareNetworkRequest(networkRequest, config.apiKey); + config.provider->prepareNetworkRequest(networkRequest); QNetworkReply *reply = m_manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson()); @@ -108,16 +108,6 @@ bool RequestHandler::cancelRequest(const QString &id) return false; } -void RequestHandler::prepareNetworkRequest( - QNetworkRequest &networkRequest, const QString &apiKey) const -{ - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - if (!apiKey.isEmpty()) { - networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8()); - } -} - bool RequestHandler::processSingleLineCompletion( QNetworkReply *reply, const QJsonObject &request, diff --git a/llmcore/RequestHandler.hpp b/llmcore/RequestHandler.hpp index 3b484af..ff1bf74 100644 --- a/llmcore/RequestHandler.hpp +++ b/llmcore/RequestHandler.hpp @@ -52,7 +52,6 @@ private: QMap m_activeRequests; QMap m_accumulatedResponses; - void prepareNetworkRequest(QNetworkRequest &networkRequest, const QString &apiKey) const; bool processSingleLineCompletion(QNetworkReply *reply, const QJsonObject &request, const QString &accumulatedResponse, diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp new file mode 100644 index 0000000..426024c --- /dev/null +++ b/providers/ClaudeProvider.cpp @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2024 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "ClaudeProvider.hpp" + +#include +#include +#include +#include +#include +#include + +#include "llmcore/ValidationUtils.hpp" +#include "logger/Logger.hpp" +#include "settings/ChatAssistantSettings.hpp" +#include "settings/CodeCompletionSettings.hpp" +#include "settings/GeneralSettings.hpp" +#include "settings/ProviderSettings.hpp" + +namespace QodeAssist::Providers { + +ClaudeProvider::ClaudeProvider() {} + +QString ClaudeProvider::name() const +{ + return "Claude"; +} + +QString ClaudeProvider::url() const +{ + return "https://api.anthropic.com"; +} + +QString ClaudeProvider::completionEndpoint() const +{ + return "/v1/messages"; +} + +QString ClaudeProvider::chatEndpoint() const +{ + return "/v1/messages"; +} + +bool ClaudeProvider::supportsModelListing() const +{ + return true; +} + +void ClaudeProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type) +{ + auto prepareMessages = [](QJsonObject &req) -> QJsonArray { + QJsonArray messages; + if (req.contains("messages")) { + QJsonArray origMessages = req["messages"].toArray(); + for (const auto &msg : origMessages) { + QJsonObject message = msg.toObject(); + if (message["role"].toString() == "system") { + req["system"] = message["content"]; + } else { + messages.append(message); + } + } + } else { + if (req.contains("system")) { + req["system"] = req["system"].toString(); + } + if (req.contains("prompt")) { + messages.append( + QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}}); + } + } + return messages; + }; + + auto applyModelParams = [&request](const auto &settings) { + request["max_tokens"] = settings.maxTokens(); + request["temperature"] = settings.temperature(); + if (settings.useTopP()) + request["top_p"] = settings.topP(); + if (settings.useTopK()) + request["top_k"] = settings.topK(); + request["stream"] = true; + }; + + QJsonArray messages = prepareMessages(request); + if (!messages.isEmpty()) { + request["messages"] = std::move(messages); + } + + if (type == LLMCore::RequestType::CodeCompletion) { + applyModelParams(Settings::codeCompletionSettings()); + } else { + applyModelParams(Settings::chatAssistantSettings()); + } +} + +bool ClaudeProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse) +{ + bool isComplete = false; + QString tempResponse; + + while (reply->canReadLine()) { + QByteArray line = reply->readLine().trimmed(); + if (line.isEmpty()) { + continue; + } + + if (!line.startsWith("data:")) { + continue; + } + + line = line.mid(6); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(line); + if (jsonResponse.isNull()) { + continue; + } + + QJsonObject responseObj = jsonResponse.object(); + QString eventType = responseObj["type"].toString(); + + if (eventType == "message_delta") { + if (responseObj.contains("delta")) { + QJsonObject delta = responseObj["delta"].toObject(); + if (delta.contains("stop_reason")) { + isComplete = true; + } + } + } else if (eventType == "content_block_delta") { + QJsonObject delta = responseObj["delta"].toObject(); + if (delta["type"].toString() == "text_delta") { + tempResponse += delta["text"].toString(); + } + } + } + + if (!tempResponse.isEmpty()) { + accumulatedResponse += tempResponse; + } + + return isComplete; +} + +QList ClaudeProvider::getInstalledModels(const QString &baseUrl) +{ + QList models; + QNetworkAccessManager manager; + + QUrl url(baseUrl + "/v1/models"); + QUrlQuery query; + query.addQueryItem("limit", "1000"); + url.setQuery(query); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("anthropic-version", "2023-06-01"); + + if (!apiKey().isEmpty()) { + request.setRawHeader("x-api-key", apiKey().toUtf8()); + } + + QNetworkReply *reply = manager.get(request); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() == QNetworkReply::NoError) { + QByteArray responseData = reply->readAll(); + QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData); + QJsonObject jsonObject = jsonResponse.object(); + + if (jsonObject.contains("data")) { + QJsonArray modelArray = jsonObject["data"].toArray(); + for (const QJsonValue &value : modelArray) { + QJsonObject modelObject = value.toObject(); + if (modelObject.contains("id")) { + QString modelId = modelObject["id"].toString(); + models.append(modelId); + } + } + } + } else { + LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString())); + } + + reply->deleteLater(); + return models; +} + +QList ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) +{ + const auto templateReq = QJsonObject{ + {"model", {}}, + {"system", {}}, + {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, + {"temperature", {}}, + {"max_tokens", {}}, + {"anthropic-version", {}}, + {"top_p", {}}, + {"top_k", {}}, + {"stop", QJsonArray{}}, + {"stream", {}}}; + + return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); +} + +QString ClaudeProvider::apiKey() const +{ + return Settings::providerSettings().claudeApiKey(); +} + +void ClaudeProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +{ + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + if (!apiKey().isEmpty()) { + networkRequest.setRawHeader("x-api-key", apiKey().toUtf8()); + networkRequest.setRawHeader("anthropic-version", "2023-06-01"); + } +} + +} // namespace QodeAssist::Providers diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp new file mode 100644 index 0000000..9d55cc3 --- /dev/null +++ b/providers/ClaudeProvider.hpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include "llmcore/Provider.hpp" + +namespace QodeAssist::Providers { + +class ClaudeProvider : public LLMCore::Provider +{ +public: + ClaudeProvider(); + + QString name() const override; + QString url() const override; + QString completionEndpoint() const override; + QString chatEndpoint() const override; + bool supportsModelListing() const override; + void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; + bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; + QList getInstalledModels(const QString &url) override; + QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + QString apiKey() const override; + void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; +}; + +} // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index 8150583..4dc1d07 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -188,4 +188,14 @@ QList LMStudioProvider::validateRequest( return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); } +QString LMStudioProvider::apiKey() const +{ + return {}; +} + +void LMStudioProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +{ + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); +} + } // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.hpp b/providers/LMStudioProvider.hpp index ae9d355..3c28cdb 100644 --- a/providers/LMStudioProvider.hpp +++ b/providers/LMStudioProvider.hpp @@ -37,6 +37,8 @@ public: bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + QString apiKey() const override; + void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; }; } // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index 49f693b..d08be2f 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -175,6 +175,16 @@ QList OllamaProvider::validateRequest(const QJsonObject &request, LLMCo return LLMCore::ValidationUtils::validateRequestFields( request, type == LLMCore::TemplateType::Fim ? fimReq : messageReq); +} + +QString OllamaProvider::apiKey() const +{ + return {}; +} + +void OllamaProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +{ + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); }; } // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp index 41c2768..f963fb0 100644 --- a/providers/OllamaProvider.hpp +++ b/providers/OllamaProvider.hpp @@ -37,6 +37,8 @@ public: bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + QString apiKey() const override; + void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index 785d986..2e0f9ab 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -18,8 +18,10 @@ */ #include "OpenAICompatProvider.hpp" + #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/ProviderSettings.hpp" #include #include @@ -161,4 +163,18 @@ QList OpenAICompatProvider::validateRequest( return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); } +QString OpenAICompatProvider::apiKey() const +{ + return Settings::providerSettings().openAiCompatApiKey(); +} + +void OpenAICompatProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +{ + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + if (!apiKey().isEmpty()) { + networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); + } +} + } // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.hpp b/providers/OpenAICompatProvider.hpp index 202d7c3..c6abebc 100644 --- a/providers/OpenAICompatProvider.hpp +++ b/providers/OpenAICompatProvider.hpp @@ -37,6 +37,8 @@ public: bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; QList getInstalledModels(const QString &url) override; QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + QString apiKey() const override; + void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenRouterAIProvider.cpp b/providers/OpenRouterAIProvider.cpp index 0435cb8..99fd94f 100644 --- a/providers/OpenRouterAIProvider.cpp +++ b/providers/OpenRouterAIProvider.cpp @@ -18,8 +18,10 @@ */ #include "OpenRouterAIProvider.hpp" + #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" +#include "settings/ProviderSettings.hpp" #include #include @@ -123,4 +125,9 @@ bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulat return false; } +QString OpenRouterProvider::apiKey() const +{ + return Settings::providerSettings().openRouterApiKey(); +} + } // namespace QodeAssist::Providers diff --git a/providers/OpenRouterAIProvider.hpp b/providers/OpenRouterAIProvider.hpp index 6f06945..a49e9b7 100644 --- a/providers/OpenRouterAIProvider.hpp +++ b/providers/OpenRouterAIProvider.hpp @@ -33,6 +33,7 @@ public: QString url() const override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; + QString apiKey() const override; }; } // namespace QodeAssist::Providers diff --git a/providers/Providers.hpp b/providers/Providers.hpp index cd4606a..a287adf 100644 --- a/providers/Providers.hpp +++ b/providers/Providers.hpp @@ -20,6 +20,7 @@ #pragma once #include "llmcore/ProvidersManager.hpp" +#include "providers/ClaudeProvider.hpp" #include "providers/LMStudioProvider.hpp" #include "providers/OllamaProvider.hpp" #include "providers/OpenAICompatProvider.hpp" @@ -34,6 +35,7 @@ inline void registerProviders() providerManager.registerProvider(); providerManager.registerProvider(); providerManager.registerProvider(); + providerManager.registerProvider(); } } // namespace QodeAssist::Providers diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index 7161e84..65cd398 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(QodeAssistSettings STATIC SettingsDialog.hpp SettingsDialog.cpp ProjectSettings.hpp ProjectSettings.cpp ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp + ProviderSettings.hpp ProviderSettings.cpp ) target_link_libraries(QodeAssistSettings diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp index 9f26214..a6a4fc1 100644 --- a/settings/CodeCompletionSettings.cpp +++ b/settings/CodeCompletionSettings.cpp @@ -62,7 +62,7 @@ CodeCompletionSettings::CodeCompletionSettings() startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER); startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)")); startSuggestionTimer.setRange(10, 10000); - startSuggestionTimer.setDefaultValue(500); + startSuggestionTimer.setDefaultValue(350); autoCompletionCharThreshold.setSettingsKey(Constants::СС_AUTO_COMPLETION_CHAR_THRESHOLD); autoCompletionCharThreshold.setLabelText(Tr::tr("AI suggestion triggers after typing")); @@ -70,7 +70,7 @@ CodeCompletionSettings::CodeCompletionSettings() Tr::tr("The number of characters that need to be typed within the typing interval " "before an AI suggestion request is sent.")); autoCompletionCharThreshold.setRange(0, 10); - autoCompletionCharThreshold.setDefaultValue(0); + autoCompletionCharThreshold.setDefaultValue(1); autoCompletionTypingInterval.setSettingsKey(Constants::СС_AUTO_COMPLETION_TYPING_INTERVAL); autoCompletionTypingInterval.setLabelText(Tr::tr("character(s) within(ms)")); @@ -78,7 +78,7 @@ CodeCompletionSettings::CodeCompletionSettings() Tr::tr("The time window (in milliseconds) during which the character threshold " "must be met to trigger an AI suggestion request.")); autoCompletionTypingInterval.setRange(500, 5000); - autoCompletionTypingInterval.setDefaultValue(2000); + autoCompletionTypingInterval.setDefaultValue(1200); // General Parameters Settings temperature.setSettingsKey(Constants::CC_TEMPERATURE); @@ -151,8 +151,10 @@ CodeCompletionSettings::CodeCompletionSettings() systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT); systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay); - systemPrompt.setDefaultValue("You are an expert C++, Qt, and QML code completion AI. Answer " - "should be ONLY in CODE and without repeating current."); + systemPrompt.setDefaultValue( + "You are an expert C++, Qt, and QML. You insert the code into the areas where the user " + "specifies. In answer should be ONLY code suggestions in code block, without comments or " + "description. Don't repeat existing code. Complete ONLY one logic expression."); useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT); useFilePathInContext.setDefaultValue(true); diff --git a/settings/ProviderSettings.cpp b/settings/ProviderSettings.cpp new file mode 100644 index 0000000..de2b1cd --- /dev/null +++ b/settings/ProviderSettings.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "ProviderSettings.hpp" + +#include +#include +#include +#include + +#include "SettingsConstants.hpp" +#include "SettingsTr.hpp" +#include "SettingsUtils.hpp" + +namespace QodeAssist::Settings { + +ProviderSettings &providerSettings() +{ + static ProviderSettings settings; + return settings; +} + +ProviderSettings::ProviderSettings() +{ + setAutoApply(false); + + setDisplayName(Tr::tr("Provider Settings")); + + // OpenRouter Settings + openRouterApiKey.setSettingsKey(Constants::OPEN_ROUTER_API_KEY); + openRouterApiKey.setLabelText(Tr::tr("OpenRouter API Key:")); + openRouterApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + openRouterApiKey.setPlaceHolderText(Tr::tr("Enter your API key here")); + openRouterApiKey.setHistoryCompleter(Constants::OPEN_ROUTER_API_KEY_HISTORY); + openRouterApiKey.setDefaultValue(""); + openRouterApiKey.setAutoApply(true); + + // OpenAI Compatible Settings + openAiCompatApiKey.setSettingsKey(Constants::OPEN_AI_COMPAT_API_KEY); + openAiCompatApiKey.setLabelText(Tr::tr("OpenAI Compatible API Key:")); + openAiCompatApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + openAiCompatApiKey.setPlaceHolderText(Tr::tr("Enter your API key here")); + openAiCompatApiKey.setHistoryCompleter(Constants::OPEN_AI_COMPAT_API_KEY_HISTORY); + openAiCompatApiKey.setDefaultValue(""); + openAiCompatApiKey.setAutoApply(true); + + // Claude Compatible Settings + claudeApiKey.setSettingsKey(Constants::CLAUDE_API_KEY); + claudeApiKey.setLabelText(Tr::tr("Claude API Key:")); + claudeApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + claudeApiKey.setPlaceHolderText(Tr::tr("Enter your API key here")); + claudeApiKey.setHistoryCompleter(Constants::CLAUDE_API_KEY_HISTORY); + claudeApiKey.setDefaultValue(""); + claudeApiKey.setAutoApply(true); + + resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults"); + + readSettings(); + + setupConnections(); + + setLayouter([this]() { + using namespace Layouting; + + return Column{ + Row{Stretch{1}, resetToDefaults}, + Space{8}, + Group{title(Tr::tr("OpenRouter Settings")), Column{openRouterApiKey}}, + Space{8}, + Group{title(Tr::tr("OpenAI Compatible Settings")), Column{openAiCompatApiKey}}, + Space{8}, + Group{title(Tr::tr("Claude Settings")), Column{claudeApiKey}}, + Stretch{1}}; + }); +} + +void ProviderSettings::setupConnections() +{ + connect( + &resetToDefaults, &ButtonAspect::clicked, this, &ProviderSettings::resetSettingsToDefaults); + connect(&openRouterApiKey, &ButtonAspect::changed, this, [this]() { + openRouterApiKey.writeSettings(); + }); + connect(&openAiCompatApiKey, &ButtonAspect::changed, this, [this]() { + openAiCompatApiKey.writeSettings(); + }); + connect(&claudeApiKey, &ButtonAspect::changed, this, [this]() { claudeApiKey.writeSettings(); }); +} + +void ProviderSettings::resetSettingsToDefaults() +{ + QMessageBox::StandardButton reply; + reply = QMessageBox::question( + Core::ICore::dialogParent(), + Tr::tr("Reset Settings"), + Tr::tr("Are you sure you want to reset all settings to default values?"), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + resetAspect(openRouterApiKey); + resetAspect(openAiCompatApiKey); + resetAspect(claudeApiKey); + } +} + +class ProviderSettingsPage : public Core::IOptionsPage +{ +public: + ProviderSettingsPage() + { + setId(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); + setDisplayName(Tr::tr("Provider Settings")); + setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY); + setSettingsProvider([] { return &providerSettings(); }); + } +}; + +const ProviderSettingsPage providerSettingsPage; + +} // namespace QodeAssist::Settings diff --git a/settings/ProviderSettings.hpp b/settings/ProviderSettings.hpp new file mode 100644 index 0000000..6635fdb --- /dev/null +++ b/settings/ProviderSettings.hpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include + +#include "ButtonAspect.hpp" + +namespace QodeAssist::Settings { + +class ProviderSettings : public Utils::AspectContainer +{ +public: + ProviderSettings(); + + ButtonAspect resetToDefaults{this}; + + // API Keys + Utils::StringAspect openRouterApiKey{this}; + Utils::StringAspect openAiCompatApiKey{this}; + Utils::StringAspect claudeApiKey{this}; + +private: + void setupConnections(); + void resetSettingsToDefaults(); +}; + +ProviderSettings &providerSettings(); + +} // namespace QodeAssist::Settings diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 0018cc0..1c163b0 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -77,6 +77,17 @@ const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "Qode Assist"; const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion"; +// Provider Settings Page ID +const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.5ProviderSettingsPageId"; + +// Provider API Keys +const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey"; +const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory"; +const char OPEN_AI_COMPAT_API_KEY[] = "QodeAssist.openAiCompatApiKey"; +const char OPEN_AI_COMPAT_API_KEY_HISTORY[] = "QodeAssist.openAiCompatApiKeyHistory"; +const char CLAUDE_API_KEY[] = "QodeAssist.claudeApiKey"; +const char CLAUDE_API_KEY_HISTORY[] = "QodeAssist.claudeApiKeyHistory"; + // context settings const char CC_READ_FULL_FILE[] = "QodeAssist.ccReadFullFile"; const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCursor"; diff --git a/templates/Claude.hpp b/templates/Claude.hpp new file mode 100644 index 0000000..682683a --- /dev/null +++ b/templates/Claude.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include + +#include "llmcore/PromptTemplate.hpp" + +namespace QodeAssist::Templates { + +class ClaudeCodeCompletion : public LLMCore::PromptTemplate +{ +public: + LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } + QString name() const override { return "Claude Code Completion"; } + QString promptTemplate() const override { return {}; } + QStringList stopWords() const override { return QStringList(); } + void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override + { + QJsonArray messages = request["messages"].toArray(); + + for (int i = 0; i < messages.size(); ++i) { + QJsonObject message = messages[i].toObject(); + QString role = message["role"].toString(); + QString content = message["content"].toString(); + + if (message["role"] == "user") { + message["content"] = QString("Complete the code ONLY between these " + "parts:\nBefore: %1\nAfter: %2\n") + .arg(context.prefix, context.suffix); + } else { + message["content"] = QString("%1").arg(content); + } + + messages[i] = message; + } + + request["messages"] = messages; + } + QString description() const override { return "Claude Chat for code completion"; } +}; + +class ClaudeChat : public LLMCore::PromptTemplate +{ +public: + LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } + QString name() const override { return "Claude Chat"; } + QString promptTemplate() const override { return {}; } + QStringList stopWords() const override { return QStringList(); } + void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override {} + QString description() const override { return "Claude Chat"; } +}; + +} // namespace QodeAssist::Templates diff --git a/templates/Templates.hpp b/templates/Templates.hpp index 098aaef..b65ce8b 100644 --- a/templates/Templates.hpp +++ b/templates/Templates.hpp @@ -23,6 +23,7 @@ #include "templates/Alpaca.hpp" #include "templates/BasicChat.hpp" #include "templates/ChatML.hpp" +#include "templates/Claude.hpp" #include "templates/CodeLlamaFim.hpp" #include "templates/CustomFimTemplate.hpp" #include "templates/DeepSeekCoderFim.hpp" @@ -49,6 +50,8 @@ inline void registerTemplates() templateManager.registerTemplate(); templateManager.registerTemplate(); templateManager.registerTemplate(); + templateManager.registerTemplate(); + templateManager.registerTemplate(); } } // namespace QodeAssist::Templates