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
248 changed files with 7204 additions and 11067 deletions

View File

@@ -45,17 +45,13 @@ jobs:
cc: "clang", cxx: "clang++"
}
qt_config:
# - {
# qt_version: "6.10.1",
# qt_creator_version: "18.0.2"
# }
# - {
# qt_version: "6.10.3",
# qt_creator_version: "19.0.2"
# }
- {
qt_version: "6.11.1",
qt_creator_version: "20.0.0-rc1"
qt_version: "6.10.1",
qt_creator_version: "18.0.2"
}
- {
qt_version: "6.10.3",
qt_creator_version: "19.0.2"
}
steps:
@@ -114,14 +110,10 @@ jobs:
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
string(REPLACE "." "" qt_version_dotless "${qt_version}")
set(qt_repo_dir "qt6_${qt_version_dotless}")
if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_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")
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
else()
@@ -135,9 +127,7 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64")
endif()
set(qt_dir_prefix "${qt_version}/gcc_64")
if (qt_version VERSION_GREATER_EQUAL "6.11.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")
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
else()
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
@@ -153,7 +143,7 @@ jobs:
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(READ ./Updates.xml updates_xml)
@@ -180,11 +170,7 @@ jobs:
)
endforeach()
set(qt_addon_packages qt5compat qtshadertools)
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
list(APPEND qt_addon_packages qttasktree)
endif()
foreach(package ${qt_addon_packages})
foreach(package qt5compat qtshadertools)
downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z
@@ -250,7 +236,7 @@ jobs:
endif()
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")
set(build_plugin_py "${dir}/build_plugin.py")
break()

View File

