Compare commits

..

14 Commits

Author SHA1 Message Date
Petr Mironychev
2aa748b14a refactor: final Agent loader 2026-06-11 19:04:45 +02:00
Petr Mironychev
f499be278d test: Update tests 2026-06-11 15:38:12 +02:00
Petr Mironychev
231a6a0215 doc: update architecture 2026-06-11 15:28:37 +02:00
Petr Mironychev
69672deb45 refactor: IProjectScanner port; ContextManager QtC-free 2026-06-11 15:21:02 +02:00
Petr Mironychev
f36173d932 refactor: Agent roaster improve 2026-06-11 14:51:49 +02:00
Petr Mironychev
e65ac23e66 refactor: Move QuickRefactor to Session way 2026-06-11 14:18:44 +02:00
Petr Mironychev
7bfe9d6f0e refactor: Change to chat conversation 2026-06-11 14:06:19 +02:00
Petr Mironychev
05fe38e289 refactor: Remove project rules 2026-06-11 13:36:23 +02:00
Petr Mironychev
2c9475cddf fix: Code completion via session 2026-06-10 17:44:03 +02:00
Petr Mironychev
3179c0c358 refactor: add to template agent roles 2026-06-09 08:52:53 +02:00
Petr Mironychev
c151c5030b refactor: Finalize agent template 2026-06-09 08:48:32 +02:00
Petr Mironychev
98a618cf87 refactor: Add agents for providers 2026-06-09 08:48:32 +02:00
Petr Mironychev
6220308a93 refactor: Move to agent-session architecture 2026-06-09 08:46:45 +02:00
Petr Mironychev
02c11ee5a0 refactor: Remove experimental flag 2026-06-09 08:21:04 +02:00
249 changed files with 7200 additions and 11063 deletions

View File

@@ -27,7 +27,7 @@ jobs:
config: config:
- { - {
name: "Windows Latest MSVC", artifact: "Windows-x64", name: "Windows Latest MSVC", artifact: "Windows-x64",
os: windows-2022, os: windows-latest,
platform: windows_x64, platform: windows_x64,
cc: "cl", cxx: "cl", cc: "cl", cxx: "cl",
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat", environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
@@ -53,10 +53,6 @@ jobs:
qt_version: "6.10.3", qt_version: "6.10.3",
qt_creator_version: "19.0.2" qt_creator_version: "19.0.2"
} }
- {
qt_version: "6.11.1",
qt_creator_version: "20.0.0"
}
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
@@ -114,14 +110,10 @@ jobs:
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}") set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
string(REPLACE "." "" qt_version_dotless "${qt_version}") string(REPLACE "." "" qt_version_dotless "${qt_version}")
set(qt_repo_dir "qt6_${qt_version_dotless}")
if ("${{ runner.os }}" STREQUAL "Windows") if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86") set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64") set(qt_package_arch_suffix "win64_msvc2022_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64") set(qt_dir_prefix "${qt_version}/msvc2022_64")
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
set(qt_repo_dir "qt6_${qt_version_dotless}_msvc2022_64")
endif()
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0") if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64") set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
else() else()
@@ -135,9 +127,7 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64") set(qt_package_arch_suffix "linux_gcc_64")
endif() endif()
set(qt_dir_prefix "${qt_version}/gcc_64") set(qt_dir_prefix "${qt_version}/gcc_64")
if (qt_version VERSION_GREATER_EQUAL "6.11.0") if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Linux-RHEL_9_6-GCC-Linux-RHEL_9_6-X86_64")
elseif (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64") set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
else() else()
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64") set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
@@ -153,7 +143,7 @@ jobs:
endif() endif()
endif() endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/${qt_repo_dir}") set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS) file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
file(READ ./Updates.xml updates_xml) file(READ ./Updates.xml updates_xml)
@@ -180,11 +170,7 @@ jobs:
) )
endforeach() endforeach()
set(qt_addon_packages qt5compat qtshadertools) foreach(package qt5compat qtshadertools)
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
list(APPEND qt_addon_packages qttasktree)
endif()
foreach(package ${qt_addon_packages})
downloadAndExtract( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z ${package}.7z
@@ -250,7 +236,7 @@ jobs:
endif() endif()
set(build_plugin_py "scripts/build_plugin.py") set(build_plugin_py "scripts/build_plugin.py")
foreach(dir "share/qtcreator/scripts" "Qt Creator.sdk/share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts") foreach(dir "share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py") if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py")
set(build_plugin_py "${dir}/build_plugin.py") set(build_plugin_py "${dir}/build_plugin.py")
break() break()

View File

@@ -1,10 +1,8 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(QodeAssist) list(APPEND CMAKE_PREFIX_PATH "/Users/palm1r/Qt/Qt Creator.sdk/lib/cmake/QtCreator")
option(QODEASSIST_EXPERIMENTAL project(QodeAssist)
"Enable experimental features" OFF)
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
@@ -42,7 +40,6 @@ add_definitions(
add_subdirectory(sources) add_subdirectory(sources)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(pluginllmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(UIControls) add_subdirectory(UIControls)
add_subdirectory(ChatView) add_subdirectory(ChatView)
@@ -51,6 +48,11 @@ if(GTest_FOUND)
add_subdirectory(test) add_subdirectory(test)
endif() endif()
option(QODEASSIST_BUILD_BENCH "Build the standalone agent bench CLI" ON)
if(QODEASSIST_BUILD_BENCH)
add_subdirectory(bench)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
QtCreator::Core QtCreator::Core
@@ -69,7 +71,6 @@ add_qtc_plugin(QodeAssist
QtCreator::Utils QtCreator::Utils
QtCreator::CPlusPlus QtCreator::CPlusPlus
LLMQore LLMQore
PluginLLMCore
ProvidersConfig ProvidersConfig
Agents Agents
Skills Skills
@@ -83,42 +84,6 @@ add_qtc_plugin(QodeAssist
QodeAssisttr.h QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
RefactorContextHelper.hpp RefactorContextHelper.hpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
templates/OpenAIResponses.hpp
providers/Providers.hpp
providers/ProviderUrlUtils.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
providers/QwenProvider.hpp providers/QwenProvider.cpp
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -130,7 +95,6 @@ add_qtc_plugin(QodeAssist
chat/ChatDocument.hpp chat/ChatDocument.cpp chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
@@ -170,10 +134,7 @@ add_qtc_plugin(QodeAssist
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
) )
if(QODEASSIST_EXPERIMENTAL) target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
endif()
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable find_program(QtCreatorExecutable

View File

@@ -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

View File

@@ -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

View File

@@ -75,8 +75,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp ChatCompressor.hpp ChatCompressor.cpp
AgentRoleController.hpp AgentRoleController.cpp ChatAgentController.hpp ChatAgentController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
FileEditController.hpp FileEditController.cpp FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp ChatHistoryStore.hpp ChatHistoryStore.cpp
@@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView
Qt::Network Qt::Network
QtCreator::Core QtCreator::Core
QtCreator::Utils QtCreator::Utils
PluginLLMCore
QodeAssistSettings QodeAssistSettings
Context Context
QodeAssistUIControlsplugin QodeAssistUIControlsplugin
QodeAssistLogger QodeAssistLogger
LLMQore LLMQore
Skills Skills
Agents
Session
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

View File

@@ -0,0 +1,105 @@
// 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>
#include <sources/settings/PipelinesConfig.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()
{
const QStringList all = m_agentFactory ? m_agentFactory->configNames() : QStringList{};
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
if (roster.isEmpty()) {
m_availableAgents = all;
} else {
QStringList filtered;
for (const QString &name : roster) {
if (all.contains(name))
filtered.append(name);
}
m_availableAgents = filtered.isEmpty() ? all : filtered;
}
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

View 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

View File

@@ -4,13 +4,20 @@
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include "ChatModel.hpp" #include <LLMQore/ContentBlocks.hpp>
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <QDateTime> #include <QDateTime>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -25,7 +32,18 @@ ChatCompressor::ChatCompressor(QObject *parent)
: QObject(parent) : QObject(parent)
{} {}
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel) void ChatCompressor::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void ChatCompressor::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
}
void ChatCompressor::startCompression(
const QString &chatFilePath, ConversationHistory *sourceHistory)
{ {
if (m_isCompressing) { if (m_isCompressing) {
emit compressionFailed(tr("Compression already in progress")); emit compressionFailed(tr("Compression already in progress"));
@@ -37,49 +55,78 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
return; return;
} }
if (!chatModel || chatModel->rowCount() == 0) { if (!sourceHistory || sourceHistory->isEmpty()) {
emit compressionFailed(tr("Chat is empty, nothing to compress")); emit compressionFailed(tr("Chat is empty, nothing to compress"));
return; return;
} }
auto providerName = Settings::generalSettings().caProvider(); if (!m_sessionManager) {
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); emit compressionFailed(tr("Chat session manager is not available"));
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
return; return;
} }
auto templateName = Settings::generalSettings().caTemplate(); QString sessionError;
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName( Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
templateName); if (!session) {
emit compressionFailed(
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
return;
}
if (!promptTemplate) { auto *client = session->client();
emit compressionFailed(tr("No template available")); if (!client) {
m_sessionManager->removeSession(session);
emit compressionFailed(tr("Chat agent has no live client"));
return; return;
} }
m_isCompressing = true; m_isCompressing = true;
m_chatModel = chatModel;
m_originalChatPath = chatFilePath; m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear(); m_session = session;
emit compressionStarted(); 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{ auto *history = session->history();
{"model", Settings::generalSettings().caModel()}, {"stream", true}}; for (const auto &msg : sourceHistory->messages()) {
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
continue;
const QString text = msg.text();
if (text.trimmed().isEmpty())
continue;
buildRequestPayload(payload, promptTemplate); Message apiMessage(msg.role());
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(text));
history->append(std::move(apiMessage));
}
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); connect(
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint session, &Session::finished, this,
: promptTemplate->endpoint(); [this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
m_provider->client()->setTransferTimeout( connect(
session, &Session::failed, this,
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onCompressionFailed(id, error.message);
});
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); 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: %1")
.arg(session->lastError().message));
return;
}
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId)); LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
} }
@@ -94,44 +141,38 @@ void ChatCompressor::cancelCompression()
return; return;
LOG_MESSAGE("Cancelling compression request"); LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState(); cleanupState();
emit compressionFailed(tr("Compression cancelled")); emit compressionFailed(tr("Compression cancelled"));
} }
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText) void ChatCompressor::onCompressionFinished(const QString &requestId)
{ {
if (!m_isCompressing || requestId != m_currentRequestId) if (!m_isCompressing || requestId != m_currentRequestId)
return; return;
m_accumulatedSummary += partialText; QString summary;
} if (m_session) {
if (auto *history = m_session->history(); history && !history->isEmpty())
summary = history->messages().back().text();
}
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText) LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
{
Q_UNUSED(fullText)
if (!m_isCompressing || requestId != m_currentRequestId) const QString compressedPath = createCompressedChatPath(m_originalChatPath);
return; const QString sourcePath = m_originalChatPath;
LOG_MESSAGE( cleanupState();
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
QString compressedPath = createCompressedChatPath(m_originalChatPath); if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) { emit compressionFailed(tr("Failed to save compressed chat"));
handleCompressionError(tr("Failed to save compressed chat"));
return; return;
} }
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath)); LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath); emit compressionCompleted(compressedPath);
} }
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error) void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
{ {
if (!m_isCompressing || requestId != m_currentRequestId) if (!m_isCompressing || requestId != m_currentRequestId)
return; return;
@@ -168,39 +209,6 @@ QString ChatCompressor::buildCompressionPrompt() const
"Create the summary now:"); "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( bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary) const QString &sourcePath, const QString &destPath, const QString &summary)
{ {
@@ -224,11 +232,11 @@ bool ChatCompressor::createCompressedChatFile(
QJsonObject summaryMessage; QJsonObject summaryMessage;
summaryMessage["role"] = "assistant"; summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces); summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
summaryMessage["isRedacted"] = false; QJsonObject textBlock;
summaryMessage["attachments"] = QJsonArray(); textBlock["type"] = "text";
summaryMessage["images"] = QJsonArray(); textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["blocks"] = QJsonArray{textBlock};
root["messages"] = QJsonArray{summaryMessage}; root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath; root["compressedFrom"] = sourcePath;
@@ -247,49 +255,17 @@ bool ChatCompressor::createCompressedChatFile(
return true; 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))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState() void ChatCompressor::cleanupState()
{ {
disconnectAllSignals(); Session *session = m_session;
m_isCompressing = false; m_isCompressing = false;
m_currentRequestId.clear(); m_currentRequestId.clear();
m_originalChatPath.clear(); m_originalChatPath.clear();
m_accumulatedSummary.clear(); m_session = nullptr;
m_chatModel = nullptr;
m_provider = nullptr; if (session && m_sessionManager)
m_sessionManager->release(session);
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -4,20 +4,19 @@
#pragma once #pragma once
#include <QJsonObject>
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QPointer>
#include <QString> #include <QString>
namespace QodeAssist::PluginLLMCore { namespace QodeAssist {
class Provider; class SessionManager;
class PromptTemplate; class Session;
} // namespace QodeAssist::PluginLLMCore class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject class ChatCompressor : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -25,7 +24,10 @@ class ChatCompressor : public QObject
public: public:
explicit ChatCompressor(QObject *parent = nullptr); explicit ChatCompressor(QObject *parent = nullptr);
void startCompression(const QString &chatFilePath, ChatModel *chatModel); void setSessionManager(SessionManager *sessionManager);
void setActiveAgent(const QString &agentName);
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
bool isCompressing() const; bool isCompressing() const;
void cancelCompression(); void cancelCompression();
@@ -35,30 +37,23 @@ signals:
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
private slots:
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
void onFullResponseReceived(const QString &requestId, const QString &fullText);
void onRequestFailed(const QString &requestId, const QString &error);
private: private:
void onCompressionFinished(const QString &requestId);
void onCompressionFailed(const QString &requestId, const QString &error);
QString createCompressedChatPath(const QString &originalPath) const; QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const; QString buildCompressionPrompt() const;
bool createCompressedChatFile( bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary); const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState(); void cleanupState();
void handleCompressionError(const QString &error); void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false; bool m_isCompressing = false;
QString m_currentRequestId; QString m_currentRequestId;
QString m_originalChatPath; QString m_originalChatPath;
QString m_accumulatedSummary; QPointer<SessionManager> m_sessionManager;
PluginLLMCore::Provider *m_provider = nullptr; QString m_activeAgent;
ChatModel *m_chatModel = nullptr; QPointer<Session> m_session;
QList<QMetaObject::Connection> m_connections;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -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

View File

@@ -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

View File

@@ -16,15 +16,20 @@
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include "ChatModel.hpp" #include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent) ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_history(history)
{} {}
QString ChatHistoryStore::historyDir() const QString ChatHistoryStore::historyDir() const
@@ -52,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
{ {
QString shortMessage; QString shortMessage;
if (m_chatModel->rowCount() > 0) { if (m_history) {
QString firstMessage for (const auto &message : m_history->messages()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString(); if (message.role() != Message::Role::User)
shortMessage = firstMessage.split('\n').first().simplified().left(30); continue;
if (shortMessage.isEmpty()) { const QString text = message.text();
QVariantList images if (!text.trimmed().isEmpty()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList(); shortMessage = text.split('\n').first().simplified().left(30);
if (!images.isEmpty()) { } else {
shortMessage = "image_chat"; for (const auto &block : message.blocks()) {
if (dynamic_cast<StoredImageContent *>(block.get())) {
shortMessage = "image_chat";
break;
}
}
} }
break;
} }
} }
@@ -107,12 +118,12 @@ QString ChatHistoryStore::autosaveFilePath(
SerializationResult ChatHistoryStore::save(const QString &filePath) const SerializationResult ChatHistoryStore::save(const QString &filePath) const
{ {
return ChatSerializer::saveToFile(m_chatModel, filePath); return ChatSerializer::saveToFile(m_history, filePath);
} }
SerializationResult ChatHistoryStore::load(const QString &filePath) const SerializationResult ChatHistoryStore::load(const QString &filePath) const
{ {
return ChatSerializer::loadFromFile(m_chatModel, filePath); return ChatSerializer::loadFromFile(m_history, filePath);
} }
void ChatHistoryStore::showSaveDialog() void ChatHistoryStore::showSaveDialog()

View File

@@ -9,16 +9,18 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
namespace QodeAssist::Chat { namespace QodeAssist {
class ConversationHistory;
}
class ChatModel; namespace QodeAssist::Chat {
class ChatHistoryStore : public QObject class ChatHistoryStore : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr); explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
QString historyDir() const; QString historyDir() const;
QString suggestedFileName() const; QString suggestedFileName() const;
@@ -42,7 +44,7 @@ signals:
private: private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const; QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ChatModel *m_chatModel; ConversationHistory *m_history;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,16 @@
#include "MessagePart.hpp" #include "MessagePart.hpp"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QHash>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -43,81 +48,19 @@ public:
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
struct ImageAttachment
{
QString fileName; // Original filename
QString storedPath; // Path to stored image file (relative to chat folder)
QString mediaType; // MIME type
};
struct Message
{
ChatRole role;
QString content;
QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
QString toolName;
QJsonObject toolArguments;
QString toolResult;
int promptTokens = 0;
int completionTokens = 0;
int cachedPromptTokens = 0;
int reasoningTokens = 0;
};
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
void setHistory(ConversationHistory *history);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index); Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const; Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
void addToolExecutionStatus(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments);
void dropTrailingAssistantMessage(const QString &requestId);
void setToolMessageData(
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments,
const QString &toolResult);
void updateToolResult(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &result);
void addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent);
void setMessageUsage( void setMessageUsage(
const QString &messageId, const QString &messageId,
int promptTokens, int promptTokens,
@@ -129,10 +72,7 @@ public:
int sessionCompletionTokens() const; int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const; int sessionCachedPromptTokens() const;
int sessionTotalTokens() const; int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -141,18 +81,60 @@ signals:
void sessionUsageChanged(); void sessionUsageChanged();
private slots: private slots:
void onFileEditApplied(const QString &editId); void onHistoryMessageAdded(int index);
void onFileEditRejected(const QString &editId); void onHistoryMessageUpdated(int index);
void onFileEditArchived(const QString &editId); void onHistoryCleared();
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage); struct AttachmentRef
{
QVector<Message> m_messages; QString fileName;
bool m_loadingFromHistory = false; QString storedPath;
};
struct ImageRef
{
QString fileName;
QString storedPath;
QString mediaType;
};
struct Row
{
ChatRole kind = ChatRole::Assistant;
int messageIndex = -1;
QString messageId;
QString content;
bool isRedacted = false;
QString editId;
QVector<AttachmentRef> attachments;
QVector<ImageRef> images;
};
struct Usage
{
int prompt = 0;
int completion = 0;
int cached = 0;
int reasoning = 0;
};
void rebuildAll();
void reprojectTail(int startMessageIndex);
int startMessageIndexFor(int messageIndex) const;
int firstRowForMessage(int messageIndex) const;
QHash<QString, QString> buildToolResultMap() const;
void appendRowsForMessage(
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
QVariantList buildImageList(const QVector<ImageRef> &images) const;
QPointer<ConversationHistory> m_history;
QVector<Row> m_rows;
QHash<QString, Usage> m_usageByMessageId;
QString m_chatFilePath; QString m_chatFilePath;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart) Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

View File

@@ -28,9 +28,16 @@
#include "QodeAssistConstants.hpp" #include "QodeAssistConstants.hpp"
#include "AgentRoleController.hpp" #include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <SessionManager.hpp>
#include <sources/settings/PipelinesConfig.hpp>
#include "ChatAgentController.hpp"
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatConfigurationController.hpp"
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include "ChatHistoryStore.hpp" #include "ChatHistoryStore.hpp"
#include "FileEditController.hpp" #include "FileEditController.hpp"
@@ -38,10 +45,8 @@
#include "InputTokenCounter.hpp" #include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp" #include "SettingsConstants.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "SessionFileRegistry.hpp" #include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "SkillsSettings.hpp" #include "SkillsSettings.hpp"
#include "sources/skills/SkillsManager.hpp" #include "sources/skills/SkillsManager.hpp"
@@ -73,19 +78,20 @@ QKeySequence sendMessageKeySequence()
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_history(new QodeAssist::ConversationHistory(this))
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_fileManager(new ChatFileManager(this)) , m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false) , m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this)) , m_chatCompressor(new ChatCompressor(this))
, m_agentRoleController(new AgentRoleController(this)) , m_agentController(new ChatAgentController(this))
, m_configurationController(new ChatConfigurationController(this)) , m_fileEditController(new FileEditController(this))
, m_fileEditController(new FileEditController(m_chatModel, this)) , m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
, m_tokenCounter( , m_historyStore(new ChatHistoryStore(m_history, this))
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
{ {
m_chatModel->setHistory(m_history);
m_clientInterface->setHistory(m_history);
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles(); m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect( connect(
&Settings::chatAssistantSettings().linkOpenFiles, &Settings::chatAssistantSettings().linkOpenFiles,
@@ -109,22 +115,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
}, },
Qt::QueuedConnection); 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( connect(
m_clientInterface, m_clientInterface,
&ClientInterface::messageReceivedCompletely, &ClientInterface::messageReceivedCompletely,
@@ -171,20 +161,30 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::inputTokensCountChanged); &ChatRootView::inputTokensCountChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::availableRolesChanged, &ChatAgentController::availableAgentsChanged,
this, this,
&ChatRootView::availableAgentRolesChanged); &ChatRootView::availableChatAgentsChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::currentRoleChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::currentAgentRoleChanged); &ChatRootView::currentChatAgentChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::baseSystemPromptChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::baseSystemPromptChanged); &ChatRootView::isThinkingSupportChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useToolsChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useThinkingChanged);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -266,14 +266,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect( connect(
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory); m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
connect( connect(
ProjectExplorer::ProjectManager::instance(), ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded, &ProjectExplorer::ProjectManager::projectAdded,
@@ -298,12 +290,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::useThinkingChanged); &ChatRootView::useThinkingChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) { connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
m_lastErrorMessage = error; m_lastErrorMessage = error;
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
@@ -324,7 +310,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) { if (m_pendingSend.active) {
PendingSend p = m_pendingSend; PendingSend p = m_pendingSend;
m_pendingSend = {}; m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking); dispatchSend(p.message, p.attachments, p.linkedFiles);
} }
}); });
@@ -337,7 +323,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) { if (m_pendingSend.active) {
PendingSend p = m_pendingSend; PendingSend p = m_pendingSend;
m_pendingSend = {}; m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking); dispatchSend(p.message, p.attachments, p.linkedFiles);
} }
}); });
} }
@@ -373,6 +359,85 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
return m_skillsManager; 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);
}
QStringList ChatRootView::availableRoles() const
{
return m_availableRoles;
}
QString ChatRootView::currentRole() const
{
return m_currentRole;
}
void ChatRootView::setCurrentRole(const QString &roleId)
{
if (m_currentRole == roleId)
return;
m_currentRole = roleId;
emit currentRoleChanged();
}
void ChatRootView::loadAvailableRoles()
{
QStringList ids;
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
ids.reserve(roles.size());
for (const auto &r : roles)
ids << r.id;
if (ids != m_availableRoles) {
m_availableRoles = ids;
emit availableRolesChanged();
}
if (!m_availableRoles.isEmpty() && !m_availableRoles.contains(m_currentRole))
setCurrentRole(m_availableRoles.contains(QStringLiteral("developer"))
? QStringLiteral("developer")
: m_availableRoles.first());
}
QVariantList ChatRootView::searchSkills(const QString &query) const QVariantList ChatRootView::searchSkills(const QString &query) const
{ {
QVariantList results; QVariantList results;
@@ -380,7 +445,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
if (!manager || !Settings::skillsSettings().enableSkills()) if (!manager || !Settings::skillsSettings().enableSkills())
return results; return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject(); auto *project = ProjectExplorer::ProjectManager::startupProject();
QStringList projectSkillDirs; QStringList projectSkillDirs;
if (project) { if (project) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
@@ -416,21 +481,17 @@ void ChatRootView::sendMessage(const QString &message)
{ {
const QStringList attachments = m_attachmentFiles; const QStringList attachments = m_attachmentFiles;
const QStringList linkedFiles = m_linkedFiles; const QStringList linkedFiles = m_linkedFiles;
const bool tools = useTools();
const bool thinking = useThinking();
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking)) if (deferSendForAutoCompress(message, attachments, linkedFiles))
return; return;
dispatchSend(message, attachments, linkedFiles, tools, thinking); dispatchSend(message, attachments, linkedFiles);
} }
bool ChatRootView::deferSendForAutoCompress( bool ChatRootView::deferSendForAutoCompress(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles)
bool useToolsArg,
bool useThinkingArg)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress()) if (!settings.autoCompress())
@@ -456,7 +517,7 @@ bool ChatRootView::deferSendForAutoCompress(
.arg(inputTokens) .arg(inputTokens)
.arg(threshold)); .arg(threshold));
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true}; m_pendingSend = {message, attachments, linkedFiles, true};
compressCurrentChat(); compressCurrentChat();
return true; return true;
} }
@@ -464,9 +525,7 @@ bool ChatRootView::deferSendForAutoCompress(
void ChatRootView::dispatchSend( void ChatRootView::dispatchSend(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles)
bool useToolsArg,
bool useThinkingArg)
{ {
if (m_recentFilePath.isEmpty()) { if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments); QString filePath = getAutosaveFilePath(message, attachments);
@@ -481,8 +540,14 @@ void ChatRootView::dispatchSend(
m_tokenCounter->recordSent(); m_tokenCounter->recordSent();
if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_clientInterface->setSkillsManager(skillsManager()); m_clientInterface->setSkillsManager(skillsManager());
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg); m_clientInterface->setSessionManager(sessionManager());
m_clientInterface->setActiveAgent(currentChatAgent());
m_clientInterface->setActiveRole(currentRole());
m_clientInterface->sendMessage(message, attachments, linkedFiles);
m_fileManager->clearIntermediateStorage(); m_fileManager->clearIntermediateStorage();
clearAttachmentFiles(); clearAttachmentFiles();
@@ -527,12 +592,6 @@ void ChatRootView::clearMessages()
clearLinkedFiles(); clearLinkedFiles();
} }
QString ChatRootView::currentTemplate() const
{
auto &settings = Settings::generalSettings();
return settings.caModel();
}
void ChatRootView::saveHistory(const QString &filePath) void ChatRootView::saveHistory(const QString &filePath)
{ {
if (filePath != m_recentFilePath) { if (filePath != m_recentFilePath) {
@@ -821,25 +880,6 @@ void ChatRootView::openChatHistoryFolder()
m_historyStore->openHistoryFolder(); 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() void ChatRootView::openSettings()
{ {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
@@ -890,13 +930,12 @@ QString ChatRootView::chatTitle() const
QString ChatRootView::computeChatTitle() const QString ChatRootView::computeChatTitle() const
{ {
if (!m_chatModel) if (!m_history)
return {}; return {};
const auto history = m_chatModel->getChatHistory(); for (const auto &msg : m_history->messages()) {
for (const auto &msg : history) { if (msg.role() != Message::Role::User)
if (msg.role != ChatModel::User)
continue; continue;
const QString content = msg.content.trimmed(); const QString content = msg.text().trimmed();
if (content.isEmpty()) if (content.isEmpty())
continue; continue;
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed(); const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
@@ -1064,11 +1103,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath) bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{ {
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath); if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString())); .arg(filePath.toFSPathString()));
return true; return true;
@@ -1120,71 +1155,14 @@ QString ChatRootView::lastErrorMessage() const
return m_lastErrorMessage; 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 bool ChatRootView::useTools() const
{ {
return Settings::chatAssistantSettings().enableChatTools(); return m_agentController->currentSupportsTools();
}
void ChatRootView::setUseTools(bool enabled)
{
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
} }
bool ChatRootView::useThinking() const bool ChatRootView::useThinking() const
{ {
return Settings::chatAssistantSettings().enableThinkingMode(); return m_agentController->currentSupportsThinking();
}
void ChatRootView::setUseThinking(bool enabled)
{
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
} }
void ChatRootView::applyFileEdit(const QString &editId) void ChatRootView::applyFileEdit(const QString &editId)
@@ -1249,10 +1227,7 @@ QString ChatRootView::lastInfoMessage() const
bool ChatRootView::isThinkingSupport() const bool ChatRootView::isThinkingSupport() const
{ {
auto providerName = Settings::generalSettings().caProvider(); return m_agentController->currentSupportsThinking();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
} }
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
@@ -1273,66 +1248,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
return imageExtensions.contains(fileInfo.suffix().toLower()); 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() void ChatRootView::compressCurrentChat()
{ {
if (m_chatCompressor->isCompressing()) { if (m_chatCompressor->isCompressing()) {
@@ -1349,7 +1264,20 @@ void ChatRootView::compressCurrentChat()
autosave(); autosave();
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel); if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_chatCompressor->setSessionManager(sessionManager());
QString compressionAgent = currentChatAgent();
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatCompression;
if (!roster.isEmpty() && agentFactory()) {
const QString picked
= AgentRouter::pickAgent(roster, AgentRouter::Context{}, *agentFactory());
if (!picked.isEmpty())
compressionAgent = picked;
}
m_chatCompressor->setActiveAgent(compressionAgent);
m_chatCompressor->startCompression(m_recentFilePath, m_history);
} }
void ChatRootView::cancelCompression() void ChatRootView::cancelCompression()

View File

@@ -11,18 +11,22 @@
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
namespace QodeAssist {
class AgentFactory;
class SessionManager;
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class AgentRoleController; class ChatAgentController;
class ChatConfigurationController;
class FileEditController; class FileEditController;
class InputTokenCounter; class InputTokenCounter;
class ChatHistoryStore; class ChatHistoryStore;
@@ -32,7 +36,6 @@ class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) 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(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
@@ -46,10 +49,8 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL) Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL) Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) Q_PROPERTY(bool useThinking READ useThinking NOTIFY useThinkingChanged 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) Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
@@ -57,13 +58,10 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL) Q_PROPERTY(QStringList availableRoles READ availableRoles NOTIFY availableRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL) Q_PROPERTY(QString currentRole READ currentRole WRITE setCurrentRole NOTIFY currentRoleChanged 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(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
@@ -75,7 +73,6 @@ public:
~ChatRootView() override; ~ChatRootView() override;
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath); void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath); void loadHistory(const QString &filePath);
@@ -104,7 +101,6 @@ public:
QString sendShortcutText() const; QString sendShortcutText() const;
Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings(); Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath); Q_INVOKABLE void openFileInEditor(const QString &filePath);
@@ -139,18 +135,11 @@ public:
void setRequestProgressStatus(bool state); void setRequestProgressStatus(bool state);
QString lastErrorMessage() const; 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; Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const; bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const; bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId); Q_INVOKABLE void rejectFileEdit(const QString &editId);
@@ -161,23 +150,19 @@ public:
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats(); 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 compressCurrentChat();
Q_INVOKABLE void cancelCompression(); Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles(); Q_INVOKABLE void loadAvailableChatAgents();
Q_INVOKABLE void applyAgentRole(const QString &roleId); QStringList availableChatAgents() const;
Q_INVOKABLE void openAgentRolesSettings(); QString currentChatAgent() const;
QStringList availableAgentRoles() const; void setCurrentChatAgent(const QString &name);
QString currentAgentRole() const;
QString baseSystemPrompt() const; Q_INVOKABLE void loadAvailableRoles();
QString currentAgentRoleDescription() const; QStringList availableRoles() const;
QString currentAgentRoleSystemPrompt() const; QString currentRole() const;
void setCurrentRole(const QString &roleId);
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const; int currentMessagePendingEdits() const;
@@ -206,7 +191,6 @@ public slots:
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged(); void attachmentFilesChanged();
void linkedFilesChanged(); void linkedFilesChanged();
void inputTokensCountChanged(); void inputTokensCountChanged();
@@ -223,20 +207,17 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void sendShortcutTextChanged(); void sendShortcutTextChanged();
void activeRulesChanged();
void activeRulesCountChanged();
void useToolsChanged(); void useToolsChanged();
void useThinkingChanged(); void useThinkingChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged(); void availableChatAgentsChanged();
void currentAgentRoleChanged(); void currentChatAgentChanged();
void baseSystemPromptChanged(); void availableRolesChanged();
void currentRoleChanged();
void isCompressingChanged(); void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
@@ -256,25 +237,22 @@ private:
bool deferSendForAutoCompress( bool deferSendForAutoCompress(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles);
bool useTools,
bool useThinking);
void dispatchSend( void dispatchSend(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles);
bool useTools,
bool useThinking);
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const; SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const; Skills::SkillsManager *skillsManager() const;
AgentFactory *agentFactory() const;
SessionManager *sessionManager() const;
QodeAssist::ConversationHistory *m_history;
ChatModel *m_chatModel; ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager; ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;
QStringList m_attachmentFiles; QStringList m_attachmentFiles;
QStringList m_linkedFiles; QStringList m_linkedFiles;
@@ -283,8 +261,6 @@ private:
QString message; QString message;
QStringList attachments; QStringList attachments;
QStringList linkedFiles; QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false; bool active = false;
}; };
PendingSend m_pendingSend; PendingSend m_pendingSend;
@@ -294,13 +270,14 @@ private:
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_lastInfoMessage; QString m_lastInfoMessage;
QString m_currentRole = QStringLiteral("developer");
QStringList m_availableRoles;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController; ChatAgentController *m_agentController;
ChatConfigurationController *m_configurationController;
FileEditController *m_fileEditController; FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter; InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore; ChatHistoryStore *m_historyStore;
@@ -308,6 +285,8 @@ private:
mutable bool m_sessionFileRegistryResolved = false; mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager; mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false; mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -5,7 +5,8 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include <QBuffer> #include <memory>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -13,12 +14,57 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <LLMQore/ContentBlocks.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <MessageSerializer.hpp>
#include <PluginBlocks.hpp>
#include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
const QString ChatSerializer::VERSION = "0.2"; namespace {
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath) const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
void registerEditFromResult(const QString &result)
{ {
const int pos = result.indexOf(kFileEditMarker);
if (pos < 0)
return;
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (!doc.isObject())
return;
const QJsonObject obj = doc.object();
const QString editId = obj.value("edit_id").toString();
const QString filePath = obj.value("file").toString();
if (editId.isEmpty() || filePath.isEmpty())
return;
Context::ChangesManager::instance().addFileEdit(
editId,
filePath,
obj.value("old_content").toString(),
obj.value("new_content").toString(),
/*autoApply=*/false,
/*isFromHistory=*/true);
}
} // namespace
const QString ChatSerializer::VERSION = "0.3";
SerializationResult ChatSerializer::saveToFile(
const ConversationHistory *history, const QString &filePath)
{
if (!history)
return {false, "No conversation history"};
if (!ensureDirectoryExists(filePath)) { if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"}; return {false, "Failed to create directory structure"};
} }
@@ -28,9 +74,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, QString("Failed to open file for writing: %1").arg(filePath)}; return {false, QString("Failed to open file for writing: %1").arg(filePath)};
} }
QJsonObject root = serializeChat(model, filePath); QJsonDocument doc(serializeChat(history));
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) { if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())}; return {false, QString("Failed to write to file: %1").arg(file.errorString())};
} }
@@ -38,8 +82,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {true, QString()}; return {true, QString()};
} }
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath) SerializationResult ChatSerializer::loadFromFile(
ConversationHistory *history, const QString &filePath)
{ {
if (!history)
return {false, "No conversation history"};
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)}; return {false, QString("Failed to open file for reading: %1").arg(filePath)};
@@ -51,180 +99,140 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
return {false, QString("JSON parse error: %1").arg(error.errorString())}; return {false, QString("JSON parse error: %1").arg(error.errorString())};
} }
QJsonObject root = doc.object(); const QJsonObject root = doc.object();
QString version = root["version"].toString(); const QString version = root["version"].toString();
if (!validateVersion(version)) { if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)}; return {false, QString("Unsupported version: %1").arg(version)};
} }
if (!deserializeChat(model, root, filePath)) { if (version == VERSION)
return {false, "Failed to deserialize chat data"}; return loadCurrent(history, root);
} return loadLegacy(history, root);
return {true, QString()};
} }
QJsonObject ChatSerializer::serializeMessage( QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
const ChatModel::Message &message, const QString &chatFilePath)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
if (message.role == ChatModel::ChatRole::Tool) {
if (!message.toolName.isEmpty())
messageObj["toolName"] = message.toolName;
if (!message.toolArguments.isEmpty())
messageObj["toolArguments"] = message.toolArguments;
if (!message.toolResult.isEmpty())
messageObj["toolResult"] = message.toolResult;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
QJsonObject attachmentObj;
attachmentObj["fileName"] = attachment.filename;
attachmentObj["storedPath"] = attachment.content;
attachmentsArray.append(attachmentObj);
}
messageObj["attachments"] = attachmentsArray;
}
if (!message.images.isEmpty()) {
QJsonArray imagesArray;
for (const auto &image : message.images) {
QJsonObject imageObj;
imageObj["fileName"] = image.fileName;
imageObj["storedPath"] = image.storedPath;
imageObj["mediaType"] = image.mediaType;
imagesArray.append(imageObj);
}
messageObj["images"] = imagesArray;
}
if (message.promptTokens > 0 || message.completionTokens > 0) {
QJsonObject usageObj;
usageObj["promptTokens"] = message.promptTokens;
usageObj["completionTokens"] = message.completionTokens;
if (message.cachedPromptTokens > 0)
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
if (message.reasoningTokens > 0)
usageObj["reasoningTokens"] = message.reasoningTokens;
messageObj["usage"] = usageObj;
}
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(
const QJsonObject &json, const QString &chatFilePath)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
message.toolName = json["toolName"].toString();
message.toolArguments = json["toolArguments"].toObject();
message.toolResult = json["toolResult"].toString();
if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) {
QJsonObject attachmentObj = attachmentValue.toObject();
Context::ContentFile attachment;
attachment.filename = attachmentObj["fileName"].toString();
attachment.content = attachmentObj["storedPath"].toString();
message.attachments.append(attachment);
}
}
if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) {
QJsonObject imageObj = imageValue.toObject();
ChatModel::ImageAttachment image;
image.fileName = imageObj["fileName"].toString();
image.storedPath = imageObj["storedPath"].toString();
image.mediaType = imageObj["mediaType"].toString();
message.images.append(image);
}
}
if (json.contains("usage")) {
const QJsonObject usageObj = json["usage"].toObject();
message.promptTokens = usageObj["promptTokens"].toInt();
message.completionTokens = usageObj["completionTokens"].toInt();
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
}
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
{ {
QJsonArray messagesArray; QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) { for (const auto &message : history->messages())
messagesArray.append(serializeMessage(message, chatFilePath)); messagesArray.append(MessageSerializer::toJson(message));
}
QJsonObject root; QJsonObject root;
root["version"] = VERSION; root["version"] = VERSION;
root["messages"] = messagesArray; root["messages"] = messagesArray;
return root; return root;
} }
bool ChatSerializer::deserializeChat( SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
{ {
QJsonArray messagesArray = json["messages"].toArray(); history->clear();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
for (const auto &messageValue : messagesArray) { const QJsonArray messagesArray = root["messages"].toArray();
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath)); for (const auto &value : messagesArray) {
bool ok = false;
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
if (ok)
history->append(std::move(message));
} }
model->clear(); registerHistoricalFileEdits(history);
return {true, QString()};
}
model->setLoadingFromHistory(true); SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
{
history->clear();
for (const auto &message : messages) { const QJsonArray arr = root["messages"].toArray();
model->addMessage( int i = 0;
message.content, while (i < arr.size()) {
message.role, const QJsonObject mj = arr[i].toObject();
message.id, const auto role = static_cast<LegacyRole>(mj["role"].toInt());
message.attachments,
message.images, if (role == LegacyRole::Tool) {
message.isRedacted, Message assistant(Message::Role::Assistant);
message.signature); Message toolResults(Message::Role::User);
if (message.role == ChatModel::ChatRole::Tool) { while (i < arr.size()
model->setToolMessageData( && static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
message.id, message.toolName, message.toolArguments, message.toolResult); const QJsonObject tj = arr[i].toObject();
const QString toolName = tj["toolName"].toString();
const QString id = tj["id"].toString();
if (!toolName.isEmpty()) {
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
id, toolName, tj["toolArguments"].toObject()));
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
id, tj["toolResult"].toString()));
}
++i;
}
if (!assistant.blocks().empty()) {
history->append(std::move(assistant));
history->append(std::move(toolResults));
}
continue;
}
++i;
if (role == LegacyRole::FileEdit)
continue; // derived from the tool result in the new model
if (role == LegacyRole::Thinking) {
const QString content = mj["content"].toString();
const QString signature = mj["signature"].toString();
Message assistant(Message::Role::Assistant);
if (mj["isRedacted"].toBool(false)) {
assistant.appendBlock(
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
} else {
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
assistant.appendBlock(
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
}
history->append(std::move(assistant));
continue;
}
if (role == LegacyRole::User) {
Message user(Message::Role::User, mj["id"].toString());
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
for (const auto &a : mj["attachments"].toArray()) {
const QJsonObject ao = a.toObject();
user.appendBlock(std::make_unique<StoredAttachmentContent>(
ao["fileName"].toString(), ao["storedPath"].toString()));
}
for (const auto &im : mj["images"].toArray()) {
const QJsonObject io = im.toObject();
user.appendBlock(std::make_unique<StoredImageContent>(
io["fileName"].toString(),
io["storedPath"].toString(),
io["mediaType"].toString()));
}
history->append(std::move(user));
} else {
const QString content = mj["content"].toString();
if (content.trimmed().isEmpty())
continue;
const Message::Role mapped
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
Message message(mapped, mj["id"].toString());
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
history->append(std::move(message));
} }
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size())
.arg(message.isRedacted)
.arg(message.signature.length()));
} }
model->setLoadingFromHistory(false); registerHistoricalFileEdits(history);
return {true, QString()};
}
return true; void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history)
{
for (const auto &message : history->messages()) {
for (const auto &block : message.blocks()) {
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
registerEditFromResult(tr->result());
}
}
} }
bool ChatSerializer::ensureDirectoryExists(const QString &filePath) bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
@@ -236,18 +244,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
bool ChatSerializer::validateVersion(const QString &version) bool ChatSerializer::validateVersion(const QString &version)
{ {
if (version == VERSION) { return version == VERSION || version == "0.2" || version == "0.1";
return true;
}
if (version == "0.1") {
LOG_MESSAGE(
"Loading chat from old format 0.1 - images folder structure has changed from _images "
"to _content");
return true;
}
return false;
} }
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath) QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)

