mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
refactor: Move to agent-session architecture
This commit is contained in:
@@ -1,124 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "AgentRoleController.hpp"
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
AgentRoleController::AgentRoleController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().systemPrompt,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&AgentRoleController::baseSystemPromptChanged);
|
||||
|
||||
loadAvailableRoles();
|
||||
}
|
||||
|
||||
QStringList AgentRoleController::availableRoles() const
|
||||
{
|
||||
return m_availableRoles;
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRole() const
|
||||
{
|
||||
return m_currentRole;
|
||||
}
|
||||
|
||||
QString AgentRoleController::baseSystemPrompt() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().systemPrompt();
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRoleDescription() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
return role.description;
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRoleSystemPrompt() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return QString();
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return QString();
|
||||
|
||||
return role.systemPrompt;
|
||||
}
|
||||
|
||||
void AgentRoleController::loadAvailableRoles()
|
||||
{
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
m_availableRoles.clear();
|
||||
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
||||
|
||||
for (const auto &role : roles)
|
||||
m_availableRoles.append(role.name);
|
||||
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
|
||||
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
for (const auto &role : roles) {
|
||||
if (role.id == lastRoleId) {
|
||||
m_currentRole = role.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit availableRolesChanged();
|
||||
emit currentRoleChanged();
|
||||
}
|
||||
|
||||
void AgentRoleController::applyRole(const QString &roleName)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
||||
settings.lastUsedRoleId.setValue("");
|
||||
settings.writeSettings();
|
||||
m_currentRole = roleName;
|
||||
emit currentRoleChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
for (const auto &role : roles) {
|
||||
if (role.name == roleName) {
|
||||
settings.lastUsedRoleId.setValue(role.id);
|
||||
settings.writeSettings();
|
||||
m_currentRole = role.name;
|
||||
emit currentRoleChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRoleController::openSettings()
|
||||
{
|
||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class AgentRoleController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRoleController(QObject *parent = nullptr);
|
||||
|
||||
QStringList availableRoles() const;
|
||||
QString currentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentRoleDescription() const;
|
||||
QString currentRoleSystemPrompt() const;
|
||||
|
||||
void loadAvailableRoles();
|
||||
void applyRole(const QString &roleName);
|
||||
void openSettings();
|
||||
|
||||
signals:
|
||||
void availableRolesChanged();
|
||||
void currentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
|
||||
private:
|
||||
QStringList m_availableRoles;
|
||||
QString m_currentRole;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -75,8 +75,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
FileItem.hpp FileItem.cpp
|
||||
ChatFileManager.hpp ChatFileManager.cpp
|
||||
ChatCompressor.hpp ChatCompressor.cpp
|
||||
AgentRoleController.hpp AgentRoleController.cpp
|
||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
||||
ChatAgentController.hpp ChatAgentController.cpp
|
||||
FileEditController.hpp FileEditController.cpp
|
||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||
@@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
PluginLLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
QodeAssistUIControlsplugin
|
||||
QodeAssistLogger
|
||||
LLMQore
|
||||
Skills
|
||||
Agents
|
||||
Session
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
|
||||
91
ChatView/ChatAgentController.cpp
Normal file
91
ChatView/ChatAgentController.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatAgentController.hpp"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
#include <AgentFactory.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
namespace {
|
||||
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
|
||||
}
|
||||
|
||||
ChatAgentController::ChatAgentController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
m_currentAgent = settings->value(kChatAgentKey).toString();
|
||||
}
|
||||
|
||||
void ChatAgentController::setAgentFactory(AgentFactory *factory)
|
||||
{
|
||||
m_agentFactory = factory;
|
||||
reload();
|
||||
}
|
||||
|
||||
QStringList ChatAgentController::availableAgents() const
|
||||
{
|
||||
return m_availableAgents;
|
||||
}
|
||||
|
||||
QString ChatAgentController::currentAgent() const
|
||||
{
|
||||
return m_currentAgent;
|
||||
}
|
||||
|
||||
void ChatAgentController::setCurrentAgent(const QString &name)
|
||||
{
|
||||
if (name == m_currentAgent || !m_availableAgents.contains(name))
|
||||
return;
|
||||
|
||||
m_currentAgent = name;
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||
emit currentAgentChanged();
|
||||
}
|
||||
|
||||
void ChatAgentController::reload()
|
||||
{
|
||||
m_availableAgents = m_agentFactory ? m_agentFactory->configNames() : QStringList{};
|
||||
emit availableAgentsChanged();
|
||||
ensureValidCurrent();
|
||||
}
|
||||
|
||||
void ChatAgentController::ensureValidCurrent()
|
||||
{
|
||||
if (m_availableAgents.contains(m_currentAgent))
|
||||
return;
|
||||
|
||||
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
|
||||
if (next == m_currentAgent)
|
||||
return;
|
||||
|
||||
m_currentAgent = next;
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||
emit currentAgentChanged();
|
||||
}
|
||||
|
||||
bool ChatAgentController::currentSupportsThinking() const
|
||||
{
|
||||
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||
return false;
|
||||
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||
return config && config->enableThinking;
|
||||
}
|
||||
|
||||
bool ChatAgentController::currentSupportsTools() const
|
||||
{
|
||||
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||
return false;
|
||||
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||
return config && config->enableTools;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
47
ChatView/ChatAgentController.hpp
Normal file
47
ChatView/ChatAgentController.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatAgentController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatAgentController(QObject *parent = nullptr);
|
||||
|
||||
void setAgentFactory(AgentFactory *factory);
|
||||
|
||||
QStringList availableAgents() const;
|
||||
QString currentAgent() const;
|
||||
void setCurrentAgent(const QString &name);
|
||||
|
||||
bool currentSupportsThinking() const;
|
||||
bool currentSupportsTools() const;
|
||||
|
||||
void reload();
|
||||
|
||||
signals:
|
||||
void availableAgentsChanged();
|
||||
void currentAgentChanged();
|
||||
|
||||
private:
|
||||
void ensureValidCurrent();
|
||||
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QStringList m_availableAgents;
|
||||
QString m_currentAgent;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -4,13 +4,21 @@
|
||||
|
||||
#include "ChatCompressor.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@@ -25,6 +33,16 @@ ChatCompressor::ChatCompressor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void ChatCompressor::setActiveAgent(const QString &agentName)
|
||||
{
|
||||
m_activeAgent = agentName;
|
||||
}
|
||||
|
||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||
{
|
||||
if (m_isCompressing) {
|
||||
@@ -42,20 +60,23 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
return;
|
||||
}
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!m_provider) {
|
||||
emit compressionFailed(tr("No provider available"));
|
||||
if (!m_sessionManager) {
|
||||
emit compressionFailed(tr("Chat session manager is not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
templateName);
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
|
||||
if (!session) {
|
||||
emit compressionFailed(
|
||||
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!promptTemplate) {
|
||||
emit compressionFailed(tr("No template available"));
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
m_sessionManager->removeSession(session);
|
||||
emit compressionFailed(tr("Chat agent has no live client"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,23 +84,52 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
m_chatModel = chatModel;
|
||||
m_originalChatPath = chatFilePath;
|
||||
m_accumulatedSummary.clear();
|
||||
m_session = session;
|
||||
|
||||
emit compressionStarted();
|
||||
|
||||
connectProviderSignals();
|
||||
session->systemPrompt()->setLayer(
|
||||
QStringLiteral("compression"),
|
||||
QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of "
|
||||
"discussion."));
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
auto *history = session->history();
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
if (msg.content.trimmed().isEmpty())
|
||||
continue;
|
||||
|
||||
buildRequestPayload(payload, promptTemplate);
|
||||
Message apiMessage(
|
||||
msg.role == ChatModel::ChatRole::User ? Message::Role::User : Message::Role::Assistant);
|
||||
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
|
||||
history->append(std::move(apiMessage));
|
||||
}
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
m_provider->client()->setTransferTimeout(
|
||||
m_connections.append(connect(
|
||||
client, &::LLMQore::BaseClient::chunkReceived,
|
||||
this, &ChatCompressor::onPartialResponseReceived, Qt::UniqueConnection));
|
||||
m_connections.append(connect(
|
||||
client, &::LLMQore::BaseClient::requestCompleted,
|
||||
this, &ChatCompressor::onFullResponseReceived, Qt::UniqueConnection));
|
||||
m_connections.append(connect(
|
||||
client, &::LLMQore::BaseClient::requestFailed,
|
||||
this, &ChatCompressor::onRequestFailed, Qt::UniqueConnection));
|
||||
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
m_currentRequestId = m_provider->sendRequest(
|
||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
|
||||
|
||||
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
handleCompressionError(tr("Failed to start compression request"));
|
||||
return;
|
||||
}
|
||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||
}
|
||||
|
||||
@@ -94,10 +144,6 @@ void ChatCompressor::cancelCompression()
|
||||
return;
|
||||
|
||||
LOG_MESSAGE("Cancelling compression request");
|
||||
|
||||
if (m_provider && !m_currentRequestId.isEmpty())
|
||||
m_provider->cancelRequest(m_currentRequestId);
|
||||
|
||||
cleanupState();
|
||||
emit compressionFailed(tr("Compression cancelled"));
|
||||
}
|
||||
@@ -120,14 +166,18 @@ void ChatCompressor::onFullResponseReceived(const QString &requestId, const QStr
|
||||
LOG_MESSAGE(
|
||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||
|
||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||
handleCompressionError(tr("Failed to save compressed chat"));
|
||||
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
const QString summary = m_accumulatedSummary;
|
||||
const QString sourcePath = m_originalChatPath;
|
||||
|
||||
cleanupState();
|
||||
|
||||
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
|
||||
emit compressionFailed(tr("Failed to save compressed chat"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||
cleanupState();
|
||||
emit compressionCompleted(compressedPath);
|
||||
}
|
||||
|
||||
@@ -168,39 +218,6 @@ QString ChatCompressor::buildCompressionPrompt() const
|
||||
"Create the summary now:");
|
||||
}
|
||||
|
||||
void ChatCompressor::buildRequestPayload(
|
||||
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
||||
{
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
context.systemPrompt = QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool
|
||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
PluginLLMCore::Message compressionRequest;
|
||||
compressionRequest.role = "user";
|
||||
compressionRequest.content = buildCompressionPrompt();
|
||||
messages.append(compressionRequest);
|
||||
|
||||
context.history = messages;
|
||||
|
||||
m_provider->prepareRequest(
|
||||
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
||||
}
|
||||
|
||||
bool ChatCompressor::createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||
{
|
||||
@@ -247,32 +264,6 @@ bool ChatCompressor::createCompressedChatFile(
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatCompressor::connectProviderSignals()
|
||||
{
|
||||
auto *c = m_provider->client();
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ChatCompressor::onPartialResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ChatCompressor::onFullResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ChatCompressor::onRequestFailed,
|
||||
Qt::UniqueConnection));
|
||||
}
|
||||
|
||||
void ChatCompressor::disconnectAllSignals()
|
||||
{
|
||||
for (const auto &connection : std::as_const(m_connections))
|
||||
@@ -284,12 +275,17 @@ void ChatCompressor::cleanupState()
|
||||
{
|
||||
disconnectAllSignals();
|
||||
|
||||
Session *session = m_session;
|
||||
|
||||
m_isCompressing = false;
|
||||
m_currentRequestId.clear();
|
||||
m_originalChatPath.clear();
|
||||
m_accumulatedSummary.clear();
|
||||
m_chatModel = nullptr;
|
||||
m_provider = nullptr;
|
||||
m_session = nullptr;
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
class Provider;
|
||||
class PromptTemplate;
|
||||
} // namespace QodeAssist::PluginLLMCore
|
||||
namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@@ -25,6 +25,9 @@ class ChatCompressor : public QObject
|
||||
public:
|
||||
explicit ChatCompressor(QObject *parent = nullptr);
|
||||
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setActiveAgent(const QString &agentName);
|
||||
|
||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||
|
||||
bool isCompressing() const;
|
||||
@@ -45,17 +48,17 @@ private:
|
||||
QString buildCompressionPrompt() const;
|
||||
bool createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||
void connectProviderSignals();
|
||||
void disconnectAllSignals();
|
||||
void cleanupState();
|
||||
void handleCompressionError(const QString &error);
|
||||
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
||||
|
||||
bool m_isCompressing = false;
|
||||
QString m_currentRequestId;
|
||||
QString m_originalChatPath;
|
||||
QString m_accumulatedSummary;
|
||||
PluginLLMCore::Provider *m_provider = nullptr;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QString m_activeAgent;
|
||||
QPointer<Session> m_session;
|
||||
ChatModel *m_chatModel = nullptr;
|
||||
|
||||
QList<QMetaObject::Connection> m_connections;
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatConfigurationController.hpp"
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
connect(
|
||||
&settings.caProvider,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatConfigurationController::updateCurrentConfiguration);
|
||||
connect(
|
||||
&settings.caModel,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatConfigurationController::updateCurrentConfiguration);
|
||||
|
||||
loadAvailableConfigurations();
|
||||
}
|
||||
|
||||
QStringList ChatConfigurationController::availableConfigurations() const
|
||||
{
|
||||
return m_availableConfigurations;
|
||||
}
|
||||
|
||||
QString ChatConfigurationController::currentConfiguration() const
|
||||
{
|
||||
return m_currentConfiguration;
|
||||
}
|
||||
|
||||
void ChatConfigurationController::updateCurrentConfiguration()
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
m_currentConfiguration
|
||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
||||
emit currentConfigurationChanged();
|
||||
}
|
||||
|
||||
void ChatConfigurationController::loadAvailableConfigurations()
|
||||
{
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::Chat);
|
||||
|
||||
m_availableConfigurations.clear();
|
||||
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
m_availableConfigurations.append(config.name);
|
||||
}
|
||||
|
||||
updateCurrentConfiguration();
|
||||
|
||||
emit availableConfigurationsChanged();
|
||||
}
|
||||
|
||||
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
||||
{
|
||||
if (configName == QObject::tr("Current Settings")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::Chat);
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
if (config.name == configName) {
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
settings.caProvider.setValue(config.provider);
|
||||
settings.caModel.setValue(config.model);
|
||||
settings.caTemplate.setValue(config.templateName);
|
||||
settings.caUrl.setValue(config.url);
|
||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
||||
|
||||
settings.writeSettings();
|
||||
|
||||
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
||||
emit currentConfigurationChanged();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatConfigurationController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatConfigurationController(QObject *parent = nullptr);
|
||||
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
void loadAvailableConfigurations();
|
||||
void applyConfiguration(const QString &configName);
|
||||
|
||||
signals:
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
private:
|
||||
void updateCurrentConfiguration();
|
||||
|
||||
QStringList m_availableConfigurations;
|
||||
QString m_currentConfiguration;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -28,9 +28,11 @@
|
||||
|
||||
#include "QodeAssistConstants.hpp"
|
||||
|
||||
#include "AgentRoleController.hpp"
|
||||
#include <AgentFactory.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
|
||||
#include "ChatAgentController.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatConfigurationController.hpp"
|
||||
#include "ChatCompressor.hpp"
|
||||
#include "ChatHistoryStore.hpp"
|
||||
#include "FileEditController.hpp"
|
||||
@@ -38,10 +40,8 @@
|
||||
#include "InputTokenCounter.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "SessionFileRegistry.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "pluginllmcore/RulesLoader.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "sources/skills/SkillsManager.hpp"
|
||||
@@ -74,13 +74,11 @@ QKeySequence sendMessageKeySequence()
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
, m_fileManager(new ChatFileManager(this))
|
||||
, m_isRequestInProgress(false)
|
||||
, m_chatCompressor(new ChatCompressor(this))
|
||||
, m_agentRoleController(new AgentRoleController(this))
|
||||
, m_configurationController(new ChatConfigurationController(this))
|
||||
, m_agentController(new ChatAgentController(this))
|
||||
, m_fileEditController(new FileEditController(m_chatModel, this))
|
||||
, m_tokenCounter(
|
||||
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
||||
@@ -109,22 +107,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(
|
||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(
|
||||
m_configurationController,
|
||||
&ChatConfigurationController::availableConfigurationsChanged,
|
||||
this,
|
||||
&ChatRootView::availableConfigurationsChanged);
|
||||
connect(
|
||||
m_configurationController,
|
||||
&ChatConfigurationController::currentConfigurationChanged,
|
||||
this,
|
||||
&ChatRootView::currentConfigurationChanged);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
@@ -171,20 +153,20 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::inputTokensCountChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::availableRolesChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::availableAgentsChanged,
|
||||
this,
|
||||
&ChatRootView::availableAgentRolesChanged);
|
||||
&ChatRootView::availableChatAgentsChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::currentRoleChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::currentAgentChanged,
|
||||
this,
|
||||
&ChatRootView::currentAgentRoleChanged);
|
||||
&ChatRootView::currentChatAgentChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::baseSystemPromptChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::currentAgentChanged,
|
||||
this,
|
||||
&ChatRootView::baseSystemPromptChanged);
|
||||
&ChatRootView::isThinkingSupportChanged);
|
||||
|
||||
auto editors = Core::EditorManager::instance();
|
||||
|
||||
@@ -266,14 +248,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
connect(
|
||||
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
||||
|
||||
refreshRules();
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
||||
this,
|
||||
&ChatRootView::refreshRules);
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::projectAdded,
|
||||
@@ -298,12 +272,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::useThinkingChanged);
|
||||
|
||||
connect(
|
||||
&Settings::generalSettings().caProvider,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isThinkingSupportChanged);
|
||||
|
||||
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||
m_lastErrorMessage = error;
|
||||
emit lastErrorMessageChanged();
|
||||
@@ -373,6 +341,48 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
|
||||
return m_skillsManager;
|
||||
}
|
||||
|
||||
AgentFactory *ChatRootView::agentFactory() const
|
||||
{
|
||||
if (!m_agentFactory) {
|
||||
if (auto *engine = qmlEngine(this)) {
|
||||
m_agentFactory = qobject_cast<AgentFactory *>(
|
||||
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
|
||||
}
|
||||
}
|
||||
return m_agentFactory;
|
||||
}
|
||||
|
||||
SessionManager *ChatRootView::sessionManager() const
|
||||
{
|
||||
if (!m_sessionManager) {
|
||||
if (auto *engine = qmlEngine(this)) {
|
||||
m_sessionManager = qobject_cast<SessionManager *>(
|
||||
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
|
||||
}
|
||||
}
|
||||
return m_sessionManager;
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableChatAgents()
|
||||
{
|
||||
m_agentController->setAgentFactory(agentFactory());
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableChatAgents() const
|
||||
{
|
||||
return m_agentController->availableAgents();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentChatAgent() const
|
||||
{
|
||||
return m_agentController->currentAgent();
|
||||
}
|
||||
|
||||
void ChatRootView::setCurrentChatAgent(const QString &name)
|
||||
{
|
||||
m_agentController->setCurrentAgent(name);
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||
{
|
||||
QVariantList results;
|
||||
@@ -380,7 +390,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||
if (!manager || !Settings::skillsSettings().enableSkills())
|
||||
return results;
|
||||
|
||||
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
@@ -481,7 +491,12 @@ void ChatRootView::dispatchSend(
|
||||
|
||||
m_tokenCounter->recordSent();
|
||||
|
||||
if (currentChatAgent().isEmpty())
|
||||
loadAvailableChatAgents();
|
||||
|
||||
m_clientInterface->setSkillsManager(skillsManager());
|
||||
m_clientInterface->setSessionManager(sessionManager());
|
||||
m_clientInterface->setActiveAgent(currentChatAgent());
|
||||
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
||||
|
||||
m_fileManager->clearIntermediateStorage();
|
||||
@@ -527,12 +542,6 @@ void ChatRootView::clearMessages()
|
||||
clearLinkedFiles();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.caModel();
|
||||
}
|
||||
|
||||
void ChatRootView::saveHistory(const QString &filePath)
|
||||
{
|
||||
if (filePath != m_recentFilePath) {
|
||||
@@ -821,25 +830,6 @@ void ChatRootView::openChatHistoryFolder()
|
||||
m_historyStore->openHistoryFolder();
|
||||
}
|
||||
|
||||
void ChatRootView::openRulesFolder()
|
||||
{
|
||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString projectPath = project->projectDirectory().toFSPathString();
|
||||
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
||||
|
||||
QDir dir(rulesPath);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void ChatRootView::openSettings()
|
||||
{
|
||||
QMetaObject::invokeMethod(
|
||||
@@ -1120,51 +1110,6 @@ QString ChatRootView::lastErrorMessage() const
|
||||
return m_lastErrorMessage;
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::activeRules() const
|
||||
{
|
||||
return m_activeRules;
|
||||
}
|
||||
|
||||
int ChatRootView::activeRulesCount() const
|
||||
{
|
||||
return m_activeRules.size();
|
||||
}
|
||||
|
||||
QString ChatRootView::getRuleContent(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_activeRules.size())
|
||||
return QString();
|
||||
|
||||
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
||||
m_activeRules[index].toMap()["filePath"].toString());
|
||||
}
|
||||
|
||||
void ChatRootView::refreshRules()
|
||||
{
|
||||
m_activeRules.clear();
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (!project) {
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto ruleFiles
|
||||
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||
|
||||
for (const auto &ruleFile : ruleFiles) {
|
||||
QVariantMap ruleMap;
|
||||
ruleMap["filePath"] = ruleFile.filePath;
|
||||
ruleMap["fileName"] = ruleFile.fileName;
|
||||
ruleMap["category"] = ruleFile.category;
|
||||
m_activeRules.append(ruleMap);
|
||||
}
|
||||
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
}
|
||||
|
||||
bool ChatRootView::useTools() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().enableChatTools();
|
||||
@@ -1249,10 +1194,7 @@ QString ChatRootView::lastInfoMessage() const
|
||||
|
||||
bool ChatRootView::isThinkingSupport() const
|
||||
{
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
||||
return m_agentController->currentSupportsThinking();
|
||||
}
|
||||
|
||||
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
||||
@@ -1273,66 +1215,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
|
||||
return imageExtensions.contains(fileInfo.suffix().toLower());
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableConfigurations()
|
||||
{
|
||||
m_configurationController->loadAvailableConfigurations();
|
||||
}
|
||||
|
||||
void ChatRootView::applyConfiguration(const QString &configName)
|
||||
{
|
||||
m_configurationController->applyConfiguration(configName);
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableConfigurations() const
|
||||
{
|
||||
return m_configurationController->availableConfigurations();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentConfiguration() const
|
||||
{
|
||||
return m_configurationController->currentConfiguration();
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableAgentRoles()
|
||||
{
|
||||
m_agentRoleController->loadAvailableRoles();
|
||||
}
|
||||
|
||||
void ChatRootView::applyAgentRole(const QString &roleName)
|
||||
{
|
||||
m_agentRoleController->applyRole(roleName);
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableAgentRoles() const
|
||||
{
|
||||
return m_agentRoleController->availableRoles();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRole() const
|
||||
{
|
||||
return m_agentRoleController->currentRole();
|
||||
}
|
||||
|
||||
QString ChatRootView::baseSystemPrompt() const
|
||||
{
|
||||
return m_agentRoleController->baseSystemPrompt();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleDescription() const
|
||||
{
|
||||
return m_agentRoleController->currentRoleDescription();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
||||
{
|
||||
return m_agentRoleController->currentRoleSystemPrompt();
|
||||
}
|
||||
|
||||
void ChatRootView::openAgentRolesSettings()
|
||||
{
|
||||
m_agentRoleController->openSettings();
|
||||
}
|
||||
|
||||
void ChatRootView::compressCurrentChat()
|
||||
{
|
||||
if (m_chatCompressor->isCompressing()) {
|
||||
@@ -1349,6 +1231,10 @@ void ChatRootView::compressCurrentChat()
|
||||
|
||||
autosave();
|
||||
|
||||
if (currentChatAgent().isEmpty())
|
||||
loadAvailableChatAgents();
|
||||
m_chatCompressor->setSessionManager(sessionManager());
|
||||
m_chatCompressor->setActiveAgent(currentChatAgent());
|
||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,18 +11,21 @@
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
class SessionManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatCompressor;
|
||||
class AgentRoleController;
|
||||
class ChatConfigurationController;
|
||||
class ChatAgentController;
|
||||
class FileEditController;
|
||||
class InputTokenCounter;
|
||||
class ChatHistoryStore;
|
||||
@@ -32,7 +35,6 @@ class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
@@ -46,8 +48,6 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
||||
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
||||
@@ -57,13 +57,8 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
|
||||
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
|
||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
||||
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
||||
@@ -75,7 +70,6 @@ public:
|
||||
~ChatRootView() override;
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
@@ -104,7 +98,6 @@ public:
|
||||
QString sendShortcutText() const;
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
Q_INVOKABLE void openChatHistoryFolder();
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
Q_INVOKABLE void openSettings();
|
||||
|
||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||
@@ -139,11 +132,6 @@ public:
|
||||
void setRequestProgressStatus(bool state);
|
||||
|
||||
QString lastErrorMessage() const;
|
||||
|
||||
QVariantList activeRules() const;
|
||||
int activeRulesCount() const;
|
||||
Q_INVOKABLE QString getRuleContent(int index);
|
||||
Q_INVOKABLE void refreshRules();
|
||||
|
||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||
|
||||
@@ -161,23 +149,14 @@ public:
|
||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
||||
|
||||
Q_INVOKABLE void loadAvailableConfigurations();
|
||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
Q_INVOKABLE void compressCurrentChat();
|
||||
Q_INVOKABLE void cancelCompression();
|
||||
|
||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||
Q_INVOKABLE void openAgentRolesSettings();
|
||||
QStringList availableAgentRoles() const;
|
||||
QString currentAgentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentAgentRoleDescription() const;
|
||||
QString currentAgentRoleSystemPrompt() const;
|
||||
|
||||
Q_INVOKABLE void loadAvailableChatAgents();
|
||||
QStringList availableChatAgents() const;
|
||||
QString currentChatAgent() const;
|
||||
void setCurrentChatAgent(const QString &name);
|
||||
|
||||
int currentMessageTotalEdits() const;
|
||||
int currentMessageAppliedEdits() const;
|
||||
int currentMessagePendingEdits() const;
|
||||
@@ -206,7 +185,6 @@ public slots:
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
void attachmentFilesChanged();
|
||||
void linkedFilesChanged();
|
||||
void inputTokensCountChanged();
|
||||
@@ -223,20 +201,15 @@ signals:
|
||||
void lastErrorMessageChanged();
|
||||
void lastInfoMessageChanged();
|
||||
void sendShortcutTextChanged();
|
||||
void activeRulesChanged();
|
||||
void activeRulesCountChanged();
|
||||
|
||||
void useToolsChanged();
|
||||
void useThinkingChanged();
|
||||
void currentMessageEditsStatsChanged();
|
||||
|
||||
void isThinkingSupportChanged();
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
void availableAgentRolesChanged();
|
||||
void currentAgentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
void availableChatAgentsChanged();
|
||||
void currentChatAgentChanged();
|
||||
|
||||
void isCompressingChanged();
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
@@ -269,12 +242,12 @@ private:
|
||||
|
||||
SessionFileRegistry *sessionFileRegistry() const;
|
||||
Skills::SkillsManager *skillsManager() const;
|
||||
AgentFactory *agentFactory() const;
|
||||
SessionManager *sessionManager() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||
ClientInterface *m_clientInterface;
|
||||
ChatFileManager *m_fileManager;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
@@ -294,13 +267,11 @@ private:
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
bool m_isRequestInProgress;
|
||||
QString m_lastErrorMessage;
|
||||
QVariantList m_activeRules;
|
||||
|
||||
|
||||
QString m_lastInfoMessage;
|
||||
|
||||
ChatCompressor *m_chatCompressor;
|
||||
AgentRoleController *m_agentRoleController;
|
||||
ChatConfigurationController *m_configurationController;
|
||||
ChatAgentController *m_agentController;
|
||||
FileEditController *m_fileEditController;
|
||||
InputTokenCounter *m_tokenCounter;
|
||||
ChatHistoryStore *m_historyStore;
|
||||
@@ -308,6 +279,8 @@ private:
|
||||
mutable bool m_sessionFileRegistryResolved = false;
|
||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||
mutable bool m_skillsManagerResolved = false;
|
||||
mutable QPointer<AgentFactory> m_agentFactory;
|
||||
mutable QPointer<SessionManager> m_sessionManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -4,65 +4,75 @@
|
||||
|
||||
#include "ClientInterface.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <projectexplorer/buildconfiguration.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImageReader>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QMimeDatabase>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include "tools/ReadOriginalHistoryTool.hpp"
|
||||
#include "tools/TodoTool.hpp"
|
||||
#include "tools/ToolsRegistration.hpp"
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "ToolsSettings.hpp"
|
||||
#include <RulesLoader.hpp>
|
||||
#include <context/ChangesManager.h>
|
||||
#include <sources/skills/SkillsManager.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_chatModel(chatModel)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{}
|
||||
|
||||
ClientInterface::~ClientInterface()
|
||||
{
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
||||
{
|
||||
m_skillsManager = skillsManager;
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface()
|
||||
void ClientInterface::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
cancelRequest();
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void ClientInterface::setActiveAgent(const QString &agentName)
|
||||
{
|
||||
m_activeAgent = agentName;
|
||||
}
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
@@ -72,6 +82,8 @@ void ClientInterface::sendMessage(
|
||||
bool useTools,
|
||||
bool useThinking)
|
||||
{
|
||||
Q_UNUSED(useThinking)
|
||||
|
||||
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
||||
LOG_MESSAGE("Ignoring empty chat message");
|
||||
return;
|
||||
@@ -84,13 +96,11 @@ void ClientInterface::sendMessage(
|
||||
|
||||
QList<QString> imageFiles;
|
||||
QList<QString> textFiles;
|
||||
|
||||
for (const QString &filePath : attachments) {
|
||||
if (isImageFile(filePath)) {
|
||||
if (isImageFile(filePath))
|
||||
imageFiles.append(filePath);
|
||||
} else {
|
||||
else
|
||||
textFiles.append(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
QList<Context::ContentFile> storedAttachments;
|
||||
@@ -116,9 +126,8 @@ void ClientInterface::sendMessage(
|
||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
for (const QString &imagePath : imageFiles) {
|
||||
QString base64Data = encodeImageToBase64(imagePath);
|
||||
if (base64Data.isEmpty()) {
|
||||
if (base64Data.isEmpty())
|
||||
continue;
|
||||
}
|
||||
|
||||
QString storedPath;
|
||||
QFileInfo fileInfo(imagePath);
|
||||
@@ -129,7 +138,6 @@ void ClientInterface::sendMessage(
|
||||
imageAttachment.storedPath = storedPath;
|
||||
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
||||
imageAttachments.append(imageAttachment);
|
||||
|
||||
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
||||
}
|
||||
}
|
||||
@@ -138,318 +146,302 @@ void ClientInterface::sendMessage(
|
||||
.arg(imageFiles.size()));
|
||||
}
|
||||
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
if (!m_sessionManager) {
|
||||
const QString error = QStringLiteral("Chat session manager is not available");
|
||||
LOG_MESSAGE(error);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
// Snapshot prior turns BEFORE the new user message is appended to the model.
|
||||
const QVector<ChatModel::Message> priorHistory = m_chatModel->getChatHistory();
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
m_chatModel
|
||||
->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
|
||||
if (!session) {
|
||||
const QString error = sessionError.isEmpty()
|
||||
? QStringLiteral("No chat agent selected")
|
||||
: sessionError;
|
||||
LOG_MESSAGE(error);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
const bool isToolsEnabled = useTools;
|
||||
|
||||
if (chatAssistantSettings.useSystemPrompt()) {
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (!role.id.isEmpty())
|
||||
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
||||
}
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
|
||||
if (project) {
|
||||
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
|
||||
systemPrompt += QString(
|
||||
"\n# Project source root: %1"
|
||||
"\n# All new source files, headers, QML and CMake edits MUST be "
|
||||
"created or modified under this directory. Use absolute paths "
|
||||
"rooted here, or project-relative paths.")
|
||||
.arg(project->projectDirectory().toUrlishString());
|
||||
|
||||
if (auto target = project->activeTarget()) {
|
||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||
systemPrompt
|
||||
+= QString(
|
||||
"\n# Build output directory (compiler artifacts only — do NOT "
|
||||
"create or edit source files here): %1")
|
||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||
}
|
||||
}
|
||||
|
||||
QString projectRules
|
||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
||||
}
|
||||
} else {
|
||||
systemPrompt += QString("\n# No active project in IDE");
|
||||
}
|
||||
|
||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
projectSkillDirs = Settings::SkillsSettings::splitLines(
|
||||
projectSettings.projectSkillDirs());
|
||||
}
|
||||
m_skillsManager->configure(
|
||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||
Settings::SkillsSettings::splitPaths(
|
||||
Settings::skillsSettings().globalSkillRoots()),
|
||||
projectSkillDirs);
|
||||
|
||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||
if (!alwaysOnSkills.isEmpty())
|
||||
systemPrompt += QString("\n\n") + alwaysOnSkills;
|
||||
|
||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||
if (!skillsCatalog.isEmpty())
|
||||
systemPrompt += QString("\n\n") + skillsCatalog;
|
||||
|
||||
static const QRegularExpression skillCommand(
|
||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||
QStringList invokedSkillNames;
|
||||
auto skillMatch = skillCommand.globalMatch(message);
|
||||
while (skillMatch.hasNext()) {
|
||||
const QString skillName = skillMatch.next().captured(1);
|
||||
if (invokedSkillNames.contains(skillName))
|
||||
continue;
|
||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||
invokedSkillNames << skillName;
|
||||
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||
.arg(invokedSkill->name, invokedSkill->body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||
}
|
||||
context.systemPrompt = systemPrompt;
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
const QString error = QStringLiteral("Chat agent has no live client");
|
||||
LOG_MESSAGE(error);
|
||||
m_sessionManager->removeSession(session);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool toolHistory = promptTemplate->supportsToolHistory();
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
int toolCallMsgIdx = -1;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
||||
if (!toolHistory || msg.toolName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolCallMsgIdx < 0) {
|
||||
PluginLLMCore::Message assistantCall;
|
||||
assistantCall.role = "assistant";
|
||||
messages.append(assistantCall);
|
||||
toolCallMsgIdx = messages.size() - 1;
|
||||
}
|
||||
|
||||
PluginLLMCore::ToolCall call;
|
||||
call.id = msg.id;
|
||||
call.name = msg.toolName;
|
||||
call.arguments = msg.toolArguments;
|
||||
messages[toolCallMsgIdx].toolCalls.append(call);
|
||||
|
||||
PluginLLMCore::Message toolResult;
|
||||
toolResult.role = "tool";
|
||||
toolResult.toolCallId = msg.id;
|
||||
toolResult.toolName = msg.toolName;
|
||||
toolResult.content = msg.toolResult;
|
||||
messages.append(toolResult);
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCallMsgIdx = -1;
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::FileEdit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
|
||||
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
apiMessage.content += "\n\nAttached files:";
|
||||
for (const auto &attachment : msg.attachments) {
|
||||
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
||||
if (!fileContent.isEmpty()) {
|
||||
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
||||
.arg(attachment.filename, decodedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
||||
apiMessage.isRedacted = msg.isRedacted;
|
||||
apiMessage.signature = msg.signature;
|
||||
|
||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
||||
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
||||
auto apiImages = loadImagesFromStorage(msg.images);
|
||||
if (!apiImages.isEmpty()) {
|
||||
apiMessage.images = apiImages;
|
||||
}
|
||||
}
|
||||
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
if (!imageFiles.isEmpty()
|
||||
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
||||
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
||||
.arg(provider->name(), QString::number(imageFiles.size())));
|
||||
}
|
||||
|
||||
context.history = messages;
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
context,
|
||||
PluginLLMCore::RequestType::Chat,
|
||||
useTools,
|
||||
useThinking);
|
||||
|
||||
provider->client()->setMaxToolContinuations(
|
||||
Settings::toolsSettings().maxToolContinuations());
|
||||
|
||||
provider->client()->setTransferTimeout(
|
||||
Tools::registerQodeAssistTools(client->tools());
|
||||
if (m_skillsManager)
|
||||
Tools::registerSkillTool(client->tools(), m_skillsManager);
|
||||
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ClientInterface::handlePartialResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&ClientInterface::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::toolStarted,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionStarted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::toolResultReady,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionCompleted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::thinkingBlockReceived,
|
||||
this,
|
||||
&ClientInterface::handleThinkingBlockReceived,
|
||||
Qt::UniqueConnection);
|
||||
const QString chatContext = buildChatContextLayer(message, linkedFiles);
|
||||
if (!chatContext.isEmpty())
|
||||
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
QJsonObject request{{"id", requestId}};
|
||||
seedHistory(*session->history(), priorHistory);
|
||||
|
||||
m_activeRequests[requestId] = {request, provider, !toolHistory};
|
||||
QString userText = message;
|
||||
if (!storedAttachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
userText += "\n\nAttached files:";
|
||||
for (const auto &attachment : storedAttachments) {
|
||||
QString fileContent
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
||||
if (!fileContent.isEmpty()) {
|
||||
QString decoded = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
userText += QString("\n\nFile: %1\n```\n%2\n```").arg(attachment.filename, decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit requestStarted(requestId);
|
||||
|
||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(userText));
|
||||
|
||||
if (!imageAttachments.isEmpty() && session->supportsImages() && !m_chatFilePath.isEmpty()) {
|
||||
for (const auto &image : imageAttachments) {
|
||||
QString base64
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, image.storedPath);
|
||||
if (base64.isEmpty())
|
||||
continue;
|
||||
blocks.push_back(std::make_unique<LLMQore::ImageContent>(
|
||||
base64, image.mediaType, LLMQore::ImageContent::ImageSourceType::Base64));
|
||||
}
|
||||
} else if (!imageAttachments.isEmpty() && !session->supportsImages()) {
|
||||
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
|
||||
.arg(m_activeAgent)
|
||||
.arg(imageAttachments.size()));
|
||||
}
|
||||
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::chunkReceived,
|
||||
this, &ClientInterface::handlePartialResponse, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::requestCompleted,
|
||||
this, &ClientInterface::handleFullResponse, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::requestFinalized,
|
||||
this, &ClientInterface::handleRequestFinalized, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::requestFailed,
|
||||
this, &ClientInterface::handleRequestFailed, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::toolStarted,
|
||||
this, &ClientInterface::handleToolExecutionStarted, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::toolResultReady,
|
||||
this, &ClientInterface::handleToolExecutionCompleted, Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &::LLMQore::BaseClient::thinkingBlockReceived,
|
||||
this, &ClientInterface::handleThinkingBlockReceived, Qt::UniqueConnection);
|
||||
|
||||
if (!m_chatFilePath.isEmpty()) {
|
||||
if (auto *todoTool
|
||||
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
|
||||
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
||||
provider->toolsManager()->tool("read_original_history"))) {
|
||||
client->tools()->tool("read_original_history"))) {
|
||||
historyTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
const LLMQore::RequestID requestId = session->send(std::move(blocks), useTools);
|
||||
if (requestId.isEmpty()) {
|
||||
const QString error = QStringLiteral("Failed to start chat request for agent: %1")
|
||||
.arg(m_activeAgent);
|
||||
LOG_MESSAGE(error);
|
||||
m_sessionManager->removeSession(session);
|
||||
emit errorOccurred(error);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject request{{"id", requestId}};
|
||||
m_activeRequests[requestId] = {request, session, /*dropPreToolText=*/false};
|
||||
|
||||
emit requestStarted(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::seedHistory(
|
||||
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const
|
||||
{
|
||||
int i = 0;
|
||||
while (i < messages.size()) {
|
||||
const ChatModel::Message &msg = messages[i];
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
||||
Message assistant(Message::Role::Assistant);
|
||||
Message toolResults(Message::Role::User);
|
||||
while (i < messages.size() && messages[i].role == ChatModel::ChatRole::Tool) {
|
||||
const ChatModel::Message &toolMsg = messages[i];
|
||||
if (!toolMsg.toolName.isEmpty()) {
|
||||
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||
toolMsg.id, toolMsg.toolName, toolMsg.toolArguments));
|
||||
toolResults.appendBlock(
|
||||
std::make_unique<LLMQore::ToolResultContent>(toolMsg.id, toolMsg.toolResult));
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (!assistant.blocks().empty()) {
|
||||
history.append(std::move(assistant));
|
||||
history.append(std::move(toolResults));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::User) {
|
||||
Message userMessage(Message::Role::User);
|
||||
QString content = msg.content;
|
||||
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
content += "\n\nAttached files:";
|
||||
for (const auto &attachment : msg.attachments) {
|
||||
QString fileContent = ChatSerializer::loadContentFromStorage(
|
||||
m_chatFilePath, attachment.content);
|
||||
if (!fileContent.isEmpty()) {
|
||||
QString decoded
|
||||
= QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
content
|
||||
+= QString("\n\nFile: %1\n```\n%2\n```").arg(attachment.filename, decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
userMessage.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||
|
||||
if (!msg.images.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
for (const auto &image : msg.images) {
|
||||
QString base64 = ChatSerializer::loadContentFromStorage(
|
||||
m_chatFilePath, image.storedPath);
|
||||
if (base64.isEmpty())
|
||||
continue;
|
||||
userMessage.appendBlock(std::make_unique<LLMQore::ImageContent>(
|
||||
base64, image.mediaType, LLMQore::ImageContent::ImageSourceType::Base64));
|
||||
}
|
||||
}
|
||||
history.append(std::move(userMessage));
|
||||
} else { // Assistant
|
||||
if (msg.content.trimmed().isEmpty())
|
||||
continue;
|
||||
Message assistant(Message::Role::Assistant);
|
||||
assistant.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
|
||||
history.append(std::move(assistant));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
QString ClientInterface::buildChatContextLayer(
|
||||
const QString &message, const QList<QString> &linkedFiles) const
|
||||
{
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
QString context;
|
||||
|
||||
if (provider && !m_chatFilePath.isEmpty()
|
||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->clearSession(m_chatFilePath);
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (project) {
|
||||
context += QString("# Active project: %1").arg(project->displayName());
|
||||
context += QString(
|
||||
"\n# Project source root: %1"
|
||||
"\n# All new source files, headers, QML and CMake edits MUST be "
|
||||
"created or modified under this directory. Use absolute paths "
|
||||
"rooted here, or project-relative paths.")
|
||||
.arg(project->projectDirectory().toUrlishString());
|
||||
|
||||
if (auto target = project->activeTarget()) {
|
||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||
context += QString(
|
||||
"\n# Build output directory (compiler artifacts only — do NOT "
|
||||
"create or edit source files here): %1")
|
||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context += QString("# No active project in IDE");
|
||||
}
|
||||
|
||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
projectSkillDirs
|
||||
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
|
||||
}
|
||||
m_skillsManager->configure(
|
||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
|
||||
projectSkillDirs);
|
||||
|
||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||
if (!alwaysOnSkills.isEmpty())
|
||||
context += QString("\n\n") + alwaysOnSkills;
|
||||
|
||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||
if (!skillsCatalog.isEmpty())
|
||||
context += QString("\n\n") + skillsCatalog;
|
||||
|
||||
static const QRegularExpression skillCommand(
|
||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||
QStringList invokedSkillNames;
|
||||
auto skillMatch = skillCommand.globalMatch(message);
|
||||
while (skillMatch.hasNext()) {
|
||||
const QString skillName = skillMatch.next().captured(1);
|
||||
if (invokedSkillNames.contains(skillName))
|
||||
continue;
|
||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||
invokedSkillNames << skillName;
|
||||
context += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||
.arg(invokedSkill->name, invokedSkill->body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
context += "\n\nLinked files for reference:\n";
|
||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||
for (const auto &file : contentFiles)
|
||||
context += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
}
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
QSet<PluginLLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider->client(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
const RequestContext &ctx = it.value();
|
||||
if (ctx.provider) {
|
||||
ctx.provider->cancelRequest(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
const auto requests = m_activeRequests;
|
||||
m_activeRequests.clear();
|
||||
m_accumulatedResponses.clear();
|
||||
m_awaitingContinuation.clear();
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||
Session *session = it.value().session;
|
||||
if (!session)
|
||||
continue;
|
||||
if (auto *client = session->client())
|
||||
disconnect(client, nullptr, this, nullptr);
|
||||
if (m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
||||
@@ -486,23 +478,6 @@ QString ClientInterface::getCurrentFileContext() const
|
||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||
}
|
||||
|
||||
QString ClientInterface::getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const
|
||||
{
|
||||
QString updatedPrompt = basePrompt;
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||
|
||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||
for (const auto &file : contentFiles) {
|
||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedPrompt;
|
||||
}
|
||||
|
||||
Context::ContextManager *ClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
@@ -532,7 +507,8 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
const QJsonObject originalRequest = it.value().originalRequest;
|
||||
Session *session = it.value().session;
|
||||
|
||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
||||
|
||||
@@ -546,13 +522,16 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
||||
}
|
||||
|
||||
LOG_MESSAGE(
|
||||
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
|
||||
+ ": " + finalText);
|
||||
"Message completed. Final response for message " + originalRequest["id"].toString() + ": "
|
||||
+ finalText);
|
||||
emit messageReceivedCompletely();
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
m_awaitingContinuation.remove(requestId);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
void ClientInterface::handleRequestFinalized(
|
||||
@@ -584,12 +563,17 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
Session *session = it.value().session;
|
||||
|
||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||
emit errorOccurred(error);
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
m_awaitingContinuation.remove(requestId);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->removeSession(session);
|
||||
}
|
||||
|
||||
void ClientInterface::handleThinkingBlockReceived(
|
||||
@@ -693,46 +677,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
||||
return imageData.toBase64();
|
||||
}
|
||||
|
||||
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||
const QList<ChatModel::ImageAttachment> &storedImages) const
|
||||
{
|
||||
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
||||
|
||||
for (const auto &storedImage : storedImages) {
|
||||
QString base64Data
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||
if (base64Data.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginLLMCore::ImageAttachment apiImage;
|
||||
apiImage.data = base64Data;
|
||||
apiImage.mediaType = storedImage.mediaType;
|
||||
apiImage.isUrl = false;
|
||||
|
||||
apiImages.append(apiImage);
|
||||
}
|
||||
|
||||
return apiImages;
|
||||
}
|
||||
|
||||
void ClientInterface::setChatFilePath(const QString &filePath)
|
||||
{
|
||||
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (provider
|
||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->clearSession(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_chatFilePath = filePath;
|
||||
m_chatModel->setChatFilePath(filePath);
|
||||
}
|
||||
|
||||
@@ -5,16 +5,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "pluginllmcore/IPromptProvider.hpp"
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
@@ -26,11 +31,12 @@ class ClientInterface : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setActiveAgent(const QString &agentName);
|
||||
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
@@ -42,7 +48,7 @@ public:
|
||||
void cancelRequest();
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
|
||||
void setChatFilePath(const QString &filePath);
|
||||
QString chatFilePath() const;
|
||||
|
||||
@@ -74,24 +80,26 @@ private slots:
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||
QString getCurrentFileContext() const;
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
||||
QString buildChatContextLayer(
|
||||
const QString &message, const QList<QString> &linkedFiles) const;
|
||||
void seedHistory(
|
||||
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const;
|
||||
bool isImageFile(const QString &filePath) const;
|
||||
QString getMediaTypeForImage(const QString &filePath) const;
|
||||
QString encodeImageToBase64(const QString &filePath) const;
|
||||
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
QPointer<Session> session;
|
||||
bool dropPreToolText = false;
|
||||
};
|
||||
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QString m_activeAgent;
|
||||
QString m_chatFilePath;
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
|
||||
@@ -42,12 +36,6 @@ InputTokenCounter::InputTokenCounter(
|
||||
this,
|
||||
&InputTokenCounter::recompute);
|
||||
|
||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
});
|
||||
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
}
|
||||
|
||||
@@ -74,24 +62,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::rewireToolsChangedConnection()
|
||||
{
|
||||
if (m_toolsChangedConn)
|
||||
QObject::disconnect(m_toolsChangedConn);
|
||||
m_toolsChangedConn = {};
|
||||
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
if (!provider)
|
||||
return;
|
||||
auto *tm = provider->toolsManager();
|
||||
if (!tm)
|
||||
return;
|
||||
|
||||
m_toolsChangedConn = connect(
|
||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
||||
}
|
||||
|
||||
void InputTokenCounter::recompute()
|
||||
{
|
||||
int inputTokens = m_messageTokens;
|
||||
@@ -136,21 +106,6 @@ void InputTokenCounter::recompute()
|
||||
inputTokens += 4; // + role
|
||||
}
|
||||
|
||||
if (settings.enableChatTools()) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
||||
providerName)) {
|
||||
if (auto *tm = provider->toolsManager()) {
|
||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
||||
if (!toolDefs.isEmpty()) {
|
||||
const QByteArray serialized
|
||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||
emit inputTokensChanged();
|
||||
}
|
||||
|
||||
@@ -37,11 +37,8 @@ signals:
|
||||
void inputTokensChanged();
|
||||
|
||||
private:
|
||||
void rewireToolsChangedConnection();
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
QMetaObject::Connection m_toolsChangedConn;
|
||||
|
||||
QStringList m_attachments;
|
||||
QStringList m_linkedFiles;
|
||||
|
||||
@@ -152,29 +152,17 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
settingsButton.onClicked: root.openSettings()
|
||||
configSelector {
|
||||
model: root.availableConfigurations
|
||||
displayText: root.currentConfiguration
|
||||
agentSelector {
|
||||
model: root.availableChatAgents
|
||||
displayText: root.currentChatAgent
|
||||
onActivated: function(index) {
|
||||
if (index > 0) {
|
||||
root.applyConfiguration(root.availableConfigurations[index])
|
||||
}
|
||||
root.currentChatAgent = root.availableChatAgents[index]
|
||||
}
|
||||
|
||||
Component.onCompleted: root.loadAvailableChatAgents()
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
roleSelector {
|
||||
model: root.availableAgentRoles
|
||||
displayText: root.currentAgentRole
|
||||
onActivated: function(index) {
|
||||
root.applyAgentRole(root.availableAgentRoles[index])
|
||||
}
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableAgentRoles()
|
||||
root.loadAvailableChatAgents()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,20 +827,7 @@ ChatRootView {
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
baseSystemPrompt: root.baseSystemPrompt
|
||||
currentAgentRole: root.currentAgentRole
|
||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||
activeRules: root.activeRules
|
||||
activeRulesCount: root.activeRulesCount
|
||||
|
||||
onOpenSettings: root.openSettings()
|
||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||
onOpenRulesFolder: root.openRulesFolder()
|
||||
onRefreshRules: root.refreshRules()
|
||||
onRuleSelected: function(index) {
|
||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -26,8 +26,7 @@ Rectangle {
|
||||
property alias toolsButton: toolsButtonId
|
||||
property alias thinkingMode: thinkingModeId
|
||||
property alias settingsButton: settingsButtonId
|
||||
property alias configSelector: configSelectorId
|
||||
property alias roleSelector: roleSelector
|
||||
property alias agentSelector: agentSelectorId
|
||||
property alias relocateTooltip: relocateTooltipId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
@@ -134,7 +133,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: configSelectorId
|
||||
id: agentSelectorId
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
@@ -142,24 +141,9 @@ Rectangle {
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: configSelectorId.hovered
|
||||
visible: agentSelectorId.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch saved AI configuration")
|
||||
}
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: roleSelector
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
model: []
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: roleSelector.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch agent role (different system prompts)")
|
||||
text: qsTr("Select chat agent (provider, model and role come from the agent)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user