mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-29 17:49:12 -04:00
308 lines
12 KiB
C++
308 lines
12 KiB
C++
// Copyright (C) 2024-2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
|
|
#include "ContextAssembler.hpp"
|
|
|
|
#include <QJsonDocument>
|
|
#include <QLoggingCategory>
|
|
#include <QSet>
|
|
|
|
#include <algorithm>
|
|
|
|
#include "Message.hpp"
|
|
#include "PluginBlocks.hpp"
|
|
|
|
namespace QodeAssist::ContextAssembler {
|
|
|
|
namespace {
|
|
|
|
Q_LOGGING_CATEGORY(ctxLog, "qodeassist.context")
|
|
|
|
QString roleToWireString(Message::Role role)
|
|
{
|
|
switch (role) {
|
|
case Message::Role::System: return QStringLiteral("system");
|
|
case Message::Role::User: return QStringLiteral("user");
|
|
case Message::Role::Assistant: return QStringLiteral("assistant");
|
|
}
|
|
return QStringLiteral("user");
|
|
}
|
|
|
|
bool isReplayableThinking(const LLMQore::ThinkingContent *block)
|
|
{
|
|
return !block->signature().isEmpty();
|
|
}
|
|
|
|
Templates::ContentBlockEntry makeTextEntry(const QString &text)
|
|
{
|
|
Templates::ContentBlockEntry e;
|
|
e.kind = Templates::ContentBlockEntry::Kind::Text;
|
|
e.text = text;
|
|
return e;
|
|
}
|
|
|
|
QString placeholderFor(const QString &what, const QString &fileName)
|
|
{
|
|
return QStringLiteral("[%1 unavailable: %2]").arg(what, fileName);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
QString Manifest::summary() const
|
|
{
|
|
QString s = QStringLiteral(
|
|
"system=%1ch, history=%2 msgs -> %3 wire, text=%4ch, thinking=%5ch, "
|
|
"tools=%6 use/%7 result (%8ch), images=%9")
|
|
.arg(systemChars)
|
|
.arg(historyMessages)
|
|
.arg(wireMessages)
|
|
.arg(textChars)
|
|
.arg(thinkingChars)
|
|
.arg(toolUseBlocks)
|
|
.arg(toolResultBlocks)
|
|
.arg(toolChars)
|
|
.arg(imageBlocks);
|
|
if (pinnedBlocks > 0)
|
|
s += QStringLiteral(", pinned=%1 (%2ch)").arg(pinnedBlocks).arg(pinnedChars);
|
|
if (hasCompletionContext)
|
|
s += QStringLiteral(", fim");
|
|
if (unsupportedBlocks > 0)
|
|
s += QStringLiteral(", unsupported=%1").arg(unsupportedBlocks);
|
|
if (!elided.isEmpty())
|
|
s += QStringLiteral(", elided=%1 [%2]").arg(elided.size()).arg(elided.join(QStringLiteral("; ")));
|
|
return s;
|
|
}
|
|
|
|
Templates::ContextData assemble(
|
|
const std::vector<Message> &history,
|
|
const QString &systemPrompt,
|
|
const ContentLoader &loader,
|
|
const QVector<PinnedBlock> &pinned,
|
|
Manifest *outManifest)
|
|
{
|
|
using Templates::ContentBlockEntry;
|
|
using Templates::ContextData;
|
|
using WireMessage = Templates::Message;
|
|
|
|
Manifest manifest;
|
|
manifest.historyMessages = static_cast<int>(history.size());
|
|
|
|
ContextData ctx;
|
|
if (!systemPrompt.isEmpty()) {
|
|
ctx.systemPrompt = systemPrompt;
|
|
manifest.systemChars = systemPrompt.size();
|
|
}
|
|
|
|
QSet<QString> resolvedToolUseIds;
|
|
QSet<QString> declaredToolUseIds;
|
|
for (const auto &m : history) {
|
|
for (const auto &blockPtr : m.blocks()) {
|
|
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(blockPtr.get()))
|
|
resolvedToolUseIds.insert(tr->toolUseId());
|
|
if (auto *tu = dynamic_cast<LLMQore::ToolUseContent *>(blockPtr.get()))
|
|
declaredToolUseIds.insert(tu->id());
|
|
}
|
|
}
|
|
|
|
QVector<WireMessage> wireHistory;
|
|
|
|
for (const auto &m : history) {
|
|
if (m.role() == Message::Role::System) {
|
|
manifest.elided << QStringLiteral("system message skipped");
|
|
continue;
|
|
}
|
|
|
|
if (auto *cc = m.lastBlockOfType<CompletionContent>()) {
|
|
ctx.prefix = cc->prefix();
|
|
ctx.suffix = cc->suffix();
|
|
manifest.hasCompletionContext = true;
|
|
continue;
|
|
}
|
|
|
|
QVector<ContentBlockEntry> blockEntries;
|
|
|
|
for (const auto &blockPtr : m.blocks()) {
|
|
auto *block = blockPtr.get();
|
|
if (!block)
|
|
continue;
|
|
|
|
if (auto *t = dynamic_cast<LLMQore::TextContent *>(block)) {
|
|
blockEntries.append(makeTextEntry(t->text()));
|
|
manifest.textChars += t->text().size();
|
|
} else if (auto *img = dynamic_cast<LLMQore::ImageContent *>(block)) {
|
|
ContentBlockEntry e;
|
|
e.kind = ContentBlockEntry::Kind::Image;
|
|
e.imageData = img->data();
|
|
e.mediaType = img->mediaType();
|
|
e.isImageUrl
|
|
= (img->sourceType() == LLMQore::ImageContent::ImageSourceType::Url);
|
|
blockEntries.append(std::move(e));
|
|
++manifest.imageBlocks;
|
|
} else if (auto *si = dynamic_cast<StoredImageContent *>(block)) {
|
|
const QString base64 = loader ? loader(si->storedPath()) : QString();
|
|
if (base64.isEmpty()) {
|
|
blockEntries.append(
|
|
makeTextEntry(placeholderFor(QStringLiteral("Image"), si->fileName())));
|
|
manifest.elided
|
|
<< QStringLiteral("image unavailable: %1").arg(si->fileName());
|
|
qCWarning(ctxLog).noquote()
|
|
<< "stored image unavailable, placeholder inserted:" << si->fileName();
|
|
continue;
|
|
}
|
|
ContentBlockEntry e;
|
|
e.kind = ContentBlockEntry::Kind::Image;
|
|
e.imageData = base64;
|
|
e.mediaType = si->mediaType();
|
|
e.isImageUrl = false;
|
|
blockEntries.append(std::move(e));
|
|
++manifest.imageBlocks;
|
|
} else if (auto *sa = dynamic_cast<StoredAttachmentContent *>(block)) {
|
|
const QString stored = loader ? loader(sa->storedPath()) : QString();
|
|
if (stored.isEmpty()) {
|
|
blockEntries.append(makeTextEntry(
|
|
placeholderFor(QStringLiteral("Attachment"), sa->fileName())));
|
|
manifest.elided
|
|
<< QStringLiteral("attachment unavailable: %1").arg(sa->fileName());
|
|
qCWarning(ctxLog).noquote()
|
|
<< "stored attachment unavailable, placeholder inserted:"
|
|
<< sa->fileName();
|
|
continue;
|
|
}
|
|
const QString text = QString::fromUtf8(QByteArray::fromBase64(stored.toUtf8()));
|
|
blockEntries.append(makeTextEntry(
|
|
QStringLiteral("File: %1\n```\n%2\n```").arg(sa->fileName(), text)));
|
|
manifest.textChars += text.size();
|
|
} else if (auto *sk = dynamic_cast<SkillInvocationContent *>(block)) {
|
|
blockEntries.append(makeTextEntry(
|
|
QStringLiteral("# Invoked Skill: %1\n\n%2").arg(sk->skillName(), sk->body())));
|
|
manifest.textChars += sk->body().size();
|
|
} else if (auto *th = dynamic_cast<LLMQore::ThinkingContent *>(block)) {
|
|
if (!isReplayableThinking(th)) {
|
|
manifest.elided << QStringLiteral("unsigned thinking dropped");
|
|
continue;
|
|
}
|
|
ContentBlockEntry e;
|
|
e.kind = ContentBlockEntry::Kind::Thinking;
|
|
e.thinking = th->thinking();
|
|
e.signature = th->signature();
|
|
blockEntries.append(std::move(e));
|
|
manifest.thinkingChars += th->thinking().size();
|
|
} else if (auto *rth = dynamic_cast<LLMQore::RedactedThinkingContent *>(block)) {
|
|
if (rth->signature().isEmpty()) {
|
|
manifest.elided << QStringLiteral("unsigned redacted thinking dropped");
|
|
continue;
|
|
}
|
|
ContentBlockEntry e;
|
|
e.kind = ContentBlockEntry::Kind::RedactedThinking;
|
|
e.signature = rth->signature();
|
|
blockEntries.append(std::move(e));
|
|
} else if (auto *tu = dynamic_cast<LLMQore::ToolUseContent *>(block)) {
|
|
if (!resolvedToolUseIds.contains(tu->id())) {
|
|
manifest.elided
|
|
<< QStringLiteral("orphan tool_use dropped: %1").arg(tu->id());
|
|
continue;
|
|
}
|
|
ContentBlockEntry e;
|
|
e.kind = ContentBlockEntry::Kind::ToolUse;
|
|
e.toolUseId = tu->id();
|
|
e.toolName = tu->name();
|
|
e.toolInput = tu->input();
|
|
blockEntries.append(std::move(e));
|
|
++manifest.toolUseBlocks;
|
|
manifest.toolChars
|
|
+= QJsonDocument(tu->input()).toJson(QJsonDocument::Compact).size();
|
|
} else if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block)) {
|
|
if (!declaredToolUseIds.contains(tr->toolUseId())) {
|
|
manifest.elided
|
|
<< QStringLiteral("orphan tool_result dropped: %1").arg(tr->toolUseId());
|
|
continue;
|
|
}
|
|
ContentBlockEntry e;
|
|
e.kind = ContentBlockEntry::Kind::ToolResult;
|
|
e.toolUseId = tr->toolUseId();
|
|
e.result = tr->result();
|
|
blockEntries.append(std::move(e));
|
|
++manifest.toolResultBlocks;
|
|
manifest.toolChars += tr->result().size();
|
|
} else {
|
|
++manifest.unsupportedBlocks;
|
|
}
|
|
}
|
|
|
|
if (blockEntries.isEmpty())
|
|
continue;
|
|
|
|
const bool hasNonThinking = std::any_of(
|
|
blockEntries.begin(), blockEntries.end(), [](const ContentBlockEntry &e) {
|
|
return e.kind != ContentBlockEntry::Kind::Thinking
|
|
&& e.kind != ContentBlockEntry::Kind::RedactedThinking;
|
|
});
|
|
if (!hasNonThinking) {
|
|
manifest.elided << QStringLiteral("thinking-only message dropped");
|
|
continue;
|
|
}
|
|
|
|
WireMessage wm;
|
|
wm.role = roleToWireString(m.role());
|
|
wm.blocks = std::move(blockEntries);
|
|
wireHistory.append(std::move(wm));
|
|
}
|
|
|
|
QVector<ContentBlockEntry> pinnedEntries;
|
|
for (const auto &p : pinned) {
|
|
if (p.text.isEmpty())
|
|
continue;
|
|
pinnedEntries.append(makeTextEntry(p.text));
|
|
++manifest.pinnedBlocks;
|
|
manifest.pinnedChars += p.text.size();
|
|
}
|
|
if (!pinnedEntries.isEmpty()) {
|
|
int anchorIndex = -1;
|
|
int toolCarrierIndex = -1;
|
|
for (int i = wireHistory.size() - 1; i >= 0; --i) {
|
|
if (wireHistory[i].role != QLatin1String("user"))
|
|
continue;
|
|
const auto &blocks = wireHistory[i].blocks;
|
|
const bool carriesToolResults = !blocks.isEmpty()
|
|
&& blocks.first().kind
|
|
== ContentBlockEntry::Kind::ToolResult;
|
|
if (!carriesToolResults) {
|
|
anchorIndex = i;
|
|
break;
|
|
}
|
|
if (toolCarrierIndex < 0)
|
|
toolCarrierIndex = i;
|
|
}
|
|
if (anchorIndex < 0)
|
|
anchorIndex = toolCarrierIndex;
|
|
if (anchorIndex < 0) {
|
|
WireMessage wm;
|
|
wm.role = QStringLiteral("user");
|
|
wireHistory.append(std::move(wm));
|
|
anchorIndex = wireHistory.size() - 1;
|
|
}
|
|
auto &target = wireHistory[anchorIndex].blocks;
|
|
qsizetype insertPos = 0;
|
|
while (insertPos < target.size()
|
|
&& target[insertPos].kind == ContentBlockEntry::Kind::ToolResult) {
|
|
++insertPos;
|
|
}
|
|
for (qsizetype i = 0; i < pinnedEntries.size(); ++i)
|
|
target.insert(insertPos + i, pinnedEntries[i]);
|
|
}
|
|
|
|
manifest.wireMessages = wireHistory.size();
|
|
if (!wireHistory.isEmpty())
|
|
ctx.history = std::move(wireHistory);
|
|
|
|
qCDebug(ctxLog).noquote() << manifest.summary();
|
|
|
|
if (outManifest)
|
|
*outManifest = std::move(manifest);
|
|
|
|
return ctx;
|
|
}
|
|
|
|
} // namespace QodeAssist::ContextAssembler
|