View File

@@ -4,11 +4,12 @@
#pragma once #pragma once
#include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include "ChatModel.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -21,26 +22,26 @@ struct SerializationResult
class ChatSerializer class ChatSerializer
{ {
public: public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath); static SerializationResult saveToFile(
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath); const ConversationHistory *history, const QString &filePath);
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
// Content management (images and text files) // Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath); static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(const QString &chatFilePath, static bool saveContentToStorage(
const QString &fileName, const QString &chatFilePath,
const QString &base64Data, const QString &fileName,
QString &storedPath); const QString &base64Data,
QString &storedPath);
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath); static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
private: private:
static const QString VERSION; static const QString VERSION;
static constexpr int CURRENT_VERSION = 1;
static QJsonObject serializeChat(const ConversationHistory *history);
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
static void registerHistoricalFileEdits(const ConversationHistory *history);
static bool ensureDirectoryExists(const QString &filePath); static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version); static bool validateVersion(const QString &version);

View File

@@ -4,73 +4,104 @@
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <LLMQore/BaseClient.hpp> #include <memory>
#include <vector>
#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/buildconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QImageReader>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QRegularExpression> #include <QRegularExpression>
#include <QUuid> #include <QUuid>
#include <coreplugin/editormanager/editormanager.h> #include <ConversationHistory.hpp>
#include <coreplugin/editormanager/ieditor.h> #include <ContextRenderer.hpp>
#include <coreplugin/idocument.h> #include <Message.hpp>
#include <projectexplorer/project.h> #include <PluginBlocks.hpp>
#include <projectexplorer/projectexplorer.h> #include <Session.hpp>
#include <projectexplorer/projectmanager.h> #include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <LLMQore/ToolsManager.hpp>
#include "tools/ReadOriginalHistoryTool.hpp" #include "tools/ReadOriginalHistoryTool.hpp"
#include "tools/TodoTool.hpp" #include "tools/TodoTool.hpp"
#include "tools/ToolsRegistration.hpp"
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "ProvidersManager.hpp"
#include "SkillsSettings.hpp" #include "SkillsSettings.hpp"
#include "ToolsSettings.hpp" #include "ToolsSettings.hpp"
#include <RulesLoader.hpp>
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
#include <sources/skills/SkillsManager.hpp> #include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( namespace {
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent) struct StoredImage
{
QString fileName;
QString storedPath;
QString mediaType;
};
} // namespace
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
: QObject(parent) : QObject(parent)
, m_promptProvider(promptProvider)
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
{} {}
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{
m_skillsManager = skillsManager;
}
ClientInterface::~ClientInterface() ClientInterface::~ClientInterface()
{ {
cancelRequest(); cancelRequest();
} }
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{
m_skillsManager = skillsManager;
}
void ClientInterface::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void ClientInterface::setHistory(ConversationHistory *history)
{
m_history = history;
}
void ClientInterface::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
}
void ClientInterface::setActiveRole(const QString &roleId)
{
m_activeRoleId = roleId;
}
void ClientInterface::sendMessage( void ClientInterface::sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments, const QList<QString> &attachments,
const QList<QString> &linkedFiles, const QList<QString> &linkedFiles)
bool useTools,
bool useThinking)
{ {
if (message.trimmed().isEmpty() && attachments.isEmpty()) { if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message"); LOG_MESSAGE("Ignoring empty chat message");
@@ -78,19 +109,16 @@ void ClientInterface::sendMessage(
} }
cancelRequest(); cancelRequest();
m_accumulatedResponses.clear();
Context::ChangesManager::instance().archiveAllNonArchivedEdits(); Context::ChangesManager::instance().archiveAllNonArchivedEdits();
QList<QString> imageFiles; QList<QString> imageFiles;
QList<QString> textFiles; QList<QString> textFiles;
for (const QString &filePath : attachments) { for (const QString &filePath : attachments) {
if (isImageFile(filePath)) { if (isImageFile(filePath))
imageFiles.append(filePath); imageFiles.append(filePath);
} else { else
textFiles.append(filePath); textFiles.append(filePath);
}
} }
QList<Context::ContentFile> storedAttachments; QList<Context::ContentFile> storedAttachments;
@@ -112,24 +140,19 @@ void ClientInterface::sendMessage(
.arg(textFiles.size())); .arg(textFiles.size()));
} }
QList<ChatModel::ImageAttachment> imageAttachments; QList<StoredImage> storedImages;
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) { for (const QString &imagePath : imageFiles) {
QString base64Data = encodeImageToBase64(imagePath); QString base64Data = encodeImageToBase64(imagePath);
if (base64Data.isEmpty()) { if (base64Data.isEmpty())
continue; continue;
}
QString storedPath; QString storedPath;
QFileInfo fileInfo(imagePath); QFileInfo fileInfo(imagePath);
if (ChatSerializer::saveContentToStorage( if (ChatSerializer::saveContentToStorage(
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
ChatModel::ImageAttachment imageAttachment; storedImages.append(
imageAttachment.fileName = fileInfo.fileName(); {fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)});
imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment);
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
} }
} }
@@ -138,328 +161,287 @@ void ClientInterface::sendMessage(
.arg(imageFiles.size())); .arg(imageFiles.size()));
} }
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments); if (!m_sessionManager) {
const QString error = QStringLiteral("Chat session manager is not available");
auto &chatAssistantSettings = Settings::chatAssistantSettings(); LOG_MESSAGE(error);
emit errorOccurred(error);
auto providerName = Settings::generalSettings().caProvider(); return;
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); }
if (!m_history) {
if (!provider) { const QString error = QStringLiteral("Chat history is not available");
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); LOG_MESSAGE(error);
emit errorOccurred(error);
return; return;
} }
auto templateName = Settings::generalSettings().caTemplate(); QString sessionError;
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
if (!session) {
if (!promptTemplate) { const QString error = sessionError.isEmpty()
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); ? QStringLiteral("No chat agent selected")
: sessionError;
LOG_MESSAGE(error);
emit errorOccurred(error);
return; return;
} }
PluginLLMCore::ContextData context; auto *client = session->client();
if (!client) {
const bool isToolsEnabled = useTools; const QString error = QStringLiteral("Chat agent has no live client");
LOG_MESSAGE(error);
if (chatAssistantSettings.useSystemPrompt()) { m_sessionManager->removeSession(session);
QString systemPrompt = chatAssistantSettings.systemPrompt(); emit errorOccurred(error);
return;
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;
} }
const bool toolHistory = promptTemplate->supportsToolHistory(); auto *project = ProjectExplorer::ProjectManager::startupProject();
Templates::ContextRenderer::Bindings bindings;
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
bindings.homeDir = QDir::homePath();
bindings.roleId = m_activeRoleId;
session->setContextBindings(bindings);
QVector<PluginLLMCore::Message> messages; const QString chatFilePath = m_chatFilePath;
int toolCallMsgIdx = -1; session->setContentLoader([chatFilePath](const QString &storedPath) {
for (const auto &msg : m_chatModel->getChatHistory()) { return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
if (msg.role == ChatModel::ChatRole::Tool) { });
if (!toolHistory || msg.toolName.isEmpty()) {
continue;
}
if (toolCallMsgIdx < 0) { m_sessionManager->toolContributors().contribute(client->tools());
PluginLLMCore::Message assistantCall; client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
assistantCall.role = "assistant"; client->setTransferTimeout(
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(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
connect( const QString chatContext = buildChatContextLayer(message, linkedFiles);
provider->client(), if (!chatContext.isEmpty())
&::LLMQore::BaseClient::chunkReceived, session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
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 customEndpoint = Settings::generalSettings().caCustomEndpoint(); std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider, !toolHistory}; for (const auto &attachment : storedAttachments) {
blocks.push_back(
std::make_unique<StoredAttachmentContent>(attachment.filename, attachment.content));
}
emit requestStarted(requestId); if (!storedImages.isEmpty() && session->supportsImages()) {
for (const auto &image : storedImages) {
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) blocks.push_back(std::make_unique<StoredImageContent>(
&& provider->toolsManager()) { image.fileName, image.storedPath, image.mediaType));
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>( }
provider->toolsManager()->tool("todo_tool"))) { } else if (!storedImages.isEmpty() && !session->supportsImages()) {
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
.arg(m_activeAgent)
.arg(storedImages.size()));
}
if (!m_chatFilePath.isEmpty()) {
if (auto *todoTool
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath); todoTool->setCurrentSessionId(m_chatFilePath);
} }
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>( if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
provider->toolsManager()->tool("read_original_history"))) { client->tools()->tool("read_original_history"))) {
historyTool->setCurrentSessionId(m_chatFilePath); historyTool->setCurrentSessionId(m_chatFilePath);
} }
} }
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) {
onSessionEvent(session, ev);
});
connect(
session, &Session::finished, this,
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); });
connect(
session, &Session::failed, this,
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onSessionFailed(id, error);
});
const LLMQore::RequestID requestId = session->send(std::move(blocks));
if (requestId.isEmpty()) {
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
.arg(m_activeAgent, session->lastError().message);
LOG_MESSAGE(error);
m_sessionManager->removeSession(session);
emit errorOccurred(error);
return;
}
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
emit requestStarted(requestId);
}
QString ClientInterface::requestIdForSession(Session *session) const
{
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().session == session)
return it.key();
}
return {};
}
void ClientInterface::onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev)
{
if (ev.kind() != ResponseEvent::Kind::Usage)
return;
const auto *usage = ev.as<ResponseEvents::Usage>();
if (!usage)
return;
const QString requestId = requestIdForSession(session);
if (!requestId.isEmpty()) {
m_chatModel->setMessageUsage(
requestId,
usage->inputTokens,
usage->outputTokens,
usage->cachedTokens,
usage->reasoningTokens);
}
emit messageUsageReceived(
usage->inputTokens, usage->outputTokens, usage->cachedTokens, usage->reasoningTokens);
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(usage->inputTokens)
.arg(usage->outputTokens)
.arg(usage->cachedTokens)
.arg(usage->reasoningTokens));
}
void ClientInterface::onSessionFinished(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
QString applyError;
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
emit messageReceivedCompletely();
m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
emit errorOccurred(error.message);
m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
QString ClientInterface::buildChatContextLayer(
const QString &message, const QList<QString> &linkedFiles) const
{
QString context;
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() void ClientInterface::clearMessages()
{ {
const auto providerName = Settings::generalSettings().caProvider(); if (m_history)
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); m_history->clear();
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);
}
}
m_chatModel->clear();
} }
void ClientInterface::cancelRequest() void ClientInterface::cancelRequest()
{ {
QSet<PluginLLMCore::Provider *> providers; const auto requests = m_activeRequests;
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());
}
}
m_activeRequests.clear(); 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 && m_sessionManager)
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request) m_sessionManager->removeSession(session);
{
const auto message = response.trimmed();
if (!message.isEmpty()) {
QString messageId = request["id"].toString();
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
} }
LOG_MESSAGE("All chat requests cancelled and state cleared");
} }
QString ClientInterface::getCurrentFileContext() const QString ClientInterface::getCurrentFileContext() const
@@ -486,166 +468,11 @@ QString ClientInterface::getCurrentFileContext() const
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); 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 Context::ContextManager *ClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
} }
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
}
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
QString applyError;
bool applySuccess
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
+ ": " + finalText);
emit messageReceivedCompletely();
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId))
return;
if (!info.usage)
return;
const auto &u = *info.usage;
m_chatModel->setMessageUsage(
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
emit messageUsageReceived(
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
LOG_MESSAGE(QString("Chat 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 ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
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);
}
void ClientInterface::handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
return;
}
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
if (thinking.isEmpty()) {
m_chatModel->addRedactedThinkingBlock(requestId, signature);
} else {
m_chatModel->addThinkingBlock(requestId, thinking, signature);
}
}
void ClientInterface::handleToolExecutionStarted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments)
{
const auto requestIt = m_activeRequests.constFind(requestId);
if (requestIt == m_activeRequests.constEnd()) {
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
return;
}
if (requestIt->dropPreToolText) {
m_chatModel->dropTrailingAssistantMessage(requestId);
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
m_awaitingContinuation.insert(requestId);
}
void ClientInterface::handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
return;
}
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
}
bool ClientInterface::isImageFile(const QString &filePath) const bool ClientInterface::isImageFile(const QString &filePath) const
{ {
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"}; static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
@@ -693,46 +520,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
return imageData.toBase64(); 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) 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_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath); m_chatModel->setChatFilePath(filePath);
} }

