mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
feat: Add session layout
This commit is contained in:
23
sources/Session/CMakeLists.txt
Normal file
23
sources/Session/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
add_library(Session STATIC
|
||||
Message.hpp Message.cpp
|
||||
MessageSerializer.hpp MessageSerializer.cpp
|
||||
PluginBlocks.hpp
|
||||
LLMRequest.hpp
|
||||
ResponseEvent.hpp
|
||||
ConversationHistory.hpp ConversationHistory.cpp
|
||||
ResponseRouter.hpp ResponseRouter.cpp
|
||||
Session.hpp Session.cpp
|
||||
SessionManager.hpp SessionManager.cpp
|
||||
SystemPromptBuilder.hpp SystemPromptBuilder.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(Session
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
LLMQore
|
||||
Agents
|
||||
)
|
||||
|
||||
target_include_directories(Session PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
82
sources/Session/ConversationHistory.cpp
Normal file
82
sources/Session/ConversationHistory.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ConversationHistory.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ConversationHistory::ConversationHistory(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
ConversationHistory::~ConversationHistory() = default;
|
||||
|
||||
void ConversationHistory::append(Message message)
|
||||
{
|
||||
m_messages.push_back(std::move(message));
|
||||
emit messageAdded(static_cast<int>(m_messages.size()) - 1);
|
||||
}
|
||||
|
||||
void ConversationHistory::appendBlockToLast(std::unique_ptr<LLMQore::ContentBlock> block)
|
||||
{
|
||||
if (m_messages.empty() || !block)
|
||||
return;
|
||||
|
||||
m_messages.back().appendBlock(std::move(block));
|
||||
emit messageUpdated(static_cast<int>(m_messages.size()) - 1);
|
||||
}
|
||||
|
||||
void ConversationHistory::appendTextDeltaToLast(const QString &delta)
|
||||
{
|
||||
if (m_messages.empty() || delta.isEmpty())
|
||||
return;
|
||||
|
||||
auto &last = m_messages.back();
|
||||
if (auto *text = last.lastBlockOfType<LLMQore::TextContent>()) {
|
||||
text->appendText(delta);
|
||||
} else {
|
||||
last.appendBlock(std::make_unique<LLMQore::TextContent>(delta));
|
||||
}
|
||||
emit messageUpdated(static_cast<int>(m_messages.size()) - 1);
|
||||
}
|
||||
|
||||
void ConversationHistory::appendThinkingDeltaToLast(const QString &delta, const QString &signature)
|
||||
{
|
||||
if (m_messages.empty() || (delta.isEmpty() && signature.isEmpty()))
|
||||
return;
|
||||
|
||||
auto &last = m_messages.back();
|
||||
auto *thinking = last.lastBlockOfType<LLMQore::ThinkingContent>();
|
||||
if (!thinking) {
|
||||
auto fresh = std::make_unique<LLMQore::ThinkingContent>(delta, signature);
|
||||
last.appendBlock(std::move(fresh));
|
||||
} else {
|
||||
if (!delta.isEmpty())
|
||||
thinking->appendThinking(delta);
|
||||
if (!signature.isEmpty())
|
||||
thinking->setSignature(signature);
|
||||
}
|
||||
emit messageUpdated(static_cast<int>(m_messages.size()) - 1);
|
||||
}
|
||||
|
||||
void ConversationHistory::clear()
|
||||
{
|
||||
if (m_messages.empty())
|
||||
return;
|
||||
|
||||
m_messages.clear();
|
||||
emit cleared();
|
||||
}
|
||||
|
||||
void ConversationHistory::resetTo(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
index = 0;
|
||||
if (static_cast<size_t>(index) >= m_messages.size())
|
||||
return;
|
||||
|
||||
m_messages.resize(static_cast<size_t>(index));
|
||||
emit reset();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
49
sources/Session/ConversationHistory.hpp
Normal file
49
sources/Session/ConversationHistory.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Message.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ConversationHistory : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(ConversationHistory)
|
||||
public:
|
||||
explicit ConversationHistory(QObject *parent = nullptr);
|
||||
~ConversationHistory() override;
|
||||
|
||||
const std::vector<Message> &messages() const noexcept { return m_messages; }
|
||||
int size() const noexcept { return static_cast<int>(m_messages.size()); }
|
||||
bool isEmpty() const noexcept { return m_messages.empty(); }
|
||||
|
||||
void append(Message message);
|
||||
|
||||
void appendBlockToLast(std::unique_ptr<LLMQore::ContentBlock> block);
|
||||
|
||||
void appendTextDeltaToLast(const QString &delta);
|
||||
void appendThinkingDeltaToLast(const QString &delta, const QString &signature = QString());
|
||||
|
||||
void clear();
|
||||
void resetTo(int index);
|
||||
|
||||
signals:
|
||||
void messageAdded(int index);
|
||||
void messageUpdated(int index);
|
||||
void cleared();
|
||||
void reset();
|
||||
|
||||
private:
|
||||
std::vector<Message> m_messages;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
32
sources/Session/LLMRequest.hpp
Normal file
32
sources/Session/LLMRequest.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "Message.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
struct LLMRequest
|
||||
{
|
||||
QString systemPrompt;
|
||||
std::vector<Message> history;
|
||||
bool toolsEnabled = false;
|
||||
bool thinkingEnabled = false;
|
||||
|
||||
std::optional<QString> fimPrefix;
|
||||
std::optional<QString> fimSuffix;
|
||||
|
||||
LLMRequest() = default;
|
||||
LLMRequest(const LLMRequest &) = delete;
|
||||
LLMRequest &operator=(const LLMRequest &) = delete;
|
||||
LLMRequest(LLMRequest &&) noexcept = default;
|
||||
LLMRequest &operator=(LLMRequest &&) noexcept = default;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
30
sources/Session/Message.cpp
Normal file
30
sources/Session/Message.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "Message.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QString Message::text() const
|
||||
{
|
||||
QString out;
|
||||
for (const auto &block : m_blocks) {
|
||||
if (auto *t = dynamic_cast<LLMQore::TextContent *>(block.get())) {
|
||||
if (!out.isEmpty())
|
||||
out += QStringLiteral("\n\n");
|
||||
out += t->text();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Message::hasToolUse() const
|
||||
{
|
||||
for (const auto &block : m_blocks) {
|
||||
if (dynamic_cast<LLMQore::ToolUseContent *>(block.get()))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
73
sources/Session/Message.hpp
Normal file
73
sources/Session/Message.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class Message
|
||||
{
|
||||
public:
|
||||
enum class Role { System, User, Assistant };
|
||||
|
||||
Message() = default;
|
||||
explicit Message(Role role, QString id = QString())
|
||||
: m_role(role)
|
||||
, m_id(std::move(id))
|
||||
{}
|
||||
|
||||
Message(const Message &) = delete;
|
||||
Message &operator=(const Message &) = delete;
|
||||
Message(Message &&) noexcept = default;
|
||||
Message &operator=(Message &&) noexcept = default;
|
||||
~Message() = default;
|
||||
|
||||
Role role() const noexcept { return m_role; }
|
||||
const QString &id() const noexcept { return m_id; }
|
||||
void setId(QString id) { m_id = std::move(id); }
|
||||
|
||||
const std::vector<std::unique_ptr<LLMQore::ContentBlock>> &blocks() const noexcept
|
||||
{
|
||||
return m_blocks;
|
||||
}
|
||||
|
||||
void appendBlock(std::unique_ptr<LLMQore::ContentBlock> block)
|
||||
{
|
||||
if (block)
|
||||
m_blocks.push_back(std::move(block));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *lastBlockOfType()
|
||||
{
|
||||
for (auto it = m_blocks.rbegin(); it != m_blocks.rend(); ++it) {
|
||||
if (auto *p = dynamic_cast<T *>(it->get()))
|
||||
return p;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
const T *lastBlockOfType() const
|
||||
{
|
||||
return const_cast<Message *>(this)->lastBlockOfType<T>();
|
||||
}
|
||||
|
||||
QString text() const;
|
||||
|
||||
bool hasToolUse() const;
|
||||
|
||||
private:
|
||||
Role m_role = Role::User;
|
||||
QString m_id;
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> m_blocks;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
212
sources/Session/MessageSerializer.cpp
Normal file
212
sources/Session/MessageSerializer.cpp
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "MessageSerializer.hpp"
|
||||
|
||||
#include "PluginBlocks.hpp"
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kKindText = "text";
|
||||
constexpr auto kKindThinking = "thinking";
|
||||
constexpr auto kKindRedactedThinking = "redacted_thinking";
|
||||
constexpr auto kKindImage = "image";
|
||||
constexpr auto kKindToolUse = "tool_use";
|
||||
constexpr auto kKindToolResult = "tool_result";
|
||||
constexpr auto kKindStoredImage = "stored_image";
|
||||
constexpr auto kKindStoredAttachment = "stored_attachment";
|
||||
constexpr auto kKindFileEdit = "file_edit";
|
||||
|
||||
QString roleToString(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 roleFromString(const QString &str, Message::Role *out)
|
||||
{
|
||||
if (str == QLatin1String("system")) {
|
||||
*out = Message::Role::System;
|
||||
return true;
|
||||
}
|
||||
if (str == QLatin1String("user")) {
|
||||
*out = Message::Role::User;
|
||||
return true;
|
||||
}
|
||||
if (str == QLatin1String("assistant")) {
|
||||
*out = Message::Role::Assistant;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject blockToJson(const LLMQore::ContentBlock &block)
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (auto *t = dynamic_cast<const LLMQore::TextContent *>(&block)) {
|
||||
obj["type"] = kKindText;
|
||||
obj["text"] = t->text();
|
||||
} else if (auto *th = dynamic_cast<const LLMQore::ThinkingContent *>(&block)) {
|
||||
obj["type"] = kKindThinking;
|
||||
obj["thinking"] = th->thinking();
|
||||
obj["signature"] = th->signature();
|
||||
} else if (auto *rth = dynamic_cast<const LLMQore::RedactedThinkingContent *>(&block)) {
|
||||
obj["type"] = kKindRedactedThinking;
|
||||
obj["signature"] = rth->signature();
|
||||
} else if (auto *img = dynamic_cast<const LLMQore::ImageContent *>(&block)) {
|
||||
obj["type"] = kKindImage;
|
||||
obj["data"] = img->data();
|
||||
obj["mediaType"] = img->mediaType();
|
||||
obj["sourceType"] = (img->sourceType() == LLMQore::ImageContent::ImageSourceType::Url)
|
||||
? QStringLiteral("url")
|
||||
: QStringLiteral("base64");
|
||||
} else if (auto *tu = dynamic_cast<const LLMQore::ToolUseContent *>(&block)) {
|
||||
obj["type"] = kKindToolUse;
|
||||
obj["id"] = tu->id();
|
||||
obj["name"] = tu->name();
|
||||
obj["input"] = tu->input();
|
||||
} else if (auto *tr = dynamic_cast<const LLMQore::ToolResultContent *>(&block)) {
|
||||
obj["type"] = kKindToolResult;
|
||||
obj["toolUseId"] = tr->toolUseId();
|
||||
obj["result"] = tr->result();
|
||||
} else if (auto *si = dynamic_cast<const StoredImageContent *>(&block)) {
|
||||
obj["type"] = kKindStoredImage;
|
||||
obj["fileName"] = si->fileName();
|
||||
obj["storedPath"] = si->storedPath();
|
||||
obj["mediaType"] = si->mediaType();
|
||||
} else if (auto *sa = dynamic_cast<const StoredAttachmentContent *>(&block)) {
|
||||
obj["type"] = kKindStoredAttachment;
|
||||
obj["fileName"] = sa->fileName();
|
||||
obj["storedPath"] = sa->storedPath();
|
||||
} else if (auto *fe = dynamic_cast<const FileEditContent *>(&block)) {
|
||||
obj["type"] = kKindFileEdit;
|
||||
obj["editId"] = fe->editId();
|
||||
obj["filePath"] = fe->filePath();
|
||||
obj["oldContent"] = fe->oldContent();
|
||||
obj["newContent"] = fe->newContent();
|
||||
obj["status"] = FileEditContent::statusToString(fe->status());
|
||||
if (!fe->statusMessage().isEmpty())
|
||||
obj["statusMessage"] = fe->statusMessage();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
std::unique_ptr<LLMQore::ContentBlock> blockFromJson(const QJsonObject &obj)
|
||||
{
|
||||
const QString type = obj.value("type").toString();
|
||||
if (type == kKindText) {
|
||||
return std::make_unique<LLMQore::TextContent>(obj.value("text").toString());
|
||||
}
|
||||
if (type == kKindThinking) {
|
||||
return std::make_unique<LLMQore::ThinkingContent>(
|
||||
obj.value("thinking").toString(), obj.value("signature").toString());
|
||||
}
|
||||
if (type == kKindRedactedThinking) {
|
||||
return std::make_unique<LLMQore::RedactedThinkingContent>(
|
||||
obj.value("signature").toString());
|
||||
}
|
||||
if (type == kKindImage) {
|
||||
const auto sourceType
|
||||
= (obj.value("sourceType").toString() == QLatin1String("url"))
|
||||
? LLMQore::ImageContent::ImageSourceType::Url
|
||||
: LLMQore::ImageContent::ImageSourceType::Base64;
|
||||
return std::make_unique<LLMQore::ImageContent>(
|
||||
obj.value("data").toString(), obj.value("mediaType").toString(), sourceType);
|
||||
}
|
||||
if (type == kKindToolUse) {
|
||||
return std::make_unique<LLMQore::ToolUseContent>(
|
||||
obj.value("id").toString(),
|
||||
obj.value("name").toString(),
|
||||
obj.value("input").toObject());
|
||||
}
|
||||
if (type == kKindToolResult) {
|
||||
return std::make_unique<LLMQore::ToolResultContent>(
|
||||
obj.value("toolUseId").toString(), obj.value("result").toString());
|
||||
}
|
||||
if (type == kKindStoredImage) {
|
||||
return std::make_unique<StoredImageContent>(
|
||||
obj.value("fileName").toString(),
|
||||
obj.value("storedPath").toString(),
|
||||
obj.value("mediaType").toString());
|
||||
}
|
||||
if (type == kKindStoredAttachment) {
|
||||
return std::make_unique<StoredAttachmentContent>(
|
||||
obj.value("fileName").toString(), obj.value("storedPath").toString());
|
||||
}
|
||||
if (type == kKindFileEdit) {
|
||||
return std::make_unique<FileEditContent>(
|
||||
obj.value("editId").toString(),
|
||||
obj.value("filePath").toString(),
|
||||
obj.value("oldContent").toString(),
|
||||
obj.value("newContent").toString(),
|
||||
FileEditContent::statusFromString(obj.value("status").toString()),
|
||||
obj.value("statusMessage").toString());
|
||||
}
|
||||
return nullptr; // unknown type — skipped
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QJsonObject MessageSerializer::toJson(const Message &message)
|
||||
{
|
||||
QJsonObject obj;
|
||||
obj["role"] = roleToString(message.role());
|
||||
if (!message.id().isEmpty())
|
||||
obj["id"] = message.id();
|
||||
|
||||
QJsonArray blocks;
|
||||
for (const auto &b : message.blocks()) {
|
||||
if (b)
|
||||
blocks.append(blockToJson(*b));
|
||||
}
|
||||
obj["blocks"] = blocks;
|
||||
return obj;
|
||||
}
|
||||
|
||||
Message MessageSerializer::fromJson(const QJsonObject &json, bool *ok)
|
||||
{
|
||||
Message::Role role;
|
||||
if (!roleFromString(json.value("role").toString(), &role)) {
|
||||
if (ok)
|
||||
*ok = false;
|
||||
return Message();
|
||||
}
|
||||
|
||||
Message m(role, json.value("id").toString());
|
||||
const QJsonArray blocks = json.value("blocks").toArray();
|
||||
int unknownBlocks = 0;
|
||||
for (const QJsonValue &v : blocks) {
|
||||
const QJsonObject blockObj = v.toObject();
|
||||
auto block = blockFromJson(blockObj);
|
||||
if (block) {
|
||||
m.appendBlock(std::move(block));
|
||||
} else {
|
||||
++unknownBlocks;
|
||||
qWarning("[QodeAssist] MessageSerializer: unknown block type '%s' "
|
||||
"in stored chat — skipped",
|
||||
qUtf8Printable(blockObj.value("type").toString()));
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
*ok = m.blocks().size() > 0 || blocks.isEmpty();
|
||||
if (unknownBlocks > 0 && !blocks.isEmpty() && m.blocks().empty())
|
||||
*ok = false;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
20
sources/Session/MessageSerializer.hpp
Normal file
20
sources/Session/MessageSerializer.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "Message.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class MessageSerializer
|
||||
{
|
||||
public:
|
||||
static QJsonObject toJson(const Message &message);
|
||||
|
||||
static Message fromJson(const QJsonObject &json, bool *ok = nullptr);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
114
sources/Session/PluginBlocks.hpp
Normal file
114
sources/Session/PluginBlocks.hpp
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class StoredImageContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
StoredImageContent(QString fileName, QString storedPath, QString mediaType)
|
||||
: m_fileName(std::move(fileName))
|
||||
, m_storedPath(std::move(storedPath))
|
||||
, m_mediaType(std::move(mediaType))
|
||||
{}
|
||||
|
||||
QString type() const override { return QStringLiteral("stored_image"); }
|
||||
|
||||
QString fileName() const { return m_fileName; }
|
||||
QString storedPath() const { return m_storedPath; }
|
||||
QString mediaType() const { return m_mediaType; }
|
||||
|
||||
private:
|
||||
QString m_fileName;
|
||||
QString m_storedPath;
|
||||
QString m_mediaType;
|
||||
};
|
||||
|
||||
class StoredAttachmentContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
StoredAttachmentContent(QString fileName, QString storedPath)
|
||||
: m_fileName(std::move(fileName))
|
||||
, m_storedPath(std::move(storedPath))
|
||||
{}
|
||||
|
||||
QString type() const override { return QStringLiteral("stored_attachment"); }
|
||||
|
||||
QString fileName() const { return m_fileName; }
|
||||
QString storedPath() const { return m_storedPath; }
|
||||
|
||||
private:
|
||||
QString m_fileName;
|
||||
QString m_storedPath;
|
||||
};
|
||||
|
||||
class FileEditContent : public LLMQore::ContentBlock
|
||||
{
|
||||
public:
|
||||
enum class Status { Pending, Applied, Rejected, Archived };
|
||||
|
||||
FileEditContent(
|
||||
QString editId,
|
||||
QString filePath,
|
||||
QString oldContent,
|
||||
QString newContent,
|
||||
Status status = Status::Pending,
|
||||
QString statusMessage = QString())
|
||||
: m_editId(std::move(editId))
|
||||
, m_filePath(std::move(filePath))
|
||||
, m_oldContent(std::move(oldContent))
|
||||
, m_newContent(std::move(newContent))
|
||||
, m_status(status)
|
||||
, m_statusMessage(std::move(statusMessage))
|
||||
{}
|
||||
|
||||
QString type() const override { return QStringLiteral("file_edit"); }
|
||||
|
||||
QString editId() const { return m_editId; }
|
||||
QString filePath() const { return m_filePath; }
|
||||
QString oldContent() const { return m_oldContent; }
|
||||
QString newContent() const { return m_newContent; }
|
||||
Status status() const { return m_status; }
|
||||
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)
|
||||
{
|
||||
switch (s) {
|
||||
case Status::Pending: return QStringLiteral("pending");
|
||||
case Status::Applied: return QStringLiteral("applied");
|
||||
case Status::Rejected: return QStringLiteral("rejected");
|
||||
case Status::Archived: return QStringLiteral("archived");
|
||||
}
|
||||
return QStringLiteral("pending");
|
||||
}
|
||||
|
||||
static Status statusFromString(const QString &s)
|
||||
{
|
||||
if (s == QLatin1String("applied"))
|
||||
return Status::Applied;
|
||||
if (s == QLatin1String("rejected"))
|
||||
return Status::Rejected;
|
||||
if (s == QLatin1String("archived"))
|
||||
return Status::Archived;
|
||||
return Status::Pending;
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_editId;
|
||||
QString m_filePath;
|
||||
QString m_oldContent;
|
||||
QString m_newContent;
|
||||
Status m_status;
|
||||
QString m_statusMessage;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
169
sources/Session/ResponseEvent.hpp
Normal file
169
sources/Session/ResponseEvent.hpp
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
namespace ResponseEvents {
|
||||
|
||||
struct TextDelta
|
||||
{
|
||||
QString text;
|
||||
};
|
||||
|
||||
struct ThinkingDelta
|
||||
{
|
||||
QString thinking;
|
||||
QString signature;
|
||||
};
|
||||
|
||||
struct ToolCallStart
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
};
|
||||
|
||||
struct ToolCallArgsDelta
|
||||
{
|
||||
QString id;
|
||||
QString jsonFragment;
|
||||
};
|
||||
|
||||
struct ToolCallEnd
|
||||
{
|
||||
QString id;
|
||||
QJsonObject finalArgs;
|
||||
};
|
||||
|
||||
struct ToolResult
|
||||
{
|
||||
QString toolUseId;
|
||||
QString text;
|
||||
bool isError = false;
|
||||
};
|
||||
|
||||
struct Usage
|
||||
{
|
||||
int inputTokens = 0;
|
||||
int outputTokens = 0;
|
||||
};
|
||||
|
||||
struct Error
|
||||
{
|
||||
QString message;
|
||||
};
|
||||
|
||||
struct MessageStop
|
||||
{
|
||||
QString stopReason;
|
||||
};
|
||||
|
||||
} // namespace ResponseEvents
|
||||
|
||||
class ResponseEvent
|
||||
{
|
||||
public:
|
||||
enum class Kind {
|
||||
MessageStart,
|
||||
TextDelta,
|
||||
ThinkingDelta,
|
||||
ToolCallStart,
|
||||
ToolCallArgsDelta,
|
||||
ToolCallEnd,
|
||||
ToolResult,
|
||||
Usage,
|
||||
MessageStop,
|
||||
Error,
|
||||
};
|
||||
|
||||
Kind kind() const noexcept { return m_kind; }
|
||||
|
||||
template<typename T>
|
||||
const T *as() const noexcept
|
||||
{
|
||||
return std::get_if<T>(&m_data);
|
||||
}
|
||||
|
||||
static ResponseEvent messageStart() { return {Kind::MessageStart, std::monostate{}}; }
|
||||
|
||||
static ResponseEvent messageStop(QString stopReason = QString())
|
||||
{
|
||||
return {Kind::MessageStop, ResponseEvents::MessageStop{std::move(stopReason)}};
|
||||
}
|
||||
|
||||
static ResponseEvent textDelta(QString text)
|
||||
{
|
||||
return {Kind::TextDelta, ResponseEvents::TextDelta{std::move(text)}};
|
||||
}
|
||||
|
||||
static ResponseEvent thinkingDelta(QString thinking, QString signature = QString())
|
||||
{
|
||||
return {
|
||||
Kind::ThinkingDelta,
|
||||
ResponseEvents::ThinkingDelta{std::move(thinking), std::move(signature)}};
|
||||
}
|
||||
|
||||
static ResponseEvent toolCallStart(QString id, QString name)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return {
|
||||
Kind::ToolResult,
|
||||
ResponseEvents::ToolResult{std::move(toolUseId), std::move(text), isError}};
|
||||
}
|
||||
|
||||
static ResponseEvent usage(int inputTokens, int outputTokens)
|
||||
{
|
||||
return {Kind::Usage, ResponseEvents::Usage{inputTokens, outputTokens}};
|
||||
}
|
||||
|
||||
static ResponseEvent error(QString message)
|
||||
{
|
||||
return {Kind::Error, ResponseEvents::Error{std::move(message)}};
|
||||
}
|
||||
|
||||
private:
|
||||
using Data = std::variant<
|
||||
std::monostate,
|
||||
ResponseEvents::TextDelta,
|
||||
ResponseEvents::ThinkingDelta,
|
||||
ResponseEvents::ToolCallStart,
|
||||
ResponseEvents::ToolCallArgsDelta,
|
||||
ResponseEvents::ToolCallEnd,
|
||||
ResponseEvents::ToolResult,
|
||||
ResponseEvents::Usage,
|
||||
ResponseEvents::Error,
|
||||
ResponseEvents::MessageStop>;
|
||||
|
||||
ResponseEvent(Kind kind, Data data)
|
||||
: m_kind(kind)
|
||||
, m_data(std::move(data))
|
||||
{}
|
||||
|
||||
Kind m_kind;
|
||||
Data m_data;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
163
sources/Session/ResponseRouter.cpp
Normal file
163
sources/Session/ResponseRouter.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ResponseRouter.hpp"
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "ConversationHistory.hpp"
|
||||
#include "Message.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ResponseRouter::ResponseRouter(
|
||||
LLMQore::BaseClient *client, ConversationHistory *history, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_client(client)
|
||||
, m_history(history)
|
||||
{
|
||||
if (!m_client)
|
||||
return;
|
||||
|
||||
connect(
|
||||
m_client.data(),
|
||||
&LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ResponseRouter::onChunk);
|
||||
connect(
|
||||
m_client.data(),
|
||||
&LLMQore::BaseClient::thinkingBlockReceived,
|
||||
this,
|
||||
&ResponseRouter::onThinking);
|
||||
connect(
|
||||
m_client.data(),
|
||||
&LLMQore::BaseClient::toolStarted,
|
||||
this,
|
||||
&ResponseRouter::onToolStarted);
|
||||
connect(
|
||||
m_client.data(),
|
||||
&LLMQore::BaseClient::toolResultReady,
|
||||
this,
|
||||
&ResponseRouter::onToolResultReady);
|
||||
connect(
|
||||
m_client.data(),
|
||||
&LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&ResponseRouter::onFinalized);
|
||||
connect(
|
||||
m_client.data(),
|
||||
&LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ResponseRouter::onFailed);
|
||||
}
|
||||
|
||||
ResponseRouter::~ResponseRouter() = default;
|
||||
|
||||
void ResponseRouter::beginRequest(const LLMQore::RequestID &id)
|
||||
{
|
||||
m_activeId = id;
|
||||
resetTurnState();
|
||||
}
|
||||
|
||||
void ResponseRouter::endRequest()
|
||||
{
|
||||
m_activeId.clear();
|
||||
resetTurnState();
|
||||
}
|
||||
|
||||
void ResponseRouter::resetTurnState()
|
||||
{
|
||||
m_assistantOpen = false;
|
||||
m_inToolResults = false;
|
||||
}
|
||||
|
||||
void ResponseRouter::ensureAssistantOpen()
|
||||
{
|
||||
if (m_assistantOpen && !m_inToolResults)
|
||||
return;
|
||||
if (m_history)
|
||||
m_history->append(Message(Message::Role::Assistant));
|
||||
emit event(ResponseEvent::messageStart());
|
||||
m_assistantOpen = true;
|
||||
m_inToolResults = false;
|
||||
}
|
||||
|
||||
void ResponseRouter::onChunk(const LLMQore::RequestID &id, const QString &chunk)
|
||||
{
|
||||
if (id != m_activeId || chunk.isEmpty())
|
||||
return;
|
||||
ensureAssistantOpen();
|
||||
if (m_history)
|
||||
m_history->appendTextDeltaToLast(chunk);
|
||||
emit event(ResponseEvent::textDelta(chunk));
|
||||
}
|
||||
|
||||
void ResponseRouter::onThinking(
|
||||
const LLMQore::RequestID &id, const QString &thinking, const QString &signature)
|
||||
{
|
||||
if (id != m_activeId || (thinking.isEmpty() && signature.isEmpty()))
|
||||
return;
|
||||
ensureAssistantOpen();
|
||||
if (m_history)
|
||||
m_history->appendThinkingDeltaToLast(thinking, signature);
|
||||
emit event(ResponseEvent::thinkingDelta(thinking, signature));
|
||||
}
|
||||
|
||||
void ResponseRouter::onToolStarted(
|
||||
const LLMQore::RequestID &id, const QString &toolId, const QString &toolName)
|
||||
{
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
ensureAssistantOpen();
|
||||
if (m_history)
|
||||
m_history->appendBlockToLast(
|
||||
std::make_unique<LLMQore::ToolUseContent>(toolId, toolName));
|
||||
emit event(ResponseEvent::toolCallStart(toolId, toolName));
|
||||
}
|
||||
|
||||
void ResponseRouter::onToolResultReady(
|
||||
const LLMQore::RequestID &id,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &result)
|
||||
{
|
||||
Q_UNUSED(toolName);
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
|
||||
if (m_history) {
|
||||
if (m_inToolResults) {
|
||||
m_history->appendBlockToLast(
|
||||
std::make_unique<LLMQore::ToolResultContent>(toolId, result));
|
||||
} else {
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(std::make_unique<LLMQore::ToolResultContent>(toolId, result));
|
||||
m_history->append(std::move(m));
|
||||
}
|
||||
}
|
||||
|
||||
m_assistantOpen = false;
|
||||
m_inToolResults = true;
|
||||
emit event(ResponseEvent::toolResult(toolId, result, /*isError=*/false));
|
||||
}
|
||||
|
||||
void ResponseRouter::onFinalized(
|
||||
const LLMQore::RequestID &id, const LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
emit event(ResponseEvent::messageStop(info.stopReason));
|
||||
endRequest();
|
||||
}
|
||||
|
||||
void ResponseRouter::onFailed(const LLMQore::RequestID &id, const QString &err)
|
||||
{
|
||||
if (id != m_activeId)
|
||||
return;
|
||||
emit event(ResponseEvent::error(err));
|
||||
endRequest();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
64
sources/Session/ResponseRouter.hpp
Normal file
64
sources/Session/ResponseRouter.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
#include "ResponseEvent.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ConversationHistory;
|
||||
|
||||
class ResponseRouter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(ResponseRouter)
|
||||
public:
|
||||
ResponseRouter(
|
||||
LLMQore::BaseClient *client,
|
||||
ConversationHistory *history,
|
||||
QObject *parent = nullptr);
|
||||
~ResponseRouter() override;
|
||||
|
||||
void beginRequest(const LLMQore::RequestID &id);
|
||||
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);
|
||||
|
||||
private slots:
|
||||
void onChunk(const LLMQore::RequestID &id, const QString &chunk);
|
||||
void onThinking(
|
||||
const LLMQore::RequestID &id, const QString &thinking, const QString &signature);
|
||||
void onToolStarted(
|
||||
const LLMQore::RequestID &id, const QString &toolId, const QString &toolName);
|
||||
void onToolResultReady(
|
||||
const LLMQore::RequestID &id,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &result);
|
||||
void onFinalized(const LLMQore::RequestID &id, const LLMQore::CompletionInfo &info);
|
||||
void onFailed(const LLMQore::RequestID &id, const QString &err);
|
||||
|
||||
private:
|
||||
void ensureAssistantOpen();
|
||||
void resetTurnState();
|
||||
|
||||
QPointer<LLMQore::BaseClient> m_client;
|
||||
QPointer<ConversationHistory> m_history;
|
||||
|
||||
LLMQore::RequestID m_activeId;
|
||||
bool m_assistantOpen = false;
|
||||
bool m_inToolResults = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
386
sources/Session/Session.cpp
Normal file
386
sources/Session/Session.cpp
Normal file
@@ -0,0 +1,386 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "Session.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
|
||||
#include <QDebug>
|
||||
#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"
|
||||
#include "SystemPromptBuilder.hpp"
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
} // 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)
|
||||
{}
|
||||
|
||||
Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_agent(agent)
|
||||
, m_history(externalHistory ? externalHistory : new ConversationHistory(this))
|
||||
, m_systemPrompt(new SystemPromptBuilder(this))
|
||||
{
|
||||
if (!m_agent) {
|
||||
m_invalidReason = QStringLiteral("Session: agent is null");
|
||||
return;
|
||||
}
|
||||
m_agent->setParent(this);
|
||||
|
||||
if (!m_agent->isValid()) {
|
||||
m_invalidReason = m_agent->invalidReason();
|
||||
return;
|
||||
}
|
||||
|
||||
auto *provider = m_agent->provider();
|
||||
auto *client = provider ? provider->client() : nullptr;
|
||||
if (!client) {
|
||||
m_invalidReason = QStringLiteral("Session: provider has no live client");
|
||||
return;
|
||||
}
|
||||
if (!m_agent->promptTemplate()) {
|
||||
m_invalidReason
|
||||
= QStringLiteral("Session: agent has no inline prompt template");
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
bool Session::isValid() const noexcept
|
||||
{
|
||||
return m_invalidReason.isEmpty();
|
||||
}
|
||||
|
||||
QString Session::invalidReason() const
|
||||
{
|
||||
return m_invalidReason;
|
||||
}
|
||||
|
||||
bool Session::isInFlight() const noexcept
|
||||
{
|
||||
return !m_inFlight.isEmpty();
|
||||
}
|
||||
|
||||
void Session::setContentLoader(ContentLoader loader)
|
||||
{
|
||||
m_contentLoader = std::move(loader);
|
||||
}
|
||||
|
||||
void Session::setContextBindings(Templates::ContextRenderer::Bindings bindings)
|
||||
{
|
||||
m_contextBindings = std::move(bindings);
|
||||
}
|
||||
|
||||
QString Session::renderAgentContext() const
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::sendText(const QString &text)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::send(
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
|
||||
std::optional<bool> toolsOverride)
|
||||
{
|
||||
if (!isValid() || userBlocks.empty())
|
||||
return {};
|
||||
if (!m_history)
|
||||
return {};
|
||||
|
||||
if (isInFlight())
|
||||
cancel();
|
||||
|
||||
Message msg(Message::Role::User);
|
||||
for (auto &b : userBlocks)
|
||||
msg.appendBlock(std::move(b));
|
||||
m_history->append(std::move(msg));
|
||||
|
||||
return dispatch(toolsOverride);
|
||||
}
|
||||
|
||||
void Session::cancel()
|
||||
{
|
||||
if (m_inFlight.isEmpty())
|
||||
return;
|
||||
|
||||
const auto id = m_inFlight;
|
||||
m_inFlight.clear();
|
||||
if (m_router)
|
||||
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)
|
||||
{
|
||||
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 {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
LLMQore::RequestID Session::dispatch(std::optional<bool> toolsOverride)
|
||||
{
|
||||
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))
|
||||
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;
|
||||
}
|
||||
|
||||
Templates::ContextData Session::toLegacyContext() 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;
|
||||
}
|
||||
|
||||
void Session::onRouterEvent(const ResponseEvent &ev)
|
||||
{
|
||||
if (m_inFlight.isEmpty())
|
||||
return; // stale events after cancel
|
||||
|
||||
emit event(ev);
|
||||
|
||||
if (ev.kind() == ResponseEvent::Kind::MessageStop) {
|
||||
const auto *stop = ev.as<ResponseEvents::MessageStop>();
|
||||
const QString reason = stop ? stop->stopReason : QString();
|
||||
const auto id = m_inFlight;
|
||||
m_inFlight.clear();
|
||||
emit finished(id, reason);
|
||||
} else if (ev.kind() == ResponseEvent::Kind::Error) {
|
||||
const auto *err = ev.as<ResponseEvents::Error>();
|
||||
const QString msg = err ? err->message : QStringLiteral("unknown error");
|
||||
const auto id = m_inFlight;
|
||||
m_inFlight.clear();
|
||||
emit failed(id, msg);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
105
sources/Session/Session.hpp
Normal file
105
sources/Session/Session.hpp
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <ContextData.hpp>
|
||||
#include <ContextRenderer.hpp>
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "ConversationHistory.hpp"
|
||||
#include "ResponseEvent.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class Agent;
|
||||
class ResponseRouter;
|
||||
class SystemPromptBuilder;
|
||||
|
||||
class Session : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(Session)
|
||||
public:
|
||||
explicit Session(QObject *parent = nullptr);
|
||||
|
||||
Session(
|
||||
Agent *agent,
|
||||
ConversationHistory *externalHistory = nullptr,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
Session(Agent *agent, QObject *parent);
|
||||
|
||||
~Session() override;
|
||||
|
||||
bool isValid() const noexcept;
|
||||
QString invalidReason() const;
|
||||
bool isInFlight() const noexcept;
|
||||
|
||||
using ContentLoader = std::function<QString(const QString &storedPath)>;
|
||||
void setContentLoader(ContentLoader loader);
|
||||
|
||||
Agent *agent() noexcept { return m_agent; }
|
||||
ConversationHistory *history() const noexcept { return m_history; }
|
||||
SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; }
|
||||
|
||||
void setContextBindings(Templates::ContextRenderer::Bindings bindings);
|
||||
|
||||
QString renderAgentContext() const;
|
||||
|
||||
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:
|
||||
void event(const QodeAssist::ResponseEvent &ev);
|
||||
|
||||
void started(const LLMQore::RequestID &id);
|
||||
void finished(const LLMQore::RequestID &id, const QString &stopReason);
|
||||
void failed(const LLMQore::RequestID &id, const QString &error);
|
||||
|
||||
private slots:
|
||||
void onRouterEvent(const QodeAssist::ResponseEvent &ev);
|
||||
|
||||
private:
|
||||
LLMQore::RequestID dispatch(std::optional<bool> toolsOverride = std::nullopt);
|
||||
Templates::ContextData toLegacyContext() const;
|
||||
|
||||
Agent *m_agent = nullptr; // child if non-null
|
||||
QPointer<ConversationHistory> m_history; // child if internal, external otherwise
|
||||
SystemPromptBuilder *m_systemPrompt = nullptr; // child
|
||||
ResponseRouter *m_router = nullptr; // child, only when valid
|
||||
|
||||
LLMQore::RequestID m_inFlight;
|
||||
QString m_invalidReason;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
105
sources/Session/SessionManager.cpp
Normal file
105
sources/Session/SessionManager.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "SessionManager.hpp"
|
||||
|
||||
#include "Agent.hpp"
|
||||
#include "AgentFactory.hpp"
|
||||
#include "Session.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
SessionManager::SessionManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
SessionManager::SessionManager(AgentFactory *agentFactory, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_agentFactory(agentFactory)
|
||||
{}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Session *SessionManager::createSession(
|
||||
const QString &agentName, ConversationHistory *externalHistory, QString *errorOut)
|
||||
{
|
||||
if (!m_agentFactory) {
|
||||
if (errorOut)
|
||||
*errorOut = QStringLiteral("SessionManager: no AgentFactory bound");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString agentErr;
|
||||
Agent *agent = m_agentFactory->create(agentName, /*parent=*/nullptr, &agentErr);
|
||||
if (!agent) {
|
||||
if (errorOut)
|
||||
*errorOut = agentErr.isEmpty()
|
||||
? QStringLiteral("SessionManager: agent '%1' not found").arg(agentName)
|
||||
: agentErr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *session = new Session(agent, externalHistory, this);
|
||||
if (!session->isValid()) {
|
||||
if (errorOut)
|
||||
*errorOut = session->invalidReason();
|
||||
delete session; // also deletes the reparented agent
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_sessions.append(session);
|
||||
emit sessionCreated(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
void SessionManager::removeSession(Session *session)
|
||||
{
|
||||
if (!session)
|
||||
return;
|
||||
|
||||
const int idx = m_sessions.indexOf(session);
|
||||
if (idx < 0)
|
||||
return;
|
||||
|
||||
if (session->isInFlight())
|
||||
session->cancel();
|
||||
|
||||
m_sessions.removeAt(idx);
|
||||
emit sessionRemoved(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
|
||||
52
sources/Session/SessionManager.hpp
Normal file
52
sources/Session/SessionManager.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class AgentFactory;
|
||||
class ConversationHistory;
|
||||
class Session;
|
||||
|
||||
class SessionManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(SessionManager)
|
||||
public:
|
||||
explicit SessionManager(QObject *parent = nullptr);
|
||||
|
||||
SessionManager(AgentFactory *agentFactory, QObject *parent = nullptr);
|
||||
|
||||
~SessionManager() override;
|
||||
|
||||
Session *createSession();
|
||||
|
||||
Session *createSession(const QString &agentName, QString *errorOut = nullptr);
|
||||
|
||||
Session *createSession(
|
||||
const QString &agentName,
|
||||
ConversationHistory *externalHistory,
|
||||
QString *errorOut = nullptr);
|
||||
|
||||
void removeSession(Session *session);
|
||||
|
||||
QList<Session *> sessions() const;
|
||||
|
||||
void cancelAll();
|
||||
|
||||
signals:
|
||||
void sessionCreated(Session *session);
|
||||
void sessionRemoved(Session *session);
|
||||
|
||||
private:
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QList<QPointer<Session>> m_sessions;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
71
sources/Session/SystemPromptBuilder.cpp
Normal file
71
sources/Session/SystemPromptBuilder.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "SystemPromptBuilder.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
SystemPromptBuilder::SystemPromptBuilder(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void SystemPromptBuilder::setLayer(const QString &name, const QString &text)
|
||||
{
|
||||
for (auto &pair : m_layers) {
|
||||
if (pair.first == name) {
|
||||
if (pair.second == text) return;
|
||||
pair.second = text;
|
||||
emit layersChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_layers.append({name, text});
|
||||
emit layersChanged();
|
||||
}
|
||||
|
||||
void SystemPromptBuilder::clearLayer(const QString &name)
|
||||
{
|
||||
for (auto it = m_layers.begin(); it != m_layers.end(); ++it) {
|
||||
if (it->first == name) {
|
||||
m_layers.erase(it);
|
||||
emit layersChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SystemPromptBuilder::clear()
|
||||
{
|
||||
if (m_layers.isEmpty()) return;
|
||||
m_layers.clear();
|
||||
emit layersChanged();
|
||||
}
|
||||
|
||||
QString SystemPromptBuilder::layer(const QString &name) const
|
||||
{
|
||||
for (const auto &pair : m_layers) {
|
||||
if (pair.first == name) return pair.second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList SystemPromptBuilder::layerNames() const
|
||||
{
|
||||
QStringList out;
|
||||
out.reserve(m_layers.size());
|
||||
for (const auto &pair : m_layers) out.append(pair.first);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString SystemPromptBuilder::compose(const QString &separator) const
|
||||
{
|
||||
QStringList parts;
|
||||
parts.reserve(m_layers.size());
|
||||
for (const auto &pair : m_layers) {
|
||||
if (!pair.second.isEmpty())
|
||||
parts.append(pair.second);
|
||||
}
|
||||
return parts.join(separator);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
36
sources/Session/SystemPromptBuilder.hpp
Normal file
36
sources/Session/SystemPromptBuilder.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class SystemPromptBuilder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SystemPromptBuilder(QObject *parent = nullptr);
|
||||
|
||||
void setLayer(const QString &name, const QString &text);
|
||||
void clearLayer(const QString &name);
|
||||
void clear();
|
||||
|
||||
QString layer(const QString &name) const;
|
||||
QStringList layerNames() const;
|
||||
bool isEmpty() const { return m_layers.isEmpty(); }
|
||||
|
||||
QString compose(const QString &separator = QStringLiteral("\n\n")) const;
|
||||
|
||||
signals:
|
||||
void layersChanged();
|
||||
|
||||
private:
|
||||
QVector<QPair<QString, QString>> m_layers;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
Reference in New Issue
Block a user