mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-18 04:11:25 -04:00
refactor: Move to agent-session architecture
This commit is contained in:
@@ -126,7 +126,6 @@ add_qtc_plugin(QodeAssist
|
||||
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
||||
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
||||
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
CodeHandler.hpp CodeHandler.cpp
|
||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
||||
@@ -166,7 +165,7 @@ add_qtc_plugin(QodeAssist
|
||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
|
||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
|
||||
|
||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||
find_program(QtCreatorExecutable
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "AgentRoleController.hpp"
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
AgentRoleController::AgentRoleController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().systemPrompt,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&AgentRoleController::baseSystemPromptChanged);
|
||||
|
||||
loadAvailableRoles();
|
||||
}
|
||||
|
||||
QStringList AgentRoleController::availableRoles() const
|
||||
{
|
||||
return m_availableRoles;
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRole() const
|
||||
{
|
||||
return m_currentRole;
|
||||
}
|
||||
|
||||
QString AgentRoleController::baseSystemPrompt() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().systemPrompt();
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRoleDescription() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
return role.description;
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRoleSystemPrompt() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return QString();
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return QString();
|
||||
|
||||
return role.systemPrompt;
|
||||
}
|
||||
|
||||
void AgentRoleController::loadAvailableRoles()
|
||||
{
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
m_availableRoles.clear();
|
||||
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
||||
|
||||
for (const auto &role : roles)
|
||||
m_availableRoles.append(role.name);
|
||||
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
|
||||
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
for (const auto &role : roles) {
|
||||
if (role.id == lastRoleId) {
|
||||
m_currentRole = role.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit availableRolesChanged();
|
||||
emit currentRoleChanged();
|
||||
}
|
||||
|
||||
void AgentRoleController::applyRole(const QString &roleName)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
||||
settings.lastUsedRoleId.setValue("");
|
||||
settings.writeSettings();
|
||||
m_currentRole = roleName;
|
||||
emit currentRoleChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
for (const auto &role : roles) {
|
||||
if (role.name == roleName) {
|
||||
settings.lastUsedRoleId.setValue(role.id);
|
||||
settings.writeSettings();
|
||||
m_currentRole = role.name;
|
||||
emit currentRoleChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRoleController::openSettings()
|
||||
{
|
||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class AgentRoleController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRoleController(QObject *parent = nullptr);
|
||||
|
||||
QStringList availableRoles() const;
|
||||
QString currentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentRoleDescription() const;
|
||||
QString currentRoleSystemPrompt() const;
|
||||
|
||||
void loadAvailableRoles();
|
||||
void applyRole(const QString &roleName);
|
||||
void openSettings();
|
||||
|
||||
signals:
|
||||
void availableRolesChanged();
|
||||
void currentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
|
||||
private:
|
||||
QStringList m_availableRoles;
|
||||
QString m_currentRole;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -75,8 +75,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
FileItem.hpp FileItem.cpp
|
||||
ChatFileManager.hpp ChatFileManager.cpp
|
||||
ChatCompressor.hpp ChatCompressor.cpp
|
||||
AgentRoleController.hpp AgentRoleController.cpp
|
||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
||||
ChatAgentController.hpp ChatAgentController.cpp
|
||||
FileEditController.hpp FileEditController.cpp
|
||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||
@@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
PluginLLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
QodeAssistUIControlsplugin
|
||||
QodeAssistLogger
|
||||
LLMQore
|
||||
Skills
|
||||
Agents
|
||||
Session
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
|
||||
91
ChatView/ChatAgentController.cpp
Normal file
91
ChatView/ChatAgentController.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatAgentController.hpp"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
#include <AgentFactory.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
namespace {
|
||||
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
|
||||
}
|
||||
|
||||
ChatAgentController::ChatAgentController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
m_currentAgent = settings->value(kChatAgentKey).toString();
|
||||
}
|
||||
|
||||
void ChatAgentController::setAgentFactory(AgentFactory *factory)
|
||||
{
|
||||
m_agentFactory = factory;
|
||||
reload();
|
||||
}
|
||||
|
||||
QStringList ChatAgentController::availableAgents() const
|
||||
{
|
||||
return m_availableAgents;
|
||||
}
|
||||
|
||||
QString ChatAgentController::currentAgent() const
|
||||
{
|
||||
return m_currentAgent;
|
||||
}
|
||||
|
||||
void ChatAgentController::setCurrentAgent(const QString &name)
|
||||
{
|
||||
if (name == m_currentAgent || !m_availableAgents.contains(name))
|
||||
return;
|
||||
|
||||
m_currentAgent = name;
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||
emit currentAgentChanged();
|
||||
}
|
||||
|
||||
void ChatAgentController::reload()
|
||||
{
|
||||
m_availableAgents = m_agentFactory ? m_agentFactory->configNames() : QStringList{};
|
||||
emit availableAgentsChanged();
|
||||
ensureValidCurrent();
|
||||
}
|
||||
|
||||
void ChatAgentController::ensureValidCurrent()
|
||||
{
|
||||
if (m_availableAgents.contains(m_currentAgent))
|
||||
return;
|
||||
|
||||
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
|
||||
if (next == m_currentAgent)
|
||||
return;
|
||||
|
||||
m_currentAgent = next;
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||
emit currentAgentChanged();
|
||||
}
|
||||
|
||||
bool ChatAgentController::currentSupportsThinking() const
|
||||
{
|
||||
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||
return false;
|
||||
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||
return config && config->enableThinking;
|
||||
}
|
||||
|
||||
bool ChatAgentController::currentSupportsTools() const
|
||||
{
|
||||
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||
return false;
|
||||
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||
return config && config->enableTools;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
47
ChatView/ChatAgentController.hpp
Normal file
47
ChatView/ChatAgentController.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatAgentController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatAgentController(QObject *parent = nullptr);
|
||||
|
||||
void setAgentFactory(AgentFactory *factory);
|
||||
|
||||
QStringList availableAgents() const;
|
||||
QString currentAgent() const;
|
||||
void setCurrentAgent(const QString &name);
|
||||
|
||||
bool currentSupportsThinking() const;
|
||||
bool currentSupportsTools() const;
|
||||
|
||||
void reload();
|
||||
|
||||
signals:
|
||||
void availableAgentsChanged();
|
||||
void currentAgentChanged();
|
||||
|
||||
private:
|
||||
void ensureValidCurrent();
|
||||
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QStringList m_availableAgents;
|
||||
QString m_currentAgent;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -4,13 +4,21 @@
|
||||
|
||||
#include "ChatCompressor.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@@ -25,6 +33,16 @@ ChatCompressor::ChatCompressor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void ChatCompressor::setActiveAgent(const QString &agentName)
|
||||
{
|
||||
m_activeAgent = agentName;
|
||||
}
|
||||
|
||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||
{
|
||||
if (m_isCompressing) {
|
||||
@@ -42,20 +60,23 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
return;
|
||||
}
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!m_provider) {
|
||||
emit compressionFailed(tr("No provider available"));
|
||||
if (!m_sessionManager) {
|
||||
emit compressionFailed(tr("Chat session manager is not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
templateName);
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
|
||||
if (!session) {
|
||||
emit compressionFailed(
|
||||
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!promptTemplate) {
|
||||
emit compressionFailed(tr("No template available"));
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
m_sessionManager->removeSession(session);
|
||||
emit compressionFailed(tr("Chat agent has no live client"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,23 +84,52 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
m_chatModel = chatModel;
|
||||
m_originalChatPath = chatFilePath;
|
||||
m_accumulatedSummary.clear();
|
||||
m_session = session;
|
||||
|
||||
emit compressionStarted();
|
||||
|
||||
connectProviderSignals();
|
||||
session->systemPrompt()->setLayer(
|
||||
QStringLiteral("compression"),
|
||||
QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of "
|
||||
"discussion."));
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
auto *history = session->history();
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
if (msg.content.trimmed().isEmpty())
|
||||
continue;
|
||||
|
||||
buildRequestPayload(payload, promptTemplate);
|
||||
Message apiMessage(
|
||||
msg.role == ChatModel::ChatRole::User ? Message::Role::User : Message::Role::Assistant);
|
||||
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
|
||||
history->append(std::move(apiMessage));
|
||||
}
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
m_provider->client()->setTransferTimeout(
|
||||
m_connections.append(connect(
|
||||
client, &::LLMQore::BaseClient::chunkReceived,
|
||||
this, &ChatCompressor::onPartialResponseReceived, Qt::UniqueConnection));
|
||||
m_connections.append(connect(
|
||||
client, &::LLMQore::BaseClient::requestCompleted,
|
||||
this, &ChatCompressor::onFullResponseReceived, Qt::UniqueConnection));
|
||||
m_connections.append(connect(
|
||||
client, &::LLMQore::BaseClient::requestFailed,
|
||||
this, &ChatCompressor::onRequestFailed, Qt::UniqueConnection));
|
||||
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
m_currentRequestId = m_provider->sendRequest(
|
||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
|
||||
|
||||
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
handleCompressionError(tr("Failed to start compression request"));
|
||||
return;
|
||||
}
|
||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||
}
|
||||
|
||||
@@ -94,10 +144,6 @@ void ChatCompressor::cancelCompression()
|
||||
return;
|
||||
|
||||
LOG_MESSAGE("Cancelling compression request");
|
||||
|
||||
if (m_provider && !m_currentRequestId.isEmpty())
|
||||
m_provider->cancelRequest(m_currentRequestId);
|
||||
|
||||
cleanupState();
|
||||
emit compressionFailed(tr("Compression cancelled"));
|
||||
}
|
||||
@@ -120,14 +166,18 @@ void ChatCompressor::onFullResponseReceived(const QString &requestId, const QStr
|
||||
LOG_MESSAGE(
|
||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||
|
||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||
handleCompressionError(tr("Failed to save compressed chat"));
|
||||
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
const QString summary = m_accumulatedSummary;
|
||||
const QString sourcePath = m_originalChatPath;
|
||||
|
||||
cleanupState();
|
||||
|
||||
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
|
||||
emit compressionFailed(tr("Failed to save compressed chat"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||
cleanupState();
|
||||
emit compressionCompleted(compressedPath);
|
||||
}
|
||||
|
||||
@@ -168,39 +218,6 @@ QString ChatCompressor::buildCompressionPrompt() const
|
||||
"Create the summary now:");
|
||||
}
|
||||
|
||||
void ChatCompressor::buildRequestPayload(
|
||||
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
||||
{
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
context.systemPrompt = QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool
|
||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
PluginLLMCore::Message compressionRequest;
|
||||
compressionRequest.role = "user";
|
||||
compressionRequest.content = buildCompressionPrompt();
|
||||
messages.append(compressionRequest);
|
||||
|
||||
context.history = messages;
|
||||
|
||||
m_provider->prepareRequest(
|
||||
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
||||
}
|
||||
|
||||
bool ChatCompressor::createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||
{
|
||||
@@ -247,32 +264,6 @@ bool ChatCompressor::createCompressedChatFile(
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatCompressor::connectProviderSignals()
|
||||
{
|
||||
auto *c = m_provider->client();
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ChatCompressor::onPartialResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ChatCompressor::onFullResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ChatCompressor::onRequestFailed,
|
||||
Qt::UniqueConnection));
|
||||
}
|
||||
|
||||
void ChatCompressor::disconnectAllSignals()
|
||||
{
|
||||
for (const auto &connection : std::as_const(m_connections))
|
||||
@@ -284,12 +275,17 @@ void ChatCompressor::cleanupState()
|
||||
{
|
||||
disconnectAllSignals();
|
||||
|
||||
Session *session = m_session;
|
||||
|
||||
m_isCompressing = false;
|
||||
m_currentRequestId.clear();
|
||||
m_originalChatPath.clear();
|
||||
m_accumulatedSummary.clear();
|
||||
m_chatModel = nullptr;
|
||||
m_provider = nullptr;
|
||||
m_session = nullptr;
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
class Provider;
|
||||
class PromptTemplate;
|
||||
} // namespace QodeAssist::PluginLLMCore
|
||||
namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@@ -25,6 +25,9 @@ class ChatCompressor : public QObject
|
||||
public:
|
||||
explicit ChatCompressor(QObject *parent = nullptr);
|
||||
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setActiveAgent(const QString &agentName);
|
||||
|
||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||
|
||||
bool isCompressing() const;
|
||||
@@ -45,17 +48,17 @@ private:
|
||||
QString buildCompressionPrompt() const;
|
||||
bool createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||
void connectProviderSignals();
|
||||
void disconnectAllSignals();
|
||||
void cleanupState();
|
||||
void handleCompressionError(const QString &error);
|
||||
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
||||
|
||||
bool m_isCompressing = false;
|
||||
QString m_currentRequestId;
|
||||
QString m_originalChatPath;
|
||||
QString m_accumulatedSummary;
|
||||
PluginLLMCore::Provider *m_provider = nullptr;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QString m_activeAgent;
|
||||
QPointer<Session> m_session;
|
||||
ChatModel *m_chatModel = nullptr;
|
||||
|
||||
QList<QMetaObject::Connection> m_connections;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatConfigurationController.hpp"
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
connect(
|
||||
&settings.caProvider,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatConfigurationController::updateCurrentConfiguration);
|
||||
connect(
|
||||
&settings.caModel,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatConfigurationController::updateCurrentConfiguration);
|
||||
|
||||
loadAvailableConfigurations();
|
||||
}
|
||||
|
||||
QStringList ChatConfigurationController::availableConfigurations() const
|
||||
{
|
||||
return m_availableConfigurations;
|
||||
}
|
||||
|
||||
QString ChatConfigurationController::currentConfiguration() const
|
||||
{
|
||||
return m_currentConfiguration;
|
||||
}
|
||||
|
||||
void ChatConfigurationController::updateCurrentConfiguration()
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
m_currentConfiguration
|
||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
||||
emit currentConfigurationChanged();
|
||||
}
|
||||
|
||||
void ChatConfigurationController::loadAvailableConfigurations()
|
||||
{
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::Chat);
|
||||
|
||||
m_availableConfigurations.clear();
|
||||
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
m_availableConfigurations.append(config.name);
|
||||
}
|
||||
|
||||
updateCurrentConfiguration();
|
||||
|
||||
emit availableConfigurationsChanged();
|
||||
}
|
||||
|
||||
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
||||
{
|
||||
if (configName == QObject::tr("Current Settings")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::Chat);
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
if (config.name == configName) {
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
settings.caProvider.setValue(config.provider);
|
||||
settings.caModel.setValue(config.model);
|
||||
settings.caTemplate.setValue(config.templateName);
|
||||
settings.caUrl.setValue(config.url);
|
||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
||||
|
||||
settings.writeSettings();
|
||||
|
||||
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
||||
emit currentConfigurationChanged();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatConfigurationController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatConfigurationController(QObject *parent = nullptr);
|
||||
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
void loadAvailableConfigurations();
|
||||
void applyConfiguration(const QString &configName);
|
||||
|
||||
signals:
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
private:
|
||||
void updateCurrentConfiguration();
|
||||
|
||||
QStringList m_availableConfigurations;
|
||||
QString m_currentConfiguration;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -28,9 +28,11 @@
|
||||
|
||||
#include "QodeAssistConstants.hpp"
|
||||
|
||||
#include "AgentRoleController.hpp"
|
||||
#include <AgentFactory.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
|
||||
#include "ChatAgentController.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatConfigurationController.hpp"
|
||||
#include "ChatCompressor.hpp"
|
||||
#include "ChatHistoryStore.hpp"
|
||||
#include "FileEditController.hpp"
|
||||
@@ -38,10 +40,8 @@
|
||||
#include "InputTokenCounter.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "SessionFileRegistry.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "pluginllmcore/RulesLoader.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "sources/skills/SkillsManager.hpp"
|
||||
@@ -74,13 +74,11 @@ QKeySequence sendMessageKeySequence()
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
, m_fileManager(new ChatFileManager(this))
|
||||
, m_isRequestInProgress(false)
|
||||
, m_chatCompressor(new ChatCompressor(this))
|
||||
, m_agentRoleController(new AgentRoleController(this))
|
||||
, m_configurationController(new ChatConfigurationController(this))
|
||||
, m_agentController(new ChatAgentController(this))
|
||||
, m_fileEditController(new FileEditController(m_chatModel, this))
|
||||
, m_tokenCounter(
|
||||
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
||||
@@ -109,22 +107,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(
|
||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(
|
||||
m_configurationController,
|
||||
&ChatConfigurationController::availableConfigurationsChanged,
|
||||
this,
|
||||
&ChatRootView::availableConfigurationsChanged);
|
||||
connect(
|
||||
m_configurationController,
|
||||
&ChatConfigurationController::currentConfigurationChanged,
|
||||
this,
|
||||
&ChatRootView::currentConfigurationChanged);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
@@ -171,20 +153,20 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::inputTokensCountChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::availableRolesChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::availableAgentsChanged,
|
||||
this,
|
||||
&ChatRootView::availableAgentRolesChanged);
|
||||
&ChatRootView::availableChatAgentsChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::currentRoleChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::currentAgentChanged,
|
||||
this,
|
||||
&ChatRootView::currentAgentRoleChanged);
|
||||
&ChatRootView::currentChatAgentChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::baseSystemPromptChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::currentAgentChanged,
|
||||
this,
|
||||
&ChatRootView::baseSystemPromptChanged);
|
||||
&ChatRootView::isThinkingSupportChanged);
|
||||
|
||||
auto editors = Core::EditorManager::instance();
|
||||
|
||||
@@ -266,14 +248,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
connect(
|
||||
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
||||
|
||||
refreshRules();
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
||||
this,
|
||||
&ChatRootView::refreshRules);
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::projectAdded,
|
||||
@@ -298,12 +272,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::useThinkingChanged);
|
||||
|
||||
connect(
|
||||
&Settings::generalSettings().caProvider,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isThinkingSupportChanged);
|
||||
|
||||
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||
m_lastErrorMessage = error;
|
||||
emit lastErrorMessageChanged();
|
||||
@@ -373,6 +341,48 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
|
||||
return m_skillsManager;
|
||||
}
|
||||
|
||||
AgentFactory *ChatRootView::agentFactory() const
|
||||
{
|
||||
if (!m_agentFactory) {
|
||||
if (auto *engine = qmlEngine(this)) {
|
||||
m_agentFactory = qobject_cast<AgentFactory *>(
|
||||
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
|
||||
}
|
||||
}
|
||||
return m_agentFactory;
|
||||
}
|
||||
|
||||
SessionManager *ChatRootView::sessionManager() const
|
||||
{
|
||||
if (!m_sessionManager) {
|
||||
if (auto *engine = qmlEngine(this)) {
|
||||
m_sessionManager = qobject_cast<SessionManager *>(
|
||||
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
|
||||
}
|
||||
}
|
||||
return m_sessionManager;
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableChatAgents()
|
||||
{
|
||||
m_agentController->setAgentFactory(agentFactory());
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableChatAgents() const
|
||||
{
|
||||
return m_agentController->availableAgents();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentChatAgent() const
|
||||
{
|
||||
return m_agentController->currentAgent();
|
||||
}
|
||||
|
||||
void ChatRootView::setCurrentChatAgent(const QString &name)
|
||||
{
|
||||
m_agentController->setCurrentAgent(name);
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||
{
|
||||
QVariantList results;
|
||||
@@ -380,7 +390,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||
if (!manager || !Settings::skillsSettings().enableSkills())
|
||||
return results;
|
||||
|
||||
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
@@ -481,7 +491,12 @@ void ChatRootView::dispatchSend(
|
||||
|
||||
m_tokenCounter->recordSent();
|
||||
|
||||
if (currentChatAgent().isEmpty())
|
||||
loadAvailableChatAgents();
|
||||
|
||||
m_clientInterface->setSkillsManager(skillsManager());
|
||||
m_clientInterface->setSessionManager(sessionManager());
|
||||
m_clientInterface->setActiveAgent(currentChatAgent());
|
||||
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
||||
|
||||
m_fileManager->clearIntermediateStorage();
|
||||
@@ -527,12 +542,6 @@ void ChatRootView::clearMessages()
|
||||
clearLinkedFiles();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.caModel();
|
||||
}
|
||||
|
||||
void ChatRootView::saveHistory(const QString &filePath)
|
||||
{
|
||||
if (filePath != m_recentFilePath) {
|
||||
@@ -821,25 +830,6 @@ void ChatRootView::openChatHistoryFolder()
|
||||
m_historyStore->openHistoryFolder();
|
||||
}
|
||||
|
||||
void ChatRootView::openRulesFolder()
|
||||
{
|
||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString projectPath = project->projectDirectory().toFSPathString();
|
||||
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
||||
|
||||
QDir dir(rulesPath);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void ChatRootView::openSettings()
|
||||
{
|
||||
QMetaObject::invokeMethod(
|
||||
@@ -1120,51 +1110,6 @@ QString ChatRootView::lastErrorMessage() const
|
||||
return m_lastErrorMessage;
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::activeRules() const
|
||||
{
|
||||
return m_activeRules;
|
||||
}
|
||||
|
||||
int ChatRootView::activeRulesCount() const
|
||||
{
|
||||
return m_activeRules.size();
|
||||
}
|
||||
|
||||
QString ChatRootView::getRuleContent(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_activeRules.size())
|
||||
return QString();
|
||||
|
||||
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
||||
m_activeRules[index].toMap()["filePath"].toString());
|
||||
}
|
||||
|
||||
void ChatRootView::refreshRules()
|
||||
{
|
||||
m_activeRules.clear();
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (!project) {
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto ruleFiles
|
||||
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||
|
||||
for (const auto &ruleFile : ruleFiles) {
|
||||
QVariantMap ruleMap;
|
||||
ruleMap["filePath"] = ruleFile.filePath;
|
||||
ruleMap["fileName"] = ruleFile.fileName;
|
||||
ruleMap["category"] = ruleFile.category;
|
||||
m_activeRules.append(ruleMap);
|
||||
}
|
||||
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
}
|
||||
|
||||
bool ChatRootView::useTools() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().enableChatTools();
|
||||
@@ -1249,10 +1194,7 @@ QString ChatRootView::lastInfoMessage() const
|
||||
|
||||
bool ChatRootView::isThinkingSupport() const
|
||||
{
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
||||
return m_agentController->currentSupportsThinking();
|
||||
}
|
||||
|
||||
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
||||
@@ -1273,66 +1215,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
|
||||
return imageExtensions.contains(fileInfo.suffix().toLower());
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableConfigurations()
|
||||
{
|
||||
m_configurationController->loadAvailableConfigurations();
|
||||
}
|
||||
|
||||
void ChatRootView::applyConfiguration(const QString &configName)
|
||||
{
|
||||
m_configurationController->applyConfiguration(configName);
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableConfigurations() const
|
||||
{
|
||||
return m_configurationController->availableConfigurations();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentConfiguration() const
|
||||
{
|
||||
return m_configurationController->currentConfiguration();
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableAgentRoles()
|
||||
{
|
||||
m_agentRoleController->loadAvailableRoles();
|
||||
}
|
||||
|
||||
void ChatRootView::applyAgentRole(const QString &roleName)
|
||||
{
|
||||
m_agentRoleController->applyRole(roleName);
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableAgentRoles() const
|
||||
{
|
||||
return m_agentRoleController->availableRoles();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRole() const
|
||||
{
|
||||
return m_agentRoleController->currentRole();
|
||||
}
|
||||
|
||||
QString ChatRootView::baseSystemPrompt() const
|
||||
{
|
||||
return m_agentRoleController->baseSystemPrompt();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleDescription() const
|
||||
{
|
||||
return m_agentRoleController->currentRoleDescription();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
||||
{
|
||||
return m_agentRoleController->currentRoleSystemPrompt();
|
||||
}
|
||||
|
||||
void ChatRootView::openAgentRolesSettings()
|
||||
{
|
||||
m_agentRoleController->openSettings();
|
||||
}
|
||||
|
||||
void ChatRootView::compressCurrentChat()
|
||||
{
|
||||
if (m_chatCompressor->isCompressing()) {
|
||||
@@ -1349,6 +1231,10 @@ void ChatRootView::compressCurrentChat()
|
||||
|
||||
autosave();
|
||||
|
||||
if (currentChatAgent().isEmpty())
|
||||
loadAvailableChatAgents();
|
||||
m_chatCompressor->setSessionManager(sessionManager());
|
||||
m_chatCompressor->setActiveAgent(currentChatAgent());
|
||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,18 +11,21 @@
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
class SessionManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatCompressor;
|
||||
class AgentRoleController;
|
||||
class ChatConfigurationController;
|
||||
class ChatAgentController;
|
||||
class FileEditController;
|
||||
class InputTokenCounter;
|
||||
class ChatHistoryStore;
|
||||
@@ -32,7 +35,6 @@ class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
@@ -46,8 +48,6 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
||||
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
||||
@@ -57,13 +57,8 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
|
||||
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
|
||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
||||
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
||||
@@ -75,7 +70,6 @@ public:
|
||||
~ChatRootView() override;
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
@@ -104,7 +98,6 @@ public:
|
||||
QString sendShortcutText() const;
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
Q_INVOKABLE void openChatHistoryFolder();
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
Q_INVOKABLE void openSettings();
|
||||
|
||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||
@@ -139,11 +132,6 @@ public:
|
||||
void setRequestProgressStatus(bool state);
|
||||
|
||||
QString lastErrorMessage() const;
|
||||
|
||||
QVariantList activeRules() const;
|
||||
int activeRulesCount() const;
|
||||
Q_INVOKABLE QString getRuleContent(int index);
|
||||
Q_INVOKABLE void refreshRules();
|
||||
|
||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||
|
||||
@@ -161,23 +149,14 @@ public:
|
||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
||||
|
||||
Q_INVOKABLE void loadAvailableConfigurations();
|
||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
Q_INVOKABLE void compressCurrentChat();
|
||||
Q_INVOKABLE void cancelCompression();
|
||||
|
||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||
Q_INVOKABLE void openAgentRolesSettings();
|
||||
QStringList availableAgentRoles() const;
|
||||
QString currentAgentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentAgentRoleDescription() const;
|
||||
QString currentAgentRoleSystemPrompt() const;
|
||||
|
||||
Q_INVOKABLE void loadAvailableChatAgents();
|
||||
QStringList availableChatAgents() const;
|
||||
QString currentChatAgent() const;
|
||||
void setCurrentChatAgent(const QString &name);
|
||||
|
||||
int currentMessageTotalEdits() const;
|
||||
int currentMessageAppliedEdits() const;
|
||||
int currentMessagePendingEdits() const;
|
||||
@@ -206,7 +185,6 @@ public slots:
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
void attachmentFilesChanged();
|
||||
void linkedFilesChanged();
|
||||
void inputTokensCountChanged();
|
||||
@@ -223,20 +201,15 @@ signals:
|
||||
void lastErrorMessageChanged();
|
||||
void lastInfoMessageChanged();
|
||||
void sendShortcutTextChanged();
|
||||
void activeRulesChanged();
|
||||
void activeRulesCountChanged();
|
||||
|
||||
void useToolsChanged();
|
||||
void useThinkingChanged();
|
||||
void currentMessageEditsStatsChanged();
|
||||
|
||||
void isThinkingSupportChanged();
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
void availableAgentRolesChanged();
|
||||
void currentAgentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
void availableChatAgentsChanged();
|
||||
void currentChatAgentChanged();
|
||||
|
||||
void isCompressingChanged();
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
@@ -269,12 +242,12 @@ private:
|
||||
|
||||
SessionFileRegistry *sessionFileRegistry() const;
|
||||
Skills::SkillsManager *skillsManager() const;
|
||||
AgentFactory *agentFactory() const;
|
||||
SessionManager *sessionManager() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||
ClientInterface *m_clientInterface;
|
||||
ChatFileManager *m_fileManager;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
@@ -294,13 +267,11 @@ private:
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
bool m_isRequestInProgress;
|
||||
QString m_lastErrorMessage;
|
||||
QVariantList m_activeRules;
|
||||
|
||||
|
||||
QString m_lastInfoMessage;
|
||||
|
||||
ChatCompressor *m_chatCompressor;
|
||||
AgentRoleController *m_agentRoleController;
|
||||
ChatConfigurationController *m_configurationController;
|
||||
ChatAgentController *m_agentController;
|
||||
FileEditController *m_fileEditController;
|
||||
InputTokenCounter *m_tokenCounter;
|
||||
ChatHistoryStore *m_historyStore;
|
||||
@@ -308,6 +279,8 @@ private:
|
||||
mutable bool m_sessionFileRegistryResolved = false;
|
||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||
mutable bool m_skillsManagerResolved = false;
|
||||
mutable QPointer<AgentFactory> m_agentFactory;
|
||||
mutable QPointer<SessionManager> m_sessionManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -4,65 +4,75 @@
|
||||
|
||||
#include "ClientInterface.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <projectexplorer/buildconfiguration.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImageReader>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QMimeDatabase>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include "tools/ReadOriginalHistoryTool.hpp"
|
||||
#include "tools/TodoTool.hpp"
|
||||
#include "tools/ToolsRegistration.hpp"
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "ToolsSettings.hpp"
|
||||
#include <RulesLoader.hpp>
|
||||
#include <context/ChangesManager.h>
|
||||
#include <sources/skills/SkillsManager.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_chatModel(chatModel)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{}
|
||||
|
||||
ClientInterface::~ClientInterface()
|
||||
{
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
||||
{
|
||||
m_skillsManager = skillsManager;
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface()
|
||||
void ClientInterface::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
cancelRequest();
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void ClientInterface::setActiveAgent(const QString &agentName)
|
||||
{
|
||||
m_activeAgent = agentName;
|
||||
}
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
@@ -72,6 +82,8 @@ void ClientInterface::sendMessage(
|
||||
bool useTools,
|
||||
bool useThinking)
|
||||
{
|
||||
Q_UNUSED(useThinking)
|
||||
|
||||
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
||||
LOG_MESSAGE("Ignoring empty chat message");
|
||||
return;
|
||||
@@ -84,13 +96,11 @@ void ClientInterface::sendMessage(
|
||||
|
||||
QList<QString> imageFiles;
|
||||
QList<QString> textFiles;
|
||||
|
||||
for (const QString &filePath : attachments) {
|
||||
if (isImageFile(filePath)) {
|
||||
if (isImageFile(filePath))
|
||||
imageFiles.append(filePath);
|
||||
} else {
|
||||
else
|
||||
textFiles.append(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
QList<Context::ContentFile> storedAttachments;
|
||||
@@ -116,9 +126,8 @@ void ClientInterface::sendMessage(
|
||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
for (const QString &imagePath : imageFiles) {
|
||||
QString base64Data = encodeImageToBase64(imagePath);
|
||||
if (base64Data.isEmpty()) {
|
||||
if (base64Data.isEmpty())
|
||||
continue;
|
||||
}
|
||||
|
||||
QString storedPath;
|
||||
QFileInfo fileInfo(imagePath);
|
||||
@@ -129,7 +138,6 @@ void ClientInterface::sendMessage(
|
||||
imageAttachment.storedPath = storedPath;
|
||||
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
||||
imageAttachments.append(imageAttachment);
|
||||
|
||||
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
||||
}
|
||||
}
|
||||
@@ -138,318 +146,302 @@ void ClientInterface::sendMessage(
|
||||
.arg(imageFiles.size()));
|
||||
}
|
||||
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
if (!m_sessionManager) {
|
||||
const QString error = QStringLiteral("Chat session manager is not available");
|
||||
LOG_MESSAGE(error);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
// Snapshot prior turns BEFORE the new user message is appended to the model.
|
||||
const QVector<ChatModel::Message> priorHistory = m_chatModel->getChatHistory();
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
m_chatModel
|
||||
->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
|
||||
if (!session) {
|
||||
const QString error = sessionError.isEmpty()
|
||||
? QStringLiteral("No chat agent selected")
|
||||
: sessionError;
|
||||
LOG_MESSAGE(error);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
const bool isToolsEnabled = useTools;
|
||||
|
||||
if (chatAssistantSettings.useSystemPrompt()) {
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (!role.id.isEmpty())
|
||||
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
||||
}
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
|
||||
if (project) {
|
||||
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
|
||||
systemPrompt += QString(
|
||||
"\n# Project source root: %1"
|
||||
"\n# All new source files, headers, QML and CMake edits MUST be "
|
||||
"created or modified under this directory. Use absolute paths "
|
||||
"rooted here, or project-relative paths.")
|
||||
.arg(project->projectDirectory().toUrlishString());
|
||||
|
||||
if (auto target = project->activeTarget()) {
|
||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||
systemPrompt
|
||||
+= QString(
|
||||
"\n# Build output directory (compiler artifacts only — do NOT "
|
||||
"create or edit source files here): %1")
|
||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||
}
|
||||
}
|
||||
|
||||
QString projectRules
|
||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
||||
}
|
||||
} else {
|
||||
systemPrompt += QString("\n# No active project in IDE");
|
||||
}
|
||||
|
||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
projectSkillDirs = Settings::SkillsSettings::splitLines(
|
||||
projectSettings.projectSkillDirs());
|
||||
}
|
||||
m_skillsManager->configure(
|
||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||
Settings::SkillsSettings::splitPaths(
|
||||
Settings::skillsSettings().globalSkillRoots()),
|
||||
projectSkillDirs);
|
||||
|
||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||
if (!alwaysOnSkills.isEmpty())
|
||||
systemPrompt += QString("\n\n") + alwaysOnSkills;
|
||||
|
||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||
if (!skillsCatalog.isEmpty())
|
||||
systemPrompt += QString("\n\n") + skillsCatalog;
|
||||
|
||||
static const QRegularExpression skillCommand(
|
||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||
QStringList invokedSkillNames;
|
||||
auto skillMatch = skillCommand.globalMatch(message);
|
||||
while (skillMatch.hasNext()) {
|
||||
const QString skillName = skillMatch.next().captured(1);
|
||||
if (invokedSkillNames.contains(skillName))
|
||||
continue;
|
||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||
invokedSkillNames << skillName;
|
||||
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||
.arg(invokedSkill->name, invokedSkill->body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||
}
|
||||
context.systemPrompt = systemPrompt;
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
const QString error = QStringLiteral("Chat agent has no live client");
|
||||
LOG_MESSAGE(error);
|
||||
m_sessionManager->removeSession(session);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool toolHistory = promptTemplate->supportsToolHistory();
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
int toolCallMsgIdx = -1;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
||||
if (!toolHistory || msg.toolName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolCallMsgIdx < 0) {
|
||||
PluginLLMCore::Message assistantCall;
|
||||
assistantCall.role = "assistant";
|
||||
messages.append(assistantCall);
|
||||
toolCallMsgIdx = messages.size() - 1;
|
||||
}
|
||||
|
||||
PluginLLMCore::ToolCall call;
|
||||
call.id = msg.id;
|
||||
call.name = msg.toolName;
|
||||
call.arguments = msg.toolArguments;
|
||||
messages[toolCallMsgIdx].toolCalls.append(call);
|
||||
|
||||
PluginLLMCore::Message toolResult;
|
||||
toolResult.role = "tool";
|
||||
toolResult.toolCallId = msg.id;
|
||||
toolResult.toolName = msg.toolName;
|
||||
toolResult.content = msg.toolResult;
|
||||
messages.append(toolResult);
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCallMsgIdx = -1;
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::FileEdit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
|
||||
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
apiMessage.content += "\n\nAttached files:";
|
||||
for (const auto &attachment : msg.attachments) {
|
||||
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
||||
if (!fileContent.isEmpty()) {
|
||||
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
||||
.arg(attachment.filename, decodedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
||||
apiMessage.isRedacted = msg.isRedacted;
|
||||
apiMessage.signature = msg.signature;
|
||||
|
||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
||||
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
||||
auto apiImages = loadImagesFromStorage(msg.images);
|
||||
if (!apiImages.isEmpty()) {
|
||||
apiMessage.images = apiImages;
|
||||
}
|
||||
}
|
||||
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
if (!imageFiles.isEmpty()
|
||||
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
||||
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
||||
.arg(provider->name(), QString::number(imageFiles.size())));
|
||||
}
|
||||
|
||||
context.history = messages;
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
context,
|
||||
PluginLLMCore::RequestType::Chat,
|
||||
useTools,
|
||||
useThinking);
|
||||
|
||||
provider->client()->setMaxToolContinuations(
|
||||
Settings::toolsSettings().maxToolContinuations());
|
||||
|
||||
provider->client()->setTransferTimeout(
|
||||
Tools::registerQodeAssistTools(client->tools());
|
||||
if (m_skillsManager)
|
||||
Tools::registerSkillTool(client->tools(), m_skillsManager);
|
||||
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ClientInterface::handlePartialResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&ClientInterface::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::toolStarted,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionStarted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::toolResultReady,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionCompleted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::thinkingBlockReceived,
|
||||
this,
|
||||
&ClientInterface::handleThinkingBlockReceived,
|
||||
Qt::UniqueConnection);
|
||||
const QString chatContext = buildChatContextLayer(message, linkedFiles);
|
||||
if (!chatContext.isEmpty())
|
||||
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
QJsonObject request{{"id", requestId}};
|
||||
seedHistory(*session->history(), priorHistory);
|
||||
|
||||
m_activeRequests[requestId] = {request, provider, !toolHistory};
|
||||
QString userText = message;
|
||||
if (!storedAttachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
userText += "\n\nAttached files:";
|
||||
for (const auto &attachment : storedAttachments) {
|
||||
QString fileContent
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
||||
if (!fileContent.isEmpty()) {
|
||||
QString decoded = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
userText += QString("\n\nFile: %1\n```\n%2\n```").arg(attachment.filename, decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit requestStarted(requestId);
|
||||
|
||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(userText));
|
||||
|
||||
if (!imageAttachments.isEmpty() && session->supportsImages() && !m_chatFilePath.isEmpty()) {
|
||||
for (const auto &image : imageAttachments) {
|
||||
QString base64
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, image.storedPath);
|
||||
if (base64.isEmpty())
|
||||
continue;
|
||||
blocks.push_back(std::make_unique<LLMQore::ImageContent>(
|
||||
base64, image.mediaType, LLMQore::ImageContent::ImageSourceType::Base64));
|
||||
}
|
||||
} else if (!imageAttachments.isEmpty() && !session->supportsImages()) {
|
||||
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
|
||||
.arg(m_activeAgent)
|
||||
.arg(imageAttachments.size()));
|
||||
}
|
||||
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::chunkReceived,
|
||||
this, &ClientInterface::handlePartialResponse, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::requestCompleted,
|
||||
this, &ClientInterface::handleFullResponse, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::requestFinalized,
|
||||
this, &ClientInterface::handleRequestFinalized, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::requestFailed,
|
||||
this, &ClientInterface::handleRequestFailed, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::toolStarted,
|
||||
this, &ClientInterface::handleToolExecutionStarted, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::toolResultReady,
|
||||
this, &ClientInterface::handleToolExecutionCompleted, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::thinkingBlockReceived,
|
||||
this, &ClientInterface::handleThinkingBlockReceived, Qt::UniqueConnection);
|
||||
|
||||
if (!m_chatFilePath.isEmpty()) {
|
||||
if (auto *todoTool
|
||||
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
|
||||
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
||||
provider->toolsManager()->tool("read_original_history"))) {
|
||||
client->tools()->tool("read_original_history"))) {
|
||||
historyTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
const LLMQore::RequestID requestId = session->send(std::move(blocks), useTools);
|
||||
if (requestId.isEmpty()) {
|
||||
const QString error = QStringLiteral("Failed to start chat request for agent: %1")
|
||||
.arg(m_activeAgent);
|
||||
LOG_MESSAGE(error);
|
||||
m_sessionManager->removeSession(session);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject request{{"id", requestId}};
|
||||
m_activeRequests[requestId] = {request, session, /*dropPreToolText=*/false};
|
||||
|
||||
emit requestStarted(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::seedHistory(
|
||||
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const
|
||||
{
|
||||
int i = 0;
|
||||
while (i < messages.size()) {
|
||||
const ChatModel::Message &msg = messages[i];
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
||||
Message assistant(Message::Role::Assistant);
|
||||
Message toolResults(Message::Role::User);
|
||||
while (i < messages.size() && messages[i].role == ChatModel::ChatRole::Tool) {
|
||||
const ChatModel::Message &toolMsg = messages[i];
|
||||
if (!toolMsg.toolName.isEmpty()) {
|
||||
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||
toolMsg.id, toolMsg.toolName, toolMsg.toolArguments));
|
||||
toolResults.appendBlock(
|
||||
std::make_unique<LLMQore::ToolResultContent>(toolMsg.id, toolMsg.toolResult));
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (!assistant.blocks().empty()) {
|
||||
history.append(std::move(assistant));
|
||||
history.append(std::move(toolResults));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::User) {
|
||||
Message userMessage(Message::Role::User);
|
||||
QString content = msg.content;
|
||||
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
content += "\n\nAttached files:";
|
||||
for (const auto &attachment : msg.attachments) {
|
||||
QString fileContent = ChatSerializer::loadContentFromStorage(
|
||||
m_chatFilePath, attachment.content);
|
||||
if (!fileContent.isEmpty()) {
|
||||
QString decoded
|
||||
= QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
content
|
||||
+= QString("\n\nFile: %1\n```\n%2\n```").arg(attachment.filename, decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
userMessage.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||
|
||||
if (!msg.images.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
for (const auto &image : msg.images) {
|
||||
QString base64 = ChatSerializer::loadContentFromStorage(
|
||||
m_chatFilePath, image.storedPath);
|
||||
if (base64.isEmpty())
|
||||
continue;
|
||||
userMessage.appendBlock(std::make_unique<LLMQore::ImageContent>(
|
||||
base64, image.mediaType, LLMQore::ImageContent::ImageSourceType::Base64));
|
||||
}
|
||||
}
|
||||
history.append(std::move(userMessage));
|
||||
} else { // Assistant
|
||||
if (msg.content.trimmed().isEmpty())
|
||||
continue;
|
||||
Message assistant(Message::Role::Assistant);
|
||||
assistant.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
|
||||
history.append(std::move(assistant));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
QString ClientInterface::buildChatContextLayer(
|
||||
const QString &message, const QList<QString> &linkedFiles) const
|
||||
{
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
QString context;
|
||||
|
||||
if (provider && !m_chatFilePath.isEmpty()
|
||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->clearSession(m_chatFilePath);
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (project) {
|
||||
context += QString("# Active project: %1").arg(project->displayName());
|
||||
context += QString(
|
||||
"\n# Project source root: %1"
|
||||
"\n# All new source files, headers, QML and CMake edits MUST be "
|
||||
"created or modified under this directory. Use absolute paths "
|
||||
"rooted here, or project-relative paths.")
|
||||
.arg(project->projectDirectory().toUrlishString());
|
||||
|
||||
if (auto target = project->activeTarget()) {
|
||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||
context += QString(
|
||||
"\n# Build output directory (compiler artifacts only — do NOT "
|
||||
"create or edit source files here): %1")
|
||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context += QString("# No active project in IDE");
|
||||
}
|
||||
|
||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
projectSkillDirs
|
||||
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
|
||||
}
|
||||
m_skillsManager->configure(
|
||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
|
||||
projectSkillDirs);
|
||||
|
||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||
if (!alwaysOnSkills.isEmpty())
|
||||
context += QString("\n\n") + alwaysOnSkills;
|
||||
|
||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||
if (!skillsCatalog.isEmpty())
|
||||
context += QString("\n\n") + skillsCatalog;
|
||||
|
||||
static const QRegularExpression skillCommand(
|
||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||
QStringList invokedSkillNames;
|
||||
auto skillMatch = skillCommand.globalMatch(message);
|
||||
while (skillMatch.hasNext()) {
|
||||
const QString skillName = skillMatch.next().captured(1);
|
||||
if (invokedSkillNames.contains(skillName))
|
||||
continue;
|
||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||
invokedSkillNames << skillName;
|
||||
context += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||
.arg(invokedSkill->name, invokedSkill->body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
context += "\n\nLinked files for reference:\n";
|
||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||
for (const auto &file : contentFiles)
|
||||
context += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
}
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
QSet<PluginLLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider->client(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
const RequestContext &ctx = it.value();
|
||||
if (ctx.provider) {
|
||||
ctx.provider->cancelRequest(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
const auto requests = m_activeRequests;
|
||||
m_activeRequests.clear();
|
||||
m_accumulatedResponses.clear();
|
||||
m_awaitingContinuation.clear();
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||
Session *session = it.value().session;
|
||||
if (!session)
|
||||
continue;
|
||||
if (auto *client = session->client())
|
||||
disconnect(client, nullptr, this, nullptr);
|
||||
if (m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
||||
@@ -486,23 +478,6 @@ QString ClientInterface::getCurrentFileContext() const
|
||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||
}
|
||||
|
||||
QString ClientInterface::getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const
|
||||
{
|
||||
QString updatedPrompt = basePrompt;
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||
|
||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||
for (const auto &file : contentFiles) {
|
||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedPrompt;
|
||||
}
|
||||
|
||||
Context::ContextManager *ClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
@@ -532,7 +507,8 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
const QJsonObject originalRequest = it.value().originalRequest;
|
||||
Session *session = it.value().session;
|
||||
|
||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
||||
|
||||
@@ -546,13 +522,16 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
||||
}
|
||||
|
||||
LOG_MESSAGE(
|
||||
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
|
||||
+ ": " + finalText);
|
||||
"Message completed. Final response for message " + originalRequest["id"].toString() + ": "
|
||||
+ finalText);
|
||||
emit messageReceivedCompletely();
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
m_awaitingContinuation.remove(requestId);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
void ClientInterface::handleRequestFinalized(
|
||||
@@ -584,12 +563,17 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
Session *session = it.value().session;
|
||||
|
||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||
emit errorOccurred(error);
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
m_awaitingContinuation.remove(requestId);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
void ClientInterface::handleThinkingBlockReceived(
|
||||
@@ -693,46 +677,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
||||
return imageData.toBase64();
|
||||
}
|
||||
|
||||
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||
const QList<ChatModel::ImageAttachment> &storedImages) const
|
||||
{
|
||||
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
||||
|
||||
for (const auto &storedImage : storedImages) {
|
||||
QString base64Data
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||
if (base64Data.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginLLMCore::ImageAttachment apiImage;
|
||||
apiImage.data = base64Data;
|
||||
apiImage.mediaType = storedImage.mediaType;
|
||||
apiImage.isUrl = false;
|
||||
|
||||
apiImages.append(apiImage);
|
||||
}
|
||||
|
||||
return apiImages;
|
||||
}
|
||||
|
||||
void ClientInterface::setChatFilePath(const QString &filePath)
|
||||
{
|
||||
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (provider
|
||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->clearSession(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_chatFilePath = filePath;
|
||||
m_chatModel->setChatFilePath(filePath);
|
||||
}
|
||||
|
||||
@@ -5,16 +5,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "pluginllmcore/IPromptProvider.hpp"
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
@@ -26,11 +31,12 @@ class ClientInterface : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setActiveAgent(const QString &agentName);
|
||||
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
@@ -42,7 +48,7 @@ public:
|
||||
void cancelRequest();
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
|
||||
void setChatFilePath(const QString &filePath);
|
||||
QString chatFilePath() const;
|
||||
|
||||
@@ -74,24 +80,26 @@ private slots:
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||
QString getCurrentFileContext() const;
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
||||
QString buildChatContextLayer(
|
||||
const QString &message, const QList<QString> &linkedFiles) const;
|
||||
void seedHistory(
|
||||
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const;
|
||||
bool isImageFile(const QString &filePath) const;
|
||||
QString getMediaTypeForImage(const QString &filePath) const;
|
||||
QString encodeImageToBase64(const QString &filePath) const;
|
||||
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
QPointer<Session> session;
|
||||
bool dropPreToolText = false;
|
||||
};
|
||||
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QString m_activeAgent;
|
||||
QString m_chatFilePath;
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
|
||||
@@ -42,12 +36,6 @@ InputTokenCounter::InputTokenCounter(
|
||||
this,
|
||||
&InputTokenCounter::recompute);
|
||||
|
||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
});
|
||||
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
}
|
||||
|
||||
@@ -74,24 +62,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::rewireToolsChangedConnection()
|
||||
{
|
||||
if (m_toolsChangedConn)
|
||||
QObject::disconnect(m_toolsChangedConn);
|
||||
m_toolsChangedConn = {};
|
||||
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
if (!provider)
|
||||
return;
|
||||
auto *tm = provider->toolsManager();
|
||||
if (!tm)
|
||||
return;
|
||||
|
||||
m_toolsChangedConn = connect(
|
||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
||||
}
|
||||
|
||||
void InputTokenCounter::recompute()
|
||||
{
|
||||
int inputTokens = m_messageTokens;
|
||||
@@ -136,21 +106,6 @@ void InputTokenCounter::recompute()
|
||||
inputTokens += 4; // + role
|
||||
}
|
||||
|
||||
if (settings.enableChatTools()) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
||||
providerName)) {
|
||||
if (auto *tm = provider->toolsManager()) {
|
||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
||||
if (!toolDefs.isEmpty()) {
|
||||
const QByteArray serialized
|
||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||
emit inputTokensChanged();
|
||||
}
|
||||
|
||||
@@ -37,11 +37,8 @@ signals:
|
||||
void inputTokensChanged();
|
||||
|
||||
private:
|
||||
void rewireToolsChangedConnection();
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
QMetaObject::Connection m_toolsChangedConn;
|
||||
|
||||
QStringList m_attachments;
|
||||
QStringList m_linkedFiles;
|
||||
|
||||
@@ -152,29 +152,17 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
settingsButton.onClicked: root.openSettings()
|
||||
configSelector {
|
||||
model: root.availableConfigurations
|
||||
displayText: root.currentConfiguration
|
||||
agentSelector {
|
||||
model: root.availableChatAgents
|
||||
displayText: root.currentChatAgent
|
||||
onActivated: function(index) {
|
||||
if (index > 0) {
|
||||
root.applyConfiguration(root.availableConfigurations[index])
|
||||
}
|
||||
root.currentChatAgent = root.availableChatAgents[index]
|
||||
}
|
||||
|
||||
Component.onCompleted: root.loadAvailableChatAgents()
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
roleSelector {
|
||||
model: root.availableAgentRoles
|
||||
displayText: root.currentAgentRole
|
||||
onActivated: function(index) {
|
||||
root.applyAgentRole(root.availableAgentRoles[index])
|
||||
}
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableAgentRoles()
|
||||
root.loadAvailableChatAgents()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,20 +827,7 @@ ChatRootView {
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
baseSystemPrompt: root.baseSystemPrompt
|
||||
currentAgentRole: root.currentAgentRole
|
||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||
activeRules: root.activeRules
|
||||
activeRulesCount: root.activeRulesCount
|
||||
|
||||
onOpenSettings: root.openSettings()
|
||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||
onOpenRulesFolder: root.openRulesFolder()
|
||||
onRefreshRules: root.refreshRules()
|
||||
onRuleSelected: function(index) {
|
||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -26,8 +26,7 @@ Rectangle {
|
||||
property alias toolsButton: toolsButtonId
|
||||
property alias thinkingMode: thinkingModeId
|
||||
property alias settingsButton: settingsButtonId
|
||||
property alias configSelector: configSelectorId
|
||||
property alias roleSelector: roleSelector
|
||||
property alias agentSelector: agentSelectorId
|
||||
property alias relocateTooltip: relocateTooltipId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
@@ -134,7 +133,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: configSelectorId
|
||||
id: agentSelectorId
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
@@ -142,24 +141,9 @@ Rectangle {
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: configSelectorId.hovered
|
||||
visible: agentSelectorId.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch saved AI configuration")
|
||||
}
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: roleSelector
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
model: []
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: roleSelector.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch agent role (different system prompts)")
|
||||
text: qsTr("Select chat agent (provider, model and role come from the agent)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
#include <settings/ButtonAspect.hpp>
|
||||
#include <QTimer>
|
||||
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void ConfigurationManager::init()
|
||||
{
|
||||
setupConnections();
|
||||
updateAllTemplateDescriptions();
|
||||
checkAllTemplate();
|
||||
}
|
||||
|
||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
||||
{
|
||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||
|
||||
if (!templ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
||||
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
||||
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
||||
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
|
||||
m_generalSettings.qrTemplateDescription.setValue(templ->description());
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::updateAllTemplateDescriptions()
|
||||
{
|
||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
||||
}
|
||||
|
||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
||||
{
|
||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||
|
||||
if (templ->name() == templateAspect.value())
|
||||
return;
|
||||
|
||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
||||
m_generalSettings.ccTemplate.setValue(templ->name());
|
||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
||||
m_generalSettings.caTemplate.setValue(templ->name());
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::checkAllTemplate()
|
||||
{
|
||||
checkTemplate(m_generalSettings.ccTemplate);
|
||||
checkTemplate(m_generalSettings.caTemplate);
|
||||
}
|
||||
|
||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_generalSettings(Settings::generalSettings())
|
||||
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
||||
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
||||
{}
|
||||
|
||||
void ConfigurationManager::setupConnections()
|
||||
{
|
||||
using Config = ConfigurationManager;
|
||||
using Button = ButtonAspect;
|
||||
|
||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||
connect(&m_generalSettings.qrSelectTemplate, &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.qrSetUrl, &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);
|
||||
|
||||
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
|
||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
||||
});
|
||||
|
||||
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
||||
});
|
||||
|
||||
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
|
||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectProvider()
|
||||
{
|
||||
const auto providersList = m_providersManager.providersNames();
|
||||
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
||||
? m_generalSettings.ccProvider
|
||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
||||
? m_generalSettings.ccPreset1Provider
|
||||
: settingsButton == &m_generalSettings.qrSelectProvider
|
||||
? m_generalSettings.qrProvider
|
||||
: m_generalSettings.caProvider;
|
||||
|
||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectModel()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
|
||||
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
|
||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
|
||||
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||
: isQuickRefactor ? m_generalSettings.qrModel
|
||||
: m_generalSettings.caModel);
|
||||
|
||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
provider->getInstalledModels(providerUrl)
|
||||
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
||||
return;
|
||||
}
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectTemplate()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
||||
|
||||
const auto templateList = isCodeCompletion || isPreset1
|
||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
||||
: isQuickRefactor ? m_generalSettings.qrTemplate
|
||||
: m_generalSettings.caTemplate;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectUrl()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
QStringList urls;
|
||||
for (const auto &name : m_providersManager.providersNames()) {
|
||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
||||
if (!urls.contains(url))
|
||||
urls.append(url);
|
||||
}
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
||||
? m_generalSettings.ccPreset1Url
|
||||
: settingsButton == &m_generalSettings.qrSetUrl
|
||||
? m_generalSettings.qrUrl
|
||||
: m_generalSettings.caUrl;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "pluginllmcore/PromptTemplateManager.hpp"
|
||||
#include "pluginllmcore/ProvidersManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ConfigurationManager &instance();
|
||||
|
||||
void init();
|
||||
|
||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
||||
void updateAllTemplateDescriptions();
|
||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
||||
void checkAllTemplate();
|
||||
|
||||
public slots:
|
||||
void selectProvider();
|
||||
void selectModel();
|
||||
void selectTemplate();
|
||||
void selectUrl();
|
||||
|
||||
private:
|
||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
||||
~ConfigurationManager() = default;
|
||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||
|
||||
Settings::GeneralSettings &m_generalSettings;
|
||||
PluginLLMCore::ProvidersManager &m_providersManager;
|
||||
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
||||
|
||||
void setupConnections();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
@@ -9,27 +9,39 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
#include <AgentFactory.hpp>
|
||||
#include <AgentRouter.hpp>
|
||||
#include <ResponseEvent.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include "sources/common/ContextData.hpp"
|
||||
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "context/Utils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include <pluginllmcore/RulesLoader.hpp>
|
||||
#include "sources/settings/PipelinesConfig.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMClientInterface::LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||
PluginLLMCore::IPromptProvider *promptProvider,
|
||||
SessionManager &sessionManager,
|
||||
AgentFactory &agentFactory,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger)
|
||||
: m_generalSettings(generalSettings)
|
||||
, m_completeSettings(completeSettings)
|
||||
, m_providerRegistry(providerRegistry)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_sessionManager(sessionManager)
|
||||
, m_agentFactory(agentFactory)
|
||||
, m_documentReader(documentReader)
|
||||
, m_performanceLogger(performanceLogger)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
@@ -51,58 +63,64 @@ void LLMClientInterface::startImpl()
|
||||
emit started();
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
void LLMClientInterface::onSessionEvent(const QString &requestId, const ResponseEvent &event)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
sendCompletionToClient(fullText, ctx.originalRequest, true);
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
if (event.kind() == ResponseEvent::Kind::TextDelta) {
|
||||
if (const auto *delta = event.as<ResponseEvents::TextDelta>())
|
||||
it.value().accumulated += delta->text;
|
||||
}
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId) || !info.usage)
|
||||
return;
|
||||
|
||||
const auto &u = *info.usage;
|
||||
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||
.arg(requestId)
|
||||
.arg(u.promptTokens)
|
||||
.arg(u.completionTokens)
|
||||
.arg(u.cachedPromptTokens)
|
||||
.arg(u.reasoningTokens));
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
void LLMClientInterface::onSessionFinished(const QString &requestId)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
const QString fullText = it.value().accumulated;
|
||||
const QJsonObject originalRequest = it.value().originalRequest;
|
||||
|
||||
sendCompletionToClient(fullText, originalRequest, true);
|
||||
finishRequest(requestId);
|
||||
}
|
||||
|
||||
void LLMClientInterface::onSessionFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||
|
||||
// Send LSP error response to client
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
|
||||
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
|
||||
|
||||
QJsonObject errorObject;
|
||||
errorObject["code"] = -32603; // Internal error code
|
||||
errorObject["message"] = error;
|
||||
response["error"] = errorObject;
|
||||
|
||||
|
||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||
|
||||
finishRequest(requestId);
|
||||
}
|
||||
|
||||
void LLMClientInterface::finishRequest(const QString &requestId)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
Session *session = it.value().session;
|
||||
m_activeRequests.erase(it);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
|
||||
if (session)
|
||||
m_sessionManager.removeSession(session);
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendData(const QByteArray &data)
|
||||
@@ -135,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
|
||||
void LLMClientInterface::handleCancelRequest()
|
||||
{
|
||||
QSet<PluginLLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider->client(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
const RequestContext &ctx = it.value();
|
||||
if (ctx.provider) {
|
||||
ctx.provider->cancelRequest(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
const auto requests = m_activeRequests;
|
||||
m_activeRequests.clear();
|
||||
|
||||
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||
m_performanceLogger.endTimeMeasurement(it.key());
|
||||
if (it.value().session)
|
||||
m_sessionManager.removeSession(it.value().session);
|
||||
}
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
}
|
||||
|
||||
@@ -237,133 +244,86 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
return;
|
||||
}
|
||||
|
||||
auto updatedContext = prepareContext(request, documentInfo);
|
||||
|
||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||
|
||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
||||
: m_generalSettings.ccPreset1Provider();
|
||||
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
|
||||
: m_generalSettings.ccPreset1Model();
|
||||
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
||||
: m_generalSettings.ccPreset1Url();
|
||||
|
||||
const auto provider = m_providerRegistry.getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
||||
const QString agentName = pickCompletionAgent(filePath);
|
||||
if (agentName.isEmpty()) {
|
||||
QString error = QString("No code completion agent matches: %1").arg(filePath);
|
||||
LOG_MESSAGE(error);
|
||||
sendErrorResponse(request, error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager.createSession(agentName, &sessionError);
|
||||
if (!session) {
|
||||
LOG_MESSAGE(sessionError);
|
||||
sendErrorResponse(request, sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
Templates::ContextData context = prepareContext(request, documentInfo);
|
||||
|
||||
if (!promptTemplate) {
|
||||
QString error = QString("No template found with name: %1").arg(templateName);
|
||||
QString editorContext;
|
||||
if (context.fileContext.has_value())
|
||||
editorContext.append(context.fileContext.value());
|
||||
|
||||
if (m_completeSettings.useOpenFilesContext())
|
||||
editorContext.append(m_contextManager->openedFilesContext({filePath}));
|
||||
|
||||
if (!editorContext.isEmpty())
|
||||
context.systemPrompt = editorContext;
|
||||
|
||||
connect(session, &Session::event, this, [this, session](const ResponseEvent &event) {
|
||||
onSessionEvent(requestIdForSession(session), event);
|
||||
});
|
||||
connect(session, &Session::finished, this, [this, session](const LLMQore::RequestID &, const QString &) {
|
||||
onSessionFinished(requestIdForSession(session));
|
||||
});
|
||||
connect(session, &Session::failed, this, [this, session](const LLMQore::RequestID &, const QString &error) {
|
||||
onSessionFailed(requestIdForSession(session), error);
|
||||
});
|
||||
|
||||
if (auto *client = session->client())
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
||||
|
||||
const LLMQore::RequestID requestId = session->sendCompletion(std::move(context));
|
||||
if (requestId.isEmpty()) {
|
||||
m_sessionManager.removeSession(session);
|
||||
QString error = QString("Failed to start completion request for agent: %1").arg(agentName);
|
||||
LOG_MESSAGE(error);
|
||||
sendErrorResponse(request, error);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
||||
|
||||
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
payload["stop"] = stopWords;
|
||||
|
||||
QString systemPrompt;
|
||||
if (m_completeSettings.useSystemPrompt())
|
||||
systemPrompt.append(
|
||||
m_completeSettings.useUserMessageTemplateForCC()
|
||||
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
||||
? m_completeSettings.systemPromptForNonFimModels()
|
||||
: m_completeSettings.systemPrompt());
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules
|
||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for completion");
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContext.fileContext.has_value())
|
||||
systemPrompt.append(updatedContext.fileContext.value());
|
||||
|
||||
if (m_completeSettings.useOpenFilesContext()) {
|
||||
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
||||
if (!updatedContext.filesMetadata) {
|
||||
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
||||
}
|
||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
||||
}
|
||||
} else {
|
||||
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
|
||||
}
|
||||
}
|
||||
|
||||
updatedContext.systemPrompt = systemPrompt;
|
||||
|
||||
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
||||
QString userMessage;
|
||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
||||
userMessage = m_completeSettings.processMessageToFIM(
|
||||
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
|
||||
} else {
|
||||
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
|
||||
}
|
||||
|
||||
// TODO refactor add message
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
messages.append({"user", userMessage});
|
||||
updatedContext.history = messages;
|
||||
}
|
||||
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
updatedContext,
|
||||
PluginLLMCore::RequestType::CodeCompletion,
|
||||
false,
|
||||
false);
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&LLMClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->client()->setTransferTimeout(
|
||||
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
||||
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
m_activeRequests[requestId] = {request, session, QString()};
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
|
||||
{
|
||||
const QStringList roster = Settings::PipelinesConfig::load().rosters.codeCompletion;
|
||||
if (roster.isEmpty())
|
||||
return {};
|
||||
|
||||
AgentRouter::Context ctx;
|
||||
ctx.filePath = filePath;
|
||||
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||
Utils::FilePath::fromString(filePath)))
|
||||
ctx.projectName = project->displayName();
|
||||
|
||||
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
|
||||
}
|
||||
|
||||
QString LLMClientInterface::requestIdForSession(Session *session) const
|
||||
{
|
||||
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||
if (it.value().session == session)
|
||||
return it.key();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Templates::ContextData LLMClientInterface::prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||
{
|
||||
QJsonObject params = request["params"].toObject();
|
||||
@@ -374,15 +334,14 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
|
||||
Context::DocumentContextReader
|
||||
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
const PluginLLMCore::ContextData legacy
|
||||
= reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
|
||||
QString LLMClientInterface::resolveEndpoint(
|
||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
||||
{
|
||||
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
: m_generalSettings.ccCustomEndpoint();
|
||||
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
||||
Templates::ContextData context;
|
||||
context.prefix = legacy.prefix;
|
||||
context.suffix = legacy.suffix;
|
||||
context.fileContext = legacy.fileContext;
|
||||
return context;
|
||||
}
|
||||
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
@@ -393,15 +352,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
void LLMClientInterface::sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
auto filePath = Context::extractFilePathFromRequest(request);
|
||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||
|
||||
QJsonObject response;
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <context/ProgrammingLanguage.hpp>
|
||||
#include <pluginllmcore/ContextData.hpp>
|
||||
#include <pluginllmcore/IPromptProvider.hpp>
|
||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||
#include <logger/IRequestPerformanceLogger.hpp>
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
@@ -23,6 +22,15 @@ class QNetworkAccessManager;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class SessionManager;
|
||||
class AgentFactory;
|
||||
class Session;
|
||||
class ResponseEvent;
|
||||
|
||||
namespace Templates {
|
||||
struct ContextData;
|
||||
}
|
||||
|
||||
class LLMClientInterface : public LanguageClient::BaseClientInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -31,8 +39,8 @@ public:
|
||||
LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||
PluginLLMCore::IPromptProvider *promptProvider,
|
||||
SessionManager &sessionManager,
|
||||
AgentFactory &agentFactory,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger);
|
||||
~LLMClientInterface() override;
|
||||
@@ -52,12 +60,6 @@ public:
|
||||
protected:
|
||||
void startImpl() override;
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void handleInitialize(const QJsonObject &request);
|
||||
void handleShutdown(const QJsonObject &request);
|
||||
@@ -67,22 +69,28 @@ private:
|
||||
void handleCancelRequest();
|
||||
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
||||
|
||||
void onSessionEvent(const QString &requestId, const ResponseEvent &event);
|
||||
void onSessionFinished(const QString &requestId);
|
||||
void onSessionFailed(const QString &requestId, const QString &error);
|
||||
void finishRequest(const QString &requestId);
|
||||
QString requestIdForSession(Session *session) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
QPointer<Session> session;
|
||||
QString accumulated;
|
||||
};
|
||||
|
||||
PluginLLMCore::ContextData prepareContext(
|
||||
Templates::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
|
||||
QString resolveEndpoint(
|
||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
|
||||
QString pickCompletionAgent(const QString &filePath) const;
|
||||
|
||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
||||
SessionManager &m_sessionManager;
|
||||
AgentFactory &m_agentFactory;
|
||||
Context::IDocumentReader &m_documentReader;
|
||||
IRequestPerformanceLogger &m_performanceLogger;
|
||||
QElapsedTimer m_completionTimer;
|
||||
|
||||
@@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||
}
|
||||
|
||||
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
|
||||
{
|
||||
m_agentFactory = agentFactory;
|
||||
}
|
||||
|
||||
QodeAssistClient::~QodeAssistClient()
|
||||
{
|
||||
cleanupConnections();
|
||||
@@ -319,6 +329,8 @@ void QodeAssistClient::requestQuickRefactor(
|
||||
|
||||
if (!m_refactorHandler) {
|
||||
m_refactorHandler = new QuickRefactorHandler(this);
|
||||
m_refactorHandler->setSessionManager(m_sessionManager);
|
||||
m_refactorHandler->setAgentFactory(m_agentFactory);
|
||||
connect(
|
||||
m_refactorHandler,
|
||||
&QuickRefactorHandler::refactoringCompleted,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LSPCompletion.hpp"
|
||||
@@ -21,6 +22,9 @@
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class SessionManager;
|
||||
class AgentFactory;
|
||||
|
||||
class QodeAssistClient : public LanguageClient::Client
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -28,6 +32,9 @@ public:
|
||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||
~QodeAssistClient() override;
|
||||
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setAgentFactory(AgentFactory *agentFactory);
|
||||
|
||||
void openDocument(TextEditor::TextDocument *document) override;
|
||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||
|
||||
@@ -68,6 +75,8 @@ private:
|
||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||
LLMClientInterface *m_llmClient;
|
||||
SessionManager *m_sessionManager{nullptr};
|
||||
AgentFactory *m_agentFactory{nullptr};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -4,24 +4,38 @@
|
||||
|
||||
#include "QuickRefactorHandler.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include <context/DocumentContextReader.hpp>
|
||||
#include <pluginllmcore/ResponseCleaner.hpp>
|
||||
#include <context/DocumentReaderQtCreator.hpp>
|
||||
#include <context/Utils.hpp>
|
||||
#include <pluginllmcore/PromptTemplateManager.hpp>
|
||||
#include <pluginllmcore/ProvidersManager.hpp>
|
||||
#include <pluginllmcore/RulesLoader.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/ChatAssistantSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <pluginllmcore/ResponseCleaner.hpp>
|
||||
#include <settings/QuickRefactorSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
|
||||
#include "sources/common/ContextData.hpp"
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
#include <AgentRouter.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include "sources/settings/PipelinesConfig.hpp"
|
||||
#include "tools/ToolsRegistration.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||
@@ -34,6 +48,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||
|
||||
QuickRefactorHandler::~QuickRefactorHandler() {}
|
||||
|
||||
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
|
||||
{
|
||||
m_agentFactory = agentFactory;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::sendRefactorRequest(
|
||||
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
||||
{
|
||||
@@ -88,61 +112,70 @@ void QuickRefactorHandler::sendRefactorRequest(
|
||||
prepareAndSendRequest(editor, instructions, range);
|
||||
}
|
||||
|
||||
QString QuickRefactorHandler::pickRefactorAgent(const QString &filePath) const
|
||||
{
|
||||
const QStringList roster = Settings::PipelinesConfig::load().rosters.quickRefactor;
|
||||
if (roster.isEmpty() || !m_agentFactory)
|
||||
return {};
|
||||
|
||||
AgentRouter::Context ctx;
|
||||
ctx.filePath = filePath;
|
||||
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||
Utils::FilePath::fromString(filePath)))
|
||||
ctx.projectName = project->displayName();
|
||||
|
||||
return AgentRouter::pickAgent(roster, ctx, *m_agentFactory);
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::prepareAndSendRequest(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &instructions,
|
||||
const Utils::Text::Range &range)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
|
||||
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
|
||||
|
||||
const auto providerName = settings.qrProvider();
|
||||
auto provider = providerRegistry.getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
||||
const auto emitError = [this, editor](const QString &error) {
|
||||
LOG_MESSAGE(error);
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = editor;
|
||||
emit refactoringCompleted(result);
|
||||
};
|
||||
|
||||
if (!m_sessionManager) {
|
||||
emitError(QStringLiteral("Quick refactor session manager is not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto templateName = settings.qrTemplate();
|
||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
QString error = QString("No template found with name: %1").arg(templateName);
|
||||
LOG_MESSAGE(error);
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = editor;
|
||||
emit refactoringCompleted(result);
|
||||
const QString filePath = editor->textDocument()->filePath().toUrlishString();
|
||||
const QString agentName = pickRefactorAgent(filePath);
|
||||
if (agentName.isEmpty()) {
|
||||
emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->createSession(agentName, &sessionError);
|
||||
if (!session) {
|
||||
emitError(sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
|
||||
: sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
m_sessionManager->removeSession(session);
|
||||
emitError(QStringLiteral("Quick refactor agent has no live client"));
|
||||
return;
|
||||
}
|
||||
|
||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
context,
|
||||
PluginLLMCore::RequestType::QuickRefactoring,
|
||||
enableTools,
|
||||
enableThinking);
|
||||
const bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||
if (enableTools) {
|
||||
Tools::registerQodeAssistTools(client->tools());
|
||||
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||
}
|
||||
|
||||
provider->client()->setMaxToolContinuations(
|
||||
Settings::toolsSettings().maxToolContinuations());
|
||||
session->systemPrompt()->setLayer(
|
||||
QStringLiteral("refactor"), buildSystemPrompt(editor, range));
|
||||
|
||||
provider->client()->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
@@ -150,43 +183,38 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
||||
m_isRefactoringInProgress = true;
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&QuickRefactorHandler::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
client, &::LLMQore::BaseClient::requestCompleted,
|
||||
this, &QuickRefactorHandler::handleFullResponse, Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
client, &::LLMQore::BaseClient::requestFinalized,
|
||||
this, &QuickRefactorHandler::handleRequestFinalized, Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
client, &::LLMQore::BaseClient::requestFailed,
|
||||
this, &QuickRefactorHandler::handleRequestFailed, Qt::UniqueConnection);
|
||||
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
const QString userMessage = instructions.isEmpty()
|
||||
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
|
||||
: instructions;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
|
||||
|
||||
const LLMQore::RequestID requestId = session->send(std::move(blocks), enableTools);
|
||||
if (requestId.isEmpty()) {
|
||||
m_isRefactoringInProgress = false;
|
||||
m_sessionManager->removeSession(session);
|
||||
emitError(QStringLiteral("Failed to start quick refactor request for agent: %1")
|
||||
.arg(agentName));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
||||
m_lastRequestId = requestId;
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions)
|
||||
QString QuickRefactorHandler::buildSystemPrompt(
|
||||
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
|
||||
{
|
||||
PluginLLMCore::ContextData context;
|
||||
Q_UNUSED(range)
|
||||
|
||||
auto textDocument = editor->textDocument();
|
||||
Context::DocumentReaderQtCreator documentReader;
|
||||
@@ -194,7 +222,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
|
||||
if (!documentInfo.document) {
|
||||
LOG_MESSAGE("Error: Document is not available");
|
||||
return context;
|
||||
return Settings::quickRefactorSettings().systemPrompt();
|
||||
}
|
||||
|
||||
QTextCursor cursor = editor->textCursor();
|
||||
@@ -270,17 +298,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
|
||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
||||
project, PluginLLMCore::RulesContext::QuickRefactor);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
||||
}
|
||||
}
|
||||
|
||||
systemPrompt += "\n\nFile information:";
|
||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||
@@ -294,7 +311,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
"\n- Your output will completely replace the selected code"
|
||||
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||
"\n- Your output will be inserted at the cursor location";
|
||||
|
||||
|
||||
systemPrompt += "\n\n## Formatting Rules:"
|
||||
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||
@@ -302,9 +319,9 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
"\n- Do NOT repeat existing code, be precise with context"
|
||||
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||
"\n- The output must be ready to insert directly into the editor as-is";
|
||||
|
||||
|
||||
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||
|
||||
|
||||
if (cursor.hasSelection()) {
|
||||
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||
int leadingSpaces = 0;
|
||||
@@ -336,7 +353,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
.arg(leadingSpaces);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||
"\n- Maintain consistent indentation for nested blocks"
|
||||
"\n- Do NOT remove or reduce the base indentation level"
|
||||
@@ -349,16 +366,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||
}
|
||||
|
||||
context.systemPrompt = systemPrompt;
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
messages.append(
|
||||
{"user",
|
||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
||||
: instructions});
|
||||
context.history = messages;
|
||||
|
||||
return context;
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleLLMResponse(
|
||||
@@ -398,10 +406,14 @@ void QuickRefactorHandler::cancelRequest()
|
||||
|
||||
auto it = m_activeRequests.find(id);
|
||||
if (it != m_activeRequests.end()) {
|
||||
auto provider = it.value().provider;
|
||||
Session *session = it.value().session;
|
||||
m_activeRequests.erase(it);
|
||||
if (provider)
|
||||
provider->cancelRequest(id);
|
||||
if (session) {
|
||||
if (auto *client = session->client())
|
||||
disconnect(client, nullptr, this, nullptr);
|
||||
if (m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
RefactorResult result;
|
||||
@@ -412,11 +424,19 @@ void QuickRefactorHandler::cancelRequest()
|
||||
|
||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
m_activeRequests.remove(requestId);
|
||||
QJsonObject request{{"id", requestId}};
|
||||
handleLLMResponse(fullText, request, true);
|
||||
}
|
||||
if (requestId != m_lastRequestId)
|
||||
return;
|
||||
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||
if (it != m_activeRequests.end())
|
||||
m_activeRequests.erase(it);
|
||||
|
||||
QJsonObject request{{"id", requestId}};
|
||||
handleLLMResponse(fullText, request, true);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFinalized(
|
||||
@@ -437,15 +457,23 @@ void QuickRefactorHandler::handleRequestFinalized(
|
||||
|
||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
m_activeRequests.remove(requestId);
|
||||
m_isRefactoringInProgress = false;
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = m_currentEditor;
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
if (requestId != m_lastRequestId)
|
||||
return;
|
||||
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||
if (it != m_activeRequests.end())
|
||||
m_activeRequests.erase(it);
|
||||
|
||||
m_isRefactoringInProgress = false;
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = m_currentEditor;
|
||||
emit refactoringCompleted(result);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <texteditor/texteditor.h>
|
||||
@@ -13,11 +14,13 @@
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <pluginllmcore/ContextData.hpp>
|
||||
#include <pluginllmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class AgentFactory;
|
||||
|
||||
struct RefactorResult
|
||||
{
|
||||
QString newText;
|
||||
@@ -35,6 +38,9 @@ public:
|
||||
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
||||
~QuickRefactorHandler() override;
|
||||
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setAgentFactory(AgentFactory *agentFactory);
|
||||
|
||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
||||
|
||||
void cancelRequest();
|
||||
@@ -56,17 +62,18 @@ private:
|
||||
const Utils::Text::Range &range);
|
||||
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
PluginLLMCore::ContextData prepareContext(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions);
|
||||
QString buildSystemPrompt(
|
||||
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
|
||||
QString pickRefactorAgent(const QString &filePath) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
QPointer<Session> session;
|
||||
};
|
||||
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
TextEditor::TextEditorWidget *m_currentEditor;
|
||||
Utils::Text::Range m_currentRange;
|
||||
|
||||
@@ -100,13 +100,9 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
|
||||
|
||||
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
||||
{
|
||||
const auto &generalSettings = Settings::generalSettings();
|
||||
|
||||
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
|
||||
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
|
||||
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
|
||||
|
||||
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
|
||||
Q_UNUSED(documentInfo)
|
||||
// Language-specific completion presets were replaced by agent match rules.
|
||||
return false;
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
|
||||
|
||||
258
docs/architecture.md
Normal file
258
docs/architecture.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# QodeAssist Architecture
|
||||
|
||||
This document describes the runtime architecture of QodeAssist after the
|
||||
migration of all LLM runtime paths onto the agent / `Session` stack
|
||||
("Stack B"). Every runtime LLM path — code completion, chat (send/stream +
|
||||
compression + token counting), and quick refactor — now goes through agents,
|
||||
`Session`, and the `Providers::GenericProvider` layer.
|
||||
|
||||
> Legend: ✅ = on Stack B (active runtime), 🔴 = legacy Stack A (isolated, no
|
||||
> runtime consumers left).
|
||||
|
||||
---
|
||||
|
||||
## 1. Top level: ownership and dependency injection
|
||||
|
||||
The plugin (`qodeassist.cpp`) owns everything via `new` + parent (no plugin-wide
|
||||
singletons; each feature receives its dependencies explicitly).
|
||||
|
||||
```
|
||||
QodeAssistPlugin
|
||||
Stack B infrastructure:
|
||||
• Providers::registerBuiltinProviders() — registers 13 client_api types
|
||||
• ProviderInstanceFactory — 14 instances from TOML
|
||||
• ProviderSecretsStore
|
||||
• AgentFactory — agents from TOML
|
||||
• SessionManager(agentFactory)
|
||||
• m_engine (QQmlEngine)
|
||||
rootContext: "agentFactory", "sessionManager" — DI for chat (QML)
|
||||
|
||||
Wired into consumers:
|
||||
• QodeAssistClient ← LLMClientInterface(*sessionManager, *agentFactory)
|
||||
← setSessionManager / setAgentFactory (for quick refactor)
|
||||
```
|
||||
|
||||
Chat lives in QML (`ChatRootView` is a `QML_ELEMENT`), so `AgentFactory` and
|
||||
`SessionManager` are exposed as **context properties on the engine's root
|
||||
context** and resolved in `ChatRootView` via
|
||||
`qmlEngine(this)->rootContext()->contextProperty(...)`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Stack B core (agent / Session)
|
||||
|
||||
```
|
||||
AgentFactory.create(name)
|
||||
configByName(name) → AgentConfig (TOML)
|
||||
providerInstance, model, endpoint, role, messageFormat,
|
||||
sampling, enableTools, enableThinking, match{filePatterns,...}
|
||||
buildProviderForAgent:
|
||||
instance = ProviderInstanceFactory.instanceByName(cfg.providerInstance)
|
||||
provider = ProviderFactory::create(instance.clientApi) ◄── keystone
|
||||
provider.setUrl(instance.url)
|
||||
provider.setApiKey(secrets.read(instance.apiKeyRef))
|
||||
▼
|
||||
Agent(config, provider)
|
||||
promptTemplate = JsonPromptTemplate::fromConfig(cfg.messageFormat) (inja)
|
||||
▼
|
||||
SessionManager.createSession(agentName) → Session(agent)
|
||||
├─ ConversationHistory — messages as ContentBlocks
|
||||
├─ SystemPromptBuilder — layers: agent.role + caller layers
|
||||
└─ ResponseRouter(client) — emits ResponseEvent
|
||||
|
||||
Session API:
|
||||
• send(blocks, toolsOverride) — chat/refactor: append user msg + dispatch
|
||||
• sendCompletion(ContextData) — completion: FIM prefix/suffix
|
||||
• client() — agent's LLMQore::BaseClient (direct streaming)
|
||||
• systemPrompt()->setLayer(...) — dynamic context layers
|
||||
• supportsImages() — provider Image capability
|
||||
• history() — for seeding from ChatModel
|
||||
```
|
||||
|
||||
`Session::sendCompletion` and `dispatch` compose `SystemPromptBuilder` layers
|
||||
(`agent.role` + caller-provided) into the request system prompt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Provider layer — the keystone (implemented during migration)
|
||||
|
||||
The Stack B provider layer previously existed only as an abstract base +
|
||||
empty factory (`registerType` was never called, no concrete providers). This
|
||||
blocked every agent from obtaining a working provider. It is now implemented
|
||||
via a single configuration-driven `GenericProvider`.
|
||||
|
||||
```
|
||||
ProviderFactory (sources/providers, namespace functions)
|
||||
registerType(name, fn) / create(name, parent) / knownNames()
|
||||
▲
|
||||
│ registerBuiltinProviders() — client_api → provider table
|
||||
│
|
||||
GenericProvider : Providers::Provider
|
||||
• owns an LLMQore::BaseClient (created by a ClientFactory)
|
||||
• prepareRequest — inherited from Provider base:
|
||||
delegates to PromptTemplate::buildFullRequest
|
||||
• client() / providerID() / capabilities() / getInstalledModels()
|
||||
```
|
||||
|
||||
### client_api → provider table
|
||||
|
||||
| client_api | LLMQore client | ProviderID | capabilities |
|
||||
|--------------------------------|-------------------------|------------------|-------------------------|
|
||||
| Claude | ClaudeClient | Claude | Tools·Thinking·Image·ModelListing |
|
||||
| Google AI | GoogleAIClient | GoogleAI | Tools·Thinking·Image·ModelListing |
|
||||
| llama.cpp | LlamaCppClient | LlamaCpp | Tools·Thinking·Image·ModelListing |
|
||||
| Mistral AI | MistralClient | MistralAI | Tools·Thinking·Image·ModelListing |
|
||||
| Codestral | MistralClient | MistralAI | Tools·Image |
|
||||
| Ollama (Native) | OllamaClient | Ollama | Tools·Thinking·Image·ModelListing |
|
||||
| Ollama (OpenAI-compatible) | OpenAIClient | OpenAICompatible | Tools·Thinking·Image·ModelListing |
|
||||
| OpenAI (Chat Completions) | OpenAIClient | OpenAI | Tools·Thinking·Image·ModelListing |
|
||||
| OpenAI (Responses API) | OpenAIResponsesClient | OpenAIResponses | Tools·Thinking·Image·ModelListing |
|
||||
| OpenAI Compatible | OpenAIClient | OpenAICompatible | Tools·Image·Thinking |
|
||||
| OpenRouter | OpenAIClient | OpenRouter | Tools·Image·Thinking·ModelListing |
|
||||
| LM Studio (Chat Completions) | OpenAIClient | LMStudio | Tools·Thinking·Image·ModelListing |
|
||||
| LM Studio (Responses API) | OpenAIResponsesClient | OpenAIResponses | Tools·Thinking·Image·ModelListing |
|
||||
|
||||
Request *shape* comes from the agent's prompt template (jinja `messageFormat`),
|
||||
so a single provider class covers every API by varying only the client factory
|
||||
and metadata.
|
||||
|
||||
---
|
||||
|
||||
## 4. Runtime paths (all on Stack B)
|
||||
|
||||
### 4a. Code completion ✅
|
||||
|
||||
```
|
||||
Qt Creator LSP (getCompletionsCycling)
|
||||
▼
|
||||
LLMClientInterface
|
||||
pickCompletionAgent: AgentRouter.pickAgent(roster.codeCompletion, {file, project})
|
||||
session = sessionManager.createSession(agent)
|
||||
ctx = Templates::ContextData{ prefix, suffix,
|
||||
systemPrompt = fileContext + openFiles }
|
||||
session.sendCompletion(ctx)
|
||||
▼ stream from session.client():
|
||||
requestCompleted → sendCompletionToClient → CodeHandler → LSP
|
||||
system prompt = agent.role; FIM template renders prefix/suffix
|
||||
```
|
||||
|
||||
### 4b. Chat ✅
|
||||
|
||||
```
|
||||
ChatRootView (QML)
|
||||
resolve agentFactory()/sessionManager() = qmlEngine(this)->rootContext()
|
||||
ChatAgentController: agent list (configNames), active agent (persisted),
|
||||
supportsThinking/Tools
|
||||
QML agent picker (TopBar.agentSelector) — replaced provider/model/template combos
|
||||
▼ dispatchSend
|
||||
ClientInterface
|
||||
session = sessionManager.createSession(currentChatAgent)
|
||||
registerQodeAssistTools(session.client().tools()) + registerSkillTool
|
||||
systemPrompt layer "chat.context" = project info + skills + linked files
|
||||
seedHistory(session.history() ← ChatModel: user/assistant/tool-call+result)
|
||||
session.send(userBlocks{text + images}, useTools)
|
||||
▼ stream from session.client() → existing handlers → ChatModel:
|
||||
chunk→addMessage thinking→addThinkingBlock
|
||||
tool→addToolExecutionStatus / updateToolResult
|
||||
finalized→usage completed→messageReceivedCompletely → removeSession
|
||||
|
||||
ChatCompressor → createSession(agent) → seed history → layer "compression" → send(prompt)
|
||||
InputTokenCounter → estimate without provider (calibrated by server usage)
|
||||
```
|
||||
|
||||
### 4c. Quick refactor ✅
|
||||
|
||||
```
|
||||
QodeAssistClient.requestQuickRefactor → QuickRefactorHandler (setSessionManager/setAgentFactory)
|
||||
pickRefactorAgent: AgentRouter.pickAgent(roster.quickRefactor, {file, project})
|
||||
session = createSession(agent)
|
||||
if useTools: registerQodeAssistTools(session.client().tools())
|
||||
systemPrompt layer "refactor" = buildSystemPrompt(tagged content +
|
||||
output requirements + indentation rules)
|
||||
session.send(blocks{instructions}, useTools)
|
||||
▼ stream from session.client():
|
||||
requestCompleted → ResponseCleaner → RefactorResult → insert into editor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration sources
|
||||
|
||||
```
|
||||
~/.config/.../qodeassist/config/
|
||||
providers/*.toml → ProviderInstance { name, client_api, url, api_key_ref }
|
||||
agents/*.toml → AgentConfig { providerInstance, model, endpoint, role,
|
||||
messageFormat, sampling, match, enable* }
|
||||
pipelines rosters → codeCompletion / chatAssistant / chatCompression / quickRefactor
|
||||
consumed by AgentRouter.pickAgent(roster, {filePath, projectName})
|
||||
|
||||
Editor policy (NOT agent config):
|
||||
CodeCompletionSettings — triggers, modelOutputHandler, context extraction,
|
||||
useOpenFilesContext
|
||||
(sampling / prompt-generation fields removed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Remaining Stack A (runtime does NOT depend on it)
|
||||
|
||||
```
|
||||
🔴 Settings UI: provider/model/template selection pages
|
||||
(ccProvider / caProvider / qrProvider) + ConfigurationManager
|
||||
→ use ProvidersManager
|
||||
🔴 root providers/* (PluginLLMCore::Provider, 14 classes)
|
||||
→ read only chat/quick-refactor sampling settings
|
||||
🔴 pluginllmcore/* (ProvidersManager, PromptTemplateManager, ResponseCleaner,
|
||||
PromptProviderChat/Fim, ContextData)
|
||||
🔴 qodeassist.cpp:144-146 registerProviders() / registerTemplates() (Stack A registration)
|
||||
🔴 qodeassist.cpp:185 MCP skill-tool loop on Stack A providers (effectively dead)
|
||||
🔴 ChatAssistantSettings / QuickRefactorSettings — sampling fields (read only by root providers)
|
||||
|
||||
ResponseCleaner (pluginllmcore) is still used by QuickRefactorHandler as a text
|
||||
utility — orthogonal to the provider stack.
|
||||
```
|
||||
|
||||
### Removed during the migration
|
||||
|
||||
- Rules subsystem (`RulesLoader` + chat "active rules" UI + QuickRefactor rules block)
|
||||
- `ChatConfigurationController`, `AgentRoleController` (chat config/role presets)
|
||||
- `m_promptProvider` (`PromptProviderFim`) in the plugin
|
||||
- `RequestType::CodeCompletion` branch in all 14 root providers
|
||||
- Sampling / prompt-generation fields in `CodeCompletionSettings`
|
||||
- ChatView no longer links `PluginLLMCore`
|
||||
|
||||
---
|
||||
|
||||
## 7. Dependency summary
|
||||
|
||||
```
|
||||
┌──────────────── Stack B (active runtime) ────────────────┐
|
||||
LLMClientInterface ─┐ │
|
||||
ClientInterface ────┼─► SessionManager ─► Session ─► Agent ─► GenericProvider ─► LLMQore::*Client
|
||||
QuickRefactorHandler─┘ │ │ │ │
|
||||
ChatCompressor ──────────────┘ │ AgentFactory ProviderFactory
|
||||
AgentRouter (rosters) │ │
|
||||
ProviderInstanceFactory (TOML)
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
|
||||
Stack A (settings UI + ConfigurationManager + MCP loop) — isolated,
|
||||
no runtime consumers remain.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Open follow-ups (optional)
|
||||
|
||||
1. **Chat picker filtering** — show only `chatAssistant`-roster agents (currently
|
||||
lists all non-hidden agents; the auto-default may land on a FIM agent).
|
||||
Requires wiring ChatView to `PipelinesConfig` (watch for OBJECT-library
|
||||
symbol duplication).
|
||||
2. **MCP tools on agent clients** — MCP skill tools are registered only on Stack A
|
||||
providers; to expose MCP tools to chat agents, register them on the session
|
||||
client alongside `registerQodeAssistTools`.
|
||||
3. **Physical Stack A teardown** — remove the provider/model/template settings UI,
|
||||
`ConfigurationManager`, root `providers/*`, `pluginllmcore/*`, and the
|
||||
registration + MCP loop in `qodeassist.cpp`. Runtime no longer depends on them.
|
||||
4. **Per-message session cost** — chat/refactor create a fresh agent/provider/client
|
||||
(and read secrets) per request; a session pool could reduce latency.
|
||||
```
|
||||
@@ -10,7 +10,6 @@ add_library(PluginLLMCore STATIC
|
||||
PromptTemplate.hpp
|
||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||
ProviderID.hpp
|
||||
RulesLoader.hpp RulesLoader.cpp
|
||||
ResponseCleaner.hpp
|
||||
)
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "RulesLoader.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
|
||||
QString RulesLoader::loadRules(const QString &projectPath, RulesContext context)
|
||||
{
|
||||
if (projectPath.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString combined;
|
||||
QString basePath = projectPath + "/.qodeassist/rules";
|
||||
|
||||
switch (context) {
|
||||
case RulesContext::Completions:
|
||||
combined += loadAllMarkdownFiles(basePath + "/completions");
|
||||
break;
|
||||
case RulesContext::Chat:
|
||||
combined += loadAllMarkdownFiles(basePath + "/common");
|
||||
combined += loadAllMarkdownFiles(basePath + "/chat");
|
||||
break;
|
||||
case RulesContext::QuickRefactor:
|
||||
combined += loadAllMarkdownFiles(basePath + "/common");
|
||||
combined += loadAllMarkdownFiles(basePath + "/quickrefactor");
|
||||
break;
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
QString RulesLoader::loadRulesForProject(ProjectExplorer::Project *project, RulesContext context)
|
||||
{
|
||||
if (!project) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString projectPath = getProjectPath(project);
|
||||
return loadRules(projectPath, context);
|
||||
}
|
||||
|
||||
ProjectExplorer::Project *RulesLoader::getActiveProject()
|
||||
{
|
||||
auto currentEditor = Core::EditorManager::currentEditor();
|
||||
if (currentEditor && currentEditor->document()) {
|
||||
Utils::FilePath filePath = currentEditor->document()->filePath();
|
||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
||||
if (project) {
|
||||
return project;
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectExplorer::ProjectManager::startupProject();
|
||||
}
|
||||
|
||||
QString RulesLoader::loadAllMarkdownFiles(const QString &dirPath)
|
||||
{
|
||||
QString combined;
|
||||
QDir dir(dirPath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
|
||||
|
||||
for (const QString &fileName : mdFiles) {
|
||||
QFile file(dir.filePath(fileName));
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
combined += file.readAll();
|
||||
combined += "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
QString RulesLoader::getProjectPath(ProjectExplorer::Project *project)
|
||||
{
|
||||
if (!project) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return project->projectDirectory().toUrlishString();
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context)
|
||||
{
|
||||
if (projectPath.isEmpty()) {
|
||||
return QVector<RuleFileInfo>();
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> result;
|
||||
QString basePath = projectPath + "/.qodeassist/rules";
|
||||
|
||||
// Always include common rules
|
||||
result.append(collectMarkdownFiles(basePath + "/common", "common"));
|
||||
|
||||
// Add context-specific rules
|
||||
switch (context) {
|
||||
case RulesContext::Completions:
|
||||
result.append(collectMarkdownFiles(basePath + "/completions", "completions"));
|
||||
break;
|
||||
case RulesContext::Chat:
|
||||
result.append(collectMarkdownFiles(basePath + "/chat", "chat"));
|
||||
break;
|
||||
case RulesContext::QuickRefactor:
|
||||
result.append(collectMarkdownFiles(basePath + "/quickrefactor", "quickrefactor"));
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> RulesLoader::getRuleFilesForProject(
|
||||
ProjectExplorer::Project *project, RulesContext context)
|
||||
{
|
||||
if (!project) {
|
||||
return QVector<RuleFileInfo>();
|
||||
}
|
||||
|
||||
QString projectPath = getProjectPath(project);
|
||||
return getRuleFiles(projectPath, context);
|
||||
}
|
||||
|
||||
QString RulesLoader::loadRuleFileContent(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
QVector<RuleFileInfo> RulesLoader::collectMarkdownFiles(
|
||||
const QString &dirPath, const QString &category)
|
||||
{
|
||||
QVector<RuleFileInfo> result;
|
||||
QDir dir(dirPath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
|
||||
|
||||
for (const QString &fileName : mdFiles) {
|
||||
QString fullPath = dir.filePath(fileName);
|
||||
result.append({fullPath, fileName, category});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::PluginLLMCore
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace ProjectExplorer {
|
||||
class Project;
|
||||
}
|
||||
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
|
||||
enum class RulesContext { Completions, Chat, QuickRefactor };
|
||||
|
||||
struct RuleFileInfo
|
||||
{
|
||||
QString filePath;
|
||||
QString fileName;
|
||||
QString category; // "common", "chat", "completions", "quickrefactor"
|
||||
};
|
||||
|
||||
class RulesLoader
|
||||
{
|
||||
public:
|
||||
static QString loadRules(const QString &projectPath, RulesContext context);
|
||||
static QString loadRulesForProject(ProjectExplorer::Project *project, RulesContext context);
|
||||
static ProjectExplorer::Project *getActiveProject();
|
||||
|
||||
// New methods for getting rule files info
|
||||
static QVector<RuleFileInfo> getRuleFiles(const QString &projectPath, RulesContext context);
|
||||
static QVector<RuleFileInfo> getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context);
|
||||
static QString loadRuleFileContent(const QString &filePath);
|
||||
|
||||
private:
|
||||
static QString loadAllMarkdownFiles(const QString &dirPath);
|
||||
static QVector<RuleFileInfo> collectMarkdownFiles(const QString &dirPath, const QString &category);
|
||||
static QString getProjectPath(ProjectExplorer::Project *project);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::PluginLLMCore
|
||||
@@ -94,10 +94,7 @@ void ClaudeProvider::prepareRequest(
|
||||
request["temperature"] = 1.0;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
request["temperature"] = Settings::codeCompletionSettings().temperature();
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
applyModelParams(qrSettings);
|
||||
|
||||
|
||||
@@ -86,9 +86,7 @@ void DeepSeekProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -92,9 +92,7 @@ void GoogleAIProvider::prepareRequest(
|
||||
request["generationConfig"] = generationConfig;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
|
||||
if (isThinkingEnabled) {
|
||||
|
||||
@@ -90,9 +90,7 @@ void LMStudioProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -82,9 +82,7 @@ void LMStudioResponsesProvider::prepareRequest(
|
||||
request["include"] = include;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
applyModelParams(qrSettings);
|
||||
|
||||
|
||||
@@ -75,9 +75,7 @@ void LlamaCppProvider::prepareRequest(
|
||||
request["chat_template_kwargs"] = chatTemplateKwargs;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
if (isThinkingEnabled) {
|
||||
applyThinkingMode();
|
||||
|
||||
@@ -88,9 +88,7 @@ void MistralAIProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -70,9 +70,7 @@ void OllamaCompatProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -82,9 +82,7 @@ void OllamaProvider::prepareRequest(
|
||||
request["options"] = options;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applySettings(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
applySettings(qrSettings);
|
||||
|
||||
|
||||
@@ -69,9 +69,7 @@ void OpenAICompatProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -88,9 +88,7 @@ void OpenAIProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -80,9 +80,7 @@ void OpenAIResponsesProvider::prepareRequest(
|
||||
request["include"] = include;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
applyModelParams(qrSettings);
|
||||
|
||||
|
||||
@@ -88,9 +88,7 @@ void QwenProvider::prepareRequest(
|
||||
request["presence_penalty"] = settings.presencePenalty();
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
applyModelParams(Settings::quickRefactorSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
|
||||
@@ -80,9 +80,7 @@ void QwenResponsesProvider::prepareRequest(
|
||||
request["include"] = include;
|
||||
};
|
||||
|
||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||
applyModelParams(qrSettings);
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
#include <QTranslator>
|
||||
|
||||
#include <QInputDialog>
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "QodeAssistClient.hpp"
|
||||
#include "UpdateStatusWidget.hpp"
|
||||
#include "Version.hpp"
|
||||
@@ -43,7 +42,6 @@
|
||||
#include "chat/ChatOutputPane.h"
|
||||
#include "chat/NavigationPanel.hpp"
|
||||
#include "context/DocumentReaderQtCreator.hpp"
|
||||
#include "pluginllmcore/PromptProviderFim.hpp"
|
||||
#include "pluginllmcore/ProvidersManager.hpp"
|
||||
#include "logger/RequestPerformanceLogger.hpp"
|
||||
#include "mcp/McpClientsManager.hpp"
|
||||
@@ -56,7 +54,6 @@
|
||||
#include "settings/ProjectSettingsPanel.hpp"
|
||||
#include "settings/AgentsSettingsPage.hpp"
|
||||
#include "settings/ProvidersSettingsPage.hpp"
|
||||
#include "sources/settings/AgentPipelinesPage.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
|
||||
@@ -65,6 +62,8 @@
|
||||
#include "ProviderSecretsStore.hpp"
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
#include <GenericProvider.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include "templates/Templates.hpp"
|
||||
#include "widgets/CustomInstructionsManager.hpp"
|
||||
#include "widgets/QuickRefactorDialog.hpp"
|
||||
@@ -74,6 +73,7 @@
|
||||
#include <ChatView/ChatWidget.hpp>
|
||||
#include <ChatView/SessionFileRegistry.hpp>
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <QQmlContext>
|
||||
#include <QUuid>
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
@@ -95,7 +95,6 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin
|
||||
public:
|
||||
QodeAssistPlugin()
|
||||
: m_updater(new PluginUpdater(this))
|
||||
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
||||
{}
|
||||
|
||||
~QodeAssistPlugin() final
|
||||
@@ -203,8 +202,8 @@ public:
|
||||
m_engine, m_sessionFileRegistry, m_skillsManager};
|
||||
|
||||
Settings::setupProjectPanel();
|
||||
ConfigurationManager::instance().init();
|
||||
|
||||
Providers::registerBuiltinProviders();
|
||||
m_providerInstanceFactory = new Providers::ProviderInstanceFactory(this);
|
||||
m_providerSecretsStore = new Providers::ProviderSecretsStore(this);
|
||||
m_providerLauncher = new Providers::ProviderLauncher(this);
|
||||
@@ -216,6 +215,9 @@ public:
|
||||
m_providersPageNavigator);
|
||||
|
||||
m_agentFactory = new AgentFactory(m_providerInstanceFactory, m_providerSecretsStore, this);
|
||||
m_sessionManager = new SessionManager(m_agentFactory, this);
|
||||
m_engine->rootContext()->setContextProperty("agentFactory", m_agentFactory);
|
||||
m_engine->rootContext()->setContextProperty("sessionManager", m_sessionManager);
|
||||
m_agentsPageNavigator = new Settings::AgentsPageNavigator(this);
|
||||
m_agentsOptionsPage = Settings::createAgentsSettingsPage(
|
||||
m_agentFactory, m_agentsPageNavigator);
|
||||
@@ -342,10 +344,12 @@ public:
|
||||
m_qodeAssistClient = new QodeAssistClient(new LLMClientInterface(
|
||||
Settings::generalSettings(),
|
||||
Settings::codeCompletionSettings(),
|
||||
PluginLLMCore::ProvidersManager::instance(),
|
||||
&m_promptProvider,
|
||||
*m_sessionManager,
|
||||
*m_agentFactory,
|
||||
m_documentReader,
|
||||
m_performanceLogger));
|
||||
m_qodeAssistClient->setSessionManager(m_sessionManager);
|
||||
m_qodeAssistClient->setAgentFactory(m_agentFactory);
|
||||
}
|
||||
|
||||
bool delayedInitialize() final
|
||||
@@ -503,7 +507,6 @@ private:
|
||||
}
|
||||
|
||||
QPointer<QodeAssistClient> m_qodeAssistClient;
|
||||
PluginLLMCore::PromptProviderFim m_promptProvider;
|
||||
Context::DocumentReaderQtCreator m_documentReader;
|
||||
RequestPerformanceLogger m_performanceLogger;
|
||||
QPointer<Chat::ChatOutputPane> m_chatOutputPane;
|
||||
@@ -524,6 +527,7 @@ private:
|
||||
QPointer<Settings::ProvidersPageNavigator> m_providersPageNavigator;
|
||||
std::unique_ptr<Core::IOptionsPage> m_providersOptionsPage;
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QPointer<Settings::AgentsPageNavigator> m_agentsPageNavigator;
|
||||
std::unique_ptr<Core::IOptionsPage> m_agentsOptionsPage;
|
||||
QPointer<Settings::AgentPipelinesPageNavigator> m_agentPipelinesPageNavigator;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
add_library(QodeAssistSettings STATIC
|
||||
GeneralSettings.hpp GeneralSettings.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
SettingsUtils.hpp
|
||||
SettingsConstants.hpp
|
||||
ButtonAspect.hpp
|
||||
@@ -47,5 +46,9 @@ target_link_libraries(QodeAssistSettings
|
||||
ProvidersConfig
|
||||
Agents
|
||||
Skills
|
||||
QodeAssistAgentPipelines
|
||||
)
|
||||
target_include_directories(QodeAssistSettings
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/sources/settings
|
||||
)
|
||||
target_include_directories(QodeAssistSettings PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
@@ -165,54 +165,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
"for triggering completions. This helps trigger completions based on actual code "
|
||||
"characters only."));
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CC_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
temperature.setDefaultValue(0.2);
|
||||
temperature.setRange(0.0, 2.0);
|
||||
temperature.setSingleStep(0.1);
|
||||
|
||||
maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens:"));
|
||||
maxTokens.setRange(-1, 900000);
|
||||
maxTokens.setDefaultValue(500);
|
||||
|
||||
// Advanced Parameters
|
||||
useTopP.setSettingsKey(Constants::CC_USE_TOP_P);
|
||||
useTopP.setDefaultValue(false);
|
||||
useTopP.setLabelText(Tr::tr("Top P:"));
|
||||
|
||||
topP.setSettingsKey(Constants::CC_TOP_P);
|
||||
topP.setDefaultValue(0.9);
|
||||
topP.setRange(0.0, 1.0);
|
||||
topP.setSingleStep(0.1);
|
||||
|
||||
useTopK.setSettingsKey(Constants::CC_USE_TOP_K);
|
||||
useTopK.setDefaultValue(false);
|
||||
useTopK.setLabelText(Tr::tr("Top K:"));
|
||||
|
||||
topK.setSettingsKey(Constants::CC_TOP_K);
|
||||
topK.setDefaultValue(50);
|
||||
topK.setRange(1, 1000);
|
||||
|
||||
usePresencePenalty.setSettingsKey(Constants::CC_USE_PRESENCE_PENALTY);
|
||||
usePresencePenalty.setDefaultValue(false);
|
||||
usePresencePenalty.setLabelText(Tr::tr("Presence Penalty:"));
|
||||
|
||||
presencePenalty.setSettingsKey(Constants::CC_PRESENCE_PENALTY);
|
||||
presencePenalty.setDefaultValue(0.0);
|
||||
presencePenalty.setRange(-2.0, 2.0);
|
||||
presencePenalty.setSingleStep(0.1);
|
||||
|
||||
useFrequencyPenalty.setSettingsKey(Constants::CC_USE_FREQUENCY_PENALTY);
|
||||
useFrequencyPenalty.setDefaultValue(false);
|
||||
useFrequencyPenalty.setLabelText(Tr::tr("Frequency Penalty:"));
|
||||
|
||||
frequencyPenalty.setSettingsKey(Constants::CC_FREQUENCY_PENALTY);
|
||||
frequencyPenalty.setDefaultValue(0.0);
|
||||
frequencyPenalty.setRange(-2.0, 2.0);
|
||||
frequencyPenalty.setSingleStep(0.1);
|
||||
|
||||
// Context Settings
|
||||
readFullFile.setSettingsKey(Constants::CC_READ_FULL_FILE);
|
||||
readFullFile.setLabelText(Tr::tr("Read Full File"));
|
||||
@@ -240,43 +192,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
|
||||
"precise and contextually appropriate code completions.\n\n");
|
||||
|
||||
useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE);
|
||||
useUserMessageTemplateForCC.setDefaultValue(true);
|
||||
useUserMessageTemplateForCC.setLabelText(
|
||||
Tr::tr("Use special system prompt and user message for non FIM models"));
|
||||
|
||||
systemPromptForNonFimModels.setSettingsKey(Constants::CC_SYSTEM_PROMPT_FOR_NON_FIM);
|
||||
systemPromptForNonFimModels.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPromptForNonFimModels.setLabelText(Tr::tr("System prompt for non FIM models:"));
|
||||
systemPromptForNonFimModels.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
|
||||
"precise and contextually appropriate code completions.\n\n"
|
||||
"Core Requirements:\n"
|
||||
"1. Continue code exactly from the cursor position, ensuring it properly connects with any "
|
||||
"existing code after the cursor\n"
|
||||
"2. Never repeat existing code before or after the cursor\n"
|
||||
"Specific Guidelines:\n"
|
||||
"- For function calls: Complete parameters with appropriate types and names\n"
|
||||
"- For class members: Respect access modifiers and class conventions\n"
|
||||
"- Respect existing indentation and formatting\n"
|
||||
"- Consider scope and visibility of referenced symbols\n"
|
||||
"- Ensure seamless integration with code both before and after the cursor\n\n"
|
||||
"Context Format:\n"
|
||||
"<code_context>\n"
|
||||
"{{code before cursor}}<cursor>{{code after cursor}}\n"
|
||||
"</code_context>\n\n"
|
||||
"Response Format:\n"
|
||||
"- No explanations or comments\n"
|
||||
"- Only include new characters needed to create valid code\n"
|
||||
"- Should be codeblock with language\n");
|
||||
|
||||
userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE);
|
||||
userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
userMessageTemplateForCC.setLabelText(Tr::tr("User message for non FIM models:"));
|
||||
userMessageTemplateForCC.setDefaultValue(
|
||||
"Here is the code context with insertion points:\n"
|
||||
"<code_context>\n${prefix}<cursor>${suffix}\n</code_context>\n\n");
|
||||
|
||||
customLanguages.setSettingsKey(Constants::CC_CUSTOM_LANGUAGES);
|
||||
customLanguages.setLabelText(
|
||||
Tr::tr("Additional Programming Languages for handling: Example: rust,//,rust rs,rs"));
|
||||
@@ -311,39 +226,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
maxChangesCacheSize.setRange(2, 1000);
|
||||
maxChangesCacheSize.setDefaultValue(10);
|
||||
|
||||
// Ollama Settings
|
||||
ollamaLivetime.setSettingsKey(Constants::CC_OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setToolTip(
|
||||
Tr::tr("Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
ollamaLivetime.setLabelText("Livetime:");
|
||||
ollamaLivetime.setDefaultValue("5m");
|
||||
ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
contextWindow.setSettingsKey(Constants::CC_OLLAMA_CONTEXT_WINDOW);
|
||||
contextWindow.setLabelText(Tr::tr("Context Window:"));
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::CC_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr("Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
@@ -357,23 +239,6 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
auto genGrid = Grid{};
|
||||
genGrid.addRow({Row{temperature}});
|
||||
genGrid.addRow({Row{maxTokens}});
|
||||
|
||||
auto advancedGrid = Grid{};
|
||||
advancedGrid.addRow({useTopP, topP});
|
||||
advancedGrid.addRow({useTopK, topK});
|
||||
advancedGrid.addRow({usePresencePenalty, presencePenalty});
|
||||
advancedGrid.addRow({useFrequencyPenalty, frequencyPenalty});
|
||||
|
||||
auto ollamaGrid = Grid{};
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto contextGrid = Grid{};
|
||||
contextGrid.addRow({Row{readFullFile}});
|
||||
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
|
||||
@@ -382,14 +247,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
Row{contextGrid, Stretch{1}},
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
Group{title(Tr::tr("Prompts for FIM models")), Column{systemPrompt}},
|
||||
Group{
|
||||
title(Tr::tr("Prompts for Non FIM models")),
|
||||
Column{
|
||||
Row{useUserMessageTemplateForCC, Stretch{1}},
|
||||
systemPromptForNonFimModels,
|
||||
userMessageTemplateForCC,
|
||||
customLanguages,
|
||||
}},
|
||||
customLanguages,
|
||||
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
|
||||
|
||||
auto generalSettings = Column{
|
||||
@@ -418,19 +276,7 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("General Parameters")),
|
||||
Column{
|
||||
Row{genGrid, Stretch{1}},
|
||||
}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")),
|
||||
Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenAI Responses API")), Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
@@ -470,16 +316,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(autoCompletion);
|
||||
resetAspect(multiLineCompletion);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
resetAspect(topP);
|
||||
resetAspect(useTopK);
|
||||
resetAspect(topK);
|
||||
resetAspect(usePresencePenalty);
|
||||
resetAspect(presencePenalty);
|
||||
resetAspect(useFrequencyPenalty);
|
||||
resetAspect(frequencyPenalty);
|
||||
resetAspect(readFullFile);
|
||||
resetAspect(readFileParts);
|
||||
resetAspect(readStringsBeforeCursor);
|
||||
@@ -488,12 +324,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
resetAspect(systemPrompt);
|
||||
resetAspect(useProjectChangesCache);
|
||||
resetAspect(maxChangesCacheSize);
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(useUserMessageTemplateForCC);
|
||||
resetAspect(userMessageTemplateForCC);
|
||||
resetAspect(systemPromptForNonFimModels);
|
||||
resetAspect(customLanguages);
|
||||
resetAspect(showProgressWidget);
|
||||
resetAspect(useOpenFilesContext);
|
||||
@@ -527,14 +357,6 @@ void CodeCompletionSettings::migrateCompletionMode()
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const
|
||||
{
|
||||
QString result = userMessageTemplateForCC();
|
||||
result.replace("${prefix}", prefix);
|
||||
result.replace("${suffix}", suffix);
|
||||
return result;
|
||||
}
|
||||
|
||||
class CodeCompletionSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -41,23 +41,6 @@ public:
|
||||
Utils::BoolAspect abortAssistOnRequest{this};
|
||||
Utils::BoolAspect useOpenFilesContext{this};
|
||||
|
||||
// General Parameters Settings
|
||||
Utils::DoubleAspect temperature{this};
|
||||
Utils::IntegerAspect maxTokens{this};
|
||||
|
||||
// Advanced Parameters
|
||||
Utils::BoolAspect useTopP{this};
|
||||
Utils::DoubleAspect topP{this};
|
||||
|
||||
Utils::BoolAspect useTopK{this};
|
||||
Utils::IntegerAspect topK{this};
|
||||
|
||||
Utils::BoolAspect usePresencePenalty{this};
|
||||
Utils::DoubleAspect presencePenalty{this};
|
||||
|
||||
Utils::BoolAspect useFrequencyPenalty{this};
|
||||
Utils::DoubleAspect frequencyPenalty{this};
|
||||
|
||||
// Context Settings
|
||||
Utils::BoolAspect readFullFile{this};
|
||||
Utils::BoolAspect readFileParts{this};
|
||||
@@ -65,21 +48,9 @@ public:
|
||||
Utils::IntegerAspect readStringsAfterCursor{this};
|
||||
Utils::BoolAspect useSystemPrompt{this};
|
||||
Utils::StringAspect systemPrompt{this};
|
||||
Utils::BoolAspect useUserMessageTemplateForCC{this};
|
||||
Utils::StringAspect systemPromptForNonFimModels{this};
|
||||
Utils::StringAspect userMessageTemplateForCC{this};
|
||||
Utils::BoolAspect useProjectChangesCache{this};
|
||||
Utils::IntegerAspect maxChangesCacheSize{this};
|
||||
|
||||
// Ollama Settings
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
QString processMessageToFIM(const QString &prefix, const QString &suffix) const;
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include "Logger.hpp"
|
||||
#include "ProviderNameMigration.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
|
||||
ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> presets;
|
||||
|
||||
AIConfiguration claudeOpus;
|
||||
claudeOpus.id = "preset_claude_opus";
|
||||
claudeOpus.name = "Claude Opus 4.7";
|
||||
claudeOpus.provider = "Claude";
|
||||
claudeOpus.model = "claude-opus-4-7";
|
||||
claudeOpus.url = "https://api.anthropic.com";
|
||||
claudeOpus.customEndpoint = "";
|
||||
claudeOpus.templateName = "Claude";
|
||||
claudeOpus.type = type;
|
||||
claudeOpus.isPredefined = true;
|
||||
|
||||
AIConfiguration claudeSonnet;
|
||||
claudeSonnet.id = "preset_claude_sonnet";
|
||||
claudeSonnet.name = "Claude Sonnet 4.6";
|
||||
claudeSonnet.provider = "Claude";
|
||||
claudeSonnet.model = "claude-sonnet-4-6";
|
||||
claudeSonnet.url = "https://api.anthropic.com";
|
||||
claudeSonnet.customEndpoint = "";
|
||||
claudeSonnet.templateName = "Claude";
|
||||
claudeSonnet.type = type;
|
||||
claudeSonnet.isPredefined = true;
|
||||
|
||||
AIConfiguration claudeHaiku;
|
||||
claudeHaiku.id = "preset_claude_haiku";
|
||||
claudeHaiku.name = "Claude Haiku 4.5";
|
||||
claudeHaiku.provider = "Claude";
|
||||
claudeHaiku.model = "claude-haiku-4-5-20251001";
|
||||
claudeHaiku.url = "https://api.anthropic.com";
|
||||
claudeHaiku.customEndpoint = "";
|
||||
claudeHaiku.templateName = "Claude";
|
||||
claudeHaiku.type = type;
|
||||
claudeHaiku.isPredefined = true;
|
||||
|
||||
AIConfiguration codestral;
|
||||
codestral.id = "preset_codestral";
|
||||
codestral.name = "Codestral";
|
||||
codestral.provider = "Codestral";
|
||||
codestral.model = "codestral-latest";
|
||||
codestral.url = "https://codestral.mistral.ai";
|
||||
codestral.customEndpoint = "";
|
||||
codestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
|
||||
codestral.type = type;
|
||||
codestral.isPredefined = true;
|
||||
|
||||
AIConfiguration mistral;
|
||||
mistral.id = "preset_mistral";
|
||||
mistral.name = "Mistral";
|
||||
mistral.provider = "Mistral AI";
|
||||
mistral.model = type == ConfigurationType::CodeCompletion ? "codestral-latest" : "mistral-large-latest";
|
||||
mistral.url = "https://api.mistral.ai";
|
||||
mistral.customEndpoint = "";
|
||||
mistral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
|
||||
mistral.type = type;
|
||||
mistral.isPredefined = true;
|
||||
|
||||
AIConfiguration geminiFlash;
|
||||
geminiFlash.id = "preset_gemini_flash";
|
||||
geminiFlash.name = "Gemini 2.5 Flash";
|
||||
geminiFlash.provider = "Google AI";
|
||||
geminiFlash.model = "gemini-2.5-flash";
|
||||
geminiFlash.url = "https://generativelanguage.googleapis.com/v1beta";
|
||||
geminiFlash.customEndpoint = "";
|
||||
geminiFlash.templateName = "Google AI";
|
||||
geminiFlash.type = type;
|
||||
geminiFlash.isPredefined = true;
|
||||
|
||||
AIConfiguration qwenPlus;
|
||||
qwenPlus.id = "preset_qwen_plus";
|
||||
qwenPlus.name = "Qwen3.6 Plus";
|
||||
qwenPlus.provider = "Qwen (OpenAI Response)";
|
||||
qwenPlus.model = "qwen3.6-plus";
|
||||
qwenPlus.url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
qwenPlus.customEndpoint = "";
|
||||
qwenPlus.templateName = "OpenAI Responses";
|
||||
qwenPlus.type = type;
|
||||
qwenPlus.isPredefined = true;
|
||||
|
||||
AIConfiguration qwenMax;
|
||||
qwenMax.id = "preset_qwen_max";
|
||||
qwenMax.name = "Qwen3.7 Max";
|
||||
qwenMax.provider = "Qwen (OpenAI Response)";
|
||||
qwenMax.model = "qwen3.7-max";
|
||||
qwenMax.url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
qwenMax.customEndpoint = "";
|
||||
qwenMax.templateName = "OpenAI Responses";
|
||||
qwenMax.type = type;
|
||||
qwenMax.isPredefined = true;
|
||||
|
||||
AIConfiguration deepSeekFlash;
|
||||
deepSeekFlash.id = "preset_deepseek_flash";
|
||||
deepSeekFlash.name = "DeepSeek V4 Flash";
|
||||
deepSeekFlash.provider = "DeepSeek";
|
||||
deepSeekFlash.model = "deepseek-v4-flash";
|
||||
deepSeekFlash.url = "https://api.deepseek.com";
|
||||
deepSeekFlash.customEndpoint = "";
|
||||
deepSeekFlash.templateName = "OpenAI Compatible";
|
||||
deepSeekFlash.type = type;
|
||||
deepSeekFlash.isPredefined = true;
|
||||
|
||||
AIConfiguration deepSeekPro;
|
||||
deepSeekPro.id = "preset_deepseek_pro";
|
||||
deepSeekPro.name = "DeepSeek V4 Pro";
|
||||
deepSeekPro.provider = "DeepSeek";
|
||||
deepSeekPro.model = "deepseek-v4-pro";
|
||||
deepSeekPro.url = "https://api.deepseek.com";
|
||||
deepSeekPro.customEndpoint = "";
|
||||
deepSeekPro.templateName = "OpenAI Compatible";
|
||||
deepSeekPro.type = type;
|
||||
deepSeekPro.isPredefined = true;
|
||||
|
||||
AIConfiguration gpt;
|
||||
gpt.id = "preset_gpt";
|
||||
gpt.name = "gpt-5.5";
|
||||
gpt.provider = "OpenAI (Responses API)";
|
||||
gpt.model = "gpt-5.5";
|
||||
gpt.url = "https://api.openai.com/v1";
|
||||
gpt.customEndpoint = "";
|
||||
gpt.templateName = "OpenAI Responses";
|
||||
gpt.type = type;
|
||||
gpt.isPredefined = true;
|
||||
|
||||
presets.append(claudeSonnet);
|
||||
presets.append(claudeHaiku);
|
||||
presets.append(claudeOpus);
|
||||
presets.append(gpt);
|
||||
presets.append(codestral);
|
||||
presets.append(mistral);
|
||||
presets.append(geminiFlash);
|
||||
presets.append(qwenPlus);
|
||||
presets.append(qwenMax);
|
||||
presets.append(deepSeekFlash);
|
||||
presets.append(deepSeekPro);
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
return "code_completion";
|
||||
case ConfigurationType::Chat:
|
||||
return "chat";
|
||||
case ConfigurationType::QuickRefactor:
|
||||
return "quick_refactor";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
QString ConfigurationManager::getConfigurationDirectory(ConfigurationType type) const
|
||||
{
|
||||
QString path = QString("%1/qodeassist/configurations/%2")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString(),
|
||||
configurationTypeToString(type));
|
||||
return path;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::ensureDirectoryExists(ConfigurationType type) const
|
||||
{
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
if (!dir.exists()) {
|
||||
return dir.mkpath(".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::loadConfigurations(ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> *configs = nullptr;
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
configs = &m_ccConfigurations;
|
||||
break;
|
||||
case ConfigurationType::Chat:
|
||||
configs = &m_caConfigurations;
|
||||
break;
|
||||
case ConfigurationType::QuickRefactor:
|
||||
configs = &m_qrConfigurations;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!configs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
configs->clear();
|
||||
|
||||
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
|
||||
configs->append(predefinedConfigs);
|
||||
|
||||
if (!ensureDirectoryExists(type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << "*.json";
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open configuration file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
|
||||
if (!doc.isObject()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
AIConfiguration config;
|
||||
config.id = obj["id"].toString();
|
||||
config.name = obj["name"].toString();
|
||||
config.provider = migrateProviderName(obj["provider"].toString());
|
||||
config.model = obj["model"].toString();
|
||||
config.templateName = obj["template"].toString();
|
||||
config.url = obj["url"].toString();
|
||||
config.customEndpoint = obj["customEndpoint"].toString();
|
||||
config.type = type;
|
||||
config.formatVersion = obj.value("formatVersion").toInt(1);
|
||||
|
||||
config.isPredefined = false;
|
||||
|
||||
if (config.id.isEmpty() || config.name.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
configs->append(config);
|
||||
}
|
||||
|
||||
emit configurationsChanged(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
|
||||
{
|
||||
if (!ensureDirectoryExists(config.type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject obj;
|
||||
obj["formatVersion"] = config.formatVersion;
|
||||
obj["id"] = config.id;
|
||||
obj["name"] = config.name;
|
||||
obj["provider"] = config.provider;
|
||||
obj["model"] = config.model;
|
||||
obj["template"] = config.templateName;
|
||||
obj["url"] = config.url;
|
||||
obj["customEndpoint"] = config.customEndpoint;
|
||||
|
||||
QString sanitizedName = config.name;
|
||||
sanitizedName.replace(" ", "_");
|
||||
sanitizedName.replace(QRegularExpression("[^a-zA-Z0-9_-]"), "");
|
||||
|
||||
QString fileName = QString("%1/%2_%3.json")
|
||||
.arg(getConfigurationDirectory(config.type), sanitizedName, config.id);
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to create configuration file: %1").arg(fileName));
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(obj);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
|
||||
loadConfigurations(config.type);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
|
||||
{
|
||||
AIConfiguration config = getConfigurationById(id, type);
|
||||
if (config.isPredefined) {
|
||||
LOG_MESSAGE(QString("Cannot delete predefined configuration: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << QString("*_%1.json").arg(id);
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
if (files.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Configuration file not found for id: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.remove()) {
|
||||
LOG_MESSAGE(QString("Failed to delete configuration file: %1")
|
||||
.arg(fileInfo.absoluteFilePath()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
loadConfigurations(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::configurations(ConfigurationType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
return m_ccConfigurations;
|
||||
case ConfigurationType::Chat:
|
||||
return m_caConfigurations;
|
||||
case ConfigurationType::QuickRefactor:
|
||||
return m_qrConfigurations;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
AIConfiguration ConfigurationManager::getConfigurationById(const QString &id,
|
||||
ConfigurationType type) const
|
||||
{
|
||||
const QVector<AIConfiguration> &configs = configurations(type);
|
||||
for (const AIConfiguration &config : configs) {
|
||||
if (config.id == id) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
return AIConfiguration();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
enum class ConfigurationType { CodeCompletion, Chat, QuickRefactor };
|
||||
|
||||
inline constexpr int CONFIGURATION_FORMAT_VERSION = 1;
|
||||
|
||||
struct AIConfiguration
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString provider;
|
||||
QString model;
|
||||
QString templateName;
|
||||
QString url;
|
||||
// Empty = use the template's endpoint; non-empty = override path.
|
||||
QString customEndpoint;
|
||||
ConfigurationType type;
|
||||
int formatVersion = CONFIGURATION_FORMAT_VERSION;
|
||||
bool isPredefined = false;
|
||||
};
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ConfigurationManager &instance();
|
||||
|
||||
bool loadConfigurations(ConfigurationType type);
|
||||
bool saveConfiguration(const AIConfiguration &config);
|
||||
bool deleteConfiguration(const QString &id, ConfigurationType type);
|
||||
|
||||
QVector<AIConfiguration> configurations(ConfigurationType type) const;
|
||||
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
|
||||
|
||||
QString getConfigurationDirectory(ConfigurationType type) const;
|
||||
|
||||
static QVector<AIConfiguration> getPredefinedConfigurations(ConfigurationType type);
|
||||
|
||||
signals:
|
||||
void configurationsChanged(ConfigurationType type);
|
||||
|
||||
private:
|
||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
||||
~ConfigurationManager() override = default;
|
||||
|
||||
bool ensureDirectoryExists(ConfigurationType type) const;
|
||||
QString configurationTypeToString(ConfigurationType type) const;
|
||||
|
||||
QVector<AIConfiguration> m_ccConfigurations;
|
||||
QVector<AIConfiguration> m_caConfigurations;
|
||||
QVector<AIConfiguration> m_qrConfigurations;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,19 +4,23 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <utils/aspects.h>
|
||||
#include <QPointer>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
namespace Utils {
|
||||
class DetailsWidget;
|
||||
namespace Core {
|
||||
class IOptionsPage;
|
||||
}
|
||||
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
class Provider;
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class GeneralSettings : public Utils::AspectContainer
|
||||
@@ -33,139 +37,9 @@ public:
|
||||
ButtonAspect checkUpdate{this};
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// code completion setttings
|
||||
Utils::SelectionAspect ccPresetConfig{this};
|
||||
ButtonAspect ccConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect ccProvider{this};
|
||||
ButtonAspect ccSelectProvider{this};
|
||||
|
||||
Utils::StringAspect ccModel{this};
|
||||
ButtonAspect ccSelectModel{this};
|
||||
|
||||
Utils::StringAspect ccTemplate{this};
|
||||
ButtonAspect ccSelectTemplate{this};
|
||||
|
||||
Utils::StringAspect ccUrl{this};
|
||||
ButtonAspect ccSetUrl{this};
|
||||
|
||||
Utils::StringAspect ccCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect ccStatus{this};
|
||||
ButtonAspect ccTest{this};
|
||||
|
||||
Utils::StringAspect ccTemplateDescription{this};
|
||||
|
||||
ButtonAspect ccSaveConfig{this};
|
||||
ButtonAspect ccLoadConfig{this};
|
||||
ButtonAspect ccOpenConfigFolder{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 ccPreset1CustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Model{this};
|
||||
ButtonAspect ccPreset1SelectModel{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Template{this};
|
||||
ButtonAspect ccPreset1SelectTemplate{this};
|
||||
|
||||
// chat assistant settings
|
||||
Utils::SelectionAspect caPresetConfig{this};
|
||||
ButtonAspect caConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect caProvider{this};
|
||||
ButtonAspect caSelectProvider{this};
|
||||
|
||||
Utils::StringAspect caModel{this};
|
||||
ButtonAspect caSelectModel{this};
|
||||
|
||||
Utils::StringAspect caTemplate{this};
|
||||
ButtonAspect caSelectTemplate{this};
|
||||
|
||||
Utils::StringAspect caUrl{this};
|
||||
ButtonAspect caSetUrl{this};
|
||||
|
||||
Utils::StringAspect caCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect caStatus{this};
|
||||
ButtonAspect caTest{this};
|
||||
|
||||
Utils::StringAspect caTemplateDescription{this};
|
||||
|
||||
ButtonAspect caSaveConfig{this};
|
||||
ButtonAspect caLoadConfig{this};
|
||||
ButtonAspect caOpenConfigFolder{this};
|
||||
|
||||
// quick refactor settings
|
||||
Utils::SelectionAspect qrPresetConfig{this};
|
||||
ButtonAspect qrConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect qrProvider{this};
|
||||
ButtonAspect qrSelectProvider{this};
|
||||
|
||||
Utils::StringAspect qrModel{this};
|
||||
ButtonAspect qrSelectModel{this};
|
||||
|
||||
Utils::StringAspect qrTemplate{this};
|
||||
ButtonAspect qrSelectTemplate{this};
|
||||
|
||||
Utils::StringAspect qrUrl{this};
|
||||
ButtonAspect qrSetUrl{this};
|
||||
|
||||
Utils::StringAspect qrCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect qrStatus{this};
|
||||
ButtonAspect qrTest{this};
|
||||
|
||||
Utils::StringAspect qrTemplateDescription{this};
|
||||
|
||||
ButtonAspect qrSaveConfig{this};
|
||||
ButtonAspect qrLoadConfig{this};
|
||||
ButtonAspect qrOpenConfigFolder{this};
|
||||
|
||||
ButtonAspect ccShowTemplateInfo{this};
|
||||
ButtonAspect caShowTemplateInfo{this};
|
||||
ButtonAspect qrShowTemplateInfo{this};
|
||||
|
||||
void showSelectionDialog(
|
||||
const QStringList &data,
|
||||
Utils::StringAspect &aspect,
|
||||
const QString &title = {},
|
||||
const QString &text = {});
|
||||
|
||||
void showModelsNotFoundDialog(Utils::StringAspect &aspect);
|
||||
|
||||
void showModelsNotSupportedDialog(Utils::StringAspect &aspect);
|
||||
|
||||
void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls);
|
||||
|
||||
void showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName);
|
||||
|
||||
void updatePreset1Visiblity(bool state);
|
||||
|
||||
void onSaveConfiguration(const QString &prefix);
|
||||
void onLoadConfiguration(const QString &prefix);
|
||||
|
||||
void loadPresetConfigurations(Utils::SelectionAspect &aspect, ConfigurationType type);
|
||||
void applyPresetConfiguration(int index, ConfigurationType type);
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetPageToDefaults();
|
||||
|
||||
QVector<AIConfiguration> m_ccPresets;
|
||||
QVector<AIConfiguration> m_caPresets;
|
||||
QVector<AIConfiguration> m_qrPresets;
|
||||
};
|
||||
|
||||
GeneralSettings &generalSettings();
|
||||
@@ -173,4 +47,22 @@ GeneralSettings &generalSettings();
|
||||
void showSettings(const Utils::Id page);
|
||||
void showSettings(const Utils::Id page, Utils::Id item);
|
||||
|
||||
class AgentsPageNavigator;
|
||||
|
||||
class AgentPipelinesPageNavigator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
|
||||
public:
|
||||
explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void editAgentRequested(const QString &agentName);
|
||||
};
|
||||
|
||||
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator);
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -179,31 +179,11 @@ const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCurs
|
||||
const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor";
|
||||
const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt";
|
||||
const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt";
|
||||
const char CC_SYSTEM_PROMPT_FOR_NON_FIM[] = "QodeAssist.ccSystemPromptForNonFim";
|
||||
const char CC_USE_USER_TEMPLATE[] = "QodeAssist.ccUseUserTemplate";
|
||||
const char CC_USER_TEMPLATE[] = "QodeAssist.ccUserTemplate";
|
||||
const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache";
|
||||
const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
|
||||
const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";
|
||||
const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt";
|
||||
|
||||
// preset prompt settings
|
||||
const char CC_TEMPERATURE[] = "QodeAssist.ccTemperature";
|
||||
const char CC_MAX_TOKENS[] = "QodeAssist.ccMaxTokens";
|
||||
const char CC_USE_TOP_P[] = "QodeAssist.ccUseTopP";
|
||||
const char CC_TOP_P[] = "QodeAssist.ccTopP";
|
||||
const char CC_USE_TOP_K[] = "QodeAssist.ccUseTopK";
|
||||
const char CC_TOP_K[] = "QodeAssist.ccTopK";
|
||||
const char CC_USE_PRESENCE_PENALTY[] = "QodeAssist.ccUsePresencePenalty";
|
||||
const char CC_PRESENCE_PENALTY[] = "QodeAssist.ccPresencePenalty";
|
||||
const char CC_USE_FREQUENCY_PENALTY[] = "QodeAssist.fimUseFrequencyPenalty";
|
||||
const char CC_FREQUENCY_PENALTY[] = "QodeAssist.fimFrequencyPenalty";
|
||||
const char CC_OLLAMA_LIVETIME[] = "QodeAssist.fimOllamaLivetime";
|
||||
const char CC_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.ccOllamaContextWindow";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char CC_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.ccOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char CA_TEMPERATURE[] = "QodeAssist.chatTemperature";
|
||||
const char CA_MAX_TOKENS[] = "QodeAssist.chatMaxTokens";
|
||||
const char CA_USE_TOP_P[] = "QodeAssist.chatUseTopP";
|
||||
|
||||
@@ -5,5 +5,6 @@ add_subdirectory(common)
|
||||
add_subdirectory(providers)
|
||||
add_subdirectory(templates)
|
||||
add_subdirectory(agents)
|
||||
add_subdirectory(Session)
|
||||
add_subdirectory(providersConfig)
|
||||
add_subdirectory(settings)
|
||||
|
||||
@@ -106,6 +106,19 @@ bool Session::isInFlight() const noexcept
|
||||
return !m_inFlight.isEmpty();
|
||||
}
|
||||
|
||||
LLMQore::BaseClient *Session::client() const noexcept
|
||||
{
|
||||
auto *provider = m_agent ? m_agent->provider() : nullptr;
|
||||
return provider ? provider->client() : nullptr;
|
||||
}
|
||||
|
||||
bool Session::supportsImages() const noexcept
|
||||
{
|
||||
auto *provider = m_agent ? m_agent->provider() : nullptr;
|
||||
return provider
|
||||
&& provider->capabilities().testFlag(Providers::ProviderCapability::Image);
|
||||
}
|
||||
|
||||
void Session::setContentLoader(ContentLoader loader)
|
||||
{
|
||||
m_contentLoader = std::move(loader);
|
||||
@@ -186,6 +199,13 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
|
||||
auto *tmpl = m_agent->promptTemplate();
|
||||
const auto &cfg = m_agent->config();
|
||||
|
||||
const QString rolePrompt = m_systemPrompt ? m_systemPrompt->compose() : QString();
|
||||
if (!rolePrompt.isEmpty()) {
|
||||
ctx.systemPrompt = (ctx.systemPrompt && !ctx.systemPrompt->isEmpty())
|
||||
? rolePrompt + QStringLiteral("\n\n") + *ctx.systemPrompt
|
||||
: rolePrompt;
|
||||
}
|
||||
|
||||
QJsonObject payload{{QStringLiteral("model"), cfg.model}};
|
||||
if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false))
|
||||
return {};
|
||||
|
||||
@@ -55,6 +55,9 @@ public:
|
||||
ConversationHistory *history() const noexcept { return m_history; }
|
||||
SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; }
|
||||
|
||||
LLMQore::BaseClient *client() const noexcept;
|
||||
bool supportsImages() const noexcept;
|
||||
|
||||
void setContextBindings(Templates::ContextRenderer::Bindings bindings);
|
||||
|
||||
QString renderAgentContext() const;
|
||||
|
||||
@@ -2,6 +2,7 @@ add_library(Providers STATIC
|
||||
ProviderID.hpp
|
||||
Provider.hpp Provider.cpp
|
||||
ProviderFactory.hpp ProviderFactory.cpp
|
||||
GenericProvider.hpp GenericProvider.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(Providers
|
||||
|
||||
113
sources/providers/GenericProvider.cpp
Normal file
113
sources/providers/GenericProvider.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "GenericProvider.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ClaudeClient.hpp>
|
||||
#include <LLMQore/GoogleAIClient.hpp>
|
||||
#include <LLMQore/LlamaCppClient.hpp>
|
||||
#include <LLMQore/MistralClient.hpp>
|
||||
#include <LLMQore/OllamaClient.hpp>
|
||||
#include <LLMQore/OpenAIClient.hpp>
|
||||
#include <LLMQore/OpenAIResponsesClient.hpp>
|
||||
|
||||
#include "ProviderFactory.hpp"
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
GenericProvider::GenericProvider(
|
||||
QString name,
|
||||
ProviderID id,
|
||||
ProviderCapabilities capabilities,
|
||||
const ClientFactory &clientFactory,
|
||||
QObject *parent)
|
||||
: Provider(parent)
|
||||
, m_name(std::move(name))
|
||||
, m_id(id)
|
||||
, m_capabilities(capabilities)
|
||||
, m_client(clientFactory(this))
|
||||
{}
|
||||
|
||||
QString GenericProvider::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
ProviderID GenericProvider::providerID() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
ProviderCapabilities GenericProvider::capabilities() const
|
||||
{
|
||||
return m_capabilities;
|
||||
}
|
||||
|
||||
::LLMQore::BaseClient *GenericProvider::client() const
|
||||
{
|
||||
return m_client;
|
||||
}
|
||||
|
||||
QFuture<QList<QString>> GenericProvider::getInstalledModels(const QString &url)
|
||||
{
|
||||
m_client->setUrl(url);
|
||||
m_client->setApiKey(apiKey());
|
||||
return m_client->listModels();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Cap = ProviderCapability;
|
||||
|
||||
template<typename ClientT>
|
||||
GenericProvider::ClientFactory makeFactory()
|
||||
{
|
||||
return [](QObject *parent) -> ::LLMQore::BaseClient * {
|
||||
return new ClientT(QString(), QString(), QString(), parent);
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerBuiltinProviders()
|
||||
{
|
||||
const auto reg = [](const QString &api,
|
||||
ProviderID id,
|
||||
ProviderCapabilities caps,
|
||||
GenericProvider::ClientFactory factory) {
|
||||
ProviderFactory::registerType(api, [=](QObject *parent) -> Provider * {
|
||||
return new GenericProvider(api, id, caps, factory, parent);
|
||||
});
|
||||
};
|
||||
|
||||
const ProviderCapabilities full
|
||||
= Cap::Tools | Cap::Thinking | Cap::Image | Cap::ModelListing;
|
||||
|
||||
reg("Claude", ProviderID::Claude, full, makeFactory<::LLMQore::ClaudeClient>());
|
||||
reg("Google AI", ProviderID::GoogleAI, full, makeFactory<::LLMQore::GoogleAIClient>());
|
||||
reg("llama.cpp", ProviderID::LlamaCpp, full, makeFactory<::LLMQore::LlamaCppClient>());
|
||||
reg("LM Studio (Chat Completions)", ProviderID::LMStudio, full,
|
||||
makeFactory<::LLMQore::OpenAIClient>());
|
||||
reg("LM Studio (Responses API)", ProviderID::OpenAIResponses, full,
|
||||
makeFactory<::LLMQore::OpenAIResponsesClient>());
|
||||
reg("Mistral AI", ProviderID::MistralAI, full, makeFactory<::LLMQore::MistralClient>());
|
||||
reg("Codestral", ProviderID::MistralAI, Cap::Tools | Cap::Image,
|
||||
makeFactory<::LLMQore::MistralClient>());
|
||||
reg("Ollama (Native)", ProviderID::Ollama, full, makeFactory<::LLMQore::OllamaClient>());
|
||||
reg("Ollama (OpenAI-compatible)", ProviderID::OpenAICompatible, full,
|
||||
makeFactory<::LLMQore::OpenAIClient>());
|
||||
reg("OpenAI (Chat Completions)", ProviderID::OpenAI, full,
|
||||
makeFactory<::LLMQore::OpenAIClient>());
|
||||
reg("OpenAI (Responses API)", ProviderID::OpenAIResponses, full,
|
||||
makeFactory<::LLMQore::OpenAIResponsesClient>());
|
||||
reg("OpenAI Compatible", ProviderID::OpenAICompatible,
|
||||
Cap::Tools | Cap::Image | Cap::Thinking, makeFactory<::LLMQore::OpenAIClient>());
|
||||
reg("OpenRouter", ProviderID::OpenRouter,
|
||||
Cap::Tools | Cap::Image | Cap::Thinking | Cap::ModelListing,
|
||||
makeFactory<::LLMQore::OpenAIClient>());
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
50
sources/providers/GenericProvider.hpp
Normal file
50
sources/providers/GenericProvider.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "Provider.hpp"
|
||||
|
||||
namespace LLMQore {
|
||||
class BaseClient;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
// A configuration-driven provider: it owns an LLMQore client and exposes a
|
||||
// fixed identity/capability set. Concrete behaviour (request shape) comes from
|
||||
// the agent's prompt template via Provider::prepareRequest, so a single class
|
||||
// covers every client_api by varying the client factory + metadata.
|
||||
class GenericProvider : public Provider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using ClientFactory = std::function<::LLMQore::BaseClient *(QObject *)>;
|
||||
|
||||
GenericProvider(
|
||||
QString name,
|
||||
ProviderID id,
|
||||
ProviderCapabilities capabilities,
|
||||
const ClientFactory &clientFactory,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||
ProviderID providerID() const override;
|
||||
ProviderCapabilities capabilities() const override;
|
||||
::LLMQore::BaseClient *client() const override;
|
||||
|
||||
private:
|
||||
QString m_name;
|
||||
ProviderID m_id;
|
||||
ProviderCapabilities m_capabilities;
|
||||
::LLMQore::BaseClient *m_client;
|
||||
};
|
||||
|
||||
// Registers every built-in client_api into ProviderFactory. Must be called once
|
||||
// at plugin startup before any agent/session is created.
|
||||
void registerBuiltinProviders();
|
||||
|
||||
} // namespace QodeAssist::Providers
|
||||
@@ -1,274 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "AgentPipelinesPage.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "../../Version.hpp"
|
||||
#include "AgentRosterWidget.hpp"
|
||||
#include "AgentsSettingsPage.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "PipelinesConfig.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentPipelinesPageNavigator::AgentPipelinesPageNavigator(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSaveDebounceMs = 300;
|
||||
|
||||
struct SlotMeta
|
||||
{
|
||||
const char *title;
|
||||
const char *hint;
|
||||
};
|
||||
|
||||
const SlotMeta kSlotMeta[] = {
|
||||
{TrConstants::CODE_COMPLETION, TrConstants::SLOT_HINT_CODE_COMPLETION},
|
||||
{TrConstants::CHAT_ASSISTANT, TrConstants::SLOT_HINT_CHAT_ASSISTANT},
|
||||
{TrConstants::CHAT_COMPRESSION, TrConstants::SLOT_HINT_CHAT_COMPRESSION},
|
||||
{TrConstants::QUICK_REFACTOR, TrConstants::SLOT_HINT_QUICK_REFACTOR},
|
||||
};
|
||||
|
||||
class AgentPipelinesPageWidget : public Core::IOptionsPageWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(AgentPipelinesPageWidget)
|
||||
public:
|
||||
AgentPipelinesPageWidget(
|
||||
const QPointer<AgentFactory> &agentFactory,
|
||||
const QPointer<AgentPipelinesPageNavigator> &navigator,
|
||||
const QPointer<AgentsPageNavigator> &agentsNavigator)
|
||||
: m_agentFactory(agentFactory)
|
||||
, m_navigator(navigator)
|
||||
, m_agentsNavigator(agentsNavigator)
|
||||
{
|
||||
m_titleLabel = new QLabel(Tr::tr(TrConstants::AGENT_PIPELINES), this);
|
||||
QFont tf = m_titleLabel->font();
|
||||
tf.setBold(true);
|
||||
tf.setPixelSize(13);
|
||||
m_titleLabel->setFont(tf);
|
||||
|
||||
m_resetBtn = new QPushButton(Tr::tr(TrConstants::RESET_TO_DEFAULTS), this);
|
||||
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||
headerRow->setSpacing(8);
|
||||
headerRow->addWidget(m_titleLabel);
|
||||
headerRow->addStretch(1);
|
||||
headerRow->addWidget(m_resetBtn);
|
||||
|
||||
auto *headerSep = new QFrame(this);
|
||||
headerSep->setFrameShape(QFrame::HLine);
|
||||
headerSep->setFrameShadow(QFrame::Sunken);
|
||||
|
||||
m_rosters[0] = new AgentRosterWidget(this);
|
||||
m_rosters[1] = new AgentRosterWidget(this);
|
||||
m_rosters[2] = new AgentRosterWidget(this);
|
||||
m_rosters[3] = new AgentRosterWidget(this);
|
||||
|
||||
for (int i = 0; i < kRosterCount; ++i)
|
||||
m_rosters[i]->setSlot(Tr::tr(kSlotMeta[i].title), Tr::tr(kSlotMeta[i].hint), {});
|
||||
|
||||
auto *content = new QWidget(this);
|
||||
auto *contentLay = new QVBoxLayout(content);
|
||||
contentLay->setContentsMargins(0, 0, 0, 0);
|
||||
contentLay->setSpacing(12);
|
||||
for (int i = 0; i < kRosterCount; ++i)
|
||||
contentLay->addWidget(m_rosters[i]);
|
||||
contentLay->addStretch(1);
|
||||
|
||||
auto *scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
scroll->setWidget(content);
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(8, 8, 8, 8);
|
||||
root->setSpacing(6);
|
||||
root->addLayout(headerRow);
|
||||
root->addWidget(headerSep);
|
||||
root->addWidget(scroll, 1);
|
||||
|
||||
m_saveDebounce = new QTimer(this);
|
||||
m_saveDebounce->setSingleShot(true);
|
||||
m_saveDebounce->setInterval(kSaveDebounceMs);
|
||||
connect(m_saveDebounce, &QTimer::timeout, this, [this]() { persistRosters(); });
|
||||
|
||||
loadFromSettings();
|
||||
|
||||
connect(m_resetBtn, &QPushButton::clicked, this, &AgentPipelinesPageWidget::onReset);
|
||||
|
||||
for (int i = 0; i < kRosterCount; ++i) {
|
||||
connect(m_rosters[i], &AgentRosterWidget::editAgentRequested, this,
|
||||
&AgentPipelinesPageWidget::onEditAgent);
|
||||
connect(m_rosters[i], &AgentRosterWidget::rosterChanged, this,
|
||||
[this](const QStringList &) { m_saveDebounce->start(); });
|
||||
}
|
||||
}
|
||||
|
||||
~AgentPipelinesPageWidget() override
|
||||
{
|
||||
if (m_saveDebounce && m_saveDebounce->isActive()) {
|
||||
m_saveDebounce->stop();
|
||||
persistRosters();
|
||||
}
|
||||
}
|
||||
|
||||
void apply() final
|
||||
{
|
||||
if (m_saveDebounce && m_saveDebounce->isActive())
|
||||
m_saveDebounce->stop();
|
||||
persistRosters();
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int kRosterCount = 4;
|
||||
|
||||
void persistRosters()
|
||||
{
|
||||
PipelineRosters rosters;
|
||||
rosters.codeCompletion = m_rosters[0]->roster();
|
||||
rosters.chatAssistant = m_rosters[1]->roster();
|
||||
rosters.chatCompression = m_rosters[2]->roster();
|
||||
rosters.quickRefactor = m_rosters[3]->roster();
|
||||
QString err;
|
||||
if (!PipelinesConfig::save(rosters, &err)) {
|
||||
LOG_MESSAGE(QStringLiteral("[Pipelines] save failed (%1): %2")
|
||||
.arg(PipelinesConfig::filePath(), err));
|
||||
if (!m_saveErrorShown) {
|
||||
m_saveErrorShown = true;
|
||||
QMessageBox::warning(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr(TrConstants::AGENT_PIPELINES),
|
||||
tr("Failed to save pipelines.toml:\n%1\n\n"
|
||||
"Further save failures will only be logged.")
|
||||
.arg(err));
|
||||
}
|
||||
} else {
|
||||
m_saveErrorShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onReset()
|
||||
{
|
||||
const auto reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr(TrConstants::RESET_SETTINGS),
|
||||
Tr::tr(TrConstants::CONFIRMATION),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
QString err;
|
||||
if (!PipelinesConfig::save(PipelineRosters::defaults(), &err))
|
||||
LOG_MESSAGE(QStringLiteral("[Pipelines] failed to reset rosters: %1").arg(err));
|
||||
|
||||
m_saveErrorShown = false;
|
||||
loadFromSettings();
|
||||
}
|
||||
|
||||
void onEditAgent(const QString &name)
|
||||
{
|
||||
if (m_agentsNavigator)
|
||||
m_agentsNavigator->requestSelectAgent(name);
|
||||
if (m_navigator)
|
||||
emit m_navigator->editAgentRequested(name);
|
||||
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
|
||||
Core::ICore::showSettings(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
|
||||
#else
|
||||
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
|
||||
#endif
|
||||
}
|
||||
|
||||
void loadFromSettings()
|
||||
{
|
||||
const PipelinesLoadResult lr = PipelinesConfig::load();
|
||||
if (lr.status == PipelinesLoadStatus::ParseError
|
||||
|| lr.status == PipelinesLoadStatus::SchemaError) {
|
||||
QMessageBox::warning(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr(TrConstants::AGENT_PIPELINES),
|
||||
tr("pipelines.toml has issues — using defaults for affected entries:\n%1\n\n"
|
||||
"Click OK to continue. Changes you make here will overwrite the file.")
|
||||
.arg(lr.message));
|
||||
}
|
||||
|
||||
AgentFactory *factory = m_agentFactory.data();
|
||||
m_rosters[0]->setRoster(lr.rosters.codeCompletion, factory);
|
||||
m_rosters[1]->setRoster(lr.rosters.chatAssistant, factory);
|
||||
m_rosters[2]->setRoster(lr.rosters.chatCompression, factory);
|
||||
m_rosters[3]->setRoster(lr.rosters.quickRefactor, factory);
|
||||
}
|
||||
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QPointer<AgentPipelinesPageNavigator> m_navigator;
|
||||
QPointer<AgentsPageNavigator> m_agentsNavigator;
|
||||
|
||||
QLabel *m_titleLabel = nullptr;
|
||||
QPushButton *m_resetBtn = nullptr;
|
||||
|
||||
AgentRosterWidget *m_rosters[kRosterCount] = {};
|
||||
|
||||
QTimer *m_saveDebounce = nullptr;
|
||||
bool m_saveErrorShown = false;
|
||||
};
|
||||
|
||||
class AgentPipelinesOptionsPage final : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
AgentPipelinesOptionsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator)
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_AGENT_PIPELINES_PAGE_ID);
|
||||
setDisplayName(Tr::tr(TrConstants::AGENT_PIPELINES));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
const QPointer<AgentFactory> factoryPtr(agentFactory);
|
||||
const QPointer<AgentPipelinesPageNavigator> navPtr(navigator);
|
||||
const QPointer<AgentsPageNavigator> agentsNavPtr(agentsNavigator);
|
||||
setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] {
|
||||
return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator)
|
||||
{
|
||||
return std::make_unique<AgentPipelinesOptionsPage>(
|
||||
agentFactory, navigator, agentsNavigator);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
#include "AgentPipelinesPage.moc"
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace Core {
|
||||
class IOptionsPage;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentsPageNavigator;
|
||||
|
||||
class AgentPipelinesPageNavigator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
|
||||
public:
|
||||
explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void editAgentRequested(const QString &agentName);
|
||||
};
|
||||
|
||||
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator);
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@@ -1,5 +1,4 @@
|
||||
add_library(QodeAssistAgentPipelines OBJECT
|
||||
AgentPipelinesPage.hpp AgentPipelinesPage.cpp
|
||||
add_library(QodeAssistAgentPipelines STATIC
|
||||
PipelinesConfig.hpp PipelinesConfig.cpp
|
||||
AgentRosterWidget.hpp AgentRosterWidget.cpp
|
||||
AgentSlotWidget.hpp AgentSlotWidget.cpp
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
add_executable(QodeAssistTest
|
||||
../CodeHandler.cpp
|
||||
../LLMClientInterface.cpp
|
||||
../LLMSuggestion.cpp
|
||||
CodeHandlerTest.cpp
|
||||
ClaudeCacheControlTest.cpp
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "CustomInstructionsManager.hpp"
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
#include "settings/ConfigurationManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
@@ -113,11 +112,6 @@ void QuickRefactorDialog::setupUi()
|
||||
actionsLayout->addWidget(m_alternativeButton);
|
||||
actionsLayout->addStretch();
|
||||
|
||||
m_configComboBox = new QComboBox(this);
|
||||
m_configComboBox->setMinimumWidth(200);
|
||||
m_configComboBox->setToolTip(Tr::tr("Switch AI configuration"));
|
||||
actionsLayout->addWidget(m_configComboBox);
|
||||
|
||||
Utils::Theme *theme = Utils::creatorTheme();
|
||||
QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white);
|
||||
|
||||
@@ -244,13 +238,6 @@ void QuickRefactorDialog::setupUi()
|
||||
&QuickRefactorDialog::onOpenInstructionsFolder);
|
||||
|
||||
loadCustomCommands();
|
||||
loadAvailableConfigurations();
|
||||
|
||||
connect(
|
||||
m_configComboBox,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this,
|
||||
&QuickRefactorDialog::onConfigurationChanged);
|
||||
|
||||
QDialogButtonBox *buttonBox
|
||||
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
@@ -577,60 +564,6 @@ void QuickRefactorDialog::onOpenSettings()
|
||||
Settings::showSettings(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID);
|
||||
}
|
||||
|
||||
QString QuickRefactorDialog::selectedConfiguration() const
|
||||
{
|
||||
return m_selectedConfiguration;
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::loadAvailableConfigurations()
|
||||
{
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
m_configComboBox->clear();
|
||||
m_configComboBox->addItem(Tr::tr("Current"), QString());
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
m_configComboBox->addItem(config.name, config.id);
|
||||
}
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
QString currentProvider = settings.qrProvider.value();
|
||||
QString currentModel = settings.qrModel.value();
|
||||
QString currentConfigText = QString("%1/%2").arg(currentProvider, currentModel);
|
||||
m_configComboBox->setItemText(0, Tr::tr("Current (%1)").arg(currentConfigText));
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onConfigurationChanged(int index)
|
||||
{
|
||||
if (index == 0) {
|
||||
m_selectedConfiguration.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
QString configId = m_configComboBox->itemData(index).toString();
|
||||
m_selectedConfiguration = m_configComboBox->itemText(index);
|
||||
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
Settings::AIConfiguration config
|
||||
= manager.getConfigurationById(configId, Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
if (!config.id.isEmpty()) {
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
settings.qrProvider.setValue(config.provider);
|
||||
settings.qrModel.setValue(config.model);
|
||||
settings.qrTemplate.setValue(config.templateName);
|
||||
settings.qrUrl.setValue(config.url);
|
||||
settings.qrCustomEndpoint.setValue(config.customEndpoint);
|
||||
|
||||
settings.writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::validateAndAccept()
|
||||
{
|
||||
QString instruction = m_instructionEdit->toPlainText().trimmed();
|
||||
|
||||
@@ -33,8 +33,6 @@ public:
|
||||
|
||||
Action selectedAction() const;
|
||||
|
||||
QString selectedConfiguration() const;
|
||||
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
@@ -50,8 +48,6 @@ private slots:
|
||||
void onOpenInstructionsFolder();
|
||||
void onOpenSettings();
|
||||
void loadCustomCommands();
|
||||
void loadAvailableConfigurations();
|
||||
void onConfigurationChanged(int index);
|
||||
void validateAndAccept();
|
||||
|
||||
private:
|
||||
@@ -71,12 +67,10 @@ private:
|
||||
QToolButton *m_toolsButton;
|
||||
QToolButton *m_thinkingButton;
|
||||
QComboBox *m_commandsComboBox;
|
||||
QComboBox *m_configComboBox;
|
||||
|
||||
Action m_selectedAction = Action::Custom;
|
||||
QString m_lastInstructions;
|
||||
QString m_selectedConfiguration;
|
||||
|
||||
|
||||
QIcon m_toolsIconOn;
|
||||
QIcon m_toolsIconOff;
|
||||
QIcon m_thinkingIconOn;
|
||||
|
||||
Reference in New Issue
Block a user