mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-17 11:49:14 -04:00
Compare commits
14 Commits
v0.9.21
...
dev-releas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa748b14a | ||
|
|
f499be278d | ||
|
|
231a6a0215 | ||
|
|
69672deb45 | ||
|
|
f36173d932 | ||
|
|
e65ac23e66 | ||
|
|
7bfe9d6f0e | ||
|
|
05fe38e289 | ||
|
|
2c9475cddf | ||
|
|
3179c0c358 | ||
|
|
c151c5030b | ||
|
|
98a618cf87 | ||
|
|
6220308a93 | ||
|
|
02c11ee5a0 |
24
.github/workflows/build_cmake.yml
vendored
24
.github/workflows/build_cmake.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
||||||
os: windows-2022,
|
os: windows-latest,
|
||||||
platform: windows_x64,
|
platform: windows_x64,
|
||||||
cc: "cl", cxx: "cl",
|
cc: "cl", cxx: "cl",
|
||||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||||
@@ -53,10 +53,6 @@ jobs:
|
|||||||
qt_version: "6.10.3",
|
qt_version: "6.10.3",
|
||||||
qt_creator_version: "19.0.2"
|
qt_creator_version: "19.0.2"
|
||||||
}
|
}
|
||||||
- {
|
|
||||||
qt_version: "6.11.1",
|
|
||||||
qt_creator_version: "20.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||||
@@ -114,14 +110,10 @@ jobs:
|
|||||||
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
|
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
|
||||||
|
|
||||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||||
set(qt_repo_dir "qt6_${qt_version_dotless}")
|
|
||||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||||
set(url_os "windows_x86")
|
set(url_os "windows_x86")
|
||||||
set(qt_package_arch_suffix "win64_msvc2022_64")
|
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||||
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||||
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
|
|
||||||
set(qt_repo_dir "qt6_${qt_version_dotless}_msvc2022_64")
|
|
||||||
endif()
|
|
||||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||||
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
|
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
|
||||||
else()
|
else()
|
||||||
@@ -135,9 +127,7 @@ jobs:
|
|||||||
set(qt_package_arch_suffix "linux_gcc_64")
|
set(qt_package_arch_suffix "linux_gcc_64")
|
||||||
endif()
|
endif()
|
||||||
set(qt_dir_prefix "${qt_version}/gcc_64")
|
set(qt_dir_prefix "${qt_version}/gcc_64")
|
||||||
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
|
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||||
set(qt_package_suffix "-Linux-RHEL_9_6-GCC-Linux-RHEL_9_6-X86_64")
|
|
||||||
elseif (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
|
||||||
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
|
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
|
||||||
else()
|
else()
|
||||||
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
||||||
@@ -153,7 +143,7 @@ jobs:
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/${qt_repo_dir}")
|
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
|
||||||
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
|
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
|
||||||
|
|
||||||
file(READ ./Updates.xml updates_xml)
|
file(READ ./Updates.xml updates_xml)
|
||||||
@@ -180,11 +170,7 @@ jobs:
|
|||||||
)
|
)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
set(qt_addon_packages qt5compat qtshadertools)
|
foreach(package qt5compat qtshadertools)
|
||||||
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
|
|
||||||
list(APPEND qt_addon_packages qttasktree)
|
|
||||||
endif()
|
|
||||||
foreach(package ${qt_addon_packages})
|
|
||||||
downloadAndExtract(
|
downloadAndExtract(
|
||||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||||
${package}.7z
|
${package}.7z
|
||||||
@@ -250,7 +236,7 @@ jobs:
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(build_plugin_py "scripts/build_plugin.py")
|
set(build_plugin_py "scripts/build_plugin.py")
|
||||||
foreach(dir "share/qtcreator/scripts" "Qt Creator.sdk/share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
|
foreach(dir "share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
|
||||||
if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py")
|
if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py")
|
||||||
set(build_plugin_py "${dir}/build_plugin.py")
|
set(build_plugin_py "${dir}/build_plugin.py")
|
||||||
break()
|
break()
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(QodeAssist)
|
list(APPEND CMAKE_PREFIX_PATH "/Users/palm1r/Qt/Qt Creator.sdk/lib/cmake/QtCreator")
|
||||||
|
|
||||||
option(QODEASSIST_EXPERIMENTAL
|
project(QodeAssist)
|
||||||
"Enable experimental features" OFF)
|
|
||||||
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
|
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
@@ -42,7 +40,6 @@ add_definitions(
|
|||||||
|
|
||||||
add_subdirectory(sources)
|
add_subdirectory(sources)
|
||||||
add_subdirectory(logger)
|
add_subdirectory(logger)
|
||||||
add_subdirectory(pluginllmcore)
|
|
||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(UIControls)
|
add_subdirectory(UIControls)
|
||||||
add_subdirectory(ChatView)
|
add_subdirectory(ChatView)
|
||||||
@@ -51,6 +48,11 @@ if(GTest_FOUND)
|
|||||||
add_subdirectory(test)
|
add_subdirectory(test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
option(QODEASSIST_BUILD_BENCH "Build the standalone agent bench CLI" ON)
|
||||||
|
if(QODEASSIST_BUILD_BENCH)
|
||||||
|
add_subdirectory(bench)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_qtc_plugin(QodeAssist
|
add_qtc_plugin(QodeAssist
|
||||||
PLUGIN_DEPENDS
|
PLUGIN_DEPENDS
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
@@ -69,7 +71,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::CPlusPlus
|
QtCreator::CPlusPlus
|
||||||
LLMQore
|
LLMQore
|
||||||
PluginLLMCore
|
|
||||||
ProvidersConfig
|
ProvidersConfig
|
||||||
Agents
|
Agents
|
||||||
Skills
|
Skills
|
||||||
@@ -83,42 +84,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
RefactorContextHelper.hpp
|
RefactorContextHelper.hpp
|
||||||
templates/Templates.hpp
|
|
||||||
templates/CodeLlamaFim.hpp
|
|
||||||
templates/Ollama.hpp
|
|
||||||
templates/Claude.hpp
|
|
||||||
templates/OpenAI.hpp
|
|
||||||
templates/MistralAI.hpp
|
|
||||||
templates/StarCoder2Fim.hpp
|
|
||||||
templates/Qwen25CoderFIM.hpp
|
|
||||||
templates/OpenAICompatible.hpp
|
|
||||||
templates/Llama3.hpp
|
|
||||||
templates/ChatML.hpp
|
|
||||||
templates/Alpaca.hpp
|
|
||||||
templates/Llama2.hpp
|
|
||||||
templates/CodeLlamaQMLFim.hpp
|
|
||||||
templates/GoogleAI.hpp
|
|
||||||
templates/LlamaCppFim.hpp
|
|
||||||
templates/Qwen3CoderFIM.hpp
|
|
||||||
templates/OpenAIResponses.hpp
|
|
||||||
providers/Providers.hpp
|
|
||||||
providers/ProviderUrlUtils.hpp
|
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
|
||||||
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
|
||||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
|
||||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
|
||||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
|
||||||
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
|
|
||||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
|
||||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
|
||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
|
||||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
|
||||||
providers/QwenProvider.hpp providers/QwenProvider.cpp
|
|
||||||
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
|
|
||||||
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
|
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
@@ -130,7 +95,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
||||||
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
||||||
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
||||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
|
||||||
CodeHandler.hpp CodeHandler.cpp
|
CodeHandler.hpp CodeHandler.cpp
|
||||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||||
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
||||||
@@ -170,10 +134,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if(QODEASSIST_EXPERIMENTAL)
|
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
|
||||||
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
|
|
||||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||||
find_program(QtCreatorExecutable
|
find_program(QtCreatorExecutable
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "AgentRoleController.hpp"
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "AgentRole.hpp"
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
AgentRoleController::AgentRoleController(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().systemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&AgentRoleController::baseSystemPromptChanged);
|
|
||||||
|
|
||||||
loadAvailableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList AgentRoleController::availableRoles() const
|
|
||||||
{
|
|
||||||
return m_availableRoles;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRole() const
|
|
||||||
{
|
|
||||||
return m_currentRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::baseSystemPrompt() const
|
|
||||||
{
|
|
||||||
return Settings::chatAssistantSettings().systemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRoleDescription() const
|
|
||||||
{
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
if (lastRoleId.isEmpty())
|
|
||||||
return Settings::AgentRolesManager::getNoRole().description;
|
|
||||||
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (role.id.isEmpty())
|
|
||||||
return Settings::AgentRolesManager::getNoRole().description;
|
|
||||||
|
|
||||||
return role.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRoleSystemPrompt() const
|
|
||||||
{
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
if (lastRoleId.isEmpty())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (role.id.isEmpty())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
return role.systemPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::loadAvailableRoles()
|
|
||||||
{
|
|
||||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
|
||||||
|
|
||||||
m_availableRoles.clear();
|
|
||||||
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
|
||||||
|
|
||||||
for (const auto &role : roles)
|
|
||||||
m_availableRoles.append(role.name);
|
|
||||||
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
|
|
||||||
|
|
||||||
if (!lastRoleId.isEmpty()) {
|
|
||||||
for (const auto &role : roles) {
|
|
||||||
if (role.id == lastRoleId) {
|
|
||||||
m_currentRole = role.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit availableRolesChanged();
|
|
||||||
emit currentRoleChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::applyRole(const QString &roleName)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
|
|
||||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
|
||||||
settings.lastUsedRoleId.setValue("");
|
|
||||||
settings.writeSettings();
|
|
||||||
m_currentRole = roleName;
|
|
||||||
emit currentRoleChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
|
||||||
|
|
||||||
for (const auto &role : roles) {
|
|
||||||
if (role.name == roleName) {
|
|
||||||
settings.lastUsedRoleId.setValue(role.id);
|
|
||||||
settings.writeSettings();
|
|
||||||
m_currentRole = role.name;
|
|
||||||
emit currentRoleChanged();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::openSettings()
|
|
||||||
{
|
|
||||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class AgentRoleController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AgentRoleController(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QStringList availableRoles() const;
|
|
||||||
QString currentRole() const;
|
|
||||||
QString baseSystemPrompt() const;
|
|
||||||
QString currentRoleDescription() const;
|
|
||||||
QString currentRoleSystemPrompt() const;
|
|
||||||
|
|
||||||
void loadAvailableRoles();
|
|
||||||
void applyRole(const QString &roleName);
|
|
||||||
void openSettings();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void availableRolesChanged();
|
|
||||||
void currentRoleChanged();
|
|
||||||
void baseSystemPromptChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QStringList m_availableRoles;
|
|
||||||
QString m_currentRole;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -75,8 +75,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
ChatCompressor.hpp ChatCompressor.cpp
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
AgentRoleController.hpp AgentRoleController.cpp
|
ChatAgentController.hpp ChatAgentController.cpp
|
||||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
|
||||||
FileEditController.hpp FileEditController.cpp
|
FileEditController.hpp FileEditController.cpp
|
||||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||||
@@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView
|
|||||||
Qt::Network
|
Qt::Network
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
PluginLLMCore
|
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
Context
|
Context
|
||||||
QodeAssistUIControlsplugin
|
QodeAssistUIControlsplugin
|
||||||
QodeAssistLogger
|
QodeAssistLogger
|
||||||
LLMQore
|
LLMQore
|
||||||
Skills
|
Skills
|
||||||
|
Agents
|
||||||
|
Session
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
|||||||
105
ChatView/ChatAgentController.cpp
Normal file
105
ChatView/ChatAgentController.cpp
Normal 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
|
||||||
47
ChatView/ChatAgentController.hpp
Normal file
47
ChatView/ChatAgentController.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatAgentController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatAgentController(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setAgentFactory(AgentFactory *factory);
|
||||||
|
|
||||||
|
QStringList availableAgents() const;
|
||||||
|
QString currentAgent() const;
|
||||||
|
void setCurrentAgent(const QString &name);
|
||||||
|
|
||||||
|
bool currentSupportsThinking() const;
|
||||||
|
bool currentSupportsTools() const;
|
||||||
|
|
||||||
|
void reload();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void availableAgentsChanged();
|
||||||
|
void currentAgentChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ensureValidCurrent();
|
||||||
|
|
||||||
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
|
QStringList m_availableAgents;
|
||||||
|
QString m_currentAgent;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -4,13 +4,20 @@
|
|||||||
|
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include "ChatModel.hpp"
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "PromptTemplateManager.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -25,7 +32,18 @@ ChatCompressor::ChatCompressor(QObject *parent)
|
|||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::setActiveAgent(const QString &agentName)
|
||||||
|
{
|
||||||
|
m_activeAgent = agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::startCompression(
|
||||||
|
const QString &chatFilePath, ConversationHistory *sourceHistory)
|
||||||
{
|
{
|
||||||
if (m_isCompressing) {
|
if (m_isCompressing) {
|
||||||
emit compressionFailed(tr("Compression already in progress"));
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
@@ -37,49 +55,78 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatModel || chatModel->rowCount() == 0) {
|
if (!sourceHistory || sourceHistory->isEmpty()) {
|
||||||
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
if (!m_sessionManager) {
|
||||||
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
emit compressionFailed(tr("Chat session manager is not available"));
|
||||||
|
|
||||||
if (!m_provider) {
|
|
||||||
emit compressionFailed(tr("No provider available"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
QString sessionError;
|
||||||
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
|
||||||
templateName);
|
if (!session) {
|
||||||
|
emit compressionFailed(
|
||||||
|
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!promptTemplate) {
|
auto *client = session->client();
|
||||||
emit compressionFailed(tr("No template available"));
|
if (!client) {
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emit compressionFailed(tr("Chat agent has no live client"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_isCompressing = true;
|
m_isCompressing = true;
|
||||||
m_chatModel = chatModel;
|
|
||||||
m_originalChatPath = chatFilePath;
|
m_originalChatPath = chatFilePath;
|
||||||
m_accumulatedSummary.clear();
|
m_session = session;
|
||||||
|
|
||||||
emit compressionStarted();
|
emit compressionStarted();
|
||||||
|
|
||||||
connectProviderSignals();
|
session->systemPrompt()->setLayer(
|
||||||
|
QStringLiteral("compression"),
|
||||||
|
QStringLiteral(
|
||||||
|
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||||
|
"Your summaries preserve key information, technical details, and the flow of "
|
||||||
|
"discussion."));
|
||||||
|
|
||||||
QJsonObject payload{
|
auto *history = session->history();
|
||||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
for (const auto &msg : sourceHistory->messages()) {
|
||||||
|
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
|
||||||
|
continue;
|
||||||
|
const QString text = msg.text();
|
||||||
|
if (text.trimmed().isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
buildRequestPayload(payload, promptTemplate);
|
Message apiMessage(msg.role());
|
||||||
|
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(text));
|
||||||
|
history->append(std::move(apiMessage));
|
||||||
|
}
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
connect(
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
session, &Session::finished, this,
|
||||||
: promptTemplate->endpoint();
|
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
|
||||||
m_provider->client()->setTransferTimeout(
|
connect(
|
||||||
|
session, &Session::failed, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onCompressionFailed(id, error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
m_currentRequestId = m_provider->sendRequest(
|
|
||||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
|
||||||
|
|
||||||
|
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
handleCompressionError(tr("Failed to start compression request: %1")
|
||||||
|
.arg(session->lastError().message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,44 +141,38 @@ void ChatCompressor::cancelCompression()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
LOG_MESSAGE("Cancelling compression request");
|
LOG_MESSAGE("Cancelling compression request");
|
||||||
|
|
||||||
if (m_provider && !m_currentRequestId.isEmpty())
|
|
||||||
m_provider->cancelRequest(m_currentRequestId);
|
|
||||||
|
|
||||||
cleanupState();
|
cleanupState();
|
||||||
emit compressionFailed(tr("Compression cancelled"));
|
emit compressionFailed(tr("Compression cancelled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
void ChatCompressor::onCompressionFinished(const QString &requestId)
|
||||||
{
|
{
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_accumulatedSummary += partialText;
|
QString summary;
|
||||||
}
|
if (m_session) {
|
||||||
|
if (auto *history = m_session->history(); history && !history->isEmpty())
|
||||||
|
summary = history->messages().back().text();
|
||||||
|
}
|
||||||
|
|
||||||
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
|
||||||
{
|
|
||||||
Q_UNUSED(fullText)
|
|
||||||
|
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
return;
|
const QString sourcePath = m_originalChatPath;
|
||||||
|
|
||||||
LOG_MESSAGE(
|
cleanupState();
|
||||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
|
||||||
|
|
||||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
|
||||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
emit compressionFailed(tr("Failed to save compressed chat"));
|
||||||
handleCompressionError(tr("Failed to save compressed chat"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||||
cleanupState();
|
|
||||||
emit compressionCompleted(compressedPath);
|
emit compressionCompleted(compressedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
|
||||||
{
|
{
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
return;
|
return;
|
||||||
@@ -168,39 +209,6 @@ QString ChatCompressor::buildCompressionPrompt() const
|
|||||||
"Create the summary now:");
|
"Create the summary now:");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::buildRequestPayload(
|
|
||||||
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
|
||||||
{
|
|
||||||
PluginLLMCore::ContextData context;
|
|
||||||
|
|
||||||
context.systemPrompt = QStringLiteral(
|
|
||||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
|
||||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
|
||||||
if (msg.role == ChatModel::ChatRole::Tool
|
|
||||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
|
||||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
PluginLLMCore::Message apiMessage;
|
|
||||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
|
||||||
apiMessage.content = msg.content;
|
|
||||||
messages.append(apiMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::Message compressionRequest;
|
|
||||||
compressionRequest.role = "user";
|
|
||||||
compressionRequest.content = buildCompressionPrompt();
|
|
||||||
messages.append(compressionRequest);
|
|
||||||
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
m_provider->prepareRequest(
|
|
||||||
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatCompressor::createCompressedChatFile(
|
bool ChatCompressor::createCompressedChatFile(
|
||||||
const QString &sourcePath, const QString &destPath, const QString &summary)
|
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||||
{
|
{
|
||||||
@@ -224,11 +232,11 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
|
|
||||||
QJsonObject summaryMessage;
|
QJsonObject summaryMessage;
|
||||||
summaryMessage["role"] = "assistant";
|
summaryMessage["role"] = "assistant";
|
||||||
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
|
||||||
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
summaryMessage["isRedacted"] = false;
|
QJsonObject textBlock;
|
||||||
summaryMessage["attachments"] = QJsonArray();
|
textBlock["type"] = "text";
|
||||||
summaryMessage["images"] = QJsonArray();
|
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||||
|
summaryMessage["blocks"] = QJsonArray{textBlock};
|
||||||
|
|
||||||
root["messages"] = QJsonArray{summaryMessage};
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
root["compressedFrom"] = sourcePath;
|
root["compressedFrom"] = sourcePath;
|
||||||
@@ -247,49 +255,17 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::connectProviderSignals()
|
|
||||||
{
|
|
||||||
auto *c = m_provider->client();
|
|
||||||
|
|
||||||
m_connections.append(connect(
|
|
||||||
c,
|
|
||||||
&::LLMQore::BaseClient::chunkReceived,
|
|
||||||
this,
|
|
||||||
&ChatCompressor::onPartialResponseReceived,
|
|
||||||
Qt::UniqueConnection));
|
|
||||||
|
|
||||||
m_connections.append(connect(
|
|
||||||
c,
|
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
|
||||||
this,
|
|
||||||
&ChatCompressor::onFullResponseReceived,
|
|
||||||
Qt::UniqueConnection));
|
|
||||||
|
|
||||||
m_connections.append(connect(
|
|
||||||
c,
|
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
|
||||||
this,
|
|
||||||
&ChatCompressor::onRequestFailed,
|
|
||||||
Qt::UniqueConnection));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatCompressor::disconnectAllSignals()
|
|
||||||
{
|
|
||||||
for (const auto &connection : std::as_const(m_connections))
|
|
||||||
disconnect(connection);
|
|
||||||
m_connections.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatCompressor::cleanupState()
|
void ChatCompressor::cleanupState()
|
||||||
{
|
{
|
||||||
disconnectAllSignals();
|
Session *session = m_session;
|
||||||
|
|
||||||
m_isCompressing = false;
|
m_isCompressing = false;
|
||||||
m_currentRequestId.clear();
|
m_currentRequestId.clear();
|
||||||
m_originalChatPath.clear();
|
m_originalChatPath.clear();
|
||||||
m_accumulatedSummary.clear();
|
m_session = nullptr;
|
||||||
m_chatModel = nullptr;
|
|
||||||
m_provider = nullptr;
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -4,20 +4,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace QodeAssist::PluginLLMCore {
|
namespace QodeAssist {
|
||||||
class Provider;
|
class SessionManager;
|
||||||
class PromptTemplate;
|
class Session;
|
||||||
} // namespace QodeAssist::PluginLLMCore
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class ChatCompressor : public QObject
|
class ChatCompressor : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -25,7 +24,10 @@ class ChatCompressor : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ChatCompressor(QObject *parent = nullptr);
|
explicit ChatCompressor(QObject *parent = nullptr);
|
||||||
|
|
||||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setActiveAgent(const QString &agentName);
|
||||||
|
|
||||||
|
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
|
||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
void cancelCompression();
|
void cancelCompression();
|
||||||
@@ -35,30 +37,23 @@ signals:
|
|||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
void compressionFailed(const QString &error);
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
|
||||||
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
|
||||||
void onRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void onCompressionFinished(const QString &requestId);
|
||||||
|
void onCompressionFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
QString createCompressedChatPath(const QString &originalPath) const;
|
QString createCompressedChatPath(const QString &originalPath) const;
|
||||||
QString buildCompressionPrompt() const;
|
QString buildCompressionPrompt() const;
|
||||||
bool createCompressedChatFile(
|
bool createCompressedChatFile(
|
||||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||||
void connectProviderSignals();
|
|
||||||
void disconnectAllSignals();
|
|
||||||
void cleanupState();
|
void cleanupState();
|
||||||
void handleCompressionError(const QString &error);
|
void handleCompressionError(const QString &error);
|
||||||
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
|
||||||
|
|
||||||
bool m_isCompressing = false;
|
bool m_isCompressing = false;
|
||||||
QString m_currentRequestId;
|
QString m_currentRequestId;
|
||||||
QString m_originalChatPath;
|
QString m_originalChatPath;
|
||||||
QString m_accumulatedSummary;
|
QPointer<SessionManager> m_sessionManager;
|
||||||
PluginLLMCore::Provider *m_provider = nullptr;
|
QString m_activeAgent;
|
||||||
ChatModel *m_chatModel = nullptr;
|
QPointer<Session> m_session;
|
||||||
|
|
||||||
QList<QMetaObject::Connection> m_connections;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatConfigurationController.hpp"
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
connect(
|
|
||||||
&settings.caProvider,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatConfigurationController::updateCurrentConfiguration);
|
|
||||||
connect(
|
|
||||||
&settings.caModel,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatConfigurationController::updateCurrentConfiguration);
|
|
||||||
|
|
||||||
loadAvailableConfigurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatConfigurationController::availableConfigurations() const
|
|
||||||
{
|
|
||||||
return m_availableConfigurations;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatConfigurationController::currentConfiguration() const
|
|
||||||
{
|
|
||||||
return m_currentConfiguration;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::updateCurrentConfiguration()
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
m_currentConfiguration
|
|
||||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::loadAvailableConfigurations()
|
|
||||||
{
|
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
|
||||||
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
|
||||||
Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
m_availableConfigurations.clear();
|
|
||||||
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
|
||||||
|
|
||||||
for (const Settings::AIConfiguration &config : configs) {
|
|
||||||
m_availableConfigurations.append(config.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentConfiguration();
|
|
||||||
|
|
||||||
emit availableConfigurationsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
|
||||||
{
|
|
||||||
if (configName == QObject::tr("Current Settings")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
|
||||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
|
||||||
Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
for (const Settings::AIConfiguration &config : configs) {
|
|
||||||
if (config.name == configName) {
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
|
|
||||||
settings.caProvider.setValue(config.provider);
|
|
||||||
settings.caModel.setValue(config.model);
|
|
||||||
settings.caTemplate.setValue(config.templateName);
|
|
||||||
settings.caUrl.setValue(config.url);
|
|
||||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
|
||||||
|
|
||||||
settings.writeSettings();
|
|
||||||
|
|
||||||
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class ChatConfigurationController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ChatConfigurationController(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QStringList availableConfigurations() const;
|
|
||||||
QString currentConfiguration() const;
|
|
||||||
|
|
||||||
void loadAvailableConfigurations();
|
|
||||||
void applyConfiguration(const QString &configName);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void availableConfigurationsChanged();
|
|
||||||
void currentConfigurationChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateCurrentConfiguration();
|
|
||||||
|
|
||||||
QStringList m_availableConfigurations;
|
|
||||||
QString m_currentConfiguration;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -16,15 +16,20 @@
|
|||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_history(history)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
QString ChatHistoryStore::historyDir() const
|
QString ChatHistoryStore::historyDir() const
|
||||||
@@ -52,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
|
|||||||
{
|
{
|
||||||
QString shortMessage;
|
QString shortMessage;
|
||||||
|
|
||||||
if (m_chatModel->rowCount() > 0) {
|
if (m_history) {
|
||||||
QString firstMessage
|
for (const auto &message : m_history->messages()) {
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
if (message.role() != Message::Role::User)
|
||||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
continue;
|
||||||
|
|
||||||
if (shortMessage.isEmpty()) {
|
const QString text = message.text();
|
||||||
QVariantList images
|
if (!text.trimmed().isEmpty()) {
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
shortMessage = text.split('\n').first().simplified().left(30);
|
||||||
if (!images.isEmpty()) {
|
} else {
|
||||||
shortMessage = "image_chat";
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (dynamic_cast<StoredImageContent *>(block.get())) {
|
||||||
|
shortMessage = "image_chat";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +118,12 @@ QString ChatHistoryStore::autosaveFilePath(
|
|||||||
|
|
||||||
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
return ChatSerializer::saveToFile(m_history, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
return ChatSerializer::loadFromFile(m_history, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatHistoryStore::showSaveDialog()
|
void ChatHistoryStore::showSaveDialog()
|
||||||
|
|||||||
@@ -9,16 +9,18 @@
|
|||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
class ChatModel;
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatHistoryStore : public QObject
|
class ChatHistoryStore : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
|
||||||
|
|
||||||
QString historyDir() const;
|
QString historyDir() const;
|
||||||
QString suggestedFileName() const;
|
QString suggestedFileName() const;
|
||||||
@@ -42,7 +44,7 @@ signals:
|
|||||||
private:
|
private:
|
||||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ConversationHistory *m_history;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,16 @@
|
|||||||
#include "MessagePart.hpp"
|
#include "MessagePart.hpp"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QVector>
|
||||||
#include <QtQmlIntegration>
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
#include "context/ContentFile.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -43,81 +48,19 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
struct ImageAttachment
|
|
||||||
{
|
|
||||||
QString fileName; // Original filename
|
|
||||||
QString storedPath; // Path to stored image file (relative to chat folder)
|
|
||||||
QString mediaType; // MIME type
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Message
|
|
||||||
{
|
|
||||||
ChatRole role;
|
|
||||||
QString content;
|
|
||||||
QString id;
|
|
||||||
bool isRedacted = false;
|
|
||||||
QString signature = QString();
|
|
||||||
|
|
||||||
QList<Context::ContentFile> attachments;
|
|
||||||
QList<ImageAttachment> images;
|
|
||||||
|
|
||||||
QString toolName;
|
|
||||||
QJsonObject toolArguments;
|
|
||||||
QString toolResult;
|
|
||||||
|
|
||||||
int promptTokens = 0;
|
|
||||||
int completionTokens = 0;
|
|
||||||
int cachedPromptTokens = 0;
|
|
||||||
int reasoningTokens = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ChatModel(QObject *parent = nullptr);
|
explicit ChatModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
Q_INVOKABLE void addMessage(
|
|
||||||
const QString &content,
|
|
||||||
ChatRole role,
|
|
||||||
const QString &id,
|
|
||||||
const QList<Context::ContentFile> &attachments = {},
|
|
||||||
const QList<ImageAttachment> &images = {},
|
|
||||||
bool isRedacted = false,
|
|
||||||
const QString &signature = QString());
|
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||||
|
|
||||||
QVector<Message> getChatHistory() const;
|
|
||||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
|
||||||
|
|
||||||
QString currentModel() const;
|
|
||||||
QString lastMessageId() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void resetModelTo(int index);
|
Q_INVOKABLE void resetModelTo(int index);
|
||||||
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments);
|
|
||||||
void dropTrailingAssistantMessage(const QString &requestId);
|
|
||||||
void setToolMessageData(
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments,
|
|
||||||
const QString &toolResult);
|
|
||||||
void updateToolResult(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &result);
|
|
||||||
void addThinkingBlock(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
|
||||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
|
||||||
|
|
||||||
void setMessageUsage(
|
void setMessageUsage(
|
||||||
const QString &messageId,
|
const QString &messageId,
|
||||||
int promptTokens,
|
int promptTokens,
|
||||||
@@ -129,10 +72,7 @@ public:
|
|||||||
int sessionCompletionTokens() const;
|
int sessionCompletionTokens() const;
|
||||||
int sessionCachedPromptTokens() const;
|
int sessionCachedPromptTokens() const;
|
||||||
int sessionTotalTokens() const;
|
int sessionTotalTokens() const;
|
||||||
|
|
||||||
void setLoadingFromHistory(bool loading);
|
|
||||||
bool isLoadingFromHistory() const;
|
|
||||||
|
|
||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
@@ -141,18 +81,60 @@ signals:
|
|||||||
void sessionUsageChanged();
|
void sessionUsageChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onFileEditApplied(const QString &editId);
|
void onHistoryMessageAdded(int index);
|
||||||
void onFileEditRejected(const QString &editId);
|
void onHistoryMessageUpdated(int index);
|
||||||
void onFileEditArchived(const QString &editId);
|
void onHistoryCleared();
|
||||||
|
void onHistoryReset();
|
||||||
|
void onFileEditStatusChanged(const QString &editId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
struct AttachmentRef
|
||||||
|
{
|
||||||
QVector<Message> m_messages;
|
QString fileName;
|
||||||
bool m_loadingFromHistory = false;
|
QString storedPath;
|
||||||
|
};
|
||||||
|
struct ImageRef
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
QString mediaType;
|
||||||
|
};
|
||||||
|
struct Row
|
||||||
|
{
|
||||||
|
ChatRole kind = ChatRole::Assistant;
|
||||||
|
int messageIndex = -1;
|
||||||
|
QString messageId;
|
||||||
|
QString content;
|
||||||
|
bool isRedacted = false;
|
||||||
|
QString editId;
|
||||||
|
QVector<AttachmentRef> attachments;
|
||||||
|
QVector<ImageRef> images;
|
||||||
|
};
|
||||||
|
struct Usage
|
||||||
|
{
|
||||||
|
int prompt = 0;
|
||||||
|
int completion = 0;
|
||||||
|
int cached = 0;
|
||||||
|
int reasoning = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void rebuildAll();
|
||||||
|
void reprojectTail(int startMessageIndex);
|
||||||
|
int startMessageIndexFor(int messageIndex) const;
|
||||||
|
int firstRowForMessage(int messageIndex) const;
|
||||||
|
QHash<QString, QString> buildToolResultMap() const;
|
||||||
|
void appendRowsForMessage(
|
||||||
|
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
|
||||||
|
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
|
||||||
|
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
|
||||||
|
QVariantList buildImageList(const QVector<ImageRef> &images) const;
|
||||||
|
|
||||||
|
QPointer<ConversationHistory> m_history;
|
||||||
|
QVector<Row> m_rows;
|
||||||
|
QHash<QString, Usage> m_usageByMessageId;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||||
|
|||||||
@@ -28,9 +28,16 @@
|
|||||||
|
|
||||||
#include "QodeAssistConstants.hpp"
|
#include "QodeAssistConstants.hpp"
|
||||||
|
|
||||||
#include "AgentRoleController.hpp"
|
#include <AgentFactory.hpp>
|
||||||
|
#include <AgentRouter.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <sources/settings/PipelinesConfig.hpp>
|
||||||
|
|
||||||
|
#include "ChatAgentController.hpp"
|
||||||
|
#include "AgentRole.hpp"
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatConfigurationController.hpp"
|
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
#include "ChatHistoryStore.hpp"
|
#include "ChatHistoryStore.hpp"
|
||||||
#include "FileEditController.hpp"
|
#include "FileEditController.hpp"
|
||||||
@@ -38,10 +45,8 @@
|
|||||||
#include "InputTokenCounter.hpp"
|
#include "InputTokenCounter.hpp"
|
||||||
#include "SettingsConstants.hpp"
|
#include "SettingsConstants.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "SessionFileRegistry.hpp"
|
#include "SessionFileRegistry.hpp"
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "pluginllmcore/RulesLoader.hpp"
|
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
#include "SkillsSettings.hpp"
|
#include "SkillsSettings.hpp"
|
||||||
#include "sources/skills/SkillsManager.hpp"
|
#include "sources/skills/SkillsManager.hpp"
|
||||||
@@ -73,19 +78,20 @@ QKeySequence sendMessageKeySequence()
|
|||||||
|
|
||||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||||
: QQuickItem(parent)
|
: QQuickItem(parent)
|
||||||
|
, m_history(new QodeAssist::ConversationHistory(this))
|
||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
|
||||||
, m_fileManager(new ChatFileManager(this))
|
, m_fileManager(new ChatFileManager(this))
|
||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
, m_chatCompressor(new ChatCompressor(this))
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
, m_agentRoleController(new AgentRoleController(this))
|
, m_agentController(new ChatAgentController(this))
|
||||||
, m_configurationController(new ChatConfigurationController(this))
|
, m_fileEditController(new FileEditController(this))
|
||||||
, m_fileEditController(new FileEditController(m_chatModel, this))
|
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
|
||||||
, m_tokenCounter(
|
, m_historyStore(new ChatHistoryStore(m_history, this))
|
||||||
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
|
||||||
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
|
|
||||||
{
|
{
|
||||||
|
m_chatModel->setHistory(m_history);
|
||||||
|
m_clientInterface->setHistory(m_history);
|
||||||
|
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
&Settings::chatAssistantSettings().linkOpenFiles,
|
&Settings::chatAssistantSettings().linkOpenFiles,
|
||||||
@@ -109,22 +115,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
m_configurationController,
|
|
||||||
&ChatConfigurationController::availableConfigurationsChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::availableConfigurationsChanged);
|
|
||||||
connect(
|
|
||||||
m_configurationController,
|
|
||||||
&ChatConfigurationController::currentConfigurationChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::currentConfigurationChanged);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
&ClientInterface::messageReceivedCompletely,
|
&ClientInterface::messageReceivedCompletely,
|
||||||
@@ -171,20 +161,30 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::inputTokensCountChanged);
|
&ChatRootView::inputTokensCountChanged);
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentController,
|
||||||
&AgentRoleController::availableRolesChanged,
|
&ChatAgentController::availableAgentsChanged,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::availableAgentRolesChanged);
|
&ChatRootView::availableChatAgentsChanged);
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentController,
|
||||||
&AgentRoleController::currentRoleChanged,
|
&ChatAgentController::currentAgentChanged,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::currentAgentRoleChanged);
|
&ChatRootView::currentChatAgentChanged);
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentController,
|
||||||
&AgentRoleController::baseSystemPromptChanged,
|
&ChatAgentController::currentAgentChanged,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::baseSystemPromptChanged);
|
&ChatRootView::isThinkingSupportChanged);
|
||||||
|
connect(
|
||||||
|
m_agentController,
|
||||||
|
&ChatAgentController::currentAgentChanged,
|
||||||
|
this,
|
||||||
|
&ChatRootView::useToolsChanged);
|
||||||
|
connect(
|
||||||
|
m_agentController,
|
||||||
|
&ChatAgentController::currentAgentChanged,
|
||||||
|
this,
|
||||||
|
&ChatRootView::useThinkingChanged);
|
||||||
|
|
||||||
auto editors = Core::EditorManager::instance();
|
auto editors = Core::EditorManager::instance();
|
||||||
|
|
||||||
@@ -266,14 +266,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
connect(
|
connect(
|
||||||
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
||||||
|
|
||||||
refreshRules();
|
|
||||||
|
|
||||||
connect(
|
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
|
||||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::refreshRules);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
&ProjectExplorer::ProjectManager::projectAdded,
|
&ProjectExplorer::ProjectManager::projectAdded,
|
||||||
@@ -298,12 +290,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::useThinkingChanged);
|
&ChatRootView::useThinkingChanged);
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::generalSettings().caProvider,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::isThinkingSupportChanged);
|
|
||||||
|
|
||||||
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||||
m_lastErrorMessage = error;
|
m_lastErrorMessage = error;
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
@@ -324,7 +310,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
if (m_pendingSend.active) {
|
if (m_pendingSend.active) {
|
||||||
PendingSend p = m_pendingSend;
|
PendingSend p = m_pendingSend;
|
||||||
m_pendingSend = {};
|
m_pendingSend = {};
|
||||||
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
dispatchSend(p.message, p.attachments, p.linkedFiles);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,7 +323,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
if (m_pendingSend.active) {
|
if (m_pendingSend.active) {
|
||||||
PendingSend p = m_pendingSend;
|
PendingSend p = m_pendingSend;
|
||||||
m_pendingSend = {};
|
m_pendingSend = {};
|
||||||
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
dispatchSend(p.message, p.attachments, p.linkedFiles);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -373,6 +359,85 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
|
|||||||
return m_skillsManager;
|
return m_skillsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AgentFactory *ChatRootView::agentFactory() const
|
||||||
|
{
|
||||||
|
if (!m_agentFactory) {
|
||||||
|
if (auto *engine = qmlEngine(this)) {
|
||||||
|
m_agentFactory = qobject_cast<AgentFactory *>(
|
||||||
|
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionManager *ChatRootView::sessionManager() const
|
||||||
|
{
|
||||||
|
if (!m_sessionManager) {
|
||||||
|
if (auto *engine = qmlEngine(this)) {
|
||||||
|
m_sessionManager = qobject_cast<SessionManager *>(
|
||||||
|
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::loadAvailableChatAgents()
|
||||||
|
{
|
||||||
|
m_agentController->setAgentFactory(agentFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatRootView::availableChatAgents() const
|
||||||
|
{
|
||||||
|
return m_agentController->availableAgents();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::currentChatAgent() const
|
||||||
|
{
|
||||||
|
return m_agentController->currentAgent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::setCurrentChatAgent(const QString &name)
|
||||||
|
{
|
||||||
|
m_agentController->setCurrentAgent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatRootView::availableRoles() const
|
||||||
|
{
|
||||||
|
return m_availableRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::currentRole() const
|
||||||
|
{
|
||||||
|
return m_currentRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::setCurrentRole(const QString &roleId)
|
||||||
|
{
|
||||||
|
if (m_currentRole == roleId)
|
||||||
|
return;
|
||||||
|
m_currentRole = roleId;
|
||||||
|
emit currentRoleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::loadAvailableRoles()
|
||||||
|
{
|
||||||
|
QStringList ids;
|
||||||
|
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||||
|
ids.reserve(roles.size());
|
||||||
|
for (const auto &r : roles)
|
||||||
|
ids << r.id;
|
||||||
|
|
||||||
|
if (ids != m_availableRoles) {
|
||||||
|
m_availableRoles = ids;
|
||||||
|
emit availableRolesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_availableRoles.isEmpty() && !m_availableRoles.contains(m_currentRole))
|
||||||
|
setCurrentRole(m_availableRoles.contains(QStringLiteral("developer"))
|
||||||
|
? QStringLiteral("developer")
|
||||||
|
: m_availableRoles.first());
|
||||||
|
}
|
||||||
|
|
||||||
QVariantList ChatRootView::searchSkills(const QString &query) const
|
QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||||
{
|
{
|
||||||
QVariantList results;
|
QVariantList results;
|
||||||
@@ -380,7 +445,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
|
|||||||
if (!manager || !Settings::skillsSettings().enableSkills())
|
if (!manager || !Settings::skillsSettings().enableSkills())
|
||||||
return results;
|
return results;
|
||||||
|
|
||||||
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
QStringList projectSkillDirs;
|
QStringList projectSkillDirs;
|
||||||
if (project) {
|
if (project) {
|
||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
@@ -416,21 +481,17 @@ void ChatRootView::sendMessage(const QString &message)
|
|||||||
{
|
{
|
||||||
const QStringList attachments = m_attachmentFiles;
|
const QStringList attachments = m_attachmentFiles;
|
||||||
const QStringList linkedFiles = m_linkedFiles;
|
const QStringList linkedFiles = m_linkedFiles;
|
||||||
const bool tools = useTools();
|
|
||||||
const bool thinking = useThinking();
|
|
||||||
|
|
||||||
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
|
if (deferSendForAutoCompress(message, attachments, linkedFiles))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
dispatchSend(message, attachments, linkedFiles, tools, thinking);
|
dispatchSend(message, attachments, linkedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::deferSendForAutoCompress(
|
bool ChatRootView::deferSendForAutoCompress(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QStringList &attachments,
|
const QStringList &attachments,
|
||||||
const QStringList &linkedFiles,
|
const QStringList &linkedFiles)
|
||||||
bool useToolsArg,
|
|
||||||
bool useThinkingArg)
|
|
||||||
{
|
{
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
if (!settings.autoCompress())
|
if (!settings.autoCompress())
|
||||||
@@ -456,7 +517,7 @@ bool ChatRootView::deferSendForAutoCompress(
|
|||||||
.arg(inputTokens)
|
.arg(inputTokens)
|
||||||
.arg(threshold));
|
.arg(threshold));
|
||||||
|
|
||||||
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
|
m_pendingSend = {message, attachments, linkedFiles, true};
|
||||||
compressCurrentChat();
|
compressCurrentChat();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -464,9 +525,7 @@ bool ChatRootView::deferSendForAutoCompress(
|
|||||||
void ChatRootView::dispatchSend(
|
void ChatRootView::dispatchSend(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QStringList &attachments,
|
const QStringList &attachments,
|
||||||
const QStringList &linkedFiles,
|
const QStringList &linkedFiles)
|
||||||
bool useToolsArg,
|
|
||||||
bool useThinkingArg)
|
|
||||||
{
|
{
|
||||||
if (m_recentFilePath.isEmpty()) {
|
if (m_recentFilePath.isEmpty()) {
|
||||||
QString filePath = getAutosaveFilePath(message, attachments);
|
QString filePath = getAutosaveFilePath(message, attachments);
|
||||||
@@ -481,8 +540,14 @@ void ChatRootView::dispatchSend(
|
|||||||
|
|
||||||
m_tokenCounter->recordSent();
|
m_tokenCounter->recordSent();
|
||||||
|
|
||||||
|
if (currentChatAgent().isEmpty())
|
||||||
|
loadAvailableChatAgents();
|
||||||
|
|
||||||
m_clientInterface->setSkillsManager(skillsManager());
|
m_clientInterface->setSkillsManager(skillsManager());
|
||||||
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
m_clientInterface->setSessionManager(sessionManager());
|
||||||
|
m_clientInterface->setActiveAgent(currentChatAgent());
|
||||||
|
m_clientInterface->setActiveRole(currentRole());
|
||||||
|
m_clientInterface->sendMessage(message, attachments, linkedFiles);
|
||||||
|
|
||||||
m_fileManager->clearIntermediateStorage();
|
m_fileManager->clearIntermediateStorage();
|
||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
@@ -527,12 +592,6 @@ void ChatRootView::clearMessages()
|
|||||||
clearLinkedFiles();
|
clearLinkedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::currentTemplate() const
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
return settings.caModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::saveHistory(const QString &filePath)
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (filePath != m_recentFilePath) {
|
if (filePath != m_recentFilePath) {
|
||||||
@@ -821,25 +880,6 @@ void ChatRootView::openChatHistoryFolder()
|
|||||||
m_historyStore->openHistoryFolder();
|
m_historyStore->openHistoryFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::openRulesFolder()
|
|
||||||
{
|
|
||||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
|
||||||
if (!project) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString projectPath = project->projectDirectory().toFSPathString();
|
|
||||||
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
|
||||||
|
|
||||||
QDir dir(rulesPath);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
dir.mkpath(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
|
||||||
QDesktopServices::openUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::openSettings()
|
void ChatRootView::openSettings()
|
||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
@@ -890,13 +930,12 @@ QString ChatRootView::chatTitle() const
|
|||||||
|
|
||||||
QString ChatRootView::computeChatTitle() const
|
QString ChatRootView::computeChatTitle() const
|
||||||
{
|
{
|
||||||
if (!m_chatModel)
|
if (!m_history)
|
||||||
return {};
|
return {};
|
||||||
const auto history = m_chatModel->getChatHistory();
|
for (const auto &msg : m_history->messages()) {
|
||||||
for (const auto &msg : history) {
|
if (msg.role() != Message::Role::User)
|
||||||
if (msg.role != ChatModel::User)
|
|
||||||
continue;
|
continue;
|
||||||
const QString content = msg.content.trimmed();
|
const QString content = msg.text().trimmed();
|
||||||
if (content.isEmpty())
|
if (content.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
||||||
@@ -1064,11 +1103,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
||||||
{
|
{
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
|
||||||
if (project
|
|
||||||
&& m_clientInterface->contextManager()
|
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(filePath.toFSPathString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
||||||
.arg(filePath.toFSPathString()));
|
.arg(filePath.toFSPathString()));
|
||||||
return true;
|
return true;
|
||||||
@@ -1120,71 +1155,14 @@ QString ChatRootView::lastErrorMessage() const
|
|||||||
return m_lastErrorMessage;
|
return m_lastErrorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList ChatRootView::activeRules() const
|
|
||||||
{
|
|
||||||
return m_activeRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::activeRulesCount() const
|
|
||||||
{
|
|
||||||
return m_activeRules.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::getRuleContent(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_activeRules.size())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
|
||||||
m_activeRules[index].toMap()["filePath"].toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::refreshRules()
|
|
||||||
{
|
|
||||||
m_activeRules.clear();
|
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (!project) {
|
|
||||||
emit activeRulesChanged();
|
|
||||||
emit activeRulesCountChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ruleFiles
|
|
||||||
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
|
||||||
|
|
||||||
for (const auto &ruleFile : ruleFiles) {
|
|
||||||
QVariantMap ruleMap;
|
|
||||||
ruleMap["filePath"] = ruleFile.filePath;
|
|
||||||
ruleMap["fileName"] = ruleFile.fileName;
|
|
||||||
ruleMap["category"] = ruleFile.category;
|
|
||||||
m_activeRules.append(ruleMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit activeRulesChanged();
|
|
||||||
emit activeRulesCountChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::useTools() const
|
bool ChatRootView::useTools() const
|
||||||
{
|
{
|
||||||
return Settings::chatAssistantSettings().enableChatTools();
|
return m_agentController->currentSupportsTools();
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::setUseTools(bool enabled)
|
|
||||||
{
|
|
||||||
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
|
|
||||||
Settings::chatAssistantSettings().writeSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::useThinking() const
|
bool ChatRootView::useThinking() const
|
||||||
{
|
{
|
||||||
return Settings::chatAssistantSettings().enableThinkingMode();
|
return m_agentController->currentSupportsThinking();
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::setUseThinking(bool enabled)
|
|
||||||
{
|
|
||||||
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
|
|
||||||
Settings::chatAssistantSettings().writeSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::applyFileEdit(const QString &editId)
|
void ChatRootView::applyFileEdit(const QString &editId)
|
||||||
@@ -1249,10 +1227,7 @@ QString ChatRootView::lastInfoMessage() const
|
|||||||
|
|
||||||
bool ChatRootView::isThinkingSupport() const
|
bool ChatRootView::isThinkingSupport() const
|
||||||
{
|
{
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
return m_agentController->currentSupportsThinking();
|
||||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
|
|
||||||
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
||||||
@@ -1273,66 +1248,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
|
|||||||
return imageExtensions.contains(fileInfo.suffix().toLower());
|
return imageExtensions.contains(fileInfo.suffix().toLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::loadAvailableConfigurations()
|
|
||||||
{
|
|
||||||
m_configurationController->loadAvailableConfigurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::applyConfiguration(const QString &configName)
|
|
||||||
{
|
|
||||||
m_configurationController->applyConfiguration(configName);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatRootView::availableConfigurations() const
|
|
||||||
{
|
|
||||||
return m_configurationController->availableConfigurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentConfiguration() const
|
|
||||||
{
|
|
||||||
return m_configurationController->currentConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::loadAvailableAgentRoles()
|
|
||||||
{
|
|
||||||
m_agentRoleController->loadAvailableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::applyAgentRole(const QString &roleName)
|
|
||||||
{
|
|
||||||
m_agentRoleController->applyRole(roleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatRootView::availableAgentRoles() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->availableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentAgentRole() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->currentRole();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::baseSystemPrompt() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->baseSystemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentAgentRoleDescription() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->currentRoleDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->currentRoleSystemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::openAgentRolesSettings()
|
|
||||||
{
|
|
||||||
m_agentRoleController->openSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::compressCurrentChat()
|
void ChatRootView::compressCurrentChat()
|
||||||
{
|
{
|
||||||
if (m_chatCompressor->isCompressing()) {
|
if (m_chatCompressor->isCompressing()) {
|
||||||
@@ -1349,7 +1264,20 @@ void ChatRootView::compressCurrentChat()
|
|||||||
|
|
||||||
autosave();
|
autosave();
|
||||||
|
|
||||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
if (currentChatAgent().isEmpty())
|
||||||
|
loadAvailableChatAgents();
|
||||||
|
m_chatCompressor->setSessionManager(sessionManager());
|
||||||
|
|
||||||
|
QString compressionAgent = currentChatAgent();
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatCompression;
|
||||||
|
if (!roster.isEmpty() && agentFactory()) {
|
||||||
|
const QString picked
|
||||||
|
= AgentRouter::pickAgent(roster, AgentRouter::Context{}, *agentFactory());
|
||||||
|
if (!picked.isEmpty())
|
||||||
|
compressionAgent = picked;
|
||||||
|
}
|
||||||
|
m_chatCompressor->setActiveAgent(compressionAgent);
|
||||||
|
m_chatCompressor->startCompression(m_recentFilePath, m_history);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::cancelCompression()
|
void ChatRootView::cancelCompression()
|
||||||
|
|||||||
@@ -11,18 +11,22 @@
|
|||||||
#include "ChatFileManager.hpp"
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
namespace QodeAssist::Skills {
|
||||||
class SkillsManager;
|
class SkillsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
class SessionManager;
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatCompressor;
|
class ChatCompressor;
|
||||||
class AgentRoleController;
|
class ChatAgentController;
|
||||||
class ChatConfigurationController;
|
|
||||||
class FileEditController;
|
class FileEditController;
|
||||||
class InputTokenCounter;
|
class InputTokenCounter;
|
||||||
class ChatHistoryStore;
|
class ChatHistoryStore;
|
||||||
@@ -32,7 +36,6 @@ class ChatRootView : public QQuickItem
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||||
@@ -46,10 +49,8 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
|
||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
Q_PROPERTY(bool useThinking READ useThinking NOTIFY useThinkingChanged FINAL)
|
||||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
|
||||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
|
||||||
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
||||||
|
|
||||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
@@ -57,13 +58,10 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
|
||||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
|
||||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
Q_PROPERTY(QStringList availableRoles READ availableRoles NOTIFY availableRolesChanged FINAL)
|
||||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
Q_PROPERTY(QString currentRole READ currentRole WRITE setCurrentRole NOTIFY currentRoleChanged FINAL)
|
||||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
|
||||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
|
||||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
||||||
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
||||||
@@ -75,7 +73,6 @@ public:
|
|||||||
~ChatRootView() override;
|
~ChatRootView() override;
|
||||||
|
|
||||||
ChatModel *chatModel() const;
|
ChatModel *chatModel() const;
|
||||||
QString currentTemplate() const;
|
|
||||||
|
|
||||||
void saveHistory(const QString &filePath);
|
void saveHistory(const QString &filePath);
|
||||||
void loadHistory(const QString &filePath);
|
void loadHistory(const QString &filePath);
|
||||||
@@ -104,7 +101,6 @@ public:
|
|||||||
QString sendShortcutText() const;
|
QString sendShortcutText() const;
|
||||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
Q_INVOKABLE void openChatHistoryFolder();
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
Q_INVOKABLE void openRulesFolder();
|
|
||||||
Q_INVOKABLE void openSettings();
|
Q_INVOKABLE void openSettings();
|
||||||
|
|
||||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
@@ -139,18 +135,11 @@ public:
|
|||||||
void setRequestProgressStatus(bool state);
|
void setRequestProgressStatus(bool state);
|
||||||
|
|
||||||
QString lastErrorMessage() const;
|
QString lastErrorMessage() const;
|
||||||
|
|
||||||
QVariantList activeRules() const;
|
|
||||||
int activeRulesCount() const;
|
|
||||||
Q_INVOKABLE QString getRuleContent(int index);
|
|
||||||
Q_INVOKABLE void refreshRules();
|
|
||||||
|
|
||||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||||
|
|
||||||
bool useTools() const;
|
bool useTools() const;
|
||||||
void setUseTools(bool enabled);
|
|
||||||
bool useThinking() const;
|
bool useThinking() const;
|
||||||
void setUseThinking(bool enabled);
|
|
||||||
|
|
||||||
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
||||||
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
||||||
@@ -161,23 +150,19 @@ public:
|
|||||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
||||||
|
|
||||||
Q_INVOKABLE void loadAvailableConfigurations();
|
|
||||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
|
||||||
QStringList availableConfigurations() const;
|
|
||||||
QString currentConfiguration() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void compressCurrentChat();
|
Q_INVOKABLE void compressCurrentChat();
|
||||||
Q_INVOKABLE void cancelCompression();
|
Q_INVOKABLE void cancelCompression();
|
||||||
|
|
||||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
Q_INVOKABLE void loadAvailableChatAgents();
|
||||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
QStringList availableChatAgents() const;
|
||||||
Q_INVOKABLE void openAgentRolesSettings();
|
QString currentChatAgent() const;
|
||||||
QStringList availableAgentRoles() const;
|
void setCurrentChatAgent(const QString &name);
|
||||||
QString currentAgentRole() const;
|
|
||||||
QString baseSystemPrompt() const;
|
Q_INVOKABLE void loadAvailableRoles();
|
||||||
QString currentAgentRoleDescription() const;
|
QStringList availableRoles() const;
|
||||||
QString currentAgentRoleSystemPrompt() const;
|
QString currentRole() const;
|
||||||
|
void setCurrentRole(const QString &roleId);
|
||||||
|
|
||||||
int currentMessageTotalEdits() const;
|
int currentMessageTotalEdits() const;
|
||||||
int currentMessageAppliedEdits() const;
|
int currentMessageAppliedEdits() const;
|
||||||
int currentMessagePendingEdits() const;
|
int currentMessagePendingEdits() const;
|
||||||
@@ -206,7 +191,6 @@ public slots:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
void currentTemplateChanged();
|
|
||||||
void attachmentFilesChanged();
|
void attachmentFilesChanged();
|
||||||
void linkedFilesChanged();
|
void linkedFilesChanged();
|
||||||
void inputTokensCountChanged();
|
void inputTokensCountChanged();
|
||||||
@@ -223,20 +207,17 @@ signals:
|
|||||||
void lastErrorMessageChanged();
|
void lastErrorMessageChanged();
|
||||||
void lastInfoMessageChanged();
|
void lastInfoMessageChanged();
|
||||||
void sendShortcutTextChanged();
|
void sendShortcutTextChanged();
|
||||||
void activeRulesChanged();
|
|
||||||
void activeRulesCountChanged();
|
|
||||||
|
|
||||||
void useToolsChanged();
|
void useToolsChanged();
|
||||||
void useThinkingChanged();
|
void useThinkingChanged();
|
||||||
void currentMessageEditsStatsChanged();
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
void isThinkingSupportChanged();
|
void isThinkingSupportChanged();
|
||||||
void availableConfigurationsChanged();
|
|
||||||
void currentConfigurationChanged();
|
|
||||||
|
|
||||||
void availableAgentRolesChanged();
|
void availableChatAgentsChanged();
|
||||||
void currentAgentRoleChanged();
|
void currentChatAgentChanged();
|
||||||
void baseSystemPromptChanged();
|
void availableRolesChanged();
|
||||||
|
void currentRoleChanged();
|
||||||
|
|
||||||
void isCompressingChanged();
|
void isCompressingChanged();
|
||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
@@ -256,25 +237,22 @@ private:
|
|||||||
bool deferSendForAutoCompress(
|
bool deferSendForAutoCompress(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QStringList &attachments,
|
const QStringList &attachments,
|
||||||
const QStringList &linkedFiles,
|
const QStringList &linkedFiles);
|
||||||
bool useTools,
|
|
||||||
bool useThinking);
|
|
||||||
void dispatchSend(
|
void dispatchSend(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QStringList &attachments,
|
const QStringList &attachments,
|
||||||
const QStringList &linkedFiles,
|
const QStringList &linkedFiles);
|
||||||
bool useTools,
|
|
||||||
bool useThinking);
|
|
||||||
bool hasImageAttachments(const QStringList &attachments) const;
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
SessionFileRegistry *sessionFileRegistry() const;
|
SessionFileRegistry *sessionFileRegistry() const;
|
||||||
Skills::SkillsManager *skillsManager() const;
|
Skills::SkillsManager *skillsManager() const;
|
||||||
|
AgentFactory *agentFactory() const;
|
||||||
|
SessionManager *sessionManager() const;
|
||||||
|
|
||||||
|
QodeAssist::ConversationHistory *m_history;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
ChatFileManager *m_fileManager;
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
@@ -283,8 +261,6 @@ private:
|
|||||||
QString message;
|
QString message;
|
||||||
QStringList attachments;
|
QStringList attachments;
|
||||||
QStringList linkedFiles;
|
QStringList linkedFiles;
|
||||||
bool useTools = false;
|
|
||||||
bool useThinking = false;
|
|
||||||
bool active = false;
|
bool active = false;
|
||||||
};
|
};
|
||||||
PendingSend m_pendingSend;
|
PendingSend m_pendingSend;
|
||||||
@@ -294,13 +270,14 @@ private:
|
|||||||
QList<Core::IEditor *> m_currentEditors;
|
QList<Core::IEditor *> m_currentEditors;
|
||||||
bool m_isRequestInProgress;
|
bool m_isRequestInProgress;
|
||||||
QString m_lastErrorMessage;
|
QString m_lastErrorMessage;
|
||||||
QVariantList m_activeRules;
|
|
||||||
|
|
||||||
QString m_lastInfoMessage;
|
QString m_lastInfoMessage;
|
||||||
|
|
||||||
|
QString m_currentRole = QStringLiteral("developer");
|
||||||
|
QStringList m_availableRoles;
|
||||||
|
|
||||||
ChatCompressor *m_chatCompressor;
|
ChatCompressor *m_chatCompressor;
|
||||||
AgentRoleController *m_agentRoleController;
|
ChatAgentController *m_agentController;
|
||||||
ChatConfigurationController *m_configurationController;
|
|
||||||
FileEditController *m_fileEditController;
|
FileEditController *m_fileEditController;
|
||||||
InputTokenCounter *m_tokenCounter;
|
InputTokenCounter *m_tokenCounter;
|
||||||
ChatHistoryStore *m_historyStore;
|
ChatHistoryStore *m_historyStore;
|
||||||
@@ -308,6 +285,8 @@ private:
|
|||||||
mutable bool m_sessionFileRegistryResolved = false;
|
mutable bool m_sessionFileRegistryResolved = false;
|
||||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||||
mutable bool m_skillsManagerResolved = false;
|
mutable bool m_skillsManagerResolved = false;
|
||||||
|
mutable QPointer<AgentFactory> m_agentFactory;
|
||||||
|
mutable QPointer<SessionManager> m_sessionManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <memory>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -13,12 +14,57 @@
|
|||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <MessageSerializer.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
const QString ChatSerializer::VERSION = "0.2";
|
namespace {
|
||||||
|
|
||||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
|
||||||
|
|
||||||
|
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
|
||||||
|
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
|
||||||
|
|
||||||
|
void registerEditFromResult(const QString &result)
|
||||||
{
|
{
|
||||||
|
const int pos = result.indexOf(kFileEditMarker);
|
||||||
|
if (pos < 0)
|
||||||
|
return;
|
||||||
|
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
if (!doc.isObject())
|
||||||
|
return;
|
||||||
|
const QJsonObject obj = doc.object();
|
||||||
|
const QString editId = obj.value("edit_id").toString();
|
||||||
|
const QString filePath = obj.value("file").toString();
|
||||||
|
if (editId.isEmpty() || filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Context::ChangesManager::instance().addFileEdit(
|
||||||
|
editId,
|
||||||
|
filePath,
|
||||||
|
obj.value("old_content").toString(),
|
||||||
|
obj.value("new_content").toString(),
|
||||||
|
/*autoApply=*/false,
|
||||||
|
/*isFromHistory=*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const QString ChatSerializer::VERSION = "0.3";
|
||||||
|
|
||||||
|
SerializationResult ChatSerializer::saveToFile(
|
||||||
|
const ConversationHistory *history, const QString &filePath)
|
||||||
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
if (!ensureDirectoryExists(filePath)) {
|
if (!ensureDirectoryExists(filePath)) {
|
||||||
return {false, "Failed to create directory structure"};
|
return {false, "Failed to create directory structure"};
|
||||||
}
|
}
|
||||||
@@ -28,9 +74,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = serializeChat(model, filePath);
|
QJsonDocument doc(serializeChat(history));
|
||||||
QJsonDocument doc(root);
|
|
||||||
|
|
||||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||||
}
|
}
|
||||||
@@ -38,8 +82,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {true, QString()};
|
return {true, QString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
SerializationResult ChatSerializer::loadFromFile(
|
||||||
|
ConversationHistory *history, const QString &filePath)
|
||||||
{
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||||
@@ -51,180 +99,140 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
|||||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = doc.object();
|
const QJsonObject root = doc.object();
|
||||||
QString version = root["version"].toString();
|
const QString version = root["version"].toString();
|
||||||
|
|
||||||
if (!validateVersion(version)) {
|
if (!validateVersion(version)) {
|
||||||
return {false, QString("Unsupported version: %1").arg(version)};
|
return {false, QString("Unsupported version: %1").arg(version)};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deserializeChat(model, root, filePath)) {
|
if (version == VERSION)
|
||||||
return {false, "Failed to deserialize chat data"};
|
return loadCurrent(history, root);
|
||||||
}
|
return loadLegacy(history, root);
|
||||||
|
|
||||||
return {true, QString()};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(
|
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
|
||||||
const ChatModel::Message &message, const QString &chatFilePath)
|
|
||||||
{
|
|
||||||
QJsonObject messageObj;
|
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
|
||||||
messageObj["content"] = message.content;
|
|
||||||
messageObj["id"] = message.id;
|
|
||||||
|
|
||||||
if (message.isRedacted) {
|
|
||||||
messageObj["isRedacted"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
|
||||||
messageObj["signature"] = message.signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
|
||||||
if (!message.toolName.isEmpty())
|
|
||||||
messageObj["toolName"] = message.toolName;
|
|
||||||
if (!message.toolArguments.isEmpty())
|
|
||||||
messageObj["toolArguments"] = message.toolArguments;
|
|
||||||
if (!message.toolResult.isEmpty())
|
|
||||||
messageObj["toolResult"] = message.toolResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.attachments.isEmpty()) {
|
|
||||||
QJsonArray attachmentsArray;
|
|
||||||
for (const auto &attachment : message.attachments) {
|
|
||||||
QJsonObject attachmentObj;
|
|
||||||
attachmentObj["fileName"] = attachment.filename;
|
|
||||||
attachmentObj["storedPath"] = attachment.content;
|
|
||||||
attachmentsArray.append(attachmentObj);
|
|
||||||
}
|
|
||||||
messageObj["attachments"] = attachmentsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.images.isEmpty()) {
|
|
||||||
QJsonArray imagesArray;
|
|
||||||
for (const auto &image : message.images) {
|
|
||||||
QJsonObject imageObj;
|
|
||||||
imageObj["fileName"] = image.fileName;
|
|
||||||
imageObj["storedPath"] = image.storedPath;
|
|
||||||
imageObj["mediaType"] = image.mediaType;
|
|
||||||
imagesArray.append(imageObj);
|
|
||||||
}
|
|
||||||
messageObj["images"] = imagesArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.promptTokens > 0 || message.completionTokens > 0) {
|
|
||||||
QJsonObject usageObj;
|
|
||||||
usageObj["promptTokens"] = message.promptTokens;
|
|
||||||
usageObj["completionTokens"] = message.completionTokens;
|
|
||||||
if (message.cachedPromptTokens > 0)
|
|
||||||
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
|
|
||||||
if (message.reasoningTokens > 0)
|
|
||||||
usageObj["reasoningTokens"] = message.reasoningTokens;
|
|
||||||
messageObj["usage"] = usageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatModel::Message ChatSerializer::deserializeMessage(
|
|
||||||
const QJsonObject &json, const QString &chatFilePath)
|
|
||||||
{
|
|
||||||
ChatModel::Message message;
|
|
||||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
|
||||||
message.content = json["content"].toString();
|
|
||||||
message.id = json["id"].toString();
|
|
||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
|
||||||
message.signature = json["signature"].toString();
|
|
||||||
message.toolName = json["toolName"].toString();
|
|
||||||
message.toolArguments = json["toolArguments"].toObject();
|
|
||||||
message.toolResult = json["toolResult"].toString();
|
|
||||||
|
|
||||||
if (json.contains("attachments")) {
|
|
||||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
|
||||||
for (const auto &attachmentValue : attachmentsArray) {
|
|
||||||
QJsonObject attachmentObj = attachmentValue.toObject();
|
|
||||||
Context::ContentFile attachment;
|
|
||||||
attachment.filename = attachmentObj["fileName"].toString();
|
|
||||||
attachment.content = attachmentObj["storedPath"].toString();
|
|
||||||
message.attachments.append(attachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("images")) {
|
|
||||||
QJsonArray imagesArray = json["images"].toArray();
|
|
||||||
for (const auto &imageValue : imagesArray) {
|
|
||||||
QJsonObject imageObj = imageValue.toObject();
|
|
||||||
ChatModel::ImageAttachment image;
|
|
||||||
image.fileName = imageObj["fileName"].toString();
|
|
||||||
image.storedPath = imageObj["storedPath"].toString();
|
|
||||||
image.mediaType = imageObj["mediaType"].toString();
|
|
||||||
message.images.append(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("usage")) {
|
|
||||||
const QJsonObject usageObj = json["usage"].toObject();
|
|
||||||
message.promptTokens = usageObj["promptTokens"].toInt();
|
|
||||||
message.completionTokens = usageObj["completionTokens"].toInt();
|
|
||||||
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
|
|
||||||
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
|
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray;
|
QJsonArray messagesArray;
|
||||||
for (const auto &message : model->getChatHistory()) {
|
for (const auto &message : history->messages())
|
||||||
messagesArray.append(serializeMessage(message, chatFilePath));
|
messagesArray.append(MessageSerializer::toJson(message));
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
root["version"] = VERSION;
|
root["version"] = VERSION;
|
||||||
root["messages"] = messagesArray;
|
root["messages"] = messagesArray;
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::deserializeChat(
|
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
|
||||||
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray = json["messages"].toArray();
|
history->clear();
|
||||||
QVector<ChatModel::Message> messages;
|
|
||||||
messages.reserve(messagesArray.size());
|
|
||||||
|
|
||||||
for (const auto &messageValue : messagesArray) {
|
const QJsonArray messagesArray = root["messages"].toArray();
|
||||||
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
|
for (const auto &value : messagesArray) {
|
||||||
|
bool ok = false;
|
||||||
|
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
|
||||||
|
if (ok)
|
||||||
|
history->append(std::move(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
registerHistoricalFileEdits(history);
|
||||||
|
return {true, QString()};
|
||||||
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(true);
|
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
|
||||||
|
{
|
||||||
|
history->clear();
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
const QJsonArray arr = root["messages"].toArray();
|
||||||
model->addMessage(
|
int i = 0;
|
||||||
message.content,
|
while (i < arr.size()) {
|
||||||
message.role,
|
const QJsonObject mj = arr[i].toObject();
|
||||||
message.id,
|
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
|
||||||
message.attachments,
|
|
||||||
message.images,
|
if (role == LegacyRole::Tool) {
|
||||||
message.isRedacted,
|
Message assistant(Message::Role::Assistant);
|
||||||
message.signature);
|
Message toolResults(Message::Role::User);
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
while (i < arr.size()
|
||||||
model->setToolMessageData(
|
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
|
||||||
message.id, message.toolName, message.toolArguments, message.toolResult);
|
const QJsonObject tj = arr[i].toObject();
|
||||||
|
const QString toolName = tj["toolName"].toString();
|
||||||
|
const QString id = tj["id"].toString();
|
||||||
|
if (!toolName.isEmpty()) {
|
||||||
|
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||||
|
id, toolName, tj["toolArguments"].toObject()));
|
||||||
|
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
|
||||||
|
id, tj["toolResult"].toString()));
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (!assistant.blocks().empty()) {
|
||||||
|
history->append(std::move(assistant));
|
||||||
|
history->append(std::move(toolResults));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
|
||||||
|
if (role == LegacyRole::FileEdit)
|
||||||
|
continue; // derived from the tool result in the new model
|
||||||
|
|
||||||
|
if (role == LegacyRole::Thinking) {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
const QString signature = mj["signature"].toString();
|
||||||
|
Message assistant(Message::Role::Assistant);
|
||||||
|
if (mj["isRedacted"].toBool(false)) {
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
|
||||||
|
} else {
|
||||||
|
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
|
||||||
|
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
|
||||||
|
}
|
||||||
|
history->append(std::move(assistant));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == LegacyRole::User) {
|
||||||
|
Message user(Message::Role::User, mj["id"].toString());
|
||||||
|
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
|
||||||
|
for (const auto &a : mj["attachments"].toArray()) {
|
||||||
|
const QJsonObject ao = a.toObject();
|
||||||
|
user.appendBlock(std::make_unique<StoredAttachmentContent>(
|
||||||
|
ao["fileName"].toString(), ao["storedPath"].toString()));
|
||||||
|
}
|
||||||
|
for (const auto &im : mj["images"].toArray()) {
|
||||||
|
const QJsonObject io = im.toObject();
|
||||||
|
user.appendBlock(std::make_unique<StoredImageContent>(
|
||||||
|
io["fileName"].toString(),
|
||||||
|
io["storedPath"].toString(),
|
||||||
|
io["mediaType"].toString()));
|
||||||
|
}
|
||||||
|
history->append(std::move(user));
|
||||||
|
} else {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
if (content.trimmed().isEmpty())
|
||||||
|
continue;
|
||||||
|
const Message::Role mapped
|
||||||
|
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
|
||||||
|
Message message(mapped, mj["id"].toString());
|
||||||
|
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||||
|
history->append(std::move(message));
|
||||||
}
|
}
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
|
||||||
.arg(message.images.size())
|
|
||||||
.arg(message.isRedacted)
|
|
||||||
.arg(message.signature.length()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
registerHistoricalFileEdits(history);
|
||||||
|
return {true, QString()};
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history)
|
||||||
|
{
|
||||||
|
for (const auto &message : history->messages()) {
|
||||||
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
|
||||||
|
registerEditFromResult(tr->result());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||||
@@ -236,18 +244,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatSerializer::validateVersion(const QString &version)
|
bool ChatSerializer::validateVersion(const QString &version)
|
||||||
{
|
{
|
||||||
if (version == VERSION) {
|
return version == VERSION || version == "0.2" || version == "0.1";
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version == "0.1") {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
|
||||||
"to _content");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -21,26 +22,26 @@ struct SerializationResult
|
|||||||
class ChatSerializer
|
class ChatSerializer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
static SerializationResult saveToFile(
|
||||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
const ConversationHistory *history, const QString &filePath);
|
||||||
|
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
|
||||||
// Public for testing purposes
|
|
||||||
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
|
|
||||||
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
|
|
||||||
// Content management (images and text files)
|
// Content management (images and text files)
|
||||||
static QString getChatContentFolder(const QString &chatFilePath);
|
static QString getChatContentFolder(const QString &chatFilePath);
|
||||||
static bool saveContentToStorage(const QString &chatFilePath,
|
static bool saveContentToStorage(
|
||||||
const QString &fileName,
|
const QString &chatFilePath,
|
||||||
const QString &base64Data,
|
const QString &fileName,
|
||||||
QString &storedPath);
|
const QString &base64Data,
|
||||||
|
QString &storedPath);
|
||||||
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static const QString VERSION;
|
||||||
static constexpr int CURRENT_VERSION = 1;
|
|
||||||
|
static QJsonObject serializeChat(const ConversationHistory *history);
|
||||||
|
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
|
||||||
|
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
|
||||||
|
static void registerHistoricalFileEdits(const ConversationHistory *history);
|
||||||
|
|
||||||
static bool ensureDirectoryExists(const QString &filePath);
|
static bool ensureDirectoryExists(const QString &filePath);
|
||||||
static bool validateVersion(const QString &version);
|
static bool validateVersion(const QString &version);
|
||||||
|
|||||||
@@ -4,73 +4,104 @@
|
|||||||
|
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <coreplugin/editormanager/ieditor.h>
|
||||||
|
#include <coreplugin/idocument.h>
|
||||||
#include <projectexplorer/buildconfiguration.h>
|
#include <projectexplorer/buildconfiguration.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
#include <projectexplorer/target.h>
|
#include <projectexplorer/target.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QImageReader>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <ConversationHistory.hpp>
|
||||||
#include <coreplugin/editormanager/ieditor.h>
|
#include <ContextRenderer.hpp>
|
||||||
#include <coreplugin/idocument.h>
|
#include <Message.hpp>
|
||||||
#include <projectexplorer/project.h>
|
#include <PluginBlocks.hpp>
|
||||||
#include <projectexplorer/projectexplorer.h>
|
#include <Session.hpp>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
|
||||||
|
|
||||||
#include "tools/ReadOriginalHistoryTool.hpp"
|
#include "tools/ReadOriginalHistoryTool.hpp"
|
||||||
#include "tools/TodoTool.hpp"
|
#include "tools/TodoTool.hpp"
|
||||||
|
#include "tools/ToolsRegistration.hpp"
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "SkillsSettings.hpp"
|
#include "SkillsSettings.hpp"
|
||||||
#include "ToolsSettings.hpp"
|
#include "ToolsSettings.hpp"
|
||||||
#include <RulesLoader.hpp>
|
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
#include <sources/skills/SkillsManager.hpp>
|
#include <sources/skills/SkillsManager.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ClientInterface::ClientInterface(
|
namespace {
|
||||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
struct StoredImage
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
QString mediaType;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_promptProvider(promptProvider)
|
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
, m_contextManager(new Context::ContextManager(this))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
|
||||||
{
|
|
||||||
m_skillsManager = skillsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientInterface::~ClientInterface()
|
ClientInterface::~ClientInterface()
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
||||||
|
{
|
||||||
|
m_skillsManager = skillsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setHistory(ConversationHistory *history)
|
||||||
|
{
|
||||||
|
m_history = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setActiveAgent(const QString &agentName)
|
||||||
|
{
|
||||||
|
m_activeAgent = agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setActiveRole(const QString &roleId)
|
||||||
|
{
|
||||||
|
m_activeRoleId = roleId;
|
||||||
|
}
|
||||||
|
|
||||||
void ClientInterface::sendMessage(
|
void ClientInterface::sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments,
|
const QList<QString> &attachments,
|
||||||
const QList<QString> &linkedFiles,
|
const QList<QString> &linkedFiles)
|
||||||
bool useTools,
|
|
||||||
bool useThinking)
|
|
||||||
{
|
{
|
||||||
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
||||||
LOG_MESSAGE("Ignoring empty chat message");
|
LOG_MESSAGE("Ignoring empty chat message");
|
||||||
@@ -78,19 +109,16 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
|
|
||||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||||
|
|
||||||
QList<QString> imageFiles;
|
QList<QString> imageFiles;
|
||||||
QList<QString> textFiles;
|
QList<QString> textFiles;
|
||||||
|
|
||||||
for (const QString &filePath : attachments) {
|
for (const QString &filePath : attachments) {
|
||||||
if (isImageFile(filePath)) {
|
if (isImageFile(filePath))
|
||||||
imageFiles.append(filePath);
|
imageFiles.append(filePath);
|
||||||
} else {
|
else
|
||||||
textFiles.append(filePath);
|
textFiles.append(filePath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<Context::ContentFile> storedAttachments;
|
QList<Context::ContentFile> storedAttachments;
|
||||||
@@ -112,24 +140,19 @@ void ClientInterface::sendMessage(
|
|||||||
.arg(textFiles.size()));
|
.arg(textFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ChatModel::ImageAttachment> imageAttachments;
|
QList<StoredImage> storedImages;
|
||||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
for (const QString &imagePath : imageFiles) {
|
for (const QString &imagePath : imageFiles) {
|
||||||
QString base64Data = encodeImageToBase64(imagePath);
|
QString base64Data = encodeImageToBase64(imagePath);
|
||||||
if (base64Data.isEmpty()) {
|
if (base64Data.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
QString storedPath;
|
QString storedPath;
|
||||||
QFileInfo fileInfo(imagePath);
|
QFileInfo fileInfo(imagePath);
|
||||||
if (ChatSerializer::saveContentToStorage(
|
if (ChatSerializer::saveContentToStorage(
|
||||||
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||||
ChatModel::ImageAttachment imageAttachment;
|
storedImages.append(
|
||||||
imageAttachment.fileName = fileInfo.fileName();
|
{fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)});
|
||||||
imageAttachment.storedPath = storedPath;
|
|
||||||
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
|
||||||
imageAttachments.append(imageAttachment);
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,328 +161,287 @@ void ClientInterface::sendMessage(
|
|||||||
.arg(imageFiles.size()));
|
.arg(imageFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
if (!m_sessionManager) {
|
||||||
|
const QString error = QStringLiteral("Chat session manager is not available");
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
return;
|
||||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
}
|
||||||
|
if (!m_history) {
|
||||||
if (!provider) {
|
const QString error = QStringLiteral("Chat history is not available");
|
||||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
QString sessionError;
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
|
||||||
|
if (!session) {
|
||||||
if (!promptTemplate) {
|
const QString error = sessionError.isEmpty()
|
||||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
? QStringLiteral("No chat agent selected")
|
||||||
|
: sessionError;
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData context;
|
auto *client = session->client();
|
||||||
|
if (!client) {
|
||||||
const bool isToolsEnabled = useTools;
|
const QString error = QStringLiteral("Chat agent has no live client");
|
||||||
|
LOG_MESSAGE(error);
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
m_sessionManager->removeSession(session);
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
emit errorOccurred(error);
|
||||||
|
return;
|
||||||
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
|
||||||
if (!lastRoleId.isEmpty()) {
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (!role.id.isEmpty())
|
|
||||||
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
|
|
||||||
if (project) {
|
|
||||||
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
|
|
||||||
systemPrompt += QString(
|
|
||||||
"\n# Project source root: %1"
|
|
||||||
"\n# All new source files, headers, QML and CMake edits MUST be "
|
|
||||||
"created or modified under this directory. Use absolute paths "
|
|
||||||
"rooted here, or project-relative paths.")
|
|
||||||
.arg(project->projectDirectory().toUrlishString());
|
|
||||||
|
|
||||||
if (auto target = project->activeTarget()) {
|
|
||||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
|
||||||
systemPrompt
|
|
||||||
+= QString(
|
|
||||||
"\n# Build output directory (compiler artifacts only — do NOT "
|
|
||||||
"create or edit source files here): %1")
|
|
||||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString projectRules
|
|
||||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
systemPrompt += QString("\n# No active project in IDE");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
|
||||||
QStringList projectSkillDirs;
|
|
||||||
if (project) {
|
|
||||||
Settings::ProjectSettings projectSettings(project);
|
|
||||||
projectSkillDirs = Settings::SkillsSettings::splitLines(
|
|
||||||
projectSettings.projectSkillDirs());
|
|
||||||
}
|
|
||||||
m_skillsManager->configure(
|
|
||||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
|
||||||
Settings::SkillsSettings::splitPaths(
|
|
||||||
Settings::skillsSettings().globalSkillRoots()),
|
|
||||||
projectSkillDirs);
|
|
||||||
|
|
||||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
|
||||||
if (!alwaysOnSkills.isEmpty())
|
|
||||||
systemPrompt += QString("\n\n") + alwaysOnSkills;
|
|
||||||
|
|
||||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
|
||||||
if (!skillsCatalog.isEmpty())
|
|
||||||
systemPrompt += QString("\n\n") + skillsCatalog;
|
|
||||||
|
|
||||||
static const QRegularExpression skillCommand(
|
|
||||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
|
||||||
QStringList invokedSkillNames;
|
|
||||||
auto skillMatch = skillCommand.globalMatch(message);
|
|
||||||
while (skillMatch.hasNext()) {
|
|
||||||
const QString skillName = skillMatch.next().captured(1);
|
|
||||||
if (invokedSkillNames.contains(skillName))
|
|
||||||
continue;
|
|
||||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
|
||||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
|
||||||
invokedSkillNames << skillName;
|
|
||||||
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
|
|
||||||
.arg(invokedSkill->name, invokedSkill->body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
|
||||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
|
||||||
}
|
|
||||||
context.systemPrompt = systemPrompt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool toolHistory = promptTemplate->supportsToolHistory();
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
|
Templates::ContextRenderer::Bindings bindings;
|
||||||
|
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||||
|
bindings.homeDir = QDir::homePath();
|
||||||
|
bindings.roleId = m_activeRoleId;
|
||||||
|
session->setContextBindings(bindings);
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
const QString chatFilePath = m_chatFilePath;
|
||||||
int toolCallMsgIdx = -1;
|
session->setContentLoader([chatFilePath](const QString &storedPath) {
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
|
||||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
});
|
||||||
if (!toolHistory || msg.toolName.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolCallMsgIdx < 0) {
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
PluginLLMCore::Message assistantCall;
|
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||||
assistantCall.role = "assistant";
|
client->setTransferTimeout(
|
||||||
messages.append(assistantCall);
|
|
||||||
toolCallMsgIdx = messages.size() - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ToolCall call;
|
|
||||||
call.id = msg.id;
|
|
||||||
call.name = msg.toolName;
|
|
||||||
call.arguments = msg.toolArguments;
|
|
||||||
messages[toolCallMsgIdx].toolCalls.append(call);
|
|
||||||
|
|
||||||
PluginLLMCore::Message toolResult;
|
|
||||||
toolResult.role = "tool";
|
|
||||||
toolResult.toolCallId = msg.id;
|
|
||||||
toolResult.toolName = msg.toolName;
|
|
||||||
toolResult.content = msg.toolResult;
|
|
||||||
messages.append(toolResult);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
toolCallMsgIdx = -1;
|
|
||||||
|
|
||||||
if (msg.role == ChatModel::ChatRole::FileEdit) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::Message apiMessage;
|
|
||||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
|
||||||
apiMessage.content = msg.content;
|
|
||||||
|
|
||||||
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
|
||||||
apiMessage.content += "\n\nAttached files:";
|
|
||||||
for (const auto &attachment : msg.attachments) {
|
|
||||||
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
|
||||||
if (!fileContent.isEmpty()) {
|
|
||||||
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
|
||||||
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
|
||||||
.arg(attachment.filename, decodedContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
|
||||||
apiMessage.isRedacted = msg.isRedacted;
|
|
||||||
apiMessage.signature = msg.signature;
|
|
||||||
|
|
||||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
|
||||||
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
|
||||||
auto apiImages = loadImagesFromStorage(msg.images);
|
|
||||||
if (!apiImages.isEmpty()) {
|
|
||||||
apiMessage.images = apiImages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(apiMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageFiles.isEmpty()
|
|
||||||
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
|
||||||
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
|
||||||
.arg(provider->name(), QString::number(imageFiles.size())));
|
|
||||||
}
|
|
||||||
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
QJsonObject payload{
|
|
||||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
|
||||||
|
|
||||||
provider->prepareRequest(
|
|
||||||
payload,
|
|
||||||
promptTemplate,
|
|
||||||
context,
|
|
||||||
PluginLLMCore::RequestType::Chat,
|
|
||||||
useTools,
|
|
||||||
useThinking);
|
|
||||||
|
|
||||||
provider->client()->setMaxToolContinuations(
|
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
connect(
|
const QString chatContext = buildChatContextLayer(message, linkedFiles);
|
||||||
provider->client(),
|
if (!chatContext.isEmpty())
|
||||||
&::LLMQore::BaseClient::chunkReceived,
|
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
||||||
this,
|
|
||||||
&ClientInterface::handlePartialResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRequestFinalized,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::toolStarted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleToolExecutionStarted,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::toolResultReady,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleToolExecutionCompleted,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::thinkingBlockReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleThinkingBlockReceived,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
|
||||||
: promptTemplate->endpoint();
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider, !toolHistory};
|
for (const auto &attachment : storedAttachments) {
|
||||||
|
blocks.push_back(
|
||||||
|
std::make_unique<StoredAttachmentContent>(attachment.filename, attachment.content));
|
||||||
|
}
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
if (!storedImages.isEmpty() && session->supportsImages()) {
|
||||||
|
for (const auto &image : storedImages) {
|
||||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
blocks.push_back(std::make_unique<StoredImageContent>(
|
||||||
&& provider->toolsManager()) {
|
image.fileName, image.storedPath, image.mediaType));
|
||||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
}
|
||||||
provider->toolsManager()->tool("todo_tool"))) {
|
} else if (!storedImages.isEmpty() && !session->supportsImages()) {
|
||||||
|
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
|
||||||
|
.arg(m_activeAgent)
|
||||||
|
.arg(storedImages.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
|
if (auto *todoTool
|
||||||
|
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
|
||||||
todoTool->setCurrentSessionId(m_chatFilePath);
|
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||||
}
|
}
|
||||||
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
||||||
provider->toolsManager()->tool("read_original_history"))) {
|
client->tools()->tool("read_original_history"))) {
|
||||||
historyTool->setCurrentSessionId(m_chatFilePath);
|
historyTool->setCurrentSessionId(m_chatFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) {
|
||||||
|
onSessionEvent(session, ev);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
session, &Session::finished, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); });
|
||||||
|
connect(
|
||||||
|
session, &Session::failed, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onSessionFailed(id, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
|
||||||
|
.arg(m_activeAgent, session->lastError().message);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emit errorOccurred(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||||
|
|
||||||
|
emit requestStarted(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::requestIdForSession(Session *session) const
|
||||||
|
{
|
||||||
|
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||||
|
if (it.value().session == session)
|
||||||
|
return it.key();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev)
|
||||||
|
{
|
||||||
|
if (ev.kind() != ResponseEvent::Kind::Usage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto *usage = ev.as<ResponseEvents::Usage>();
|
||||||
|
if (!usage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString requestId = requestIdForSession(session);
|
||||||
|
if (!requestId.isEmpty()) {
|
||||||
|
m_chatModel->setMessageUsage(
|
||||||
|
requestId,
|
||||||
|
usage->inputTokens,
|
||||||
|
usage->outputTokens,
|
||||||
|
usage->cachedTokens,
|
||||||
|
usage->reasoningTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit messageUsageReceived(
|
||||||
|
usage->inputTokens, usage->outputTokens, usage->cachedTokens, usage->reasoningTokens);
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||||
|
.arg(requestId)
|
||||||
|
.arg(usage->inputTokens)
|
||||||
|
.arg(usage->outputTokens)
|
||||||
|
.arg(usage->cachedTokens)
|
||||||
|
.arg(usage->reasoningTokens));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionFinished(const QString &requestId)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
|
|
||||||
|
QString applyError;
|
||||||
|
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
|
||||||
|
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||||
|
.arg(requestId, applyError));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit messageReceivedCompletely();
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
|
||||||
|
emit errorOccurred(error.message);
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::buildChatContextLayer(
|
||||||
|
const QString &message, const QList<QString> &linkedFiles) const
|
||||||
|
{
|
||||||
|
QString context;
|
||||||
|
|
||||||
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
|
if (project) {
|
||||||
|
context += QString("# Active project: %1").arg(project->displayName());
|
||||||
|
context += QString(
|
||||||
|
"\n# Project source root: %1"
|
||||||
|
"\n# All new source files, headers, QML and CMake edits MUST be "
|
||||||
|
"created or modified under this directory. Use absolute paths "
|
||||||
|
"rooted here, or project-relative paths.")
|
||||||
|
.arg(project->projectDirectory().toUrlishString());
|
||||||
|
|
||||||
|
if (auto target = project->activeTarget()) {
|
||||||
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
|
context += QString(
|
||||||
|
"\n# Build output directory (compiler artifacts only — do NOT "
|
||||||
|
"create or edit source files here): %1")
|
||||||
|
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context += QString("# No active project in IDE");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||||
|
QStringList projectSkillDirs;
|
||||||
|
if (project) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
projectSkillDirs
|
||||||
|
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
|
||||||
|
}
|
||||||
|
m_skillsManager->configure(
|
||||||
|
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||||
|
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
|
||||||
|
projectSkillDirs);
|
||||||
|
|
||||||
|
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||||
|
if (!alwaysOnSkills.isEmpty())
|
||||||
|
context += QString("\n\n") + alwaysOnSkills;
|
||||||
|
|
||||||
|
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||||
|
if (!skillsCatalog.isEmpty())
|
||||||
|
context += QString("\n\n") + skillsCatalog;
|
||||||
|
|
||||||
|
static const QRegularExpression skillCommand(
|
||||||
|
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||||
|
QStringList invokedSkillNames;
|
||||||
|
auto skillMatch = skillCommand.globalMatch(message);
|
||||||
|
while (skillMatch.hasNext()) {
|
||||||
|
const QString skillName = skillMatch.next().captured(1);
|
||||||
|
if (invokedSkillNames.contains(skillName))
|
||||||
|
continue;
|
||||||
|
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||||
|
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||||
|
invokedSkillNames << skillName;
|
||||||
|
context += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||||
|
.arg(invokedSkill->name, invokedSkill->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linkedFiles.isEmpty()) {
|
||||||
|
context += "\n\nLinked files for reference:\n";
|
||||||
|
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||||
|
for (const auto &file : contentFiles)
|
||||||
|
context += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
{
|
{
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
if (m_history)
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
m_history->clear();
|
||||||
|
|
||||||
if (provider && !m_chatFilePath.isEmpty()
|
|
||||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
|
||||||
&& provider->toolsManager()) {
|
|
||||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
|
||||||
provider->toolsManager()->tool("todo_tool"))) {
|
|
||||||
todoTool->clearSession(m_chatFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
QSet<PluginLLMCore::Provider *> providers;
|
const auto requests = m_activeRequests;
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value().provider) {
|
|
||||||
providers.insert(it.value().provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
|
||||||
disconnect(provider->client(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
if (ctx.provider) {
|
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
m_awaitingContinuation.clear();
|
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||||
}
|
Session *session = it.value().session;
|
||||||
|
if (session && m_sessionManager)
|
||||||
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
m_sessionManager->removeSession(session);
|
||||||
{
|
|
||||||
const auto message = response.trimmed();
|
|
||||||
|
|
||||||
if (!message.isEmpty()) {
|
|
||||||
QString messageId = request["id"].toString();
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::getCurrentFileContext() const
|
QString ClientInterface::getCurrentFileContext() const
|
||||||
@@ -486,166 +468,11 @@ QString ClientInterface::getCurrentFileContext() const
|
|||||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::getSystemPromptWithLinkedFiles(
|
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const
|
|
||||||
{
|
|
||||||
QString updatedPrompt = basePrompt;
|
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
|
||||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
|
||||||
|
|
||||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
|
||||||
for (const auto &file : contentFiles) {
|
|
||||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
Context::ContextManager *ClientInterface::contextManager() const
|
Context::ContextManager *ClientInterface::contextManager() const
|
||||||
{
|
{
|
||||||
return m_contextManager;
|
return m_contextManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_awaitingContinuation.remove(requestId)) {
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_accumulatedResponses[requestId] += partialText;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
|
|
||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
|
||||||
|
|
||||||
QString applyError;
|
|
||||||
bool applySuccess
|
|
||||||
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
|
|
||||||
|
|
||||||
if (!applySuccess) {
|
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
|
||||||
.arg(requestId, applyError));
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
|
|
||||||
+ ": " + finalText);
|
|
||||||
emit messageReceivedCompletely();
|
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
m_awaitingContinuation.remove(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId))
|
|
||||||
return;
|
|
||||||
if (!info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
m_chatModel->setMessageUsage(
|
|
||||||
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
|
||||||
|
|
||||||
emit messageUsageReceived(
|
|
||||||
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
|
||||||
emit errorOccurred(error);
|
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
m_awaitingContinuation.remove(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleThinkingBlockReceived(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_awaitingContinuation.remove(requestId)) {
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thinking.isEmpty()) {
|
|
||||||
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
|
||||||
} else {
|
|
||||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionStarted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments)
|
|
||||||
{
|
|
||||||
const auto requestIt = m_activeRequests.constFind(requestId);
|
|
||||||
if (requestIt == m_activeRequests.constEnd()) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestIt->dropPreToolText) {
|
|
||||||
m_chatModel->dropTrailingAssistantMessage(requestId);
|
|
||||||
}
|
|
||||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
|
|
||||||
m_awaitingContinuation.insert(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ClientInterface::isImageFile(const QString &filePath) const
|
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
@@ -693,46 +520,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
|||||||
return imageData.toBase64();
|
return imageData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
|
||||||
const QList<ChatModel::ImageAttachment> &storedImages) const
|
|
||||||
{
|
|
||||||
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
|
||||||
|
|
||||||
for (const auto &storedImage : storedImages) {
|
|
||||||
QString base64Data
|
|
||||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
|
||||||
if (base64Data.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ImageAttachment apiImage;
|
|
||||||
apiImage.data = base64Data;
|
|
||||||
apiImage.mediaType = storedImage.mediaType;
|
|
||||||
apiImage.isUrl = false;
|
|
||||||
|
|
||||||
apiImages.append(apiImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiImages;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::setChatFilePath(const QString &filePath)
|
void ClientInterface::setChatFilePath(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (provider
|
|
||||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
|
||||||
&& provider->toolsManager()) {
|
|
||||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
|
||||||
provider->toolsManager()->tool("todo_tool"))) {
|
|
||||||
todoTool->clearSession(m_chatFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatFilePath = filePath;
|
m_chatFilePath = filePath;
|
||||||
m_chatModel->setChatFilePath(filePath);
|
m_chatModel->setChatFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSet>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Provider.hpp"
|
#include <ErrorInfo.hpp>
|
||||||
#include "pluginllmcore/IPromptProvider.hpp"
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <ResponseEvent.hpp>
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class SessionManager;
|
||||||
|
class Session;
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
namespace QodeAssist::Skills {
|
||||||
class SkillsManager;
|
class SkillsManager;
|
||||||
}
|
}
|
||||||
@@ -26,23 +31,24 @@ class ClientInterface : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ClientInterface(
|
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||||
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
|
void setActiveAgent(const QString &agentName);
|
||||||
|
void setActiveRole(const QString &roleId);
|
||||||
|
|
||||||
void sendMessage(
|
void sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments = {},
|
const QList<QString> &attachments = {},
|
||||||
const QList<QString> &linkedFiles = {},
|
const QList<QString> &linkedFiles = {});
|
||||||
bool useTools = false,
|
|
||||||
bool useThinking = false);
|
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
Context::ContextManager *contextManager() const;
|
Context::ContextManager *contextManager() const;
|
||||||
|
|
||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
@@ -53,50 +59,35 @@ signals:
|
|||||||
void messageUsageReceived(
|
void messageUsageReceived(
|
||||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
void handleThinkingBlockReceived(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void handleToolExecutionStarted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments);
|
|
||||||
void handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
||||||
|
void onSessionFinished(const QString &requestId);
|
||||||
|
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
|
|
||||||
QString getCurrentFileContext() const;
|
QString getCurrentFileContext() const;
|
||||||
QString getSystemPromptWithLinkedFiles(
|
QString buildChatContextLayer(
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
const QString &message, const QList<QString> &linkedFiles) const;
|
||||||
|
QString requestIdForSession(Session *session) const;
|
||||||
bool isImageFile(const QString &filePath) const;
|
bool isImageFile(const QString &filePath) const;
|
||||||
QString getMediaTypeForImage(const QString &filePath) const;
|
QString getMediaTypeForImage(const QString &filePath) const;
|
||||||
QString encodeImageToBase64(const QString &filePath) const;
|
QString encodeImageToBase64(const QString &filePath) const;
|
||||||
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<Session> session;
|
||||||
bool dropPreToolText = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
|
QPointer<ConversationHistory> m_history;
|
||||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||||
|
QPointer<SessionManager> m_sessionManager;
|
||||||
|
QString m_activeAgent;
|
||||||
|
QString m_activeRoleId;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
QHash<QString, QString> m_accumulatedResponses;
|
|
||||||
QSet<QString> m_awaitingContinuation;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -10,15 +10,13 @@
|
|||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "context/ChangesManager.h"
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
FileEditController::FileEditController(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
|
||||||
{
|
{
|
||||||
auto &changes = Context::ChangesManager::instance();
|
auto &changes = Context::ChangesManager::instance();
|
||||||
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||||
@@ -80,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit applied successfully"));
|
emit infoMessage(QString("File edit applied successfully"));
|
||||||
updateFileEditStatus(editId, "applied");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -95,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit rejected"));
|
emit infoMessage(QString("File edit rejected"));
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -110,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit undone successfully"));
|
emit infoMessage(QString("File edit undone successfully"));
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -163,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
|
||||||
{
|
|
||||||
auto messages = m_chatModel->getChatHistory();
|
|
||||||
for (int i = 0; i < messages.size(); ++i) {
|
|
||||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
|
||||||
QString content = messages[i].content;
|
|
||||||
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
|
|
||||||
QString jsonStr = content;
|
|
||||||
if (markerPos >= 0) {
|
|
||||||
jsonStr = content.mid(markerPos + marker.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject obj = doc.object();
|
|
||||||
obj["status"] = status;
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (!edit.statusMessage.isEmpty()) {
|
|
||||||
obj["status_message"] = edit.statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString updatedContent = marker
|
|
||||||
+ QString::fromUtf8(
|
|
||||||
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
|
||||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
|
||||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::applyAllForCurrentMessage()
|
void FileEditController::applyAllForCurrentMessage()
|
||||||
{
|
{
|
||||||
if (m_currentRequestId.isEmpty()) {
|
if (m_currentRequestId.isEmpty()) {
|
||||||
@@ -223,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
|
|||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
|
|||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,12 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class FileEditController : public QObject
|
class FileEditController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit FileEditController(QObject *parent = nullptr);
|
||||||
|
|
||||||
void setCurrentRequestId(const QString &requestId);
|
void setCurrentRequestId(const QString &requestId);
|
||||||
void clearCurrentRequestId();
|
void clearCurrentRequestId();
|
||||||
@@ -41,9 +39,6 @@ signals:
|
|||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
QString m_currentRequestId;
|
QString m_currentRequestId;
|
||||||
int m_totalEdits{0};
|
int m_totalEdits{0};
|
||||||
int m_appliedEdits{0};
|
int m_appliedEdits{0};
|
||||||
|
|||||||
@@ -6,26 +6,22 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
#include "context/TokenUtils.hpp"
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
InputTokenCounter::InputTokenCounter(
|
InputTokenCounter::InputTokenCounter(
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_history(history)
|
||||||
, m_contextManager(contextManager)
|
, m_contextManager(contextManager)
|
||||||
{
|
{
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
@@ -42,12 +38,6 @@ InputTokenCounter::InputTokenCounter(
|
|||||||
this,
|
this,
|
||||||
&InputTokenCounter::recompute);
|
&InputTokenCounter::recompute);
|
||||||
|
|
||||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
recompute();
|
|
||||||
});
|
|
||||||
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
recompute();
|
recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,24 +64,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
|||||||
recompute();
|
recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputTokenCounter::rewireToolsChangedConnection()
|
|
||||||
{
|
|
||||||
if (m_toolsChangedConn)
|
|
||||||
QObject::disconnect(m_toolsChangedConn);
|
|
||||||
m_toolsChangedConn = {};
|
|
||||||
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
if (!provider)
|
|
||||||
return;
|
|
||||||
auto *tm = provider->toolsManager();
|
|
||||||
if (!tm)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_toolsChangedConn = connect(
|
|
||||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::recompute()
|
void InputTokenCounter::recompute()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokens;
|
int inputTokens = m_messageTokens;
|
||||||
@@ -130,24 +102,10 @@ void InputTokenCounter::recompute()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &history = m_chatModel->getChatHistory();
|
if (m_history) {
|
||||||
for (const auto &message : history) {
|
for (const auto &message : m_history->messages()) {
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
inputTokens += Context::TokenUtils::estimateTokens(message.text());
|
||||||
inputTokens += 4; // + role
|
inputTokens += 4; // + role
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.enableChatTools()) {
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
|
||||||
providerName)) {
|
|
||||||
if (auto *tm = provider->toolsManager()) {
|
|
||||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
|
||||||
if (!toolDefs.isEmpty()) {
|
|
||||||
const QByteArray serialized
|
|
||||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
|
||||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,25 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
class ContextManager;
|
class ContextManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class InputTokenCounter : public QObject
|
class InputTokenCounter : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
InputTokenCounter(
|
InputTokenCounter(
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
ConversationHistory *history,
|
||||||
|
Context::ContextManager *contextManager,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
int inputTokens() const;
|
int inputTokens() const;
|
||||||
|
|
||||||
@@ -37,11 +41,8 @@ signals:
|
|||||||
void inputTokensChanged();
|
void inputTokensChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void rewireToolsChangedConnection();
|
ConversationHistory *m_history;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
QMetaObject::Connection m_toolsChangedConn;
|
|
||||||
|
|
||||||
QStringList m_attachments;
|
QStringList m_attachments;
|
||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
|
|||||||
@@ -138,43 +138,31 @@ ChatRootView {
|
|||||||
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
||||||
? qsTr("Move this chat to an editor tab")
|
? qsTr("Move this chat to an editor tab")
|
||||||
: qsTr("Move this chat to a separate window")
|
: qsTr("Move this chat to a separate window")
|
||||||
toolsButton {
|
|
||||||
checked: root.useTools
|
|
||||||
onCheckedChanged: {
|
|
||||||
root.useTools = toolsButton.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thinkingMode {
|
|
||||||
checked: root.useThinking
|
|
||||||
enabled: root.isThinkingSupport
|
|
||||||
onCheckedChanged: {
|
|
||||||
root.useThinking = thinkingMode.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsButton.onClicked: root.openSettings()
|
settingsButton.onClicked: root.openSettings()
|
||||||
configSelector {
|
agentSelector {
|
||||||
model: root.availableConfigurations
|
model: root.availableChatAgents
|
||||||
displayText: root.currentConfiguration
|
displayText: root.currentChatAgent
|
||||||
onActivated: function(index) {
|
onActivated: function(index) {
|
||||||
if (index > 0) {
|
root.currentChatAgent = root.availableChatAgents[index]
|
||||||
root.applyConfiguration(root.availableConfigurations[index])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.loadAvailableChatAgents()
|
||||||
|
|
||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
root.loadAvailableConfigurations()
|
root.loadAvailableChatAgents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
roleSelector {
|
roleSelector {
|
||||||
model: root.availableAgentRoles
|
model: root.availableRoles
|
||||||
displayText: root.currentAgentRole
|
displayText: root.currentRole
|
||||||
onActivated: function(index) {
|
onActivated: function(index) {
|
||||||
root.applyAgentRole(root.availableAgentRoles[index])
|
root.currentRole = root.availableRoles[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.loadAvailableRoles()
|
||||||
|
|
||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
root.loadAvailableAgentRoles()
|
root.loadAvailableRoles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -839,20 +827,7 @@ ChatRootView {
|
|||||||
x: (parent.width - width) / 2
|
x: (parent.width - width) / 2
|
||||||
y: (parent.height - height) / 2
|
y: (parent.height - height) / 2
|
||||||
|
|
||||||
baseSystemPrompt: root.baseSystemPrompt
|
|
||||||
currentAgentRole: root.currentAgentRole
|
|
||||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
|
||||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
|
||||||
activeRules: root.activeRules
|
|
||||||
activeRulesCount: root.activeRulesCount
|
|
||||||
|
|
||||||
onOpenSettings: root.openSettings()
|
onOpenSettings: root.openSettings()
|
||||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
|
||||||
onOpenRulesFolder: root.openRulesFolder()
|
|
||||||
onRefreshRules: root.refreshRules()
|
|
||||||
onRuleSelected: function(index) {
|
|
||||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ Rectangle {
|
|||||||
property alias pinButton: pinButtonId
|
property alias pinButton: pinButtonId
|
||||||
property alias relocateButton: relocateButtonId
|
property alias relocateButton: relocateButtonId
|
||||||
property alias contextButton: contextButtonId
|
property alias contextButton: contextButtonId
|
||||||
property alias toolsButton: toolsButtonId
|
|
||||||
property alias thinkingMode: thinkingModeId
|
|
||||||
property alias settingsButton: settingsButtonId
|
property alias settingsButton: settingsButtonId
|
||||||
property alias configSelector: configSelectorId
|
property alias agentSelector: agentSelectorId
|
||||||
property alias roleSelector: roleSelector
|
property alias roleSelector: roleSelectorId
|
||||||
property alias relocateTooltip: relocateTooltipId
|
property alias relocateTooltip: relocateTooltipId
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
@@ -134,7 +132,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoAComboBox {
|
||||||
id: configSelectorId
|
id: agentSelectorId
|
||||||
|
|
||||||
implicitHeight: 25
|
implicitHeight: 25
|
||||||
|
|
||||||
@@ -142,14 +140,14 @@ Rectangle {
|
|||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
QoAToolTip {
|
QoAToolTip {
|
||||||
visible: configSelectorId.hovered
|
visible: agentSelectorId.hovered
|
||||||
delay: 250
|
delay: 250
|
||||||
text: qsTr("Switch saved AI configuration")
|
text: qsTr("Select chat agent (provider and model come from the agent)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoAComboBox {
|
||||||
id: roleSelector
|
id: roleSelectorId
|
||||||
|
|
||||||
implicitHeight: 25
|
implicitHeight: 25
|
||||||
|
|
||||||
@@ -157,9 +155,9 @@ Rectangle {
|
|||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
QoAToolTip {
|
QoAToolTip {
|
||||||
visible: roleSelector.hovered
|
visible: roleSelectorId.hovered
|
||||||
delay: 250
|
delay: 250
|
||||||
text: qsTr("Switch agent role (different system prompts)")
|
text: qsTr("Select the role (system prompt) for the chat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,62 +165,6 @@ Rectangle {
|
|||||||
Row {
|
Row {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: toolsButtonId
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
checkable: true
|
|
||||||
opacity: enabled ? 1.0 : 0.2
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: toolsButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: {
|
|
||||||
if (!toolsButtonId.enabled) {
|
|
||||||
return qsTr("Tools are disabled in General Settings")
|
|
||||||
}
|
|
||||||
return toolsButtonId.checked
|
|
||||||
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
|
||||||
: qsTr("Tools disabled: Simple conversation without tool access")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: thinkingModeId
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
checkable: true
|
|
||||||
opacity: enabled ? 1.0 : 0.2
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: thinkingModeId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: thinkingModeId.enabled
|
|
||||||
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
|
||||||
: qsTr("Thinking Mode disabled"))
|
|
||||||
: qsTr("Thinking Mode is not available for this provider")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: settingsButtonId
|
id: settingsButtonId
|
||||||
|
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
|
||||||
|
|
||||||
#include <settings/ButtonAspect.hpp>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include "QodeAssisttr.h"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
ConfigurationManager &ConfigurationManager::instance()
|
|
||||||
{
|
|
||||||
static ConfigurationManager instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::init()
|
|
||||||
{
|
|
||||||
setupConnections();
|
|
||||||
updateAllTemplateDescriptions();
|
|
||||||
checkAllTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (!templ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
|
|
||||||
m_generalSettings.qrTemplateDescription.setValue(templ->description());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateAllTemplateDescriptions()
|
|
||||||
{
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (templ->name() == templateAspect.value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplate.setValue(templ->name());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplate.setValue(templ->name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkAllTemplate()
|
|
||||||
{
|
|
||||||
checkTemplate(m_generalSettings.ccTemplate);
|
|
||||||
checkTemplate(m_generalSettings.caTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_generalSettings(Settings::generalSettings())
|
|
||||||
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
|
||||||
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
|
||||||
{}
|
|
||||||
|
|
||||||
void ConfigurationManager::setupConnections()
|
|
||||||
{
|
|
||||||
using Config = ConfigurationManager;
|
|
||||||
using Button = ButtonAspect;
|
|
||||||
|
|
||||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(
|
|
||||||
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
|
|
||||||
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectProvider()
|
|
||||||
{
|
|
||||||
const auto providersList = m_providersManager.providersNames();
|
|
||||||
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
|
||||||
? m_generalSettings.ccProvider
|
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
|
||||||
? m_generalSettings.ccPreset1Provider
|
|
||||||
: settingsButton == &m_generalSettings.qrSelectProvider
|
|
||||||
? m_generalSettings.qrProvider
|
|
||||||
: m_generalSettings.caProvider;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectModel()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
|
|
||||||
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
|
|
||||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
|
||||||
: m_generalSettings.caUrl.volatileValue();
|
|
||||||
|
|
||||||
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrModel
|
|
||||||
: m_generalSettings.caModel);
|
|
||||||
|
|
||||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
|
||||||
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
|
||||||
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
provider->getInstalledModels(providerUrl)
|
|
||||||
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
|
||||||
if (modelList.isEmpty()) {
|
|
||||||
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectTemplate()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
|
||||||
|
|
||||||
const auto templateList = isCodeCompletion || isPreset1
|
|
||||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
|
||||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrTemplate
|
|
||||||
: m_generalSettings.caTemplate;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectUrl()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QStringList urls;
|
|
||||||
for (const auto &name : m_providersManager.providersNames()) {
|
|
||||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
|
||||||
if (!urls.contains(url))
|
|
||||||
urls.append(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
|
||||||
? m_generalSettings.ccPreset1Url
|
|
||||||
: settingsButton == &m_generalSettings.qrSetUrl
|
|
||||||
? m_generalSettings.qrUrl
|
|
||||||
: m_generalSettings.caUrl;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
|
||||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include "pluginllmcore/PromptTemplateManager.hpp"
|
|
||||||
#include "pluginllmcore/ProvidersManager.hpp"
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
class ConfigurationManager : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
static ConfigurationManager &instance();
|
|
||||||
|
|
||||||
void init();
|
|
||||||
|
|
||||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
|
||||||
void updateAllTemplateDescriptions();
|
|
||||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
|
||||||
void checkAllTemplate();
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void selectProvider();
|
|
||||||
void selectModel();
|
|
||||||
void selectTemplate();
|
|
||||||
void selectUrl();
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
|
||||||
~ConfigurationManager() = default;
|
|
||||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
|
||||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
|
||||||
|
|
||||||
Settings::GeneralSettings &m_generalSettings;
|
|
||||||
PluginLLMCore::ProvidersManager &m_providersManager;
|
|
||||||
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
|
||||||
|
|
||||||
void setupConnections();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
@@ -9,27 +9,47 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
#include <Agent.hpp>
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <AgentRouter.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
#include "sources/common/ContextData.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include "context/DocumentContextReader.hpp"
|
#include "context/DocumentContextReader.hpp"
|
||||||
#include "context/Utils.hpp"
|
#include "context/Utils.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include <pluginllmcore/RulesLoader.hpp>
|
#include "sources/settings/PipelinesConfig.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
LLMClientInterface::LLMClientInterface(
|
LLMClientInterface::LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
AgentFactory &agentFactory,
|
||||||
PluginLLMCore::IPromptProvider *promptProvider,
|
SessionManager &sessionManager,
|
||||||
Context::IDocumentReader &documentReader,
|
Context::IDocumentReader &documentReader,
|
||||||
IRequestPerformanceLogger &performanceLogger)
|
IRequestPerformanceLogger &performanceLogger)
|
||||||
: m_generalSettings(generalSettings)
|
: m_generalSettings(generalSettings)
|
||||||
, m_completeSettings(completeSettings)
|
, m_completeSettings(completeSettings)
|
||||||
, m_providerRegistry(providerRegistry)
|
, m_agentFactory(agentFactory)
|
||||||
, m_promptProvider(promptProvider)
|
, m_sessionManager(sessionManager)
|
||||||
, m_documentReader(documentReader)
|
, m_documentReader(documentReader)
|
||||||
, m_performanceLogger(performanceLogger)
|
, m_performanceLogger(performanceLogger)
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
, m_contextManager(new Context::ContextManager(this))
|
||||||
@@ -51,58 +71,56 @@ void LLMClientInterface::startImpl()
|
|||||||
emit started();
|
emit started();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
void LLMClientInterface::onCompletionFinished(const QString &requestId)
|
||||||
{
|
{
|
||||||
auto it = m_activeRequests.find(requestId);
|
auto it = m_activeRequests.find(requestId);
|
||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
QString fullText;
|
||||||
sendCompletionToClient(fullText, ctx.originalRequest, true);
|
if (Session *session = it.value().session) {
|
||||||
|
if (auto *history = session->history(); history && !history->isEmpty())
|
||||||
|
fullText = history->messages().back().text();
|
||||||
|
}
|
||||||
|
const QJsonObject originalRequest = it.value().originalRequest;
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
sendCompletionToClient(fullText, originalRequest, true);
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
finishRequest(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFinalized(
|
void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId) || !info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
{
|
||||||
auto it = m_activeRequests.find(requestId);
|
auto it = m_activeRequests.find(requestId);
|
||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||||
|
|
||||||
// Send LSP error response to client
|
|
||||||
QJsonObject response;
|
QJsonObject response;
|
||||||
response["jsonrpc"] = "2.0";
|
response["jsonrpc"] = "2.0";
|
||||||
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
|
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
|
||||||
|
|
||||||
QJsonObject errorObject;
|
QJsonObject errorObject;
|
||||||
errorObject["code"] = -32603; // Internal error code
|
errorObject["code"] = -32603; // Internal error code
|
||||||
errorObject["message"] = error;
|
errorObject["message"] = error;
|
||||||
response["error"] = errorObject;
|
response["error"] = errorObject;
|
||||||
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
|
finishRequest(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::finishRequest(const QString &requestId)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
m_performanceLogger.endTimeMeasurement(requestId);
|
||||||
|
|
||||||
|
if (session)
|
||||||
|
m_sessionManager.release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::sendData(const QByteArray &data)
|
void LLMClientInterface::sendData(const QByteArray &data)
|
||||||
@@ -135,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
|
|
||||||
void LLMClientInterface::handleCancelRequest()
|
void LLMClientInterface::handleCancelRequest()
|
||||||
{
|
{
|
||||||
QSet<PluginLLMCore::Provider *> providers;
|
const auto requests = m_activeRequests;
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value().provider) {
|
|
||||||
providers.insert(it.value().provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
|
||||||
disconnect(provider->client(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
if (ctx.provider) {
|
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
|
|
||||||
|
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||||
|
m_performanceLogger.endTimeMeasurement(it.key());
|
||||||
|
if (Session *session = it.value().session)
|
||||||
|
m_sessionManager.release(session);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,133 +244,87 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto updatedContext = prepareContext(request, documentInfo);
|
const QString agentName = pickCompletionAgent(filePath);
|
||||||
|
if (agentName.isEmpty()) {
|
||||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
QString error = QString("No code completion agent matches: %1").arg(filePath);
|
||||||
|
|
||||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
|
||||||
: m_generalSettings.ccPreset1Provider();
|
|
||||||
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
|
|
||||||
: m_generalSettings.ccPreset1Model();
|
|
||||||
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
|
||||||
: m_generalSettings.ccPreset1Url();
|
|
||||||
|
|
||||||
const auto provider = m_providerRegistry.getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
sendErrorResponse(request, error);
|
sendErrorResponse(request, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
QString sessionError;
|
||||||
: m_generalSettings.ccPreset1Template();
|
Session *session = m_sessionManager.acquire(agentName, &sessionError);
|
||||||
|
if (!session) {
|
||||||
|
LOG_MESSAGE(sessionError);
|
||||||
|
sendErrorResponse(request, sessionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
Templates::ContextData context = prepareContext(request, documentInfo);
|
||||||
|
|
||||||
if (!promptTemplate) {
|
QString editorContext;
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
if (context.fileContext.has_value())
|
||||||
|
editorContext.append(context.fileContext.value());
|
||||||
|
|
||||||
|
if (m_completeSettings.useOpenFilesContext())
|
||||||
|
editorContext.append(m_contextManager->openedFilesContext({filePath}));
|
||||||
|
|
||||||
|
if (!editorContext.isEmpty())
|
||||||
|
session->systemPrompt()->setLayer(QStringLiteral("completion.context"), editorContext);
|
||||||
|
|
||||||
|
connect(session, &Session::finished, this, [this, session](const LLMQore::RequestID &, const QString &) {
|
||||||
|
onCompletionFinished(requestIdForSession(session));
|
||||||
|
});
|
||||||
|
connect(session, &Session::failed, this, [this, session](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onCompletionFailed(requestIdForSession(session), error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auto *client = session->client())
|
||||||
|
client->setTransferTimeout(
|
||||||
|
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(std::make_unique<CompletionContent>(
|
||||||
|
context.prefix.value_or(QString()), context.suffix.value_or(QString())));
|
||||||
|
const LLMQore::RequestID requestId = session->send(std::move(blocks), /*toolsOverride=*/false);
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
QString error = QString("Failed to start completion request for agent '%1': %2")
|
||||||
|
.arg(agentName, session->lastError().message);
|
||||||
|
session->deleteLater();
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
sendErrorResponse(request, error);
|
sendErrorResponse(request, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
m_activeRequests[requestId] = {request, session};
|
||||||
|
|
||||||
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
|
||||||
if (!stopWords.isEmpty())
|
|
||||||
payload["stop"] = stopWords;
|
|
||||||
|
|
||||||
QString systemPrompt;
|
|
||||||
if (m_completeSettings.useSystemPrompt())
|
|
||||||
systemPrompt.append(
|
|
||||||
m_completeSettings.useUserMessageTemplateForCC()
|
|
||||||
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
|
||||||
? m_completeSettings.systemPromptForNonFimModels()
|
|
||||||
: m_completeSettings.systemPrompt());
|
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (project) {
|
|
||||||
QString projectRules
|
|
||||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
||||||
LOG_MESSAGE("Loaded project rules for completion");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedContext.fileContext.has_value())
|
|
||||||
systemPrompt.append(updatedContext.fileContext.value());
|
|
||||||
|
|
||||||
if (m_completeSettings.useOpenFilesContext()) {
|
|
||||||
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
|
||||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
|
||||||
if (!updatedContext.filesMetadata) {
|
|
||||||
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
|
||||||
}
|
|
||||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedContext.systemPrompt = systemPrompt;
|
|
||||||
|
|
||||||
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
|
||||||
QString userMessage;
|
|
||||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
|
||||||
userMessage = m_completeSettings.processMessageToFIM(
|
|
||||||
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
|
|
||||||
} else {
|
|
||||||
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO refactor add message
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
messages.append({"user", userMessage});
|
|
||||||
updatedContext.history = messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
provider->prepareRequest(
|
|
||||||
payload,
|
|
||||||
promptTemplate,
|
|
||||||
updatedContext,
|
|
||||||
PluginLLMCore::RequestType::CodeCompletion,
|
|
||||||
false,
|
|
||||||
false);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFinalized,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
|
||||||
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
|
||||||
|
{
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().rosters.codeCompletion;
|
||||||
|
if (roster.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
AgentRouter::Context ctx;
|
||||||
|
ctx.filePath = filePath;
|
||||||
|
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||||
|
Utils::FilePath::fromString(filePath)))
|
||||||
|
ctx.projectName = project->displayName();
|
||||||
|
|
||||||
|
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LLMClientInterface::requestIdForSession(Session *session) const
|
||||||
|
{
|
||||||
|
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||||
|
if (it.value().session == session)
|
||||||
|
return it.key();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Templates::ContextData LLMClientInterface::prepareContext(
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||||
{
|
{
|
||||||
QJsonObject params = request["params"].toObject();
|
QJsonObject params = request["params"].toObject();
|
||||||
@@ -377,14 +338,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
|||||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString LLMClientInterface::resolveEndpoint(
|
|
||||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
|
||||||
{
|
|
||||||
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
|
||||||
: m_generalSettings.ccCustomEndpoint();
|
|
||||||
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||||
{
|
{
|
||||||
return m_contextManager;
|
return m_contextManager;
|
||||||
@@ -393,15 +346,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
|
|||||||
void LLMClientInterface::sendCompletionToClient(
|
void LLMClientInterface::sendCompletionToClient(
|
||||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||||
{
|
{
|
||||||
auto filePath = Context::extractFilePathFromRequest(request);
|
|
||||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
|
||||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
|
||||||
: m_generalSettings.ccPreset1Template();
|
|
||||||
|
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
|
||||||
|
|
||||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||||
|
|
||||||
QJsonObject response;
|
QJsonObject response;
|
||||||
|
|||||||
@@ -8,12 +8,11 @@
|
|||||||
#include <languageclient/languageclientinterface.h>
|
#include <languageclient/languageclientinterface.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
#include <context/IDocumentReader.hpp>
|
#include <context/IDocumentReader.hpp>
|
||||||
#include <context/ProgrammingLanguage.hpp>
|
#include <context/ProgrammingLanguage.hpp>
|
||||||
#include <pluginllmcore/ContextData.hpp>
|
|
||||||
#include <pluginllmcore/IPromptProvider.hpp>
|
|
||||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
|
||||||
#include <logger/IRequestPerformanceLogger.hpp>
|
#include <logger/IRequestPerformanceLogger.hpp>
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
@@ -23,6 +22,14 @@ class QNetworkAccessManager;
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class AgentFactory;
|
||||||
|
class Session;
|
||||||
|
class SessionManager;
|
||||||
|
|
||||||
|
namespace Templates {
|
||||||
|
struct ContextData;
|
||||||
|
}
|
||||||
|
|
||||||
class LLMClientInterface : public LanguageClient::BaseClientInterface
|
class LLMClientInterface : public LanguageClient::BaseClientInterface
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -31,8 +38,8 @@ public:
|
|||||||
LLMClientInterface(
|
LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
AgentFactory &agentFactory,
|
||||||
PluginLLMCore::IPromptProvider *promptProvider,
|
SessionManager &sessionManager,
|
||||||
Context::IDocumentReader &documentReader,
|
Context::IDocumentReader &documentReader,
|
||||||
IRequestPerformanceLogger &performanceLogger);
|
IRequestPerformanceLogger &performanceLogger);
|
||||||
~LLMClientInterface() override;
|
~LLMClientInterface() override;
|
||||||
@@ -52,12 +59,6 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
void startImpl() override;
|
void startImpl() override;
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleInitialize(const QJsonObject &request);
|
void handleInitialize(const QJsonObject &request);
|
||||||
void handleShutdown(const QJsonObject &request);
|
void handleShutdown(const QJsonObject &request);
|
||||||
@@ -67,22 +68,26 @@ private:
|
|||||||
void handleCancelRequest();
|
void handleCancelRequest();
|
||||||
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
||||||
|
|
||||||
|
void onCompletionFinished(const QString &requestId);
|
||||||
|
void onCompletionFailed(const QString &requestId, const QString &error);
|
||||||
|
void finishRequest(const QString &requestId);
|
||||||
|
QString requestIdForSession(Session *session) const;
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<Session> session;
|
||||||
};
|
};
|
||||||
|
|
||||||
PluginLLMCore::ContextData prepareContext(
|
Templates::ContextData prepareContext(
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||||
|
|
||||||
QString resolveEndpoint(
|
QString pickCompletionAgent(const QString &filePath) const;
|
||||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
|
|
||||||
|
|
||||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||||
const Settings::GeneralSettings &m_generalSettings;
|
const Settings::GeneralSettings &m_generalSettings;
|
||||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
AgentFactory &m_agentFactory;
|
||||||
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
SessionManager &m_sessionManager;
|
||||||
Context::IDocumentReader &m_documentReader;
|
Context::IDocumentReader &m_documentReader;
|
||||||
IRequestPerformanceLogger &m_performanceLogger;
|
IRequestPerformanceLogger &m_performanceLogger;
|
||||||
QElapsedTimer m_completionTimer;
|
QElapsedTimer m_completionTimer;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.21",
|
"Version" : "0.9.20",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
"CompatVersion" : "${IDE_VERSION}",
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
|
|||||||
@@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
|
||||||
|
{
|
||||||
|
m_agentFactory = agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
@@ -263,9 +273,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
->ignoreManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
@@ -309,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
->ignoreManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
@@ -319,6 +327,8 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
|
|
||||||
if (!m_refactorHandler) {
|
if (!m_refactorHandler) {
|
||||||
m_refactorHandler = new QuickRefactorHandler(this);
|
m_refactorHandler = new QuickRefactorHandler(this);
|
||||||
|
m_refactorHandler->setSessionManager(m_sessionManager);
|
||||||
|
m_refactorHandler->setAgentFactory(m_agentFactory);
|
||||||
connect(
|
connect(
|
||||||
m_refactorHandler,
|
m_refactorHandler,
|
||||||
&QuickRefactorHandler::refactoringCompleted,
|
&QuickRefactorHandler::refactoringCompleted,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
#include "LSPCompletion.hpp"
|
#include "LSPCompletion.hpp"
|
||||||
@@ -16,11 +17,12 @@
|
|||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
#include "widgets/RefactorWidgetHandler.hpp"
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <pluginllmcore/IPromptProvider.hpp>
|
|
||||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class SessionManager;
|
||||||
|
class AgentFactory;
|
||||||
|
|
||||||
class QodeAssistClient : public LanguageClient::Client
|
class QodeAssistClient : public LanguageClient::Client
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -28,6 +30,9 @@ public:
|
|||||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||||
~QodeAssistClient() override;
|
~QodeAssistClient() override;
|
||||||
|
|
||||||
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setAgentFactory(AgentFactory *agentFactory);
|
||||||
|
|
||||||
void openDocument(TextEditor::TextDocument *document) override;
|
void openDocument(TextEditor::TextDocument *document) override;
|
||||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||||
|
|
||||||
@@ -68,6 +73,8 @@ private:
|
|||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||||
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||||
LLMClientInterface *m_llmClient;
|
LLMClientInterface *m_llmClient;
|
||||||
|
SessionManager *m_sessionManager{nullptr};
|
||||||
|
AgentFactory *m_agentFactory{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -4,24 +4,40 @@
|
|||||||
|
|
||||||
#include "QuickRefactorHandler.hpp"
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
#include <context/DocumentContextReader.hpp>
|
#include <context/DocumentContextReader.hpp>
|
||||||
#include <pluginllmcore/ResponseCleaner.hpp>
|
|
||||||
#include <context/DocumentReaderQtCreator.hpp>
|
#include <context/DocumentReaderQtCreator.hpp>
|
||||||
#include <context/Utils.hpp>
|
#include <context/Utils.hpp>
|
||||||
#include <pluginllmcore/PromptTemplateManager.hpp>
|
|
||||||
#include <pluginllmcore/ProvidersManager.hpp>
|
|
||||||
#include <pluginllmcore/RulesLoader.hpp>
|
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <settings/ChatAssistantSettings.hpp>
|
#include <sources/common/ResponseCleaner.hpp>
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
#include <settings/QuickRefactorSettings.hpp>
|
#include <settings/QuickRefactorSettings.hpp>
|
||||||
#include <settings/ToolsSettings.hpp>
|
#include <settings/ToolsSettings.hpp>
|
||||||
|
|
||||||
|
#include "sources/common/ContextData.hpp"
|
||||||
|
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <AgentRouter.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
|
#include "sources/settings/PipelinesConfig.hpp"
|
||||||
|
#include "tools/ToolsRegistration.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||||
@@ -34,6 +50,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
|||||||
|
|
||||||
QuickRefactorHandler::~QuickRefactorHandler() {}
|
QuickRefactorHandler::~QuickRefactorHandler() {}
|
||||||
|
|
||||||
|
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
|
||||||
|
{
|
||||||
|
m_agentFactory = agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::sendRefactorRequest(
|
void QuickRefactorHandler::sendRefactorRequest(
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
||||||
{
|
{
|
||||||
@@ -88,105 +114,109 @@ void QuickRefactorHandler::sendRefactorRequest(
|
|||||||
prepareAndSendRequest(editor, instructions, range);
|
prepareAndSendRequest(editor, instructions, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString QuickRefactorHandler::pickRefactorAgent(const QString &filePath) const
|
||||||
|
{
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().rosters.quickRefactor;
|
||||||
|
if (roster.isEmpty() || !m_agentFactory)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
AgentRouter::Context ctx;
|
||||||
|
ctx.filePath = filePath;
|
||||||
|
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||||
|
Utils::FilePath::fromString(filePath)))
|
||||||
|
ctx.projectName = project->displayName();
|
||||||
|
|
||||||
|
return AgentRouter::pickAgent(roster, ctx, *m_agentFactory);
|
||||||
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::prepareAndSendRequest(
|
void QuickRefactorHandler::prepareAndSendRequest(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor,
|
||||||
const QString &instructions,
|
const QString &instructions,
|
||||||
const Utils::Text::Range &range)
|
const Utils::Text::Range &range)
|
||||||
{
|
{
|
||||||
auto &settings = Settings::generalSettings();
|
const auto emitError = [this, editor](const QString &error) {
|
||||||
|
|
||||||
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
|
|
||||||
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
|
|
||||||
|
|
||||||
const auto providerName = settings.qrProvider();
|
|
||||||
auto provider = providerRegistry.getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errorMessage = error;
|
result.errorMessage = error;
|
||||||
result.editor = editor;
|
result.editor = editor;
|
||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!m_sessionManager) {
|
||||||
|
emitError(QStringLiteral("Quick refactor session manager is not available"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto templateName = settings.qrTemplate();
|
const QString filePath = editor->textDocument()->filePath().toUrlishString();
|
||||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
const QString agentName = pickRefactorAgent(filePath);
|
||||||
|
if (agentName.isEmpty()) {
|
||||||
if (!promptTemplate) {
|
emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath));
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
|
||||||
LOG_MESSAGE(error);
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = error;
|
|
||||||
result.editor = editor;
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject payload{
|
QString sessionError;
|
||||||
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
Session *session = m_sessionManager->acquire(agentName, &sessionError);
|
||||||
|
if (!session) {
|
||||||
|
emitError(sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
|
||||||
|
: sessionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
auto *client = session->client();
|
||||||
|
if (!client) {
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emitError(QStringLiteral("Quick refactor agent has no live client"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
const bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
if (enableTools) {
|
||||||
provider->prepareRequest(
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
payload,
|
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||||
promptTemplate,
|
}
|
||||||
context,
|
|
||||||
PluginLLMCore::RequestType::QuickRefactoring,
|
|
||||||
enableTools,
|
|
||||||
enableThinking);
|
|
||||||
|
|
||||||
provider->client()->setMaxToolContinuations(
|
session->systemPrompt()->setLayer(
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
QStringLiteral("refactor"), buildSystemPrompt(editor, range));
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
m_isRefactoringInProgress = true;
|
m_isRefactoringInProgress = true;
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
session, &Session::finished, this,
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
[this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); });
|
||||||
this,
|
|
||||||
&QuickRefactorHandler::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
session, &Session::failed, this,
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
this,
|
onRefactorFailed(id, error);
|
||||||
&QuickRefactorHandler::handleRequestFinalized,
|
});
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
connect(
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
provider->client(),
|
const QString userMessage = instructions.isEmpty()
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
|
||||||
this,
|
: instructions;
|
||||||
&QuickRefactorHandler::handleRequestFailed,
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
const LLMQore::RequestID requestId = session->send(std::move(blocks), enableTools);
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
m_isRefactoringInProgress = false;
|
||||||
|
const QString reason = session->lastError().message;
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
|
||||||
|
.arg(agentName, reason));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
|
||||||
: promptTemplate->endpoint();
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
|
||||||
m_lastRequestId = requestId;
|
m_lastRequestId = requestId;
|
||||||
QJsonObject request{{"id", requestId}};
|
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
QString QuickRefactorHandler::buildSystemPrompt(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
|
||||||
const Utils::Text::Range &range,
|
|
||||||
const QString &instructions)
|
|
||||||
{
|
{
|
||||||
PluginLLMCore::ContextData context;
|
Q_UNUSED(range)
|
||||||
|
|
||||||
auto textDocument = editor->textDocument();
|
auto textDocument = editor->textDocument();
|
||||||
Context::DocumentReaderQtCreator documentReader;
|
Context::DocumentReaderQtCreator documentReader;
|
||||||
@@ -194,7 +224,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
|
|
||||||
if (!documentInfo.document) {
|
if (!documentInfo.document) {
|
||||||
LOG_MESSAGE("Error: Document is not available");
|
LOG_MESSAGE("Error: Document is not available");
|
||||||
return context;
|
return Settings::quickRefactorSettings().systemPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor cursor = editor->textCursor();
|
QTextCursor cursor = editor->textCursor();
|
||||||
@@ -270,17 +300,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
|
|
||||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (project) {
|
|
||||||
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
|
||||||
project, PluginLLMCore::RulesContext::QuickRefactor);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
||||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
systemPrompt += "\n\nFile information:";
|
systemPrompt += "\n\nFile information:";
|
||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||||
@@ -294,7 +313,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
"\n- Your output will completely replace the selected code"
|
"\n- Your output will completely replace the selected code"
|
||||||
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||||
"\n- Your output will be inserted at the cursor location";
|
"\n- Your output will be inserted at the cursor location";
|
||||||
|
|
||||||
systemPrompt += "\n\n## Formatting Rules:"
|
systemPrompt += "\n\n## Formatting Rules:"
|
||||||
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||||
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||||
@@ -302,9 +321,9 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
"\n- Do NOT repeat existing code, be precise with context"
|
"\n- Do NOT repeat existing code, be precise with context"
|
||||||
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||||
"\n- The output must be ready to insert directly into the editor as-is";
|
"\n- The output must be ready to insert directly into the editor as-is";
|
||||||
|
|
||||||
systemPrompt += "\n\n## Indentation and Whitespace:";
|
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||||
|
|
||||||
if (cursor.hasSelection()) {
|
if (cursor.hasSelection()) {
|
||||||
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||||
int leadingSpaces = 0;
|
int leadingSpaces = 0;
|
||||||
@@ -336,7 +355,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
.arg(leadingSpaces);
|
.arg(leadingSpaces);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||||
"\n- Maintain consistent indentation for nested blocks"
|
"\n- Maintain consistent indentation for nested blocks"
|
||||||
"\n- Do NOT remove or reduce the base indentation level"
|
"\n- Do NOT remove or reduce the base indentation level"
|
||||||
@@ -349,42 +368,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
}
|
}
|
||||||
|
|
||||||
context.systemPrompt = systemPrompt;
|
return systemPrompt;
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
messages.append(
|
|
||||||
{"user",
|
|
||||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
|
||||||
: instructions});
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleLLMResponse(
|
|
||||||
const QString &response, const QJsonObject &request, bool isComplete)
|
|
||||||
{
|
|
||||||
if (request["id"].toString() != m_lastRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isComplete) {
|
|
||||||
m_isRefactoringInProgress = false;
|
|
||||||
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
|
||||||
|
|
||||||
RefactorResult result;
|
|
||||||
result.newText = cleanedResponse;
|
|
||||||
result.insertRange = m_currentRange;
|
|
||||||
result.success = true;
|
|
||||||
result.editor = m_currentEditor;
|
|
||||||
|
|
||||||
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
|
||||||
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
|
||||||
LOG_MESSAGE(cleanedResponse);
|
|
||||||
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
|
||||||
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::cancelRequest()
|
void QuickRefactorHandler::cancelRequest()
|
||||||
@@ -398,10 +382,10 @@ void QuickRefactorHandler::cancelRequest()
|
|||||||
|
|
||||||
auto it = m_activeRequests.find(id);
|
auto it = m_activeRequests.find(id);
|
||||||
if (it != m_activeRequests.end()) {
|
if (it != m_activeRequests.end()) {
|
||||||
auto provider = it.value().provider;
|
Session *session = it.value().session;
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
if (provider)
|
if (session && m_sessionManager)
|
||||||
provider->cancelRequest(id);
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
@@ -410,42 +394,66 @@ void QuickRefactorHandler::cancelRequest()
|
|||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
|
||||||
{
|
{
|
||||||
if (requestId == m_lastRequestId) {
|
if (requestId != m_lastRequestId)
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
handleLLMResponse(fullText, request, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (requestId != m_lastRequestId || !info.usage)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
auto it = m_activeRequests.find(requestId);
|
||||||
LOG_MESSAGE(
|
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||||
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
if (it != m_activeRequests.end())
|
||||||
.arg(requestId)
|
m_activeRequests.erase(it);
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
QString fullText;
|
||||||
.arg(u.cachedPromptTokens)
|
if (session) {
|
||||||
.arg(u.reasoningTokens));
|
if (auto *history = session->history(); history && !history->isEmpty())
|
||||||
|
fullText = history->messages().back().text();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_isRefactoringInProgress = false;
|
||||||
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
|
const QString cleanedResponse = ResponseCleaner::clean(fullText);
|
||||||
|
|
||||||
|
RefactorResult result;
|
||||||
|
result.newText = cleanedResponse;
|
||||||
|
result.insertRange = m_currentRange;
|
||||||
|
result.success = true;
|
||||||
|
result.editor = m_currentEditor;
|
||||||
|
|
||||||
|
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
||||||
|
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
||||||
|
LOG_MESSAGE(cleanedResponse);
|
||||||
|
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
||||||
|
|
||||||
|
emit refactoringCompleted(result);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
void QuickRefactorHandler::onRefactorFailed(
|
||||||
|
const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
{
|
{
|
||||||
if (requestId == m_lastRequestId) {
|
if (requestId != m_lastRequestId)
|
||||||
m_activeRequests.remove(requestId);
|
return;
|
||||||
m_isRefactoringInProgress = false;
|
|
||||||
RefactorResult result;
|
auto it = m_activeRequests.find(requestId);
|
||||||
result.success = false;
|
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||||
result.errorMessage = error;
|
if (it != m_activeRequests.end())
|
||||||
result.editor = m_currentEditor;
|
m_activeRequests.erase(it);
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
m_isRefactoringInProgress = false;
|
||||||
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
|
RefactorResult result;
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessage = error.message;
|
||||||
|
result.editor = m_currentEditor;
|
||||||
|
emit refactoringCompleted(result);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -6,18 +6,22 @@
|
|||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
#include <utils/textutils.h>
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
#include <ErrorInfo.hpp>
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
#include <context/IDocumentReader.hpp>
|
#include <context/IDocumentReader.hpp>
|
||||||
#include <pluginllmcore/ContextData.hpp>
|
|
||||||
#include <pluginllmcore/Provider.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class SessionManager;
|
||||||
|
class Session;
|
||||||
|
class AgentFactory;
|
||||||
|
|
||||||
struct RefactorResult
|
struct RefactorResult
|
||||||
{
|
{
|
||||||
QString newText;
|
QString newText;
|
||||||
@@ -35,6 +39,9 @@ public:
|
|||||||
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
||||||
~QuickRefactorHandler() override;
|
~QuickRefactorHandler() override;
|
||||||
|
|
||||||
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setAgentFactory(AgentFactory *agentFactory);
|
||||||
|
|
||||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
||||||
|
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
@@ -43,30 +50,26 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void prepareAndSendRequest(
|
void prepareAndSendRequest(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor,
|
||||||
const QString &instructions,
|
const QString &instructions,
|
||||||
const Utils::Text::Range &range);
|
const Utils::Text::Range &range);
|
||||||
|
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
void onRefactorFinished(const QString &requestId);
|
||||||
PluginLLMCore::ContextData prepareContext(
|
void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
TextEditor::TextEditorWidget *editor,
|
QString buildSystemPrompt(
|
||||||
const Utils::Text::Range &range,
|
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
|
||||||
const QString &instructions);
|
QString pickRefactorAgent(const QString &filePath) const;
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<Session> session;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QPointer<SessionManager> m_sessionManager;
|
||||||
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
TextEditor::TextEditorWidget *m_currentEditor;
|
TextEditor::TextEditorWidget *m_currentEditor;
|
||||||
Utils::Text::Range m_currentRange;
|
Utils::Text::Range m_currentRange;
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -216,9 +216,9 @@ For optimal coding assistance, we recommend using these top-tier models:
|
|||||||
|
|
||||||
### Additional Configuration
|
### Additional Configuration
|
||||||
|
|
||||||
|
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents or override bundled ones with TOML profiles
|
||||||
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
|
||||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -473,7 +473,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
||||||
- **Tool Calling** is available for Chat and Quick Refactor when enabled
|
- **Tool Calling** is available for Chat and Quick Refactor when enabled
|
||||||
|
|
||||||
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
See [Agent Roles Guide](docs/agent-roles.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
@@ -532,7 +532,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
|||||||
|
|
||||||
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
||||||
|
|
||||||
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features.
|
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||||
|
|
||||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||||
|
|
||||||
@@ -580,6 +580,10 @@ cmake --build .
|
|||||||
|
|
||||||
## For Contributors
|
## For Contributors
|
||||||
|
|
||||||
|
### Adding an agent preset
|
||||||
|
|
||||||
|
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
||||||
|
|||||||
27
bench/CMakeLists.txt
Normal file
27
bench/CMakeLists.txt
Normal 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
592
bench/main.cpp
Normal 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
6
bench/run-bench.sh.in
Normal 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" "$@"
|
||||||
@@ -2,6 +2,8 @@ add_library(Context STATIC
|
|||||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||||
ChangesManager.h ChangesManager.cpp
|
ChangesManager.h ChangesManager.cpp
|
||||||
ContextManager.hpp ContextManager.cpp
|
ContextManager.hpp ContextManager.cpp
|
||||||
|
IProjectScanner.hpp
|
||||||
|
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
|
||||||
ContentFile.hpp
|
ContentFile.hpp
|
||||||
DocumentReaderQtCreator.hpp
|
DocumentReaderQtCreator.hpp
|
||||||
IDocumentReader.hpp
|
IDocumentReader.hpp
|
||||||
@@ -21,7 +23,7 @@ target_link_libraries(Context
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::ProjectExplorer
|
QtCreator::ProjectExplorer
|
||||||
PRIVATE
|
PRIVATE
|
||||||
PluginLLMCore
|
Common
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,24 @@
|
|||||||
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
#include <projectexplorer/projectnodes.h>
|
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
#include "ProjectScannerQtCreator.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
ContextManager::ContextManager(QObject *parent)
|
ContextManager::ContextManager(QObject *parent)
|
||||||
: QObject(parent)
|
: ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
|
||||||
, m_ignoreManager(new IgnoreManager(this))
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
ContextManager::ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_scanner(std::move(scanner))
|
||||||
|
{}
|
||||||
|
|
||||||
|
ContextManager::~ContextManager() = default;
|
||||||
|
|
||||||
QString ContextManager::readFile(const QString &filePath) const
|
QString ContextManager::readFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
@@ -37,7 +36,7 @@ QString ContextManager::readFile(const QString &filePath) const
|
|||||||
QTextStream in(&file);
|
QTextStream in(&file);
|
||||||
QString content = in.readAll();
|
QString content = in.readAll();
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
|
|||||||
{
|
{
|
||||||
QList<ContentFile> files;
|
QList<ContentFile> files;
|
||||||
for (const QString &path : filePaths) {
|
for (const QString &path : filePaths) {
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(
|
if (m_scanner->shouldIgnore(path)) {
|
||||||
Utils::FilePath::fromString(path));
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(path, project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
|
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
|
|
||||||
{
|
|
||||||
QStringList sourceFiles;
|
|
||||||
if (!project)
|
|
||||||
return sourceFiles;
|
|
||||||
|
|
||||||
auto projectNode = project->rootProjectNode();
|
|
||||||
if (!projectNode)
|
|
||||||
return sourceFiles;
|
|
||||||
|
|
||||||
projectNode->forEachNode(
|
|
||||||
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
|
|
||||||
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
|
|
||||||
sourceFiles.append(fileNode->filePath().toUrlishString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
return sourceFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
ContentFile contentFile;
|
ContentFile contentFile;
|
||||||
@@ -100,77 +76,26 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
|
|||||||
|
|
||||||
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
||||||
{
|
{
|
||||||
const auto &generalSettings = Settings::generalSettings();
|
Q_UNUSED(documentInfo)
|
||||||
|
return false;
|
||||||
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
|
|
||||||
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
|
|
||||||
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
|
|
||||||
|
|
||||||
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
|
QString ContextManager::openedFilesContext(const QStringList &excludeFiles) const
|
||||||
{
|
|
||||||
auto documents = Core::DocumentModel::openedDocuments();
|
|
||||||
|
|
||||||
QList<QPair<QString, QString>> files;
|
|
||||||
|
|
||||||
for (const auto *document : std::as_const(documents)) {
|
|
||||||
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
|
||||||
if (!textDocument)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto filePath = textDocument->filePath().toUrlishString();
|
|
||||||
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!excludeFiles.contains(filePath)) {
|
|
||||||
files.append({filePath, textDocument->plainText()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
|
|
||||||
{
|
{
|
||||||
QString context = "User files context:\n";
|
QString context = "User files context:\n";
|
||||||
|
|
||||||
auto documents = Core::DocumentModel::openedDocuments();
|
for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
|
||||||
|
context += QString("File: %1\n").arg(file.filePath);
|
||||||
for (const auto *document : documents) {
|
context += file.content;
|
||||||
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
|
||||||
if (!textDocument)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto filePath = textDocument->filePath().toUrlishString();
|
|
||||||
if (excludeFiles.contains(filePath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
context += QString("File: %1\n").arg(filePath);
|
|
||||||
context += textDocument->plainText();
|
|
||||||
|
|
||||||
context += "\n";
|
context += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
IgnoreManager *ContextManager::ignoreManager() const
|
bool ContextManager::shouldIgnore(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return m_ignoreManager;
|
return m_scanner->shouldIgnore(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -4,18 +4,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ContentFile.hpp"
|
#include "ContentFile.hpp"
|
||||||
#include "IContextManager.hpp"
|
#include "IContextManager.hpp"
|
||||||
#include "IgnoreManager.hpp"
|
#include "IProjectScanner.hpp"
|
||||||
#include "ProgrammingLanguage.hpp"
|
#include "ProgrammingLanguage.hpp"
|
||||||
|
|
||||||
namespace ProjectExplorer {
|
|
||||||
class Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class ContextManager : public QObject, public IContextManager
|
class ContextManager : public QObject, public IContextManager
|
||||||
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ContextManager(QObject *parent = nullptr);
|
explicit ContextManager(QObject *parent = nullptr);
|
||||||
~ContextManager() override = default;
|
ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent = nullptr);
|
||||||
|
~ContextManager() override;
|
||||||
|
|
||||||
QString readFile(const QString &filePath) const override;
|
QString readFile(const QString &filePath) const override;
|
||||||
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
|
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
|
||||||
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
|
|
||||||
ContentFile createContentFile(const QString &filePath) const override;
|
ContentFile createContentFile(const QString &filePath) const override;
|
||||||
|
|
||||||
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
|
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
|
||||||
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
|
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
|
||||||
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
|
|
||||||
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
|
|
||||||
|
|
||||||
IgnoreManager *ignoreManager() const;
|
QString openedFilesContext(const QStringList &excludeFiles = QStringList{}) const;
|
||||||
|
|
||||||
|
bool shouldIgnore(const QString &filePath) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
IgnoreManager *m_ignoreManager;
|
std::unique_ptr<IProjectScanner> m_scanner;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ CopyrightInfo DocumentContextReader::copyrightInfo() const
|
|||||||
return m_copyrightInfo;
|
return m_copyrightInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData DocumentContextReader::prepareContext(
|
Templates::ContextData DocumentContextReader::prepareContext(
|
||||||
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
|
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
|
||||||
{
|
{
|
||||||
QString contextBefore;
|
QString contextBefore;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
|
||||||
#include <pluginllmcore/ContextData.hpp>
|
#include <sources/common/ContextData.hpp>
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
@@ -58,7 +58,7 @@ public:
|
|||||||
|
|
||||||
CopyrightInfo copyrightInfo() const;
|
CopyrightInfo copyrightInfo() const;
|
||||||
|
|
||||||
PluginLLMCore::ContextData prepareContext(
|
Templates::ContextData prepareContext(
|
||||||
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
|
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -11,10 +11,6 @@
|
|||||||
#include "IDocumentReader.hpp"
|
#include "IDocumentReader.hpp"
|
||||||
#include "ProgrammingLanguage.hpp"
|
#include "ProgrammingLanguage.hpp"
|
||||||
|
|
||||||
namespace ProjectExplorer {
|
|
||||||
class Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class IContextManager
|
class IContextManager
|
||||||
@@ -24,7 +20,6 @@ public:
|
|||||||
|
|
||||||
virtual QString readFile(const QString &filePath) const = 0;
|
virtual QString readFile(const QString &filePath) const = 0;
|
||||||
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
|
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
|
||||||
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
|
|
||||||
virtual ContentFile createContentFile(const QString &filePath) const = 0;
|
virtual ContentFile createContentFile(const QString &filePath) const = 0;
|
||||||
|
|
||||||
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
|
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
|
||||||
|
|||||||
28
context/IProjectScanner.hpp
Normal file
28
context/IProjectScanner.hpp
Normal 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
|
||||||
53
context/ProjectScannerQtCreator.cpp
Normal file
53
context/ProjectScannerQtCreator.cpp
Normal 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
|
||||||
28
context/ProjectScannerQtCreator.hpp
Normal file
28
context/ProjectScannerQtCreator.hpp
Normal 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
|
||||||
@@ -168,7 +168,6 @@ This allows roles to augment rather than replace your base configuration.
|
|||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
|
|
||||||
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
|
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
|
||||||
- [File Context](file-context.md) - Attaching files to chat context
|
- [File Context](file-context.md) - Attaching files to chat context
|
||||||
|
|
||||||
|
|||||||
395
docs/agent-templates-design.md
Normal file
395
docs/agent-templates-design.md
Normal 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
317
docs/architecture.md
Normal 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.
|
||||||
|
```
|
||||||
@@ -112,4 +112,3 @@ No additional configuration is required.
|
|||||||
|
|
||||||
- [Agent Roles](agent-roles.md) - Switch between AI personas
|
- [Agent Roles](agent-roles.md) - Switch between AI personas
|
||||||
- [File Context](file-context.md) - Attach files to chat
|
- [File Context](file-context.md) - Attach files to chat
|
||||||
- [Project Rules](project-rules.md) - Customize AI behavior
|
|
||||||
|
|||||||
1
docs/core-class-diagram.svg
Normal file
1
docs/core-class-diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 66 KiB |
339
docs/creating-agents.md
Normal file
339
docs/creating-agents.md
Normal 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.
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -206,7 +206,6 @@ The LLM receives:
|
|||||||
- **Cursor Position**: Marked with `<cursor>` tag
|
- **Cursor Position**: Marked with `<cursor>` tag
|
||||||
- **Selection Markers**: `<selection_start>` and `<selection_end>` tags
|
- **Selection Markers**: `<selection_start>` and `<selection_end>` tags
|
||||||
- **Your Instructions**: Built-in, custom, or typed
|
- **Your Instructions**: Built-in, custom, or typed
|
||||||
- **Project Rules**: If configured (see [Project Rules](project-rules.md))
|
|
||||||
|
|
||||||
### Context Configuration
|
### Context Configuration
|
||||||
|
|
||||||
@@ -270,7 +269,6 @@ Fully local setup for offline or secure environments.
|
|||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
|
|
||||||
- [File Context](file-context.md) - Attaching files to chat context
|
- [File Context](file-context.md) - Attaching files to chat context
|
||||||
- [Ignoring Files](ignoring-files.md) - Exclude files from AI context
|
- [Ignoring Files](ignoring-files.md) - Exclude files from AI context
|
||||||
- [Provider Configuration](../README.md#configuration) - Setting up LLM providers
|
- [Provider Configuration](../README.md#configuration) - Setting up LLM providers
|
||||||
|
|||||||
652
docs/target-architecture.md
Normal file
652
docs/target-architecture.md
Normal 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` | L0–L2, 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: **L0–L2 build into
|
||||||
|
targets with no Qt Creator linkage.** Bench links L0–L2 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 L1–L4. 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.
|
||||||
@@ -15,9 +15,8 @@
|
|||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <pluginllmcore/Provider.hpp>
|
|
||||||
#include <pluginllmcore/ProvidersManager.hpp>
|
|
||||||
#include <settings/McpSettings.hpp>
|
#include <settings/McpSettings.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Mcp {
|
namespace QodeAssist::Mcp {
|
||||||
@@ -176,18 +175,14 @@ QList<McpServerConnection *> McpClientsManager::connections() const
|
|||||||
return m_connections;
|
return m_connections;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<PluginLLMCore::Provider *> McpClientsManager::toolsCapableProviders() const
|
void McpClientsManager::registerToolsOn(::LLMQore::ToolsManager *tools) const
|
||||||
{
|
{
|
||||||
QList<PluginLLMCore::Provider *> out;
|
if (!tools)
|
||||||
auto &pm = PluginLLMCore::ProvidersManager::instance();
|
return;
|
||||||
for (const QString &name : pm.providersNames()) {
|
for (auto *c : m_connections) {
|
||||||
auto *p = pm.getProviderByName(name);
|
if (c)
|
||||||
if (!p)
|
c->registerToolsOn(tools);
|
||||||
continue;
|
|
||||||
if (p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools))
|
|
||||||
out.append(p);
|
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject McpClientsManager::builtinServers()
|
QJsonObject McpClientsManager::builtinServers()
|
||||||
@@ -319,8 +314,6 @@ void McpClientsManager::loadFromDisk()
|
|||||||
newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject()));
|
newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject()));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto providers = toolsCapableProviders();
|
|
||||||
|
|
||||||
const bool masterEnabled = Settings::mcpSettings().enableMcpClients();
|
const bool masterEnabled = Settings::mcpSettings().enableMcpClients();
|
||||||
|
|
||||||
QList<McpServerConnection *> keep;
|
QList<McpServerConnection *> keep;
|
||||||
@@ -350,7 +343,6 @@ void McpClientsManager::loadFromDisk()
|
|||||||
existing->deleteLater();
|
existing->deleteLater();
|
||||||
}
|
}
|
||||||
c = new McpServerConnection(cfg, this);
|
c = new McpServerConnection(cfg, this);
|
||||||
c->setProviders(providers);
|
|
||||||
connect(
|
connect(
|
||||||
c,
|
c,
|
||||||
&McpServerConnection::stateChanged,
|
&McpServerConnection::stateChanged,
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ public:
|
|||||||
bool removeServer(const QString &name);
|
bool removeServer(const QString &name);
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
|
void registerToolsOn(::LLMQore::ToolsManager *tools) const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void serversChanged();
|
void serversChanged();
|
||||||
void writeFailed(const QString &reason);
|
void writeFailed(const QString &reason);
|
||||||
@@ -50,7 +52,6 @@ private:
|
|||||||
void setupWatcher();
|
void setupWatcher();
|
||||||
void updateWatchedPaths();
|
void updateWatchedPaths();
|
||||||
|
|
||||||
QList<PluginLLMCore::Provider *> toolsCapableProviders() const;
|
|
||||||
static QJsonObject builtinServers();
|
static QJsonObject builtinServers();
|
||||||
QJsonObject readRoot() const;
|
QJsonObject readRoot() const;
|
||||||
bool writeRoot(const QJsonObject &root);
|
bool writeRoot(const QJsonObject &root);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <pluginllmcore/Provider.hpp>
|
|
||||||
#include <settings/McpSettings.hpp>
|
#include <settings/McpSettings.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Mcp {
|
namespace QodeAssist::Mcp {
|
||||||
@@ -35,13 +34,6 @@ QString transportToString(McpTransportKind k)
|
|||||||
return k == McpTransportKind::Http ? QStringLiteral("http") : QStringLiteral("stdio");
|
return k == McpTransportKind::Http ? QStringLiteral("http") : QStringLiteral("stdio");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool providerSupportsTools(PluginLLMCore::Provider *p)
|
|
||||||
{
|
|
||||||
if (!p)
|
|
||||||
return false;
|
|
||||||
return p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
McpServerConfig McpServerConfig::fromJson(const QString &name, const QJsonObject &obj)
|
McpServerConfig McpServerConfig::fromJson(const QString &name, const QJsonObject &obj)
|
||||||
@@ -133,15 +125,6 @@ McpServerConnection::~McpServerConnection()
|
|||||||
disconnectFromServer();
|
disconnectFromServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void McpServerConnection::setProviders(const QList<PluginLLMCore::Provider *> &providers)
|
|
||||||
{
|
|
||||||
m_providers.clear();
|
|
||||||
for (auto *p : providers) {
|
|
||||||
if (providerSupportsTools(p))
|
|
||||||
m_providers.append(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::LLMQore::Mcp::McpTransport *McpServerConnection::createTransport()
|
::LLMQore::Mcp::McpTransport *McpServerConnection::createTransport()
|
||||||
{
|
{
|
||||||
if (m_config.transport == McpTransportKind::Http) {
|
if (m_config.transport == McpTransportKind::Http) {
|
||||||
@@ -293,40 +276,20 @@ void McpServerConnection::fetchAndRegisterTools()
|
|||||||
[this](const QList<::LLMQore::Mcp::ToolInfo> &tools) {
|
[this](const QList<::LLMQore::Mcp::ToolInfo> &tools) {
|
||||||
if (m_listToolsWatchdog)
|
if (m_listToolsWatchdog)
|
||||||
m_listToolsWatchdog->stop();
|
m_listToolsWatchdog->stop();
|
||||||
if (m_providers.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("MCP client [%1]: no tools-capable providers to "
|
|
||||||
"register %2 tools into")
|
|
||||||
.arg(m_config.name)
|
|
||||||
.arg(tools.size()));
|
|
||||||
setState(
|
|
||||||
McpConnectionState::Connected,
|
|
||||||
QStringLiteral("Connected (%1 tools)").arg(tools.size()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
m_tools.clear();
|
||||||
for (const auto &info : tools) {
|
for (const auto &info : tools) {
|
||||||
if (info.name.isEmpty())
|
if (info.name.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
m_toolIds.append(info.name);
|
m_tools.append(info);
|
||||||
for (const auto &p : m_providers) {
|
|
||||||
if (!p)
|
|
||||||
continue;
|
|
||||||
auto *tm = p->toolsManager();
|
|
||||||
if (!tm)
|
|
||||||
continue;
|
|
||||||
auto *remote = new ::LLMQore::Mcp::McpRemoteTool(
|
|
||||||
m_client.data(), info, tm);
|
|
||||||
tm->addTool(remote);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(QString("MCP client [%1]: registered %2 tools across %3 providers")
|
LOG_MESSAGE(QString("MCP client [%1]: discovered %2 tools")
|
||||||
.arg(m_config.name)
|
.arg(m_config.name)
|
||||||
.arg(tools.size())
|
.arg(m_tools.size()));
|
||||||
.arg(m_providers.size()));
|
|
||||||
setState(
|
setState(
|
||||||
McpConnectionState::Connected,
|
McpConnectionState::Connected,
|
||||||
QStringLiteral("Connected (%1 tools)").arg(tools.size()));
|
QStringLiteral("Connected (%1 tools)").arg(m_tools.size()));
|
||||||
})
|
})
|
||||||
.onFailed(this, [this](const std::exception &e) {
|
.onFailed(this, [this](const std::exception &e) {
|
||||||
if (m_listToolsWatchdog)
|
if (m_listToolsWatchdog)
|
||||||
@@ -337,21 +300,19 @@ void McpServerConnection::fetchAndRegisterTools()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void McpServerConnection::registerToolsOn(::LLMQore::ToolsManager *tools)
|
||||||
|
{
|
||||||
|
if (!tools || !m_client || m_state != McpConnectionState::Connected)
|
||||||
|
return;
|
||||||
|
for (const auto &info : m_tools) {
|
||||||
|
auto *remote = new ::LLMQore::Mcp::McpRemoteTool(m_client.data(), info, tools);
|
||||||
|
tools->addTool(remote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void McpServerConnection::unregisterTools()
|
void McpServerConnection::unregisterTools()
|
||||||
{
|
{
|
||||||
if (m_toolIds.isEmpty())
|
m_tools.clear();
|
||||||
return;
|
|
||||||
|
|
||||||
for (const auto &p : m_providers) {
|
|
||||||
if (!p)
|
|
||||||
continue;
|
|
||||||
auto *tm = p->toolsManager();
|
|
||||||
if (!tm)
|
|
||||||
continue;
|
|
||||||
for (const QString &id : m_toolIds)
|
|
||||||
tm->removeTool(id);
|
|
||||||
}
|
|
||||||
m_toolIds.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void McpServerConnection::disconnectFromServer()
|
void McpServerConnection::disconnectFromServer()
|
||||||
|
|||||||
@@ -14,15 +14,17 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <LLMQore/McpTypes.hpp>
|
||||||
|
|
||||||
|
namespace LLMQore {
|
||||||
|
class ToolsManager;
|
||||||
|
}
|
||||||
|
|
||||||
namespace LLMQore::Mcp {
|
namespace LLMQore::Mcp {
|
||||||
class McpClient;
|
class McpClient;
|
||||||
class McpTransport;
|
class McpTransport;
|
||||||
} // namespace LLMQore::Mcp
|
} // namespace LLMQore::Mcp
|
||||||
|
|
||||||
namespace QodeAssist::PluginLLMCore {
|
|
||||||
class Provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Mcp {
|
namespace QodeAssist::Mcp {
|
||||||
|
|
||||||
enum class McpTransportKind { Http, Stdio };
|
enum class McpTransportKind { Http, Stdio };
|
||||||
@@ -61,10 +63,17 @@ public:
|
|||||||
const McpServerConfig &config() const { return m_config; }
|
const McpServerConfig &config() const { return m_config; }
|
||||||
McpConnectionState state() const { return m_state; }
|
McpConnectionState state() const { return m_state; }
|
||||||
QString statusText() const { return m_statusText; }
|
QString statusText() const { return m_statusText; }
|
||||||
int toolCount() const { return m_toolIds.size(); }
|
int toolCount() const { return m_tools.size(); }
|
||||||
QStringList toolNames() const { return m_toolIds; }
|
QStringList toolNames() const
|
||||||
|
{
|
||||||
|
QStringList names;
|
||||||
|
names.reserve(m_tools.size());
|
||||||
|
for (const auto &tool : m_tools)
|
||||||
|
names << tool.name;
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
void setProviders(const QList<PluginLLMCore::Provider *> &providers);
|
void registerToolsOn(::LLMQore::ToolsManager *tools);
|
||||||
|
|
||||||
void connectToServer();
|
void connectToServer();
|
||||||
void disconnectFromServer();
|
void disconnectFromServer();
|
||||||
@@ -75,7 +84,6 @@ signals:
|
|||||||
private:
|
private:
|
||||||
void setState(McpConnectionState state, const QString &text = {});
|
void setState(McpConnectionState state, const QString &text = {});
|
||||||
void fetchAndRegisterTools();
|
void fetchAndRegisterTools();
|
||||||
void registerTools(const QList<::LLMQore::Mcp::McpClient *> & /*unused*/);
|
|
||||||
void unregisterTools();
|
void unregisterTools();
|
||||||
::LLMQore::Mcp::McpTransport *createTransport();
|
::LLMQore::Mcp::McpTransport *createTransport();
|
||||||
|
|
||||||
@@ -87,8 +95,7 @@ private:
|
|||||||
QPointer<::LLMQore::Mcp::McpTransport> m_transport;
|
QPointer<::LLMQore::Mcp::McpTransport> m_transport;
|
||||||
QPointer<QTimer> m_listToolsWatchdog;
|
QPointer<QTimer> m_listToolsWatchdog;
|
||||||
|
|
||||||
QList<QPointer<PluginLLMCore::Provider>> m_providers;
|
QList<::LLMQore::Mcp::ToolInfo> m_tools;
|
||||||
QStringList m_toolIds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Mcp
|
} // namespace QodeAssist::Mcp
|
||||||
|
|||||||
@@ -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})
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 };
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "RulesLoader.hpp"
|
|
||||||
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFile>
|
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
|
|
||||||
namespace QodeAssist::PluginLLMCore {
|
|
||||||
|
|
||||||
QString RulesLoader::loadRules(const QString &projectPath, RulesContext context)
|
|
||||||
{
|
|
||||||
if (projectPath.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString combined;
|
|
||||||
QString basePath = projectPath + "/.qodeassist/rules";
|
|
||||||
|
|
||||||
switch (context) {
|
|
||||||
case RulesContext::Completions:
|
|
||||||
combined += loadAllMarkdownFiles(basePath + "/completions");
|
|
||||||
break;
|
|
||||||
case RulesContext::Chat:
|
|
||||||
combined += loadAllMarkdownFiles(basePath + "/common");
|
|
||||||
combined += loadAllMarkdownFiles(basePath + "/chat");
|
|
||||||
break;
|
|
||||||
case RulesContext::QuickRefactor:
|
|
||||||
combined += loadAllMarkdownFiles(basePath + "/common");
|
|
||||||
combined += loadAllMarkdownFiles(basePath + "/quickrefactor");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RulesLoader::loadRulesForProject(ProjectExplorer::Project *project, RulesContext context)
|
|
||||||
{
|
|
||||||
if (!project) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString projectPath = getProjectPath(project);
|
|
||||||
return loadRules(projectPath, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProjectExplorer::Project *RulesLoader::getActiveProject()
|
|
||||||
{
|
|
||||||
auto currentEditor = Core::EditorManager::currentEditor();
|
|
||||||
if (currentEditor && currentEditor->document()) {
|
|
||||||
Utils::FilePath filePath = currentEditor->document()->filePath();
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
|
||||||
if (project) {
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProjectExplorer::ProjectManager::startupProject();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RulesLoader::loadAllMarkdownFiles(const QString &dirPath)
|
|
||||||
{
|
|
||||||
QString combined;
|
|
||||||
QDir dir(dirPath);
|
|
||||||
|
|
||||||
if (!dir.exists()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
|
|
||||||
|
|
||||||
for (const QString &fileName : mdFiles) {
|
|
||||||
QFile file(dir.filePath(fileName));
|
|
||||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
||||||
combined += file.readAll();
|
|
||||||
combined += "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return combined;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RulesLoader::getProjectPath(ProjectExplorer::Project *project)
|
|
||||||
{
|
|
||||||
if (!project) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return project->projectDirectory().toUrlishString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<RuleFileInfo> RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context)
|
|
||||||
{
|
|
||||||
if (projectPath.isEmpty()) {
|
|
||||||
return QVector<RuleFileInfo>();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<RuleFileInfo> result;
|
|
||||||
QString basePath = projectPath + "/.qodeassist/rules";
|
|
||||||
|
|
||||||
// Always include common rules
|
|
||||||
result.append(collectMarkdownFiles(basePath + "/common", "common"));
|
|
||||||
|
|
||||||
// Add context-specific rules
|
|
||||||
switch (context) {
|
|
||||||
case RulesContext::Completions:
|
|
||||||
result.append(collectMarkdownFiles(basePath + "/completions", "completions"));
|
|
||||||
break;
|
|
||||||
case RulesContext::Chat:
|
|
||||||
result.append(collectMarkdownFiles(basePath + "/chat", "chat"));
|
|
||||||
break;
|
|
||||||
case RulesContext::QuickRefactor:
|
|
||||||
result.append(collectMarkdownFiles(basePath + "/quickrefactor", "quickrefactor"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<RuleFileInfo> RulesLoader::getRuleFilesForProject(
|
|
||||||
ProjectExplorer::Project *project, RulesContext context)
|
|
||||||
{
|
|
||||||
if (!project) {
|
|
||||||
return QVector<RuleFileInfo>();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString projectPath = getProjectPath(project);
|
|
||||||
return getRuleFiles(projectPath, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RulesLoader::loadRuleFileContent(const QString &filePath)
|
|
||||||
{
|
|
||||||
QFile file(filePath);
|
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.readAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<RuleFileInfo> RulesLoader::collectMarkdownFiles(
|
|
||||||
const QString &dirPath, const QString &category)
|
|
||||||
{
|
|
||||||
QVector<RuleFileInfo> result;
|
|
||||||
QDir dir(dirPath);
|
|
||||||
|
|
||||||
if (!dir.exists()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
|
|
||||||
|
|
||||||
for (const QString &fileName : mdFiles) {
|
|
||||||
QString fullPath = dir.filePath(fileName);
|
|
||||||
result.append({fullPath, fileName, category});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::PluginLLMCore
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace ProjectExplorer {
|
|
||||||
class Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::PluginLLMCore {
|
|
||||||
|
|
||||||
enum class RulesContext { Completions, Chat, QuickRefactor };
|
|
||||||
|
|
||||||
struct RuleFileInfo
|
|
||||||
{
|
|
||||||
QString filePath;
|
|
||||||
QString fileName;
|
|
||||||
QString category; // "common", "chat", "completions", "quickrefactor"
|
|
||||||
};
|
|
||||||
|
|
||||||
class RulesLoader
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static QString loadRules(const QString &projectPath, RulesContext context);
|
|
||||||
static QString loadRulesForProject(ProjectExplorer::Project *project, RulesContext context);
|
|
||||||
static ProjectExplorer::Project *getActiveProject();
|
|
||||||
|
|
||||||
// New methods for getting rule files info
|
|
||||||
static QVector<RuleFileInfo> getRuleFiles(const QString &projectPath, RulesContext context);
|
|
||||||
static QVector<RuleFileInfo> getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context);
|
|
||||||
static QString loadRuleFileContent(const QString &filePath);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static QString loadAllMarkdownFiles(const QString &dirPath);
|
|
||||||
static QVector<RuleFileInfo> collectMarkdownFiles(const QString &dirPath, const QString &category);
|
|
||||||
static QString getProjectPath(ProjectExplorer::Project *project);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::PluginLLMCore
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "OpenAICompatProvider.hpp"
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
|
||||||
|
|
||||||
#include "tools/ToolsRegistration.hpp"
|
|
||||||
#include "logger/Logger.hpp"
|
|
||||||
#include "settings/ChatAssistantSettings.hpp"
|
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
|
||||||
#include "settings/QuickRefactorSettings.hpp"
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
#include "settings/ProviderSettings.hpp"
|
|
||||||
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
|
||||||
|
|
||||||
OpenAICompatProvider::OpenAICompatProvider(QObject *parent)
|
|
||||||
: PluginLLMCore::Provider(parent)
|
|
||||||
, m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this))
|
|
||||||
{
|
|
||||||
Tools::registerQodeAssistTools(m_client->tools());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString OpenAICompatProvider::name() const
|
|
||||||
{
|
|
||||||
return "OpenAI Compatible";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString OpenAICompatProvider::apiKey() const
|
|
||||||
{
|
|
||||||
return Settings::providerSettings().openAiCompatApiKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString OpenAICompatProvider::url() const
|
|
||||||
{
|
|
||||||
return "http://localhost:1234/v1";
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenAICompatProvider::prepareRequest(
|
|
||||||
QJsonObject &request,
|
|
||||||
PluginLLMCore::PromptTemplate *prompt,
|
|
||||||
PluginLLMCore::ContextData context,
|
|
||||||
PluginLLMCore::RequestType type,
|
|
||||||
bool isToolsEnabled,
|
|
||||||
bool isThinkingEnabled)
|
|
||||||
{
|
|
||||||
if (!prompt->isSupportProvider(providerID())) {
|
|
||||||
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt->prepareRequest(request, context);
|
|
||||||
|
|
||||||
auto applyModelParams = [&request](const auto &settings) {
|
|
||||||
request["max_tokens"] = settings.maxTokens();
|
|
||||||
request["temperature"] = settings.temperature();
|
|
||||||
|
|
||||||
if (settings.useTopP())
|
|
||||||
request["top_p"] = settings.topP();
|
|
||||||
if (settings.useTopK())
|
|
||||||
request["top_k"] = settings.topK();
|
|
||||||
if (settings.useFrequencyPenalty())
|
|
||||||
request["frequency_penalty"] = settings.frequencyPenalty();
|
|
||||||
if (settings.usePresencePenalty())
|
|
||||||
request["presence_penalty"] = settings.presencePenalty();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
|
||||||
applyModelParams(Settings::codeCompletionSettings());
|
|
||||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
|
||||||
applyModelParams(Settings::quickRefactorSettings());
|
|
||||||
} else {
|
|
||||||
applyModelParams(Settings::chatAssistantSettings());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isToolsEnabled) {
|
|
||||||
auto toolsDefinitions = m_client->tools()->getToolsDefinitions();
|
|
||||||
if (!toolsDefinitions.isEmpty()) {
|
|
||||||
request["tools"] = toolsDefinitions;
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Added %1 tools to OpenAICompat request").arg(toolsDefinitions.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<QList<QString>> OpenAICompatProvider::getInstalledModels(const QString &)
|
|
||||||
{
|
|
||||||
return QtFuture::makeReadyFuture(QList<QString>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ProviderID OpenAICompatProvider::providerID() const
|
|
||||||
{
|
|
||||||
return PluginLLMCore::ProviderID::OpenAICompatible;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ProviderCapabilities OpenAICompatProvider::capabilities() const
|
|
||||||
{
|
|
||||||
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image
|
|
||||||
| PluginLLMCore::ProviderCapability::Thinking;
|
|
||||||
}
|
|
||||||
|
|
||||||
::LLMQore::BaseClient *OpenAICompatProvider::client() const
|
|
||||||
{
|
|
||||||
return m_client;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user