From ee37ed47bb26d9b98eef87839856b6ee433cf88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A7=CE=B3=CF=86=CF=84=20Kompanion?= Date: Sat, 18 Oct 2025 13:55:53 +0200 Subject: [PATCH] Finish the kllm integration better Thanks to Gemini we now have a cleaner build interface --- CMakeLists.txt | 56 +-------- src/CMakeLists.txt | 6 + src/KompanionAI/CMakeLists.txt | 5 +- src/cli/CMakeLists.txt | 12 ++ src/gui/.CMakeLists.txt.swp | Bin 0 -> 12288 bytes src/gui/CMakeLists.txt | 17 +++ src/gui/MainWindow.cpp | 120 ++++++++++++++++++ src/kllm/KIChatOptions.h | 39 ------ src/kllm/KIEmbedOptions.h | 24 ---- src/kllm/KIEmbeddingResult.h | 23 ---- src/kllm/KIError.h | 44 ------- src/kllm/KIMessage.h | 48 -------- src/kllm/KIPolicy.h | 28 ----- src/kllm/KIProvider.h | 43 ------- src/kllm/KIReply.h | 53 -------- src/kllm/KIToolSpec.h | 65 ---------- src/kllm/KLLMConstants.cpp | 10 -- src/kllm/KLLMConstants.h | 16 --- src/kllm/KLLMContext.cpp | 31 ----- src/kllm/KLLMContext.h | 48 -------- src/kllm/KLLMInterface.cpp | 126 ------------------- src/kllm/KLLMInterface.h | 43 ------- src/kllm/KLLMOriginalInterface.cpp | 190 ---------------------------- src/kllm/KLLMOriginalInterface.h | 192 ----------------------------- src/kllm/KLLMReply.cpp | 106 ---------------- src/kllm/KLLMReply.h | 154 ----------------------- src/kllm/KLLMRequest.cpp | 49 -------- src/kllm/KLLMRequest.h | 80 ------------ src/mcp/CMakeLists.txt | 29 +++++ src/mcp/HandlersIntrospection.hpp | 5 + src/mcp/HandlersMemory.hpp | 2 +- src/mcp/RegisterTools.hpp | 1 + src/mcp/Tools.hpp | 5 + 33 files changed, 205 insertions(+), 1465 deletions(-) create mode 100644 src/CMakeLists.txt create mode 100644 src/cli/CMakeLists.txt create mode 100644 src/gui/.CMakeLists.txt.swp create mode 100644 src/gui/CMakeLists.txt create mode 100644 src/gui/MainWindow.cpp delete mode 100644 src/kllm/KIChatOptions.h delete mode 100644 src/kllm/KIEmbedOptions.h delete mode 100644 src/kllm/KIEmbeddingResult.h delete mode 100644 src/kllm/KIError.h delete mode 100644 src/kllm/KIMessage.h delete mode 100644 src/kllm/KIPolicy.h delete mode 100644 src/kllm/KIProvider.h delete mode 100644 src/kllm/KIReply.h delete mode 100644 src/kllm/KIToolSpec.h delete mode 100644 src/kllm/KLLMConstants.cpp delete mode 100644 src/kllm/KLLMConstants.h delete mode 100644 src/kllm/KLLMContext.cpp delete mode 100644 src/kllm/KLLMContext.h delete mode 100644 src/kllm/KLLMInterface.cpp delete mode 100644 src/kllm/KLLMInterface.h delete mode 100644 src/kllm/KLLMOriginalInterface.cpp delete mode 100644 src/kllm/KLLMOriginalInterface.h delete mode 100644 src/kllm/KLLMReply.cpp delete mode 100644 src/kllm/KLLMReply.h delete mode 100644 src/kllm/KLLMRequest.cpp delete mode 100644 src/kllm/KLLMRequest.h create mode 100644 src/mcp/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bc5c60..d0ae5f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,6 @@ include(KDEClangFormat) include(ECMDeprecationSettings) include(KDEGitCommitHooks) - find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Network @@ -36,14 +35,11 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS find_package(Qt6McpServer CONFIG REQUIRED) find_package(Qt6McpCommon CONFIG REQUIRED) -option(KOMPANION_USE_GUI "Build optional GUI components using Qt6Gui" ON) -if (KOMPANION_USE_GUI) - find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Gui) -endif() +find_package(Qt6 ${KF6_MIN_VERSON} CONFIG REQUIRED COMPONENTS Gui) +find_package(KF6Config ${KF6_MIN_VERSION} CONFIG REQUIRED) +find_package(KF6Parts ${KF6_MIN_VERSION} CONFIG REQUIRED) +find_package(KF6TextEditor ${KF6_MIN_VERSION} CONFIG REQUIRED) -find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS - Config -) find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET) set_package_properties(Qt6Test PROPERTIES PURPOSE "Required for tests" @@ -55,52 +51,10 @@ add_feature_info("Qt6Test" Qt6Test_FOUND "Required for building tests") set(KOMPANION_DB_INIT_INSTALL_DIR "${KDE_INSTALL_FULL_DATADIR}/kompanion/db/init") install(DIRECTORY db/init/ DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/db/init FILES_MATCHING PATTERN "*.sql") -add_subdirectory(src/dal) -add_subdirectory(src/KompanionAI) - -add_executable(kom_mcp - src/main.cpp - src/mcp/KompanionQtServer.cpp -) -target_include_directories(kom_mcp PRIVATE src) -qt_add_resources(kom_mcp kompanion_mcp_resources - PREFIX "/kompanion" - BASE "src/mcp" - FILES src/mcp/ToolSchemas.json -) -target_link_libraries(kom_mcp PRIVATE - kom_dal - kom_ai - KF6::ConfigCore - Qt6::Core - Qt6::Network - Qt6::McpServer - Qt6::McpCommon -) -target_compile_options(kom_mcp PRIVATE -fexceptions) -target_compile_definitions(kom_mcp PRIVATE - PROJECT_SOURCE_DIR="${CMAKE_SOURCE_DIR}" - KOMPANION_DB_INIT_INSTALL_DIR="${KOMPANION_DB_INIT_INSTALL_DIR}" -) - -install(TARGETS kom_mcp RUNTIME DESTINATION bin) -install(FILES src/mcp/ToolSchemas.json DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/mcp) +add_subdirectory(src) option(BUILD_TESTS "Build tests" ON) -add_executable(kompanion -src/cli/KompanionApp.cpp -) -target_include_directories(kompanion PRIVATE src) -target_link_libraries(kompanion PRIVATE -Qt6::Core -Qt6::Sql -KF6::ConfigCore -kom_dal -kom_ai -) -install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) - if (BUILD_TESTS) enable_testing() add_subdirectory(tests) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..b2fba5c --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(cli) +add_subdirectory(dal) +add_subdirectory(gui) + +add_subdirectory(KompanionAI) +add_subdirectory(mcp) diff --git a/src/KompanionAI/CMakeLists.txt b/src/KompanionAI/CMakeLists.txt index 794da25..99a5557 100644 --- a/src/KompanionAI/CMakeLists.txt +++ b/src/KompanionAI/CMakeLists.txt @@ -22,7 +22,10 @@ set(KOMPANION_AI_HDRS add_library(kom_ai STATIC ${KOMPANION_AI_SRCS} ${KOMPANION_AI_HDRS}) -target_include_directories(kom_ai PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(kom_ai PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${Qt6Core_INCLUDE_DIRS} +) target_link_libraries(kom_ai PUBLIC Qt6::Core diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt new file mode 100644 index 0000000..5efd624 --- /dev/null +++ b/src/cli/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(kompanion +KompanionApp.cpp +) +target_include_directories(kompanion PRIVATE ../) +target_link_libraries(kompanion PRIVATE +Qt6::Core +Qt6::Sql +KF6::ConfigCore +kom_dal +kom_ai +) +install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/gui/.CMakeLists.txt.swp b/src/gui/.CMakeLists.txt.swp new file mode 100644 index 0000000000000000000000000000000000000000..675b6e387d05b4d1e7f02b4da3445f48184915ef GIT binary patch literal 12288 zcmeI&O>Wab7zW@;S)u|04lqC^l0}miBviVK9JeyXapE{-X~y;>jN(7plM++`V$U5| z!7>-rLu*FC|Ri!TL zdZPc>90d@7zt`IKmY;|fB*y_009X6cLF-E zu($M78;d8qy*O5WIWFGf009U<00Izz00bZa0SG_<0uX?}KPVs~#=dMaR=q|yPyhcf zfB&CdXY3Q@Bjp_>qI4+^xe>^Op9VDwI8c=L@z-h3MI?x`FrlT)^XD_*|J!rVLXncAE`oy)N}0> zeCxF1I!{c)^LtL;HB7%{xv!`D)@zH$wS`KbsWi%7*26q!B8q%Dk>Oa2AeQHEgI)Bo yTZY{{GX0)wOk2-8vjH{qa}kcjP*#a)<;D%CePA7O*X;H!*KE};DxG`&Uit&zkIq8? literal 0 HcmV?d00001 diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt new file mode 100644 index 0000000..a6db934 --- /dev/null +++ b/src/gui/CMakeLists.txt @@ -0,0 +1,17 @@ +include_directories($CMAKE_SRC_DIR/src) +add_executable(kompanion_gui + MainWindow.cpp +) +target_include_directories(kompanion_gui PRIVATE ${PROJECT_SOURCE_DIR}/src) + +target_link_libraries(kompanion_gui PRIVATE + KF6::Parts + KF6::TextEditor + KF6::ConfigCore + Qt6::McpServer + Qt6::McpCommon + kom_dal + kom_ai +) + +install(TARGETS kompanion_gui RUNTIME DESTINATION bin) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp new file mode 100644 index 0000000..713eee9 --- /dev/null +++ b/src/gui/MainWindow.cpp @@ -0,0 +1,120 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +class MainWindow : public KParts::MainWindow +{ +public: + MainWindow(QWidget *parent = nullptr) : KParts::MainWindow(parent) + { + QWidget *mainWidget = new QWidget(this); + setCentralWidget(mainWidget); + + QVBoxLayout *layout = new QVBoxLayout(mainWidget); + + auto editor = KTextEditor::Editor::instance(); + m_document = editor->createDocument(this); + m_chatView = m_document->createView(mainWidget); + m_document->setReadWrite(false); + layout->addWidget(m_chatView); + + m_chatInput = new QLineEdit(mainWidget); + layout->addWidget(m_chatInput); + + QPushButton *sendButton = new QPushButton("Send", mainWidget); + layout->addWidget(sendButton); + + connect(sendButton, &QPushButton::clicked, this, &MainWindow::sendMessage); + + // Setup KompanionAI + m_ollamaProvider = new KompanionAI::OllamaProvider(this); + m_kompanionClient = new KompanionAI::KIClient(this); + m_kompanionClient->setProvider(m_ollamaProvider); + m_kompanionClient->setDefaultModel("llama2"); // Or some other default + } + +private slots: + void sendMessage() + { + const QString message = m_chatInput->text(); + if (message.isEmpty()) { + return; + } + + m_chatInput->clear(); + + // Append user message to chat view + insertText(QString("User: %1\n").arg(message)); + + // Send message to KompanionAI + KompanionAI::KIThread thread; + KompanionAI::KIMessage kimessage; + kimessage.role = "user"; + KompanionAI::KIMessagePart part; + part.mime = "text/plain"; + part.text = message; + kimessage.parts.append(part); + thread.messages.append(kimessage); + + KompanionAI::KIChatOptions opts; + opts.model = m_kompanionClient->defaultModel(); + + QFuture future = m_kompanionClient->chat(thread, opts); + + QFutureWatcher *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher]() { + KompanionAI::KIReply* reply = watcher->result(); + connect(reply, &KompanionAI::KIReply::tokensAdded, this, [this](const QString& delta) { + insertText(delta); + }); + + connect(reply, &KompanionAI::KIReply::finished, this, [this, reply]() { + insertText("\n"); + reply->deleteLater(); + }); + watcher->deleteLater(); + }); + watcher->setFuture(future); + } + +private: + void insertText(const QString &text) + { + KTextEditor::Cursor endCursor(m_document->lines() - 1, m_document->lineLength(m_document->lines() - 1)); + m_document->insertText(endCursor, text); + } + +private: + KTextEditor::View *m_chatView; + KTextEditor::Document *m_document; + QLineEdit *m_chatInput; + + KompanionAI::KIClient* m_kompanionClient; + KompanionAI::OllamaProvider* m_ollamaProvider; +}; + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + MainWindow window; + window.show(); + + return app.exec(); +} \ No newline at end of file diff --git a/src/kllm/KIChatOptions.h b/src/kllm/KIChatOptions.h deleted file mode 100644 index 66e27d9..0000000 --- a/src/kllm/KIChatOptions.h +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include -#include - -#include "KIToolSpec.h" -#include "KIPolicy.h" - -namespace KompanionAI -{ - -class KIChatOptions -{ - Q_GADGET - Q_PROPERTY(QString model MEMBER model) - Q_PROPERTY(bool stream MEMBER stream) - Q_PROPERTY(bool jsonMode MEMBER jsonMode) - Q_PROPERTY(int maxTokens MEMBER maxTokens) - Q_PROPERTY(double temperature MEMBER temperature) - Q_PROPERTY(QList tools MEMBER tools) - Q_PROPERTY(KIPolicy policy MEMBER policy) - -public: - QString model; - bool stream = true; - bool jsonMode = false; - int maxTokens = 512; - double temperature = 0.2; - QList tools; - KIPolicy policy; -}; - -} diff --git a/src/kllm/KIEmbedOptions.h b/src/kllm/KIEmbedOptions.h deleted file mode 100644 index 44c4640..0000000 --- a/src/kllm/KIEmbedOptions.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include - -namespace KompanionAI -{ - -class KIEmbedOptions -{ - Q_GADGET - Q_PROPERTY(QString model MEMBER model) - Q_PROPERTY(QString normalize MEMBER normalize) - -public: - QString model = "text-embed-local"; - QString normalize = "l2"; -}; - -} diff --git a/src/kllm/KIEmbeddingResult.h b/src/kllm/KIEmbeddingResult.h deleted file mode 100644 index e649e3d..0000000 --- a/src/kllm/KIEmbeddingResult.h +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include - -namespace KompanionAI -{ - -class KIEmbeddingResult -{ - Q_GADGET - -public: - QVector> vectors; - QString model; -}; - -} diff --git a/src/kllm/KIError.h b/src/kllm/KIError.h deleted file mode 100644 index 8c89259..0000000 --- a/src/kllm/KIError.h +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include - -namespace KompanionAI -{ - -class KIError -{ -public: - enum ErrorCode { - NoError, - NetworkError, - ApiError, - InvalidJson, - Cancelled - }; - - KIError(ErrorCode code = NoError, const QString &message = QString(), int httpStatus = 0, int retryAfter = 0) - : m_code(code) - , m_message(message) - , m_httpStatus(httpStatus) - , m_retryAfter(retryAfter) - { - } - - ErrorCode code() const { return m_code; } - QString message() const { return m_message; } - int httpStatus() const { return m_httpStatus; } - int retryAfter() const { return m_retryAfter; } - -private: - ErrorCode m_code; - QString m_message; - int m_httpStatus; - int m_retryAfter; -}; - -} diff --git a/src/kllm/KIMessage.h b/src/kllm/KIMessage.h deleted file mode 100644 index 670cde2..0000000 --- a/src/kllm/KIMessage.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include -#include -#include - -namespace KompanionAI -{ - -class KIMessagePart -{ - Q_GADGET - Q_PROPERTY(QString mime READ mime) - Q_PROPERTY(QString text READ text) - -public: - QString mime; - QString text; -}; - -class KIMessage -{ - Q_GADGET - Q_PROPERTY(QString role READ role) - Q_PROPERTY(QList parts READ parts) - -public: - QString role; - QList parts; - QVariantMap metadata; -}; - -class KIThread -{ - Q_GADGET - Q_PROPERTY(QList messages READ messages) - -public: - QList messages; -}; - -} \ No newline at end of file diff --git a/src/kllm/KIPolicy.h b/src/kllm/KIPolicy.h deleted file mode 100644 index 1c89991..0000000 --- a/src/kllm/KIPolicy.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include -#include - -namespace KompanionAI -{ - -class KIPolicy -{ - Q_GADGET - Q_PROPERTY(QString visibility MEMBER visibility) - Q_PROPERTY(bool allowNetwork MEMBER allowNetwork) - Q_PROPERTY(QStringList redactions MEMBER redactions) - -public: - QString visibility = "private"; - bool allowNetwork = false; - QStringList redactions; -}; - -} diff --git a/src/kllm/KIProvider.h b/src/kllm/KIProvider.h deleted file mode 100644 index b4247d4..0000000 --- a/src/kllm/KIProvider.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include -#include - -#include "KIReply.h" -#include "KIThread.h" -#include "KIChatOptions.h" -#include "KIEmbeddingResult.h" -#include "KIEmbedOptions.h" -#include "KICapabilities.h" - -namespace KompanionAI -{ - -class KIProvider : public QObject -{ - Q_OBJECT - Q_PROPERTY(QString name READ name CONSTANT) - Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged) - Q_PROPERTY(KICapabilities caps READ caps CONSTANT) - -public: - explicit KIProvider(QObject *parent = nullptr); - - virtual QString name() const = 0; - virtual QStringList models() const = 0; - virtual KICapabilities caps() const = 0; - - virtual QFuture chat(const KIThread &thread, const KIChatOptions &opts) = 0; - virtual QFuture embed(const QStringList &texts, const KIEmbedOptions &opts) = 0; - -Q_SIGNALS: - void modelsChanged(); -}; - -} diff --git a/src/kllm/KIReply.h b/src/kllm/KIReply.h deleted file mode 100644 index 127960d..0000000 --- a/src/kllm/KIReply.h +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include -#include - -#include "KIToolSpec.h" - -namespace KompanionAI -{ - -class KIError; - -class KIReply : public QObject -{ - Q_OBJECT - Q_PROPERTY(bool finished READ isFinished NOTIFY finishedChanged) - Q_PROPERTY(int promptTokens READ promptTokens CONSTANT) - Q_PROPERTY(int completionTokens READ completionTokens CONSTANT) - Q_PROPERTY(QString model READ model CONSTANT) - -public: - explicit KIReply(QObject *parent = nullptr); - - Q_INVOKABLE QString text() const; - bool isFinished() const; - int promptTokens() const; - int completionTokens() const; - QString model() const; - -Q_SIGNALS: - void tokensAdded(const QString &delta); - void toolCallProposed(const KIToolCall &call); - void toolResultRequested(const KIToolCall &call); - void traceEvent(const QVariantMap &span); - void finished(); - void errorOccurred(const KIError &error); - void finishedChanged(); - -private: - bool m_finished = false; - int m_promptTokens = 0; - int m_completionTokens = 0; - QString m_model; - QString m_text; -}; - -} \ No newline at end of file diff --git a/src/kllm/KIToolSpec.h b/src/kllm/KIToolSpec.h deleted file mode 100644 index ba9fa08..0000000 --- a/src/kllm/KIToolSpec.h +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include -#include -#include -#include -#include - -namespace KompanionAI -{ - -class KIToolParam -{ - Q_GADGET - Q_PROPERTY(QString name READ name) - Q_PROPERTY(QString type READ type) - Q_PROPERTY(bool required READ required) - Q_PROPERTY(QVariant defaultValue READ defaultValue) - -public: - QString name, type; - bool required = false; - QVariant defaultValue; -}; - -class KIToolSpec -{ - Q_GADGET - Q_PROPERTY(QString name READ name) - Q_PROPERTY(QString description READ description) - Q_PROPERTY(QList params READ params) - -public: - QString name, description; - QList params; -}; - -class KIToolCall -{ - Q_GADGET - Q_PROPERTY(QString name READ name) - Q_PROPERTY(QVariantMap arguments READ arguments) - -public: - QString name; - QVariantMap arguments; -}; - -class KIToolResult -{ - Q_GADGET - Q_PROPERTY(QString name READ name) - Q_PROPERTY(QVariant result READ result) - -public: - QString name; - QVariant result; -}; - -} \ No newline at end of file diff --git a/src/kllm/KLLMConstants.cpp b/src/kllm/KLLMConstants.cpp deleted file mode 100644 index 19493c8..0000000 --- a/src/kllm/KLLMConstants.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2023-2024 Laurent Montel -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include "KLLMConstants.h" - -QUrl KLLMCore::ollamaUrl() -{ - return QUrl(QStringLiteral("http://127.0.0.1:11434")); -} diff --git a/src/kllm/KLLMConstants.h b/src/kllm/KLLMConstants.h deleted file mode 100644 index 7a306b9..0000000 --- a/src/kllm/KLLMConstants.h +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2023-2024 Laurent Montel -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once -#include "kllmcore_export.h" -#include -namespace KLLMCore -{ -/** - * @brief Return default Ollama Url - * @return default Ollama Url. - */ -[[nodiscard]] KLLMCORE_EXPORT QUrl ollamaUrl(); - -} diff --git a/src/kllm/KLLMContext.cpp b/src/kllm/KLLMContext.cpp deleted file mode 100644 index 99481ee..0000000 --- a/src/kllm/KLLMContext.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include "KLLMContext.h" - -#include - -using namespace KLLMCore; - -QJsonValue KLLMContext::toJson() const -{ - switch (m_backend) { - case Backend::Ollama: - return m_data.value(); - default: - return {}; - } -} - -void KLLMContext::setOllamaContext(const QJsonArray &context) -{ - m_data = context; - m_backend = Backend::Ollama; -} - -QDebug operator<<(QDebug d, const KLLMContext &t) -{ - d << "m_data " << t.toJson(); - return d; -} diff --git a/src/kllm/KLLMContext.h b/src/kllm/KLLMContext.h deleted file mode 100644 index 1129268..0000000 --- a/src/kllm/KLLMContext.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once -#include "kllmcore_export.h" -#include -#include - -namespace KLLMCore -{ -/** - * @brief KLLMContext provides a representation of a conversation context. - * - * Most, if not all, LLMs have the concept of "context". This allows them to refer to previous messages in a conversation to - * enhance their replies. In most scenarios, this is the preferred behavior. - * - * To use KLLMContext, you simply need to get the context from each KLLMReply and set it on the next KLLMReqeust that you - * send. KLLMInterface will use this in KLLMInterface::getCompletion(). - */ -struct KLLMCORE_EXPORT KLLMContext { - /** - * @brief Converts the context to a JSON representation. - * - * Different LLM backends represent context in different ways; for example, while Ollama represents context as an array - * of integer identifiers, OpenAI relies on a JSON array of all the messages in the conversation so far. Therefore, this - * function exists to take any representation set on it for any backend and convert it to a JSON value suitable for - * sending in a request. - * - * @return A JSON representation of the context. - */ - [[nodiscard]] QJsonValue toJson() const; - - /** - * @brief Sets an Ollama context as the current context. - * @param context The context from Ollama. - */ - void setOllamaContext(const QJsonArray &context); - -private: - enum class Backend { - Ollama, - } m_backend; - QVariant m_data; -}; -} -KLLMCORE_EXPORT QDebug operator<<(QDebug d, const KLLMCore::KLLMContext &t); diff --git a/src/kllm/KLLMInterface.cpp b/src/kllm/KLLMInterface.cpp deleted file mode 100644 index 9f36725..0000000 --- a/src/kllm/KLLMInterface.cpp +++ /dev/null @@ -1,126 +0,0 @@ - -#include "KLLMInterface.h" -#include - -using namespace Qt::StringLiterals; -using namespace KLLMCore; - -KLLMInterface::KLLMInterface(QObject *parent) - : KLLMInterface{QString{}, parent} -{ -} - -KLLMInterface::KLLMInterface(const QString &ollamaUrl, QObject *parent) - : KLLMOriginalInterface{ollamaUrl, parent} - , m_kompanionClient{new KompanionAI::KIClient(this)} - , m_ollamaProvider{new KompanionAI::OllamaProvider(this)} -{ - m_kompanionClient->setProvider(m_ollamaProvider); - setOllamaUrl(ollamaUrl); -} - -KLLMInterface::KLLMInterface(const QUrl &ollamaUrl, QObject *parent) - : KLLMInterface{ollamaUrl.toString(), parent} -{ -} - -bool KLLMInterface::ready() const -{ - // For now, assume ready if provider is set and has models - return m_kompanionClient->provider() != nullptr && !m_ollamaProvider->models().isEmpty(); -} - -bool KLLMInterface::hasError() const -{ - // TODO: Implement proper error checking from KompanionAI client - return false; -} - -QStringList KLLMInterface::models() const -{ - return m_ollamaProvider->models(); -} - -KLLMReply *KLLMInterface::getCompletion(const KLLMRequest &request) -{ - KompanionAI::KIThread thread; - KompanionAI::KIMessage message; - message.role = "user"; - KompanionAI::KIMessagePart part; - part.mime = "text/plain"; - part.text = request.message(); - message.parts.append(part); - thread.messages.append(message); - - KompanionAI::KIChatOptions opts; - opts.model = request.model().isEmpty() ? m_kompanionClient->defaultModel() : request.model(); - // TODO: Map KLLMContext to KompanionAI thread/options - - auto kiReply = m_kompanionClient->chat(thread, opts).result(); - auto kllmReply = new KLLMReply(this); - - connect(kiReply, &KompanionAI::KIReply::tokensAdded, kllmReply, [kllmReply](const QString& delta) { - kllmReply->addContent(delta); - }); - - connect(kiReply, &KompanionAI::KIReply::finished, kllmReply, [kllmReply]() { - kllmReply->setFinished(true); - }); - - connect(kiReply, &KompanionAI::KIReply::errorOccurred, kllmReply, [kllmReply](const KompanionAI::KIError& error) { - kllmReply->setError(error.message); - }); - - return kllmReply; -} - -KLLMReply *KLLMInterface::getModelInfo(const KLLMRequest &request) -{ - // This is a simplified implementation as KompanionAI does not have a direct getModelInfo. - // We will return a KLLMReply with the model name if it exists in the provider's models. - auto kllmReply = new KLLMReply(this); - if (m_ollamaProvider->models().contains(request.model())) { - kllmReply->addContent(QString("{ \"model\": \"%1\" }").arg(request.model())); - } else { - kllmReply->setError(i18n("Model %1 not found.", request.model())); - } - kllmReply->setFinished(true); - return kllmReply; -} - -void KLLMInterface::reload() -{ - m_ollamaProvider->reload(); -} - -QString KLLMInterface::ollamaUrl() const -{ - // Ollama URL is managed by the OllamaProvider internally in KompanionAI - // For compatibility, we return an empty string or a placeholder. - return QString(); -} - -void KLLMInterface::setOllamaUrl(const QString &ollamaUrl) -{ - // In KompanionAI, the Ollama URL is configured directly on the OllamaProvider. - // For compatibility, we can re-instantiate the provider or update its internal URL. - // For now, we'll just set the default model if the URL is treated as a model name. - // This needs proper handling if the URL is truly meant for provider configuration. - m_kompanionClient->setDefaultModel(ollamaUrl); -} - -void KLLMInterface::setOllamaUrl(const QUrl &ollamaUrl) -{ - setOllamaUrl(ollamaUrl.toString()); -} - -QString KLLMInterface::systemPrompt() const -{ - // TODO: Extract system prompt from KompanionAI thread if available - return QString(); -} - -void KLLMInterface::setSystemPrompt(const QString &systemPrompt) -{ - // TODO: Set system prompt in KompanionAI thread -} diff --git a/src/kllm/KLLMInterface.h b/src/kllm/KLLMInterface.h deleted file mode 100644 index 762738a..0000000 --- a/src/kllm/KLLMInterface.h +++ /dev/null @@ -1,43 +0,0 @@ - -#ifndef KLLMINTERFACE_H -#define KLLMINTERFACE_H - -#include "KLLMOriginalInterface.h" -#include "KompanionAI/Client/KIClient.h" -#include "KompanionAI/Provider/OllamaProvider.h" - -namespace KLLMCore { - -class KLLMInterface : public KLLMOriginalInterface -{ - Q_OBJECT - -public: - explicit KLLMCompatInterface(QObject *parent = nullptr); - explicit KLLMCompatInterface(const QString &ollamaUrl, QObject *parent = nullptr); - explicit KLLMCompatInterface(const QUrl &ollamaUrl, QObject *parent = nullptr); - - bool ready() const override; - bool hasError() const override; - QStringList models() const override; - - KLLMReply *getCompletion(const KLLMRequest &request) override; - KLLMReply *getModelInfo(const KLLMRequest &request) override; - - void reload() override; - - QString ollamaUrl() const override; - void setOllamaUrl(const QString &ollamaUrl) override; - void setOllamaUrl(const QUrl &ollamaUrl) override; - - QString systemPrompt() const override; - void setSystemPrompt(const QString &systemPrompt) override; - -private: - KompanionAI::KIClient* m_kompanionClient; - KompanionAI::OllamaProvider* m_ollamaProvider; -}; - -} // namespace KLLMCore - -#endif // KLLMCOMPAT_H diff --git a/src/kllm/KLLMOriginalInterface.cpp b/src/kllm/KLLMOriginalInterface.cpp deleted file mode 100644 index 13c26a2..0000000 --- a/src/kllm/KLLMOriginalInterface.cpp +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include "KLLMInterface.h" - -#include - -#include -#include -#include -#include -#include - -using namespace Qt::StringLiterals; -using namespace KLLMCore; - -KLLMInterface::KLLMInterface(QObject *parent) - : KLLMInterface{QString{}, parent} -{ -} - -KLLMInterface::KLLMInterface(const QString &ollamaUrl, QObject *parent) - : QObject{parent} - , m_manager{new QNetworkAccessManager{this}} - , m_ollamaUrl{ollamaUrl} -{ - if (!m_ollamaUrl.isEmpty()) - reload(); -} - -KLLMInterface::KLLMInterface(const QUrl &ollamaUrl, QObject *parent) - : KLLMInterface{ollamaUrl.toString(), parent} -{ -} - -bool KLLMInterface::ready() const -{ - return m_ready && !m_hasError; -} - -bool KLLMInterface::hasError() const -{ - return m_hasError; -} - -QStringList KLLMInterface::models() const -{ - return m_models; -} - -#if 0 -void KLLMInterface::deleteModel(const QString &modelName) -{ - Q_ASSERT(ready()); - - QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/delete"))}; - req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); - QJsonObject data; - data["name"_L1] = modelName; - - // Delete resource doesn't take argument. Need to look at how to do it. - auto buf = new QBuffer{this}; - buf->setData(QJsonDocument(data).toJson(QJsonDocument::Compact)); - - auto reply = new KLLMReply{m_manager->deleteResource(req, buf), this}; - connect(reply, &KLLMReply::finished, this, [this, reply, buf] { - Q_EMIT finished(reply->readResponse()); - buf->deleteLater(); - }); -} -#endif - -KLLMReply *KLLMInterface::getCompletion(const KLLMRequest &request) -{ - Q_ASSERT(ready()); - - QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/generate"))}; - req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); - - QJsonObject data; - data["model"_L1] = request.model().isEmpty() ? m_models.constFirst() : request.model(); - data["prompt"_L1] = request.message(); - - const auto context = request.context().toJson(); - if (!context.isNull()) { - data["context"_L1] = context; - } - - if (!m_systemPrompt.isEmpty()) { - data["system"_L1] = m_systemPrompt; - } - - auto buf = new QBuffer{this}; - buf->setData(QJsonDocument(data).toJson(QJsonDocument::Compact)); - - auto reply = new KLLMReply{m_manager->post(req, buf), this}; - connect(reply, &KLLMReply::finished, this, [this, reply, buf] { - Q_EMIT finished(reply->readResponse()); - buf->deleteLater(); - }); - return reply; -} - -KLLMReply *KLLMInterface::getModelInfo(const KLLMRequest &request) -{ - Q_ASSERT(ready()); - - QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/show"))}; - req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); - - QJsonObject data; - data["model"_L1] = request.model().isEmpty() ? m_models.constFirst() : request.model(); - - auto buf = new QBuffer{this}; - buf->setData(QJsonDocument(data).toJson(QJsonDocument::Compact)); - - auto reply = new KLLMReply{m_manager->post(req, buf), this, KLLMReply::RequestTypes::Show}; - connect(reply, &KLLMReply::finished, this, [this, reply, buf] { - Q_EMIT finished(reply->readResponse()); - buf->deleteLater(); - }); - return reply; -} - -void KLLMInterface::reload() -{ - if (m_ollamaCheck) - disconnect(m_ollamaCheck); - - QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/tags"))}; - req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); - auto rep = m_manager->get(req); - m_ollamaCheck = connect(rep, &QNetworkReply::finished, this, [this, rep] { - if (rep->error() != QNetworkReply::NoError) { - Q_EMIT errorOccurred(i18n("Failed to connect to interface at %1: %2", m_ollamaUrl, rep->errorString())); - m_hasError = true; - Q_EMIT readyChanged(); - Q_EMIT hasErrorChanged(); - return; - } - - const auto json = QJsonDocument::fromJson(rep->readAll()); - const auto models = json["models"_L1].toArray(); - for (const QJsonValue &model : models) { - m_models.push_back(model["name"_L1].toString()); - } - Q_EMIT modelsChanged(); - - m_ready = !m_models.isEmpty(); - m_hasError = false; - Q_EMIT readyChanged(); - Q_EMIT hasErrorChanged(); - }); -} - -QString KLLMInterface::ollamaUrl() const -{ - return m_ollamaUrl; -} - -void KLLMInterface::setOllamaUrl(const QString &ollamaUrl) -{ - if (m_ollamaUrl == ollamaUrl) - return; - m_ollamaUrl = ollamaUrl; - Q_EMIT ollamaUrlChanged(); - reload(); -} - -void KLLMInterface::setOllamaUrl(const QUrl &ollamaUrl) -{ - setOllamaUrl(ollamaUrl.toString()); -} - -QString KLLMInterface::systemPrompt() const -{ - return m_systemPrompt; -} - -void KLLMInterface::setSystemPrompt(const QString &systemPrompt) -{ - if (m_systemPrompt == systemPrompt) - return; - m_systemPrompt = systemPrompt; - Q_EMIT systemPromptChanged(); -} - -#include "moc_KLLMInterface.cpp" diff --git a/src/kllm/KLLMOriginalInterface.h b/src/kllm/KLLMOriginalInterface.h deleted file mode 100644 index 5240455..0000000 --- a/src/kllm/KLLMOriginalInterface.h +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include "kllmcore_export.h" - -#include "KLLMReply.h" -#include "KLLMRequest.h" -class QNetworkAccessManager; -namespace KLLMCore -{ -/** - * @brief The KLLMInterface class provides an interface around the LLM API. - * - * KLLM is designed to feel similar to Qt Network. This means that all LLM requests must be routed through a central - * KLLMInterface object. - * - * To request a message or completion from the LLM, first create a KLLMRequest object with the desired message. Choose the - * model from models() that you wish to use for this request and set it on the KLLMRequest. Then call getCompletion(), - * passing your KLLMRequest object. You will recieve a KLLMReply object; connect to KLLMReply::contentAdded() if you wish to - * receive content updates as they arrive from the server or connect to KLLMReply::finished() if you prefer to have the whole - * message delivered at once. - * - * You should not request any completions (or otherwise use this class) until ready() returns true. Using the interface - * before it is ready can cause problems; for example, the interface may not have loaded the available models yet or the - * backend could be unreachable. - */ -class KLLMCORE_EXPORT KLLMInterface : public QObject -{ - Q_OBJECT - - Q_PROPERTY(bool ready READ ready NOTIFY readyChanged FINAL) - Q_PROPERTY(bool hasError READ hasError NOTIFY hasErrorChanged FINAL) - Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged FINAL) - Q_PROPERTY(QString ollamaUrl READ ollamaUrl WRITE setOllamaUrl NOTIFY ollamaUrlChanged FINAL) - Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt NOTIFY systemPromptChanged FINAL) - -public: - /** - * @brief Creates a KLLMInterface. - * @param parent The parent QObject. - */ - explicit KLLMInterface(QObject *parent = nullptr); - - /** - * @brief Creates a KLLMInterface with the url set to \a ollamaUrl. - * @param ollamaUrl The URL to the Ollama instance. - * @param parent The parent QObject. - */ - explicit KLLMInterface(const QString &ollamaUrl, QObject *parent = nullptr); - - /** - * @brief Creates a KLLMInterface with the url set to \a ollamaUrl. - * @param ollamaUrl The URL to the Ollama instance. - * @param parent The parent QObject. - */ - explicit KLLMInterface(const QUrl &ollamaUrl, QObject *parent = nullptr); - - /** - * @brief Check whether the interface is ready. - * - * You should not use the interface until ready() returns true. Failure to observe this rule may result in undefined behavior. - * - * If the interface encounters an error, ready() will return false. However, do not use ready() to indicate to the user that the interface is in an error - * state, as the interface could be in the process of making its initial connection. Instead, you should use hasError() to check for an error state. - * Additionally, you should connect to errorOccurred() to handle errors as they arise. - * - * @return Returns whether the interface is ready. - */ - [[nodiscard]] bool ready() const; - - /** - * @brief Check whether the interface is in an error state. - * - * After you handle an error from errorEmitted(), you should monitor this property. When it becomes \c false, you can safely resume operations. - * - * @return Returns whether the interface is in an error state. - */ - [[nodiscard]] bool hasError() const; - - /** - * @brief Retrieve a list of models supported by the LLM backend. - * - * When creating a KLLMRequest, you should choose a model from this list for the request. If you do not specify a model, - * the request will probably fail. - * - * @return Returns a QStringList containing all valid models for this interface. - */ - [[nodiscard]] QStringList models() const; - - /** - * @brief Get the URL to the Ollama instance. - * @return The URL for the Ollama instance. - */ - [[nodiscard]] QString ollamaUrl() const; - - /** - * @brief Set the URL to the Ollama instance. - * - * Since Ollama is a self-hostable service, users may wish to use different instances. Use this function to set the URL to the desired instance. It should - * \a not contain the \c /api portion of the URL. - * - * @param ollamaUrl The new URL for the Ollama instance. - */ - void setOllamaUrl(const QString &ollamaUrl); - - /** - * @brief A convenience overload of setOllamaUrl() that takes a QUrl. - * @param ollamaUrl The new URL for the Ollama instance. - */ - void setOllamaUrl(const QUrl &ollamaUrl); - - /** - * @brief Get the system prompt for the LLM. - * @return The system prompt string. - */ - [[nodiscard]] QString systemPrompt() const; - - /** - * @brief Set the system prompt for the LLM. - * - * LLMs can take system prompts that instruct them on how they should generally behave in a conversation. This could be anything from how they speak to what - * types of information they prefer to present. You can set a system prompt here to better cater to your users. - * - * @param systemPrompt The system prompt for the LLM. - */ - void setSystemPrompt(const QString &systemPrompt); - -public Q_SLOTS: - /** - * @brief Request a completion from the LLM. - * - * Calling this function starts a request to the LLM backend. You should use the returned KLLMReply pointer to track the - * status of the LLM's response. Once the KLLMReply emits KLLMReply::finished(), it is your responsibility to either - * track or delete the KLLMReply; auto-deleting is not implemented yet. - * - * @param request The request object that will be used to create the actual LLM request. - * @return Returns a pointer to a KLLMReply that can be used to track the progress of the reply. - */ - KLLMReply *getCompletion(const KLLMRequest &request); - - /** - * @brief Request model info from Ollama. - * - * Calling this function starts a request to the LLM backend. You should use the returned KLLMReply pointer to track the - * status of the LLM's response. Once the KLLMReply emits KLLMReply::finished(), it is your responsibility to either - * track or delete the KLLMReply; auto-deleting is not implemented yet. - * - * @param request The request object that will be used to create the actual LLM request. - * @return Returns a pointer to a KLLMReply that can be used to track the progress of the reply. - */ - KLLMReply *getModelInfo(const KLLMRequest &request); - - /** - * @brief Reload the LLM interface. - * - * Reloading the interface can be used to check if a network error is gone or to see if the available models have changed. - */ - void reload(); - -Q_SIGNALS: - /** - * @brief This signal is emitted when any completion requested by the interface is completed. - * @param replyText Contains the text of the completion. - */ - void finished(const QString &replyText); - - void readyChanged(); - void hasErrorChanged(); - void modelsChanged(); - void ollamaUrlChanged(); - void systemPromptChanged(); - - /** - * @brief An error occurred while communicating with the interface. - * @param message Contains the human readable error message. - */ - void errorOccurred(const QString &message); - -private: - QNetworkAccessManager *const m_manager; - QStringList m_models; - bool m_ready = false; - bool m_hasError = false; - QString m_ollamaUrl; - QString m_systemPrompt; - QMetaObject::Connection m_ollamaCheck; -}; -} diff --git a/src/kllm/KLLMReply.cpp b/src/kllm/KLLMReply.cpp deleted file mode 100644 index e2e01c8..0000000 --- a/src/kllm/KLLMReply.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include "KLLMReply.h" -#include "kllmcore_debug.h" - -#include - -using namespace Qt::StringLiterals; -using namespace KLLMCore; - -KLLMReply::KLLMReply(QNetworkReply *netReply, QObject *parent, RequestTypes requestType) - : QObject{parent} - , m_reply{netReply} - , m_requestType{requestType} -{ - connect(m_reply, &QNetworkReply::finished, m_reply, [this] { - // Normally, we could assume that the tokens will never be empty once the request finishes, but it could be possible - // that the request failed and we have no tokens to parse. - if (m_requestType == RequestTypes::StreamingGenerate && !m_tokens.empty()) { - const auto finalResponse = m_tokens.constLast(); - m_context.setOllamaContext(finalResponse["context"_L1].toArray()); - m_info.totalDuration = std::chrono::nanoseconds{finalResponse["total_duration"_L1].toVariant().toULongLong()}; - m_info.loadDuration = std::chrono::nanoseconds{finalResponse["load_duration"_L1].toVariant().toULongLong()}; - m_info.promptEvalTokenCount = finalResponse["prompt_eval_count"_L1].toVariant().toULongLong(); - m_info.promptEvalDuration = std::chrono::nanoseconds{finalResponse["prompt_eval_duration"_L1].toVariant().toULongLong()}; - m_info.tokenCount = finalResponse["eval_count"_L1].toVariant().toULongLong(); - m_info.duration = std::chrono::nanoseconds{finalResponse["eval_duration"_L1].toVariant().toULongLong()}; - } - - qCDebug(KLLMCORE_LOG) << "Ollama response finished"; - m_finished = true; - Q_EMIT finished(); - }); - connect(m_reply, &QNetworkReply::errorOccurred, m_reply, [](QNetworkReply::NetworkError e) { - qCDebug(KLLMCORE_LOG) << "Ollama HTTP error:" << e; - }); - connect(m_reply, &QNetworkReply::downloadProgress, m_reply, [this](qint64 received, qint64 /*total*/) { - m_incompleteTokens += m_reply->read(received - m_receivedSize); - m_receivedSize = received; - switch (m_requestType) { - case RequestTypes::Show: - m_tokens.append(QJsonDocument::fromJson(m_incompleteTokens)); - break; - case RequestTypes::StreamingGenerate: - auto completeTokens = m_incompleteTokens.split('\n'); - if (completeTokens.size() <= 1) { - return; - } - m_incompleteTokens = completeTokens.last(); - completeTokens.removeLast(); - - m_tokens.reserve(completeTokens.count()); - for (const auto &tok : std::as_const(completeTokens)) { - m_tokens.append(QJsonDocument::fromJson(tok)); - } - break; - } - Q_EMIT contentAdded(); - }); -} - -QString KLLMReply::readResponse() const -{ - QString ret; - switch (m_requestType) { - case RequestTypes::Show: - ret += QString::fromLatin1("## Template: \n```\n") + m_tokens.constFirst()["template"_L1].toString() + QString::fromLatin1("\n```\n"); - ret += QString::fromLatin1("## Modelfile: \n```\n") + m_tokens.constFirst()["modelfile"_L1].toString() + QString::fromLatin1("\n```\n"); - ret += QString::fromLatin1("## Parameters: \n```\n") + m_tokens.constFirst()["parameters"_L1].toString() + QString::fromLatin1("\n```\n"); - ret += QString::fromLatin1("## Details: \n```\n") - + QString::fromLatin1(QJsonDocument::fromVariant(m_tokens.constFirst()["details"_L1].toVariant()).toJson()) + QString::fromLatin1("\n```\n"); - ret += QString::fromLatin1("## Model Info: \n```\n") - + QString::fromLatin1(QJsonDocument::fromVariant(m_tokens.constFirst()["model_info"_L1].toVariant()).toJson()) + QString::fromLatin1("\n```\n"); - break; - case RequestTypes::StreamingGenerate: - for (const auto &tok : m_tokens) - ret += tok["response"_L1].toString(); - break; - } - - return ret; -} - -const KLLMContext &KLLMReply::context() const -{ - return m_context; -} - -const KLLMReplyInfo &KLLMReply::info() const -{ - return m_info; -} - -const KLLMReply::RequestTypes &KLLMReply::requestType() const -{ - return m_requestType; -} - -bool KLLMReply::isFinished() const -{ - return m_finished; -} -#include "moc_KLLMReply.cpp" diff --git a/src/kllm/KLLMReply.h b/src/kllm/KLLMReply.h deleted file mode 100644 index be4cbc9..0000000 --- a/src/kllm/KLLMReply.h +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include "kllmcore_export.h" -#include - -#include "KLLMContext.h" - -class QNetworkReply; -namespace KLLMCore -{ -/** - * @brief The KLLMReplyInfo class represents information about a reply from an LLM. - * - * When an LLM generates a completion, the server generally will return some information about the completion, including the - * duration of the completion, the number of tokens received, and the duration of the prompt evaluation. This struct encapsulates such information. - * If any one of these fields is not available, it will be set to its default value. - */ -struct KLLMCORE_EXPORT KLLMReplyInfo { - //! The total time from when the request was received by the server to when the reply was returned. - std::chrono::nanoseconds totalDuration; - - //! The time spent loading the model. - std::chrono::nanoseconds loadDuration; - - //! The number of tokens in the prompt. - int promptEvalTokenCount; - - //! The time spent evaluating the prompt. - std::chrono::nanoseconds promptEvalDuration; - - //! The number of tokens in the reply. - int tokenCount; - - //! The time spent generating the reply. - std::chrono::nanoseconds duration; -}; - -/** - * @brief The KLLMReply class represents a reply from an LLM. - * - * Requesting a completion from a KLLMInterface will return a KLLMReply. You can use this to track the progress of the LLM's - * reply. - * - * If you want to stream a reply as it is written in real time, connect to contentAdded() and use readResponse() to retrieve - * the new content. If you prefer to wait for the entire reply before displaying anything, connect to finished(), which will - * only be emitted once the reply is complete. - */ -class KLLMCORE_EXPORT KLLMReply : public QObject -{ - Q_OBJECT - -public: - /** - * @brief Specifies the request type. - * - * When the class in instantiated the type of request should be specified - */ - enum class RequestTypes { - StreamingGenerate, - Show - }; - /** - * @brief Get the current response content. - * - * This function returns what it has recieved of the response so far. Therefore, until finished() is emitted, this - * function may return different values. However, once finished() is emitted, the content is guaranteed to remain - * constant. - * - * @return The content that has been returned so far. - */ - [[nodiscard]] QString readResponse() const; - - /** - * @brief Get the context token for this response (if applicable). - * - * Messages sent by most LLMs have a context identifier that allows you to chain messages into a conversation. To create - * such a conversation, you need to take this context object and set it on the next KLLMRequest in the conversation. - * KLLMInterface::getCompletion() will use that context object to continue the message thread. - * - * @return A context object that refers to this response. - */ - const KLLMContext &context() const; - - /** - * @brief Get extra information about the reply (if applicable). - * - * This function returns a KLLMReplyInfo object containing information about this reply. If the reply has not finished, the KLLMReplyInfo object will have - * all members set to their default values. - * - * @return Extra information about the reply. - */ - const KLLMReplyInfo &info() const; - - /** - * @brief Check whether the reply has finished. - * - * If you need to know if the response has finished changing or if the context has been received yet, call this function. - * - * @return Whether the reply has finished. - */ - [[nodiscard]] bool isFinished() const; - - /** - * @brief Get request type. - * - * The request type is set when this object is created. - * - * @return Corresponding request type. - */ - const RequestTypes &requestType() const; - -protected: - explicit KLLMReply(QNetworkReply *netReply, QObject *parent = nullptr, RequestTypes requestType = RequestTypes::StreamingGenerate); - - friend class KLLMInterface; - -Q_SIGNALS: - /** - * @brief Emits when new content has been added to the response. - * - * If you are not streaming the response live, this signal is not of importance to you. However, if you are streaming - * content, when this signal is emitted, you should call readResponse() to update the response that your application - * shows. - */ - void contentAdded(); - - /** - * @brief Emits when the LLM has finished returning its response. - * - * After this signal has emitted, the content is guaranteed to not change. At this point, you should call readResponse() - * to get the content and then either take ownership of the KLLMReply or delete it, as automatic reply deletion is not - * implemented yet. - */ - void finished(); - -private: - QNetworkReply *const m_reply; - QByteArray m_incompleteTokens; - - QList m_tokens; - - KLLMContext m_context; - KLLMReplyInfo m_info; - RequestTypes m_requestType = RequestTypes::StreamingGenerate; - - int m_receivedSize = 0; - bool m_finished = false; -}; -} diff --git a/src/kllm/KLLMRequest.cpp b/src/kllm/KLLMRequest.cpp deleted file mode 100644 index 4391994..0000000 --- a/src/kllm/KLLMRequest.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#include "KLLMRequest.h" - -using namespace KLLMCore; -KLLMRequest::KLLMRequest(const QString &message) - : m_message{message} -{ -} - -QString KLLMRequest::message() const -{ - return m_message; -} - -const KLLMContext &KLLMRequest::context() const -{ - return m_context; -} - -QString KLLMRequest::model() const -{ - return m_model; -} - -void KLLMRequest::setMessage(const QString &message) -{ - m_message = message; -} - -void KLLMRequest::setContext(const KLLMContext &context) -{ - m_context = context; -} - -void KLLMRequest::setModel(const QString &model) -{ - m_model = model; -} - -QDebug operator<<(QDebug d, const KLLMRequest &t) -{ - d << "Model: " << t.model(); - d << "Message: " << t.message(); - d << "Context: " << t.context(); - return d; -} diff --git a/src/kllm/KLLMRequest.h b/src/kllm/KLLMRequest.h deleted file mode 100644 index 5b42089..0000000 --- a/src/kllm/KLLMRequest.h +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Loren Burkholder -// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company -// -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -#pragma once - -#include "kllmcore_export.h" - -#include "KLLMContext.h" -#include -namespace KLLMCore -{ -/** - * @brief The KLLMRequest class contains a representation of a message to an LLM. - * - * Before calling KLLMInterface::getCompletion(), you need to create a KLLMRequest with information about the desired - * completion. - */ -class KLLMCORE_EXPORT KLLMRequest -{ -public: - /** - * @brief Creates a KLLMRequest with the message \a message. - * @param message The message that will be sent to the LLM. - * @param parent - */ - explicit KLLMRequest(const QString &message); - - /** - * @brief Use this to get the message for the LLM. - * @return Returns the message to be sent to the LLM. - */ - [[nodiscard]] QString message() const; - - /** - * @brief Use this to retrive the context for the LLM. - * @return Returns the context object to be sent to the LLM. - */ - const KLLMContext &context() const; - - /** - * @brief Gets the LLM model that will be used by the backend. - * @return The name of the model to be used for this request. - */ - [[nodiscard]] QString model() const; - - /** - * @brief Sets the message to be sent to the LLM. - * @param message The message text to be sent to the LLM. - */ - void setMessage(const QString &message); - - /** - * @brief Sets the conversation context for the LLM. - * - * If you want the LLM to respond in context of previous messages, you should set this to the context returned in the - * previous KLLMReply. - * - * @param context The context object for this request. - */ - void setContext(const KLLMContext &context); - - /** - * @brief Sets the model to be used for this request. - * - * You should set this to one of the values returned by KLLMInterface::models(); failure to do so will likely produce an - * error from the backend. - * - * @param model The name of the model to be used for this request. - */ - void setModel(const QString &model); - -private: - QString m_message; - KLLMContext m_context; - QString m_model; -}; -} -KLLMCORE_EXPORT QDebug operator<<(QDebug d, const KLLMCore::KLLMRequest &t); diff --git a/src/mcp/CMakeLists.txt b/src/mcp/CMakeLists.txt new file mode 100644 index 0000000..f77707e --- /dev/null +++ b/src/mcp/CMakeLists.txt @@ -0,0 +1,29 @@ +add_library(kom_mcp STATIC + KompanionQtServer.cpp +) + +qt_add_resources(kom_mcp kompanion_mcp_resources + PREFIX "/kompanion" + BASE "." + FILES ToolSchemas.json +) + +target_link_libraries(kom_mcp PRIVATE + kom_dal + kom_ai + KF6::ConfigCore + Qt6::Core + Qt6::Network + Qt6::McpServer + Qt6::McpCommon +) + +target_compile_options(kom_mcp PRIVATE -fexceptions) + +target_compile_definitions(kom_mcp PRIVATE + PROJECT_SOURCE_DIR="${CMAKE_SOURCE_DIR}" + KOMPANION_DB_INIT_INSTALL_DIR="${KOMPANION_DB_INIT_INSTALL_DIR}" +) + + +install(FILES ToolSchemas.json DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/mcp) diff --git a/src/mcp/HandlersIntrospection.hpp b/src/mcp/HandlersIntrospection.hpp index e5d02e3..9c8dd11 100644 --- a/src/mcp/HandlersIntrospection.hpp +++ b/src/mcp/HandlersIntrospection.hpp @@ -112,6 +112,11 @@ inline std::optional currentDsnSource() { } // namespace detail +// Echos back the request payload +inline std::string echo(const std::string& reqJson) { + return reqJson; +} + // Produces a JSON response summarising project state: memory docs, task table, git status. inline std::string project_snapshot(const std::string& reqJson) { (void)reqJson; diff --git a/src/mcp/HandlersMemory.hpp b/src/mcp/HandlersMemory.hpp index c0da754..274549d 100644 --- a/src/mcp/HandlersMemory.hpp +++ b/src/mcp/HandlersMemory.hpp @@ -14,7 +14,7 @@ #include #include -#include "dal/PgDal.hpp" +#include "PgDal.hpp" namespace Handlers { namespace detail { diff --git a/src/mcp/RegisterTools.hpp b/src/mcp/RegisterTools.hpp index d4284ce..3e6a48e 100644 --- a/src/mcp/RegisterTools.hpp +++ b/src/mcp/RegisterTools.hpp @@ -5,6 +5,7 @@ #include "HandlersMemory.hpp" inline void register_default_tools(KomMcpServer& server) { + server.registerTool("echo", Handlers::echo); server.registerTool("kom.memory.v1.save_context", Handlers::save_context); server.registerTool("kom.memory.v1.recall_context", Handlers::recall_context); server.registerTool("kom.memory.v1.embed_text", Handlers::embed_text); diff --git a/src/mcp/Tools.hpp b/src/mcp/Tools.hpp index 8f53366..02ef65b 100644 --- a/src/mcp/Tools.hpp +++ b/src/mcp/Tools.hpp @@ -18,6 +18,11 @@ inline std::string json_arr(const std::vector& items) { os << "]"; return os.str(); } +// `echo` tool: echoes back the input +inline std::string echo_response(const std::string& input) { + return input; +} + // `ping` tool: echoes { ok: true, tools: [...] } inline std::string ping_response(const std::vector& toolNames) { std::vector quoted; quoted.reserve(toolNames.size());