mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-01 10:39:14 -04:00
refactor: Move to agent architecture
This commit is contained in:
@@ -3,12 +3,15 @@ add_library(Session STATIC
|
||||
MessageSerializer.hpp MessageSerializer.cpp
|
||||
PluginBlocks.hpp
|
||||
LLMRequest.hpp
|
||||
ErrorInfo.hpp
|
||||
ResponseEvent.hpp
|
||||
ContextAssembler.hpp ContextAssembler.cpp
|
||||
ConversationHistory.hpp ConversationHistory.cpp
|
||||
ResponseRouter.hpp ResponseRouter.cpp
|
||||
Session.hpp Session.cpp
|
||||
SessionManager.hpp SessionManager.cpp
|
||||
SystemPromptBuilder.hpp SystemPromptBuilder.cpp
|
||||
ToolContributorRegistry.hpp
|
||||
)
|
||||
|
||||
target_link_libraries(Session
|
||||
|
||||
307
sources/Session/ContextAssembler.cpp
Normal file
307
sources/Session/ContextAssembler.cpp
Normal file
@@ -0,0 +1,307 @@
|
||||
// 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
|
||||
58
sources/Session/ContextAssembler.hpp
Normal file
58
sources/Session/ContextAssembler.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ContextData.hpp>
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class Message;
|
||||
|
||||
namespace ContextAssembler {
|
||||
|
||||
using ContentLoader = std::function<QString(const QString &storedPath)>;
|
||||
|
||||
struct PinnedBlock
|
||||
{
|
||||
QString id;
|
||||
QString text;
|
||||
};
|
||||
|
||||
struct Manifest
|
||||
{
|
||||
qsizetype systemChars = 0;
|
||||
int historyMessages = 0;
|
||||
int wireMessages = 0;
|
||||
qsizetype textChars = 0;
|
||||
qsizetype thinkingChars = 0;
|
||||
qsizetype toolChars = 0;
|
||||
qsizetype pinnedChars = 0;
|
||||
int imageBlocks = 0;
|
||||
int toolUseBlocks = 0;
|
||||
int toolResultBlocks = 0;
|
||||
int pinnedBlocks = 0;
|
||||
int unsupportedBlocks = 0;
|
||||
bool hasCompletionContext = false;
|
||||
QStringList elided;
|
||||
|
||||
QString summary() const;
|
||||
};
|
||||
|
||||
Templates::ContextData assemble(
|
||||
const std::vector<Message> &history,
|
||||
const QString &systemPrompt,
|
||||
const ContentLoader &loader,
|
||||
const QVector<PinnedBlock> &pinned = {},
|
||||
Manifest *outManifest = nullptr);
|
||||
|
||||
} // namespace ContextAssembler
|
||||
} // namespace QodeAssist
|
||||
61
sources/Session/ErrorInfo.hpp
Normal file
61
sources/Session/ErrorInfo.hpp
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
enum class ErrorCategory {
|
||||
Config,
|
||||
Auth,
|
||||
Network,
|
||||
Provider,
|
||||
Validation,
|
||||
Tool,
|
||||
};
|
||||
|
||||
struct ErrorInfo
|
||||
{
|
||||
ErrorCategory category = ErrorCategory::Provider;
|
||||
QString message;
|
||||
QString providerDetail;
|
||||
|
||||
bool isEmpty() const noexcept { return message.isEmpty(); }
|
||||
};
|
||||
|
||||
[[nodiscard]] inline ErrorInfo makeError(
|
||||
ErrorCategory category, QString message, QString providerDetail = QString())
|
||||
{
|
||||
return ErrorInfo{category, std::move(message), std::move(providerDetail)};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline ErrorCategory categorizeProviderError(const QString &raw)
|
||||
{
|
||||
const QString text = raw.toLower();
|
||||
|
||||
const auto contains = [&text](const char *needle) {
|
||||
return text.contains(QLatin1String(needle));
|
||||
};
|
||||
|
||||
if (contains("401") || contains("403") || contains("unauthorized")
|
||||
|| contains("forbidden") || contains("api key") || contains("apikey")
|
||||
|| contains("authentication") || contains("invalid token"))
|
||||
return ErrorCategory::Auth;
|
||||
|
||||
if (contains("timeout") || contains("timed out") || contains("connection")
|
||||
|| contains("could not resolve") || contains("unreachable")
|
||||
|| contains("network") || contains("ssl") || contains("refused"))
|
||||
return ErrorCategory::Network;
|
||||
|
||||
return ErrorCategory::Provider;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
Q_DECLARE_METATYPE(QodeAssist::ErrorInfo)
|
||||
@@ -26,6 +26,7 @@ constexpr auto kKindToolResult = "tool_result";
|
||||
constexpr auto kKindStoredImage = "stored_image";
|
||||
constexpr auto kKindStoredAttachment = "stored_attachment";
|
||||
constexpr auto kKindFileEdit = "file_edit";
|
||||
constexpr auto kKindSkillInvocation = "skill_invocation";
|
||||
|
||||
QString roleToString(Message::Role role)
|
||||
{
|
||||
@@ -92,6 +93,10 @@ QJsonObject blockToJson(const LLMQore::ContentBlock &block)
|
||||
obj["type"] = kKindStoredAttachment;
|
||||
obj["fileName"] = sa->fileName();
|
||||
obj["storedPath"] = sa->storedPath();
|
||||
} else if (auto *sk = dynamic_cast<const SkillInvocationContent *>(&block)) {
|
||||
obj["type"] = kKindSkillInvocation;
|
||||
obj["skillName"] = sk->skillName();
|
||||
obj["body"] = sk->body();
|
||||
} else if (auto *fe = dynamic_cast<const FileEditContent *>(&block)) {
|
||||
obj["type"] = kKindFileEdit;
|
||||
obj["editId"] = fe->editId();
|
||||
@@ -147,6 +152,10 @@ std::unique_ptr<LLMQore::ContentBlock> blockFromJson(const QJsonObject &obj)
|
||||
return std::make_unique<StoredAttachmentContent>(
|
||||
obj.value("fileName").toString(), obj.value("storedPath").toString());
|
||||
}
|
||||
if (type == kKindSkillInvocation) {
|
||||
return std::make_unique<SkillInvocationContent>(
|
||||
obj.value("skillName").toString(), obj.value("body").toString());
|
||||
}
|
||||
if (type == kKindFileEdit) {
|
||||
return std::make_unique<FileEditContent>(
|
||||
obj.value("editId").toString(),
|
||||
|
||||
@@ -31,6 +31,24 @@ private:
|
||||
QString m_mediaType;
|
||||
};
|
||||
|
||||
class CompletionContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
CompletionContent(QString prefix, QString suffix)
|
||||
: m_prefix(std::move(prefix))
|
||||
, m_suffix(std::move(suffix))
|
||||
{}
|
||||
|
||||
QString type() const override { return QStringLiteral("completion"); }
|
||||
|
||||
QString prefix() const { return m_prefix; }
|
||||
QString suffix() const { return m_suffix; }
|
||||
|
||||
private:
|
||||
QString m_prefix;
|
||||
QString m_suffix;
|
||||
};
|
||||
|
||||
class StoredAttachmentContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
@@ -49,6 +67,24 @@ private:
|
||||
QString m_storedPath;
|
||||
};
|
||||
|
||||
class SkillInvocationContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
SkillInvocationContent(QString skillName, QString body)
|
||||
: m_skillName(std::move(skillName))
|
||||
, m_body(std::move(body))
|
||||
{}
|
||||
|
||||
QString type() const override { return QStringLiteral("skill_invocation"); }
|
||||
|
||||
QString skillName() const { return m_skillName; }
|
||||
QString body() const { return m_body; }
|
||||
|
||||
private:
|
||||
QString m_skillName;
|
||||
QString m_body;
|
||||
};
|
||||
|
||||
class FileEditContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
@@ -79,7 +115,6 @@ public:
|
||||
QString statusMessage() const { return m_statusMessage; }
|
||||
|
||||
void setStatus(Status status) { m_status = status; }
|
||||
void setStatusMessage(QString msg) { m_statusMessage = std::move(msg); }
|
||||
|
||||
static QString statusToString(Status s)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "ErrorInfo.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
namespace ResponseEvents {
|
||||
@@ -45,6 +47,7 @@ struct ToolCallEnd
|
||||
struct ToolResult
|
||||
{
|
||||
QString toolUseId;
|
||||
QString name;
|
||||
QString text;
|
||||
bool isError = false;
|
||||
};
|
||||
@@ -53,11 +56,14 @@ struct Usage
|
||||
{
|
||||
int inputTokens = 0;
|
||||
int outputTokens = 0;
|
||||
int cachedTokens = 0;
|
||||
int reasoningTokens = 0;
|
||||
};
|
||||
|
||||
struct Error
|
||||
{
|
||||
QString message;
|
||||
ErrorCategory category = ErrorCategory::Provider;
|
||||
};
|
||||
|
||||
struct MessageStop
|
||||
@@ -115,34 +121,33 @@ public:
|
||||
return {Kind::ToolCallStart, ResponseEvents::ToolCallStart{std::move(id), std::move(name)}};
|
||||
}
|
||||
|
||||
static ResponseEvent toolCallArgsDelta(QString id, QString jsonFragment)
|
||||
{
|
||||
return {
|
||||
Kind::ToolCallArgsDelta,
|
||||
ResponseEvents::ToolCallArgsDelta{std::move(id), std::move(jsonFragment)}};
|
||||
}
|
||||
|
||||
static ResponseEvent toolCallEnd(QString id, QJsonObject finalArgs)
|
||||
{
|
||||
return {
|
||||
Kind::ToolCallEnd, ResponseEvents::ToolCallEnd{std::move(id), std::move(finalArgs)}};
|
||||
}
|
||||
|
||||
static ResponseEvent toolResult(QString toolUseId, QString text, bool isError = false)
|
||||
static ResponseEvent toolResult(
|
||||
QString toolUseId, QString name, QString text, bool isError = false)
|
||||
{
|
||||
return {
|
||||
Kind::ToolResult,
|
||||
ResponseEvents::ToolResult{std::move(toolUseId), std::move(text), isError}};
|
||||
ResponseEvents::ToolResult{
|
||||
std::move(toolUseId), std::move(name), std::move(text), isError}};
|
||||
}
|
||||
|
||||
static ResponseEvent usage(int inputTokens, int outputTokens)
|
||||
static ResponseEvent usage(
|
||||
int inputTokens, int outputTokens, int cachedTokens = 0, int reasoningTokens = 0)
|
||||
{
|
||||
return {Kind::Usage, ResponseEvents::Usage{inputTokens, outputTokens}};
|
||||
return {
|
||||
Kind::Usage,
|
||||
ResponseEvents::Usage{inputTokens, outputTokens, cachedTokens, reasoningTokens}};
|
||||
}
|
||||
|
||||
static ResponseEvent error(QString message)
|
||||
static ResponseEvent error(
|
||||
QString message, ErrorCategory category = ErrorCategory::Provider)
|
||||
{
|
||||
return {Kind::Error, ResponseEvents::Error{std::move(message)}};
|
||||
return {Kind::Error, ResponseEvents::Error{std::move(message), category}};
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -79,7 +79,7 @@ void ResponseRouter::ensureAssistantOpen()
|
||||
if (m_assistantOpen && !m_inToolResults)
|
||||
return;
|
||||
if (m_history)
|
||||
m_history->append(Message(Message::Role::Assistant));
|
||||
m_history->append(Message(Message::Role::Assistant, m_activeId));
|
||||
emit event(ResponseEvent::messageStart());
|
||||
m_assistantOpen = true;
|
||||
m_inToolResults = false;
|
||||
@@ -107,15 +107,19 @@ void ResponseRouter::onThinking(
|
||||
}
|
||||
|
||||
void ResponseRouter::onToolStarted(
|
||||
const LLMQore::RequestID &id, const QString &toolId, const QString &toolName)
|
||||
const LLMQore::RequestID &id,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &arguments)
|
||||
{
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
ensureAssistantOpen();
|
||||
if (m_history)
|
||||
m_history->appendBlockToLast(
|
||||
std::make_unique<LLMQore::ToolUseContent>(toolId, toolName));
|
||||
std::make_unique<LLMQore::ToolUseContent>(toolId, toolName, arguments));
|
||||
emit event(ResponseEvent::toolCallStart(toolId, toolName));
|
||||
emit event(ResponseEvent::toolCallEnd(toolId, arguments));
|
||||
}
|
||||
|
||||
void ResponseRouter::onToolResultReady(
|
||||
@@ -124,7 +128,6 @@ void ResponseRouter::onToolResultReady(
|
||||
const QString &toolName,
|
||||
const QString &result)
|
||||
{
|
||||
Q_UNUSED(toolName);
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
|
||||
@@ -141,7 +144,7 @@ void ResponseRouter::onToolResultReady(
|
||||
|
||||
m_assistantOpen = false;
|
||||
m_inToolResults = true;
|
||||
emit event(ResponseEvent::toolResult(toolId, result, /*isError=*/false));
|
||||
emit event(ResponseEvent::toolResult(toolId, toolName, result, /*isError=*/false));
|
||||
}
|
||||
|
||||
void ResponseRouter::onFinalized(
|
||||
@@ -149,6 +152,13 @@ void ResponseRouter::onFinalized(
|
||||
{
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
if (info.usage) {
|
||||
emit event(ResponseEvent::usage(
|
||||
info.usage->promptTokens,
|
||||
info.usage->completionTokens,
|
||||
info.usage->cachedPromptTokens,
|
||||
info.usage->reasoningTokens));
|
||||
}
|
||||
emit event(ResponseEvent::messageStop(info.stopReason));
|
||||
endRequest();
|
||||
}
|
||||
@@ -157,7 +167,7 @@ void ResponseRouter::onFailed(const LLMQore::RequestID &id, const QString &err)
|
||||
{
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
emit event(ResponseEvent::error(err));
|
||||
emit event(ResponseEvent::error(err, categorizeProviderError(err)));
|
||||
endRequest();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
@@ -31,7 +32,6 @@ public:
|
||||
void endRequest();
|
||||
|
||||
bool isActive() const noexcept { return !m_activeId.isEmpty(); }
|
||||
LLMQore::RequestID activeRequestId() const noexcept { return m_activeId; }
|
||||
|
||||
signals:
|
||||
void event(const QodeAssist::ResponseEvent &ev);
|
||||
@@ -41,7 +41,10 @@ private slots:
|
||||
void onThinking(
|
||||
const LLMQore::RequestID &id, const QString &thinking, const QString &signature);
|
||||
void onToolStarted(
|
||||
const LLMQore::RequestID &id, const QString &toolId, const QString &toolName);
|
||||
const LLMQore::RequestID &id,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &arguments);
|
||||
void onToolResultReady(
|
||||
const LLMQore::RequestID &id,
|
||||
const QString &toolId,
|
||||
|
||||
@@ -10,13 +10,10 @@
|
||||
#include <QJsonObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Agent.hpp"
|
||||
#include "AgentConfig.hpp"
|
||||
#include "ContextData.hpp"
|
||||
#include "Message.hpp"
|
||||
#include "PluginBlocks.hpp"
|
||||
#include "PromptTemplate.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "ResponseRouter.hpp"
|
||||
@@ -26,26 +23,10 @@ namespace QodeAssist {
|
||||
|
||||
namespace {
|
||||
|
||||
QString roleToLegacyString(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");
|
||||
}
|
||||
[[maybe_unused]] const int kErrorInfoMetaTypeId = qRegisterMetaType<QodeAssist::ErrorInfo>();
|
||||
|
||||
} // namespace
|
||||
|
||||
Session::Session(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_history(new ConversationHistory(this))
|
||||
, m_systemPrompt(new SystemPromptBuilder(this))
|
||||
{
|
||||
m_invalidReason = QStringLiteral("Session: no agent attached");
|
||||
}
|
||||
|
||||
Session::Session(Agent *agent, QObject *parent)
|
||||
: Session(agent, /*externalHistory=*/nullptr, parent)
|
||||
{}
|
||||
@@ -81,14 +62,12 @@ Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *pa
|
||||
|
||||
m_router = new ResponseRouter(client, m_history, this);
|
||||
connect(m_router, &ResponseRouter::event, this, &Session::onRouterEvent);
|
||||
|
||||
m_systemPrompt->setLayer(QStringLiteral("agent.role"), m_agent->config().role);
|
||||
}
|
||||
|
||||
Session::~Session()
|
||||
{
|
||||
if (isInFlight())
|
||||
cancel();
|
||||
teardownInFlight();
|
||||
}
|
||||
|
||||
bool Session::isValid() const noexcept
|
||||
@@ -106,6 +85,17 @@ bool Session::isInFlight() const noexcept
|
||||
return !m_inFlight.isEmpty();
|
||||
}
|
||||
|
||||
const ErrorInfo &Session::lastError() const noexcept
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
LLMQore::BaseClient *Session::client() const noexcept
|
||||
{
|
||||
auto *provider = m_agent ? m_agent->provider() : nullptr;
|
||||
return provider ? provider->client() : nullptr;
|
||||
}
|
||||
|
||||
void Session::setContentLoader(ContentLoader loader)
|
||||
{
|
||||
m_contentLoader = std::move(loader);
|
||||
@@ -116,36 +106,36 @@ void Session::setContextBindings(Templates::ContextRenderer::Bindings bindings)
|
||||
m_contextBindings = std::move(bindings);
|
||||
}
|
||||
|
||||
QString Session::renderAgentContext() const
|
||||
void Session::pinContext(const QString &id, PinnedProvider provider)
|
||||
{
|
||||
if (!m_agent)
|
||||
return {};
|
||||
const auto &cfg = m_agent->config();
|
||||
if (cfg.context.isEmpty())
|
||||
return {};
|
||||
QString err;
|
||||
QString rendered = Templates::ContextRenderer::render(cfg.context, m_contextBindings, &err);
|
||||
if (!err.isEmpty())
|
||||
qWarning("[QodeAssist] agent.context render failed: %s", qUtf8Printable(err));
|
||||
return rendered;
|
||||
if (!provider) {
|
||||
unpinContext(id);
|
||||
return;
|
||||
}
|
||||
for (auto &entry : m_pinnedProviders) {
|
||||
if (entry.first == id) {
|
||||
entry.second = std::move(provider);
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_pinnedProviders.emplace_back(id, std::move(provider));
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::sendText(const QString &text)
|
||||
void Session::unpinContext(const QString &id)
|
||||
{
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
if (!text.isEmpty())
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(text));
|
||||
return send(std::move(blocks));
|
||||
std::erase_if(m_pinnedProviders, [&id](const auto &entry) { return entry.first == id; });
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::send(
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
|
||||
std::optional<bool> toolsOverride)
|
||||
LLMQore::RequestID Session::send(std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks)
|
||||
{
|
||||
if (!isValid() || userBlocks.empty())
|
||||
if (!isValid()) {
|
||||
m_lastError = makeError(ErrorCategory::Config, invalidReason());
|
||||
return {};
|
||||
if (!m_history)
|
||||
}
|
||||
if (userBlocks.empty() || !m_history) {
|
||||
m_lastError = makeError(ErrorCategory::Validation, QStringLiteral("Session: nothing to send"));
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isInFlight())
|
||||
cancel();
|
||||
@@ -155,10 +145,32 @@ LLMQore::RequestID Session::send(
|
||||
msg.appendBlock(std::move(b));
|
||||
m_history->append(std::move(msg));
|
||||
|
||||
return dispatch(toolsOverride);
|
||||
return dispatch();
|
||||
}
|
||||
|
||||
QVector<ContextAssembler::PinnedBlock> Session::materializePinned() const
|
||||
{
|
||||
QVector<ContextAssembler::PinnedBlock> pinned;
|
||||
pinned.reserve(static_cast<qsizetype>(m_pinnedProviders.size()));
|
||||
for (const auto &entry : m_pinnedProviders) {
|
||||
const QString text = entry.second();
|
||||
if (!text.isEmpty())
|
||||
pinned.append({entry.first, text});
|
||||
}
|
||||
return pinned;
|
||||
}
|
||||
|
||||
void Session::cancel()
|
||||
{
|
||||
if (m_inFlight.isEmpty())
|
||||
return;
|
||||
|
||||
const auto id = m_inFlight;
|
||||
teardownInFlight();
|
||||
emit cancelled(id);
|
||||
}
|
||||
|
||||
void Session::teardownInFlight()
|
||||
{
|
||||
if (m_inFlight.isEmpty())
|
||||
return;
|
||||
@@ -169,30 +181,59 @@ void Session::cancel()
|
||||
m_router->endRequest();
|
||||
if (m_agent && m_agent->provider())
|
||||
m_agent->provider()->cancelRequest(id);
|
||||
emit failed(id, QStringLiteral("Cancelled by user"));
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
|
||||
LLMQore::RequestID Session::dispatch()
|
||||
{
|
||||
if (!isValid())
|
||||
return {};
|
||||
if (isInFlight())
|
||||
cancel();
|
||||
|
||||
if (m_history)
|
||||
m_history->clear();
|
||||
|
||||
auto *provider = m_agent->provider();
|
||||
auto *tmpl = m_agent->promptTemplate();
|
||||
const auto &cfg = m_agent->config();
|
||||
|
||||
QJsonObject payload{{QStringLiteral("model"), cfg.model}};
|
||||
if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false))
|
||||
return {};
|
||||
if (cfg.systemPrompt.isEmpty()) {
|
||||
m_systemPrompt->clearLayer(QStringLiteral("agent.system"));
|
||||
} else {
|
||||
QString renderErr;
|
||||
const QString renderedContext = Templates::ContextRenderer::render(
|
||||
cfg.systemPrompt, m_contextBindings, &renderErr);
|
||||
if (!renderErr.isEmpty()) {
|
||||
m_lastError = makeError(
|
||||
ErrorCategory::Validation,
|
||||
QStringLiteral("Agent '%1' system_prompt render failed: %2")
|
||||
.arg(cfg.name, renderErr));
|
||||
qWarning("[QodeAssist] %s", qUtf8Printable(m_lastError.message));
|
||||
return {};
|
||||
}
|
||||
if (renderedContext.isEmpty())
|
||||
m_systemPrompt->clearLayer(QStringLiteral("agent.system"));
|
||||
else
|
||||
m_systemPrompt->setLayer(
|
||||
QStringLiteral("agent.system"), renderedContext, SystemPromptBuilder::kAgentPriority);
|
||||
}
|
||||
|
||||
const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint);
|
||||
if (id.isEmpty())
|
||||
return dispatchContext(assembleContext(), cfg.enableTools);
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::dispatchContext(const Templates::ContextData &ctx, bool tools)
|
||||
{
|
||||
m_lastError = {};
|
||||
|
||||
auto *provider = m_agent->provider();
|
||||
const auto &cfg = m_agent->config();
|
||||
|
||||
QString prepareErr;
|
||||
const QJsonObject payload = buildPayload(ctx, tools, &prepareErr);
|
||||
if (payload.isEmpty()) {
|
||||
m_lastError = makeError(ErrorCategory::Validation, prepareErr, prepareErr);
|
||||
return {};
|
||||
}
|
||||
|
||||
QString endpoint = cfg.endpoint;
|
||||
endpoint.replace(QStringLiteral("${MODEL}"), cfg.model);
|
||||
const auto id = provider->sendRequest(QUrl(provider->url()), payload, endpoint);
|
||||
if (id.isEmpty()) {
|
||||
m_lastError = makeError(
|
||||
ErrorCategory::Provider,
|
||||
QStringLiteral("Provider '%1' failed to start the request").arg(provider->name()));
|
||||
return {};
|
||||
}
|
||||
|
||||
m_inFlight = id;
|
||||
if (m_router)
|
||||
@@ -201,165 +242,29 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
|
||||
return id;
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::dispatch(std::optional<bool> toolsOverride)
|
||||
QJsonObject Session::buildPayload(
|
||||
const Templates::ContextData &ctx, bool tools, QString *errOut) const
|
||||
{
|
||||
auto *provider = m_agent->provider();
|
||||
auto *tmpl = m_agent->promptTemplate();
|
||||
const auto &cfg = m_agent->config();
|
||||
|
||||
const QString renderedContext = renderAgentContext();
|
||||
if (renderedContext.isEmpty())
|
||||
m_systemPrompt->clearLayer(QStringLiteral("agent.context"));
|
||||
else
|
||||
m_systemPrompt->setLayer(QStringLiteral("agent.context"), renderedContext);
|
||||
|
||||
Templates::ContextData ctx = toLegacyContext();
|
||||
QJsonObject payload{{QStringLiteral("model"), cfg.model}};
|
||||
|
||||
const bool tools = toolsOverride.value_or(cfg.enableTools);
|
||||
if (!provider->prepareRequest(payload, tmpl, ctx, tools, cfg.enableThinking))
|
||||
QString prepareErr;
|
||||
if (!provider->prepareRequest(payload, tmpl, ctx, tools, &prepareErr)) {
|
||||
if (errOut)
|
||||
*errOut = prepareErr;
|
||||
return {};
|
||||
|
||||
const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint);
|
||||
if (id.isEmpty())
|
||||
return {};
|
||||
|
||||
m_inFlight = id;
|
||||
if (m_router)
|
||||
m_router->beginRequest(id);
|
||||
emit started(id);
|
||||
return id;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
Templates::ContextData Session::toLegacyContext() const
|
||||
Templates::ContextData Session::assembleContext() const
|
||||
{
|
||||
if (!m_history)
|
||||
return {};
|
||||
return buildLegacyContext(m_history->messages(), m_systemPrompt->compose(), m_contentLoader);
|
||||
}
|
||||
|
||||
Templates::ContextData Session::buildLegacyContext(
|
||||
const std::vector<Message> &history,
|
||||
const QString &systemPrompt,
|
||||
const ContentLoader &loader)
|
||||
{
|
||||
using Templates::ContentBlockEntry;
|
||||
using Templates::ContextData;
|
||||
using LegacyMessage = Templates::Message;
|
||||
|
||||
ContextData ctx;
|
||||
if (!systemPrompt.isEmpty())
|
||||
ctx.systemPrompt = systemPrompt;
|
||||
|
||||
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<LegacyMessage> hist;
|
||||
|
||||
for (const auto &m : history) {
|
||||
QVector<ContentBlockEntry> blockEntries;
|
||||
|
||||
for (const auto &blockPtr : m.blocks()) {
|
||||
auto *block = blockPtr.get();
|
||||
if (!block)
|
||||
continue;
|
||||
|
||||
if (auto *t = dynamic_cast<LLMQore::TextContent *>(block)) {
|
||||
ContentBlockEntry e;
|
||||
e.kind = ContentBlockEntry::Kind::Text;
|
||||
e.text = t->text();
|
||||
blockEntries.append(std::move(e));
|
||||
} 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));
|
||||
} else if (auto *si = dynamic_cast<StoredImageContent *>(block)) {
|
||||
if (!loader)
|
||||
continue;
|
||||
const QString base64 = loader(si->storedPath());
|
||||
if (base64.isEmpty())
|
||||
continue;
|
||||
ContentBlockEntry e;
|
||||
e.kind = ContentBlockEntry::Kind::Image;
|
||||
e.imageData = base64;
|
||||
e.mediaType = si->mediaType();
|
||||
e.isImageUrl = false;
|
||||
blockEntries.append(std::move(e));
|
||||
} else if (auto *sa = dynamic_cast<StoredAttachmentContent *>(block)) {
|
||||
if (!loader)
|
||||
continue;
|
||||
const QString text = loader(sa->storedPath());
|
||||
if (text.isEmpty())
|
||||
continue;
|
||||
ContentBlockEntry e;
|
||||
e.kind = ContentBlockEntry::Kind::Text;
|
||||
e.text = QStringLiteral("File: %1\n```\n%2\n```")
|
||||
.arg(sa->fileName(), text);
|
||||
blockEntries.append(std::move(e));
|
||||
} else if (auto *th = dynamic_cast<LLMQore::ThinkingContent *>(block)) {
|
||||
ContentBlockEntry e;
|
||||
e.kind = ContentBlockEntry::Kind::Thinking;
|
||||
e.thinking = th->thinking();
|
||||
e.signature = th->signature();
|
||||
blockEntries.append(std::move(e));
|
||||
} else if (auto *rth = dynamic_cast<LLMQore::RedactedThinkingContent *>(block)) {
|
||||
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()))
|
||||
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));
|
||||
} else if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block)) {
|
||||
if (!declaredToolUseIds.contains(tr->toolUseId()))
|
||||
continue;
|
||||
ContentBlockEntry e;
|
||||
e.kind = ContentBlockEntry::Kind::ToolResult;
|
||||
e.toolUseId = tr->toolUseId();
|
||||
e.result = tr->result();
|
||||
blockEntries.append(std::move(e));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
continue;
|
||||
|
||||
LegacyMessage lm;
|
||||
lm.role = roleToLegacyString(m.role());
|
||||
lm.blocks = std::move(blockEntries);
|
||||
hist.append(std::move(lm));
|
||||
}
|
||||
|
||||
if (!hist.isEmpty())
|
||||
ctx.history = std::move(hist);
|
||||
|
||||
return ctx;
|
||||
return ContextAssembler::assemble(
|
||||
m_history->messages(), m_systemPrompt->compose(), m_contentLoader, materializePinned());
|
||||
}
|
||||
|
||||
void Session::onRouterEvent(const ResponseEvent &ev)
|
||||
@@ -378,9 +283,11 @@ void Session::onRouterEvent(const ResponseEvent &ev)
|
||||
} else if (ev.kind() == ResponseEvent::Kind::Error) {
|
||||
const auto *err = ev.as<ResponseEvents::Error>();
|
||||
const QString msg = err ? err->message : QStringLiteral("unknown error");
|
||||
const ErrorCategory category = err ? err->category : ErrorCategory::Provider;
|
||||
m_lastError = makeError(category, msg, msg);
|
||||
const auto id = m_inFlight;
|
||||
m_inFlight.clear();
|
||||
emit failed(id, msg);
|
||||
emit failed(id, m_lastError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ContextAssembler.hpp"
|
||||
#include "ConversationHistory.hpp"
|
||||
#include "ErrorInfo.hpp"
|
||||
#include "ResponseEvent.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
@@ -33,8 +34,6 @@ class Session : public QObject
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(Session)
|
||||
public:
|
||||
explicit Session(QObject *parent = nullptr);
|
||||
|
||||
Session(
|
||||
Agent *agent,
|
||||
ConversationHistory *externalHistory = nullptr,
|
||||
@@ -47,26 +46,25 @@ public:
|
||||
bool isValid() const noexcept;
|
||||
QString invalidReason() const;
|
||||
bool isInFlight() const noexcept;
|
||||
const ErrorInfo &lastError() const noexcept;
|
||||
|
||||
using ContentLoader = std::function<QString(const QString &storedPath)>;
|
||||
using ContentLoader = ContextAssembler::ContentLoader;
|
||||
void setContentLoader(ContentLoader loader);
|
||||
|
||||
using PinnedProvider = std::function<QString()>;
|
||||
void pinContext(const QString &id, PinnedProvider provider);
|
||||
void unpinContext(const QString &id);
|
||||
|
||||
Agent *agent() noexcept { return m_agent; }
|
||||
ConversationHistory *history() const noexcept { return m_history; }
|
||||
SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; }
|
||||
|
||||
LLMQore::BaseClient *client() const noexcept;
|
||||
|
||||
void setContextBindings(Templates::ContextRenderer::Bindings bindings);
|
||||
|
||||
QString renderAgentContext() const;
|
||||
LLMQore::RequestID send(std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks);
|
||||
|
||||
LLMQore::RequestID send(
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
|
||||
std::optional<bool> toolsOverride = std::nullopt);
|
||||
|
||||
LLMQore::RequestID sendText(const QString &text);
|
||||
|
||||
LLMQore::RequestID sendCompletion(Templates::ContextData ctx);
|
||||
|
||||
void cancel();
|
||||
|
||||
signals:
|
||||
@@ -74,14 +72,19 @@ signals:
|
||||
|
||||
void started(const LLMQore::RequestID &id);
|
||||
void finished(const LLMQore::RequestID &id, const QString &stopReason);
|
||||
void failed(const LLMQore::RequestID &id, const QString &error);
|
||||
void failed(const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error);
|
||||
void cancelled(const LLMQore::RequestID &id);
|
||||
|
||||
private slots:
|
||||
void onRouterEvent(const QodeAssist::ResponseEvent &ev);
|
||||
|
||||
private:
|
||||
LLMQore::RequestID dispatch(std::optional<bool> toolsOverride = std::nullopt);
|
||||
Templates::ContextData toLegacyContext() const;
|
||||
LLMQore::RequestID dispatch();
|
||||
LLMQore::RequestID dispatchContext(const Templates::ContextData &ctx, bool tools);
|
||||
void teardownInFlight();
|
||||
Templates::ContextData assembleContext() const;
|
||||
QVector<ContextAssembler::PinnedBlock> materializePinned() const;
|
||||
QJsonObject buildPayload(const Templates::ContextData &ctx, bool tools, QString *errOut) const;
|
||||
|
||||
Agent *m_agent = nullptr; // child if non-null
|
||||
QPointer<ConversationHistory> m_history; // child if internal, external otherwise
|
||||
@@ -90,17 +93,11 @@ private:
|
||||
|
||||
LLMQore::RequestID m_inFlight;
|
||||
QString m_invalidReason;
|
||||
ErrorInfo m_lastError;
|
||||
|
||||
Templates::ContextRenderer::Bindings m_contextBindings;
|
||||
|
||||
public:
|
||||
static Templates::ContextData buildLegacyContext(
|
||||
const std::vector<Message> &history,
|
||||
const QString &systemPrompt,
|
||||
const ContentLoader &loader = ContentLoader{});
|
||||
|
||||
private:
|
||||
ContentLoader m_contentLoader;
|
||||
std::vector<std::pair<QString, PinnedProvider>> m_pinnedProviders;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
|
||||
#include "SessionManager.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
|
||||
#include "Agent.hpp"
|
||||
#include "AgentFactory.hpp"
|
||||
#include "ConversationHistory.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "SystemPromptBuilder.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
SessionManager::SessionManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
SessionManager::SessionManager(AgentFactory *agentFactory, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_agentFactory(agentFactory)
|
||||
@@ -21,14 +22,6 @@ SessionManager::SessionManager(AgentFactory *agentFactory, QObject *parent)
|
||||
|
||||
SessionManager::~SessionManager() = default;
|
||||
|
||||
Session *SessionManager::createSession()
|
||||
{
|
||||
auto *session = new Session(this);
|
||||
m_sessions.append(session);
|
||||
emit sessionCreated(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
Session *SessionManager::createSession(const QString &agentName, QString *errorOut)
|
||||
{
|
||||
return createSession(agentName, /*externalHistory=*/nullptr, errorOut);
|
||||
@@ -66,6 +59,64 @@ Session *SessionManager::createSession(
|
||||
return session;
|
||||
}
|
||||
|
||||
Session *SessionManager::acquire(const QString &agentName, QString *errorOut)
|
||||
{
|
||||
auto &bucket = m_pool[agentName];
|
||||
while (!bucket.isEmpty()) {
|
||||
QPointer<Session> pooled = bucket.takeLast();
|
||||
if (pooled && pooled->isValid()) {
|
||||
resetSession(pooled);
|
||||
m_sessions.append(pooled);
|
||||
return pooled.data();
|
||||
}
|
||||
if (pooled)
|
||||
pooled->deleteLater();
|
||||
}
|
||||
|
||||
return createSession(agentName, /*externalHistory=*/nullptr, errorOut);
|
||||
}
|
||||
|
||||
void SessionManager::release(Session *session)
|
||||
{
|
||||
if (!session)
|
||||
return;
|
||||
|
||||
const int idx = m_sessions.indexOf(session);
|
||||
if (idx < 0)
|
||||
return;
|
||||
m_sessions.removeAt(idx);
|
||||
|
||||
if (session->isInFlight())
|
||||
session->cancel();
|
||||
|
||||
session->disconnect();
|
||||
resetSession(session);
|
||||
|
||||
const QString agentName
|
||||
= session->agent() ? session->agent()->config().name : QString();
|
||||
QList<QPointer<Session>> &bucket = m_pool[agentName];
|
||||
if (agentName.isEmpty() || bucket.size() >= kMaxPooledPerAgent) {
|
||||
emit sessionRemoved(session);
|
||||
session->deleteLater();
|
||||
} else {
|
||||
bucket.append(session);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionManager::resetSession(Session *session)
|
||||
{
|
||||
if (!session)
|
||||
return;
|
||||
if (auto *history = session->history())
|
||||
history->clear();
|
||||
if (auto *systemPrompt = session->systemPrompt())
|
||||
systemPrompt->clear();
|
||||
if (auto *client = session->client()) {
|
||||
if (auto *tools = client->tools())
|
||||
tools->removeAllTools();
|
||||
}
|
||||
}
|
||||
|
||||
void SessionManager::removeSession(Session *session)
|
||||
{
|
||||
if (!session)
|
||||
@@ -83,24 +134,4 @@ void SessionManager::removeSession(Session *session)
|
||||
session->deleteLater();
|
||||
}
|
||||
|
||||
QList<Session *> SessionManager::sessions() const
|
||||
{
|
||||
QList<Session *> out;
|
||||
out.reserve(m_sessions.size());
|
||||
for (const auto &p : m_sessions) {
|
||||
if (p)
|
||||
out.append(p.data());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void SessionManager::cancelAll()
|
||||
{
|
||||
const auto snapshot = m_sessions;
|
||||
for (const auto &p : snapshot) {
|
||||
if (p && p->isInFlight())
|
||||
p->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
#include "ToolContributorRegistry.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class AgentFactory;
|
||||
@@ -20,14 +23,10 @@ class SessionManager : public QObject
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(SessionManager)
|
||||
public:
|
||||
explicit SessionManager(QObject *parent = nullptr);
|
||||
|
||||
SessionManager(AgentFactory *agentFactory, QObject *parent = nullptr);
|
||||
explicit SessionManager(AgentFactory *agentFactory, QObject *parent = nullptr);
|
||||
|
||||
~SessionManager() override;
|
||||
|
||||
Session *createSession();
|
||||
|
||||
Session *createSession(const QString &agentName, QString *errorOut = nullptr);
|
||||
|
||||
Session *createSession(
|
||||
@@ -35,19 +34,27 @@ public:
|
||||
ConversationHistory *externalHistory,
|
||||
QString *errorOut = nullptr);
|
||||
|
||||
Session *acquire(const QString &agentName, QString *errorOut = nullptr);
|
||||
void release(Session *session);
|
||||
|
||||
void removeSession(Session *session);
|
||||
|
||||
QList<Session *> sessions() const;
|
||||
|
||||
void cancelAll();
|
||||
ToolContributorRegistry &toolContributors() noexcept { return m_toolContributors; }
|
||||
const ToolContributorRegistry &toolContributors() const noexcept { return m_toolContributors; }
|
||||
|
||||
signals:
|
||||
void sessionCreated(Session *session);
|
||||
void sessionRemoved(Session *session);
|
||||
|
||||
private:
|
||||
void resetSession(Session *session);
|
||||
|
||||
static constexpr int kMaxPooledPerAgent = 2;
|
||||
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QList<QPointer<Session>> m_sessions;
|
||||
QHash<QString, QList<QPointer<Session>>> m_pool;
|
||||
ToolContributorRegistry m_toolContributors;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -4,30 +4,34 @@
|
||||
|
||||
#include "SystemPromptBuilder.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
SystemPromptBuilder::SystemPromptBuilder(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void SystemPromptBuilder::setLayer(const QString &name, const QString &text)
|
||||
void SystemPromptBuilder::setLayer(const QString &name, const QString &text, int priority)
|
||||
{
|
||||
for (auto &pair : m_layers) {
|
||||
if (pair.first == name) {
|
||||
if (pair.second == text) return;
|
||||
pair.second = text;
|
||||
for (auto &layer : m_layers) {
|
||||
if (layer.name == name) {
|
||||
if (layer.text == text && layer.priority == priority)
|
||||
return;
|
||||
layer.text = text;
|
||||
layer.priority = priority;
|
||||
emit layersChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_layers.append({name, text});
|
||||
m_layers.append({name, text, priority});
|
||||
emit layersChanged();
|
||||
}
|
||||
|
||||
void SystemPromptBuilder::clearLayer(const QString &name)
|
||||
{
|
||||
for (auto it = m_layers.begin(); it != m_layers.end(); ++it) {
|
||||
if (it->first == name) {
|
||||
if (it->name == name) {
|
||||
m_layers.erase(it);
|
||||
emit layersChanged();
|
||||
return;
|
||||
@@ -44,8 +48,8 @@ void SystemPromptBuilder::clear()
|
||||
|
||||
QString SystemPromptBuilder::layer(const QString &name) const
|
||||
{
|
||||
for (const auto &pair : m_layers) {
|
||||
if (pair.first == name) return pair.second;
|
||||
for (const auto &l : m_layers) {
|
||||
if (l.name == name) return l.text;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -54,17 +58,22 @@ QStringList SystemPromptBuilder::layerNames() const
|
||||
{
|
||||
QStringList out;
|
||||
out.reserve(m_layers.size());
|
||||
for (const auto &pair : m_layers) out.append(pair.first);
|
||||
for (const auto &l : m_layers) out.append(l.name);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString SystemPromptBuilder::compose(const QString &separator) const
|
||||
{
|
||||
QVector<Layer> ordered = m_layers;
|
||||
std::stable_sort(
|
||||
ordered.begin(), ordered.end(),
|
||||
[](const Layer &a, const Layer &b) { return a.priority < b.priority; });
|
||||
|
||||
QStringList parts;
|
||||
parts.reserve(m_layers.size());
|
||||
for (const auto &pair : m_layers) {
|
||||
if (!pair.second.isEmpty())
|
||||
parts.append(pair.second);
|
||||
parts.reserve(ordered.size());
|
||||
for (const auto &l : ordered) {
|
||||
if (!l.text.isEmpty())
|
||||
parts.append(l.text);
|
||||
}
|
||||
return parts.join(separator);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ class SystemPromptBuilder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static constexpr int kAgentPriority = 0;
|
||||
static constexpr int kDefaultPriority = 100;
|
||||
|
||||
explicit SystemPromptBuilder(QObject *parent = nullptr);
|
||||
|
||||
void setLayer(const QString &name, const QString &text);
|
||||
void setLayer(const QString &name, const QString &text, int priority = kDefaultPriority);
|
||||
void clearLayer(const QString &name);
|
||||
void clear();
|
||||
|
||||
@@ -31,7 +34,14 @@ signals:
|
||||
void layersChanged();
|
||||
|
||||
private:
|
||||
QVector<QPair<QString, QString>> m_layers;
|
||||
struct Layer
|
||||
{
|
||||
QString name;
|
||||
QString text;
|
||||
int priority = kDefaultPriority;
|
||||
};
|
||||
|
||||
QVector<Layer> m_layers;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
41
sources/Session/ToolContributorRegistry.hpp
Normal file
41
sources/Session/ToolContributorRegistry.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace LLMQore {
|
||||
class ToolsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ToolContributorRegistry
|
||||
{
|
||||
public:
|
||||
using Contributor = std::function<void(LLMQore::ToolsManager *)>;
|
||||
|
||||
void add(Contributor contributor)
|
||||
{
|
||||
if (contributor)
|
||||
m_contributors.push_back(std::move(contributor));
|
||||
}
|
||||
|
||||
void contribute(LLMQore::ToolsManager *tools) const
|
||||
{
|
||||
if (!tools)
|
||||
return;
|
||||
for (const auto &contributor : m_contributors)
|
||||
contributor(tools);
|
||||
}
|
||||
|
||||
void clear() { m_contributors.clear(); }
|
||||
|
||||
private:
|
||||
std::vector<Contributor> m_contributors;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
Reference in New Issue
Block a user