mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
refactor: Change to chat conversation
This commit is contained in:
@@ -9,7 +9,6 @@
|
|||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <LLMQore/ContentBlocks.hpp>
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
@@ -43,7 +42,8 @@ void ChatCompressor::setActiveAgent(const QString &agentName)
|
|||||||
m_activeAgent = agentName;
|
m_activeAgent = agentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
void ChatCompressor::startCompression(
|
||||||
|
const QString &chatFilePath, ConversationHistory *sourceHistory)
|
||||||
{
|
{
|
||||||
if (m_isCompressing) {
|
if (m_isCompressing) {
|
||||||
emit compressionFailed(tr("Compression already in progress"));
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
@@ -55,7 +55,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatModel || chatModel->rowCount() == 0) {
|
if (!sourceHistory || sourceHistory->isEmpty()) {
|
||||||
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,9 +81,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_isCompressing = true;
|
m_isCompressing = true;
|
||||||
m_chatModel = chatModel;
|
|
||||||
m_originalChatPath = chatFilePath;
|
m_originalChatPath = chatFilePath;
|
||||||
m_accumulatedSummary.clear();
|
|
||||||
m_session = session;
|
m_session = session;
|
||||||
|
|
||||||
emit compressionStarted();
|
emit compressionStarted();
|
||||||
@@ -96,28 +94,26 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
|||||||
"discussion."));
|
"discussion."));
|
||||||
|
|
||||||
auto *history = session->history();
|
auto *history = session->history();
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
for (const auto &msg : sourceHistory->messages()) {
|
||||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit
|
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
|
||||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
|
||||||
continue;
|
continue;
|
||||||
if (msg.content.trimmed().isEmpty())
|
const QString text = msg.text();
|
||||||
|
if (text.trimmed().isEmpty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Message apiMessage(
|
Message apiMessage(msg.role());
|
||||||
msg.role == ChatModel::ChatRole::User ? Message::Role::User : Message::Role::Assistant);
|
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(text));
|
||||||
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
|
|
||||||
history->append(std::move(apiMessage));
|
history->append(std::move(apiMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_connections.append(connect(
|
connect(
|
||||||
client, &::LLMQore::BaseClient::chunkReceived,
|
session, &Session::finished, this,
|
||||||
this, &ChatCompressor::onPartialResponseReceived, Qt::UniqueConnection));
|
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
|
||||||
m_connections.append(connect(
|
connect(
|
||||||
client, &::LLMQore::BaseClient::requestCompleted,
|
session, &Session::failed, this,
|
||||||
this, &ChatCompressor::onFullResponseReceived, Qt::UniqueConnection));
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
m_connections.append(connect(
|
onCompressionFailed(id, error.message);
|
||||||
client, &::LLMQore::BaseClient::requestFailed,
|
});
|
||||||
this, &ChatCompressor::onRequestFailed, Qt::UniqueConnection));
|
|
||||||
|
|
||||||
client->setTransferTimeout(
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
@@ -149,26 +145,20 @@ void ChatCompressor::cancelCompression()
|
|||||||
emit compressionFailed(tr("Compression cancelled"));
|
emit compressionFailed(tr("Compression cancelled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
void ChatCompressor::onCompressionFinished(const QString &requestId)
|
||||||
{
|
{
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_accumulatedSummary += partialText;
|
QString summary;
|
||||||
}
|
if (m_session) {
|
||||||
|
if (auto *history = m_session->history(); history && !history->isEmpty())
|
||||||
|
summary = history->messages().back().text();
|
||||||
|
}
|
||||||
|
|
||||||
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
|
||||||
{
|
|
||||||
Q_UNUSED(fullText)
|
|
||||||
|
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
|
||||||
|
|
||||||
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
const QString summary = m_accumulatedSummary;
|
|
||||||
const QString sourcePath = m_originalChatPath;
|
const QString sourcePath = m_originalChatPath;
|
||||||
|
|
||||||
cleanupState();
|
cleanupState();
|
||||||
@@ -182,7 +172,7 @@ void ChatCompressor::onFullResponseReceived(const QString &requestId, const QStr
|
|||||||
emit compressionCompleted(compressedPath);
|
emit compressionCompleted(compressedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
|
||||||
{
|
{
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
return;
|
return;
|
||||||
@@ -242,11 +232,11 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
|
|
||||||
QJsonObject summaryMessage;
|
QJsonObject summaryMessage;
|
||||||
summaryMessage["role"] = "assistant";
|
summaryMessage["role"] = "assistant";
|
||||||
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
|
||||||
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
summaryMessage["isRedacted"] = false;
|
QJsonObject textBlock;
|
||||||
summaryMessage["attachments"] = QJsonArray();
|
textBlock["type"] = "text";
|
||||||
summaryMessage["images"] = QJsonArray();
|
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||||
|
summaryMessage["blocks"] = QJsonArray{textBlock};
|
||||||
|
|
||||||
root["messages"] = QJsonArray{summaryMessage};
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
root["compressedFrom"] = sourcePath;
|
root["compressedFrom"] = sourcePath;
|
||||||
@@ -265,24 +255,13 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::disconnectAllSignals()
|
|
||||||
{
|
|
||||||
for (const auto &connection : std::as_const(m_connections))
|
|
||||||
disconnect(connection);
|
|
||||||
m_connections.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatCompressor::cleanupState()
|
void ChatCompressor::cleanupState()
|
||||||
{
|
{
|
||||||
disconnectAllSignals();
|
|
||||||
|
|
||||||
Session *session = m_session;
|
Session *session = m_session;
|
||||||
|
|
||||||
m_isCompressing = false;
|
m_isCompressing = false;
|
||||||
m_currentRequestId.clear();
|
m_currentRequestId.clear();
|
||||||
m_originalChatPath.clear();
|
m_originalChatPath.clear();
|
||||||
m_accumulatedSummary.clear();
|
|
||||||
m_chatModel = nullptr;
|
|
||||||
m_session = nullptr;
|
m_session = nullptr;
|
||||||
|
|
||||||
if (session && m_sessionManager)
|
if (session && m_sessionManager)
|
||||||
|
|||||||
@@ -12,12 +12,11 @@
|
|||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
class SessionManager;
|
class SessionManager;
|
||||||
class Session;
|
class Session;
|
||||||
|
class ConversationHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class ChatCompressor : public QObject
|
class ChatCompressor : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -28,7 +27,7 @@ public:
|
|||||||
void setSessionManager(SessionManager *sessionManager);
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
void setActiveAgent(const QString &agentName);
|
void setActiveAgent(const QString &agentName);
|
||||||
|
|
||||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
|
||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
void cancelCompression();
|
void cancelCompression();
|
||||||
@@ -38,30 +37,23 @@ signals:
|
|||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
void compressionFailed(const QString &error);
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
|
||||||
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
|
||||||
void onRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void onCompressionFinished(const QString &requestId);
|
||||||
|
void onCompressionFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
QString createCompressedChatPath(const QString &originalPath) const;
|
QString createCompressedChatPath(const QString &originalPath) const;
|
||||||
QString buildCompressionPrompt() const;
|
QString buildCompressionPrompt() const;
|
||||||
bool createCompressedChatFile(
|
bool createCompressedChatFile(
|
||||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||||
void disconnectAllSignals();
|
|
||||||
void cleanupState();
|
void cleanupState();
|
||||||
void handleCompressionError(const QString &error);
|
void handleCompressionError(const QString &error);
|
||||||
|
|
||||||
bool m_isCompressing = false;
|
bool m_isCompressing = false;
|
||||||
QString m_currentRequestId;
|
QString m_currentRequestId;
|
||||||
QString m_originalChatPath;
|
QString m_originalChatPath;
|
||||||
QString m_accumulatedSummary;
|
|
||||||
QPointer<SessionManager> m_sessionManager;
|
QPointer<SessionManager> m_sessionManager;
|
||||||
QString m_activeAgent;
|
QString m_activeAgent;
|
||||||
QPointer<Session> m_session;
|
QPointer<Session> m_session;
|
||||||
ChatModel *m_chatModel = nullptr;
|
|
||||||
|
|
||||||
QList<QMetaObject::Connection> m_connections;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -16,15 +16,20 @@
|
|||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_history(history)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
QString ChatHistoryStore::historyDir() const
|
QString ChatHistoryStore::historyDir() const
|
||||||
@@ -52,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
|
|||||||
{
|
{
|
||||||
QString shortMessage;
|
QString shortMessage;
|
||||||
|
|
||||||
if (m_chatModel->rowCount() > 0) {
|
if (m_history) {
|
||||||
QString firstMessage
|
for (const auto &message : m_history->messages()) {
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
if (message.role() != Message::Role::User)
|
||||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
continue;
|
||||||
|
|
||||||
if (shortMessage.isEmpty()) {
|
const QString text = message.text();
|
||||||
QVariantList images
|
if (!text.trimmed().isEmpty()) {
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
shortMessage = text.split('\n').first().simplified().left(30);
|
||||||
if (!images.isEmpty()) {
|
} else {
|
||||||
shortMessage = "image_chat";
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (dynamic_cast<StoredImageContent *>(block.get())) {
|
||||||
|
shortMessage = "image_chat";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +118,12 @@ QString ChatHistoryStore::autosaveFilePath(
|
|||||||
|
|
||||||
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
return ChatSerializer::saveToFile(m_history, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
return ChatSerializer::loadFromFile(m_history, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatHistoryStore::showSaveDialog()
|
void ChatHistoryStore::showSaveDialog()
|
||||||
|
|||||||
@@ -9,16 +9,18 @@
|
|||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
class ChatModel;
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatHistoryStore : public QObject
|
class ChatHistoryStore : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
|
||||||
|
|
||||||
QString historyDir() const;
|
QString historyDir() const;
|
||||||
QString suggestedFileName() const;
|
QString suggestedFileName() const;
|
||||||
@@ -42,7 +44,7 @@ signals:
|
|||||||
private:
|
private:
|
||||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ConversationHistory *m_history;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,16 @@
|
|||||||
#include "MessagePart.hpp"
|
#include "MessagePart.hpp"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QVector>
|
||||||
#include <QtQmlIntegration>
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
#include "context/ContentFile.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -43,80 +48,19 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
struct ImageAttachment
|
|
||||||
{
|
|
||||||
QString fileName; // Original filename
|
|
||||||
QString storedPath; // Path to stored image file (relative to chat folder)
|
|
||||||
QString mediaType; // MIME type
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Message
|
|
||||||
{
|
|
||||||
ChatRole role;
|
|
||||||
QString content;
|
|
||||||
QString id;
|
|
||||||
bool isRedacted = false;
|
|
||||||
QString signature = QString();
|
|
||||||
|
|
||||||
QList<Context::ContentFile> attachments;
|
|
||||||
QList<ImageAttachment> images;
|
|
||||||
|
|
||||||
QString toolName;
|
|
||||||
QJsonObject toolArguments;
|
|
||||||
QString toolResult;
|
|
||||||
|
|
||||||
int promptTokens = 0;
|
|
||||||
int completionTokens = 0;
|
|
||||||
int cachedPromptTokens = 0;
|
|
||||||
int reasoningTokens = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ChatModel(QObject *parent = nullptr);
|
explicit ChatModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
Q_INVOKABLE void addMessage(
|
|
||||||
const QString &content,
|
|
||||||
ChatRole role,
|
|
||||||
const QString &id,
|
|
||||||
const QList<Context::ContentFile> &attachments = {},
|
|
||||||
const QList<ImageAttachment> &images = {},
|
|
||||||
bool isRedacted = false,
|
|
||||||
const QString &signature = QString());
|
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||||
|
|
||||||
QVector<Message> getChatHistory() const;
|
|
||||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
|
||||||
|
|
||||||
QString currentModel() const;
|
|
||||||
QString lastMessageId() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void resetModelTo(int index);
|
Q_INVOKABLE void resetModelTo(int index);
|
||||||
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments);
|
|
||||||
void setToolMessageData(
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments,
|
|
||||||
const QString &toolResult);
|
|
||||||
void updateToolResult(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &result);
|
|
||||||
void addThinkingBlock(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
|
||||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
|
||||||
|
|
||||||
void setMessageUsage(
|
void setMessageUsage(
|
||||||
const QString &messageId,
|
const QString &messageId,
|
||||||
int promptTokens,
|
int promptTokens,
|
||||||
@@ -128,10 +72,7 @@ public:
|
|||||||
int sessionCompletionTokens() const;
|
int sessionCompletionTokens() const;
|
||||||
int sessionCachedPromptTokens() const;
|
int sessionCachedPromptTokens() const;
|
||||||
int sessionTotalTokens() const;
|
int sessionTotalTokens() const;
|
||||||
|
|
||||||
void setLoadingFromHistory(bool loading);
|
|
||||||
bool isLoadingFromHistory() const;
|
|
||||||
|
|
||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
@@ -140,18 +81,60 @@ signals:
|
|||||||
void sessionUsageChanged();
|
void sessionUsageChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onFileEditApplied(const QString &editId);
|
void onHistoryMessageAdded(int index);
|
||||||
void onFileEditRejected(const QString &editId);
|
void onHistoryMessageUpdated(int index);
|
||||||
void onFileEditArchived(const QString &editId);
|
void onHistoryCleared();
|
||||||
|
void onHistoryReset();
|
||||||
|
void onFileEditStatusChanged(const QString &editId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
struct AttachmentRef
|
||||||
|
{
|
||||||
QVector<Message> m_messages;
|
QString fileName;
|
||||||
bool m_loadingFromHistory = false;
|
QString storedPath;
|
||||||
|
};
|
||||||
|
struct ImageRef
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
QString mediaType;
|
||||||
|
};
|
||||||
|
struct Row
|
||||||
|
{
|
||||||
|
ChatRole kind = ChatRole::Assistant;
|
||||||
|
int messageIndex = -1;
|
||||||
|
QString messageId;
|
||||||
|
QString content;
|
||||||
|
bool isRedacted = false;
|
||||||
|
QString editId;
|
||||||
|
QVector<AttachmentRef> attachments;
|
||||||
|
QVector<ImageRef> images;
|
||||||
|
};
|
||||||
|
struct Usage
|
||||||
|
{
|
||||||
|
int prompt = 0;
|
||||||
|
int completion = 0;
|
||||||
|
int cached = 0;
|
||||||
|
int reasoning = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void rebuildAll();
|
||||||
|
void reprojectTail(int startMessageIndex);
|
||||||
|
int startMessageIndexFor(int messageIndex) const;
|
||||||
|
int firstRowForMessage(int messageIndex) const;
|
||||||
|
QHash<QString, QString> buildToolResultMap() const;
|
||||||
|
void appendRowsForMessage(
|
||||||
|
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
|
||||||
|
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
|
||||||
|
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
|
||||||
|
QVariantList buildImageList(const QVector<ImageRef> &images) const;
|
||||||
|
|
||||||
|
QPointer<ConversationHistory> m_history;
|
||||||
|
QVector<Row> m_rows;
|
||||||
|
QHash<QString, Usage> m_usageByMessageId;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
#include "QodeAssistConstants.hpp"
|
#include "QodeAssistConstants.hpp"
|
||||||
|
|
||||||
#include <AgentFactory.hpp>
|
#include <AgentFactory.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
#include <SessionManager.hpp>
|
#include <SessionManager.hpp>
|
||||||
|
|
||||||
#include "ChatAgentController.hpp"
|
#include "ChatAgentController.hpp"
|
||||||
@@ -74,17 +76,20 @@ QKeySequence sendMessageKeySequence()
|
|||||||
|
|
||||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||||
: QQuickItem(parent)
|
: QQuickItem(parent)
|
||||||
|
, m_history(new QodeAssist::ConversationHistory(this))
|
||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||||
, m_fileManager(new ChatFileManager(this))
|
, m_fileManager(new ChatFileManager(this))
|
||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
, m_chatCompressor(new ChatCompressor(this))
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
, m_agentController(new ChatAgentController(this))
|
, m_agentController(new ChatAgentController(this))
|
||||||
, m_fileEditController(new FileEditController(m_chatModel, this))
|
, m_fileEditController(new FileEditController(this))
|
||||||
, m_tokenCounter(
|
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
|
||||||
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
, m_historyStore(new ChatHistoryStore(m_history, this))
|
||||||
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
|
|
||||||
{
|
{
|
||||||
|
m_chatModel->setHistory(m_history);
|
||||||
|
m_clientInterface->setHistory(m_history);
|
||||||
|
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
&Settings::chatAssistantSettings().linkOpenFiles,
|
&Settings::chatAssistantSettings().linkOpenFiles,
|
||||||
@@ -923,13 +928,12 @@ QString ChatRootView::chatTitle() const
|
|||||||
|
|
||||||
QString ChatRootView::computeChatTitle() const
|
QString ChatRootView::computeChatTitle() const
|
||||||
{
|
{
|
||||||
if (!m_chatModel)
|
if (!m_history)
|
||||||
return {};
|
return {};
|
||||||
const auto history = m_chatModel->getChatHistory();
|
for (const auto &msg : m_history->messages()) {
|
||||||
for (const auto &msg : history) {
|
if (msg.role() != Message::Role::User)
|
||||||
if (msg.role != ChatModel::User)
|
|
||||||
continue;
|
continue;
|
||||||
const QString content = msg.content.trimmed();
|
const QString content = msg.text().trimmed();
|
||||||
if (content.isEmpty())
|
if (content.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
||||||
@@ -1266,7 +1270,7 @@ void ChatRootView::compressCurrentChat()
|
|||||||
loadAvailableChatAgents();
|
loadAvailableChatAgents();
|
||||||
m_chatCompressor->setSessionManager(sessionManager());
|
m_chatCompressor->setSessionManager(sessionManager());
|
||||||
m_chatCompressor->setActiveAgent(currentChatAgent());
|
m_chatCompressor->setActiveAgent(currentChatAgent());
|
||||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
m_chatCompressor->startCompression(m_recentFilePath, m_history);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::cancelCompression()
|
void ChatRootView::cancelCompression()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class SkillsManager;
|
|||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
class AgentFactory;
|
class AgentFactory;
|
||||||
class SessionManager;
|
class SessionManager;
|
||||||
|
class ConversationHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -248,6 +249,7 @@ private:
|
|||||||
AgentFactory *agentFactory() const;
|
AgentFactory *agentFactory() const;
|
||||||
SessionManager *sessionManager() const;
|
SessionManager *sessionManager() const;
|
||||||
|
|
||||||
|
QodeAssist::ConversationHistory *m_history;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
ChatFileManager *m_fileManager;
|
ChatFileManager *m_fileManager;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <memory>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -13,12 +14,57 @@
|
|||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <MessageSerializer.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
const QString ChatSerializer::VERSION = "0.2";
|
namespace {
|
||||||
|
|
||||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
|
||||||
|
|
||||||
|
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
|
||||||
|
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
|
||||||
|
|
||||||
|
void registerEditFromResult(const QString &result)
|
||||||
{
|
{
|
||||||
|
const int pos = result.indexOf(kFileEditMarker);
|
||||||
|
if (pos < 0)
|
||||||
|
return;
|
||||||
|
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
if (!doc.isObject())
|
||||||
|
return;
|
||||||
|
const QJsonObject obj = doc.object();
|
||||||
|
const QString editId = obj.value("edit_id").toString();
|
||||||
|
const QString filePath = obj.value("file").toString();
|
||||||
|
if (editId.isEmpty() || filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Context::ChangesManager::instance().addFileEdit(
|
||||||
|
editId,
|
||||||
|
filePath,
|
||||||
|
obj.value("old_content").toString(),
|
||||||
|
obj.value("new_content").toString(),
|
||||||
|
/*autoApply=*/false,
|
||||||
|
/*isFromHistory=*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const QString ChatSerializer::VERSION = "0.3";
|
||||||
|
|
||||||
|
SerializationResult ChatSerializer::saveToFile(
|
||||||
|
const ConversationHistory *history, const QString &filePath)
|
||||||
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
if (!ensureDirectoryExists(filePath)) {
|
if (!ensureDirectoryExists(filePath)) {
|
||||||
return {false, "Failed to create directory structure"};
|
return {false, "Failed to create directory structure"};
|
||||||
}
|
}
|
||||||
@@ -28,9 +74,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = serializeChat(model, filePath);
|
QJsonDocument doc(serializeChat(history));
|
||||||
QJsonDocument doc(root);
|
|
||||||
|
|
||||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||||
}
|
}
|
||||||
@@ -38,8 +82,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {true, QString()};
|
return {true, QString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
SerializationResult ChatSerializer::loadFromFile(
|
||||||
|
ConversationHistory *history, const QString &filePath)
|
||||||
{
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||||
@@ -51,180 +99,140 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
|||||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = doc.object();
|
const QJsonObject root = doc.object();
|
||||||
QString version = root["version"].toString();
|
const QString version = root["version"].toString();
|
||||||
|
|
||||||
if (!validateVersion(version)) {
|
if (!validateVersion(version)) {
|
||||||
return {false, QString("Unsupported version: %1").arg(version)};
|
return {false, QString("Unsupported version: %1").arg(version)};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deserializeChat(model, root, filePath)) {
|
if (version == VERSION)
|
||||||
return {false, "Failed to deserialize chat data"};
|
return loadCurrent(history, root);
|
||||||
}
|
return loadLegacy(history, root);
|
||||||
|
|
||||||
return {true, QString()};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(
|
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
|
||||||
const ChatModel::Message &message, const QString &chatFilePath)
|
|
||||||
{
|
|
||||||
QJsonObject messageObj;
|
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
|
||||||
messageObj["content"] = message.content;
|
|
||||||
messageObj["id"] = message.id;
|
|
||||||
|
|
||||||
if (message.isRedacted) {
|
|
||||||
messageObj["isRedacted"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
|
||||||
messageObj["signature"] = message.signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
|
||||||
if (!message.toolName.isEmpty())
|
|
||||||
messageObj["toolName"] = message.toolName;
|
|
||||||
if (!message.toolArguments.isEmpty())
|
|
||||||
messageObj["toolArguments"] = message.toolArguments;
|
|
||||||
if (!message.toolResult.isEmpty())
|
|
||||||
messageObj["toolResult"] = message.toolResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.attachments.isEmpty()) {
|
|
||||||
QJsonArray attachmentsArray;
|
|
||||||
for (const auto &attachment : message.attachments) {
|
|
||||||
QJsonObject attachmentObj;
|
|
||||||
attachmentObj["fileName"] = attachment.filename;
|
|
||||||
attachmentObj["storedPath"] = attachment.content;
|
|
||||||
attachmentsArray.append(attachmentObj);
|
|
||||||
}
|
|
||||||
messageObj["attachments"] = attachmentsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.images.isEmpty()) {
|
|
||||||
QJsonArray imagesArray;
|
|
||||||
for (const auto &image : message.images) {
|
|
||||||
QJsonObject imageObj;
|
|
||||||
imageObj["fileName"] = image.fileName;
|
|
||||||
imageObj["storedPath"] = image.storedPath;
|
|
||||||
imageObj["mediaType"] = image.mediaType;
|
|
||||||
imagesArray.append(imageObj);
|
|
||||||
}
|
|
||||||
messageObj["images"] = imagesArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.promptTokens > 0 || message.completionTokens > 0) {
|
|
||||||
QJsonObject usageObj;
|
|
||||||
usageObj["promptTokens"] = message.promptTokens;
|
|
||||||
usageObj["completionTokens"] = message.completionTokens;
|
|
||||||
if (message.cachedPromptTokens > 0)
|
|
||||||
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
|
|
||||||
if (message.reasoningTokens > 0)
|
|
||||||
usageObj["reasoningTokens"] = message.reasoningTokens;
|
|
||||||
messageObj["usage"] = usageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatModel::Message ChatSerializer::deserializeMessage(
|
|
||||||
const QJsonObject &json, const QString &chatFilePath)
|
|
||||||
{
|
|
||||||
ChatModel::Message message;
|
|
||||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
|
||||||
message.content = json["content"].toString();
|
|
||||||
message.id = json["id"].toString();
|
|
||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
|
||||||
message.signature = json["signature"].toString();
|
|
||||||
message.toolName = json["toolName"].toString();
|
|
||||||
message.toolArguments = json["toolArguments"].toObject();
|
|
||||||
message.toolResult = json["toolResult"].toString();
|
|
||||||
|
|
||||||
if (json.contains("attachments")) {
|
|
||||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
|
||||||
for (const auto &attachmentValue : attachmentsArray) {
|
|
||||||
QJsonObject attachmentObj = attachmentValue.toObject();
|
|
||||||
Context::ContentFile attachment;
|
|
||||||
attachment.filename = attachmentObj["fileName"].toString();
|
|
||||||
attachment.content = attachmentObj["storedPath"].toString();
|
|
||||||
message.attachments.append(attachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("images")) {
|
|
||||||
QJsonArray imagesArray = json["images"].toArray();
|
|
||||||
for (const auto &imageValue : imagesArray) {
|
|
||||||
QJsonObject imageObj = imageValue.toObject();
|
|
||||||
ChatModel::ImageAttachment image;
|
|
||||||
image.fileName = imageObj["fileName"].toString();
|
|
||||||
image.storedPath = imageObj["storedPath"].toString();
|
|
||||||
image.mediaType = imageObj["mediaType"].toString();
|
|
||||||
message.images.append(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("usage")) {
|
|
||||||
const QJsonObject usageObj = json["usage"].toObject();
|
|
||||||
message.promptTokens = usageObj["promptTokens"].toInt();
|
|
||||||
message.completionTokens = usageObj["completionTokens"].toInt();
|
|
||||||
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
|
|
||||||
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
|
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray;
|
QJsonArray messagesArray;
|
||||||
for (const auto &message : model->getChatHistory()) {
|
for (const auto &message : history->messages())
|
||||||
messagesArray.append(serializeMessage(message, chatFilePath));
|
messagesArray.append(MessageSerializer::toJson(message));
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
root["version"] = VERSION;
|
root["version"] = VERSION;
|
||||||
root["messages"] = messagesArray;
|
root["messages"] = messagesArray;
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::deserializeChat(
|
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
|
||||||
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray = json["messages"].toArray();
|
history->clear();
|
||||||
QVector<ChatModel::Message> messages;
|
|
||||||
messages.reserve(messagesArray.size());
|
|
||||||
|
|
||||||
for (const auto &messageValue : messagesArray) {
|
const QJsonArray messagesArray = root["messages"].toArray();
|
||||||
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
|
for (const auto &value : messagesArray) {
|
||||||
|
bool ok = false;
|
||||||
|
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
|
||||||
|
if (ok)
|
||||||
|
history->append(std::move(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
registerHistoricalFileEdits(history);
|
||||||
|
return {true, QString()};
|
||||||
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(true);
|
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
|
||||||
|
{
|
||||||
|
history->clear();
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
const QJsonArray arr = root["messages"].toArray();
|
||||||
model->addMessage(
|
int i = 0;
|
||||||
message.content,
|
while (i < arr.size()) {
|
||||||
message.role,
|
const QJsonObject mj = arr[i].toObject();
|
||||||
message.id,
|
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
|
||||||
message.attachments,
|
|
||||||
message.images,
|
if (role == LegacyRole::Tool) {
|
||||||
message.isRedacted,
|
Message assistant(Message::Role::Assistant);
|
||||||
message.signature);
|
Message toolResults(Message::Role::User);
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
while (i < arr.size()
|
||||||
model->setToolMessageData(
|
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
|
||||||
message.id, message.toolName, message.toolArguments, message.toolResult);
|
const QJsonObject tj = arr[i].toObject();
|
||||||
|
const QString toolName = tj["toolName"].toString();
|
||||||
|
const QString id = tj["id"].toString();
|
||||||
|
if (!toolName.isEmpty()) {
|
||||||
|
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||||
|
id, toolName, tj["toolArguments"].toObject()));
|
||||||
|
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
|
||||||
|
id, tj["toolResult"].toString()));
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (!assistant.blocks().empty()) {
|
||||||
|
history->append(std::move(assistant));
|
||||||
|
history->append(std::move(toolResults));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
|
||||||
|
if (role == LegacyRole::FileEdit)
|
||||||
|
continue; // derived from the tool result in the new model
|
||||||
|
|
||||||
|
if (role == LegacyRole::Thinking) {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
const QString signature = mj["signature"].toString();
|
||||||
|
Message assistant(Message::Role::Assistant);
|
||||||
|
if (mj["isRedacted"].toBool(false)) {
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
|
||||||
|
} else {
|
||||||
|
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
|
||||||
|
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
|
||||||
|
}
|
||||||
|
history->append(std::move(assistant));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == LegacyRole::User) {
|
||||||
|
Message user(Message::Role::User, mj["id"].toString());
|
||||||
|
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
|
||||||
|
for (const auto &a : mj["attachments"].toArray()) {
|
||||||
|
const QJsonObject ao = a.toObject();
|
||||||
|
user.appendBlock(std::make_unique<StoredAttachmentContent>(
|
||||||
|
ao["fileName"].toString(), ao["storedPath"].toString()));
|
||||||
|
}
|
||||||
|
for (const auto &im : mj["images"].toArray()) {
|
||||||
|
const QJsonObject io = im.toObject();
|
||||||
|
user.appendBlock(std::make_unique<StoredImageContent>(
|
||||||
|
io["fileName"].toString(),
|
||||||
|
io["storedPath"].toString(),
|
||||||
|
io["mediaType"].toString()));
|
||||||
|
}
|
||||||
|
history->append(std::move(user));
|
||||||
|
} else {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
if (content.trimmed().isEmpty())
|
||||||
|
continue;
|
||||||
|
const Message::Role mapped
|
||||||
|
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
|
||||||
|
Message message(mapped, mj["id"].toString());
|
||||||
|
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||||
|
history->append(std::move(message));
|
||||||
}
|
}
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
|
||||||
.arg(message.images.size())
|
|
||||||
.arg(message.isRedacted)
|
|
||||||
.arg(message.signature.length()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
registerHistoricalFileEdits(history);
|
||||||
|
return {true, QString()};
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history)
|
||||||
|
{
|
||||||
|
for (const auto &message : history->messages()) {
|
||||||
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
|
||||||
|
registerEditFromResult(tr->result());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||||
@@ -236,18 +244,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatSerializer::validateVersion(const QString &version)
|
bool ChatSerializer::validateVersion(const QString &version)
|
||||||
{
|
{
|
||||||
if (version == VERSION) {
|
return version == VERSION || version == "0.2" || version == "0.1";
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version == "0.1") {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
|
||||||
"to _content");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -21,26 +22,26 @@ struct SerializationResult
|
|||||||
class ChatSerializer
|
class ChatSerializer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
static SerializationResult saveToFile(
|
||||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
const ConversationHistory *history, const QString &filePath);
|
||||||
|
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
|
||||||
// Public for testing purposes
|
|
||||||
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
|
|
||||||
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
|
|
||||||
// Content management (images and text files)
|
// Content management (images and text files)
|
||||||
static QString getChatContentFolder(const QString &chatFilePath);
|
static QString getChatContentFolder(const QString &chatFilePath);
|
||||||
static bool saveContentToStorage(const QString &chatFilePath,
|
static bool saveContentToStorage(
|
||||||
const QString &fileName,
|
const QString &chatFilePath,
|
||||||
const QString &base64Data,
|
const QString &fileName,
|
||||||
QString &storedPath);
|
const QString &base64Data,
|
||||||
|
QString &storedPath);
|
||||||
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static const QString VERSION;
|
||||||
static constexpr int CURRENT_VERSION = 1;
|
|
||||||
|
static QJsonObject serializeChat(const ConversationHistory *history);
|
||||||
|
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
|
||||||
|
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
|
||||||
|
static void registerHistoricalFileEdits(const ConversationHistory *history);
|
||||||
|
|
||||||
static bool ensureDirectoryExists(const QString &filePath);
|
static bool ensureDirectoryExists(const QString &filePath);
|
||||||
static bool validateVersion(const QString &version);
|
static bool validateVersion(const QString &version);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <LLMQore/ContentBlocks.hpp>
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
@@ -29,11 +31,10 @@
|
|||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include <ConversationHistory.hpp>
|
#include <ConversationHistory.hpp>
|
||||||
#include <Message.hpp>
|
|
||||||
#include <ContextRenderer.hpp>
|
#include <ContextRenderer.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
#include <Session.hpp>
|
#include <Session.hpp>
|
||||||
|
|
||||||
#include <QDir>
|
|
||||||
#include <SessionManager.hpp>
|
#include <SessionManager.hpp>
|
||||||
#include <SystemPromptBuilder.hpp>
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
@@ -52,6 +53,15 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct StoredImage
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
QString mediaType;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
@@ -73,6 +83,11 @@ void ClientInterface::setSessionManager(SessionManager *sessionManager)
|
|||||||
m_sessionManager = sessionManager;
|
m_sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setHistory(ConversationHistory *history)
|
||||||
|
{
|
||||||
|
m_history = history;
|
||||||
|
}
|
||||||
|
|
||||||
void ClientInterface::setActiveAgent(const QString &agentName)
|
void ClientInterface::setActiveAgent(const QString &agentName)
|
||||||
{
|
{
|
||||||
m_activeAgent = agentName;
|
m_activeAgent = agentName;
|
||||||
@@ -94,7 +109,6 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
|
|
||||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||||
|
|
||||||
@@ -126,7 +140,7 @@ void ClientInterface::sendMessage(
|
|||||||
.arg(textFiles.size()));
|
.arg(textFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ChatModel::ImageAttachment> imageAttachments;
|
QList<StoredImage> storedImages;
|
||||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
for (const QString &imagePath : imageFiles) {
|
for (const QString &imagePath : imageFiles) {
|
||||||
QString base64Data = encodeImageToBase64(imagePath);
|
QString base64Data = encodeImageToBase64(imagePath);
|
||||||
@@ -137,11 +151,8 @@ void ClientInterface::sendMessage(
|
|||||||
QFileInfo fileInfo(imagePath);
|
QFileInfo fileInfo(imagePath);
|
||||||
if (ChatSerializer::saveContentToStorage(
|
if (ChatSerializer::saveContentToStorage(
|
||||||
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||||
ChatModel::ImageAttachment imageAttachment;
|
storedImages.append(
|
||||||
imageAttachment.fileName = fileInfo.fileName();
|
{fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)});
|
||||||
imageAttachment.storedPath = storedPath;
|
|
||||||
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
|
||||||
imageAttachments.append(imageAttachment);
|
|
||||||
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,15 +167,15 @@ void ClientInterface::sendMessage(
|
|||||||
emit errorOccurred(error);
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!m_history) {
|
||||||
// Snapshot prior turns BEFORE the new user message is appended to the model.
|
const QString error = QStringLiteral("Chat history is not available");
|
||||||
const QVector<ChatModel::Message> priorHistory = m_chatModel->getChatHistory();
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
m_chatModel
|
return;
|
||||||
->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
}
|
||||||
|
|
||||||
QString sessionError;
|
QString sessionError;
|
||||||
Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
|
Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const QString error = sessionError.isEmpty()
|
const QString error = sessionError.isEmpty()
|
||||||
? QStringLiteral("No chat agent selected")
|
? QStringLiteral("No chat agent selected")
|
||||||
@@ -190,8 +201,12 @@ void ClientInterface::sendMessage(
|
|||||||
bindings.roleId = m_activeRoleId;
|
bindings.roleId = m_activeRoleId;
|
||||||
session->setContextBindings(bindings);
|
session->setContextBindings(bindings);
|
||||||
|
|
||||||
if (m_sessionManager)
|
const QString chatFilePath = m_chatFilePath;
|
||||||
m_sessionManager->toolContributors().contribute(client->tools());
|
session->setContentLoader([chatFilePath](const QString &storedPath) {
|
||||||
|
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||||
client->setTransferTimeout(
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
@@ -200,61 +215,25 @@ void ClientInterface::sendMessage(
|
|||||||
if (!chatContext.isEmpty())
|
if (!chatContext.isEmpty())
|
||||||
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
||||||
|
|
||||||
seedHistory(*session->history(), priorHistory);
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
|
||||||
|
|
||||||
QString userText = message;
|
for (const auto &attachment : storedAttachments) {
|
||||||
if (!storedAttachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
blocks.push_back(
|
||||||
userText += "\n\nAttached files:";
|
std::make_unique<StoredAttachmentContent>(attachment.filename, attachment.content));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
if (!storedImages.isEmpty() && session->supportsImages()) {
|
||||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(userText));
|
for (const auto &image : storedImages) {
|
||||||
|
blocks.push_back(std::make_unique<StoredImageContent>(
|
||||||
if (!imageAttachments.isEmpty() && session->supportsImages() && !m_chatFilePath.isEmpty()) {
|
image.fileName, image.storedPath, image.mediaType));
|
||||||
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()) {
|
} else if (!storedImages.isEmpty() && !session->supportsImages()) {
|
||||||
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
|
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
|
||||||
.arg(m_activeAgent)
|
.arg(m_activeAgent)
|
||||||
.arg(imageAttachments.size()));
|
.arg(storedImages.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 (!m_chatFilePath.isEmpty()) {
|
||||||
if (auto *todoTool
|
if (auto *todoTool
|
||||||
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
|
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
|
||||||
@@ -266,6 +245,18 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) {
|
||||||
|
onSessionEvent(session, ev);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
session, &Session::finished, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); });
|
||||||
|
connect(
|
||||||
|
session, &Session::failed, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onSessionFailed(id, error);
|
||||||
|
});
|
||||||
|
|
||||||
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||||
if (requestId.isEmpty()) {
|
if (requestId.isEmpty()) {
|
||||||
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
|
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
|
||||||
@@ -276,83 +267,87 @@ void ClientInterface::sendMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject request{{"id", requestId}};
|
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||||
m_activeRequests[requestId] = {request, session};
|
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
emit requestStarted(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::seedHistory(
|
QString ClientInterface::requestIdForSession(Session *session) const
|
||||||
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const
|
|
||||||
{
|
{
|
||||||
int i = 0;
|
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||||
while (i < messages.size()) {
|
if (it.value().session == session)
|
||||||
const ChatModel::Message &msg = messages[i];
|
return it.key();
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev)
|
||||||
|
{
|
||||||
|
if (ev.kind() != ResponseEvent::Kind::Usage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto *usage = ev.as<ResponseEvents::Usage>();
|
||||||
|
if (!usage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString requestId = requestIdForSession(session);
|
||||||
|
if (!requestId.isEmpty()) {
|
||||||
|
m_chatModel->setMessageUsage(
|
||||||
|
requestId,
|
||||||
|
usage->inputTokens,
|
||||||
|
usage->outputTokens,
|
||||||
|
usage->cachedTokens,
|
||||||
|
usage->reasoningTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit messageUsageReceived(
|
||||||
|
usage->inputTokens, usage->outputTokens, usage->cachedTokens, usage->reasoningTokens);
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||||
|
.arg(requestId)
|
||||||
|
.arg(usage->inputTokens)
|
||||||
|
.arg(usage->outputTokens)
|
||||||
|
.arg(usage->cachedTokens)
|
||||||
|
.arg(usage->reasoningTokens));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionFinished(const QString &requestId)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
|
|
||||||
|
QString applyError;
|
||||||
|
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
|
||||||
|
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||||
|
.arg(requestId, applyError));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit messageReceivedCompletely();
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
|
||||||
|
emit errorOccurred(error.message);
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::buildChatContextLayer(
|
QString ClientInterface::buildChatContextLayer(
|
||||||
@@ -431,39 +426,24 @@ QString ClientInterface::buildChatContextLayer(
|
|||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
{
|
{
|
||||||
m_chatModel->clear();
|
if (m_history)
|
||||||
|
m_history->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
const auto requests = m_activeRequests;
|
const auto requests = m_activeRequests;
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
m_awaitingContinuation.clear();
|
|
||||||
|
|
||||||
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||||
Session *session = it.value().session;
|
Session *session = it.value().session;
|
||||||
if (!session)
|
if (session && m_sessionManager)
|
||||||
continue;
|
|
||||||
if (auto *client = session->client())
|
|
||||||
disconnect(client, nullptr, this, nullptr);
|
|
||||||
if (m_sessionManager)
|
|
||||||
m_sessionManager->removeSession(session);
|
m_sessionManager->removeSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
|
||||||
{
|
|
||||||
const auto message = response.trimmed();
|
|
||||||
|
|
||||||
if (!message.isEmpty()) {
|
|
||||||
QString messageId = request["id"].toString();
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ClientInterface::getCurrentFileContext() const
|
QString ClientInterface::getCurrentFileContext() const
|
||||||
{
|
{
|
||||||
auto currentEditor = Core::EditorManager::currentEditor();
|
auto currentEditor = Core::EditorManager::currentEditor();
|
||||||
@@ -493,149 +473,6 @@ Context::ContextManager *ClientInterface::contextManager() const
|
|||||||
return m_contextManager;
|
return m_contextManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_awaitingContinuation.remove(requestId)) {
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_accumulatedResponses[requestId] += partialText;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const QJsonObject originalRequest = it.value().originalRequest;
|
|
||||||
Session *session = it.value().session;
|
|
||||||
|
|
||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
|
||||||
|
|
||||||
QString applyError;
|
|
||||||
bool applySuccess
|
|
||||||
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
|
|
||||||
|
|
||||||
if (!applySuccess) {
|
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
|
||||||
.arg(requestId, applyError));
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Message completed. Final response for message " + 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(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId))
|
|
||||||
return;
|
|
||||||
if (!info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
m_chatModel->setMessageUsage(
|
|
||||||
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
|
||||||
|
|
||||||
emit messageUsageReceived(
|
|
||||||
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
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(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_awaitingContinuation.remove(requestId)) {
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thinking.isEmpty()) {
|
|
||||||
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
|
||||||
} else {
|
|
||||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionStarted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
|
|
||||||
m_awaitingContinuation.insert(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ClientInterface::isImageFile(const QString &filePath) const
|
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QSet>
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
|
#include <ErrorInfo.hpp>
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <ResponseEvent.hpp>
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
@@ -36,6 +36,7 @@ public:
|
|||||||
|
|
||||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||||
void setSessionManager(SessionManager *sessionManager);
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
void setActiveAgent(const QString &agentName);
|
void setActiveAgent(const QString &agentName);
|
||||||
void setActiveRole(const QString &roleId);
|
void setActiveRole(const QString &roleId);
|
||||||
|
|
||||||
@@ -58,31 +59,15 @@ signals:
|
|||||||
void messageUsageReceived(
|
void messageUsageReceived(
|
||||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
void handleThinkingBlockReceived(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void handleToolExecutionStarted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments);
|
|
||||||
void handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
||||||
|
void onSessionFinished(const QString &requestId);
|
||||||
|
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
|
|
||||||
QString getCurrentFileContext() const;
|
QString getCurrentFileContext() const;
|
||||||
QString buildChatContextLayer(
|
QString buildChatContextLayer(
|
||||||
const QString &message, const QList<QString> &linkedFiles) const;
|
const QString &message, const QList<QString> &linkedFiles) const;
|
||||||
void seedHistory(
|
QString requestIdForSession(Session *session) const;
|
||||||
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const;
|
|
||||||
bool isImageFile(const QString &filePath) const;
|
bool isImageFile(const QString &filePath) const;
|
||||||
QString getMediaTypeForImage(const QString &filePath) const;
|
QString getMediaTypeForImage(const QString &filePath) const;
|
||||||
QString encodeImageToBase64(const QString &filePath) const;
|
QString encodeImageToBase64(const QString &filePath) const;
|
||||||
@@ -95,6 +80,7 @@ private:
|
|||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
|
QPointer<ConversationHistory> m_history;
|
||||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||||
QPointer<SessionManager> m_sessionManager;
|
QPointer<SessionManager> m_sessionManager;
|
||||||
QString m_activeAgent;
|
QString m_activeAgent;
|
||||||
@@ -102,8 +88,6 @@ private:
|
|||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
QHash<QString, QString> m_accumulatedResponses;
|
|
||||||
QSet<QString> m_awaitingContinuation;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -10,15 +10,13 @@
|
|||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "context/ChangesManager.h"
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
FileEditController::FileEditController(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
|
||||||
{
|
{
|
||||||
auto &changes = Context::ChangesManager::instance();
|
auto &changes = Context::ChangesManager::instance();
|
||||||
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||||
@@ -80,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit applied successfully"));
|
emit infoMessage(QString("File edit applied successfully"));
|
||||||
updateFileEditStatus(editId, "applied");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -95,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit rejected"));
|
emit infoMessage(QString("File edit rejected"));
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -110,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit undone successfully"));
|
emit infoMessage(QString("File edit undone successfully"));
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -163,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
|
||||||
{
|
|
||||||
auto messages = m_chatModel->getChatHistory();
|
|
||||||
for (int i = 0; i < messages.size(); ++i) {
|
|
||||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
|
||||||
QString content = messages[i].content;
|
|
||||||
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
|
|
||||||
QString jsonStr = content;
|
|
||||||
if (markerPos >= 0) {
|
|
||||||
jsonStr = content.mid(markerPos + marker.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject obj = doc.object();
|
|
||||||
obj["status"] = status;
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (!edit.statusMessage.isEmpty()) {
|
|
||||||
obj["status_message"] = edit.statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString updatedContent = marker
|
|
||||||
+ QString::fromUtf8(
|
|
||||||
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
|
||||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
|
||||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::applyAllForCurrentMessage()
|
void FileEditController::applyAllForCurrentMessage()
|
||||||
{
|
{
|
||||||
if (m_currentRequestId.isEmpty()) {
|
if (m_currentRequestId.isEmpty()) {
|
||||||
@@ -223,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
|
|||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
|
|||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,12 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class FileEditController : public QObject
|
class FileEditController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit FileEditController(QObject *parent = nullptr);
|
||||||
|
|
||||||
void setCurrentRequestId(const QString &requestId);
|
void setCurrentRequestId(const QString &requestId);
|
||||||
void clearCurrentRequestId();
|
void clearCurrentRequestId();
|
||||||
@@ -41,9 +39,6 @@ signals:
|
|||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
QString m_currentRequestId;
|
QString m_currentRequestId;
|
||||||
int m_totalEdits{0};
|
int m_totalEdits{0};
|
||||||
int m_appliedEdits{0};
|
int m_appliedEdits{0};
|
||||||
|
|||||||
@@ -9,17 +9,19 @@
|
|||||||
#include <utils/aspects.h>
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
#include "context/TokenUtils.hpp"
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
InputTokenCounter::InputTokenCounter(
|
InputTokenCounter::InputTokenCounter(
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_history(history)
|
||||||
, m_contextManager(contextManager)
|
, m_contextManager(contextManager)
|
||||||
{
|
{
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
@@ -100,10 +102,11 @@ void InputTokenCounter::recompute()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &history = m_chatModel->getChatHistory();
|
if (m_history) {
|
||||||
for (const auto &message : history) {
|
for (const auto &message : m_history->messages()) {
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
inputTokens += Context::TokenUtils::estimateTokens(message.text());
|
||||||
inputTokens += 4; // + role
|
inputTokens += 4; // + role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||||
|
|||||||
@@ -7,21 +7,25 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
class ContextManager;
|
class ContextManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class InputTokenCounter : public QObject
|
class InputTokenCounter : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
InputTokenCounter(
|
InputTokenCounter(
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
ConversationHistory *history,
|
||||||
|
Context::ContextManager *contextManager,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
int inputTokens() const;
|
int inputTokens() const;
|
||||||
|
|
||||||
@@ -37,7 +41,7 @@ signals:
|
|||||||
void inputTokensChanged();
|
void inputTokensChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ChatModel *m_chatModel;
|
ConversationHistory *m_history;
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
|
|
||||||
QStringList m_attachments;
|
QStringList m_attachments;
|
||||||
|
|||||||
@@ -325,9 +325,10 @@ Templates::ContextData Session::buildLegacyContext(
|
|||||||
} else if (auto *sa = dynamic_cast<StoredAttachmentContent *>(block)) {
|
} else if (auto *sa = dynamic_cast<StoredAttachmentContent *>(block)) {
|
||||||
if (!loader)
|
if (!loader)
|
||||||
continue;
|
continue;
|
||||||
const QString text = loader(sa->storedPath());
|
const QString stored = loader(sa->storedPath());
|
||||||
if (text.isEmpty())
|
if (stored.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
|
const QString text = QString::fromUtf8(QByteArray::fromBase64(stored.toUtf8()));
|
||||||
ContentBlockEntry e;
|
ContentBlockEntry e;
|
||||||
e.kind = ContentBlockEntry::Kind::Text;
|
e.kind = ContentBlockEntry::Kind::Text;
|
||||||
e.text = QStringLiteral("File: %1\n```\n%2\n```")
|
e.text = QStringLiteral("File: %1\n```\n%2\n```")
|
||||||
|
|||||||
Reference in New Issue
Block a user