mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 11:41:03 -04:00
320 lines
9.6 KiB
C++
320 lines
9.6 KiB
C++
// Copyright (C) 2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
|
|
#include "session/HistorySerializer.hpp"
|
|
|
|
#include <QJsonArray>
|
|
|
|
#include "session/HistoryProjection.hpp"
|
|
|
|
namespace QodeAssist::Session {
|
|
|
|
namespace {
|
|
|
|
QString roleToString(MessageRole role)
|
|
{
|
|
switch (role) {
|
|
case MessageRole::System:
|
|
return QStringLiteral("system");
|
|
case MessageRole::User:
|
|
return QStringLiteral("user");
|
|
case MessageRole::Assistant:
|
|
return QStringLiteral("assistant");
|
|
}
|
|
return QStringLiteral("assistant");
|
|
}
|
|
|
|
MessageRole roleFromString(const QString &role)
|
|
{
|
|
if (role == QLatin1String("system"))
|
|
return MessageRole::System;
|
|
if (role == QLatin1String("user"))
|
|
return MessageRole::User;
|
|
return MessageRole::Assistant;
|
|
}
|
|
|
|
RowKind legacyRowKind(int role)
|
|
{
|
|
switch (role) {
|
|
case 0:
|
|
return RowKind::System;
|
|
case 1:
|
|
return RowKind::User;
|
|
case 2:
|
|
return RowKind::Assistant;
|
|
case 3:
|
|
return RowKind::Tool;
|
|
case 4:
|
|
return RowKind::FileEdit;
|
|
case 5:
|
|
return RowKind::Thinking;
|
|
default:
|
|
return RowKind::Assistant;
|
|
}
|
|
}
|
|
|
|
QJsonObject usageToJson(const Usage &usage)
|
|
{
|
|
QJsonObject json;
|
|
json["promptTokens"] = usage.promptTokens;
|
|
json["completionTokens"] = usage.completionTokens;
|
|
if (usage.cachedPromptTokens > 0)
|
|
json["cachedPromptTokens"] = usage.cachedPromptTokens;
|
|
if (usage.reasoningTokens > 0)
|
|
json["reasoningTokens"] = usage.reasoningTokens;
|
|
return json;
|
|
}
|
|
|
|
Usage usageFromJson(const QJsonObject &json)
|
|
{
|
|
Usage usage;
|
|
usage.promptTokens = json["promptTokens"].toInt();
|
|
usage.completionTokens = json["completionTokens"].toInt();
|
|
usage.cachedPromptTokens = json["cachedPromptTokens"].toInt();
|
|
usage.reasoningTokens = json["reasoningTokens"].toInt();
|
|
return usage;
|
|
}
|
|
|
|
QJsonObject blockToJson(const ContentBlock &block)
|
|
{
|
|
static_assert(
|
|
std::variant_size_v<ContentBlock> == 8,
|
|
"ContentBlock gained an alternative; extend blockToJson and blockFromJson below or it is "
|
|
"written without a type and dropped on the next load");
|
|
|
|
QJsonObject json;
|
|
|
|
if (const auto *text = std::get_if<TextBlock>(&block)) {
|
|
json["type"] = "text";
|
|
json["text"] = text->text;
|
|
} else if (const auto *thinking = std::get_if<ThinkingBlock>(&block)) {
|
|
json["type"] = "thinking";
|
|
json["text"] = thinking->text;
|
|
if (!thinking->signature.isEmpty())
|
|
json["signature"] = thinking->signature;
|
|
if (thinking->redacted)
|
|
json["redacted"] = true;
|
|
} else if (const auto *tool = std::get_if<ToolCallBlock>(&block)) {
|
|
json["type"] = "tool_call";
|
|
json["id"] = tool->id;
|
|
json["name"] = tool->name;
|
|
if (!tool->arguments.isEmpty())
|
|
json["arguments"] = tool->arguments;
|
|
if (!tool->result.isEmpty())
|
|
json["result"] = tool->result;
|
|
if (!tool->kind.isEmpty())
|
|
json["kind"] = tool->kind;
|
|
if (!tool->status.isEmpty())
|
|
json["status"] = tool->status;
|
|
if (!tool->details.isEmpty())
|
|
json["details"] = tool->details;
|
|
if (tool->fromAgent)
|
|
json["fromAgent"] = true;
|
|
} else if (const auto *attachment = std::get_if<AttachmentBlock>(&block)) {
|
|
json["type"] = "attachment";
|
|
json["fileName"] = attachment->fileName;
|
|
json["storedPath"] = attachment->storedPath;
|
|
} else if (const auto *image = std::get_if<ImageBlock>(&block)) {
|
|
json["type"] = "image";
|
|
json["fileName"] = image->fileName;
|
|
json["storedPath"] = image->storedPath;
|
|
json["mediaType"] = image->mediaType;
|
|
} else if (const auto *edit = std::get_if<FileEditBlock>(&block)) {
|
|
json["type"] = "file_edit";
|
|
json["id"] = edit->id;
|
|
json["payload"] = edit->payload;
|
|
} else if (const auto *permission = std::get_if<PermissionBlock>(&block)) {
|
|
json = permissionBlockToJson(*permission);
|
|
json["type"] = "permission";
|
|
} else if (const auto *plan = std::get_if<PlanBlock>(&block)) {
|
|
json = planBlockToJson(*plan);
|
|
json["type"] = "plan";
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
std::optional<ContentBlock> blockFromJson(const QJsonObject &json)
|
|
{
|
|
const QString type = json["type"].toString();
|
|
|
|
if (type == QLatin1String("text"))
|
|
return ContentBlock{TextBlock{json["text"].toString()}};
|
|
|
|
if (type == QLatin1String("thinking")) {
|
|
return ContentBlock{ThinkingBlock{
|
|
json["text"].toString(), json["signature"].toString(), json["redacted"].toBool(false)}};
|
|
}
|
|
|
|
if (type == QLatin1String("tool_call")) {
|
|
return ContentBlock{ToolCallBlock{
|
|
json["id"].toString(),
|
|
json["name"].toString(),
|
|
json["arguments"].toObject(),
|
|
json["result"].toString(),
|
|
json["kind"].toString(),
|
|
restoredToolStatus(json["status"].toString()),
|
|
json["details"].toObject(),
|
|
json["fromAgent"].toBool(false)}};
|
|
}
|
|
|
|
if (type == QLatin1String("attachment"))
|
|
return ContentBlock{
|
|
AttachmentBlock{json["fileName"].toString(), json["storedPath"].toString()}};
|
|
|
|
if (type == QLatin1String("image")) {
|
|
return ContentBlock{ImageBlock{
|
|
json["fileName"].toString(),
|
|
json["storedPath"].toString(),
|
|
json["mediaType"].toString()}};
|
|
}
|
|
|
|
if (type == QLatin1String("file_edit"))
|
|
return ContentBlock{FileEditBlock{json["id"].toString(), json["payload"].toString()}};
|
|
|
|
if (type == QLatin1String("permission"))
|
|
return ContentBlock{restoredPermissionBlock(permissionBlockFromJson(json))};
|
|
|
|
if (type == QLatin1String("plan"))
|
|
return ContentBlock{planBlockFromJson(json)};
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
QJsonObject messageToJson(const Message &message)
|
|
{
|
|
QJsonArray blocks;
|
|
for (const ContentBlock &block : message.blocks)
|
|
blocks.append(blockToJson(block));
|
|
|
|
QJsonObject json;
|
|
json["role"] = roleToString(message.role);
|
|
if (!message.id.isEmpty())
|
|
json["id"] = message.id;
|
|
json["blocks"] = blocks;
|
|
if (!message.usage.isEmpty())
|
|
json["usage"] = usageToJson(message.usage);
|
|
|
|
return json;
|
|
}
|
|
|
|
Message messageFromJson(const QJsonObject &json, int &droppedBlocks)
|
|
{
|
|
Message message;
|
|
message.role = roleFromString(json["role"].toString());
|
|
message.id = json["id"].toString();
|
|
|
|
const QJsonArray blocks = json["blocks"].toArray();
|
|
for (const QJsonValue &value : blocks) {
|
|
if (auto block = blockFromJson(value.toObject()))
|
|
message.blocks.append(*block);
|
|
else
|
|
++droppedBlocks;
|
|
}
|
|
|
|
if (json.contains("usage"))
|
|
message.usage = usageFromJson(json["usage"].toObject());
|
|
|
|
return message;
|
|
}
|
|
|
|
MessageRow legacyRowFromJson(const QJsonObject &json)
|
|
{
|
|
MessageRow row;
|
|
row.kind = legacyRowKind(json["role"].toInt());
|
|
row.id = json["id"].toString();
|
|
row.content = json["content"].toString();
|
|
row.redacted = json["isRedacted"].toBool(false);
|
|
row.signature = json["signature"].toString();
|
|
row.toolName = json["toolName"].toString();
|
|
row.toolArguments = json["toolArguments"].toObject();
|
|
row.toolResult = json["toolResult"].toString();
|
|
|
|
const QJsonArray attachments = json["attachments"].toArray();
|
|
for (const QJsonValue &value : attachments) {
|
|
const QJsonObject attachment = value.toObject();
|
|
row.attachments.append(
|
|
AttachmentBlock{attachment["fileName"].toString(), attachment["storedPath"].toString()});
|
|
}
|
|
|
|
const QJsonArray images = json["images"].toArray();
|
|
for (const QJsonValue &value : images) {
|
|
const QJsonObject image = value.toObject();
|
|
row.images.append(
|
|
ImageBlock{
|
|
image["fileName"].toString(),
|
|
image["storedPath"].toString(),
|
|
image["mediaType"].toString()});
|
|
}
|
|
|
|
if (json.contains("usage"))
|
|
row.usage = usageFromJson(json["usage"].toObject());
|
|
|
|
return row;
|
|
}
|
|
|
|
ConversationHistory historyFromLegacyJson(const QJsonObject &root)
|
|
{
|
|
QList<MessageRow> rows;
|
|
const QJsonArray messages = root["messages"].toArray();
|
|
for (const QJsonValue &value : messages)
|
|
rows.append(legacyRowFromJson(value.toObject()));
|
|
|
|
return buildFromRows(rows);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
QString HistorySerializer::currentVersion()
|
|
{
|
|
return QStringLiteral("0.4");
|
|
}
|
|
|
|
bool HistorySerializer::isSupportedVersion(const QString &version)
|
|
{
|
|
return version == currentVersion() || version == QLatin1String("0.2")
|
|
|| version == QLatin1String("0.1");
|
|
}
|
|
|
|
QJsonObject HistorySerializer::toJson(const ConversationHistory &history)
|
|
{
|
|
QJsonArray messages;
|
|
for (const Message &message : history.messages())
|
|
messages.append(messageToJson(message));
|
|
|
|
QJsonObject root;
|
|
root["version"] = currentVersion();
|
|
root["messages"] = messages;
|
|
|
|
return root;
|
|
}
|
|
|
|
std::optional<ConversationHistory> HistorySerializer::fromJson(
|
|
const QJsonObject &root, int *droppedBlocks)
|
|
{
|
|
if (droppedBlocks)
|
|
*droppedBlocks = 0;
|
|
|
|
const QString version = root["version"].toString();
|
|
|
|
if (!isSupportedVersion(version) || !root["messages"].isArray())
|
|
return std::nullopt;
|
|
|
|
if (version != currentVersion())
|
|
return historyFromLegacyJson(root);
|
|
|
|
int dropped = 0;
|
|
ConversationHistory history;
|
|
const QJsonArray messages = root["messages"].toArray();
|
|
for (const QJsonValue &value : messages)
|
|
history.append(messageFromJson(value.toObject(), dropped));
|
|
|
|
if (droppedBlocks)
|
|
*droppedBlocks = dropped;
|
|
|
|
return history;
|
|
}
|
|
|
|
} // namespace QodeAssist::Session
|