feat: Rename old llmcore module to pluginllmcore

This commit is contained in:
Petr Mironychev
2026-03-30 00:49:45 +02:00
parent 7b0b04a1ee
commit f58fad9578
123 changed files with 1018 additions and 1018 deletions

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "BaseTool.hpp"
namespace QodeAssist::PluginLLMCore {
BaseTool::BaseTool(QObject *parent)
: QObject(parent)
{}
QJsonObject BaseTool::customizeForOpenAI(const QJsonObject &baseDefinition) const
{
QJsonObject function;
function["name"] = name();
function["description"] = description();
function["parameters"] = baseDefinition;
QJsonObject tool;
tool["type"] = "function";
tool["function"] = function;
return tool;
}
QJsonObject BaseTool::customizeForClaude(const QJsonObject &baseDefinition) const
{
QJsonObject tool;
tool["name"] = name();
tool["description"] = description();
tool["input_schema"] = baseDefinition;
return tool;
}
QJsonObject BaseTool::customizeForOllama(const QJsonObject &baseDefinition) const
{
return customizeForOpenAI(baseDefinition);
}
QJsonObject BaseTool::customizeForGoogle(const QJsonObject &baseDefinition) const
{
QJsonObject functionDeclaration;
functionDeclaration["name"] = name();
functionDeclaration["description"] = description();
functionDeclaration["parameters"] = baseDefinition;
QJsonArray functionDeclarations;
functionDeclarations.append(functionDeclaration);
QJsonObject tool;
tool["function_declarations"] = functionDeclarations;
return tool;
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QString>
namespace QodeAssist::PluginLLMCore {
enum class ToolSchemaFormat { OpenAI, Claude, Ollama, Google };
enum ToolPermission {
None = 0,
FileSystemRead = 1 << 0,
FileSystemWrite = 1 << 1,
NetworkAccess = 1 << 2
};
Q_DECLARE_FLAGS(ToolPermissions, ToolPermission)
Q_DECLARE_OPERATORS_FOR_FLAGS(ToolPermissions)
enum class RunToolsFilter {
ALL, // Run all tools (no filtering)
OnlyRead, // Run only read tools (FileSystemRead + None)
OnlyWrite, // Run only write tools (FileSystemWrite)
OnlyNetworking // Run only network tools (NetworkAccess)
};
class BaseTool : public QObject
{
Q_OBJECT
public:
explicit BaseTool(QObject *parent = nullptr);
~BaseTool() override = default;
virtual QString name() const = 0;
virtual QString stringName() const = 0;
virtual QString description() const = 0;
virtual QJsonObject getDefinition(ToolSchemaFormat format) const = 0;
virtual ToolPermissions requiredPermissions() const = 0;
virtual QFuture<QString> executeAsync(const QJsonObject &input = QJsonObject()) = 0;
protected:
virtual QJsonObject customizeForOpenAI(const QJsonObject &baseDefinition) const;
virtual QJsonObject customizeForClaude(const QJsonObject &baseDefinition) const;
virtual QJsonObject customizeForOllama(const QJsonObject &baseDefinition) const;
virtual QJsonObject customizeForGoogle(const QJsonObject &baseDefinition) const;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,35 @@
add_library(PluginLLMCore STATIC
RequestType.hpp
Provider.hpp Provider.cpp
ProvidersManager.hpp ProvidersManager.cpp
ContextData.hpp
IPromptProvider.hpp
IProviderRegistry.hpp
PromptProviderChat.hpp
PromptProviderFim.hpp
PromptTemplate.hpp
PromptTemplateManager.hpp PromptTemplateManager.cpp
RequestConfig.hpp
ValidationUtils.hpp ValidationUtils.cpp
ProviderID.hpp
HttpClient.hpp HttpClient.cpp
DataBuffers.hpp
SSEBuffer.hpp SSEBuffer.cpp
BaseTool.hpp BaseTool.cpp
ContentBlocks.hpp
RulesLoader.hpp RulesLoader.cpp
ResponseCleaner.hpp
)
target_link_libraries(PluginLLMCore
PUBLIC
Qt::Core
Qt::Network
QtCreator::Core
QtCreator::Utils
QtCreator::ExtensionSystem
PRIVATE
QodeAssistLogger
)
target_include_directories(PluginLLMCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -0,0 +1,252 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QString>
namespace QodeAssist::PluginLLMCore {
enum class MessageState { Building, Complete, RequiresToolExecution, Final };
enum class ProviderFormat { Claude, OpenAI };
class ContentBlock : public QObject
{
Q_OBJECT
public:
explicit ContentBlock(QObject *parent = nullptr)
: QObject(parent)
{}
virtual ~ContentBlock() = default;
virtual QString type() const = 0;
virtual QJsonValue toJson(ProviderFormat format) const = 0;
};
class TextContent : public ContentBlock
{
Q_OBJECT
public:
explicit TextContent(const QString &text = QString())
: ContentBlock()
, m_text(text)
{}
QString type() const override { return "text"; }
QString text() const { return m_text; }
void appendText(const QString &text) { m_text += text; }
void setText(const QString &text) { m_text = text; }
QJsonValue toJson(ProviderFormat format) const override
{
Q_UNUSED(format);
return QJsonObject{{"type", "text"}, {"text", m_text}};
}
private:
QString m_text;
};
class ImageContent : public ContentBlock
{
Q_OBJECT
public:
enum class ImageSourceType { Base64, Url };
ImageContent(const QString &data, const QString &mediaType, ImageSourceType sourceType = ImageSourceType::Base64)
: ContentBlock()
, m_data(data)
, m_mediaType(mediaType)
, m_sourceType(sourceType)
{}
QString type() const override { return "image"; }
QString data() const { return m_data; }
QString mediaType() const { return m_mediaType; }
ImageSourceType sourceType() const { return m_sourceType; }
QJsonValue toJson(ProviderFormat format) const override
{
if (format == ProviderFormat::Claude) {
QJsonObject source;
if (m_sourceType == ImageSourceType::Base64) {
source["type"] = "base64";
source["media_type"] = m_mediaType;
source["data"] = m_data;
} else {
source["type"] = "url";
source["url"] = m_data;
}
return QJsonObject{{"type", "image"}, {"source", source}};
} else { // OpenAI format
QJsonObject imageUrl;
if (m_sourceType == ImageSourceType::Base64) {
imageUrl["url"] = QString("data:%1;base64,%2").arg(m_mediaType, m_data);
} else {
imageUrl["url"] = m_data;
}
return QJsonObject{{"type", "image_url"}, {"image_url", imageUrl}};
}
}
private:
QString m_data;
QString m_mediaType;
ImageSourceType m_sourceType;
};
class ToolUseContent : public ContentBlock
{
Q_OBJECT
public:
ToolUseContent(const QString &id, const QString &name, const QJsonObject &input = QJsonObject())
: ContentBlock()
, m_id(id)
, m_name(name)
, m_input(input)
{}
QString type() const override { return "tool_use"; }
QString id() const { return m_id; }
QString name() const { return m_name; }
QJsonObject input() const { return m_input; }
void setInput(const QJsonObject &input) { m_input = input; }
QJsonValue toJson(ProviderFormat format) const override
{
if (format == ProviderFormat::Claude) {
return QJsonObject{
{"type", "tool_use"}, {"id", m_id}, {"name", m_name}, {"input", m_input}};
} else { // OpenAI
QJsonDocument doc(m_input);
return QJsonObject{
{"id", m_id},
{"type", "function"},
{"function",
QJsonObject{
{"name", m_name},
{"arguments", QString::fromUtf8(doc.toJson(QJsonDocument::Compact))}}}};
}
}
private:
QString m_id;
QString m_name;
QJsonObject m_input;
};
class ToolResultContent : public ContentBlock
{
Q_OBJECT
public:
ToolResultContent(const QString &toolUseId, const QString &result)
: ContentBlock()
, m_toolUseId(toolUseId)
, m_result(result)
{}
QString type() const override { return "tool_result"; }
QString toolUseId() const { return m_toolUseId; }
QString result() const { return m_result; }
QJsonValue toJson(ProviderFormat format) const override
{
if (format == ProviderFormat::Claude) {
return QJsonObject{
{"type", "tool_result"}, {"tool_use_id", m_toolUseId}, {"content", m_result}};
} else { // OpenAI
return QJsonObject{{"role", "tool"}, {"tool_call_id", m_toolUseId}, {"content", m_result}};
}
}
private:
QString m_toolUseId;
QString m_result;
};
class ThinkingContent : public ContentBlock
{
Q_OBJECT
public:
explicit ThinkingContent(const QString &thinking = QString(), const QString &signature = QString())
: ContentBlock()
, m_thinking(thinking)
, m_signature(signature)
{}
QString type() const override { return "thinking"; }
QString thinking() const { return m_thinking; }
QString signature() const { return m_signature; }
void appendThinking(const QString &text) { m_thinking += text; }
void setThinking(const QString &text) { m_thinking = text; }
void setSignature(const QString &signature) { m_signature = signature; }
QJsonValue toJson(ProviderFormat format) const override
{
Q_UNUSED(format);
// Only include signature field if it's not empty
// Empty signature is rejected by API with "Invalid signature" error
// In streaming mode, signature is not provided, so we omit the field entirely
QJsonObject obj{{"type", "thinking"}, {"thinking", m_thinking}};
if (!m_signature.isEmpty()) {
obj["signature"] = m_signature;
}
return obj;
}
private:
QString m_thinking;
QString m_signature;
};
class RedactedThinkingContent : public ContentBlock
{
Q_OBJECT
public:
explicit RedactedThinkingContent(const QString &signature = QString())
: ContentBlock()
, m_signature(signature)
{}
QString type() const override { return "redacted_thinking"; }
QString signature() const { return m_signature; }
void setSignature(const QString &signature) { m_signature = signature; }
QJsonValue toJson(ProviderFormat format) const override
{
Q_UNUSED(format);
// Only include signature field if it's not empty
// Empty signature is rejected by API with "Invalid signature" error
QJsonObject obj{{"type", "redacted_thinking"}};
if (!m_signature.isEmpty()) {
obj["signature"] = m_signature;
}
return obj;
}
private:
QString m_signature;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include <QVector>
namespace QodeAssist::PluginLLMCore {
struct ImageAttachment
{
QString data; // Base64 encoded data or URL
QString mediaType; // e.g., "image/png", "image/jpeg", "image/webp", "image/gif"
bool isUrl = false; // true if data is URL, false if base64
bool operator==(const ImageAttachment &) const = default;
};
struct Message
{
QString role;
QString content;
QString signature;
bool isThinking = false;
bool isRedacted = false;
std::optional<QVector<ImageAttachment>> images;
// clang-format off
bool operator==(const Message&) const = default;
// clang-format on
};
struct FileMetadata
{
QString filePath;
QString content;
bool operator==(const FileMetadata &) const = default;
};
struct ContextData
{
std::optional<QString> systemPrompt;
std::optional<QString> prefix;
std::optional<QString> suffix;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
std::optional<QList<FileMetadata>> filesMetadata;
bool operator==(const ContextData &) const = default;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "SSEBuffer.hpp"
#include <QString>
namespace QodeAssist::PluginLLMCore {
struct DataBuffers
{
SSEBuffer rawStreamBuffer;
QString responseContent;
void clear()
{
rawStreamBuffer.clear();
responseContent.clear();
}
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,276 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "HttpClient.hpp"
#include <QJsonDocument>
#include <QMutexLocker>
#include <Logger.hpp>
namespace QodeAssist::PluginLLMCore {
HttpClient::HttpClient(QObject *parent)
: QObject(parent)
, m_manager(new QNetworkAccessManager(this))
{}
HttpClient::~HttpClient()
{
QMutexLocker locker(&m_mutex);
for (auto *reply : std::as_const(m_activeRequests)) {
reply->abort();
reply->deleteLater();
}
m_activeRequests.clear();
}
QFuture<QByteArray> HttpClient::get(const QNetworkRequest &request)
{
LOG_MESSAGE(QString("HttpClient: GET %1").arg(request.url().toString()));
auto promise = std::make_shared<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply = m_manager->get(request);
setupNonStreamingReply(reply, promise);
return promise->future();
}
QFuture<QByteArray> HttpClient::post(const QNetworkRequest &request, const QJsonObject &payload)
{
QJsonDocument doc(payload);
LOG_MESSAGE(QString("HttpClient: POST %1, data: %2")
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
auto promise = std::make_shared<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
setupNonStreamingReply(reply, promise);
return promise->future();
}
QFuture<QByteArray> HttpClient::del(const QNetworkRequest &request,
std::optional<QJsonObject> payload)
{
auto promise = std::make_shared<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply;
if (payload) {
QJsonDocument doc(*payload);
LOG_MESSAGE(QString("HttpClient: DELETE %1, data: %2")
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
reply = m_manager->sendCustomRequest(request, "DELETE", doc.toJson(QJsonDocument::Compact));
} else {
LOG_MESSAGE(QString("HttpClient: DELETE %1").arg(request.url().toString()));
reply = m_manager->deleteResource(request);
}
setupNonStreamingReply(reply, promise);
return promise->future();
}
void HttpClient::setupNonStreamingReply(QNetworkReply *reply,
std::shared_ptr<QPromise<QByteArray>> promise)
{
connect(reply, &QNetworkReply::finished, this, [this, reply, promise]() {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray responseBody = reply->readAll();
QNetworkReply::NetworkError networkError = reply->error();
QString networkErrorString = reply->errorString();
reply->disconnect();
reply->deleteLater();
LOG_MESSAGE(
QString("HttpClient: Non-streaming request - HTTP Status: %1").arg(statusCode));
bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
if (hasError) {
QString errorMsg = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
LOG_MESSAGE(QString("HttpClient: Non-streaming request - Error: %1").arg(errorMsg));
promise->setException(
std::make_exception_ptr(std::runtime_error(errorMsg.toStdString())));
} else {
promise->addResult(responseBody);
}
promise->finish();
});
}
void HttpClient::postStreaming(const QString &requestId, const QNetworkRequest &request,
const QJsonObject &payload)
{
QJsonDocument doc(payload);
LOG_MESSAGE(QString("HttpClient: POST streaming %1, data: %2")
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
addActiveRequest(reply, requestId);
connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead);
connect(reply, &QNetworkReply::finished, this, &HttpClient::onStreamingFinished);
}
void HttpClient::cancelRequest(const QString &requestId)
{
QMutexLocker locker(&m_mutex);
auto it = m_activeRequests.find(requestId);
if (it != m_activeRequests.end()) {
QNetworkReply *reply = it.value();
if (reply) {
reply->disconnect();
reply->abort();
reply->deleteLater();
}
m_activeRequests.erase(it);
LOG_MESSAGE(QString("HttpClient: Cancelled request: %1").arg(requestId));
}
}
void HttpClient::onReadyRead()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply || reply->isFinished())
return;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode >= 400)
return;
QString requestId = findRequestId(reply);
if (requestId.isEmpty())
return;
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
emit dataReceived(requestId, data);
}
}
void HttpClient::onStreamingFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply)
return;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray responseBody = reply->readAll();
QNetworkReply::NetworkError networkError = reply->error();
QString networkErrorString = reply->errorString();
reply->disconnect();
QString requestId;
std::optional<QString> error;
{
QMutexLocker locker(&m_mutex);
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value() == reply) {
requestId = it.key();
m_activeRequests.erase(it);
break;
}
}
if (requestId.isEmpty()) {
reply->deleteLater();
return;
}
bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
if (hasError) {
error = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
}
LOG_MESSAGE(
QString("HttpClient: Request %1 - HTTP Status: %2").arg(requestId).arg(statusCode));
if (!responseBody.isEmpty()) {
LOG_MESSAGE(QString("HttpClient: Request %1 - Response body (%2 bytes): %3")
.arg(requestId)
.arg(responseBody.size())
.arg(QString::fromUtf8(responseBody)));
}
if (error) {
LOG_MESSAGE(QString("HttpClient: Request %1 - Error: %2").arg(requestId, *error));
}
}
reply->deleteLater();
if (!requestId.isEmpty()) {
emit requestFinished(requestId, error);
}
}
QString HttpClient::findRequestId(QNetworkReply *reply)
{
QMutexLocker locker(&m_mutex);
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value() == reply)
return it.key();
}
return {};
}
void HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
{
QMutexLocker locker(&m_mutex);
m_activeRequests[requestId] = reply;
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
}
QString HttpClient::parseErrorFromResponse(
int statusCode, const QByteArray &responseBody, const QString &networkErrorString)
{
if (!responseBody.isEmpty()) {
QJsonDocument errorDoc = QJsonDocument::fromJson(responseBody);
if (!errorDoc.isNull() && errorDoc.isObject()) {
QJsonObject errorObj = errorDoc.object();
if (errorObj.contains("error")) {
QJsonObject error = errorObj["error"].toObject();
QString message = error["message"].toString();
QString type = error["type"].toString();
QString code = error["code"].toString();
QString errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(message);
if (!type.isEmpty())
errorMsg += QString(" (type: %1)").arg(type);
if (!code.isEmpty())
errorMsg += QString(" (code: %1)").arg(code);
return errorMsg;
}
return QString("HTTP %1: %2")
.arg(statusCode)
.arg(QString::fromUtf8(responseBody));
}
return QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
}
return QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString);
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,76 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <QFuture>
#include <QHash>
#include <QJsonObject>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <QPromise>
namespace QodeAssist::PluginLLMCore {
class HttpClient : public QObject
{
Q_OBJECT
public:
HttpClient(QObject *parent = nullptr);
~HttpClient();
// Non-streaming — return QFuture with full response
QFuture<QByteArray> get(const QNetworkRequest &request);
QFuture<QByteArray> post(const QNetworkRequest &request, const QJsonObject &payload);
QFuture<QByteArray> del(const QNetworkRequest &request,
std::optional<QJsonObject> payload = std::nullopt);
// Streaming — signal-based with requestId
void postStreaming(const QString &requestId, const QNetworkRequest &request,
const QJsonObject &payload);
void cancelRequest(const QString &requestId);
signals:
void dataReceived(const QString &requestId, const QByteArray &data);
void requestFinished(const QString &requestId, std::optional<QString> error);
private slots:
void onReadyRead();
void onStreamingFinished();
private:
void setupNonStreamingReply(QNetworkReply *reply, std::shared_ptr<QPromise<QByteArray>> promise);
QString findRequestId(QNetworkReply *reply);
void addActiveRequest(QNetworkReply *reply, const QString &requestId);
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody,
const QString &networkErrorString);
QNetworkAccessManager *m_manager;
QHash<QString, QNetworkReply *> m_activeRequests;
mutable QMutex m_mutex;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "PromptTemplate.hpp"
#include <QString>
namespace QodeAssist::PluginLLMCore {
class IPromptProvider
{
public:
virtual ~IPromptProvider() = default;
virtual PromptTemplate *getTemplateByName(const QString &templateName) const = 0;
virtual QStringList templatesNames() const = 0;
virtual QStringList getTemplatesForProvider(ProviderID id) const = 0;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "Provider.hpp"
namespace QodeAssist::PluginLLMCore {
class IProviderRegistry
{
public:
virtual ~IProviderRegistry() = default;
virtual Provider *getProviderByName(const QString &providerName) = 0;
virtual QStringList providersNames() const = 0;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "BaseTool.hpp"
namespace QodeAssist::PluginLLMCore {
class IToolsManager
{
public:
virtual ~IToolsManager() = default;
virtual void executeToolCall(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &input) = 0;
virtual QJsonArray getToolsDefinitions(
ToolSchemaFormat format,
RunToolsFilter filter = RunToolsFilter::ALL) const = 0;
virtual void cleanupRequest(const QString &requestId) = 0;
virtual void setCurrentSessionId(const QString &sessionId) = 0;
virtual void clearTodoSession(const QString &sessionId) = 0;
};
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplate.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::PluginLLMCore {
class PromptProviderChat : public IPromptProvider
{
public:
explicit PromptProviderChat(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderChat() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getChatTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getChatTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::PluginLLMCore {
class PromptProviderFim : public IPromptProvider
{
public:
explicit PromptProviderFim(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderFim() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getFimTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getFimTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QList>
#include <QString>
#include "ContextData.hpp"
#include "ProviderID.hpp"
namespace QodeAssist::PluginLLMCore {
enum class TemplateType { Chat, FIM, FIMOnChat };
class PromptTemplate
{
public:
virtual ~PromptTemplate() = default;
virtual TemplateType type() const = 0;
virtual QString name() const = 0;
virtual QStringList stopWords() const = 0;
virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0;
virtual QString description() const = 0;
virtual bool isSupportProvider(ProviderID id) const = 0;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "PromptTemplateManager.hpp"
#include <QMessageBox>
namespace QodeAssist::PluginLLMCore {
PromptTemplateManager &PromptTemplateManager::instance()
{
static PromptTemplateManager instance;
return instance;
}
QStringList PromptTemplateManager::fimTemplatesNames() const
{
return m_fimTemplates.keys();
}
QStringList PromptTemplateManager::chatTemplatesNames() const
{
return m_chatTemplates.keys();
}
QStringList PromptTemplateManager::getFimTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_fimTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
QStringList PromptTemplateManager::getChatTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_chatTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
PromptTemplateManager::~PromptTemplateManager()
{
qDeleteAll(m_fimTemplates);
}
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
{
if (!m_fimTemplates.contains(templateName)) {
QMessageBox::warning(
nullptr,
QObject::tr("Template Not Found"),
QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.")
.arg(templateName));
return m_fimTemplates.first();
}
return m_fimTemplates[templateName];
}
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
{
if (!m_chatTemplates.contains(templateName)) {
QMessageBox::warning(
nullptr,
QObject::tr("Template Not Found"),
QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.")
.arg(templateName));
return m_chatTemplates.first();
}
return m_chatTemplates[templateName];
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QMap>
#include <QString>
#include "PromptTemplate.hpp"
namespace QodeAssist::PluginLLMCore {
class PromptTemplateManager
{
public:
static PromptTemplateManager &instance();
~PromptTemplateManager();
template<typename T>
void registerTemplate()
{
static_assert(std::is_base_of<PromptTemplate, T>::value, "T must inherit from PromptTemplate");
T *template_ptr = new T();
QString name = template_ptr->name();
m_fimTemplates[name] = template_ptr;
if (template_ptr->type() == TemplateType::Chat) {
m_chatTemplates[name] = template_ptr;
}
}
PromptTemplate *getFimTemplateByName(const QString &templateName);
PromptTemplate *getChatTemplateByName(const QString &templateName);
QStringList fimTemplatesNames() const;
QStringList chatTemplatesNames() const;
QStringList getFimTemplatesForProvider(ProviderID id);
QStringList getChatTemplatesForProvider(ProviderID id);
private:
PromptTemplateManager() = default;
PromptTemplateManager(const PromptTemplateManager &) = delete;
PromptTemplateManager &operator=(const PromptTemplateManager &) = delete;
QMap<QString, PromptTemplate *> m_fimTemplates;
QMap<QString, PromptTemplate *> m_chatTemplates;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,36 @@
#include "Provider.hpp"
#include <QJsonDocument>
namespace QodeAssist::PluginLLMCore {
Provider::Provider(QObject *parent)
: QObject(parent)
, m_httpClient(new HttpClient(this))
{
connect(m_httpClient, &HttpClient::dataReceived, this, &Provider::onDataReceived);
connect(m_httpClient, &HttpClient::requestFinished, this, &Provider::onRequestFinished);
}
void Provider::cancelRequest(const RequestID &requestId)
{
m_httpClient->cancelRequest(requestId);
}
HttpClient *Provider::httpClient() const
{
return m_httpClient;
}
QJsonObject Provider::parseEventLine(const QString &line)
{
if (!line.startsWith("data: "))
return QJsonObject();
QString jsonStr = line.mid(6);
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
return doc.object();
}
} // namespace QodeAssist::PluginLLMCore

118
pluginllmcore/Provider.hpp Normal file
View File

@ -0,0 +1,118 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <QFuture>
#include <utils/environment.h>
#include <QNetworkRequest>
#include <QObject>
#include <QString>
#include "ContextData.hpp"
#include "DataBuffers.hpp"
#include "HttpClient.hpp"
#include "IToolsManager.hpp"
#include "PromptTemplate.hpp"
#include "RequestType.hpp"
class QNetworkReply;
class QJsonObject;
namespace QodeAssist::PluginLLMCore {
class Provider : public QObject
{
Q_OBJECT
public:
explicit Provider(QObject *parent = nullptr);
virtual ~Provider() = default;
virtual QString name() const = 0;
virtual QString url() const = 0;
virtual QString completionEndpoint() const = 0;
virtual QString chatEndpoint() const = 0;
virtual bool supportsModelListing() const = 0;
virtual void prepareRequest(
QJsonObject &request,
PluginLLMCore::PromptTemplate *prompt,
PluginLLMCore::ContextData context,
PluginLLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
= 0;
virtual QFuture<QList<QString>> getInstalledModels(const QString &url) = 0;
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
virtual QString apiKey() const = 0;
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
virtual ProviderID providerID() const = 0;
virtual void sendRequest(const RequestID &requestId, const QUrl &url, const QJsonObject &payload)
= 0;
virtual bool supportsTools() const { return false; };
virtual bool supportThinking() const { return false; };
virtual bool supportImage() const { return false; };
virtual void cancelRequest(const RequestID &requestId);
virtual IToolsManager *toolsManager() const { return nullptr; }
HttpClient *httpClient() const;
public slots:
virtual void onDataReceived(
const QodeAssist::PluginLLMCore::RequestID &requestId, const QByteArray &data)
= 0;
virtual void onRequestFinished(
const QodeAssist::PluginLLMCore::RequestID &requestId, std::optional<QString> error)
= 0;
signals:
void partialResponseReceived(
const QodeAssist::PluginLLMCore::RequestID &requestId, const QString &partialText);
void fullResponseReceived(
const QodeAssist::PluginLLMCore::RequestID &requestId, const QString &fullText);
void requestFailed(const QodeAssist::PluginLLMCore::RequestID &requestId, const QString &error);
void toolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName);
void toolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &result);
void continuationStarted(const QodeAssist::PluginLLMCore::RequestID &requestId);
void thinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void redactedThinkingBlockReceived(const QString &requestId, const QString &signature);
protected:
QJsonObject parseEventLine(const QString &line);
QHash<RequestID, DataBuffers> m_dataBuffers;
QHash<RequestID, QUrl> m_requestUrls;
private:
HttpClient *m_httpClient;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
namespace QodeAssist::PluginLLMCore {
enum class ProviderID {
Any,
Ollama,
LMStudio,
Claude,
OpenAI,
OpenAICompatible,
OpenAIResponses,
MistralAI,
OpenRouter,
GoogleAI,
LlamaCpp
};
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ProvidersManager.hpp"
namespace QodeAssist::PluginLLMCore {
ProvidersManager &ProvidersManager::instance()
{
static ProvidersManager instance;
return instance;
}
QStringList ProvidersManager::providersNames() const
{
return m_providers.keys();
}
ProvidersManager::~ProvidersManager()
{
qDeleteAll(m_providers);
}
Provider *ProvidersManager::getProviderByName(const QString &providerName)
{
if (!m_providers.contains(providerName))
return m_providers.first();
return m_providers[providerName];
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include "IProviderRegistry.hpp"
#include <QMap>
namespace QodeAssist::PluginLLMCore {
class ProvidersManager : public IProviderRegistry
{
public:
static ProvidersManager &instance();
~ProvidersManager();
template<typename T>
void registerProvider()
{
static_assert(std::is_base_of<Provider, T>::value, "T must inherit from Provider");
T *provider = new T();
QString name = provider->name();
m_providers[name] = provider;
}
Provider *getProviderByName(const QString &providerName) override;
QStringList providersNames() const override;
private:
ProvidersManager() = default;
ProvidersManager(const ProvidersManager &) = delete;
ProvidersManager &operator=(const ProvidersManager &) = delete;
QMap<QString, Provider *> m_providers;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "PromptTemplate.hpp"
#include "Provider.hpp"
#include "RequestType.hpp"
#include <QJsonObject>
#include <QUrl>
namespace QodeAssist::PluginLLMCore {
struct LLMConfig
{
QUrl url;
Provider *provider;
PromptTemplate *promptTemplate;
QJsonObject providerRequest;
RequestType requestType;
bool multiLineCompletion;
QString apiKey;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RequestHandlerBase.hpp"
namespace QodeAssist::LLMCore {
RequestHandlerBase::RequestHandlerBase(QObject *parent)
: QObject(parent)
{}
RequestHandlerBase::~RequestHandlerBase() = default;
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "RequestConfig.hpp"
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class RequestHandlerBase : public QObject
{
Q_OBJECT
public:
explicit RequestHandlerBase(QObject *parent = nullptr);
~RequestHandlerBase() override;
virtual void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) = 0;
virtual bool cancelRequest(const QString &id) = 0;
signals:
void completionReceived(const QString &completion, const QJsonObject &request, bool isComplete);
void requestFinished(const QString &requestId, bool success, const QString &errorString);
void requestCancelled(const QString &id);
};
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include <QString>
#pragma once
namespace QodeAssist::PluginLLMCore {
enum RequestType { CodeCompletion, Chat, Embedding, QuickRefactoring };
using RequestID = QString;
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include <QStringList>
#include <QRegularExpression>
namespace QodeAssist::PluginLLMCore {
class ResponseCleaner
{
public:
static QString clean(const QString &response)
{
QString cleaned = removeCodeBlocks(response);
cleaned = trimWhitespace(cleaned);
cleaned = removeExplanations(cleaned);
return cleaned;
}
private:
static QString removeCodeBlocks(const QString &text)
{
if (!text.contains("```")) {
return text;
}
QRegularExpression codeBlockRegex("```\\w*\\n([\\s\\S]*?)```");
QRegularExpressionMatch match = codeBlockRegex.match(text);
if (match.hasMatch()) {
return match.captured(1);
}
int firstFence = text.indexOf("```");
int lastFence = text.lastIndexOf("```");
if (firstFence != -1 && lastFence > firstFence) {
int firstNewLine = text.indexOf('\n', firstFence);
if (firstNewLine != -1) {
return text.mid(firstNewLine + 1, lastFence - firstNewLine - 1);
}
}
return text;
}
static QString trimWhitespace(const QString &text)
{
QString result = text;
while (result.startsWith('\n') || result.startsWith('\r')) {
result = result.mid(1);
}
while (result.endsWith('\n') || result.endsWith('\r')) {
result.chop(1);
}
return result;
}
static QString removeExplanations(const QString &text)
{
static const QStringList explanationPrefixes = {
"here's the", "here is the", "here's", "here is",
"the refactored", "refactored code:", "code:",
"i've refactored", "i refactored", "i've changed", "i changed"
};
QStringList lines = text.split('\n');
int startLine = 0;
for (int i = 0; i < qMin(3, lines.size()); ++i) {
QString line = lines[i].trimmed().toLower();
bool isExplanation = false;
for (const QString &prefix : explanationPrefixes) {
if (line.startsWith(prefix) || line.contains(prefix + " code")) {
isExplanation = true;
break;
}
}
if (line.length() < 50 && line.endsWith(':')) {
isExplanation = true;
}
if (isExplanation) {
startLine = i + 1;
} else if (!line.isEmpty()) {
break;
}
}
if (startLine > 0 && startLine < lines.size()) {
lines = lines.mid(startLine);
return lines.join('\n');
}
return text;
}
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,181 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RulesLoader.hpp"
#include <QDir>
#include <QFile>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
namespace QodeAssist::PluginLLMCore {
QString RulesLoader::loadRules(const QString &projectPath, RulesContext context)
{
if (projectPath.isEmpty()) {
return QString();
}
QString combined;
QString basePath = projectPath + "/.qodeassist/rules";
switch (context) {
case RulesContext::Completions:
combined += loadAllMarkdownFiles(basePath + "/completions");
break;
case RulesContext::Chat:
combined += loadAllMarkdownFiles(basePath + "/common");
combined += loadAllMarkdownFiles(basePath + "/chat");
break;
case RulesContext::QuickRefactor:
combined += loadAllMarkdownFiles(basePath + "/common");
combined += loadAllMarkdownFiles(basePath + "/quickrefactor");
break;
}
return combined;
}
QString RulesLoader::loadRulesForProject(ProjectExplorer::Project *project, RulesContext context)
{
if (!project) {
return QString();
}
QString projectPath = getProjectPath(project);
return loadRules(projectPath, context);
}
ProjectExplorer::Project *RulesLoader::getActiveProject()
{
auto currentEditor = Core::EditorManager::currentEditor();
if (currentEditor && currentEditor->document()) {
Utils::FilePath filePath = currentEditor->document()->filePath();
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project) {
return project;
}
}
return ProjectExplorer::ProjectManager::startupProject();
}
QString RulesLoader::loadAllMarkdownFiles(const QString &dirPath)
{
QString combined;
QDir dir(dirPath);
if (!dir.exists()) {
return QString();
}
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
for (const QString &fileName : mdFiles) {
QFile file(dir.filePath(fileName));
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
combined += file.readAll();
combined += "\n\n";
}
}
return combined;
}
QString RulesLoader::getProjectPath(ProjectExplorer::Project *project)
{
if (!project) {
return QString();
}
return project->projectDirectory().toUrlishString();
}
QVector<RuleFileInfo> RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context)
{
if (projectPath.isEmpty()) {
return QVector<RuleFileInfo>();
}
QVector<RuleFileInfo> result;
QString basePath = projectPath + "/.qodeassist/rules";
// Always include common rules
result.append(collectMarkdownFiles(basePath + "/common", "common"));
// Add context-specific rules
switch (context) {
case RulesContext::Completions:
result.append(collectMarkdownFiles(basePath + "/completions", "completions"));
break;
case RulesContext::Chat:
result.append(collectMarkdownFiles(basePath + "/chat", "chat"));
break;
case RulesContext::QuickRefactor:
result.append(collectMarkdownFiles(basePath + "/quickrefactor", "quickrefactor"));
break;
}
return result;
}
QVector<RuleFileInfo> RulesLoader::getRuleFilesForProject(
ProjectExplorer::Project *project, RulesContext context)
{
if (!project) {
return QVector<RuleFileInfo>();
}
QString projectPath = getProjectPath(project);
return getRuleFiles(projectPath, context);
}
QString RulesLoader::loadRuleFileContent(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return QString();
}
return file.readAll();
}
QVector<RuleFileInfo> RulesLoader::collectMarkdownFiles(
const QString &dirPath, const QString &category)
{
QVector<RuleFileInfo> result;
QDir dir(dirPath);
if (!dir.exists()) {
return result;
}
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
for (const QString &fileName : mdFiles) {
QString fullPath = dir.filePath(fileName);
result.append({fullPath, fileName, category});
}
return result;
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::PluginLLMCore {
enum class RulesContext { Completions, Chat, QuickRefactor };
struct RuleFileInfo
{
QString filePath;
QString fileName;
QString category; // "common", "chat", "completions", "quickrefactor"
};
class RulesLoader
{
public:
static QString loadRules(const QString &projectPath, RulesContext context);
static QString loadRulesForProject(ProjectExplorer::Project *project, RulesContext context);
static ProjectExplorer::Project *getActiveProject();
// New methods for getting rule files info
static QVector<RuleFileInfo> getRuleFiles(const QString &projectPath, RulesContext context);
static QVector<RuleFileInfo> getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context);
static QString loadRuleFileContent(const QString &filePath);
private:
static QString loadAllMarkdownFiles(const QString &dirPath);
static QVector<RuleFileInfo> collectMarkdownFiles(const QString &dirPath, const QString &category);
static QString getProjectPath(ProjectExplorer::Project *project);
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "SSEBuffer.hpp"
namespace QodeAssist::PluginLLMCore {
QStringList SSEBuffer::processData(const QByteArray &data)
{
m_buffer += QString::fromUtf8(data);
QStringList lines = m_buffer.split('\n');
m_buffer = lines.takeLast();
lines.removeAll(QString());
return lines;
}
void SSEBuffer::clear()
{
m_buffer.clear();
}
QString SSEBuffer::currentBuffer() const
{
return m_buffer;
}
bool SSEBuffer::hasIncompleteData() const
{
return !m_buffer.isEmpty();
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include <QStringList>
namespace QodeAssist::PluginLLMCore {
class SSEBuffer
{
public:
SSEBuffer() = default;
QStringList processData(const QByteArray &data);
void clear();
QString currentBuffer() const;
bool hasIncompleteData() const;
private:
QString m_buffer;
};
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ValidationUtils.hpp"
#include <QJsonArray>
namespace QodeAssist::PluginLLMCore {
QStringList ValidationUtils::validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj)
{
QStringList errors;
validateFields(request, templateObj, errors);
validateNestedObjects(request, templateObj, errors);
return errors;
}
void ValidationUtils::validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (!templateObj.contains(it.key())) {
errors << QString("unknown field '%1'").arg(it.key());
}
}
}
void ValidationUtils::validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (templateObj.contains(it.key()) && it.value().isObject()
&& templateObj[it.key()].isObject()) {
validateFields(it.value().toObject(), templateObj[it.key()].toObject(), errors);
validateNestedObjects(it.value().toObject(), templateObj[it.key()].toObject(), errors);
}
}
}
} // namespace QodeAssist::PluginLLMCore

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QStringList>
namespace QodeAssist::PluginLLMCore {
class ValidationUtils
{
public:
static QStringList validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj);
private:
static void validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
static void validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
};
} // namespace QodeAssist::PluginLLMCore