Files
QodeAssist/tools/ReadOriginalHistoryTool.cpp

197 lines
6.0 KiB
C++

// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ReadOriginalHistoryTool.hpp"
#include <LLMQore/ToolExceptions.hpp>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtConcurrent>
namespace QodeAssist::Tools {
namespace {
QString roleName(int role)
{
switch (role) {
case 0:
return QStringLiteral("system");
case 1:
return QStringLiteral("user");
case 2:
return QStringLiteral("assistant");
case 3:
return QStringLiteral("tool");
case 4:
return QStringLiteral("file_edit");
case 5:
return QStringLiteral("thinking");
default:
return QStringLiteral("unknown");
}
}
QJsonObject readJsonObject(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
return doc.isObject() ? doc.object() : QJsonObject{};
}
QString resolveRootHistoryPath(const QString &sessionPath)
{
QString current = sessionPath;
QString rootPath;
for (int depth = 0; depth < 32; ++depth) {
const QJsonObject obj = readJsonObject(current);
const QString parent = obj.value("compressedFrom").toString();
if (parent.isEmpty() || parent == current)
break;
if (!QFile::exists(parent))
break;
rootPath = parent;
current = parent;
}
return rootPath;
}
} // namespace
ReadOriginalHistoryTool::ReadOriginalHistoryTool(QObject *parent)
: BaseTool(parent)
{}
QString ReadOriginalHistoryTool::id() const
{
return "read_original_history";
}
QString ReadOriginalHistoryTool::displayName() const
{
return "Reading pre-compression history";
}
QString ReadOriginalHistoryTool::description() const
{
return "Read the original, full chat history from before this conversation was "
"compressed into a summary. Use this only when the summary in context is "
"missing a detail you need (an exact code snippet, file path, decision, or "
"wording). The result can be large, so prefer the 'query' parameter to search "
"and 'offset'/'limit' to page through messages. Returns nothing useful if the "
"conversation was never compressed.";
}
QJsonObject ReadOriginalHistoryTool::parametersSchema() const
{
QJsonObject properties;
properties["query"] = QJsonObject{
{"type", "string"},
{"description",
"Optional case-insensitive substring. When set, only messages whose content "
"contains it are returned."}};
properties["role"] = QJsonObject{
{"type", "string"},
{"description",
"Optional role filter: 'user', 'assistant', 'system' or 'tool'."}};
properties["offset"] = QJsonObject{
{"type", "integer"},
{"description", "Index of the first matching message to return (default 0)."}};
properties["limit"] = QJsonObject{
{"type", "integer"},
{"description", "Maximum number of messages to return (default 20)."}};
QJsonObject definition;
definition["type"] = "object";
definition["properties"] = properties;
definition["required"] = QJsonArray{};
return definition;
}
QFuture<LLMQore::ToolResult> ReadOriginalHistoryTool::executeAsync(const QJsonObject &input)
{
QString sessionPath;
{
QMutexLocker locker(&m_mutex);
sessionPath = m_currentSessionId;
}
return QtConcurrent::run([input, sessionPath]() -> LLMQore::ToolResult {
if (sessionPath.isEmpty()) {
throw LLMQore::ToolRuntimeError(
"No active chat session, cannot locate pre-compression history.");
}
const QString rootPath = resolveRootHistoryPath(sessionPath);
if (rootPath.isEmpty()) {
return LLMQore::ToolResult::text(
"This conversation was never compressed; there is no separate "
"pre-compression history. The messages already in context are the full "
"history.");
}
const QJsonObject root = readJsonObject(rootPath);
const QJsonArray messages = root.value("messages").toArray();
const QString query = input.value("query").toString().trimmed();
const QString roleFilter = input.value("role").toString().trimmed().toLower();
const int offset = qMax(0, input.value("offset").toInt(0));
const int limit = qBound(1, input.value("limit").toInt(20), 200);
QStringList matched;
int matchCount = 0;
for (int i = 0; i < messages.size(); ++i) {
const QJsonObject msg = messages.at(i).toObject();
const QString role = roleName(msg.value("role").toInt());
const QString content = msg.value("content").toString();
if (!roleFilter.isEmpty() && role != roleFilter)
continue;
if (!query.isEmpty() && !content.contains(query, Qt::CaseInsensitive))
continue;
++matchCount;
if (matchCount <= offset || matched.size() >= limit)
continue;
matched.append(QString("[#%1 %2]\n%3").arg(i).arg(role, content));
}
const int shown = matched.size();
QString header = QString("Pre-compression history (%1): %2 matching message(s)")
.arg(rootPath)
.arg(matchCount);
if (shown < matchCount || offset > 0) {
header += QString(", showing %1-%2")
.arg(offset + 1)
.arg(offset + shown);
}
if (shown == 0)
return LLMQore::ToolResult::text(header + "\n\nNo messages to display.");
return LLMQore::ToolResult::text(header + "\n\n" + matched.join("\n\n---\n\n"));
});
}
void ReadOriginalHistoryTool::setCurrentSessionId(const QString &sessionId)
{
QMutexLocker locker(&m_mutex);
m_currentSessionId = sessionId;
}
} // namespace QodeAssist::Tools