feat: Add language-specific LLM preset configuration

- Add ability to configure separate provider/model/template for specific programming language
- Add UI controls for language preset configuration
- Support custom provider selection per language
- Support custom model selection per language
- Support custom template selection per language
This commit is contained in:
Petr Mironychev 2025-02-02 22:57:18 +01:00
parent e836b86569
commit 2a0beb6c4c
11 changed files with 310 additions and 21 deletions

View File

@ -57,6 +57,13 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
}
void ConfigurationManager::selectProvider()
@ -69,6 +76,8 @@ void ConfigurationManager::selectProvider()
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
@ -86,14 +95,19 @@ void ConfigurationManager::selectModel()
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel;
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model
: m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) {
@ -122,11 +136,13 @@ void ConfigurationManager::selectTemplate()
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
: m_templateManger.chatTemplatesNames();
const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
: m_templateManger.chatTemplatesNames();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
@ -150,8 +166,9 @@ void ConfigurationManager::selectUrl()
urls.append(url);
}
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
? m_generalSettings.ccUrl
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
: settingsButton == &m_generalSettings.ccPreset1SetUrl
? m_generalSettings.ccPreset1Url
: m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {

View File

@ -146,20 +146,41 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
emit finished();
}
bool QodeAssist::LLMClientInterface::isSpecifyCompletion(const QJsonObject &request)
{
auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(request);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
}
void LLMClientInterface::handleCompletion(const QJsonObject &request)
{
auto updatedContext = prepareContext(request);
const auto updatedContext = prepareContext(request);
auto &completeSettings = Settings::codeCompletionSettings();
auto &generalSettings = Settings::generalSettings();
auto providerName = Settings::generalSettings().ccProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
bool isPreset1Active = isSpecifyCompletion(request);
const auto providerName = !isPreset1Active ? generalSettings.ccProvider()
: generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? generalSettings.ccModel()
: generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? generalSettings.ccUrl() : generalSettings.ccPreset1Url();
const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return;
}
auto templateName = Settings::generalSettings().ccTemplate();
auto templateName = !isPreset1Active ? generalSettings.ccTemplate()
: generalSettings.ccPreset1Template();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
@ -168,19 +189,18 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return;
}
// TODO refactor to dynamic presets system
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::CodeCompletion;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QUrl(QString("%1%2").arg(
Settings::generalSettings().ccUrl(),
url,
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
: provider->chatEndpoint()));
config.apiKey = provider->apiKey();
config.providerRequest
= {{"model", Settings::generalSettings().ccModel()},
{"stream", Settings::codeCompletionSettings().stream()}};
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}};
config.multiLineCompletion = completeSettings.multiLineCompletion();
@ -246,11 +266,33 @@ LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &reque
return reader.prepareContext(lineNumber, cursorPosition);
}
Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return Context::ProgrammingLanguage::Unknown;
}
return Context::ProgrammingLanguageUtils::fromMimeType(textDocument->mimeType());
}
void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request,
bool isComplete)
{
auto templateName = Settings::generalSettings().ccTemplate();
bool isPreset1Active = isSpecifyCompletion(request);
auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
: Settings::generalSettings().ccPreset1Template();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);

View File