View File

@@ -5,16 +5,21 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QSet> #include <QPointer>
#include <QString> #include <QString>
#include <QVector>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp" #include <ErrorInfo.hpp>
#include "pluginllmcore/IPromptProvider.hpp"
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <ResponseEvent.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
@@ -26,23 +31,24 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager); void setSkillsManager(Skills::SkillsManager *skillsManager);
void setSessionManager(SessionManager *sessionManager);
void setHistory(ConversationHistory *history);
void setActiveAgent(const QString &agentName);
void setActiveRole(const QString &roleId);
void sendMessage( void sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {}, const QList<QString> &linkedFiles = {});
bool useTools = false,
bool useThinking = false);
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
Context::ContextManager *contextManager() const; Context::ContextManager *contextManager() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -53,50 +59,35 @@ signals:
void messageUsageReceived( void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens); int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText);
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);
void handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void handleToolExecutionStarted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput);
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request); void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( QString buildChatContextLayer(
const QString &basePrompt, const QList<QString> &linkedFiles) const; const QString &message, const QList<QString> &linkedFiles) const;
QString requestIdForSession(Session *session) const;
bool isImageFile(const QString &filePath) const; bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
bool dropPreToolText = false;
}; };
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QPointer<ConversationHistory> m_history;
Skills::SkillsManager *m_skillsManager = nullptr; Skills::SkillsManager *m_skillsManager = nullptr;
QPointer<SessionManager> m_sessionManager;
QString m_activeAgent;
QString m_activeRoleId;
QString m_chatFilePath; QString m_chatFilePath;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
QSet<QString> m_awaitingContinuation;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -10,15 +10,13 @@
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include "ChatModel.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent) FileEditController::FileEditController(QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel)
{ {
auto &changes = Context::ChangesManager::instance(); auto &changes = Context::ChangesManager::instance();
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) { connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
@@ -80,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId)); LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) { if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully")); emit infoMessage(QString("File edit applied successfully"));
updateFileEditStatus(editId, "applied");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -95,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId)); LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) { if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected")); emit infoMessage(QString("File edit rejected"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -110,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId)); LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) { if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully")); emit infoMessage(QString("File edit undone successfully"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -163,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath)); LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
} }
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
{
auto messages = m_chatModel->getChatHistory();
for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker);
QString jsonStr = content;
if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length());
}
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject obj = doc.object();
obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage;
}
QString updatedContent = marker
+ QString::fromUtf8(
QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
}
break;
}
}
updateStats();
}
void FileEditController::applyAllForCurrentMessage() void FileEditController::applyAllForCurrentMessage()
{ {
if (m_currentRequestId.isEmpty()) { if (m_currentRequestId.isEmpty()) {
@@ -223,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
: QString("Failed to apply some file edits:\n%1").arg(errorMsg)); : QString("Failed to apply some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
updateStats(); updateStats();
} }
@@ -255,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
: QString("Failed to undo some file edits:\n%1").arg(errorMsg)); : QString("Failed to undo some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
updateStats(); updateStats();
} }

View File

@@ -9,14 +9,12 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class FileEditController : public QObject class FileEditController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr); explicit FileEditController(QObject *parent = nullptr);
void setCurrentRequestId(const QString &requestId); void setCurrentRequestId(const QString &requestId);
void clearCurrentRequestId(); void clearCurrentRequestId();
@@ -41,9 +39,6 @@ signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status);
ChatModel *m_chatModel;
QString m_currentRequestId; QString m_currentRequestId;
int m_totalEdits{0}; int m_totalEdits{0};
int m_appliedEdits{0}; int m_appliedEdits{0};

View File

@@ -6,26 +6,22 @@
#include <algorithm> #include <algorithm>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <utils/aspects.h> #include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatModel.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
InputTokenCounter::InputTokenCounter( InputTokenCounter::InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent) ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_history(history)
, m_contextManager(contextManager) , m_contextManager(contextManager)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
@@ -42,12 +38,6 @@ InputTokenCounter::InputTokenCounter(
this, this,
&InputTokenCounter::recompute); &InputTokenCounter::recompute);
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
rewireToolsChangedConnection();
recompute();
});
rewireToolsChangedConnection();
recompute(); recompute();
} }
@@ -74,24 +64,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
recompute(); 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() void InputTokenCounter::recompute()
{ {
int inputTokens = m_messageTokens; int inputTokens = m_messageTokens;
@@ -130,24 +102,10 @@ void InputTokenCounter::recompute()
} }
} }
const auto &history = m_chatModel->getChatHistory(); if (m_history) {
for (const auto &message : history) { for (const auto &message : m_history->messages()) {
inputTokens += Context::TokenUtils::estimateTokens(message.content); inputTokens += Context::TokenUtils::estimateTokens(message.text());
inputTokens += 4; // + role 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);
}
}
} }
} }

View File

@@ -7,21 +7,25 @@
#include <QObject> #include <QObject>
#include <QStringList> #include <QStringList>
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager; class ContextManager;
} }
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class InputTokenCounter : public QObject class InputTokenCounter : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
InputTokenCounter( InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr); ConversationHistory *history,
Context::ContextManager *contextManager,
QObject *parent = nullptr);
int inputTokens() const; int inputTokens() const;
@@ -37,11 +41,8 @@ signals:
void inputTokensChanged(); void inputTokensChanged();
private: private:
void rewireToolsChangedConnection(); ConversationHistory *m_history;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QMetaObject::Connection m_toolsChangedConn;
QStringList m_attachments; QStringList m_attachments;
QStringList m_linkedFiles; QStringList m_linkedFiles;

View File

@@ -138,43 +138,31 @@ ChatRootView {
relocateTooltip.text: (typeof _chatview !== 'undefined') relocateTooltip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab") ? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window") : qsTr("Move this chat to a separate window")
toolsButton {
checked: root.useTools
onCheckedChanged: {
root.useTools = toolsButton.checked
}
}
thinkingMode {
checked: root.useThinking
enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
}
}
settingsButton.onClicked: root.openSettings() settingsButton.onClicked: root.openSettings()
configSelector { agentSelector {
model: root.availableConfigurations model: root.availableChatAgents
displayText: root.currentConfiguration displayText: root.currentChatAgent
onActivated: function(index) { onActivated: function(index) {
if (index > 0) { root.currentChatAgent = root.availableChatAgents[index]
root.applyConfiguration(root.availableConfigurations[index])
}
} }
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableConfigurations() root.loadAvailableChatAgents()
} }
} }
roleSelector { roleSelector {
model: root.availableAgentRoles model: root.availableRoles
displayText: root.currentAgentRole displayText: root.currentRole
onActivated: function(index) { onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index]) root.currentRole = root.availableRoles[index]
} }
Component.onCompleted: root.loadAvailableRoles()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableAgentRoles() root.loadAvailableRoles()
} }
} }
} }
@@ -839,20 +827,7 @@ ChatRootView {
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 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() onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
} }
Connections { Connections {

View File

@@ -23,11 +23,9 @@ Rectangle {
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId property alias agentSelector: agentSelectorId
property alias roleSelector: roleSelector property alias roleSelector: roleSelectorId
property alias relocateTooltip: relocateTooltipId property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
@@ -134,7 +132,7 @@ Rectangle {
} }
QoAComboBox { QoAComboBox {
id: configSelectorId id: agentSelectorId
implicitHeight: 25 implicitHeight: 25
@@ -142,14 +140,14 @@ Rectangle {
currentIndex: 0 currentIndex: 0
QoAToolTip { QoAToolTip {
visible: configSelectorId.hovered visible: agentSelectorId.hovered
delay: 250 delay: 250
text: qsTr("Switch saved AI configuration") text: qsTr("Select chat agent (provider and model come from the agent)")
} }
} }
QoAComboBox { QoAComboBox {
id: roleSelector id: roleSelectorId
implicitHeight: 25 implicitHeight: 25
@@ -157,9 +155,9 @@ Rectangle {
currentIndex: 0 currentIndex: 0
QoAToolTip { QoAToolTip {
visible: roleSelector.hovered visible: roleSelectorId.hovered
delay: 250 delay: 250
text: qsTr("Switch agent role (different system prompts)") text: qsTr("Select the role (system prompt) for the chat")
} }
} }
} }
@@ -167,62 +165,6 @@ Rectangle {
Row { Row {
spacing: 10 spacing: 10
QoAButton {
id: toolsButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: toolsButtonId.hovered
delay: 250
text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return toolsButtonId.checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: thinkingModeId.hovered
delay: 250
text: thinkingModeId.enabled
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
QoAButton { QoAButton {
id: settingsButtonId id: settingsButtonId

View File

@@ -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

View File

@@ -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

View File

@@ -9,27 +9,47 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <Agent.hpp>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <PluginBlocks.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/common/ContextData.hpp"
#include <LLMQore/ContentBlocks.hpp>
#include <memory>
#include <vector>
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp" #include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp" #include "context/Utils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <pluginllmcore/RulesLoader.hpp> #include "sources/settings/PipelinesConfig.hpp"
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, AgentFactory &agentFactory,
PluginLLMCore::IPromptProvider *promptProvider, SessionManager &sessionManager,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger) IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings) : m_generalSettings(generalSettings)
, m_completeSettings(completeSettings) , m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry) , m_agentFactory(agentFactory)
, m_promptProvider(promptProvider) , m_sessionManager(sessionManager)
, m_documentReader(documentReader) , m_documentReader(documentReader)
, m_performanceLogger(performanceLogger) , m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
@@ -51,58 +71,56 @@ void LLMClientInterface::startImpl()
emit started(); emit started();
} }
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText) void LLMClientInterface::onCompletionFinished(const QString &requestId)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value(); QString fullText;
sendCompletionToClient(fullText, ctx.originalRequest, true); if (Session *session = it.value().session) {
if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
}
const QJsonObject originalRequest = it.value().originalRequest;
m_activeRequests.erase(it); sendCompletionToClient(fullText, originalRequest, true);
m_performanceLogger.endTimeMeasurement(requestId); finishRequest(requestId);
} }
void LLMClientInterface::handleRequestFinalized( void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
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)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"]; response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
QJsonObject errorObject; QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error; errorObject["message"] = error;
response["error"] = errorObject; response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); 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_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId); m_performanceLogger.endTimeMeasurement(requestId);
if (session)
m_sessionManager.release(session);
} }
void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::sendData(const QByteArray &data)
@@ -135,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest() void LLMClientInterface::handleCancelRequest()
{ {
QSet<PluginLLMCore::Provider *> providers; const auto requests = m_activeRequests;
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());
}
}
m_activeRequests.clear(); m_activeRequests.clear();
for (auto it = requests.begin(); it != requests.end(); ++it) {
m_performanceLogger.endTimeMeasurement(it.key());
if (Session *session = it.value().session)
m_sessionManager.release(session);
}
LOG_MESSAGE("All requests cancelled and state cleared"); LOG_MESSAGE("All requests cancelled and state cleared");
} }
@@ -237,133 +244,87 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return; return;
} }
auto updatedContext = prepareContext(request, documentInfo); const QString agentName = pickCompletionAgent(filePath);
if (agentName.isEmpty()) {
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo); QString error = QString("No code completion agent matches: %1").arg(filePath);
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);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() QString sessionError;
: m_generalSettings.ccPreset1Template(); Session *session = m_sessionManager.acquire(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 editorContext;
QString error = QString("No template found with name: %1").arg(templateName); if (context.fileContext.has_value())
editorContext.append(context.fileContext.value());
if (m_completeSettings.useOpenFilesContext())
editorContext.append(m_contextManager->openedFilesContext({filePath}));
if (!editorContext.isEmpty())
session->systemPrompt()->setLayer(QStringLiteral("completion.context"), editorContext);
connect(session, &Session::finished, this, [this, session](const LLMQore::RequestID &, const QString &) {
onCompletionFinished(requestIdForSession(session));
});
connect(session, &Session::failed, this, [this, session](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &error) {
onCompletionFailed(requestIdForSession(session), error.message);
});
if (auto *client = session->client())
client->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<CompletionContent>(
context.prefix.value_or(QString()), context.suffix.value_or(QString())));
const LLMQore::RequestID requestId = session->send(std::move(blocks), /*toolsOverride=*/false);
if (requestId.isEmpty()) {
QString error = QString("Failed to start completion request for agent '%1': %2")
.arg(agentName, session->lastError().message);
session->deleteLater();
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
QJsonObject payload{{"model", modelName}, {"stream", true}}; m_activeRequests[requestId] = {request, session};
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_performanceLogger.startTimeMeasurement(requestId); 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) const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
@@ -377,14 +338,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); return 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();
}
Context::ContextManager *LLMClientInterface::contextManager() const Context::ContextManager *LLMClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
@@ -393,15 +346,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient( void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete) 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 position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;

View File

