diff --git a/CMakeLists.txt b/CMakeLists.txt index 66fe792..a269b24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,9 +34,11 @@ add_qtc_plugin(QodeAssist templates/PromptTemplate.hpp templates/CodeLLamaTemplate.hpp templates/StarCoder2Template.hpp + templates/CodeQwenChat.hpp providers/LLMProvider.hpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp + providers/OpenAICompatProvider.h providers/OpenAICompatProvider.cpp LLMProvidersManager.hpp LLMProvidersManager.cpp QodeAssistSettings.hpp QodeAssistSettings.cpp QodeAssist.qrc diff --git a/DocumentContextReader.cpp b/DocumentContextReader.cpp index 056857c..c52047e 100644 --- a/DocumentContextReader.cpp +++ b/DocumentContextReader.cpp @@ -25,6 +25,26 @@ #include "QodeAssistSettings.hpp" +const QRegularExpression &getYearRegex() +{ + static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b"); + return yearRegex; +} + +const QRegularExpression &getNameRegex() +{ + static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b"); + return nameRegex; +} + +const QRegularExpression &getCommentRegex() +{ + static const QRegularExpression + commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))", + QRegularExpression::MultilineOption); + return commentRegex; +} + namespace QodeAssist { DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument) @@ -126,21 +146,27 @@ CopyrightInfo DocumentContextReader::findCopyright() CopyrightInfo result = {-1, -1, false}; QString text = m_document->toPlainText(); - QRegularExpressionMatchIterator matchIterator = getCopyrightRegex().globalMatch(text); + QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text); QList copyrightBlocks; while (matchIterator.hasNext()) { QRegularExpressionMatch match = matchIterator.next(); - int startPos = match.capturedStart(); - int endPos = match.capturedEnd(); + QString matchedText = match.captured().toLower(); - CopyrightInfo info; - info.startLine = m_document->findBlock(startPos).blockNumber(); - info.endLine = m_document->findBlock(endPos).blockNumber(); - info.found = true; + if (matchedText.contains("copyright") || matchedText.contains("(C)") + || matchedText.contains("(c)") || matchedText.contains("©") + || getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) { + int startPos = match.capturedStart(); + int endPos = match.capturedEnd(); - copyrightBlocks.append(info); + CopyrightInfo info; + info.startLine = m_document->findBlock(startPos).blockNumber(); + info.endLine = m_document->findBlock(endPos).blockNumber(); + info.found = true; + + copyrightBlocks.append(info); + } } for (int i = 0; i < copyrightBlocks.size() - 1; ++i) { @@ -178,12 +204,9 @@ QString DocumentContextReader::getContextBetween(int startLine, return context; } -const QRegularExpression &DocumentContextReader::getCopyrightRegex() +CopyrightInfo DocumentContextReader::copyrightInfo() const { - static const QRegularExpression copyrightRegex( - R"((?:/\*[\s\S]*?Copyright[\s\S]*?\*/| // Copyright[\s\S]*?(?:\n\s*//.*)*|///.*Copyright[\s\S]*?(?:\n\s*///.*)*)|(?://))", - QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption); - return copyrightRegex; + return m_copyrightInfo; } } // namespace QodeAssist diff --git a/DocumentContextReader.hpp b/DocumentContextReader.hpp index b43a9f4..65ce53a 100644 --- a/DocumentContextReader.hpp +++ b/DocumentContextReader.hpp @@ -46,12 +46,12 @@ public: CopyrightInfo findCopyright(); QString getContextBetween(int startLine, int endLine, int cursorPosition) const; + CopyrightInfo copyrightInfo() const; + private: TextEditor::TextDocument *m_textDocument; QTextDocument *m_document; CopyrightInfo m_copyrightInfo; - - static const QRegularExpression &getCopyrightRegex(); }; } // namespace QodeAssist diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 686f4cc..77f88cf 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -120,8 +120,10 @@ QString LLMClientInterface::сontextBefore(TextEditor::TextEditorWidget *widget, return QString(); DocumentContextReader reader(widget->textDocument()); - QString languageAndFileInfo = reader.getLanguageAndFileInfo(); + const auto ©right = reader.copyrightInfo(); + logMessage(QString{"Line Number: %1"}.arg(lineNumber)); + logMessage(QString("Copyright found %1 %2").arg(copyright.found).arg(copyright.endLine)); if (lineNumber < reader.findCopyright().endLine) return QString(); @@ -135,9 +137,7 @@ QString LLMClientInterface::сontextBefore(TextEditor::TextEditorWidget *widget, } return QString("%1\n%2\n%3") - .arg(reader.getSpecificInstructions()) - .arg(reader.getLanguageAndFileInfo()) - .arg(contextBefore); + .arg(reader.getSpecificInstructions(), reader.getLanguageAndFileInfo(), contextBefore); } QString LLMClientInterface::сontextAfter(TextEditor::TextEditorWidget *widget, @@ -177,7 +177,7 @@ void LLMClientInterface::handleInitialize(const QJsonObject &request) result["capabilities"] = capabilities; QJsonObject serverInfo; - serverInfo["name"] = "Ollama LSP Server"; + serverInfo["name"] = "QodeAssist LSP Server"; serverInfo["version"] = "0.1"; result["serverInfo"] = serverInfo; @@ -324,22 +324,22 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion, void LLMClientInterface::sendLLMRequest(const QJsonObject &request, const ContextPair &prompt) { - QJsonObject ollamaRequest = {{"model", settings().modelName.value()}, {"stream", true}}; + QJsonObject qodeRequest = {{"model", settings().modelName.value()}, {"stream", true}}; auto currentTemplate = PromptTemplateManager::instance().getCurrentTemplate(); - currentTemplate->prepareRequest(ollamaRequest, prompt.prefix, prompt.suffix); + currentTemplate->prepareRequest(qodeRequest, prompt.prefix, prompt.suffix); auto &providerManager = LLMProvidersManager::instance(); - providerManager.getCurrentProvider()->prepareRequest(ollamaRequest); + providerManager.getCurrentProvider()->prepareRequest(qodeRequest); logMessage( QString("Sending request to llm: \nurl: %1\nRequest body:\n%2") .arg(m_serverUrl.toString()) - .arg(QString::fromUtf8(QJsonDocument(ollamaRequest).toJson(QJsonDocument::Indented)))); + .arg(QString::fromUtf8(QJsonDocument(qodeRequest).toJson(QJsonDocument::Indented)))); QNetworkRequest networkRequest(m_serverUrl); networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QNetworkReply *reply = m_manager->post(networkRequest, QJsonDocument(ollamaRequest).toJson()); + QNetworkReply *reply = m_manager->post(networkRequest, QJsonDocument(qodeRequest).toJson()); if (!reply) { logMessage("Error: Failed to create network reply"); return; @@ -356,7 +356,7 @@ void LLMClientInterface::sendLLMRequest(const QJsonObject &request, const Contex reply->deleteLater(); m_activeRequests.remove(requestId); if (reply->error() != QNetworkReply::NoError) { - logMessage(QString("Error in Ollama request: %1").arg(reply->errorString())); + logMessage(QString("Error in QodeAssist request: %1").arg(reply->errorString())); } else { logMessage("Request finished successfully"); } diff --git a/QodeAssist.json.in b/QodeAssist.json.in index 0f31cc6..8228dc7 100644 --- a/QodeAssist.json.in +++ b/QodeAssist.json.in @@ -1,6 +1,6 @@ { "Name" : "QodeAssist", - "Version" : "0.0.4", + "Version" : "0.0.5", "CompatVersion" : "${IDE_VERSION_COMPAT}", "Vendor" : "Petr Mironychev", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", diff --git a/QodeAssistConstants.hpp b/QodeAssistConstants.hpp index 3478190..b6e4be9 100644 --- a/QodeAssistConstants.hpp +++ b/QodeAssistConstants.hpp @@ -54,6 +54,7 @@ const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold"; const char OLLAMA_LIVETIME[] = "QodeAssist.ollamaLivetime"; const char SPECIFIC_INSTRUCTIONS[] = "QodeAssist.specificInstractions"; const char MULTILINE_COMPLETION[] = "QodeAssist.multilineCompletion"; +const char API_KEY[] = "QodeAssist.apiKey"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category"; diff --git a/QodeAssistSettings.cpp b/QodeAssistSettings.cpp index 77b3b59..43a4f33 100644 --- a/QodeAssistSettings.cpp +++ b/QodeAssistSettings.cpp @@ -123,15 +123,15 @@ QodeAssistSettings::QodeAssistSettings() topP.setSettingsKey(Constants::TOP_P); topP.setLabelText(Tr::tr("top_p")); topP.setDefaultValue(0.9); - topP.setRange(0.0, 10.0); + topP.setRange(0.0, 1.0); useTopK.setSettingsKey(Constants::USE_TOP_K); useTopK.setDefaultValue(false); topK.setSettingsKey(Constants::TOP_K); topK.setLabelText(Tr::tr("top_k")); - topK.setDefaultValue(0.1); - topK.setRange(0, 10.0); + topK.setDefaultValue(50); + topK.setRange(1, 1000); usePresencePenalty.setSettingsKey(Constants::USE_PRESENCE_PENALTY); usePresencePenalty.setDefaultValue(false); @@ -170,6 +170,11 @@ QodeAssistSettings::QodeAssistSettings() multiLineCompletion.setDefaultValue(true); multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion")); + apiKey.setSettingsKey(Constants::API_KEY); + apiKey.setLabelText(Tr::tr("API Key:")); + apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay); + apiKey.setPlaceHolderText(Tr::tr("Enter your API key here")); + const auto &manager = LLMProvidersManager::instance(); if (!manager.getProviderNames().isEmpty()) { const auto providerNames = manager.getProviderNames(); @@ -215,14 +220,15 @@ QodeAssistSettings::QodeAssistSettings() Form{Column{Row{selectModels, modelName}}}}, Group{title(Tr::tr("FIM Prompt Settings")), Form{Column{fimPrompts, + readFullFile, maxFileThreshold, + readStringsBeforeCursor, + readStringsAfterCursor, ollamaLivetime, + apiKey, specificInstractions, temperature, maxTokens, - readFullFile, - readStringsBeforeCursor, - readStringsAfterCursor, startSuggestionTimer, Row{useTopP, topP, Stretch{1}}, Row{useTopK, topK, Stretch{1}}, diff --git a/QodeAssistSettings.hpp b/QodeAssistSettings.hpp index 32cba11..da6690e 100644 --- a/QodeAssistSettings.hpp +++ b/QodeAssistSettings.hpp @@ -81,7 +81,7 @@ public: Utils::DoubleAspect topP{this}; Utils::BoolAspect useTopK{this}; - Utils::DoubleAspect topK{this}; + Utils::IntegerAspect topK{this}; Utils::BoolAspect usePresencePenalty{this}; Utils::DoubleAspect presencePenalty{this}; @@ -98,6 +98,8 @@ public: Utils::StringAspect specificInstractions{this}; Utils::BoolAspect multiLineCompletion{this}; + Utils::StringAspect apiKey{this}; + ButtonAspect resetToDefaults{this}; private: diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp new file mode 100644 index 0000000..a554562 --- /dev/null +++ b/providers/OpenAICompatProvider.cpp @@ -0,0 +1,129 @@ +/* + * 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 "OpenAICompatProvider.h" + +#include +#include +#include +#include +#include + +#include "PromptTemplateManager.hpp" +#include "QodeAssistSettings.hpp" + +namespace QodeAssist::Providers { + +OpenAICompatProvider::OpenAICompatProvider() {} + +QString OpenAICompatProvider::name() const +{ + return "OpenAI Compatible (experimental)"; +} + +QString OpenAICompatProvider::url() const +{ + return "http://localhost"; +} + +int OpenAICompatProvider::defaultPort() const +{ + return 1234; +} + +QString OpenAICompatProvider::completionEndpoint() const +{ + return "/v1/chat/completions"; +} + +void OpenAICompatProvider::prepareRequest(QJsonObject &request) +{ + const auto ¤tTemplate = PromptTemplateManager::instance().getCurrentTemplate(); + + if (request.contains("prompt")) { + QJsonArray messages{ + {QJsonObject{{"role", "user"}, {"content", request.take("prompt").toString()}}}}; + request["messages"] = std::move(messages); + } + + request["max_tokens"] = settings().maxTokens(); + request["temperature"] = settings().temperature(); + request["stop"] = QJsonArray::fromStringList(currentTemplate->stopWords()); + if (settings().useTopP()) + request["top_p"] = settings().topP(); + if (settings().useTopK()) + request["top_k"] = settings().topK(); + if (settings().useFrequencyPenalty()) + request["frequency_penalty"] = settings().frequencyPenalty(); + if (settings().usePresencePenalty()) + request["presence_penalty"] = settings().presencePenalty(); + + const QString &apiKey = settings().apiKey.value(); + if (!apiKey.isEmpty()) { + request["api_key"] = apiKey; + } +} + +bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse) +{ + bool isComplete = false; + while (reply->canReadLine()) { + QByteArray line = reply->readLine().trimmed(); + if (line.isEmpty()) { + continue; + } + if (line == "data: [DONE]") { + isComplete = true; + break; + } + if (line.startsWith("data: ")) { + line = line.mid(6); // Remove "data: " prefix + } + QJsonDocument jsonResponse = QJsonDocument::fromJson(line); + if (jsonResponse.isNull()) { + qWarning() << "Invalid JSON response from LM Studio:" << line; + continue; + } + QJsonObject responseObj = jsonResponse.object(); + if (responseObj.contains("choices")) { + QJsonArray choices = responseObj["choices"].toArray(); + if (!choices.isEmpty()) { + QJsonObject choice = choices.first().toObject(); + QJsonObject delta = choice["delta"].toObject(); + if (delta.contains("content")) { + QString completion = delta["content"].toString(); + + accumulatedResponse += completion; + } + if (choice["finish_reason"].toString() == "stop") { + isComplete = true; + break; + } + } + } + } + return isComplete; +} + +QList OpenAICompatProvider::getInstalledModels(const Utils::Environment &env) +{ + return QStringList(); +} + +} // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.h b/providers/OpenAICompatProvider.h new file mode 100644 index 0000000..edb94a7 --- /dev/null +++ b/providers/OpenAICompatProvider.h @@ -0,0 +1,40 @@ +/* + * 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 "LLMProvider.hpp" + +namespace QodeAssist::Providers { + +class OpenAICompatProvider : public LLMProvider +{ +public: + OpenAICompatProvider(); + + QString name() const override; + QString url() const override; + int defaultPort() const override; + QString completionEndpoint() const override; + void prepareRequest(QJsonObject &request) override; + bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; + QList getInstalledModels(const Utils::Environment &env) override; +}; + +} // namespace QodeAssist::Providers diff --git a/qodeassist.cpp b/qodeassist.cpp index 9308730..90fd069 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -43,7 +43,9 @@ #include "QodeAssistClient.hpp" #include "providers/LMStudioProvider.hpp" #include "providers/OllamaProvider.hpp" +#include "providers/OpenAICompatProvider.h" #include "templates/CodeLLamaTemplate.hpp" +#include "templates/CodeQwenChat.hpp" #include "templates/StarCoder2Template.hpp" using namespace Utils; @@ -72,10 +74,12 @@ public: auto &providerManager = LLMProvidersManager::instance(); providerManager.registerProvider(); providerManager.registerProvider(); + providerManager.registerProvider(); auto &templateManager = PromptTemplateManager::instance(); templateManager.registerTemplate(); templateManager.registerTemplate(); + templateManager.registerTemplate(); Utils::Icon QCODEASSIST_ICON( {{":/resources/images/qoderassist-icon.png", Utils::Theme::IconsBaseColor}}); @@ -83,7 +87,7 @@ public: ActionBuilder requestAction(this, Constants::QODE_ASSIST_REQUEST_SUGGESTION); requestAction.setToolTip( Tr::tr("Generate Qode Assist suggestion at the current cursor position.")); - requestAction.setText(Tr::tr("Request Ollama Suggestion")); + requestAction.setText(Tr::tr("Request QodeAssist Suggestion")); requestAction.setIcon(QCODEASSIST_ICON.icon()); const QKeySequence defaultShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Q); requestAction.setDefaultKeySequence(defaultShortcut); diff --git a/templates/CodeQwenChat.hpp b/templates/CodeQwenChat.hpp new file mode 100644 index 0000000..dff2f07 --- /dev/null +++ b/templates/CodeQwenChat.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 "PromptTemplate.hpp" + +namespace QodeAssist::Templates { + +class CodeQwenChatTemplate : public PromptTemplate +{ +public: + QString name() const override { return "CodeQwenChat (experimental)"; } + QString promptTemplate() const override { return "\n### Instruction:%1%2 ### Response:\n"; } + QStringList stopWords() const override + { + return QStringList() << "### Instruction:" << "### Response:" << "\n\n### "; + } + void prepareRequest(QJsonObject &request, + const QString &prefix, + const QString &suffix) const override + { + QString formattedPrompt = promptTemplate().arg(prefix, suffix); + request["prompt"] = formattedPrompt; + } +}; + +} // namespace QodeAssist::Templates