@@ -1,10 +1,8 @@
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
"Enable experimental features" OFF)
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
project(QodeAssist)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
@@ -42,7 +40,6 @@ add_definitions(
add_subdirectory(sources)
add_subdirectory(logger)
add_subdirectory(pluginllmcore)
add_subdirectory(settings)
add_subdirectory(UIControls)
add_subdirectory(ChatView)
@@ -51,6 +48,11 @@ if(GTest_FOUND)
add_subdirectory(test)
endif()
option(QODEASSIST_BUILD_BENCH "Build the standalone agent bench CLI" ON)
if(QODEASSIST_BUILD_BENCH)
add_subdirectory(bench)
endif()
add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS
QtCreator::Core
@@ -69,7 +71,6 @@ add_qtc_plugin(QodeAssist
QtCreator::Utils
QtCreator::CPlusPlus
LLMQore
PluginLLMCore
ProvidersConfig
Agents
Skills
@@ -83,42 +84,6 @@ add_qtc_plugin(QodeAssist
QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp
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
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
@@ -130,7 +95,6 @@ add_qtc_plugin(QodeAssist
chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
@@ -170,10 +134,7 @@ add_qtc_plugin(QodeAssist
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
)
if(QODEASSIST_EXPERIMENTAL)
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
endif()
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
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
ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp
AgentRoleController.hpp AgentRoleController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
ChatAgentController.hpp ChatAgentController.cpp
FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp
@@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView
Qt::Network
QtCreator::Core
QtCreator::Utils
PluginLLMCore
QodeAssistSettings
Context
QodeAssistUIControlsplugin
QodeAssistLogger
LLMQore
Skills
Agents
Session
)
target_include_directories(QodeAssistChatView

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 <memory>
#include <LLMQore/BaseClient.hpp>
#include "ChatModel.hpp"
#include <LLMQore/ContentBlocks.hpp>
#include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
@@ -25,7 +32,18 @@ ChatCompressor::ChatCompressor(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) {
emit compressionFailed(tr("Compression already in progress"));
@@ -37,49 +55,78 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
return;
}
if (!chatModel || chatModel->rowCount() == 0) {
if (!sourceHistory || sourceHistory->isEmpty()) {
emit compressionFailed(tr("Chat is empty, nothing to compress"));
return;
}
auto providerName = Settings::generalSettings().caProvider();
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
if (!m_sessionManager) {
emit compressionFailed(tr("Chat session manager is not available"));
return;
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
QString sessionError;
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
if (!session) {
emit compressionFailed(
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
return;
}
if (!promptTemplate) {
emit compressionFailed(tr("No template available"));
auto *client = session->client();
if (!client) {
m_sessionManager->removeSession(session);
emit compressionFailed(tr("Chat agent has no live client"));
return;
}
m_isCompressing = true;
m_chatModel = chatModel;
m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear();
m_session = session;
emit compressionStarted();
connectProviderSignals();
session->systemPrompt()->setLayer(
QStringLiteral("compression"),
QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of "
"discussion."));
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
auto *history = session->history();
for (const auto &msg : 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();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
m_provider->client()->setTransferTimeout(
connect(
session, &Session::finished, this,
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
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));
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));
}
@@ -94,44 +141,38 @@ void ChatCompressor::cancelCompression()
return;
LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState();
emit compressionFailed(tr("Compression cancelled"));
}
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
void ChatCompressor::onCompressionFinished(const QString &requestId)
{
if (!m_isCompressing || requestId != m_currentRequestId)
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)
{
Q_UNUSED(fullText)
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
if (!m_isCompressing || requestId != m_currentRequestId)
return;
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
const QString sourcePath = m_originalChatPath;
LOG_MESSAGE(
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
cleanupState();
QString compressedPath = createCompressedChatPath(m_originalChatPath);
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
handleCompressionError(tr("Failed to save compressed chat"));
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
emit compressionFailed(tr("Failed to save compressed chat"));
return;
}
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath);
}
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
{
if (!m_isCompressing || requestId != m_currentRequestId)
return;
@@ -168,39 +209,6 @@ QString ChatCompressor::buildCompressionPrompt() const
"Create the summary now:");
}
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary)
{
@@ -224,11 +232,11 @@ bool ChatCompressor::createCompressedChatFile(
QJsonObject summaryMessage;
summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
summaryMessage["isRedacted"] = false;
summaryMessage["attachments"] = QJsonArray();
summaryMessage["images"] = QJsonArray();
QJsonObject textBlock;
textBlock["type"] = "text";
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["blocks"] = QJsonArray{textBlock};
root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath;
@@ -247,49 +255,17 @@ bool ChatCompressor::createCompressedChatFile(
return true;
}
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals()
{
for (const auto &connection : std::as_const(m_connections))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState()
{
disconnectAllSignals();
Session *session = m_session;
m_isCompressing = false;
m_currentRequestId.clear();
m_originalChatPath.clear();
m_accumulatedSummary.clear();
m_chatModel = nullptr;
m_provider = nullptr;
m_session = nullptr;
if (session && m_sessionManager)
m_sessionManager->release(session);
}
} // namespace QodeAssist::Chat

View File

@@ -4,20 +4,19 @@
#pragma once
#include <QJsonObject>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QString>
namespace QodeAssist::PluginLLMCore {
class Provider;
class PromptTemplate;
} // namespace QodeAssist::PluginLLMCore
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject
{
Q_OBJECT
@@ -25,7 +24,10 @@ class ChatCompressor : public QObject
public:
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;
void cancelCompression();
@@ -35,30 +37,23 @@ signals:
void compressionCompleted(const QString &compressedChatPath);
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:
void onCompressionFinished(const QString &requestId);
void onCompressionFailed(const QString &requestId, const QString &error);
QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const;
bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState();
void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false;
QString m_currentRequestId;
QString m_originalChatPath;
QString m_accumulatedSummary;
PluginLLMCore::Provider *m_provider = nullptr;
ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections;
QPointer<SessionManager> m_sessionManager;
QString m_activeAgent;
QPointer<Session> m_session;
};
} // 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/projectmanager.h>
#include "ChatModel.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "Logger.hpp"
#include "ProjectSettings.hpp"
namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
, m_history(history)
{}
QString ChatHistoryStore::historyDir() const
@@ -52,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
{
QString shortMessage;
if (m_chatModel->rowCount() > 0) {
QString firstMessage
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
shortMessage = firstMessage.split('\n').first().simplified().left(30);
if (m_history) {
for (const auto &message : m_history->messages()) {
if (message.role() != Message::Role::User)
continue;
if (shortMessage.isEmpty()) {
QVariantList images
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
if (!images.isEmpty()) {
shortMessage = "image_chat";
const QString text = message.text();
if (!text.trimmed().isEmpty()) {
shortMessage = text.split('\n').first().simplified().left(30);
} else {
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
{
return ChatSerializer::saveToFile(m_chatModel, filePath);
return ChatSerializer::saveToFile(m_history, filePath);
}
SerializationResult ChatHistoryStore::load(const QString &filePath) const
{
return ChatSerializer::loadFromFile(m_chatModel, filePath);
return ChatSerializer::loadFromFile(m_history, filePath);
}
void ChatHistoryStore::showSaveDialog()

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,16 @@
#include "MessagePart.hpp"
#include <QAbstractListModel>
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration>
#include "context/ContentFile.hpp"
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat {
@@ -43,81 +48,19 @@ public:
};
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);
void setHistory(ConversationHistory *history);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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 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 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(
const QString &messageId,
int promptTokens,
@@ -129,10 +72,7 @@ public:
int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const;
int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
void setChatFilePath(const QString &filePath);
QString chatFilePath() const;
@@ -141,18 +81,60 @@ signals:
void sessionUsageChanged();
private slots:
void onFileEditApplied(const QString &editId);
void onFileEditRejected(const QString &editId);
void onFileEditArchived(const QString &editId);
void onHistoryMessageAdded(int index);
void onHistoryMessageUpdated(int index);
void onHistoryCleared();
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
QVector<Message> m_messages;
bool m_loadingFromHistory = false;
struct AttachmentRef
{
QString fileName;
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;
};
} // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

View File

@@ -28,9 +28,16 @@
#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 "ChatConfigurationController.hpp"
#include "ChatCompressor.hpp"
#include "ChatHistoryStore.hpp"
#include "FileEditController.hpp"
@@ -38,10 +45,8 @@
#include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp"
#include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp"
#include "SkillsSettings.hpp"
#include "sources/skills/SkillsManager.hpp"
@@ -73,19 +78,20 @@ QKeySequence sendMessageKeySequence()
ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent)
, m_history(new QodeAssist::ConversationHistory(this))
, m_chatModel(new ChatModel(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_clientInterface(new ClientInterface(m_chatModel, this))
, m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this))
, m_agentRoleController(new AgentRoleController(this))
, m_configurationController(new ChatConfigurationController(this))
, m_fileEditController(new FileEditController(m_chatModel, this))
, m_tokenCounter(
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
, m_agentController(new ChatAgentController(this))
, m_fileEditController(new FileEditController(this))
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_history, this))
{
m_chatModel->setHistory(m_history);
m_clientInterface->setHistory(m_history);
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(
&Settings::chatAssistantSettings().linkOpenFiles,
@@ -109,22 +115,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
},
Qt::QueuedConnection);
auto &settings = Settings::generalSettings();
connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(
m_configurationController,
&ChatConfigurationController::availableConfigurationsChanged,
this,
&ChatRootView::availableConfigurationsChanged);
connect(
m_configurationController,
&ChatConfigurationController::currentConfigurationChanged,
this,
&ChatRootView::currentConfigurationChanged);
connect(
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
@@ -171,20 +161,30 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::inputTokensCountChanged);
connect(
m_agentRoleController,
&AgentRoleController::availableRolesChanged,
m_agentController,
&ChatAgentController::availableAgentsChanged,
this,
&ChatRootView::availableAgentRolesChanged);
&ChatRootView::availableChatAgentsChanged);
connect(
m_agentRoleController,
&AgentRoleController::currentRoleChanged,
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::currentAgentRoleChanged);
&ChatRootView::currentChatAgentChanged);
connect(
m_agentRoleController,
&AgentRoleController::baseSystemPromptChanged,
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::baseSystemPromptChanged);
&ChatRootView::isThinkingSupportChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useToolsChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useThinkingChanged);
auto editors = Core::EditorManager::instance();
@@ -266,14 +266,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded,
@@ -298,12 +290,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::useThinkingChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
m_lastErrorMessage = error;
emit lastErrorMessageChanged();
@@ -324,7 +310,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) {
PendingSend p = 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) {
PendingSend p = 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;
}
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 results;
@@ -380,7 +445,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
if (!manager || !Settings::skillsSettings().enableSkills())
return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
auto *project = ProjectExplorer::ProjectManager::startupProject();
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
@@ -416,21 +481,17 @@ void ChatRootView::sendMessage(const QString &message)
{
const QStringList attachments = m_attachmentFiles;
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;
dispatchSend(message, attachments, linkedFiles, tools, thinking);
dispatchSend(message, attachments, linkedFiles);
}
bool ChatRootView::deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
const QStringList &linkedFiles)
{
auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress())
@@ -456,7 +517,7 @@ bool ChatRootView::deferSendForAutoCompress(
.arg(inputTokens)
.arg(threshold));
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
m_pendingSend = {message, attachments, linkedFiles, true};
compressCurrentChat();
return true;
}
@@ -464,9 +525,7 @@ bool ChatRootView::deferSendForAutoCompress(
void ChatRootView::dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
const QStringList &linkedFiles)
{
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments);
@@ -481,8 +540,14 @@ void ChatRootView::dispatchSend(
m_tokenCounter->recordSent();
if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
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();
clearAttachmentFiles();
@@ -527,12 +592,6 @@ void ChatRootView::clearMessages()
clearLinkedFiles();
}
QString ChatRootView::currentTemplate() const
{
auto &settings = Settings::generalSettings();
return settings.caModel();
}
void ChatRootView::saveHistory(const QString &filePath)
{
if (filePath != m_recentFilePath) {
@@ -821,25 +880,6 @@ void ChatRootView::openChatHistoryFolder()
m_historyStore->openHistoryFolder();
}
void ChatRootView::openRulesFolder()
{
auto project = ProjectExplorer::ProjectManager::startupProject();
if (!project) {
return;
}
QString projectPath = project->projectDirectory().toFSPathString();
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
QDir dir(rulesPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void ChatRootView::openSettings()
{
QMetaObject::invokeMethod(
@@ -890,13 +930,12 @@ QString ChatRootView::chatTitle() const
QString ChatRootView::computeChatTitle() const
{
if (!m_chatModel)
if (!m_history)
return {};
const auto history = m_chatModel->getChatHistory();
for (const auto &msg : history) {
if (msg.role != ChatModel::User)
for (const auto &msg : m_history->messages()) {
if (msg.role() != Message::Role::User)
continue;
const QString content = msg.content.trimmed();
const QString content = msg.text().trimmed();
if (content.isEmpty())
continue;
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)
{
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString()));
return true;
@@ -1120,71 +1155,14 @@ QString ChatRootView::lastErrorMessage() const
return m_lastErrorMessage;
}
QVariantList ChatRootView::activeRules() const
{
return m_activeRules;
}
int ChatRootView::activeRulesCount() const
{
return m_activeRules.size();
}
QString ChatRootView::getRuleContent(int index)
{
if (index < 0 || index >= m_activeRules.size())
return QString();
return PluginLLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString());
}
void ChatRootView::refreshRules()
{
m_activeRules.clear();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (!project) {
emit activeRulesChanged();
emit activeRulesCountChanged();
return;
}
auto ruleFiles
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap;
ruleMap["filePath"] = ruleFile.filePath;
ruleMap["fileName"] = ruleFile.fileName;
ruleMap["category"] = ruleFile.category;
m_activeRules.append(ruleMap);
}
emit activeRulesChanged();
emit activeRulesCountChanged();
}
bool ChatRootView::useTools() const
{
return Settings::chatAssistantSettings().enableChatTools();
}
void ChatRootView::setUseTools(bool enabled)
{
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
return m_agentController->currentSupportsTools();
}
bool ChatRootView::useThinking() const
{
return Settings::chatAssistantSettings().enableThinkingMode();
}
void ChatRootView::setUseThinking(bool enabled)
{
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
return m_agentController->currentSupportsThinking();
}
void ChatRootView::applyFileEdit(const QString &editId)
@@ -1249,10 +1227,7 @@ QString ChatRootView::lastInfoMessage() const
bool ChatRootView::isThinkingSupport() const
{
auto providerName = Settings::generalSettings().caProvider();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
return m_agentController->currentSupportsThinking();
}
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
@@ -1273,66 +1248,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
return imageExtensions.contains(fileInfo.suffix().toLower());
}
void ChatRootView::loadAvailableConfigurations()
{
m_configurationController->loadAvailableConfigurations();
}
void ChatRootView::applyConfiguration(const QString &configName)
{
m_configurationController->applyConfiguration(configName);
}
QStringList ChatRootView::availableConfigurations() const
{
return m_configurationController->availableConfigurations();
}
QString ChatRootView::currentConfiguration() const
{
return m_configurationController->currentConfiguration();
}
void ChatRootView::loadAvailableAgentRoles()
{
m_agentRoleController->loadAvailableRoles();
}
void ChatRootView::applyAgentRole(const QString &roleName)
{
m_agentRoleController->applyRole(roleName);
}
QStringList ChatRootView::availableAgentRoles() const
{
return m_agentRoleController->availableRoles();
}
QString ChatRootView::currentAgentRole() const
{
return m_agentRoleController->currentRole();
}
QString ChatRootView::baseSystemPrompt() const
{
return m_agentRoleController->baseSystemPrompt();
}
QString ChatRootView::currentAgentRoleDescription() const
{
return m_agentRoleController->currentRoleDescription();
}
QString ChatRootView::currentAgentRoleSystemPrompt() const
{
return m_agentRoleController->currentRoleSystemPrompt();
}
void ChatRootView::openAgentRolesSettings()
{
m_agentRoleController->openSettings();
}
void ChatRootView::compressCurrentChat()
{
if (m_chatCompressor->isCompressing()) {
@@ -1349,7 +1264,20 @@ void ChatRootView::compressCurrentChat()
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()

View File

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

View File

@@ -5,7 +5,8 @@
#include "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QBuffer>
#include <memory>
#include <QDir>
#include <QFile>
#include <QFileInfo>
@@ -13,12 +14,57 @@
#include <QJsonDocument>
#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 {
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)) {
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)};
}
QJsonObject root = serializeChat(model, filePath);
QJsonDocument doc(root);
QJsonDocument doc(serializeChat(history));
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
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()};
}
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);
if (!file.open(QIODevice::ReadOnly)) {
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())};
}
QJsonObject root = doc.object();
QString version = root["version"].toString();
const QJsonObject root = doc.object();
const QString version = root["version"].toString();
if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)};
}
if (!deserializeChat(model, root, filePath)) {
return {false, "Failed to deserialize chat data"};
}
return {true, QString()};
if (version == VERSION)
return loadCurrent(history, root);
return loadLegacy(history, root);
}
QJsonObject ChatSerializer::serializeMessage(
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)
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message, chatFilePath));
}
for (const auto &message : history->messages())
messagesArray.append(MessageSerializer::toJson(message));
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
return root;
}
bool ChatSerializer::deserializeChat(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
history->clear();
for (const auto &messageValue : messagesArray) {
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
const QJsonArray messagesArray = root["messages"].toArray();
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) {
model->addMessage(
message.content,
message.role,
message.id,
message.attachments,
message.images,
message.isRedacted,
message.signature);
if (message.role == ChatModel::ChatRole::Tool) {
model->setToolMessageData(
message.id, message.toolName, message.toolArguments, message.toolResult);
const QJsonArray arr = root["messages"].toArray();
int i = 0;
while (i < arr.size()) {
const QJsonObject mj = arr[i].toObject();
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
if (role == LegacyRole::Tool) {
Message assistant(Message::Role::Assistant);
Message toolResults(Message::Role::User);
while (i < arr.size()
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
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)
@@ -236,18 +244,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
bool ChatSerializer::validateVersion(const QString &version)
{
if (version == VERSION) {
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;
return version == VERSION || version == "0.2" || version == "0.1";
}
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)

View File

@@ -4,11 +4,12 @@
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "ChatModel.hpp"
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat {
@@ -21,26 +22,26 @@ struct SerializationResult
class ChatSerializer
{
public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
static SerializationResult loadFromFile(ChatModel *model, 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);
static SerializationResult saveToFile(
const ConversationHistory *history, const QString &filePath);
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
// Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath);
static bool saveContentToStorage(
const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath);
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
private:
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 validateVersion(const QString &version);

View File

@@ -4,73 +4,104 @@
#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/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QImageReader>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QUuid>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <LLMQore/ToolsManager.hpp>
#include <ConversationHistory.hpp>
#include <ContextRenderer.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "tools/ReadOriginalHistoryTool.hpp"
#include "tools/TodoTool.hpp"
#include "tools/ToolsRegistration.hpp"
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "ProvidersManager.hpp"
#include "SkillsSettings.hpp"
#include "ToolsSettings.hpp"
#include <RulesLoader.hpp>
#include <context/ChangesManager.h>
#include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat {
ClientInterface::ClientInterface(
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
namespace {
struct StoredImage
{
QString fileName;
QString storedPath;
QString mediaType;
};
} // namespace
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
: QObject(parent)
, m_promptProvider(promptProvider)
, m_chatModel(chatModel)
, m_contextManager(new Context::ContextManager(this))
{}
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{
m_skillsManager = skillsManager;
}
ClientInterface::~ClientInterface()
{
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(
const QString &message,
const QList<QString> &attachments,
const QList<QString> &linkedFiles,
bool useTools,
bool useThinking)
const QList<QString> &linkedFiles)
{
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message");
@@ -78,19 +109,16 @@ void ClientInterface::sendMessage(
}
cancelRequest();
m_accumulatedResponses.clear();
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
QList<QString> imageFiles;
QList<QString> textFiles;
for (const QString &filePath : attachments) {
if (isImageFile(filePath)) {
if (isImageFile(filePath))
imageFiles.append(filePath);
} else {
else
textFiles.append(filePath);
}
}
QList<Context::ContentFile> storedAttachments;
@@ -112,24 +140,19 @@ void ClientInterface::sendMessage(
.arg(textFiles.size()));
}
QList<ChatModel::ImageAttachment> imageAttachments;
QList<StoredImage> storedImages;
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) {
QString base64Data = encodeImageToBase64(imagePath);
if (base64Data.isEmpty()) {
if (base64Data.isEmpty())
continue;
}
QString storedPath;
QFileInfo fileInfo(imagePath);
if (ChatSerializer::saveContentToStorage(
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
ChatModel::ImageAttachment imageAttachment;
imageAttachment.fileName = fileInfo.fileName();
imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment);
storedImages.append(
{fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)});
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
}
}
@@ -138,328 +161,287 @@ void ClientInterface::sendMessage(
.arg(imageFiles.size()));
}
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
auto &chatAssistantSettings = Settings::chatAssistantSettings();
auto providerName = Settings::generalSettings().caProvider();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
if (!m_sessionManager) {
const QString error = QStringLiteral("Chat session manager is not available");
LOG_MESSAGE(error);
emit errorOccurred(error);
return;
}
if (!m_history) {
const QString error = QStringLiteral("Chat history is not available");
LOG_MESSAGE(error);
emit errorOccurred(error);
return;
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
QString sessionError;
Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
if (!session) {
const QString error = sessionError.isEmpty()
? QStringLiteral("No chat agent selected")
: sessionError;
LOG_MESSAGE(error);
emit errorOccurred(error);
return;
}
PluginLLMCore::ContextData context;
const bool isToolsEnabled = useTools;
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
if (!lastRoleId.isEmpty()) {
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
systemPrompt += QString(
"\n# Project source root: %1"
"\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt
+= QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString());
}
}
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
}
} else {
systemPrompt += QString("\n# No active project in IDE");
}
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs = Settings::SkillsSettings::splitLines(
projectSettings.projectSkillDirs());
}
m_skillsManager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),
Settings::SkillsSettings::splitPaths(
Settings::skillsSettings().globalSkillRoots()),
projectSkillDirs);
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
if (!alwaysOnSkills.isEmpty())
systemPrompt += QString("\n\n") + alwaysOnSkills;
const QString skillsCatalog = m_skillsManager->catalogText();
if (!skillsCatalog.isEmpty())
systemPrompt += QString("\n\n") + skillsCatalog;
static const QRegularExpression skillCommand(
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
QStringList invokedSkillNames;
auto skillMatch = skillCommand.globalMatch(message);
while (skillMatch.hasNext()) {
const QString skillName = skillMatch.next().captured(1);
if (invokedSkillNames.contains(skillName))
continue;
const auto invokedSkill = m_skillsManager->findByName(skillName);
if (invokedSkill && !invokedSkill->body.isEmpty()) {
invokedSkillNames << skillName;
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
.arg(invokedSkill->name, invokedSkill->body);
}
}
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
auto *client = session->client();
if (!client) {
const QString error = QStringLiteral("Chat agent has no live client");
LOG_MESSAGE(error);
m_sessionManager->removeSession(session);
emit errorOccurred(error);
return;
}
const bool toolHistory = promptTemplate->supportsToolHistory();
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;
int toolCallMsgIdx = -1;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool) {
if (!toolHistory || msg.toolName.isEmpty()) {
continue;
}
const QString chatFilePath = m_chatFilePath;
session->setContentLoader([chatFilePath](const QString &storedPath) {
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
});
if (toolCallMsgIdx < 0) {
PluginLLMCore::Message assistantCall;
assistantCall.role = "assistant";
messages.append(assistantCall);
toolCallMsgIdx = messages.size() - 1;
}
PluginLLMCore::ToolCall call;
call.id = msg.id;
call.name = msg.toolName;
call.arguments = msg.toolArguments;
messages[toolCallMsgIdx].toolCalls.append(call);
PluginLLMCore::Message toolResult;
toolResult.role = "tool";
toolResult.toolCallId = msg.id;
toolResult.toolName = msg.toolName;
toolResult.content = msg.toolResult;
messages.append(toolResult);
continue;
}
toolCallMsgIdx = -1;
if (msg.role == ChatModel::ChatRole::FileEdit) {
continue;
}
PluginLLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
apiMessage.content += "\n\nAttached files:";
for (const auto &attachment : msg.attachments) {
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
.arg(attachment.filename, decodedContent);
}
}
}
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
}
}
messages.append(apiMessage);
}
if (!imageFiles.isEmpty()
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size())));
}
context.history = messages;
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
context,
PluginLLMCore::RequestType::Chat,
useTools,
useThinking);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
provider->client()->setTransferTimeout(
m_sessionManager->toolContributors().contribute(client->tools());
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
connect(
provider->client(),
&::LLMQore::BaseClient::chunkReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&ClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
const QString chatContext = buildChatContextLayer(message, linkedFiles);
if (!chatContext.isEmpty())
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}};
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
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 (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
if (!storedImages.isEmpty() && session->supportsImages()) {
for (const auto &image : storedImages) {
blocks.push_back(std::make_unique<StoredImageContent>(
image.fileName, image.storedPath, image.mediaType));
}
} 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);
}
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
provider->toolsManager()->tool("read_original_history"))) {
client->tools()->tool("read_original_history"))) {
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()
{
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
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();
if (m_history)
m_history->clear();
}
void ClientInterface::cancelRequest()
{
QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
const auto requests = m_activeRequests;
m_activeRequests.clear();
m_accumulatedResponses.clear();
m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
{
const auto message = response.trimmed();
if (!message.isEmpty()) {
QString messageId = request["id"].toString();
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
for (auto it = requests.begin(); it != requests.end(); ++it) {
Session *session = it.value().session;
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
LOG_MESSAGE("All chat requests cancelled and state cleared");
}
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);
}
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const
{
return m_contextManager;
}
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
{
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();
}
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue;
}
PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
apiImages.append(apiImage);
}
return apiImages;
}
void ClientInterface::setChatFilePath(const QString &filePath)
{
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}
m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath);
}