@@ -8,12 +8,11 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <QPointer>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp> #include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
@@ -23,6 +22,14 @@ class QNetworkAccessManager;
namespace QodeAssist { namespace QodeAssist {
class AgentFactory;
class Session;
class SessionManager;
namespace Templates {
struct ContextData;
}
class LLMClientInterface : public LanguageClient::BaseClientInterface class LLMClientInterface : public LanguageClient::BaseClientInterface
{ {
Q_OBJECT Q_OBJECT
@@ -31,8 +38,8 @@ public:
LLMClientInterface( LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, AgentFactory &agentFactory,
PluginLLMCore::IPromptProvider *promptProvider, SessionManager &sessionManager,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger); IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override; ~LLMClientInterface() override;
@@ -52,12 +59,6 @@ public:
protected: protected:
void startImpl() override; 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: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request); void handleShutdown(const QJsonObject &request);
@@ -67,22 +68,26 @@ private:
void handleCancelRequest(); void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage); void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
void onCompletionFinished(const QString &requestId);
void onCompletionFailed(const QString &requestId, const QString &error);
void finishRequest(const QString &requestId);
QString requestIdForSession(Session *session) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
}; };
PluginLLMCore::ContextData prepareContext( Templates::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint( QString pickCompletionAgent(const QString &filePath) const;
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings; const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings; const Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; AgentFactory &m_agentFactory;
PluginLLMCore::IProviderRegistry &m_providerRegistry; SessionManager &m_sessionManager;
Context::IDocumentReader &m_documentReader; Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger; IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;

View File

@@ -1,7 +1,7 @@
{ {
"Id" : "qodeassist", "Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.9.21", "Version" : "0.9.20",
"CompatVersion" : "${IDE_VERSION}", "CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev", "VendorId" : "petrmironychev",

View File

@@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this); m_refactorWidgetHandler = new RefactorWidgetHandler(this);
} }
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
QodeAssistClient::~QodeAssistClient() QodeAssistClient::~QodeAssistClient()
{ {
cleanupConnections(); cleanupConnections();
@@ -263,9 +273,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
return; return;
if (m_llmClient->contextManager() if (m_llmClient->contextManager()->shouldIgnore(
->ignoreManager() editor->textDocument()->filePath().toUrlishString())) {
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -309,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager() if (m_llmClient->contextManager()->shouldIgnore(
->ignoreManager() editor->textDocument()->filePath().toUrlishString())) {
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -319,6 +327,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!m_refactorHandler) { if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this); m_refactorHandler = new QuickRefactorHandler(this);
m_refactorHandler->setSessionManager(m_sessionManager);
m_refactorHandler->setAgentFactory(m_agentFactory);
connect( connect(
m_refactorHandler, m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted, &QuickRefactorHandler::refactoringCompleted,

View File

@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp" #include "LSPCompletion.hpp"
@@ -16,11 +17,12 @@
#include "widgets/EditorChatButtonHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class AgentFactory;
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
Q_OBJECT Q_OBJECT
@@ -28,6 +30,9 @@ public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override; ~QodeAssistClient() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
@@ -68,6 +73,8 @@ private:
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr}; RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient; LLMClientInterface *m_llmClient;
SessionManager *m_sessionManager{nullptr};
AgentFactory *m_agentFactory{nullptr};
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -4,24 +4,40 @@
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <context/DocumentContextReader.hpp> #include <context/DocumentContextReader.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp> #include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp> #include <context/Utils.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp> #include <sources/common/ResponseCleaner.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp> #include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.hpp> #include <settings/ToolsSettings.hpp>
#include "sources/common/ContextData.hpp"
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/settings/PipelinesConfig.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist { namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent) QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
@@ -34,6 +50,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
QuickRefactorHandler::~QuickRefactorHandler() {} QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
void QuickRefactorHandler::sendRefactorRequest( void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions) TextEditor::TextEditorWidget *editor, const QString &instructions)
{ {
@@ -88,105 +114,109 @@ void QuickRefactorHandler::sendRefactorRequest(
prepareAndSendRequest(editor, instructions, range); 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( void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range) const Utils::Text::Range &range)
{ {
auto &settings = Settings::generalSettings(); const auto emitError = [this, editor](const QString &error) {
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);
LOG_MESSAGE(error); LOG_MESSAGE(error);
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = error; result.errorMessage = error;
result.editor = editor; result.editor = editor;
emit refactoringCompleted(result); emit refactoringCompleted(result);
};
if (!m_sessionManager) {
emitError(QStringLiteral("Quick refactor session manager is not available"));
return; return;
} }
const auto templateName = settings.qrTemplate(); const QString filePath = editor->textDocument()->filePath().toUrlishString();
auto promptTemplate = promptManager.getChatTemplateByName(templateName); const QString agentName = pickRefactorAgent(filePath);
if (agentName.isEmpty()) {
if (!promptTemplate) { emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath));
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);
return; return;
} }
QJsonObject payload{ QString sessionError;
{"model", Settings::generalSettings().qrModel()}, {"stream", true}}; Session *session = m_sessionManager->acquire(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(); const bool enableTools = Settings::quickRefactorSettings().useTools();
bool enableThinking = Settings::quickRefactorSettings().useThinking(); if (enableTools) {
provider->prepareRequest( m_sessionManager->toolContributors().contribute(client->tools());
payload, client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
promptTemplate, }
context,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
provider->client()->setMaxToolContinuations( session->systemPrompt()->setLayer(
Settings::toolsSettings().maxToolContinuations()); QStringLiteral("refactor"), buildSystemPrompt(editor, range));
provider->client()->setTransferTimeout( client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_isRefactoringInProgress = true; m_isRefactoringInProgress = true;
connect( connect(
provider->client(), session, &Session::finished, this,
&::LLMQore::BaseClient::requestCompleted, [this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); });
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect( connect(
provider->client(), session, &Session::failed, this,
&::LLMQore::BaseClient::requestFinalized, [this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
this, onRefactorFailed(id, error);
&QuickRefactorHandler::handleRequestFinalized, });
Qt::UniqueConnection);
connect( std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
provider->client(), const QString userMessage = instructions.isEmpty()
&::LLMQore::BaseClient::requestFailed, ? QStringLiteral("Refactor the code to improve its quality and maintainability.")
this, : instructions;
&QuickRefactorHandler::handleRequestFailed, blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
Qt::UniqueConnection);
const LLMQore::RequestID requestId = session->send(std::move(blocks), enableTools);
if (requestId.isEmpty()) {
m_isRefactoringInProgress = false;
const QString reason = session->lastError().message;
m_sessionManager->removeSession(session);
emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
.arg(agentName, reason));
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; m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}}; m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
m_activeRequests[requestId] = {request, provider};
} }
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( QString QuickRefactorHandler::buildSystemPrompt(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
const Utils::Text::Range &range,
const QString &instructions)
{ {
PluginLLMCore::ContextData context; Q_UNUSED(range)
auto textDocument = editor->textDocument(); auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader; Context::DocumentReaderQtCreator documentReader;
@@ -194,7 +224,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
if (!documentInfo.document) { if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available"); LOG_MESSAGE("Error: Document is not available");
return context; return Settings::quickRefactorSettings().systemPrompt();
} }
QTextCursor cursor = editor->textCursor(); QTextCursor cursor = editor->textCursor();
@@ -270,17 +300,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); 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 += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType; systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath; systemPrompt += "\nFile path: " + documentInfo.filePath;
@@ -294,7 +313,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
"\n- Your output will completely replace the selected code" "\n- Your output will completely replace the selected code"
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position" : "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
"\n- Your output will be inserted at the cursor location"; "\n- Your output will be inserted at the cursor location";
systemPrompt += "\n\n## Formatting Rules:" systemPrompt += "\n\n## Formatting Rules:"
"\n- Output ONLY the code itself, without ANY explanations or descriptions" "\n- Output ONLY the code itself, without ANY explanations or descriptions"
"\n- Do NOT include markdown code blocks (no ```, no language tags)" "\n- Do NOT include markdown code blocks (no ```, no language tags)"
@@ -302,9 +321,9 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
"\n- Do NOT repeat existing code, be precise with context" "\n- Do NOT repeat existing code, be precise with context"
"\n- Do NOT send in answer <cursor> or </cursor> and other tags" "\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"; "\n- The output must be ready to insert directly into the editor as-is";
systemPrompt += "\n\n## Indentation and Whitespace:"; systemPrompt += "\n\n## Indentation and Whitespace:";
if (cursor.hasSelection()) { if (cursor.hasSelection()) {
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart()); QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
int leadingSpaces = 0; int leadingSpaces = 0;
@@ -336,7 +355,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
.arg(leadingSpaces); .arg(leadingSpaces);
} }
} }
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code" systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
"\n- Maintain consistent indentation for nested blocks" "\n- Maintain consistent indentation for nested blocks"
"\n- Do NOT remove or reduce the base indentation level" "\n- Do NOT remove or reduce the base indentation level"
@@ -349,42 +368,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath}); systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
} }
context.systemPrompt = systemPrompt; return 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;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
m_isRefactoringInProgress = false;
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
result.editor = m_currentEditor;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
} }
void QuickRefactorHandler::cancelRequest() void QuickRefactorHandler::cancelRequest()
@@ -398,10 +382,10 @@ void QuickRefactorHandler::cancelRequest()
auto it = m_activeRequests.find(id); auto it = m_activeRequests.find(id);
if (it != m_activeRequests.end()) { if (it != m_activeRequests.end()) {
auto provider = it.value().provider; Session *session = it.value().session;
m_activeRequests.erase(it); m_activeRequests.erase(it);
if (provider) if (session && m_sessionManager)
provider->cancelRequest(id); m_sessionManager->release(session);
} }
RefactorResult result; RefactorResult result;
@@ -410,42 +394,66 @@ void QuickRefactorHandler::cancelRequest()
emit refactoringCompleted(result); emit refactoringCompleted(result);
} }
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText) void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
{ {
if (requestId == m_lastRequestId) { if (requestId != m_lastRequestId)
m_activeRequests.remove(requestId);
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
}
}
void QuickRefactorHandler::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (requestId != m_lastRequestId || !info.usage)
return; return;
const auto &u = *info.usage; auto it = m_activeRequests.find(requestId);
LOG_MESSAGE( Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5") if (it != m_activeRequests.end())
.arg(requestId) m_activeRequests.erase(it);
.arg(u.promptTokens)
.arg(u.completionTokens) QString fullText;
.arg(u.cachedPromptTokens) if (session) {
.arg(u.reasoningTokens)); if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
}
m_isRefactoringInProgress = false;
m_lastRequestId.clear();
const QString cleanedResponse = ResponseCleaner::clean(fullText);
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
result.editor = m_currentEditor;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->release(session);
} }
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error) void QuickRefactorHandler::onRefactorFailed(
const QString &requestId, const QodeAssist::ErrorInfo &error)
{ {
if (requestId == m_lastRequestId) { if (requestId != m_lastRequestId)
m_activeRequests.remove(requestId); return;
m_isRefactoringInProgress = false;
RefactorResult result; auto it = m_activeRequests.find(requestId);
result.success = false; Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
result.errorMessage = error; if (it != m_activeRequests.end())
result.editor = m_currentEditor; m_activeRequests.erase(it);
emit refactoringCompleted(result);
} m_isRefactoringInProgress = false;
m_lastRequestId.clear();
RefactorResult result;
result.success = false;
result.errorMessage = error.message;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->release(session);
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -6,18 +6,22 @@
#include <QJsonObject> #include <QJsonObject>
#include <QObject> #include <QObject>
#include <QPointer>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/textutils.h> #include <utils/textutils.h>
#include <ErrorInfo.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class Session;
class AgentFactory;
struct RefactorResult struct RefactorResult
{ {
QString newText; QString newText;
@@ -35,6 +39,9 @@ public:
explicit QuickRefactorHandler(QObject *parent = nullptr); explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override; ~QuickRefactorHandler() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest(); void cancelRequest();
@@ -43,30 +50,26 @@ public:
signals: signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result); void refactoringCompleted(const QodeAssist::RefactorResult &result);
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: private:
void prepareAndSendRequest( void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range); const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void onRefactorFinished(const QString &requestId);
PluginLLMCore::ContextData prepareContext( void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
TextEditor::TextEditorWidget *editor, QString buildSystemPrompt(
const Utils::Text::Range &range, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
const QString &instructions); QString pickRefactorAgent(const QString &filePath) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
}; };
QPointer<SessionManager> m_sessionManager;
QPointer<AgentFactory> m_agentFactory;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor; TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange; Utils::Text::Range m_currentRange;

View File

@@ -216,9 +216,9 @@ For optimal coding assistance, we recommend using these top-tier models:
### Additional Configuration ### Additional Configuration
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents or override bundled ones with TOML profiles
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts - **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens - **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore` - **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features ## Features
@@ -473,7 +473,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Custom Instructions** provide reusable templates that can be augmented with specific details - **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled - **Tool Calling** is available for Chat and Quick Refactor when enabled
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details. See [Agent Roles Guide](docs/agent-roles.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
## QtCreator Version Compatibility ## QtCreator Version Compatibility
@@ -532,7 +532,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository. 1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. 2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers. 3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
@@ -580,6 +580,10 @@ cmake --build .
## For Contributors ## For Contributors
### Adding an agent preset
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
### Code Style ### Code Style
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc - **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc

27
bench/CMakeLists.txt Normal file
View File

@@ -0,0 +1,27 @@
add_executable(QodeAssistBench
main.cpp
)
target_link_libraries(QodeAssistBench PRIVATE
Qt::Core
Session
Agents
Providers
ProvidersConfig
LLMQore
)
set_target_properties(QodeAssistBench PROPERTIES
OUTPUT_NAME bench
FOLDER "qtc_runnable"
)
if(APPLE)
get_target_property(_qtcCoreLoc QtCreator::Core LOCATION)
get_filename_component(_qtcCoreDir "${_qtcCoreLoc}" DIRECTORY)
get_filename_component(QTC_FRAMEWORKS_DIR "${_qtcCoreDir}/../../Frameworks" ABSOLUTE)
if(EXISTS "${QTC_FRAMEWORKS_DIR}")
configure_file(run-bench.sh.in "${CMAKE_CURRENT_BINARY_DIR}/run-bench.sh" @ONLY)
execute_process(COMMAND chmod +x "${CMAKE_CURRENT_BINARY_DIR}/run-bench.sh")
endif()
endif()

592
bench/main.cpp Normal file
View File

@@ -0,0 +1,592 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QTextStream>
#include <QTimer>
#include <functional>
#include <memory>
#include <optional>
#include <vector>
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/BaseTool.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolRegistry.hpp>
#include <LLMQore/ToolResult.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <Agent.hpp>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <ContextData.hpp>
#include <ContextRenderer.hpp>
#include <PluginBlocks.hpp>
#include <GenericProvider.hpp>
#include <Provider.hpp>
#include <ProviderInstance.hpp>
#include <ProviderInstanceFactory.hpp>
#include <ProviderSecretsStore.hpp>
#include <ResponseEvent.hpp>
#include <Session.hpp>
using namespace QodeAssist;
namespace {
QTextStream &out()
{
static QTextStream s(stdout);
return s;
}
QTextStream &err()
{
static QTextStream s(stderr);
return s;
}
QString readStdin()
{
QTextStream in(stdin);
return in.readAll();
}
QHash<QString, QString> parseEnvFile(const QString &path, QString *errorOut)
{
QHash<QString, QString> map;
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
if (errorOut)
*errorOut = QStringLiteral("cannot open env file: %1").arg(path);
return map;
}
QTextStream in(&f);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty() || line.startsWith(QLatin1Char('#')))
continue;
if (line.startsWith(QLatin1String("export ")))
line = line.mid(7).trimmed();
const int eq = line.indexOf(QLatin1Char('='));
if (eq <= 0)
continue;
const QString key = line.left(eq).trimmed();
QString value = line.mid(eq + 1).trimmed();
if (value.size() >= 2
&& ((value.startsWith(QLatin1Char('"')) && value.endsWith(QLatin1Char('"')))
|| (value.startsWith(QLatin1Char('\'')) && value.endsWith(QLatin1Char('\''))))) {
value = value.mid(1, value.size() - 2);
}
map.insert(key, value);
}
return map;
}
QStringList apiKeyCandidates(const QString &clientApi, const QString &apiKeyRef)
{
QStringList c;
if (!apiKeyRef.isEmpty())
c << apiKeyRef;
if (clientApi == QLatin1String("Claude"))
c << QStringLiteral("ANTHROPIC_API_KEY");
else if (clientApi.startsWith(QLatin1String("OpenAI")))
c << QStringLiteral("OPENAI_API_KEY");
else if (clientApi == QLatin1String("Mistral AI"))
c << QStringLiteral("MISTRAL_API_KEY");
else if (clientApi == QLatin1String("Codestral"))
c << QStringLiteral("CODESTRAL_API_KEY");
else if (clientApi == QLatin1String("Google AI"))
c << QStringLiteral("GEMINI_API_KEY") << QStringLiteral("GOOGLE_API_KEY");
else if (clientApi == QLatin1String("OpenRouter"))
c << QStringLiteral("OPENROUTER_API_KEY");
QString derived = clientApi.toUpper();
derived.replace(QRegularExpression(QStringLiteral("[^A-Z0-9]+")), QStringLiteral("_"));
derived = derived.trimmed();
while (derived.startsWith(QLatin1Char('_')))
derived.remove(0, 1);
while (derived.endsWith(QLatin1Char('_')))
derived.chop(1);
if (!derived.isEmpty())
c << derived + QStringLiteral("_API_KEY");
return c;
}
QString resolveApiKey(
const QHash<QString, QString> &envFile, const QString &clientApi, const QString &apiKeyRef)
{
for (const QString &name : apiKeyCandidates(clientApi, apiKeyRef)) {
auto it = envFile.constFind(name);
if (it != envFile.constEnd() && !it.value().isEmpty())
return it.value();
const QByteArray fromProc = qgetenv(name.toUtf8().constData());
if (!fromProc.isEmpty())
return QString::fromUtf8(fromProc);
}
return {};
}
QString imageMediaType(const QString &path)
{
const QString ext = QFileInfo(path).suffix().toLower();
if (ext == QLatin1String("png"))
return QStringLiteral("image/png");
if (ext == QLatin1String("jpg") || ext == QLatin1String("jpeg"))
return QStringLiteral("image/jpeg");
if (ext == QLatin1String("gif"))
return QStringLiteral("image/gif");
if (ext == QLatin1String("webp"))
return QStringLiteral("image/webp");
return {};
}
class BenchEchoTool : public LLMQore::BaseTool
{
public:
using BaseTool::BaseTool;
QString id() const override { return QStringLiteral("bench_echo"); }
QString displayName() const override { return QStringLiteral("Bench echo"); }
QString description() const override
{
return QStringLiteral("Echoes the given text back verbatim. "
"Use whenever the user asks to echo something.");
}
QJsonObject parametersSchema() const override
{
return QJsonObject{
{QStringLiteral("type"), QStringLiteral("object")},
{QStringLiteral("properties"),
QJsonObject{
{QStringLiteral("text"),
QJsonObject{
{QStringLiteral("type"), QStringLiteral("string")},
{QStringLiteral("description"), QStringLiteral("Text to echo back")}}}}},
{QStringLiteral("required"), QJsonArray{QStringLiteral("text")}}};
}
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input) override
{
return QtFuture::makeReadyValueFuture(LLMQore::ToolResult::text(
QStringLiteral("echo: %1").arg(input.value(QStringLiteral("text")).toString())));
}
};
class BenchAddTool : public LLMQore::BaseTool
{
public:
using BaseTool::BaseTool;
QString id() const override { return QStringLiteral("bench_add"); }
QString displayName() const override { return QStringLiteral("Bench add"); }
QString description() const override
{
return QStringLiteral("Adds two numbers and returns the sum. "
"Use whenever the user asks to add numbers.");
}
QJsonObject parametersSchema() const override
{
return QJsonObject{
{QStringLiteral("type"), QStringLiteral("object")},
{QStringLiteral("properties"),
QJsonObject{
{QStringLiteral("a"),
QJsonObject{{QStringLiteral("type"), QStringLiteral("number")}}},
{QStringLiteral("b"),
QJsonObject{{QStringLiteral("type"), QStringLiteral("number")}}}}},
{QStringLiteral("required"),
QJsonArray{QStringLiteral("a"), QStringLiteral("b")}}};
}
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input) override
{
const double sum = input.value(QStringLiteral("a")).toDouble()
+ input.value(QStringLiteral("b")).toDouble();
return QtFuture::makeReadyValueFuture(
LLMQore::ToolResult::text(QString::number(sum)));
}
};
void printEvent(const ResponseEvent &ev, bool showThinking)
{
switch (ev.kind()) {
case ResponseEvent::Kind::TextDelta:
if (const auto *d = ev.as<ResponseEvents::TextDelta>()) {
out() << d->text;
out().flush();
}
break;
case ResponseEvent::Kind::ThinkingDelta:
if (showThinking) {
if (const auto *d = ev.as<ResponseEvents::ThinkingDelta>()) {
err() << d->thinking;
err().flush();
}
}
break;
case ResponseEvent::Kind::ToolCallStart:
if (const auto *d = ev.as<ResponseEvents::ToolCallStart>())
err() << "\n[tool-call] " << d->name << " (" << d->id << ")\n";
break;
case ResponseEvent::Kind::ToolCallEnd:
if (const auto *d = ev.as<ResponseEvents::ToolCallEnd>()) {
const QString args
= QString::fromUtf8(QJsonDocument(d->finalArgs).toJson(QJsonDocument::Compact));
err() << "[tool-args] " << args << "\n";
}
break;
case ResponseEvent::Kind::ToolResult:
if (const auto *d = ev.as<ResponseEvents::ToolResult>())
err() << "[tool-result" << (d->isError ? " ERROR" : "") << "] " << d->text << "\n";
break;
case ResponseEvent::Kind::Usage:
if (const auto *d = ev.as<ResponseEvents::Usage>()) {
err() << "\n[usage] in=" << d->inputTokens << " out=" << d->outputTokens
<< " cached=" << d->cachedTokens << " reasoning=" << d->reasoningTokens << "\n";
}
break;
case ResponseEvent::Kind::Error:
if (const auto *d = ev.as<ResponseEvents::Error>())
err() << "\n[error] " << d->message << "\n";
break;
case ResponseEvent::Kind::MessageStart:
case ResponseEvent::Kind::ToolCallArgsDelta:
case ResponseEvent::Kind::MessageStop:
break;
}
}
} // namespace
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setOrganizationName(QStringLiteral("QtProject"));
QCoreApplication::setApplicationName(QStringLiteral("QtCreator"));
QCommandLineParser parser;
parser.setApplicationDescription(
"QodeAssist bench — drive an agent through the live session pipeline.");
parser.addHelpOption();
QCommandLineOption listOpt(QStringList{"l", "list"}, "List available agent profiles and exit.");
QCommandLineOption agentOpt(
QStringList{"a", "agent"}, "Agent profile name to run.", "name");
QCommandLineOption fileOpt(
QStringList{"f", "file"}, "Load an agent from a TOML file instead of by name.", "path");
QCommandLineOption promptOpt(
QStringList{"p", "prompt"},
"Prompt text. Repeatable: each occurrence is one chat turn, sent after the "
"previous turn finishes (history is replayed through the agent template). "
"If omitted, positional args or stdin are used as a single turn.",
"text");
QCommandLineOption noThinkingOpt("no-thinking", "Hide thinking deltas from output.");
QCommandLineOption envOpt(
QStringList{"e", "env"},
"Read API keys from a dotenv file (KEY=VALUE per line). Defaults to ./.env if present.",
"path");
QCommandLineOption apiKeyOpt(
"api-key", "API key to use for the agent's provider (overrides env/settings).", "value");
QCommandLineOption timeoutOpt(
"timeout",
"Network transfer timeout in seconds (a stalled stream fails instead of hanging). "
"Default 60, 0 disables.",
"seconds");
QCommandLineOption projectDirOpt(
QStringList{"C", "project-dir"},
"Project root for the agent's context (${PROJECT_DIR}). Defaults to the current directory.",
"path");
QCommandLineOption imageOpt(
QStringList{"i", "image"},
"Attach an image file (png/jpeg/gif/webp). Repeatable. Requires a vision-capable agent.",
"path");
QCommandLineOption mcpOpt(
"mcp",
"Load MCP servers from a JSON config (mcpServers map) to give the agent executable tools.",
"path");
QCommandLineOption builtinToolsOpt(
"builtin-tools",
"Register local test tools (bench_echo, bench_add) and force tools on. "
"Lets the model exercise tool calls without an MCP server, e.g. "
"-p \"echo hello via the tool\" -p \"now add 2 and 3\".");
QCommandLineOption fimOpt(
"fim",
"Fill-in-the-middle completion mode: send prompt as the prefix and --suffix as the suffix.");
QCommandLineOption suffixOpt(
"suffix", "Suffix code after the cursor (FIM mode only).", "text");
parser.addOption(listOpt);
parser.addOption(agentOpt);
parser.addOption(fileOpt);
parser.addOption(promptOpt);
parser.addOption(noThinkingOpt);
parser.addOption(envOpt);
parser.addOption(apiKeyOpt);
parser.addOption(timeoutOpt);
parser.addOption(projectDirOpt);
parser.addOption(imageOpt);
parser.addOption(mcpOpt);
parser.addOption(builtinToolsOpt);
parser.addOption(fimOpt);
parser.addOption(suffixOpt);
parser.addPositionalArgument("prompt", "Prompt text (alternative to --prompt).", "[prompt...]");
parser.process(app);
Providers::registerBuiltinProviders();
auto *instances = new Providers::ProviderInstanceFactory(&app);
auto *secrets = new Providers::ProviderSecretsStore(&app);
auto *agentFactory = new AgentFactory(instances, secrets, &app);
if (parser.isSet(listOpt)) {
const QStringList names = agentFactory->configNames();
if (names.isEmpty())
err() << "No agent profiles found.\n";
for (const QString &n : names)
out() << n << "\n";
return 0;
}
QString error;
Agent *agent = nullptr;
if (parser.isSet(fileOpt)) {
agent = agentFactory->createFromFile(parser.value(fileOpt), &app, &error);
} else if (parser.isSet(agentOpt)) {
agent = agentFactory->create(parser.value(agentOpt), &app, &error);
} else {
err() << "Specify an agent with --agent <name> or --file <path>, or use --list.\n";
return 2;
}
if (!agent) {
err() << "Failed to create agent: " << error << "\n";
return 1;
}
const bool fimMode = parser.isSet(fimOpt);
Session *session = new Session(agent, &app);
if (!session->isValid()) {
err() << "Failed to create session: " << session->invalidReason() << "\n";
return 1;
}
{
bool ok = false;
const int timeoutSecs = parser.isSet(timeoutOpt)
? parser.value(timeoutOpt).toInt(&ok)
: 60;
if (parser.isSet(timeoutOpt) && !ok) {
err() << "Invalid --timeout value.\n";
return 2;
}
if (timeoutSecs > 0)
if (auto *client = session->client())
client->setTransferTimeout(timeoutSecs * 1000);
}
{
QHash<QString, QString> envFile;
QString envPath = parser.value(envOpt);
if (envPath.isEmpty() && QFile::exists(QStringLiteral(".env")))
envPath = QStringLiteral(".env");
if (!envPath.isEmpty()) {
QString envErr;
envFile = parseEnvFile(envPath, &envErr);
if (!envErr.isEmpty())
err() << "[env] " << envErr << "\n";
}
QString key = parser.value(apiKeyOpt);
if (key.isEmpty()) {
const AgentConfig &cfg = agent->config();
const Providers::ProviderInstance *inst
= instances->instanceByName(cfg.providerInstance);
if (inst)
key = resolveApiKey(envFile, inst->clientApi, inst->apiKeyRef);
}
if (!key.isEmpty() && agent->provider())
agent->provider()->setApiKey(key);
}
{
Templates::ContextRenderer::Bindings bindings;
bindings.projectDir = parser.isSet(projectDirOpt)
? QDir(parser.value(projectDirOpt)).absolutePath()
: QDir::currentPath();
bindings.homeDir = QDir::homePath();
session->setContextBindings(bindings);
}
const QStringList imagePaths = parser.values(imageOpt);
QStringList turns = parser.values(promptOpt);
if (turns.isEmpty()) {
QString prompt = parser.positionalArguments().join(QLatin1Char(' '));
if (prompt.isEmpty() && imagePaths.isEmpty())
prompt = readStdin().trimmed();
if (!prompt.isEmpty())
turns << prompt;
}
if (turns.isEmpty() && imagePaths.isEmpty()) {
err() << "Empty prompt.\n";
return 2;
}
if (fimMode && turns.size() > 1) {
err() << "FIM mode takes a single prompt; extra turns ignored.\n";
turns = {turns.first()};
}
if (!imagePaths.isEmpty() && !session->supportsImages())
err() << "[warning] agent's provider does not advertise image support.\n";
std::optional<bool> toolsOverride;
if (parser.isSet(builtinToolsOpt) || parser.isSet(mcpOpt))
toolsOverride = true;
if (parser.isSet(builtinToolsOpt)) {
auto *tools = session->client()->tools();
tools->addTool(new BenchEchoTool(tools));
tools->addTool(new BenchAddTool(tools));
err() << "[tools] registered bench_echo, bench_add\n";
}
const bool showThinking = !parser.isSet(noThinkingOpt);
int exitCode = 0;
int nextTurn = 0;
std::function<void()> sendNextTurn;
QObject::connect(
session, &Session::event, &app, [showThinking](const ResponseEvent &ev) {
printEvent(ev, showThinking);
});
QObject::connect(
session, &Session::finished, &app,
[&](const LLMQore::RequestID &, const QString &reason) {
err() << "\n[done] stopReason=" << (reason.isEmpty() ? "<none>" : reason) << "\n";
if (!fimMode && nextTurn < turns.size()) {
sendNextTurn();
return;
}
QCoreApplication::quit();
});
QObject::connect(
session, &Session::failed, &app,
[&](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &info) {
err() << "\n[failed] " << info.message << "\n";
exitCode = 1;
QCoreApplication::quit();
});
QObject::connect(
session, &Session::cancelled, &app, [&](const LLMQore::RequestID &) {
err() << "\n[cancelled]\n";
QCoreApplication::quit();
});
sendNextTurn = [&] {
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
if (nextTurn == 0) {
for (const QString &imgPath : imagePaths) {
QFile img(imgPath);
if (!img.open(QIODevice::ReadOnly)) {
err() << "[image] cannot open: " << imgPath << "\n";
exitCode = 1;
QCoreApplication::quit();
return;
}
const QString media = imageMediaType(imgPath);
if (media.isEmpty()) {
err() << "[image] unsupported type: " << imgPath << "\n";
exitCode = 1;
QCoreApplication::quit();
return;
}
const QString b64 = QString::fromLatin1(img.readAll().toBase64());
blocks.push_back(std::make_unique<LLMQore::ImageContent>(
b64, media, LLMQore::ImageContent::ImageSourceType::Base64));
}
}
const QString text = turns.value(nextTurn);
if (!text.isEmpty())
blocks.push_back(std::make_unique<LLMQore::TextContent>(text));
if (blocks.empty()) {
err() << "Nothing to send.\n";
exitCode = 1;
QCoreApplication::quit();
return;
}
if (turns.size() > 1)
err() << "\n[turn " << (nextTurn + 1) << "/" << turns.size() << "] " << text << "\n";
++nextTurn;
if (session->send(std::move(blocks), toolsOverride).isEmpty()) {
err() << "Failed to dispatch request: " << session->lastError().message << "\n";
exitCode = 1;
QCoreApplication::quit();
}
};
auto dispatch = [&] {
if (fimMode) {
const QString prefix = turns.value(0);
const QString suffix = parser.isSet(suffixOpt) ? parser.value(suffixOpt) : QString();
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<QodeAssist::CompletionContent>(prefix, suffix));
if (session->send(std::move(blocks), /*toolsOverride=*/false).isEmpty()) {
err() << "Failed to dispatch FIM request: " << session->lastError().message << "\n";
exitCode = 1;
QCoreApplication::quit();
}
return;
}
sendNextTurn();
};
if (parser.isSet(mcpOpt)) {
const QString mcpPath = parser.value(mcpOpt);
QFile mcpFile(mcpPath);
if (!mcpFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
err() << "[mcp] cannot open config: " << mcpPath << "\n";
return 2;
}
QJsonParseError jerr;
const QJsonDocument mcpDoc = QJsonDocument::fromJson(mcpFile.readAll(), &jerr);
if (jerr.error != QJsonParseError::NoError || !mcpDoc.isObject()) {
err() << "[mcp] invalid JSON config: " << jerr.errorString() << "\n";
return 2;
}
auto *client = session->client();
if (!client) {
err() << "[mcp] session has no client.\n";
return 1;
}
auto *tools = client->tools();
tools->loadMcpServers(mcpDoc.object());
err() << "[mcp] loading servers, waiting for tools...\n";
auto dispatched = std::make_shared<bool>(false);
auto fire = [&, dispatched] {
if (*dispatched)
return;
*dispatched = true;
const int n = tools->getToolsDefinitions().size();
err() << "[mcp] " << n << " tool(s) available.\n";
dispatch();
};
QObject::connect(tools, &LLMQore::ToolRegistry::toolsChanged, &app, [&, fire] {
if (!tools->getToolsDefinitions().isEmpty())
fire();
});
QTimer::singleShot(15000, &app, fire);
} else {
QTimer::singleShot(0, &app, dispatch);
}
app.exec();
return exitCode;
}

6
bench/run-bench.sh.in Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Generated by CMake. Runs bench with a single Qt copy (Qt Creator's bundled
# frameworks) to avoid duplicate-Qt objc warnings.
DIR="$(cd "$(dirname "$0")" && pwd)"
export DYLD_FRAMEWORK_PATH="@QTC_FRAMEWORKS_DIR@${DYLD_FRAMEWORK_PATH:+:$DYLD_FRAMEWORK_PATH}"
exec "$DIR/bench" "$@"

View File

@@ -2,6 +2,8 @@ add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp DocumentContextReader.hpp DocumentContextReader.cpp
ChangesManager.h ChangesManager.cpp ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp ContextManager.hpp ContextManager.cpp
IProjectScanner.hpp
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
ContentFile.hpp ContentFile.hpp
DocumentReaderQtCreator.hpp DocumentReaderQtCreator.hpp
IDocumentReader.hpp IDocumentReader.hpp
@@ -21,7 +23,7 @@ target_link_libraries(Context
QtCreator::Utils QtCreator::Utils
QtCreator::ProjectExplorer QtCreator::ProjectExplorer
PRIVATE PRIVATE
PluginLLMCore Common
QodeAssistSettings QodeAssistSettings
) )

View File

@@ -6,25 +6,24 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonObject>
#include <QTextStream> #include <QTextStream>
#include "settings/GeneralSettings.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectScannerQtCreator.hpp"
namespace QodeAssist::Context { namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent) ContextManager::ContextManager(QObject *parent)
: QObject(parent) : ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
, m_ignoreManager(new IgnoreManager(this))
{} {}
ContextManager::ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent)
: QObject(parent)
, m_scanner(std::move(scanner))
{}
ContextManager::~ContextManager() = default;
QString ContextManager::readFile(const QString &filePath) const QString ContextManager::readFile(const QString &filePath) const
{ {
QFile file(filePath); QFile file(filePath);
@@ -37,7 +36,7 @@ QString ContextManager::readFile(const QString &filePath) const
QTextStream in(&file); QTextStream in(&file);
QString content = in.readAll(); QString content = in.readAll();
file.close(); file.close();
return content; return content;
} }
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
{ {
QList<ContentFile> files; QList<ContentFile> files;
for (const QString &path : filePaths) { for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile( if (m_scanner->shouldIgnore(path)) {
Utils::FilePath::fromString(path));
if (project && m_ignoreManager->shouldIgnore(path, project)) {
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path)); LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
continue; continue;
} }
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
return files; return files;
} }
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
{
QStringList sourceFiles;
if (!project)
return sourceFiles;
auto projectNode = project->rootProjectNode();
if (!projectNode)
return sourceFiles;
projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
sourceFiles.append(fileNode->filePath().toUrlishString());
}
},
nullptr);
return sourceFiles;
}
ContentFile ContextManager::createContentFile(const QString &filePath) const ContentFile ContextManager::createContentFile(const QString &filePath) const
{ {
ContentFile contentFile; ContentFile contentFile;
@@ -100,77 +76,26 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{ {
const auto &generalSettings = Settings::generalSettings(); Q_UNUSED(documentInfo)
return false;
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
} }
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const QString ContextManager::openedFilesContext(const QStringList &excludeFiles) const
{
auto documents = Core::DocumentModel::openedDocuments();
QList<QPair<QString, QString>> files;
for (const auto *document : std::as_const(documents)) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
if (!excludeFiles.contains(filePath)) {
files.append({filePath, textDocument->plainText()});
}
}
return files;
}
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
{ {
QString context = "User files context:\n"; QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments(); for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
context += QString("File: %1\n").arg(file.filePath);
for (const auto *document : documents) { context += file.content;
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
context += "\n"; context += "\n";
} }
return context; return context;
} }
IgnoreManager *ContextManager::ignoreManager() const bool ContextManager::shouldIgnore(const QString &filePath) const
{ {
return m_ignoreManager; return m_scanner->shouldIgnore(filePath);
} }
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -4,18 +4,16 @@
#pragma once #pragma once
#include <memory>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include "ContentFile.hpp" #include "ContentFile.hpp"
#include "IContextManager.hpp" #include "IContextManager.hpp"
#include "IgnoreManager.hpp" #include "IProjectScanner.hpp"
#include "ProgrammingLanguage.hpp" #include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager : public QObject, public IContextManager class ContextManager : public QObject, public IContextManager
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
public: public:
explicit ContextManager(QObject *parent = nullptr); explicit ContextManager(QObject *parent = nullptr);
~ContextManager() override = default; ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent = nullptr);
~ContextManager() override;
QString readFile(const QString &filePath) const override; QString readFile(const QString &filePath) const override;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override; QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override; ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override; ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override; bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const; QString openedFilesContext(const QStringList &excludeFiles = QStringList{}) const;
bool shouldIgnore(const QString &filePath) const;
private: private:
IgnoreManager *m_ignoreManager; std::unique_ptr<IProjectScanner> m_scanner;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -254,7 +254,7 @@ CopyrightInfo DocumentContextReader::copyrightInfo() const
return m_copyrightInfo; return m_copyrightInfo;
} }
PluginLLMCore::ContextData DocumentContextReader::prepareContext( Templates::ContextData DocumentContextReader::prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
{ {
QString contextBefore; QString contextBefore;

View File

@@ -7,7 +7,7 @@
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <QTextDocument> #include <QTextDocument>
#include <pluginllmcore/ContextData.hpp> #include <sources/common/ContextData.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
namespace QodeAssist::Context { namespace QodeAssist::Context {
@@ -58,7 +58,7 @@ public:
CopyrightInfo copyrightInfo() const; CopyrightInfo copyrightInfo() const;
PluginLLMCore::ContextData prepareContext( Templates::ContextData prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const; int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
private: private:

View File

@@ -11,10 +11,6 @@
#include "IDocumentReader.hpp" #include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp" #include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class IContextManager class IContextManager
@@ -24,7 +20,6 @@ public:
virtual QString readFile(const QString &filePath) const = 0; virtual QString readFile(const QString &filePath) const = 0;
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0; virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
virtual ContentFile createContentFile(const QString &filePath) const = 0; virtual ContentFile createContentFile(const QString &filePath) const = 0;
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0; virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;

View File

@@ -0,0 +1,28 @@
// 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 <QList>
#include <QString>
#include <QStringList>
namespace QodeAssist::Context {
struct OpenedTextFile
{
QString filePath;
QString content;
};
class IProjectScanner
{
public:
virtual ~IProjectScanner() = default;
virtual QList<OpenedTextFile> openedTextFiles(const QStringList &excludeFiles = {}) const = 0;
virtual bool shouldIgnore(const QString &filePath) const = 0;
};
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,53 @@
// 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 "ProjectScannerQtCreator.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h>
#include <utils/filepath.h>
#include "IgnoreManager.hpp"
namespace QodeAssist::Context {
ProjectScannerQtCreator::ProjectScannerQtCreator()
: m_ignoreManager(std::make_unique<IgnoreManager>())
{}
ProjectScannerQtCreator::~ProjectScannerQtCreator() = default;
QList<OpenedTextFile> ProjectScannerQtCreator::openedTextFiles(
const QStringList &excludeFiles) const
{
QList<OpenedTextFile> files;
const auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
const auto *textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
const QString filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
if (shouldIgnore(filePath))
continue;
files.append({filePath, textDocument->plainText()});
}
return files;
}
bool ProjectScannerQtCreator::shouldIgnore(const QString &filePath) const
{
auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath));
return project && m_ignoreManager->shouldIgnore(filePath, project);
}
} // namespace QodeAssist::Context

View File

@@ -0,0 +1,28 @@
// 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 "IProjectScanner.hpp"
namespace QodeAssist::Context {
class IgnoreManager;
class ProjectScannerQtCreator : public IProjectScanner
{
public:
ProjectScannerQtCreator();
~ProjectScannerQtCreator() override;
QList<OpenedTextFile> openedTextFiles(const QStringList &excludeFiles = {}) const override;
bool shouldIgnore(const QString &filePath) const override;
private:
std::unique_ptr<IgnoreManager> m_ignoreManager;
};
} // namespace QodeAssist::Context

View File

@@ -168,7 +168,6 @@ This allows roles to augment rather than replace your base configuration.
## Related Documentation ## Related Documentation
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality - [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
- [File Context](file-context.md) - Attaching files to chat context - [File Context](file-context.md) - Attaching files to chat context

View File

@@ -0,0 +1,395 @@
# Agent Templates — Design Note (body model, include, extends)
Status: agreed design / ready to implement. Dev-facing (not end-user docs).
Scope: how agent TOML profiles describe the request and share structure.
## Problem this replaces
The shipped model has each agent embed a `[template].message_format` jinja string
that hand-builds the **whole** request body as text, plus `[template.sampling]` and
`[template.thinking.*]` blocks merged in by `applySampling`. Pains:
- Massive copy-paste: 9 OpenAI-compatible agents share a byte-identical ~50-line
`message_format`; 4 Claude agents share another; `role` + README `context` are
identical across 18 files.
- `[template.sampling]` / `[template.thinking.overrides]` /
`[template.thinking.request_block.*]` describe **merge machinery**, not the request
body — they don't look like the actual API call. The `overrides` vs `request_block`
split is meaningless (both are deep-merged into the request identically).
- Manual JSON-by-string-concatenation: trailing-comma bookkeeping
(`{% if not loop.is_last %},{% endif %}`) everywhere; a missing comma fails
silently at runtime (`renderBody` returns nullopt, only a `qWarning`).
- `include` is hard-disabled, so there is no way to share a sub-fragment.
## Agreed model
### 1. `[body]` is a deep-mergeable table = the request body, 1:1 with the API
Replace the `message_format` string and the `sampling`/`thinking` blocks with a
single `[body]` TOML table whose keys are the **literal request-body fields**.
Because it is a table (not a string), `extends` / `deepMerge` can override it
field-by-field — variants become a 2-line delta instead of a copied body.
Field-value rules at build time (per key in `[body]`, applied recursively):
- **string containing jinja** (`{{` or `{%`) → render through inja, splice the
output as **raw JSON** (array / object / string). Empty render → key omitted.
- **string without jinja** (e.g. `"high"`) → literal JSON string, as-is.
- **number / bool / inline-table** → as-is.
So `messages` / `contents` and `system` / `system_instruction` are just **string
fields holding jinja**; everything else (`max_tokens`, `temperature`, `stream`,
`thinking`, `output_config`, `generationConfig`, …) is a literal value that reads
exactly like the curl body.
No runtime toggles: thinking / tools / streaming are **fixed per agent**. A thinking
agent literally carries the `thinking` fields; a non-thinking variant is a separate
file. There is no `{% if thinking %}` in the body. `system` uses
`{% if existsIn(ctx, "system_prompt") %}` only because that is about *presence of
data*, not a mode toggle. `enable_thinking` / `enable_tools` are **capability hints**
(used for UI badges and to decide tool-definition injection) — the body is the source
of truth for what is actually sent, so a thinking agent's body must carry the thinking
fields regardless of the flag.
Outside the body:
- `model` — the TOML `model` is the **default**; a per-agent override chosen in
QodeAssist settings wins. Overrides are stored in `agent_models.json`
(agentName → model) and applied by `AgentFactory` when it builds the agent
(`AgentFactory::effectiveModel`/`setModelOverride`); `Session` still seeds the
payload `model` from the resolved `cfg.model`. URL-model providers (Google) put a
`${MODEL}` placeholder in `endpoint`; `Session` substitutes the resolved model into
the endpoint before sending (same substitution style as `${PROJECT_DIR}`/`${HOME}`),
so the override drives the URL too.
- `tools` — injected by the **provider** when `enable_tools` is set (tool
definitions are dynamic, from `ToolsManager`; they can't be authored in TOML).
- `stream` — always on. Literal `"stream": true` in the body for OpenAI / Claude /
Mistral / Responses / Ollama; encoded in the `endpoint` URL for Google.
### 2. `include` re-enabled as whitelisted partials
The message-array rendering (the complex, comma-heavy part) lives in
`sources/agents/partials/*.jinja`, shared via `{% include %}`. The throwing include
callback is replaced by a sandboxed resolver that:
- rejects names containing `..`, a leading `/`, or a scheme/drive;
- resolves only against known roots: bundled `:/agents/partials/` then the user
`partials/` dir;
- parses/caches the partial in the same `inja::Environment`.
A missing/typo'd partial is a **load-time** error.
### 3. `extends` shares config down a hierarchy
`extends` already exists (`resolveExtends` + `deepMerge` + `abstract`/`hidden`); it
keeps doing what it does, now over the structured `[body]` too. Each API-shape base
sets `system_prompt = """{{ agent_role() }}"""` (the role text comes from the role
JSON via the `agent_role` callback; see below). No shared root base. Between the
API-shape base and the concrete agents sits one thin abstract base **per provider**
(provider_instance + endpoint only) — the designated extension point for user
agents, so a custom agent is `extends` + `name` + `model`:
```
openai_base (abstract) → system_prompt + [body] (API shape)
├─ mistral_base (abstract) → provider, endpoint (per-provider)
│ ├─ mistral_chat → name, model
│ └─ mistral_reasoning → name, model + enable_thinking
├─ openrouter_base (abstract) ...
└─ openai_chat → name, model (own provider = no mid layer)
anthropic_base (abstract) → system_prompt + provider/endpoint + [body]
└─ claude_sonnet46 → name, model + [body] thinking / output_config
google_base (abstract) → system_prompt + provider + [body]
└─ gemini_chat → endpoint (${MODEL}) + [body.generationConfig] thinkingConfig
```
Bundled agents are read-only: the loader rejects a user file that reuses a bundled
`name`. Customisation = a user agent under a new name extending a bundled base (or a
concrete bundled agent); the per-agent model override in settings covers the
model-only case without any file.
Notes:
- `[body]` is shared whole when identical (the 8 OpenAI-compatible providers); a
variant overrides only the differing field — no duplicated body.
- Arrays (`tags`) are **replaced** on override, not appended (`deepMerge` recurses
objects only). A child that wants base tags + extras restates the full list.
- Division of labour: **include** shares the message-rendering fragment across
unrelated families; **extends** shares config (system_prompt / endpoint / body)
down one inheritance chain.
- With `model` gone, per-model files collapse: agents that previously differed only
by `model` become one agent (the client picks the model). A separate file is only
needed when the body genuinely differs (effort, no-thinking, …).
### System prompt — a composable template with building blocks
The old `role` (static text) and `context` (jinja) layers collapse into one
`agent.system` layer in `Session`, rendered through `ContextRenderer`. The agent's
`system_prompt` field IS that template, and the user edits it (in the profile) to
compose the prompt from building-block callbacks:
- `{{ agent_role("<id>") }}` — insert a role's text (Developer/Reviewer/Researcher…).
Implemented as a `ContextRenderer` callback (`registerAgentRole`) that reads
`userResourcePath("qodeassist/agent_roles/<id>.json")["systemPrompt"]`. Returns "" if
missing. Lives in `sources/agents` (no dependency on `settings/`), so it works in the
plugin and bench. The role text lives once in the role JSON (managed by the settings
Roles UI); the chat bases just carry `system_prompt = """{{ agent_role("developer") }}"""`.
- `{{ read_file("...") }}` / `file_exists` / `${PROJECT_DIR}` / `${HOME}` — existing
`ContextRenderer` helpers, composable in the same template.
So a profile can do `system_prompt = """{{ agent_role("developer") }}\n\n{{ read_file("…") }}"""`.
`qodeassist.cpp` calls `AgentRolesManager::ensureDefaultRoles()` at startup so the default
role JSONs exist before agents load. There is NO per-agent settings override — the edit
point is the profile's `system_prompt`. Code-completion/FIM agents set no `system_prompt`.
## Worked examples
OpenAI base:
```toml
abstract = true
system_prompt = """{{ agent_role("developer") }}"""
provider_instance = "OpenAI (Chat Completions)"
endpoint = "/chat/completions"
enable_tools = true
[body]
max_tokens = 8192
temperature = 0.7
stream = true
messages = """
[ {% include "partials/openai_messages.jinja" %} ]
"""
```
Mistral reasoning child (delta only):
```toml
extends = "OpenAI Base Chat"
name = "Mistral Reasoning Chat"
provider_instance = "Mistral AI"
endpoint = "/v1/chat/completions"
enable_thinking = true
[body]
reasoning_effort = "medium"
```
Claude base (literally the curl body):
```toml
abstract = true
system_prompt = """{{ agent_role("developer") }}"""
provider_instance = "Claude"
endpoint = "/v1/messages"
enable_thinking = true
enable_tools = true
[body]
max_tokens = 16000
temperature = 1
stream = true
thinking = { type = "adaptive", display = "summarized" }
output_config = { effort = "high" }
system = """{% if existsIn(ctx, "system_prompt") %}{{ tojson(ctx.system_prompt) }}{% endif %}"""
messages = """
[ {% include "partials/anthropic_messages.jinja" %} ]
"""
```
Sonnet child (delta only):
```toml
extends = "Anthropic Base Chat"
name = "Claude Sonnet"
[body.output_config]
effort = "medium"
```
Google base (`${MODEL}` in endpoint; streaming in the URL):
```toml
abstract = true
system_prompt = """{{ agent_role("developer") }}"""
provider_instance = "Google AI"
endpoint = "/models/${MODEL}:streamGenerateContent?alt=sse"
enable_thinking = true
enable_tools = true
[body]
system_instruction = """{% if existsIn(ctx, "system_prompt") %}{ "parts": [ { "text": {{ tojson(ctx.system_prompt) }} } ] }{% endif %}"""
contents = """
[ {% include "partials/google_contents.jinja" %} ]
"""
[body.generationConfig]
maxOutputTokens = 16000
temperature = 1
thinkingConfig = { includeThoughts = true, thinkingBudget = 8192 }
```
### Partials
`partials/openai_messages.jinja` dispatches per message:
```jinja
{% if existsIn(ctx, "system_prompt") %}
{ "role": "system", "content": {{ tojson(ctx.system_prompt) }} },
{% endif %}
{% for msg in ctx.history %}
{% if msg.role == "assistant" %}{% include "partials/openai_assistant.jinja" %}
{% else if length(filter_by_type(msg.content_blocks, "tool_result")) > 0 %}{% include "partials/openai_tool_results.jinja" %}
{% else %}{% include "partials/openai_user.jinja" %}
{% endif %}
{% endfor %}
```
`partials/openai_assistant.jinja`:
```jinja
{% set tcalls = filter_by_type(msg.content_blocks, "tool_use") %}
{
"role": "assistant",
"content": {{ tojson(msg.content) }}
{% if length(tcalls) > 0 %}
, "tool_calls": [
{% for b in tcalls %}
{ "id": {{ tojson(b.id) }}, "type": "function",
"function": { "name": {{ tojson(b.name) }}, "arguments": {{ tojson(tojson(b.input)) }} } },
{% endfor %}
]
{% endif %}
},
```
`partials/openai_tool_results.jinja`:
```jinja
{% for b in filter_by_type(msg.content_blocks, "tool_result") %}
{ "role": "tool", "tool_call_id": {{ tojson(b.tool_use_id) }}, "content": {{ tojson(b.content) }} },
{% endfor %}
```
`partials/openai_user.jinja`:
```jinja
{% if existsIn(msg, "images") %}
{ "role": "user", "content": {% include "partials/openai_image_content.jinja" %} },
{% else %}
{ "role": "user", "content": {{ tojson(msg.content) }} },
{% endif %}
```
`partials/openai_image_content.jinja`:
```jinja
[
{ "type": "text", "text": {{ tojson(msg.content) }} }
{% for img in msg.images %}
,
{% if img.is_url %}
{ "type": "image_url", "image_url": { "url": {{ tojson(img.data) }} } }
{% else %}
{ "type": "image_url", "image_url": { "url": "data:{{ img.media_type }};base64,{{ img.data }}" } }
{% endif %}
{% endfor %}
]
```
`partials/anthropic_messages.jinja`:
```jinja
{% for msg in ctx.history %}
{
"role": {{ tojson(msg.role) }},
"content": [
{% for b in msg.content_blocks %}
{% if b.type == "image" %}{% include "partials/anthropic_image.jinja" %}
{% else %}{{ tojson(b) }},
{% endif %}
{% endfor %}
]
},
{% endfor %}
```
`partials/anthropic_image.jinja`:
```jinja
{
"type": "image",
"source":
{% if b.is_url %}
{ "type": "url", "url": {{ tojson(b.data) }} }
{% else %}
{ "type": "base64", "media_type": {{ tojson(b.media_type) }}, "data": {{ tojson(b.data) }} }
{% endif %}
},
```
`partials/google_contents.jinja`:
```jinja
{% for msg in ctx.history %}
{
"role": {% if msg.role == "assistant" %}"model"{% else %}"user"{% endif %},
"parts": [ {% for b in msg.content_blocks %}{% include "partials/google_part.jinja" %}{% endfor %} ]
},
{% endfor %}
```
`partials/google_part.jinja`:
```jinja
{% if b.type == "text" %}
{ "text": {{ tojson(b.text) }} },
{% else if b.type == "thinking" %}
{ "text": {{ tojson(b.thinking) }}, "thought": true, "thoughtSignature": {{ tojson(b.signature) }} },
{% else if b.type == "tool_use" %}
{ "functionCall": { "name": {{ tojson(b.name) }}, "args": {{ tojson(b.input) }} } },
{% else if b.type == "tool_result" %}
{ "functionResponse": { "name": {{ tojson(b.name) }}, "response": { "result": {{ tojson(b.content) }} } } },
{% else if b.type == "image" %}
{% if b.is_url %}
{ "file_data": { "mime_type": {{ tojson(b.media_type) }}, "file_uri": {{ tojson(b.data) }} } },
{% else %}
{ "inline_data": { "mime_type": {{ tojson(b.media_type) }}, "data": {{ tojson(b.data) }} } },
{% endif %}
{% else %}
{ "text": "" },
{% endif %}
```
## C++ work
In `JsonPromptTemplate`:
- Parse `[body]` as a `QJsonObject` (not a string). Walk it recursively and build the
request: render jinja-bearing string values via inja and splice the parsed JSON;
pass literal strings / scalars / inline-tables through; drop keys whose render is
empty.
- **Delete** `m_sampling`, `m_thinking`, and `applySampling` entirely — the body is
the request; there is no separate sampling/thinking merge.
- Drop the `thinkingEnabled` parameter from `buildFullRequest` /
`Provider::prepareRequest` / `Session` — it no longer affects rendering.
- Add a **JSON-aware** trailing-comma stripper before `QJsonDocument::fromJson`
(tracks string/escape state so `,}` / `,]` inside string values are not touched).
This is what lets partials emit an unconditional `,` after every element and drop
all `loop.is_last` bookkeeping.
In `AgentConfig` / `AgentLoader`:
- Replace `messageFormat` (string) with `body` (`QJsonObject`); merge `role` +
`context` into `system_prompt`. `[template].sampling` / `[template].thinking` are
removed.
- `extends` / `deepMerge` are unchanged; they now also merge `[body]`.
- Validate at load: a referenced partial must resolve; the assembled body must parse
as JSON (render once against a synthetic context with tool_use / tool_result /
image). Catches breakage at startup, not mid-conversation.
Model selection (per-agent override):
- `AgentFactory` owns an agentName → model map loaded from `agent_models.json`
(`loadModelOverrides`/`saveModelOverrides`). `create()`/`createFromFile()` apply the
override into the built `AgentConfig`; `effectiveModel()` exposes the resolved value;
`setModelOverride()` persists. The settings UI (`AgentDetailPane`) edits it via an
editable Model field; list/roster widgets display `effectiveModel`.
- `Session` substitutes `${MODEL}` in `cfg.endpoint` with the resolved model before
`sendRequest` (covers Google, whose model lives in the URL), and still seeds the
payload `model` from `cfg.model`. The provider keeps injecting `tools` when
`enable_tools` is set.
In `Session`:
- Collapse the `agent.role` + `agent.context` system-prompt layers into one rendered
`system_prompt` layer.
## Implementation order
1. JSON-aware trailing-comma stripper + whitelisted `include` resolver (enables
readable partials).
2. `[body]`-table model in `JsonPromptTemplate` + loader; delete
sampling/thinking/`applySampling`; drop `thinkingEnabled`.
3. `system_prompt` merge in loader + `Session`.
4. Per-agent model override in `AgentFactory` (`agent_models.json`) + `${MODEL}`
endpoint substitution in `Session`; editable Model field in settings; convert
bundled agents to the base/partials/`extends` layout.
5. Load-time validation (partial resolves, body parses).

317
docs/architecture.md Normal file
View File

@@ -0,0 +1,317 @@
# QodeAssist Architecture
This document describes the **current** runtime architecture, after the §10
rework in `target-architecture.md` was completed. Every runtime LLM path —
code completion, chat (send/stream + compression + token counting), and quick
refactor — flows through one stack: agents, `Session`, and the
`Providers::GenericProvider` layer. There is no legacy parallel path; the old
"Stack A" (root `providers/*`, `pluginllmcore/*`, `ConfigurationManager`, the
provider/model/template settings pages) has been removed.
For the design rationale, layering contract, and cross-cutting policies, see
[`target-architecture.md`](target-architecture.md). This file documents how the
code is wired today.
---
## 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
• Providers::registerBuiltinProviders() — client_api → provider table
• ProviderInstanceFactory — provider instances from TOML
• ProviderSecretsStore — secrets behind a port
• AgentFactory — agents from TOML + agent_models.json
• SessionManager(agentFactory) — owns the ToolContributorRegistry
toolContributors().add(registerQodeAssistTools)
toolContributors().add(registerSkillTool)
toolContributors().add(McpClientsManager::registerToolsOn)
• m_engine (QQmlEngine)
rootContext: "agentFactory", "sessionManager" — DI for chat (QML)
Wired into consumers:
• QodeAssistClient ← LLMClientInterface(generalSettings, completeSettings,
agentFactory, sessionManager, documentReader,
performanceLogger)
← setSessionManager / setAgentFactory (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. Core (agent / Session)
```
AgentFactory.create(name)
configByName(name) → AgentConfig (TOML, [body] table; model override from
agent_models.json applied here)
buildProviderForAgent:
instance = ProviderInstanceFactory.instanceByName(cfg.providerInstance)
provider = ProviderFactory::create(instance.clientApi)
provider.setUrl(instance.url)
provider.setApiKey(secrets.read(instance.apiKeyRef))
Agent(config, provider)
promptTemplate = JsonPromptTemplate::fromConfig(cfg) — compiles [body] (inja),
validated at load against a synthetic context
provider.setPromptCaching(cfg.cachePrompt, cfg.cacheTtl == "1h")
SessionManager — two ways to obtain a Session:
• createSession(agentName, externalHistory?) — chat: attaches a persistent,
externally-owned history
• acquire(agentName) / release(session) — one-shot pipelines: a small
per-agent pool of internal-history
sessions; acquire hands out a
session with cleared history,
cleared system-prompt layers and
cleared client tools
Session(agent[, externalHistory])
├─ ConversationHistory — messages as polymorphic ContentBlocks
├─ SystemPromptBuilder — ordered named layers (priority-sorted)
└─ ResponseRouter(client) — adapts client signals → typed ResponseEvent
Session API:
• send(blocks, toolsOverride) — the ONLY dispatch entry point: append a user
message and dispatch. Completion/chat/refactor
differ only in block content + template.
• cancel() — tears down in-flight; emits cancelled(id)
• history() / systemPrompt() / client() / supportsImages()
• setContentLoader(loader) — resolves Stored* attachment/image blocks
• lastError() → ErrorInfo — typed synchronous start-failure detail
Session signals (three-state, mutually exclusive per request):
• finished(id, stopReason)
• failed(id, ErrorInfo{category, message, providerDetail})
• cancelled(id)
+ event(ResponseEvent) — live delta stream for the chat UI
```
`Session::dispatch` renders the agent's `system_prompt` into the `agent.system`
layer, composes all `SystemPromptBuilder` layers into the request system prompt,
and substitutes `${MODEL}` in the endpoint before sending.
---
## 3. Provider layer
One configuration-driven `GenericProvider` covers every API; it varies only by
the LLMQore client factory and metadata. Request *shape* belongs to the agent's
`JsonPromptTemplate` (the `[body]` table), never to the provider.
```
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 → PromptTemplate::buildFullRequest; injects tools when
enable_tools; applies ClaudeCacheControl when prompt caching is on
• 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 |
---
## 4. Configuration model
```
~/.config/.../qodeassist/config/
providers/*.toml → ProviderInstance { name, client_api, url, api_key_ref }
agents/*.toml → AgentConfig { schema_version, providerInstance, model,
endpoint, system_prompt, [body], match,
enable_tools, enable_thinking, cache_prompt,
extends, abstract, hidden, tags }
agent_models.json → per-agent model override (applied by AgentFactory)
agent_roles/*.json → role text, pulled into system_prompt via {{ agent_role(id) }}
pipelines rosters → codeCompletion / chatAssistant / chatCompression / quickRefactor
consumed by AgentRouter.pickAgent(roster, {filePath, projectName})
Editor policy (NOT agent config):
CodeCompletionSettings — triggers, modelOutputHandler, context extraction,
useOpenFilesContext
```
`[body]` **is** the request body (deep-mergeable through `extends`; Jinja-bearing
string values render and splice as raw JSON, literals pass through, empty renders
drop the key). `include` resolves only sandboxed partial roots. Profiles validate
at load: a referenced partial must resolve and the assembled body must parse as
JSON against a synthetic context — config errors surface in the agents settings
page, never as a silent runtime drop. The loader also lints: unknown top-level /
`[match]` keys and same-layer duplicate names are warnings; a user file that
reuses a bundled agent's name is rejected (bundled agents cannot be replaced —
users extend them, or the per-provider abstract bases, under a new name);
`abstract` and `hidden` are never inherited through `extends`. Full spec:
[`agent-templates-design.md`](agent-templates-design.md); user-facing guide:
[`creating-agents.md`](creating-agents.md).
---
## 5. Runtime paths
`AgentRouter.pickAgent(roster, {file, project})` is the only agent picker; every
pipeline resolves its agent through a roster.
### 5a. Code completion
```
Qt Creator LSP (getCompletionsCycling)
LLMClientInterface
agent = AgentRouter.pickAgent(roster.codeCompletion, {file, project})
session = sessionManager.acquire(agent) — pooled
systemPrompt layer "completion.context" = fileContext + open-files context
session.send( blocks{ CompletionContent(prefix, suffix) }, tools=off )
▼ on Session::finished:
history().lastAssistantText() → CodeHandler (output-mode) → LSP items
→ sessionManager.release(session)
```
The completion context travels as a `CompletionContent` block; the template
exposes it as `ctx.prefix` / `ctx.suffix`. FIM vs instruct is purely agent
config (the body), not feature code. Completion never touches the delta stream —
it waits for `finished` and reads the last message.
### 5b. Chat
`ChatRootView` owns one persistent `ConversationHistory` for the whole chat view
and injects it into every collaborator. **History is the single source of truth.**
```
ChatRootView (QML) — owns ConversationHistory m_history
ChatModel.setHistory(m_history) — ChatModel is a PROJECTION:
subscribes to messageAdded/Updated/cleared/reset, flattens blocks→rows,
overlays file-edit status from ChangesManager, holds a per-message usage map
ChatAgentController — agent list filtered to the
chatAssistant roster; active agent persisted
▼ dispatchSend
ClientInterface
session = sessionManager.createSession(activeAgent, m_history)
sessionManager.toolContributors().contribute(client.tools()) — builtin+skills+MCP
session.setContentLoader(ChatSerializer::loadContentFromStorage)
systemPrompt layer "chat.context" = project info + skills + linked files
session.send( blocks{ TextContent + StoredAttachmentContent + StoredImageContent } )
▼ consumes Session signals (NOT raw client signals):
event(Usage) → ChatModel.setMessageUsage + token-counter calibration
finished(id) → ChangesManager.applyPendingEditsForRequest + persist;
removeSession (the persistent history survives)
failed(id, ErrorInfo) → surface error; removeSession
ChatCompressor → acquire(chatCompression-roster agent) → seed history from the
chat's messages → "compression" layer → send → read summary from
the compression session's own history → release
InputTokenCounter → estimates over ConversationHistory (calibrated by Usage events)
ChatSerializer → persists ConversationHistory via MessageSerializer (v0.3);
imports legacy v0.1/v0.2 files
```
`ChatModel`'s QML role surface (roleType / content / attachments / images /
isRedacted / token roles) is unchanged, so the QML delegates were untouched. The
projection's incremental updates avoid model resets on the streaming hot path.
### 5c. Quick refactor
```
QodeAssistClient.requestQuickRefactor → QuickRefactorHandler
agent = AgentRouter.pickAgent(roster.quickRefactor, {file, project})
session = sessionManager.acquire(agent)
if useTools: sessionManager.toolContributors().contribute(client.tools())
systemPrompt layer "refactor" = tagged selection + output + indentation rules
session.send(blocks{instructions}, useTools)
▼ on Session::finished:
history().lastAssistantText() → ResponseCleaner → RefactorResult → editor insert
→ sessionManager.release(session)
on Session::failed(ErrorInfo) → RefactorResult{error}
```
---
## 6. Context layer
The context services sit behind IDE-agnostic ports; Qt Creator API use lives in
the adapters.
```
EditorContext — IDocumentReader (port) ← DocumentReaderQtCreator (TextEditor API)
ProjectContext — IProjectScanner (port) ← ProjectScannerQtCreator (ProjectExplorer
+ Core::DocumentModel + the IgnoreManager for .qodeassistignore)
TokenEstimator — TokenUtils (pure) ← InputTokenCounter (thin UI consumer)
```
`ContextManager` is now Qt-Creator-free: it delegates open-file enumeration and
ignore filtering to an injected `IProjectScanner` (defaulting to the QtC adapter),
and keeps only filesystem reads + formatting. `ContextManager::shouldIgnore(path)`
replaced the previously exposed `ignoreManager()`.
---
## 7. Cross-cutting
- **Request lifecycle** — a session has at most one in-flight request; `send()`
while in flight cancels the previous. Every request ends in exactly one of
`finished` / `failed` / `cancelled`. Cancellation is not an error; no consumer
string-matches a message to tell them apart.
- **Typed errors** — `ErrorInfo { category ∈ {Config, Auth, Network, Provider,
Validation, Tool}, message, providerDetail }`. `ResponseRouter` categorizes wire
errors (best-effort) at the boundary; `Session::failed` carries the typed value.
- **Tools** — `SessionManager` owns a `ToolContributorRegistry`; built-in ToolKit,
the skill tool, and MCP client tools register once and are contributed to chat
and quick-refactor session clients uniformly.
- **Threading** — the core runs on the GUI thread; concurrency is the Qt event
loop plus async network I/O. Blocking work hides behind L3 ports.
---
## 8. Tests
`test/` (GTest + Qt::Test) covers the two engines most affected by the rework:
- `JsonPromptTemplateTest` — the `[body]` engine: jinja render + JSON splice,
literal passthrough, empty-render key drop, nested literals, and load-time
rejection of bodies that render invalid JSON.
- `ResponseRouterTest` — a fake `BaseClient` replays a recorded provider stream;
asserts the assistant message is stamped with the request id, history is built
correctly (thinking + text + tool use/result), the typed event stream is emitted,
and wire errors are categorized.
- `BundledAgentsTest` — loads every bundled agent through the real loader (extends
+ partials resolved from the qrc) and renders each `[body]` against the synthetic
validation context. This is the load-time validation guarantee run in CI: a broken
bundled body, partial, or `extends` chain fails the test instead of surfacing as a
silent runtime drop.
---
## 9. Remaining follow-ups (optional)
1. **Qt-Creator-free core build + CI** — `AgentFactory` / `ContextRenderer` still
call `Core::ICore::userResourcePath`, so the core targets link `QtCreator::Core`.
A `ResourcePaths` port + adapter would let the core build without Qt Creator and
enable a CI job that fails on a layering-violating include. (The bundled-agent
render check already runs in the QtC-linked test binary — see §8.)
2. **§9 target module layout** — the `core/ ide/ features/ hosts/` physical target
split in `target-architecture.md` is not yet reflected in the directory layout.
```

View File

@@ -112,4 +112,3 @@ No additional configuration is required.
- [Agent Roles](agent-roles.md) - Switch between AI personas - [Agent Roles](agent-roles.md) - Switch between AI personas
- [File Context](file-context.md) - Attach files to chat - [File Context](file-context.md) - Attach files to chat
- [Project Rules](project-rules.md) - Customize AI behavior

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 66 KiB

339
docs/creating-agents.md Normal file
View File

@@ -0,0 +1,339 @@
# Creating and Extending Agents
An *agent* is a TOML profile that tells QodeAssist which provider to call,
which model to use, and exactly what request body to send. All bundled agents
(Settings → QodeAssist → Agents) are built from the same files described here —
anything a bundled agent does, a user agent can do too.
## Where user agents live
Drop `*.toml` files into the user agents directory:
| OS | Path |
|---|---|
| Linux / macOS | `~/.config/QtProject/qtcreator/qodeassist/config/agents/` |
| Windows | `%APPDATA%\QtProject\qtcreator\qodeassist\config\agents\` |
QodeAssist creates the directory on startup. Files are loaded at plugin
startup; after adding or editing a file, restart Qt Creator.
Two layers are loaded:
1. **Bundled** agents shipped inside the plugin — read-only.
2. **User** agents from the directory above (marked with a `user` pill).
Agent `name`s are global across both layers. A user file that reuses a
bundled agent's `name` is rejected with an error — bundled agents cannot be
replaced; create your own agent under a new name and `extends` what you want
to build on. Two *user* files with the same `name` produce a warning, and
the alphabetically later file wins.
Load errors and warnings (TOML syntax, unknown keys, missing `extends`
parents, bodies that don't render to valid JSON) are reported in Qt Creator's
**General Messages** pane, prefixed with `[Agents]`.
## Minimal example
```toml
schema_version = 1
name = "My DeepSeek Chat"
description = "DeepSeek V3 via an OpenAI-compatible endpoint."
provider_instance = "OpenAI Compatible"
model = "deepseek-chat"
endpoint = "/chat/completions"
system_prompt = """{{ agent_role() }}"""
[body]
max_tokens = 8192
temperature = 0.7
stream = true
messages = """
[ {% include "partials/openai_messages.jinja" %} ]
"""
```
Shorter still — extend a bundled provider base and state only the delta:
```toml
schema_version = 1
extends = "OpenAI Compatible Base Chat"
name = "My DeepSeek Chat"
model = "deepseek-chat"
```
Bundled agents themselves are read-only. To get a variant of a preset, create
your own agent under a new name and put it where you want it in the roster:
```toml
schema_version = 1
extends = "Mistral Base Chat"
name = "My Mistral (low temp)"
model = "mistral-small-latest"
[body]
temperature = 0.3
```
If all you want is a different model for a preset, you don't need a file at
all — set the per-agent model override in the settings UI.
## Key reference
| Key | Required | Meaning |
|---|---|---|
| `schema_version` | no (default 1) | Format version; the plugin refuses files newer than it supports. |
| `name` | yes | Unique identifier; shown in the UI, referenced by rosters and `extends`. |
| `description` | no | Tooltip text in the Agents list. |
| `provider_instance` | yes* | Name of a provider instance (see below). |
| `model` | yes* | Default model; can be overridden per agent in settings. |
| `endpoint` | yes* | Path appended to the provider instance URL. May contain `${MODEL}` (e.g. Google: `/models/${MODEL}:streamGenerateContent?alt=sse`). |
| `system_prompt` | no | Jinja template for the system prompt (see building blocks below). FIM agents usually omit it. |
| `tags` | no | Free-form strings shown as pills in the UI for discoverability. |
| `enable_thinking` | no | Capability hint (UI badge). The `[body]` is the source of truth for what is sent. |
| `enable_tools` | no | Lets the provider inject tool definitions into the request. |
| `cache_prompt` / `cache_ttl` | no | Prompt caching (Anthropic); `cache_ttl = "1h"` selects the long TTL. |
| `extends` | no | Name of a parent agent to inherit from. |
| `abstract` | no | Mark as template-only: it can be extended but is never loaded as a usable agent. Not inherited. |
| `hidden` | no | Loaded and usable, but not listed in selection UIs. Not inherited. |
| `[match]` | no | Routing constraints (see Routing). |
| `[body]` | yes* | The literal request body (see below). |
\* required after `extends` resolution — a child inherits these from its
parent, so it only states what differs.
### Required keys checked at load
A concrete (non-abstract) agent must end up with `name`,
`provider_instance`, `model`, `endpoint`, and a non-empty `[body]`. Unknown
keys anywhere at the top level or in `[match]` produce a warning — this
catches typos like `enable_thinkin`.
## Provider instances
`provider_instance` refers to a provider configuration (URL + API key
reference + client API). Bundled instances:
`Claude`, `Codestral`, `Google AI`, `llama.cpp`,
`LM Studio (Chat Completions)`, `LM Studio (Responses API)`, `Mistral AI`,
`Ollama (Native)`, `Ollama (OpenAI-compatible)`, `OpenAI (Chat Completions)`,
`OpenAI (Responses API)`, `OpenAI Compatible`, `OpenRouter`.
User-defined instances live next to agents in
`…/qodeassist/config/providers/*.toml` and follow the same
override-by-name layering.
## `extends` — inheriting from another agent
A child deep-merges over its parent: scalar keys are replaced, tables (such
as `[body]` and `[body.options]`) merge key-by-key, and **arrays are replaced
whole** (a child that wants the parent's `tags` plus one more must restate
the full list). Chains can be deeper than one level; cycles and missing
parents are load errors.
`abstract` and `hidden` are never inherited — extending a hidden agent
yields a visible child unless the child says otherwise.
Every provider ships an abstract base that already carries the provider
instance, endpoint, and request body — extending one and setting `model` is
usually all a custom agent needs:
| Base | Provider / API |
|---|---|
| `Anthropic Base Chat` | Claude, Anthropic Messages (`/v1/messages`) |
| `OpenAI Base Chat` | OpenAI, Chat Completions (`/chat/completions`) |
| `OpenAI Responses Base` | OpenAI, Responses API (`/responses`) |
| `OpenAI Compatible Base Chat` | Any OpenAI-compatible server |
| `Google Base Chat` | Google AI, Gemini `generateContent` |
| `Mistral Base Chat` | Mistral AI, Chat Completions |
| `Codestral Base Chat` | Codestral, Chat Completions |
| `Codestral FIM Base` | Mistral AI, `/v1/fim/completions` code completion |
| `OpenRouter Base Chat` | OpenRouter, Chat Completions |
| `LM Studio Base Chat` | LM Studio, Chat Completions |
| `LM Studio Responses Base` | LM Studio, Responses API |
| `llama.cpp Base Chat` | llama.cpp server, Chat Completions |
| `Ollama Base Chat` | Ollama, native `/api/chat` |
| `Ollama (OpenAI-compatible) Base Chat` | Ollama, OpenAI-compatible endpoint |
| `Ollama FIM Base` | Ollama, native `/api/generate` fill-in-the-middle |
Concrete agents work as parents too — `extends = "Claude Sonnet 4.6 Chat"`
inherits everything including the model.
## `[body]` — the request, literally
`[body]` is the request body, written exactly like the provider's curl
example. Per key, recursively:
- **string containing jinja** (`{{` or `{%`) — rendered, and the output is
spliced in as raw JSON. A render that produces nothing drops the key.
- **plain string / number / bool / table** — passed through unchanged.
```toml
[body]
max_tokens = 16000
stream = true
thinking = { type = "adaptive", display = "summarized" }
messages = """
[ {% include "partials/anthropic_messages.jinja" %} ]
"""
```
There are no runtime toggles: a thinking variant is a separate agent file
that carries the thinking fields in its body.
Every agent body is dry-run rendered at load against a synthetic
conversation (text, thinking, tool calls, tool results, images), so jinja
syntax errors, unknown callbacks, missing partials, and invalid JSON are
reported at startup — not mid-conversation. Trailing commas emitted by loops
are stripped automatically; don't bother with `loop.is_last` bookkeeping.
### Template data (`ctx`)
| Field | Content |
|---|---|
| `ctx.system_prompt` | Rendered system prompt (present only if the agent has one). |
| `ctx.prefix` / `ctx.suffix` | Code around the cursor (FIM/completion sessions). |
| `ctx.files_metadata` | Array of `{ file_path, content }` for attached files. |
| `ctx.history` | Array of messages: `{ role, content, content_blocks, images? }`. |
`content` is the message's flattened text; `content_blocks` is the
structured form:
| `type` | Fields |
|---|---|
| `text` | `text` |
| `thinking` | `thinking`, `signature` |
| `redacted_thinking` | `data` |
| `tool_use` | `id`, `name`, `input` (JSON object) |
| `tool_result` | `tool_use_id`, `content`, `name` |
| `image` | `data`, `media_type`, `is_url` |
### Callbacks available in `[body]`
| Callback | Purpose |
|---|---|
| `tojson(x)` | Serialize any value as JSON (correct quoting/escaping). Use it for every interpolated value. |
| `filter_by_type(blocks, "tool_use")` | Subset of `content_blocks` with the given type. |
| `filter_skip_role(history, "system")` | History without messages of a role. |
| `strip_signature_suffix(s)` | Remove a trailing `[Signature: …]` marker. |
### Partials and `{% include %}`
The repetitive message-array rendering lives in shared partials. Includes
resolve against the bundled set first, then the user agent's own directory —
so a user agent can both reuse bundled partials and ship its own next to its
TOML (e.g. `partials/my_messages.jinja`). Paths with `..` or a leading `/`
are rejected.
Bundled partials: `partials/openai_messages.jinja`,
`partials/openai_responses_input.jinja`, `partials/anthropic_messages.jinja`,
`partials/google_contents.jinja`, `partials/ollama_messages.jinja` (plus the
per-message helpers they include).
## `system_prompt` — composable building blocks
`system_prompt` is itself a jinja template, rendered with:
| Helper | Purpose |
|---|---|
| `{{ agent_role() }}` | Text of the role currently selected in the chat (falls back to `developer`). |
| `{{ agent_role("reviewer") }}` | A specific role by id (Settings → QodeAssist → Roles). |
| `{{ read_file("${PROJECT_DIR}/STYLE.md") }}` | Inline a file. Reads are restricted to the project directory and `~/qodeassist`. |
| `{{ file_exists(p) }}` / `{{ read_dir(p) }}` | Existence check / directory listing (same root restrictions). |
| `{{ head_lines(s, n) }}` | First `n` lines of a string. |
| `basename`, `dirname`, `ext`, `lower`, `upper` | Path/string helpers. |
| `${PROJECT_DIR}`, `${HOME}` | Substituted before rendering. |
Example:
```toml
system_prompt = """
{{ agent_role() }}
{% if file_exists("${PROJECT_DIR}/.qodeassist-style.md") %}
Project conventions:
{{ read_file("${PROJECT_DIR}/.qodeassist-style.md") }}
{% endif %}
"""
```
## Routing — `[match]` and rosters
Each pipeline (code completion, chat, compression, quick refactor) has an
ordered roster of agents. For the current file, the **first roster entry
whose `[match]` accepts** wins.
```toml
[match]
file_patterns = ["*.qml", "*.js"]
path_patterns = ["*/tests/*"]
project_names = ["MyProject"]
```
- Dimensions are ANDed; an empty dimension is unconstrained; an entirely
empty/absent `[match]` is a catch-all.
- `file_patterns` are case-insensitive globs tested against the file name
and the full path; `path_patterns` against the full path only.
- `project_names` are exact, case-sensitive project names.
Typical setup: a specialized agent (e.g. `Qt CodeLlama 13B QML FIM` with
`*.qml`) first, a catch-all agent last.
## Models
The TOML `model` is only the default. The settings UI can set a per-agent
override (stored in `agent_models.json`); the resolved model is also
substituted into `${MODEL}` in `endpoint` before sending.
## Contributing your agent to QodeAssist
The bundled agent set grows through contributions — if you've made an agent
for a provider or model that others could use, please send it upstream
instead of keeping it local. No C++ is needed:
1. Develop and verify the agent locally in the user agents directory.
2. In a fork, copy the TOML to `sources/agents/` and register the file in
`sources/agents/agents.qrc`.
3. Keep it a thin delta: extend the matching provider base and set only
`name`, `description`, `model`, `tags` (and `[body]` keys that genuinely
differ). Look at `mistral_chat.toml` or `ollama_qwen25_coder_fim.toml`
for the expected shape.
4. Run the tests (`QodeAssistTest`): `BundledAgentsTest` automatically
loads every bundled agent, resolves its `extends` chain, and dry-renders
its `[body]` — if your TOML passes, it works.
5. Open a pull request.
Conventions:
- File name: `<provider>_<model_or_purpose>_<kind>.toml`
(e.g. `ollama_qwen25_coder_fim.toml`).
- `name` is user-visible and must be unique; include the provider and model
(e.g. `Ollama Qwen2.5-Coder FIM`).
- Specialized completion agents should carry a `[match]` block so routing
can pick them automatically (e.g. `file_patterns = ["*.qml"]`).
- A whole new provider with an OpenAI-compatible API is also TOML-only: a
provider instance file in `sources/providersConfig/`, one abstract
`<Provider> Base Chat`, and concrete agents on top. New request/response
*formats* are the only thing that needs C++.
## Troubleshooting
- **Agent missing from the list** — check General Messages for `[Agents]
error:` lines; the file failed to parse, resolve, or validate.
- **`… has the same name as a bundled agent — bundled agents cannot be
replaced`** — pick a different `name`; use `extends` to inherit from the
bundled agent instead.
- **`Unknown key 'x' … ignored (typo?)`** — the key isn't part of the
schema; compare with the table above.
- **`Agent 'X' extends unknown agent 'Y'`** — the parent's `name` (not file
name) must match exactly; the parent must be bundled or in the same
directory.
- **`[body] failed to render to valid JSON`** — the dry run failed; the log
contains the rendered snippet. Usually a missing `tojson(...)` around an
interpolated string.
- **Edits not picked up** — agents are loaded at startup; restart
Qt Creator.

View File

@@ -1,35 +0,0 @@
# Project Rules Configuration
QodeAssist supports project-specific rules to customize AI behavior for your codebase. Create a `.qodeassist/rules/` directory in your project root.
## Quick Start
```bash
mkdir -p .qodeassist/rules/{common,completion,chat,quickrefactor}
```
## Directory Structure
```
.qodeassist/
└── rules/
├── common/ # Applied to all contexts
├── completion/ # Code completion only
├── chat/ # Chat assistant only
└── quickrefactor/ # Quick refactor only
```
All `.md` files in each directory are automatically loaded and added to the system prompt.
## Example
Create `.qodeassist/rules/common/general.md`:
```markdown
# Project Guidelines
- Use snake_case for private members
- Prefix interfaces with 'I'
- Always document public APIs
- Prefer Qt containers over STL
```

View File

@@ -206,7 +206,6 @@ The LLM receives:
- **Cursor Position**: Marked with `<cursor>` tag - **Cursor Position**: Marked with `<cursor>` tag
- **Selection Markers**: `<selection_start>` and `<selection_end>` tags - **Selection Markers**: `<selection_start>` and `<selection_end>` tags
- **Your Instructions**: Built-in, custom, or typed - **Your Instructions**: Built-in, custom, or typed
- **Project Rules**: If configured (see [Project Rules](project-rules.md))
### Context Configuration ### Context Configuration
@@ -270,7 +269,6 @@ Fully local setup for offline or secure environments.
## Related Documentation ## Related Documentation
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
- [File Context](file-context.md) - Attaching files to chat context - [File Context](file-context.md) - Attaching files to chat context
- [Ignoring Files](ignoring-files.md) - Exclude files from AI context - [Ignoring Files](ignoring-files.md) - Exclude files from AI context
- [Provider Configuration](../README.md#configuration) - Setting up LLM providers - [Provider Configuration](../README.md#configuration) - Setting up LLM providers

652
docs/target-architecture.md Normal file
View File

@@ -0,0 +1,652 @@
# QodeAssist — Target Architecture (v1.0)
Status: design baseline, derived from the fixed use-case inventory below.
Scope: the complete plugin, designed "from scratch" — what the architecture
should be if nothing legacy constrained it. The current code (see
`architecture.md`) already converges on this; §10 lists the remaining deltas.
---
## 1. Use-case inventory (requirements baseline)
Every architectural decision below is justified by one of these. Features not
on this list (Rules system, legacy provider/model/template pickers, Stack A)
are intentionally out of scope.
| # | Use case | What the user gets |
|---|----------|--------------------|
| U1 | **Code completion** | Inline FIM/instruct suggestions via LSP; auto + manual trigger, multiline, smart-context suppression, accept full / word-by-word |
| U2 | **Chat assistant** | 4 placements (sidebar, bottom pane, editor tab, floating window); streaming text + thinking blocks + tool blocks + file-edit blocks (apply/undo); attachments, linked files, @-mentions, open-files sync; token counter; persisted history; one-click summarization; runtime agent + role pickers |
| U3 | **Quick refactor** | Selection + instruction by hotkey; custom-instructions library; separate agent; optional tools; streamed result inserted into the editor |
| U4 | **Tools** | read/create/edit file, search, find, list, build, diagnostics, terminal, todo, load_skill; per-tool enable |
| U5 | **Skills** | discovery from `.qodeassist/skills`, `.claude/skills`, `~/.claude/skills`; auto-injection, explicit `/` picker, always-on |
| U6 | **MCP** | server mode (expose plugin tools, HTTP/SSE + stdio bridge) and client hub (consume external tools in chat/refactor) |
| U7 | **Providers** | 13 `client_api` types over one GenericProvider; secrets store; local-server autostart; model listing |
| U8 | **Agents** | TOML profiles: `extends`, `[body]` table 1:1 with the wire request, Jinja partials, `match` rules, per-agent model override, per-pipeline rosters |
| U9 | **Roles** | JSON roles composed into `system_prompt` via `{{ agent_role(id) }}` |
| U10 | **Bench CLI** | headless agent testing on the same core stack, `.env` secrets |
| U11 | **Configuration UI** | settings pages for everything above; per-project settings; updater + status widget |
---
## 2. Design principles
1. **One stack.** Every LLM byte — completion, chat, compression, refactor,
bench — flows through the same `Session` pipeline. No parallel legacy path.
2. **Hexagonal core.** The runtime (agents, sessions, providers, templates,
prompt rendering) has zero Qt Creator dependencies. The IDE and the bench
CLI are two hosts composing the same core; IDE-specific facts enter only
through ports (document reading, project scanning, secrets, tool hosting).
3. **Configuration is declarative, code is mechanism.** What is sent (request
`[body]`, system prompt, endpoint, model) lives in TOML/JSON/Jinja and is
user-overridable; *how* it is sent (streaming, retries, tool loop, event
routing) lives in C++ and is identical for all providers.
4. **Capability-driven behavior.** Providers and agents declare capabilities
(tools, thinking, images, model listing); features and UI adapt to the
declared set instead of switching on provider names.
5. **Single source of truth for conversation state.** `ConversationHistory`
owns the messages; `ChatModel` and persistence are projections of it, never
independent copies.
6. **Per-feature composition roots, no singletons.** Each feature constructs
and owns its dependencies (`new` + parent); shared services are passed
explicitly (constructor/setter, QML context properties for the chat).
7. **Streaming-first event model.** One typed `ResponseEvent` stream is the
only contract between the core and every consumer. Deltas exist for live
UI (chat); one-shot pipelines (completion, refactor, bench) ignore them,
wait for `finished`, and read the final assistant message from history.
8. **Fail at load, not mid-conversation.** Agent profiles are validated when
loaded (partials resolve, assembled body parses as JSON against a synthetic
context), so a config error never surfaces as a silent runtime drop.
---
## 3. Layered model
```mermaid
flowchart TB
subgraph HOSTS["Hosts — composition roots"]
PLUGIN["Qt Creator plugin<br/>qodeassist.cpp"]
BENCH["bench CLI"]
end
subgraph L5["L5 · Presentation"]
LSP["LSP bridge<br/>inline suggestions"]
QMLUI["ChatView QML<br/>4 placements"]
RW["Refactor widgets"]
SUI["Settings pages"]
end
subgraph L4["L4 · Features"]
FCOMP["CompletionFeature"]
FCHAT["ChatFeature"]
FREF["RefactorFeature"]
end
subgraph L3["L3 · Capabilities"]
CTX["ContextEngine<br/>ports + QtC adapters"]
TOOLS["ToolKit"]
SKILLS["SkillsEngine"]
MCPH["McpHub<br/>client + server"]
end
subgraph L2["L2 · Core runtime — IDE-independent"]
SM["SessionManager"]
SESS["Session"]
AGF["AgentFactory + AgentRouter"]
AG["Agent"]
PROV["GenericProvider"]
TPL["JsonPromptTemplate"]
end
subgraph L1["L1 · Declarative config"]
PCONF["providers/*.toml"]
ACONF["agents/*.toml + partials/*.jinja"]
ROST["rosters / pipelines"]
ROLES["agent_roles/*.json"]
SKCONF["skills/*.md"]
SEC["SecretsStore"]
end
subgraph L0["L0 · Wire — LLMQore"]
CLIENTS["*Client — SSE streaming"]
TOOLFW["Tool framework"]
MCPT["MCP transports"]
end
PLUGIN --> L4
PLUGIN --> SUI
BENCH --> SM
LSP --> FCOMP
QMLUI --> FCHAT
RW --> FREF
FCOMP --> SM
FCHAT --> SM
FREF --> SM
FCOMP --> CTX
FCHAT --> CTX
FREF --> CTX
FCHAT --> SKILLS
FCHAT --> TOOLS
FREF --> TOOLS
TOOLS --> TOOLFW
MCPH --> MCPT
SM --> SESS
SESS --> AG
AGF --> AG
AG --> PROV
AG --> TPL
AGF --> ACONF
AGF --> PCONF
AGF --> SEC
AGF --> ROST
TPL --> ROLES
PROV --> CLIENTS
SKILLS --> SKCONF
```
### Layer contracts
| Layer | Contains | May depend on | Must NOT depend on |
|-------|----------|---------------|--------------------|
| **L0 Wire** | LLMQore clients (one per wire protocol: Claude, OpenAI Chat, OpenAI Responses, Google, Ollama, Mistral, llama.cpp), tool framework, MCP transports | Qt Network | anything above |
| **L1 Config** | `ProviderInstance`, `AgentProfile` (+ loader/validator), rosters, roles, skills, secrets port | toml++, inja | Qt Creator, L2+ |
| **L2 Core** | `Agent`, `AgentFactory`, `AgentRouter`, `Provider`/`GenericProvider`, `JsonPromptTemplate`, `Session`, `SessionManager`, `ConversationHistory`, `SystemPromptBuilder`, `ResponseRouter`, `ToolContributorRegistry` | L0, L1 | Qt Creator, QML, features |
| **L3 Capabilities** | `ContextEngine` (ports + QtC adapters), `ToolKit` (built-in tools), `SkillsEngine`, `McpHub` | L0L2, QtC APIs *only in adapters* | features, UI |
| **L4 Features** | `CompletionFeature`, `ChatFeature` (send/stream, compression, token counting, file edits), `RefactorFeature` | L2, L3 | each other |
| **L5 Presentation** | LSP bridge, ChatView QML, refactor widgets, settings pages | its feature | core internals |
| **Hosts** | plugin shell, bench CLI | everything (composition only) | — |
The hard rule that makes U10 (bench) and testability free: **L0L2 build into
targets with no Qt Creator linkage.** Bench links L0L2 plus a thin CLI host;
the plugin adds L3 adapters, L4, L5.
---
## 4. Core domain model
Rendered copy: [core-class-diagram.svg](core-class-diagram.svg) (regenerate
when the diagram below changes).
```mermaid
classDiagram
direction TB
class SessionManager {
+acquire(agentName) Session
+release(session)
+toolContributors() ToolContributorRegistry
}
class Session {
+send(blocks, toolPolicy)
+cancel()
+history() ConversationHistory
+systemPrompt() SystemPromptBuilder
+event(ResponseEvent)
+finished(id, stopReason)
+failed(id, ErrorInfo)
+cancelled(id)
}
class ConversationHistory {
+messages() vector~Message~
+lastAssistantText() string
+append(Message)
+reset(vector~Message~)
}
class Message {
+role Role
+blocks vector~ContentBlock~
}
class SystemPromptBuilder {
+setLayer(id, text, priority)
+removeLayer(id)
+compose() string
}
class ResponseRouter {
+attach(BaseClient)
+event(ResponseEvent)
}
class Agent {
+config() AgentConfig
+provider() Provider
+promptTemplate() PromptTemplate
}
class AgentFactory {
+create(name) Agent
+configByName(name) AgentConfig
+effectiveModel(name) string
}
class AgentRouter {
+pickAgent(roster, fileCtx) string
}
class Provider {
<<interface>>
+capabilities() Capabilities
+prepareRequest(request, ctx)
+sendRequest(json) RequestID
+cancelRequest(RequestID)
}
class GenericProvider {
-client BaseClient
}
class PromptTemplate {
<<interface>>
+buildFullRequest(request, ctx)
}
class JsonPromptTemplate {
-bodySpec QJsonObject
-env InjaEnvironment
}
class ToolContributorRegistry {
+registerContributor(fn)
+applyTo(ToolsManager)
}
SessionManager o-- Session : pools
SessionManager --> AgentFactory : builds via
SessionManager --> ToolContributorRegistry
Session *-- ConversationHistory
Session *-- SystemPromptBuilder
Session *-- ResponseRouter
Session --> Agent
ConversationHistory o-- Message
Agent *-- Provider
Agent *-- PromptTemplate
AgentFactory ..> Agent : creates
AgentFactory --> AgentRouter
GenericProvider --|> Provider
JsonPromptTemplate --|> PromptTemplate
```
Responsibilities, one line each:
- **Agent** — immutable bundle of *what to call*: resolved config + provider +
compiled prompt template. No request state.
- **Session** — one conversation's runtime: owns history, system-prompt
layers, response routing, the in-flight request, and the tool-execution
loop (tool_use → execute → tool_result → continue). `send(blocks)` is the
*only* entry point: every pipeline appends a user message and dispatches;
there are no per-pipeline send variants. What differs between completion,
chat, and refactor is the agent's template and the consumption mode (deltas
vs final message), never the Session API.
- **SessionManager** — creates/pools sessions per agent; the single place
features go to get one. Pooling (not per-message construction) covers the
"fresh agent + provider + secrets read per request" latency cost. It reuses
only the expensive parts (agent, provider, compiled template, secrets read):
`acquire` hands out a session with cleared history and system-prompt
layers, so one-shot pipelines never see a previous exchange.
- **AgentRouter** — the *only* agent picker. Every pipeline (completion, chat,
compression, refactor) resolves its agent through
`pickAgent(roster, {file, project})`; no feature-local picker logic.
- **GenericProvider** — one class for all 13 client APIs; varies only by
LLMQore client factory + metadata. Request *shape* belongs to the template,
never to the provider.
- **JsonPromptTemplate** — compiles the agent's `[body]` table; renders
Jinja-bearing string values, splices raw JSON, drops empty keys; validated
at load time.
- **SystemPromptBuilder** — ordered named layers (`agent.system`,
`chat.context`, `refactor`, `compression`); features mutate only their own
layer.
- **ResponseRouter / ResponseEvent** — adapts LLMQore client signals into one
typed stream: `TextDelta`, `ThinkingDelta`, `ToolCallStart/End`,
`ToolResult`, `Usage`, `Error`, `MessageStop`.
- **ToolContributorRegistry** — contributors (built-in ToolKit, SkillTool,
McpHub) register once; `SessionManager` applies them to every new session's
`ToolsManager`. This is how MCP tools reach chat *and* refactor (U6) without
feature code knowing about MCP.
---
## 5. Runtime flows
### 5.1 Chat (U2) — the richest path
```mermaid
sequenceDiagram
autonumber
actor U as User
participant V as ChatView QML
participant F as ChatFeature
participant SM as SessionManager
participant S as Session
participant T as JsonPromptTemplate
participant P as GenericProvider
participant C as LLMQore Client
participant R as ResponseRouter
U->>V: message + attachments
V->>F: sendMessage(text, files, images)
F->>SM: acquire(activeAgent)
SM-->>F: Session (pooled)
F->>S: systemPrompt().setLayer("chat.context", project + skills + linked files)
F->>S: send(userBlocks, toolPolicy)
S->>T: buildFullRequest(history, system, ctx)
T-->>S: request JSON (body is 1:1 with the API)
S->>P: sendRequest(json)
P->>C: HTTP POST, SSE stream
loop streaming
C-->>R: chunk / thinking / tool_use / usage
R-->>S: ResponseEvent
S-->>F: event(ResponseEvent)
F-->>V: ChatModel projection update
end
opt tool call requested
S->>S: execute tool via ToolsManager
S->>P: continue with tool_result
end
C-->>R: finalized
R-->>S: MessageStop + Usage
S-->>F: finished()
F->>SM: release(session)
```
State ownership in chat: `Session.history()` is the truth. `ChatModel` is a
QML projection built from history events (`messageAdded`, `messageUpdated`);
`ChatSerializer`/`ChatHistoryStore` persist *history*, and restoring a chat
seeds a new session's history — never the other way around. File-edit blocks,
apply/undo, and the token counter are ChatFeature concerns layered on the
event stream.
### 5.2 Completion (U1)
```
LSP getCompletionsCycling
→ CompletionFeature
agent = AgentRouter.pickAgent(roster.codeCompletion, {file, project})
session = SessionManager.acquire(agent)
ctx = ContextEngine: prefix/suffix + open-files context (policy from
CodeCompletionSettings — editor policy, not agent config)
session.send(blocks{completion context}, tools=off)
on finished → history().lastAssistantText()
→ CodeHandler (output-mode post-processing) → LSP items
```
No special Session method: the completion context travels as the content of
an ordinary user message (a structured block carrying prefix/suffix + file
context), and the template context exposes it as `ctx.prefix` / `ctx.suffix`.
FIM vs instruct is *agent config* (template + body), not feature code: a FIM
agent's body renders `prefix`/`suffix` into FIM fields; an instruct agent's
body renders the same exchange as a chat-shaped request. The feature is
identical for both — and since completion has no incremental UI, it never
touches the delta stream: it waits for `finished` and reads the last message.
### 5.3 Quick refactor (U3)
```
Hotkey → RefactorFeature
agent = AgentRouter.pickAgent(roster.quickRefactor, {file, project})
session = SessionManager.acquire(agent)
session.systemPrompt().setLayer("refactor", tagged selection + output rules)
session.send(blocks{instruction}, toolPolicy)
on finished → history().lastAssistantText()
→ ResponseCleaner → RefactorResult → editor insert (accept/reject)
```
Same consumption mode as completion: the feature listens to
`Session::finished`/`failed` only (events at most drive a progress spinner
and cancel) and reads the result from history — it never connects to raw
client signals. Tool calls during refactor run inside the session's tool
loop; history's last assistant message is whatever the model produced after
the final tool round.
### 5.4 Compression (U2) and bench (U10)
Compression is ChatFeature reusing the same path with
`roster.chatCompression` and a `"compression"` system layer; the summary
starts a new history. Bench is a host: CLI args + `.env` secrets → L1 + L2
composition → `Session.send` → events printed to stdout. Anything bench can't
do without the IDE is, by construction, an L3+ concern.
---
## 6. Configuration model
```mermaid
erDiagram
AGENT_PROFILE ||--o| AGENT_PROFILE : extends
AGENT_PROFILE }o--|| PROVIDER_INSTANCE : provider_instance
AGENT_PROFILE }o--o{ PARTIAL : includes
AGENT_PROFILE }o--o{ ROLE : agent_role
ROSTER }o--o{ AGENT_PROFILE : ranks
MODEL_OVERRIDE |o--|| AGENT_PROFILE : overrides_model
PROVIDER_INSTANCE }o--|| CLIENT_API : client_api
PROVIDER_INSTANCE }o--o| SECRET : api_key_ref
PROVIDER_INSTANCE ||--o| LAUNCH_CONFIG : autostarts
AGENT_PROFILE {
string name
bool abstract
string system_prompt "jinja, composes agent_role()"
json body "request body, 1:1 with API"
string endpoint "may contain MODEL placeholder"
string model "default; override wins"
bool enable_tools "capability hint"
bool enable_thinking "capability hint"
json match "file, path, project patterns"
}
PROVIDER_INSTANCE {
string name
string client_api
string url
string api_key_ref
}
ROLE {
string id
string systemPrompt
}
ROSTER {
string pipeline "completion, chat, compression, refactor"
list agents "ordered candidates"
}
```
Rules of the config layer (full spec: `agent-templates-design.md`):
- `[body]` **is** the request body — field-by-field, deep-mergeable through
`extends`; Jinja-bearing strings render and splice as raw JSON, literals
pass through. No separate sampling/thinking merge machinery.
- `include` resolves only sandboxed partial roots (bundled `:/agents/partials/`,
then user `partials/`); a missing partial is a load-time error.
- Two-level hierarchy: one abstract base per provider family, thin children.
- Per-agent model override lives in `agent_models.json` and is applied by
`AgentFactory`; `${MODEL}` in `endpoint` covers URL-model providers.
- Roles are JSON managed by the Roles settings UI; profiles pull them in with
`{{ agent_role("<id>") }}` — the only system-prompt edit point is the
profile.
- Secrets never appear in TOML; `api_key_ref` resolves through the
`SecretsStore` port (QtC keychain in the plugin, `.env` in bench).
---
## 7. Capabilities layer
**ContextEngine** replaces the monolithic ContextManager with three focused
services behind IDE-agnostic ports:
| Service | Port (L2-visible) | QtC adapter |
|---------|-------------------|-------------|
| `EditorContext` — current doc, selection, prefix/suffix | `IDocumentReader` | TextEditor API |
| `ProjectContext` — root, file listing, ignore filtering (`.qodeassistignore`), open files, changes | `IProjectScanner` | ProjectExplorer API |
| `TokenEstimator` — input estimates, calibrated by server usage | — (pure) | — |
**ToolKit** registers the built-in tools (U4) with the
`ToolContributorRegistry`; each tool declares a permission class (read /
write / execute) so per-tool enablement (settings) and confirmation policy
(terminal commands) live in one place.
**SkillsEngine** (U5): discovery + watching of the three skill roots; exposes
`catalogText()` (names + descriptions for the system prompt),
`alwaysOnBodies()`, and the `load_skill` tool; the `/` picker injects a
skill's body into a single message.
**McpHub** (U6): client side connects configured servers and contributes
their tools through the same registry (tools reach every session uniformly);
server side exposes ToolKit over HTTP/SSE + stdio bridge.
---
## 8. Cross-cutting policies
Architecture is the rules as much as the boxes. These policies bind every
layer and are part of the contract:
### 8.1 Threading
The core runs on the GUI thread; concurrency is the Qt event loop plus async
network I/O — no shared-state threading anywhere in L1L4. Work that can
block (project scans, token estimation over large trees) hides behind L3
ports; an adapter may use worker threads internally but delivers results as
queued signals. Core types are therefore deliberately not thread-safe.
### 8.2 Request lifecycle
A session has at most one in-flight request; `send()` while in flight cancels
the previous request first. Every request terminates in exactly one of three
states — `finished(stopReason)`, `failed(error)`, `cancelled()` — and
cancellation is *not* an error: no consumer may string-match a message to
tell them apart.
### 8.3 Errors
Runtime errors are typed, not strings: `ErrorInfo { category, message,
providerDetail }` with categories `Config | Auth | Network | Provider |
Validation | Tool`. The category drives UI affordances (Auth → open provider
settings, Network → offer retry); free text is for logs only. Load-time
errors (principle 8) surface in the agents settings page, never as a failed
send.
### 8.4 Timeouts and retries
Transfer timeouts are per-pipeline policy (completion short, chat/refactor
from settings), applied by the feature — never baked into agent profiles. A
streaming request is never silently retried after the first byte; automatic
retry with capped backoff is allowed only for connection-phase failures.
Anything beyond that is an explicit user action.
### 8.5 Observability
One `RequestID` correlates feature → session → provider → client → events →
logs. Each layer logs under its own category (`qodeassist.session`,
`qodeassist.provider`, `qodeassist.tools`, …); request bodies are logged only
at debug level, and secrets are redacted unconditionally. `Usage` events are
the single source feeding the token counter, `TokenEstimator` calibration,
and the performance log.
### 8.6 Config compatibility
Agent profiles carry a `schema_version`; the loader migrates old user
configs forward or rejects them with an actionable message — silent
reinterpretation is forbidden. Bundled profiles are read-only resources that
user profiles shadow by name. Persisted chat history is versioned the same
way.
### 8.7 Security
Secrets exist only behind the `SecretsStore` port; they never reach TOML,
logs, or persisted chats. Tool permission classes (read / write / execute)
centralize the confirmation policy. The MCP server is opt-in and binds
loopback by default; skill and partial roots are sandboxed — nothing resolves
outside its declared directory.
### 8.8 Testing
The test pyramid follows the layers:
| Layer | Strategy |
|-------|----------|
| L1 | loader/validator unit tests; golden-file snapshots of every bundled profile's rendered body against a synthetic context — the same check as load-time validation, run in CI |
| L2 | `Session` / `ResponseRouter` replay tests over recorded SSE fixtures per provider; fake `BaseClient`, no network |
| L3 | contract tests against the ports; QtC adapters covered only by plugin integration |
| E2E | bench (U10) against live providers — the same composition the plugin uses |
Layering is enforced mechanically, not by review: each layer is its own
CMake target, and the core targets do not link Qt Creator — a violating
include fails the build.
---
## 9. Module / target layout
```
core/ # no Qt Creator linkage — bench + tests link this
config/ # L1: ProviderInstance, AgentProfile, loaders,
# validators, rosters, roles, secrets port
providers/ # L2: Provider, GenericProvider, ProviderFactory,
# ClaudeCacheControl
prompt/ # L2: JsonPromptTemplate, ContextRenderer, partials
agents/ # L2: Agent, AgentFactory, AgentRouter
session/ # L2: Session, SessionManager, ConversationHistory,
# SystemPromptBuilder, ResponseRouter, events
skills/ # L3 (IDE-free part): SkillsEngine, loaders
ide/ # Qt Creator adapters only
context/ # EditorContext, ProjectContext adapters, ignore
tools/ # built-in ToolKit (build, issues, editor edits…)
mcp/ # McpHub managers
features/
completion/ # LSP bridge + CompletionFeature + CodeHandler
chat/ # ChatFeature: ClientInterface, ChatModel(projection),
# Compressor, TokenCounter, FileEditController,
# serializer/store
refactor/ # RefactorFeature + custom instructions
ui/
ChatView qml/, widgets/, settings pages
hosts/
plugin/ # qodeassist.cpp — composition root, actions, panes
bench/ # CLI composition root
tests/
config/ # loader cases + golden rendered-body snapshots
session/ # SSE replay fixtures per provider, fake client
external/
llmqore/ inja/ tomlplusplus/
```
Dependency direction is strictly downward in the table of §3; `features/*`
never include each other; `ui/*` talks only to its feature; `hosts/*` are the
only places allowed to know about everything.
---
## 10. Deltas from the current working tree
What "from scratch" changes relative to today's code — the migration
checklist to call the architecture done:
1. **Stack A physical teardown** — delete root `providers/*`,
`pluginllmcore/*`, `ConfigurationManager`, legacy provider/model/template
settings pages, and the Stack A registration + MCP loop in
`qodeassist.cpp`. Runtime already has no consumers.
2. **Single history owner** — make `ChatModel` a projection of
`Session::history()` (subscribe to history signals) instead of a parallel
message store with seed-on-send; `ChatCompressor` reads history, not the
model.
3. **Single send path** — delete `Session::sendCompletion(ContextData)`;
the completion context becomes user-message content sent through the one
`send()` (the completion handler already reads its result from history's
last message). Move `QuickRefactorHandler` off raw `BaseClient` signals
(`requestCompleted`/`requestFinalized`/`requestFailed`) onto
`Session::finished`/`failed` + `history().lastAssistantText()`.
4. **Three-state request lifecycle** — add `cancelled` to `Session`; today
`cancel()` emits `failed(id, "Cancelled by user")` and consumers must
string-match to tell cancellation from failure (§8.2).
5. **Typed errors** — replace `lastError` strings and the `failed(QString)`
payload with `ErrorInfo` categories (§8.3).
6. **One agent picker** — fold `pickCompletionAgent` / `pickRefactorAgent`
remnants into `AgentRouter.pickAgent(roster, …)` exclusively; chat picker
filters to the `chatAssistant` roster.
7. **MCP tools on session clients** — register MCP-contributed tools through
`ToolContributorRegistry` so chat/refactor sessions get them (today they
are registered only on dead Stack A providers).
8. **Session pooling**`SessionManager.acquire/release` with a small pool
per agent, replacing per-message agent + provider + secrets construction.
9. **ContextManager split** — extract `EditorContext` / `ProjectContext` /
`TokenEstimator` behind ports; move QtC API use into `ide/context`.
10. **`[body]` model completion** — finish `agent-templates-design.md`
(body-table rendering, sandboxed `include`, load-time validation, model
override + `${MODEL}`, `schema_version` gate), delete sampling/thinking
merge machinery.
11. **Message type unification** — one `Message`/`ContentBlock` shape from
history to QML (roles, text, thinking, tool use/result, images); delete
the parallel `ChatModel::Message` struct.
12. **Test scaffolding** — golden rendered-body snapshots + SSE replay
fixtures (§8.8); CI builds the core targets without Qt Creator so a
layering violation fails the build.
13. **Stale docs cleanup**`project-rules.md` describes the removed Rules
system; mark or delete.

View File

@@ -15,9 +15,8 @@
#include <QSaveFile> #include <QSaveFile>
#include <QTimer> #include <QTimer>
#include <LLMQore/ToolsManager.hpp>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <pluginllmcore/Provider.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <settings/McpSettings.hpp> #include <settings/McpSettings.hpp>
namespace QodeAssist::Mcp { namespace QodeAssist::Mcp {
@@ -176,18 +175,14 @@ QList<McpServerConnection *> McpClientsManager::connections() const
return m_connections; return m_connections;
} }
QList<PluginLLMCore::Provider *> McpClientsManager::toolsCapableProviders() const void McpClientsManager::registerToolsOn(::LLMQore::ToolsManager *tools) const
{ {
QList<PluginLLMCore::Provider *> out; if (!tools)
auto &pm = PluginLLMCore::ProvidersManager::instance(); return;
for (const QString &name : pm.providersNames()) { for (auto *c : m_connections) {
auto *p = pm.getProviderByName(name); if (c)
if (!p) c->registerToolsOn(tools);
continue;
if (p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools))
out.append(p);
} }
return out;
} }
QJsonObject McpClientsManager::builtinServers() QJsonObject McpClientsManager::builtinServers()
@@ -319,8 +314,6 @@ void McpClientsManager::loadFromDisk()
newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject())); newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject()));
} }
const auto providers = toolsCapableProviders();
const bool masterEnabled = Settings::mcpSettings().enableMcpClients(); const bool masterEnabled = Settings::mcpSettings().enableMcpClients();
QList<McpServerConnection *> keep; QList<McpServerConnection *> keep;
@@ -350,7 +343,6 @@ void McpClientsManager::loadFromDisk()
existing->deleteLater(); existing->deleteLater();
} }
c = new McpServerConnection(cfg, this); c = new McpServerConnection(cfg, this);
c->setProviders(providers);
connect( connect(
c, c,
&McpServerConnection::stateChanged, &McpServerConnection::stateChanged,

View File

@@ -35,6 +35,8 @@ public:
bool removeServer(const QString &name); bool removeServer(const QString &name);
void reload(); void reload();
void registerToolsOn(::LLMQore::ToolsManager *tools) const;
signals: signals:
void serversChanged(); void serversChanged();
void writeFailed(const QString &reason); void writeFailed(const QString &reason);
@@ -50,7 +52,6 @@ private:
void setupWatcher(); void setupWatcher();
void updateWatchedPaths(); void updateWatchedPaths();
QList<PluginLLMCore::Provider *> toolsCapableProviders() const;
static QJsonObject builtinServers(); static QJsonObject builtinServers();
QJsonObject readRoot() const; QJsonObject readRoot() const;
bool writeRoot(const QJsonObject &root); bool writeRoot(const QJsonObject &root);

View File

@@ -23,7 +23,6 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <pluginllmcore/Provider.hpp>
#include <settings/McpSettings.hpp> #include <settings/McpSettings.hpp>
namespace QodeAssist::Mcp { namespace QodeAssist::Mcp {
@@ -35,13 +34,6 @@ QString transportToString(McpTransportKind k)
return k == McpTransportKind::Http ? QStringLiteral("http") : QStringLiteral("stdio"); return k == McpTransportKind::Http ? QStringLiteral("http") : QStringLiteral("stdio");
} }
bool providerSupportsTools(PluginLLMCore::Provider *p)
{
if (!p)
return false;
return p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools);
}
} // namespace } // namespace
McpServerConfig McpServerConfig::fromJson(const QString &name, const QJsonObject &obj) McpServerConfig McpServerConfig::fromJson(const QString &name, const QJsonObject &obj)
@@ -133,15 +125,6 @@ McpServerConnection::~McpServerConnection()
disconnectFromServer(); disconnectFromServer();
} }
void McpServerConnection::setProviders(const QList<PluginLLMCore::Provider *> &providers)
{
m_providers.clear();
for (auto *p : providers) {
if (providerSupportsTools(p))
m_providers.append(p);
}
}
::LLMQore::Mcp::McpTransport *McpServerConnection::createTransport() ::LLMQore::Mcp::McpTransport *McpServerConnection::createTransport()
{ {
if (m_config.transport == McpTransportKind::Http) { if (m_config.transport == McpTransportKind::Http) {
@@ -293,40 +276,20 @@ void McpServerConnection::fetchAndRegisterTools()
[this](const QList<::LLMQore::Mcp::ToolInfo> &tools) { [this](const QList<::LLMQore::Mcp::ToolInfo> &tools) {
if (m_listToolsWatchdog) if (m_listToolsWatchdog)
m_listToolsWatchdog->stop(); m_listToolsWatchdog->stop();
if (m_providers.isEmpty()) {
LOG_MESSAGE(QString("MCP client [%1]: no tools-capable providers to "
"register %2 tools into")
.arg(m_config.name)
.arg(tools.size()));
setState(
McpConnectionState::Connected,
QStringLiteral("Connected (%1 tools)").arg(tools.size()));
return;
}
m_tools.clear();
for (const auto &info : tools) { for (const auto &info : tools) {
if (info.name.isEmpty()) if (info.name.isEmpty())
continue; continue;
m_toolIds.append(info.name); m_tools.append(info);
for (const auto &p : m_providers) {
if (!p)
continue;
auto *tm = p->toolsManager();
if (!tm)
continue;
auto *remote = new ::LLMQore::Mcp::McpRemoteTool(
m_client.data(), info, tm);
tm->addTool(remote);
}
} }
LOG_MESSAGE(QString("MCP client [%1]: registered %2 tools across %3 providers") LOG_MESSAGE(QString("MCP client [%1]: discovered %2 tools")
.arg(m_config.name) .arg(m_config.name)
.arg(tools.size()) .arg(m_tools.size()));
.arg(m_providers.size()));
setState( setState(
McpConnectionState::Connected, McpConnectionState::Connected,
QStringLiteral("Connected (%1 tools)").arg(tools.size())); QStringLiteral("Connected (%1 tools)").arg(m_tools.size()));
}) })
.onFailed(this, [this](const std::exception &e) { .onFailed(this, [this](const std::exception &e) {
if (m_listToolsWatchdog) if (m_listToolsWatchdog)
@@ -337,21 +300,19 @@ void McpServerConnection::fetchAndRegisterTools()
}); });
} }
void McpServerConnection::registerToolsOn(::LLMQore::ToolsManager *tools)
{
if (!tools || !m_client || m_state != McpConnectionState::Connected)
return;
for (const auto &info : m_tools) {
auto *remote = new ::LLMQore::Mcp::McpRemoteTool(m_client.data(), info, tools);
tools->addTool(remote);
}
}
void McpServerConnection::unregisterTools() void McpServerConnection::unregisterTools()
{ {
if (m_toolIds.isEmpty()) m_tools.clear();
return;
for (const auto &p : m_providers) {
if (!p)
continue;
auto *tm = p->toolsManager();
if (!tm)
continue;
for (const QString &id : m_toolIds)
tm->removeTool(id);
}
m_toolIds.clear();
} }
void McpServerConnection::disconnectFromServer() void McpServerConnection::disconnectFromServer()

View File

@@ -14,15 +14,17 @@
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
#include <LLMQore/McpTypes.hpp>
namespace LLMQore {
class ToolsManager;
}
namespace LLMQore::Mcp { namespace LLMQore::Mcp {
class McpClient; class McpClient;
class McpTransport; class McpTransport;
} // namespace LLMQore::Mcp } // namespace LLMQore::Mcp
namespace QodeAssist::PluginLLMCore {
class Provider;
}
namespace QodeAssist::Mcp { namespace QodeAssist::Mcp {
enum class McpTransportKind { Http, Stdio }; enum class McpTransportKind { Http, Stdio };
@@ -61,10 +63,17 @@ public:
const McpServerConfig &config() const { return m_config; } const McpServerConfig &config() const { return m_config; }
McpConnectionState state() const { return m_state; } McpConnectionState state() const { return m_state; }
QString statusText() const { return m_statusText; } QString statusText() const { return m_statusText; }
int toolCount() const { return m_toolIds.size(); } int toolCount() const { return m_tools.size(); }
QStringList toolNames() const { return m_toolIds; } QStringList toolNames() const
{
QStringList names;
names.reserve(m_tools.size());
for (const auto &tool : m_tools)
names << tool.name;
return names;
}
void setProviders(const QList<PluginLLMCore::Provider *> &providers); void registerToolsOn(::LLMQore::ToolsManager *tools);
void connectToServer(); void connectToServer();
void disconnectFromServer(); void disconnectFromServer();
@@ -75,7 +84,6 @@ signals:
private: private:
void setState(McpConnectionState state, const QString &text = {}); void setState(McpConnectionState state, const QString &text = {});
void fetchAndRegisterTools(); void fetchAndRegisterTools();
void registerTools(const QList<::LLMQore::Mcp::McpClient *> & /*unused*/);
void unregisterTools(); void unregisterTools();
::LLMQore::Mcp::McpTransport *createTransport(); ::LLMQore::Mcp::McpTransport *createTransport();
@@ -87,8 +95,7 @@ private:
QPointer<::LLMQore::Mcp::McpTransport> m_transport; QPointer<::LLMQore::Mcp::McpTransport> m_transport;
QPointer<QTimer> m_listToolsWatchdog; QPointer<QTimer> m_listToolsWatchdog;
QList<QPointer<PluginLLMCore::Provider>> m_providers; QList<::LLMQore::Mcp::ToolInfo> m_tools;
QStringList m_toolIds;
}; };
} // namespace QodeAssist::Mcp } // namespace QodeAssist::Mcp

View File

@@ -1,29 +0,0 @@
add_library(PluginLLMCore STATIC
RequestType.hpp
Provider.hpp Provider.cpp
ProvidersManager.hpp ProvidersManager.cpp
ContextData.hpp
IPromptProvider.hpp
IProviderRegistry.hpp
PromptProviderChat.hpp
PromptProviderFim.hpp
PromptTemplate.hpp
PromptTemplateManager.hpp PromptTemplateManager.cpp
ProviderID.hpp
RulesLoader.hpp RulesLoader.cpp
ResponseCleaner.hpp
)
target_link_libraries(PluginLLMCore
PUBLIC
Qt::Core
Qt::Network
QtCreator::Core
QtCreator::Utils
QtCreator::ExtensionSystem
LLMQore
PRIVATE
QodeAssistLogger
)
target_include_directories(PluginLLMCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -1,68 +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 <QJsonObject>
#include <QString>
#include <QVector>
namespace QodeAssist::PluginLLMCore {
struct ImageAttachment
{
QString data; // Base64 encoded data or URL
QString mediaType; // e.g., "image/png", "image/jpeg", "image/webp", "image/gif"
bool isUrl = false; // true if data is URL, false if base64
bool operator==(const ImageAttachment &) const = default;
};
struct ToolCall
{
QString id;
QString name;
QJsonObject arguments;
bool operator==(const ToolCall &) const = default;
};
struct Message
{
QString role;
QString content;
QString signature;
bool isThinking = false;
bool isRedacted = false;
std::optional<QVector<ImageAttachment>> images;
QVector<ToolCall> toolCalls;
QString toolCallId;
QString toolName;
// clang-format off
bool operator==(const Message&) const = default;
// clang-format on
};
struct FileMetadata
{
QString filePath;
QString content;
bool operator==(const FileMetadata &) const = default;
};
struct ContextData
{
std::optional<QString> systemPrompt;
std::optional<QString> prefix;
std::optional<QString> suffix;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
std::optional<QList<FileMetadata>> filesMetadata;
bool operator==(const ContextData &) const = default;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,24 +0,0 @@
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include "PromptTemplate.hpp"
#include <QString>
namespace QodeAssist::PluginLLMCore {
class IPromptProvider
{
public:
virtual ~IPromptProvider() = default;
virtual PromptTemplate *getTemplateByName(const QString &templateName) const = 0;
virtual QStringList templatesNames() const = 0;
virtual QStringList getTemplatesForProvider(ProviderID id) const = 0;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,21 +0,0 @@
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include "Provider.hpp"
namespace QodeAssist::PluginLLMCore {
class IProviderRegistry
{
public:
virtual ~IProviderRegistry() = default;
virtual Provider *getProviderByName(const QString &providerName) = 0;
virtual QStringList providersNames() const = 0;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,38 +0,0 @@
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplate.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::PluginLLMCore {
class PromptProviderChat : public IPromptProvider
{
public:
explicit PromptProviderChat(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderChat() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getChatTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getChatTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,37 +0,0 @@
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::PluginLLMCore {
class PromptProviderFim : public IPromptProvider
{
public:
explicit PromptProviderFim(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderFim() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getFimTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getFimTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,33 +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 <QJsonObject>
#include <QList>
#include <QString>
#include "ContextData.hpp"
#include "ProviderID.hpp"
namespace QodeAssist::PluginLLMCore {
enum class TemplateType { Chat, FIM, FIMOnChat };
class PromptTemplate
{
public:
virtual ~PromptTemplate() = default;
virtual TemplateType type() const = 0;
virtual QString name() const = 0;
virtual QStringList stopWords() const = 0;
virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0;
virtual QString description() const = 0;
virtual bool isSupportProvider(ProviderID id) const = 0;
virtual QString endpoint() const { return {}; }
virtual bool supportsToolHistory() const { return false; }
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,84 +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 "PromptTemplateManager.hpp"
#include <QMessageBox>
namespace QodeAssist::PluginLLMCore {
PromptTemplateManager &PromptTemplateManager::instance()
{
static PromptTemplateManager instance;
return instance;
}
QStringList PromptTemplateManager::fimTemplatesNames() const
{
return m_fimTemplates.keys();
}
QStringList PromptTemplateManager::chatTemplatesNames() const
{
return m_chatTemplates.keys();
}
QStringList PromptTemplateManager::getFimTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_fimTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
QStringList PromptTemplateManager::getChatTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_chatTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
PromptTemplateManager::~PromptTemplateManager()
{
qDeleteAll(m_fimTemplates);
}
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
{
if (!m_fimTemplates.contains(templateName)) {
QMessageBox::warning(
nullptr,
QObject::tr("Template Not Found"),
QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.")
.arg(templateName));
return m_fimTemplates.first();
}
return m_fimTemplates[templateName];
}
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
{
if (!m_chatTemplates.contains(templateName)) {
QMessageBox::warning(
nullptr,
QObject::tr("Template Not Found"),
QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.")
.arg(templateName));
return m_chatTemplates.first();
}
return m_chatTemplates[templateName];
}
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,50 +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 <QMap>
#include <QString>
#include "PromptTemplate.hpp"
namespace QodeAssist::PluginLLMCore {
class PromptTemplateManager
{
public:
static PromptTemplateManager &instance();
~PromptTemplateManager();
template<typename T>
void registerTemplate()
{
static_assert(std::is_base_of<PromptTemplate, T>::value, "T must inherit from PromptTemplate");
T *template_ptr = new T();
QString name = template_ptr->name();
m_fimTemplates[name] = template_ptr;
if (template_ptr->type() == TemplateType::Chat) {
m_chatTemplates[name] = template_ptr;
}
}
PromptTemplate *getFimTemplateByName(const QString &templateName);
PromptTemplate *getChatTemplateByName(const QString &templateName);
QStringList fimTemplatesNames() const;
QStringList chatTemplatesNames() const;
QStringList getFimTemplatesForProvider(ProviderID id);
QStringList getChatTemplatesForProvider(ProviderID id);
private:
PromptTemplateManager() = default;
PromptTemplateManager(const PromptTemplateManager &) = delete;
PromptTemplateManager &operator=(const PromptTemplateManager &) = delete;
QMap<QString, PromptTemplate *> m_fimTemplates;
QMap<QString, PromptTemplate *> m_chatTemplates;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,50 +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 "Provider.hpp"
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonDocument>
#include <Logger.hpp>
namespace QodeAssist::PluginLLMCore {
Provider::Provider(QObject *parent)
: QObject(parent)
{}
LLMQore::RequestID Provider::sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint)
{
auto *c = client();
c->setUrl(url.toString());
c->setApiKey(apiKey());
auto requestId = c->sendMessage(payload, endpoint);
LOG_MESSAGE(
QString("%1: Sending request %2 to %3%4").arg(name(), requestId, url.toString(), endpoint));
LOG_MESSAGE(
QString("%1: Payload:\n%2")
.arg(name(), QString::fromUtf8(QJsonDocument(payload).toJson(QJsonDocument::Indented))));
return requestId;
}
void Provider::cancelRequest(const LLMQore::RequestID &requestId)
{
LOG_MESSAGE(QString("%1: Cancelling request %2").arg(name(), requestId));
client()->cancelRequest(requestId);
}
::LLMQore::ToolsManager *Provider::toolsManager() const
{
return client()->tools();
}
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,67 +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 <QFlags>
#include <QFuture>
#include <QObject>
#include <QString>
#include <utils/environment.h>
#include "ContextData.hpp"
#include "PromptTemplate.hpp"
#include "LLMQore/BaseClient.hpp"
#include "RequestType.hpp"
namespace LLMQore {
class BaseClient;
class ToolsManager;
}
class QJsonObject;
namespace QodeAssist::PluginLLMCore {
enum class ProviderCapability {
Tools = 0x1,
Thinking = 0x2,
Image = 0x4,
ModelListing = 0x8,
};
Q_DECLARE_FLAGS(ProviderCapabilities, ProviderCapability)
Q_DECLARE_OPERATORS_FOR_FLAGS(ProviderCapabilities)
class Provider : public QObject
{
Q_OBJECT
public:
explicit Provider(QObject *parent = nullptr);
virtual ~Provider() = default;
virtual QString name() const = 0;
virtual QString url() const = 0;
virtual void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
= 0;
virtual QFuture<QList<QString>> getInstalledModels(const QString &url) = 0;
virtual ProviderID providerID() const = 0;
virtual ProviderCapabilities capabilities() const { return {}; }
virtual ::LLMQore::BaseClient *client() const = 0;
virtual QString apiKey() const = 0;
virtual LLMQore::RequestID sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint);
void cancelRequest(const LLMQore::RequestID &requestId);
::LLMQore::ToolsManager *toolsManager() const;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,22 +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
namespace QodeAssist::PluginLLMCore {
enum class ProviderID {
Any,
Ollama,
LMStudio,
Claude,
OpenAI,
OpenAICompatible,
OpenAIResponses,
MistralAI,
OpenRouter,
GoogleAI,
LlamaCpp,
Qwen,
DeepSeek
};
}

View File

@@ -1,32 +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 "ProvidersManager.hpp"
namespace QodeAssist::PluginLLMCore {
ProvidersManager &ProvidersManager::instance()
{
static ProvidersManager instance;
return instance;
}
QStringList ProvidersManager::providersNames() const
{
return m_providers.keys();
}
ProvidersManager::~ProvidersManager()
{
qDeleteAll(m_providers);
}
Provider *ProvidersManager::getProviderByName(const QString &providerName)
{
if (!m_providers.contains(providerName))
return m_providers.first();
return m_providers[providerName];
}
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,41 +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 <QString>
#include "IProviderRegistry.hpp"
#include <QMap>
namespace QodeAssist::PluginLLMCore {
class ProvidersManager : public IProviderRegistry
{
public:
static ProvidersManager &instance();
~ProvidersManager();
template<typename T>
void registerProvider()
{
static_assert(std::is_base_of<Provider, T>::value, "T must inherit from Provider");
T *provider = new T();
QString name = provider->name();
m_providers[name] = provider;
}
Provider *getProviderByName(const QString &providerName) override;
QStringList providersNames() const override;
private:
ProvidersManager() = default;
ProvidersManager(const ProvidersManager &) = delete;
ProvidersManager &operator=(const ProvidersManager &) = delete;
QMap<QString, Provider *> m_providers;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,13 +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 <QString>
#pragma once
namespace QodeAssist::PluginLLMCore {
enum RequestType { CodeCompletion, Chat, Embedding, QuickRefactoring };
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,162 +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 "ClaudeProvider.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <LLMQore/ToolsManager.hpp>
#include "ClaudeCacheControl.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist::Providers {
ClaudeProvider::ClaudeProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::ClaudeClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString ClaudeProvider::name() const
{
return "Claude";
}
QString ClaudeProvider::apiKey() const
{
return Settings::providerSettings().claudeApiKey();
}
QString ClaudeProvider::url() const
{
return "https://api.anthropic.com";
}
void ClaudeProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
request["stream"] = true;
};
auto applyThinkingMode = [&request](const auto &settings) {
const QString model = request.value("model").toString().toLower();
const bool useAdaptiveThinking = model.contains("opus-4-8") || model.contains("opus-4-7")
|| model.contains("opus-4-6") || model.contains("sonnet-4-6");
QJsonObject thinkingObj;
if (useAdaptiveThinking) {
thinkingObj["type"] = "adaptive";
const int budget = settings.thinkingBudgetTokens();
QString effort = "high";
if (budget < 8000)
effort = "low";
else if (budget < 24000)
effort = "medium";
QJsonObject outputConfig;
outputConfig["effort"] = effort;
request["output_config"] = outputConfig;
} else {
thinkingObj["type"] = "enabled";
thinkingObj["budget_tokens"] = settings.thinkingBudgetTokens();
}
request["thinking"] = thinkingObj;
request["max_tokens"] = settings.thinkingMaxTokens();
request["temperature"] = 1.0;
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
request["temperature"] = Settings::codeCompletionSettings().temperature();
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings);
if (isThinkingEnabled) {
applyThinkingMode(qrSettings);
} else {
request["temperature"] = qrSettings.temperature();
}
} else {
const auto &chatSettings = Settings::chatAssistantSettings();
applyModelParams(chatSettings);
if (isThinkingEnabled) {
applyThinkingMode(chatSettings);
} else {
request["temperature"] = chatSettings.temperature();
}
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to Claude request").arg(toolsDefinitions.size()));
}
}
const auto &ps = Settings::providerSettings();
const bool cachingOn = ps.claudeEnablePromptCaching()
&& type != PluginLLMCore::RequestType::CodeCompletion;
m_client->setUseExtendedCacheTTL(cachingOn && ps.claudeUseExtendedCacheTTL());
if (cachingOn) {
ClaudeCacheControl::apply(request, ps.claudeUseExtendedCacheTTL());
}
}
QFuture<QList<QString>> ClaudeProvider::getInstalledModels(const QString &baseUrl)
{
m_client->setUrl(baseUrl);
m_client->setApiKey(apiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID ClaudeProvider::providerID() const
{
return PluginLLMCore::ProviderID::Claude;
}
PluginLLMCore::ProviderCapabilities ClaudeProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking
| PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing;
}
::LLMQore::BaseClient *ClaudeProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -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 <pluginllmcore/Provider.hpp>
#include <LLMQore/ClaudeClient.hpp>
namespace QodeAssist::Providers {
class ClaudeProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit ClaudeProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
private:
::LLMQore::ClaudeClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,35 +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 "CodestralProvider.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
CodestralProvider::CodestralProvider(QObject *parent)
: MistralAIProvider(parent)
{}
QString CodestralProvider::name() const
{
return "Codestral";
}
QString CodestralProvider::apiKey() const
{
return Settings::providerSettings().codestralApiKey();
}
QString CodestralProvider::url() const
{
return "https://codestral.mistral.ai";
}
PluginLLMCore::ProviderCapabilities CodestralProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image;
}
} // namespace QodeAssist::Providers

View File

@@ -1,22 +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 "MistralAIProvider.hpp"
namespace QodeAssist::Providers {
class CodestralProvider : public MistralAIProvider
{
public:
explicit CodestralProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
QString apiKey() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
};
} // namespace QodeAssist::Providers

View File

@@ -1,111 +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 "DeepSeekProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "tools/ToolsRegistration.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
DeepSeekProvider::DeepSeekProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString DeepSeekProvider::name() const
{
return "DeepSeek";
}
QString DeepSeekProvider::apiKey() const
{
return Settings::providerSettings().deepSeekApiKey();
}
QString DeepSeekProvider::url() const
{
return "https://api.deepseek.com";
}
QFuture<QList<QString>> DeepSeekProvider::getInstalledModels(const QString &url)
{
m_client->setUrl(url);
m_client->setApiKey(apiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID DeepSeekProvider::providerID() const
{
return PluginLLMCore::ProviderID::DeepSeek;
}
PluginLLMCore::ProviderCapabilities DeepSeekProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools
| PluginLLMCore::ProviderCapability::Thinking
| PluginLLMCore::ProviderCapability::ModelListing;
}
void DeepSeekProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to DeepSeek request").arg(toolsDefinitions.size()));
}
}
}
::LLMQore::BaseClient *DeepSeekProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -1,38 +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 <LLMQore/OpenAIClient.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist::Providers {
class DeepSeekProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit DeepSeekProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
private:
::LLMQore::OpenAIClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,161 +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 "GoogleAIProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include "tools/ToolsRegistration.hpp"
#include <QJsonDocument>
#include <QJsonObject>
#include <QtCore/qurlquery.h>
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
namespace QodeAssist::Providers {
GoogleAIProvider::GoogleAIProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::GoogleAIClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString GoogleAIProvider::name() const
{
return "Google AI";
}
QString GoogleAIProvider::apiKey() const
{
return Settings::providerSettings().googleAiApiKey();
}
QString GoogleAIProvider::url() const
{
return "https://generativelanguage.googleapis.com/v1beta";
}
void GoogleAIProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
QJsonObject generationConfig;
generationConfig["maxOutputTokens"] = settings.maxTokens();
generationConfig["temperature"] = settings.temperature();
if (settings.useTopP())
generationConfig["topP"] = settings.topP();
if (settings.useTopK())
generationConfig["topK"] = settings.topK();
request["generationConfig"] = generationConfig;
};
auto applyThinkingMode = [&request](const auto &settings) {
QJsonObject generationConfig;
generationConfig["maxOutputTokens"] = settings.thinkingMaxTokens();
if (settings.useTopP())
generationConfig["topP"] = settings.topP();
if (settings.useTopK())
generationConfig["topK"] = settings.topK();
generationConfig["temperature"] = 1.0;
QJsonObject thinkingConfig;
thinkingConfig["includeThoughts"] = true;
int budgetTokens = settings.thinkingBudgetTokens();
if (budgetTokens != -1) {
thinkingConfig["thinkingBudget"] = budgetTokens;
}
generationConfig["thinkingConfig"] = thinkingConfig;
request["generationConfig"] = generationConfig;
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings();
if (isThinkingEnabled) {
applyThinkingMode(qrSettings);
} else {
applyModelParams(qrSettings);
}
} else {
const auto &chatSettings = Settings::chatAssistantSettings();
if (isThinkingEnabled) {
applyThinkingMode(chatSettings);
} else {
applyModelParams(chatSettings);
}
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to Google AI request").arg(toolsDefinitions.size()));
}
}
}
QFuture<QList<QString>> GoogleAIProvider::getInstalledModels(const QString &baseUrl)
{
m_client->setUrl(baseUrl);
m_client->setApiKey(apiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID GoogleAIProvider::providerID() const
{
return PluginLLMCore::ProviderID::GoogleAI;
}
PluginLLMCore::ProviderCapabilities GoogleAIProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking
| PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing;
}
LLMQore::RequestID GoogleAIProvider::sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint)
{
QJsonObject cleaned = payload;
if (cleaned.contains("model")) {
m_client->setModel(cleaned["model"].toString());
cleaned.remove("model");
}
cleaned.remove("stream");
return PluginLLMCore::Provider::sendRequest(url, cleaned, endpoint);
}
::LLMQore::BaseClient *GoogleAIProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -1,42 +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 <pluginllmcore/Provider.hpp>
#include <LLMQore/GoogleAIClient.hpp>
namespace QodeAssist::Providers {
class GoogleAIProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit GoogleAIProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
LLMQore::RequestID sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint) override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
private:
::LLMQore::GoogleAIClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,122 +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 "LMStudioProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "providers/ProviderUrlUtils.hpp"
#include "tools/ToolsRegistration.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
LMStudioProvider::LMStudioProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString LMStudioProvider::name() const
{
return "LM Studio (Chat Completions)";
}
QString LMStudioProvider::apiKey() const
{
return {};
}
QString LMStudioProvider::url() const
{
return "http://localhost:1234";
}
QFuture<QList<QString>> LMStudioProvider::getInstalledModels(const QString &url)
{
m_client->setUrl(ensureOpenAIV1Base(url));
m_client->setApiKey(apiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID LMStudioProvider::providerID() const
{
return PluginLLMCore::ProviderID::LMStudio;
}
PluginLLMCore::ProviderCapabilities LMStudioProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing
| PluginLLMCore::ProviderCapability::Thinking;
}
void LMStudioProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to LMStudio request").arg(toolsDefinitions.size()));
}
}
}
LLMQore::RequestID LMStudioProvider::sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint)
{
return PluginLLMCore::Provider::sendRequest(
QUrl(ensureOpenAIV1Base(url.toString())), payload, endpoint);
}
::LLMQore::BaseClient *LMStudioProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -1,41 +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 <LLMQore/OpenAIClient.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist::Providers {
class LMStudioProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit LMStudioProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
LLMQore::RequestID sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint) override;
private:
::LLMQore::OpenAIClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,146 +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 "LMStudioResponsesProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "logger/Logger.hpp"
#include "providers/ProviderUrlUtils.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "tools/ToolsRegistration.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
LMStudioResponsesProvider::LMStudioResponsesProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::OpenAIResponsesClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString LMStudioResponsesProvider::name() const
{
return "LM Studio (Responses API)";
}
QString LMStudioResponsesProvider::apiKey() const
{
return {};
}
QString LMStudioResponsesProvider::url() const
{
return "http://localhost:1234";
}
void LMStudioResponsesProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_output_tokens"] = settings.maxTokens();
if (settings.useTopP()) {
request["top_p"] = settings.topP();
}
};
auto applyThinkingMode = [&request](const auto &settings) {
QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower();
if (effortStr.isEmpty()) {
effortStr = "medium";
}
QJsonObject reasoning;
reasoning["effort"] = effortStr;
request["reasoning"] = reasoning;
request["max_output_tokens"] = settings.thinkingMaxTokens();
request["store"] = true;
QJsonArray include;
include.append("reasoning.encrypted_content");
request["include"] = include;
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings);
if (isThinkingEnabled) {
applyThinkingMode(qrSettings);
}
} else {
const auto &chatSettings = Settings::chatAssistantSettings();
applyModelParams(chatSettings);
if (isThinkingEnabled) {
applyThinkingMode(chatSettings);
}
}
if (isToolsEnabled) {
const auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to LM Studio Responses request")
.arg(toolsDefinitions.size()));
}
}
request["stream"] = true;
}
QFuture<QList<QString>> LMStudioResponsesProvider::getInstalledModels(const QString &baseUrl)
{
m_client->setUrl(ensureOpenAIV1Base(baseUrl));
m_client->setApiKey(apiKey());
return m_client->listModels();
}
LLMQore::RequestID LMStudioResponsesProvider::sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint)
{
return PluginLLMCore::Provider::sendRequest(
QUrl(ensureOpenAIV1Base(url.toString())), payload, endpoint);
}
PluginLLMCore::ProviderID LMStudioResponsesProvider::providerID() const
{
return PluginLLMCore::ProviderID::OpenAIResponses;
}
PluginLLMCore::ProviderCapabilities LMStudioResponsesProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking
| PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing;
}
::LLMQore::BaseClient *LMStudioResponsesProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -1,41 +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 <LLMQore/OpenAIResponsesClient.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist::Providers {
class LMStudioResponsesProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit LMStudioResponsesProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
LLMQore::RequestID sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint) override;
private:
::LLMQore::OpenAIResponsesClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,127 +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 "LlamaCppProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "tools/ToolsRegistration.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
LlamaCppProvider::LlamaCppProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::LlamaCppClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString LlamaCppProvider::name() const
{
return "llama.cpp";
}
QString LlamaCppProvider::apiKey() const
{
return Settings::providerSettings().llamaCppApiKey();
}
QString LlamaCppProvider::url() const
{
return "http://localhost:8080";
}
void LlamaCppProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
auto applyThinkingMode = [&request]() {
QJsonObject chatTemplateKwargs = request["chat_template_kwargs"].toObject();
chatTemplateKwargs["enable_thinking"] = true;
request["chat_template_kwargs"] = chatTemplateKwargs;
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings());
if (isThinkingEnabled) {
applyThinkingMode();
LOG_MESSAGE(QString("LlamaCppProvider: Thinking mode enabled for QuickRefactoring"));
}
} else {
applyModelParams(Settings::chatAssistantSettings());
if (isThinkingEnabled) {
applyThinkingMode();
LOG_MESSAGE(QString("LlamaCppProvider: Thinking mode enabled for Chat"));
}
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to llama.cpp request").arg(toolsDefinitions.size()));
}
}
}
QFuture<QList<QString>> LlamaCppProvider::getInstalledModels(const QString &baseUrl)
{
m_client->setUrl(baseUrl);
m_client->setApiKey(Settings::providerSettings().llamaCppApiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID LlamaCppProvider::providerID() const
{
return PluginLLMCore::ProviderID::LlamaCpp;
}
PluginLLMCore::ProviderCapabilities LlamaCppProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking
| PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing;
}
::LLMQore::BaseClient *LlamaCppProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -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 <pluginllmcore/Provider.hpp>
#include <LLMQore/LlamaCppClient.hpp>
namespace QodeAssist::Providers {
class LlamaCppProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit LlamaCppProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
private:
::LLMQore::LlamaCppClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,113 +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 "MistralAIProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "tools/ToolsRegistration.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
MistralAIProvider::MistralAIProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::MistralClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString MistralAIProvider::name() const
{
return "Mistral AI";
}
QString MistralAIProvider::apiKey() const
{
return Settings::providerSettings().mistralAiApiKey();
}
QString MistralAIProvider::url() const
{
return "https://api.mistral.ai";
}
QFuture<QList<QString>> MistralAIProvider::getInstalledModels(const QString &url)
{
m_client->setUrl(url);
m_client->setApiKey(apiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID MistralAIProvider::providerID() const
{
return PluginLLMCore::ProviderID::MistralAI;
}
PluginLLMCore::ProviderCapabilities MistralAIProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing
| PluginLLMCore::ProviderCapability::Thinking;
}
void MistralAIProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(QString("Added %1 tools to Mistral request").arg(toolsDefinitions.size()));
}
}
}
::LLMQore::BaseClient *MistralAIProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -1,38 +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 <LLMQore/MistralClient.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist::Providers {
class MistralAIProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit MistralAIProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
private:
::LLMQore::MistralClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,126 +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 "OllamaCompatProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "tools/ToolsRegistration.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
OllamaCompatProvider::OllamaCompatProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString OllamaCompatProvider::name() const
{
return "Ollama (OpenAI-compatible)";
}
QString OllamaCompatProvider::apiKey() const
{
return Settings::providerSettings().ollamaBasicAuthApiKey();
}
QString OllamaCompatProvider::url() const
{
return "http://localhost:11434";
}
void OllamaCompatProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(
QString("Added %1 tools to OllamaCompat request").arg(toolsDefinitions.size()));
}
}
}
QFuture<QList<QString>> OllamaCompatProvider::getInstalledModels(const QString &baseUrl)
{
QString url = baseUrl;
if (!url.endsWith(QStringLiteral("/v1")))
url += QStringLiteral("/v1");
m_client->setUrl(url);
m_client->setApiKey(apiKey());
return m_client->listModels();
}
LLMQore::RequestID OllamaCompatProvider::sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint)
{
const QString effectiveEndpoint
= endpoint.isEmpty() ? QStringLiteral("/v1/chat/completions") : endpoint;
return PluginLLMCore::Provider::sendRequest(url, payload, effectiveEndpoint);
}
PluginLLMCore::ProviderID OllamaCompatProvider::providerID() const
{
return PluginLLMCore::ProviderID::OpenAICompatible;
}
PluginLLMCore::ProviderCapabilities OllamaCompatProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing
| PluginLLMCore::ProviderCapability::Thinking;
}
::LLMQore::BaseClient *OllamaCompatProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -1,41 +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 <LLMQore/OpenAIClient.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist::Providers {
class OllamaCompatProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit OllamaCompatProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
LLMQore::RequestID sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint) override;
private:
::LLMQore::OpenAIClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,139 +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 "OllamaProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist::Providers {
OllamaProvider::OllamaProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::OllamaClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString OllamaProvider::name() const
{
return "Ollama (Native)";
}
QString OllamaProvider::apiKey() const
{
return Settings::providerSettings().ollamaBasicAuthApiKey();
}
QString OllamaProvider::url() const
{
return "http://localhost:11434";
}
void OllamaProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applySettings = [&request](const auto &settings) {
QJsonObject options;
options["num_predict"] = settings.maxTokens();
options["temperature"] = settings.temperature();
options["stop"] = request.take("stop");
if (settings.useTopP())
options["top_p"] = settings.topP();
if (settings.useTopK())
options["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
options["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
options["presence_penalty"] = settings.presencePenalty();
request["options"] = options;
request["keep_alive"] = settings.ollamaLivetime();
};
auto applyThinkingMode = [&request]() {
request["enable_thinking"] = true;
QJsonObject options = request["options"].toObject();
options["temperature"] = 1.0;
request["options"] = options;
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applySettings(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings();
applySettings(qrSettings);
if (isThinkingEnabled) {
applyThinkingMode();
LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for QuickRefactoring"));
}
} else {
const auto &chatSettings = Settings::chatAssistantSettings();
applySettings(chatSettings);
if (isThinkingEnabled) {
applyThinkingMode();
LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for Chat"));
}
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(
QString("OllamaProvider: Added %1 tools to request").arg(toolsDefinitions.size()));
}
}
}
QFuture<QList<QString>> OllamaProvider::getInstalledModels(const QString &baseUrl)
{
m_client->setUrl(baseUrl);
m_client->setApiKey(Settings::providerSettings().ollamaBasicAuthApiKey());
return m_client->listModels();
}
PluginLLMCore::ProviderID OllamaProvider::providerID() const
{
return PluginLLMCore::ProviderID::Ollama;
}
PluginLLMCore::ProviderCapabilities OllamaProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking
| PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::ModelListing;
}
::LLMQore::BaseClient *OllamaProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

