refactor: Move to agent-session architecture

This commit is contained in:
Petr Mironychev
2026-06-01 11:47:52 +02:00
parent 02c11ee5a0
commit 6220308a93
66 changed files with 1764 additions and 4039 deletions

View File

@@ -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);
}