@ -22,6 +22,7 @@
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp>
#include <llmcore/RequestHandler.hpp>
@ -58,8 +59,10 @@ private:
void handleExit(const QJsonObject &request);
void handleCancelRequest(const QJsonObject &request);
LLMCore::ContextData prepareContext(const QJsonObject &request,
const QStringView &accumulatedCompletion = QString{});
LLMCore::ContextData prepareContext(
const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
bool isSpecifyCompletion(const QJsonObject &request);
LLMCore::RequestHandler m_requestHandler;
QElapsedTimer m_completionTimer;

View File

@ -4,6 +4,7 @@ add_library(Context STATIC
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
TokenUtils.hpp TokenUtils.cpp
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
)
target_link_libraries(Context

View File

@ -0,0 +1,70 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "ProgrammingLanguage.hpp"
namespace QodeAssist::Context {
ProgrammingLanguage ProgrammingLanguageUtils::fromMimeType(const QString &mimeType)
{
if (mimeType == "text/x-qml" || mimeType == "application/javascript"
|| mimeType == "text/javascript" || mimeType == "text/x-javascript") {
return ProgrammingLanguage::QML;
}
if (mimeType == "text/x-c++src" || mimeType == "text/x-c++hdr" || mimeType == "text/x-csrc"
|| mimeType == "text/x-chdr") {
return ProgrammingLanguage::Cpp;
}
if (mimeType == "text/x-python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
QString ProgrammingLanguageUtils::toString(ProgrammingLanguage language)
{
switch (language) {
case ProgrammingLanguage::Cpp:
return "c/c++";
case ProgrammingLanguage::QML:
return "qml";
case ProgrammingLanguage::Python:
return "python";
case ProgrammingLanguage::Unknown:
default:
return QString();
}
}
ProgrammingLanguage ProgrammingLanguageUtils::fromString(const QString &str)
{
QString lower = str.toLower();
if (lower == "c/c++") {
return ProgrammingLanguage::Cpp;
}
if (lower == "qml") {
return ProgrammingLanguage::QML;
}
if (lower == "python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
} // namespace QodeAssist::Context

View File

@ -0,0 +1,43 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
namespace QodeAssist::Context {
enum class ProgrammingLanguage {
QML, // QML/JavaScript
Cpp, // C/C++
Python,
Unknown,
};
namespace ProgrammingLanguageUtils {
ProgrammingLanguage fromMimeType(const QString &mimeType);
QString toString(ProgrammingLanguage language);
ProgrammingLanguage fromString(const QString &str);
} // namespace ProgrammingLanguageUtils
} // namespace QodeAssist::Context

View File

@ -35,11 +35,26 @@ public:
void addToLayoutImpl(Layouting::Layout &parent) override
{
auto button = new QPushButton(m_buttonText);
button->setVisible(m_visible);
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);
connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible);
parent.addItem(button);
}
void updateVisibility(bool visible)
{
if (m_visible == visible)
return;
m_visible = visible;
emit visibleChanged(visible);
}
QString m_buttonText;
signals:
void clicked();
void visibleChanged(bool state);
private:
bool m_visible = true;
};

View File

@ -89,6 +89,39 @@ GeneralSettings::GeneralSettings()
ccStatus.setDefaultValue("");
ccTest.m_buttonText = TrConstants::TEST;
// preset1
specifyPreset1.setSettingsKey(Constants::CC_SPECIFY_PRESET1);
specifyPreset1.setLabelText(TrConstants::ADD_NEW_PRESET);
specifyPreset1.setDefaultValue(false);
preset1Language.setSettingsKey(Constants::CC_PRESET1_LANGUAGE);
preset1Language.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
// see ProgrammingLanguageUtils
preset1Language.addOption("qml");
preset1Language.addOption("c/c++");
preset1Language.addOption("python");
initStringAspect(
ccPreset1Provider, Constants::CC_PRESET1_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccPreset1Provider.setReadOnly(true);
ccPreset1SelectProvider.m_buttonText = TrConstants::SELECT;
initStringAspect(
ccPreset1Url, Constants::CC_PRESET1_URL, TrConstants::URL, "http://localhost:11434");
ccPreset1Url.setHistoryCompleter(Constants::CC_PRESET1_URL_HISTORY);
ccPreset1SetUrl.m_buttonText = TrConstants::SELECT;
initStringAspect(
ccPreset1Model, Constants::CC_PRESET1_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b");
ccPreset1Model.setHistoryCompleter(Constants::CC_PRESET1_MODEL_HISTORY);
ccPreset1SelectModel.m_buttonText = TrConstants::SELECT;
initStringAspect(
ccPreset1Template, Constants::CC_PRESET1_TEMPLATE, TrConstants::TEMPLATE, "Ollama Auto FIM");
ccPreset1Template.setReadOnly(true);
ccPreset1SelectTemplate.m_buttonText = TrConstants::SELECT;
// chat assistance
initStringAspect(caProvider, Constants::CA_PROVIDER, TrConstants::PROVIDER, "Ollama");
caProvider.setReadOnly(true);
caSelectProvider.m_buttonText = TrConstants::SELECT;
@ -117,6 +150,8 @@ GeneralSettings::GeneralSettings()
setupConnections();
updatePreset1Visiblity(specifyPreset1.value());
setLayouter([this]() {
using namespace Layouting;
@ -126,13 +161,21 @@ GeneralSettings::GeneralSettings()
ccGrid.addRow({ccModel, ccSelectModel});
ccGrid.addRow({ccTemplate, ccSelectTemplate});
auto ccPreset1Grid = Grid{};
ccPreset1Grid.addRow({ccPreset1Provider, ccPreset1SelectProvider});
ccPreset1Grid.addRow({ccPreset1Url, ccPreset1SetUrl});
ccPreset1Grid.addRow({ccPreset1Model, ccPreset1SelectModel});
ccPreset1Grid.addRow({ccPreset1Template, ccPreset1SelectTemplate});
auto caGrid = Grid{};
caGrid.addRow({caProvider, caSelectProvider});
caGrid.addRow({caUrl, caSetUrl});
caGrid.addRow({caModel, caSelectModel});
caGrid.addRow({caTemplate, caSelectTemplate});
auto ccGroup = Group{title(TrConstants::CODE_COMPLETION), ccGrid};
auto ccGroup = Group{
title(TrConstants::CODE_COMPLETION),
Column{ccGrid, Row{specifyPreset1, preset1Language, Stretch{1}}, ccPreset1Grid}};
auto caGroup = Group{title(TrConstants::CHAT_ASSISTANT), caGrid};
auto rootLayout = Column{
@ -267,9 +310,11 @@ void GeneralSettings::showUrlSelectionDialog(
dialog.addSpacing();
QStringList allUrls = predefinedUrls;
QString key
= QString("CompleterHistory/")
.append((&aspect == &ccUrl) ? Constants::CC_URL_HISTORY : Constants::CA_URL_HISTORY);
QString key = QString("CompleterHistory/")
.append(
(&aspect == &ccUrl) ? Constants::CC_URL_HISTORY
: (&aspect == &ccPreset1Url) ? Constants::CC_PRESET1_URL_HISTORY
: Constants::CA_URL_HISTORY);
QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList();
allUrls.append(historyList);
allUrls.removeDuplicates();
@ -297,6 +342,18 @@ void GeneralSettings::showUrlSelectionDialog(
dialog.exec();
}
void GeneralSettings::updatePreset1Visiblity(bool state)
{
ccPreset1Provider.setVisible(specifyPreset1.volatileValue());
ccPreset1SelectProvider.updateVisibility(specifyPreset1.volatileValue());
ccPreset1Url.setVisible(specifyPreset1.volatileValue());
ccPreset1SetUrl.updateVisibility(specifyPreset1.volatileValue());
ccPreset1Model.setVisible(specifyPreset1.volatileValue());
ccPreset1SelectModel.updateVisibility(specifyPreset1.volatileValue());
ccPreset1Template.setVisible(specifyPreset1.volatileValue());
ccPreset1SelectTemplate.updateVisibility(specifyPreset1.volatileValue());
}
void GeneralSettings::setupConnections()
{
connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
@ -306,6 +363,10 @@ void GeneralSettings::setupConnections()
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
});
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
updatePreset1Visiblity(specifyPreset1.volatileValue());
});
}
void GeneralSettings::resetPageToDefaults()
@ -328,6 +389,12 @@ void GeneralSettings::resetPageToDefaults()
resetAspect(caTemplate);
resetAspect(caUrl);
resetAspect(enableCheckUpdate);
resetAspect(specifyPreset1);
resetAspect(preset1Language);
resetAspect(ccPreset1Provider);
resetAspect(ccPreset1Model);
resetAspect(ccPreset1Template);
resetAspect(ccPreset1Url);
writeSettings();
}
}

View File

@ -55,6 +55,23 @@ public:
Utils::StringAspect ccStatus{this};
ButtonAspect ccTest{this};
// TODO create dynamic presets system
// preset1 for code completion settings
Utils::BoolAspect specifyPreset1{this};
Utils::SelectionAspect preset1Language{this};
Utils::StringAspect ccPreset1Provider{this};
ButtonAspect ccPreset1SelectProvider{this};
Utils::StringAspect ccPreset1Url{this};
ButtonAspect ccPreset1SetUrl{this};
Utils::StringAspect ccPreset1Model{this};
ButtonAspect ccPreset1SelectModel{this};
Utils::StringAspect ccPreset1Template{this};
ButtonAspect ccPreset1SelectTemplate{this};
// chat assistant settings
Utils::StringAspect caProvider{this};
ButtonAspect caSelectProvider{this};
@ -82,6 +99,8 @@ public:
void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls);
void updatePreset1Visiblity(bool state);
private:
void setupConnections();
void resetPageToDefaults();

View File

@ -46,6 +46,15 @@ const char CA_TEMPLATE[] = "QodeAssist.caTemplate";
const char CA_URL[] = "QodeAssist.caUrl";
const char CA_URL_HISTORY[] = "QodeAssist.caUrlHistory";
const char CC_SPECIFY_PRESET1[] = "QodeAssist.ccSpecifyPreset1";
const char CC_PRESET1_LANGUAGE[] = "QodeAssist.ccPreset1Language";
const char CC_PRESET1_PROVIDER[] = "QodeAssist.ccPreset1Provider";
const char CC_PRESET1_MODEL[] = "QodeAssist.ccPreset1Model";
const char CC_PRESET1_MODEL_HISTORY[] = "QodeAssist.ccPreset1ModelHistory";
const char CC_PRESET1_TEMPLATE[] = "QodeAssist.ccPreset1Template";
const char CC_PRESET1_URL[] = "QodeAssist.ccPreset1Url";
const char CC_PRESET1_URL_HISTORY[] = "QodeAssist.ccPreset1UrlHistory";
// settings
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
const char CC_AUTO_COMPLETION[] = "QodeAssist.ccAutoCompletion";

View File

@ -86,6 +86,9 @@ inline const char ENTER_MODEL_MANUALLY_BUTTON[]
inline const char AUTO_COMPLETION_SETTINGS[]
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Auto Completion Settings");
inline const char ADD_NEW_PRESET[]
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Add new preset for language");
} // namespace TrConstants
struct Tr