View File

@@ -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 <pluginllmcore/Provider.hpp>
#include <LLMQore/OllamaClient.hpp>
namespace QodeAssist::Providers {
class OllamaProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit OllamaProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
PluginLLMCore::ProviderID providerID() const override;
PluginLLMCore::ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
QString apiKey() const override;
private:
::LLMQore::OllamaClient *m_client;
};
} // namespace QodeAssist::Providers

View File

@@ -1,111 +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 "OpenAICompatProvider.hpp"
#include <LLMQore/ToolsManager.hpp>
#include "tools/ToolsRegistration.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Providers {
OpenAICompatProvider::OpenAICompatProvider(QObject *parent)
: PluginLLMCore::Provider(parent)
, m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this))
{
Tools::registerQodeAssistTools(m_client->tools());
}
QString OpenAICompatProvider::name() const
{
return "OpenAI Compatible";
}
QString OpenAICompatProvider::apiKey() const
{
return Settings::providerSettings().openAiCompatApiKey();
}
QString OpenAICompatProvider::url() const
{
return "http://localhost:1234/v1";
}
void OpenAICompatProvider::prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
if (type == PluginLLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
if (isToolsEnabled) {
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
if (!toolsDefinitions.isEmpty()) {
request["tools"] = toolsDefinitions;
LOG_MESSAGE(
QString("Added %1 tools to OpenAICompat request").arg(toolsDefinitions.size()));
}
}
}
QFuture<QList<QString>> OpenAICompatProvider::getInstalledModels(const QString &)
{
return QtFuture::makeReadyFuture(QList<QString>{});
}
PluginLLMCore::ProviderID OpenAICompatProvider::providerID() const
{
return PluginLLMCore::ProviderID::OpenAICompatible;
}
PluginLLMCore::ProviderCapabilities OpenAICompatProvider::capabilities() const
{
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image
| PluginLLMCore::ProviderCapability::Thinking;
}
::LLMQore::BaseClient *OpenAICompatProvider::client() const
{
return m_client;
}
} // namespace QodeAssist::Providers

Some files were not shown because too many files have changed in this diff Show More