View File

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

View File

@@ -10,15 +10,13 @@
#include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h>
#include "ChatModel.hpp"
#include "Logger.hpp"
#include "context/ChangesManager.h"
namespace QodeAssist::Chat {
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
FileEditController::FileEditController(QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
{
auto &changes = Context::ChangesManager::instance();
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));
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully"));
updateFileEditStatus(editId, "applied");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
@@ -95,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected"));
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
@@ -110,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully"));
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
@@ -163,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
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()
{
if (m_currentRequestId.isEmpty()) {
@@ -223,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
: 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();
}
@@ -255,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
: 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();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,11 +23,9 @@ Rectangle {
property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
property alias agentSelector: agentSelectorId
property alias roleSelector: roleSelectorId
property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ?
@@ -134,7 +132,7 @@ Rectangle {
}
QoAComboBox {
id: configSelectorId
id: agentSelectorId
implicitHeight: 25
@@ -142,14 +140,14 @@ Rectangle {
currentIndex: 0
QoAToolTip {
visible: configSelectorId.hovered
visible: agentSelectorId.hovered
delay: 250
text: qsTr("Switch saved AI configuration")
text: qsTr("Select chat agent (provider and model come from the agent)")
}
}
QoAComboBox {
id: roleSelector
id: roleSelectorId
implicitHeight: 25
@@ -157,9 +155,9 @@ Rectangle {
currentIndex: 0
QoAToolTip {
visible: roleSelector.hovered
visible: roleSelectorId.hovered
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 {
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 {
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 <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 "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include <pluginllmcore/RulesLoader.hpp>
#include "sources/settings/PipelinesConfig.hpp"
namespace QodeAssist {
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::IPromptProvider *promptProvider,
AgentFactory &agentFactory,
SessionManager &sessionManager,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_agentFactory(agentFactory)
, m_sessionManager(sessionManager)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
@@ -51,58 +71,56 @@ void LLMClientInterface::startImpl()
emit started();
}
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
void LLMClientInterface::onCompletionFinished(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
sendCompletionToClient(fullText, ctx.originalRequest, true);
QString fullText;
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);
m_performanceLogger.endTimeMeasurement(requestId);
sendCompletionToClient(fullText, originalRequest, true);
finishRequest(requestId);
}
void LLMClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
finishRequest(requestId);
}
void LLMClientInterface::finishRequest(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
if (session)
m_sessionManager.release(session);
}
void LLMClientInterface::sendData(const QByteArray &data)
@@ -135,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest()
{
QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
const auto requests = m_activeRequests;
m_activeRequests.clear();
for (auto it = requests.begin(); it != requests.end(); ++it) {
m_performanceLogger.endTimeMeasurement(it.key());
if (Session *session = it.value().session)
m_sessionManager.release(session);
}
LOG_MESSAGE("All requests cancelled and state cleared");
}
@@ -237,133 +244,87 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return;
}
auto updatedContext = prepareContext(request, documentInfo);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
const QString agentName = pickCompletionAgent(filePath);
if (agentName.isEmpty()) {
QString error = QString("No code completion agent matches: %1").arg(filePath);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
QString sessionError;
Session *session = m_sessionManager.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 error = QString("No template found with name: %1").arg(templateName);
QString editorContext;
if (context.fileContext.has_value())
editorContext.append(context.fileContext.value());
if (m_completeSettings.useOpenFilesContext())
editorContext.append(m_contextManager->openedFilesContext({filePath}));
if (!editorContext.isEmpty())
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);
sendErrorResponse(request, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<PluginLLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
PluginLLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_activeRequests[requestId] = {request, session};
m_performanceLogger.startTimeMeasurement(requestId);
}
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
{
const QStringList roster = Settings::PipelinesConfig::load().rosters.codeCompletion;
if (roster.isEmpty())
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
}
QString LLMClientInterface::requestIdForSession(Session *session) const
{
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().session == session)
return it.key();
}
return {};
}
Templates::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{
QJsonObject params = request["params"].toObject();
@@ -377,14 +338,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
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
{
return m_contextManager;
@@ -393,15 +346,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -216,9 +216,9 @@ For optimal coding assistance, we recommend using these top-tier models:
### 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
- **[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`
## 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
- **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
@@ -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.
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.
@@ -580,6 +580,10 @@ cmake --build .
## 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
- **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
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
IProjectScanner.hpp
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
ContentFile.hpp
DocumentReaderQtCreator.hpp
IDocumentReader.hpp
@@ -21,7 +23,7 @@ target_link_libraries(Context
QtCreator::Utils
QtCreator::ProjectExplorer
PRIVATE
PluginLLMCore
Common
QodeAssistSettings
)

View File

@@ -6,25 +6,24 @@
#include <QFile>
#include <QFileInfo>
#include <QJsonObject>
#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 "ProjectScannerQtCreator.hpp"
namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
, m_ignoreManager(new IgnoreManager(this))
: ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
{}
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
{
QFile file(filePath);
@@ -37,7 +36,7 @@ QString ContextManager::readFile(const QString &filePath) const
QTextStream in(&file);
QString content = in.readAll();
file.close();
return content;
}
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
{
QList<ContentFile> files;
for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(path));
if (project && m_ignoreManager->shouldIgnore(path, project)) {
if (m_scanner->shouldIgnore(path)) {
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
continue;
}
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
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 contentFile;
@@ -100,77 +76,26 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{
const auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
Q_UNUSED(documentInfo)
return false;
}
QList<QPair<QString, QString>> ContextManager::openedFiles(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 ContextManager::openedFilesContext(const QStringList &excludeFiles) const
{
QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
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();
for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
context += QString("File: %1\n").arg(file.filePath);
context += file.content;
context += "\n";
}
return context;
}
IgnoreManager *ContextManager::ignoreManager() const
bool ContextManager::shouldIgnore(const QString &filePath) const
{
return m_ignoreManager;
return m_scanner->shouldIgnore(filePath);
}
} // namespace QodeAssist::Context

View File

@@ -4,18 +4,16 @@
#pragma once
#include <memory>
#include <QObject>
#include <QString>
#include "ContentFile.hpp"
#include "IContextManager.hpp"
#include "IgnoreManager.hpp"
#include "IProjectScanner.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class ContextManager : public QObject, public IContextManager
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
public:
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;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(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:
IgnoreManager *m_ignoreManager;
std::unique_ptr<IProjectScanner> m_scanner;
};
} // namespace QodeAssist::Context

View File

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

View File

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

View File

@@ -11,10 +11,6 @@
#include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IContextManager
@@ -24,7 +20,6 @@ public:
virtual QString readFile(const QString &filePath) 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 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
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
- [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
- [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
- **Selection Markers**: `<selection_start>` and `<selection_end>` tags
- **Your Instructions**: Built-in, custom, or typed
- **Project Rules**: If configured (see [Project Rules](project-rules.md))
### Context Configuration
@@ -270,7 +269,6 @@ Fully local setup for offline or secure environments.
## Related Documentation
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
- [File Context](file-context.md) - Attaching files to chat context
- [Ignoring Files](ignoring-files.md) - Exclude files from AI context
- [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 <QTimer>
#include <LLMQore/ToolsManager.hpp>
#include <logger/Logger.hpp>
#include <pluginllmcore/Provider.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <settings/McpSettings.hpp>
namespace QodeAssist::Mcp {
@@ -176,18 +175,14 @@ QList<McpServerConnection *> McpClientsManager::connections() const
return m_connections;
}
QList<PluginLLMCore::Provider *> McpClientsManager::toolsCapableProviders() const
void McpClientsManager::registerToolsOn(::LLMQore::ToolsManager *tools) const
{
QList<PluginLLMCore::Provider *> out;
auto &pm = PluginLLMCore::ProvidersManager::instance();
for (const QString &name : pm.providersNames()) {
auto *p = pm.getProviderByName(name);
if (!p)
continue;
if (p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools))
out.append(p);
if (!tools)
return;
for (auto *c : m_connections) {
if (c)
c->registerToolsOn(tools);
}
return out;
}
QJsonObject McpClientsManager::builtinServers()
@@ -319,8 +314,6 @@ void McpClientsManager::loadFromDisk()
newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject()));
}
const auto providers = toolsCapableProviders();
const bool masterEnabled = Settings::mcpSettings().enableMcpClients();
QList<McpServerConnection *> keep;
@@ -350,7 +343,6 @@ void McpClientsManager::loadFromDisk()
existing->deleteLater();
}
c = new McpServerConnection(cfg, this);
c->setProviders(providers);
connect(
c,
&McpServerConnection::stateChanged,

View File

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

View File

@@ -23,7 +23,6 @@
#include <QStandardPaths>
#include <logger/Logger.hpp>
#include <pluginllmcore/Provider.hpp>
#include <settings/McpSettings.hpp>
namespace QodeAssist::Mcp {
@@ -35,13 +34,6 @@ QString transportToString(McpTransportKind k)
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
McpServerConfig McpServerConfig::fromJson(const QString &name, const QJsonObject &obj)
@@ -133,15 +125,6 @@ McpServerConnection::~McpServerConnection()
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()
{
if (m_config.transport == McpTransportKind::Http) {
@@ -293,40 +276,20 @@ void McpServerConnection::fetchAndRegisterTools()
[this](const QList<::LLMQore::Mcp::ToolInfo> &tools) {
if (m_listToolsWatchdog)
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) {
if (info.name.isEmpty())
continue;
m_toolIds.append(info.name);
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);
}
m_tools.append(info);
}
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(tools.size())
.arg(m_providers.size()));
.arg(m_tools.size()));
setState(
McpConnectionState::Connected,
QStringLiteral("Connected (%1 tools)").arg(tools.size()));
QStringLiteral("Connected (%1 tools)").arg(m_tools.size()));
})
.onFailed(this, [this](const std::exception &e) {
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()
{
if (m_toolIds.isEmpty())
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();
m_tools.clear();
}
void McpServerConnection::disconnectFromServer()

View File

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

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 OpenAICompatProvider : public PluginLLMCore::Provider
{
Q_OBJECT
public:
explicit OpenAICompatProvider(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

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