From 851e681cf50558c1225fcd296269a6ebd2926a98 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Sat, 30 Aug 2025 18:34:10 +0200 Subject: [PATCH] feat: Add new http client --- llmcore/CMakeLists.txt | 1 + llmcore/HttpClient.cpp | 150 +++++++++++++++++++++++++++++++++++++++++ llmcore/HttpClient.hpp | 69 +++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 llmcore/HttpClient.cpp create mode 100644 llmcore/HttpClient.hpp diff --git a/llmcore/CMakeLists.txt b/llmcore/CMakeLists.txt index 2aaf91a..151f9ac 100644 --- a/llmcore/CMakeLists.txt +++ b/llmcore/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(LLMCore STATIC OpenAIMessage.hpp OpenAIMessage.cpp ValidationUtils.hpp ValidationUtils.cpp ProviderID.hpp + HttpClient.hpp HttpClient.cpp ) target_link_libraries(LLMCore diff --git a/llmcore/HttpClient.cpp b/llmcore/HttpClient.cpp new file mode 100644 index 0000000..1e41044 --- /dev/null +++ b/llmcore/HttpClient.cpp @@ -0,0 +1,150 @@ +/* + * 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 . + */ + +#include "HttpClient.hpp" + +#include +#include +#include + +#include + +namespace QodeAssist::LLMCore { + +HttpClient::HttpClient(QObject *parent) + : QObject(parent) + , m_manager(new QNetworkAccessManager(this)) +{ + connect(this, &HttpClient::sendRequest, this, &HttpClient::onSendRequest); +} + +HttpClient::~HttpClient() +{ + QMutexLocker locker(&m_mutex); + for (auto *reply : std::as_const(m_activeRequests)) { + reply->abort(); + reply->deleteLater(); + } + m_activeRequests.clear(); +} + +void HttpClient::onSendRequest(const HttpRequest &request) +{ + QNetworkRequest networkRequest(request.url); + networkRequest.setTransferTimeout(300000); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + networkRequest.setRawHeader("Accept", "text/event-stream"); + networkRequest.setRawHeader("Cache-Control", "no-cache"); + networkRequest.setRawHeader("Connection", "keep-alive"); + + if (request.headers.has_value()) { + for (const auto &[headername, value] : request.headers->asKeyValueRange()) { + networkRequest.setRawHeader(headername.toUtf8(), value.toUtf8()); + } + } + + QJsonDocument doc(request.payload); + LOG_MESSAGE(QString("HttpClient: Sending POST to %1").arg(request.url.toString())); + LOG_MESSAGE(QString("HttpClient: data: %1").arg(doc.toJson(QJsonDocument::Indented))); + + QNetworkReply *reply = m_manager->post(networkRequest, doc.toJson(QJsonDocument::Compact)); + addActiveRequest(reply, request.requestId); + + connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead); + connect(reply, &QNetworkReply::finished, this, &HttpClient::onFinished); +} + +void HttpClient::onReadyRead() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) + return; + + QString requestId; + { + QMutexLocker locker(&m_mutex); + for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { + if (it.value() == reply) { + requestId = it.key(); + break; + } + } + } + + if (requestId.isEmpty()) + return; + + QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + emit dataReceived(requestId, data); + } +} + +void HttpClient::onFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) + return; + + QString requestId; + bool hasError = false; + QString errorMsg; + { + 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 (reply->error() != QNetworkReply::NoError) { + hasError = true; + errorMsg = reply->errorString(); + } + } + + reply->deleteLater(); + + if (!requestId.isEmpty()) { + emit requestFinished(requestId, !hasError, errorMsg); + } +} + +QString 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)); + return requestId; +} + +void HttpClient::cancelRequest(const QString &requestId) +{ + QMutexLocker locker(&m_mutex); + if (auto it = m_activeRequests.find(requestId); it != m_activeRequests.end()) { + it.value()->abort(); + it.value()->deleteLater(); + m_activeRequests.erase(it); + LOG_MESSAGE(QString("HttpClient: Cancelled request: %1").arg(requestId)); + } +} + +} // namespace QodeAssist::LLMCore diff --git a/llmcore/HttpClient.hpp b/llmcore/HttpClient.hpp new file mode 100644 index 0000000..18ac023 --- /dev/null +++ b/llmcore/HttpClient.hpp @@ -0,0 +1,69 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QodeAssist::LLMCore { + +struct HttpRequest +{ + QUrl url; + QString requestId; + QJsonObject payload; + std::optional> headers; +}; + +class HttpClient : public QObject +{ + Q_OBJECT + +public: + HttpClient(QObject *parent = nullptr); + ~HttpClient(); + + void cancelRequest(const QString &requestId); + +signals: + void sendRequest(const QodeAssist::LLMCore::HttpRequest &request); + void dataReceived(const QString &requestId, const QByteArray &data); + void requestFinished(const QString &requestId, bool success, const QString &error); + +private slots: + void onSendRequest(const QodeAssist::LLMCore::HttpRequest &request); + void onReadyRead(); + void onFinished(); + +private: + QString addActiveRequest(QNetworkReply *reply, const QString &requestId); + + QNetworkAccessManager *m_manager; + QHash m_activeRequests; + mutable QMutex m_mutex; +}; + +} // namespace QodeAssist::LLMCore