diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9377b130ab..3d88e2e4df 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -288,6 +288,8 @@ set(SOURCE_FILES services/cryptoservice.h services/scriptingservice.cpp services/scriptingservice.h + services/openaiservice.cpp + services/openaiservice.h libraries/piwiktracker/piwiktracker.h libraries/piwiktracker/piwiktracker.cpp libraries/qkeysequencewidget/qkeysequencewidget/src/qkeysequencewidget_p.h diff --git a/src/QOwnNotes.pro b/src/QOwnNotes.pro index 405427d9a3..05cdf5886a 100644 --- a/src/QOwnNotes.pro +++ b/src/QOwnNotes.pro @@ -165,6 +165,7 @@ SOURCES += main.cpp\ services/scriptingservice.cpp \ services/websocketserverservice.cpp \ services/webappclientservice.cpp \ + services/openaiservice.cpp \ dialogs/masterdialog.cpp \ utils/misc.cpp \ utils/git.cpp \ @@ -272,6 +273,7 @@ HEADERS += mainwindow.h \ dialogs/passworddialog.h \ services/metricsservice.h \ services/cryptoservice.h \ + services/openaiservice.h \ dialogs/masterdialog.h \ utils/misc.h \ utils/git.h \ diff --git a/src/dialogs/settingsdialog.h b/src/dialogs/settingsdialog.h index 454115475d..f8f32ade7c 100644 --- a/src/dialogs/settingsdialog.h +++ b/src/dialogs/settingsdialog.h @@ -60,7 +60,8 @@ class SettingsDialog : public MasterDialog { LayoutPage, WebCompanionPage, WebApplicationPage, - ExperimentalPage + ExperimentalPage, + AiPage }; explicit SettingsDialog(int page = 0, QWidget *parent = 0); diff --git a/src/dialogs/settingsdialog.ui b/src/dialogs/settingsdialog.ui index 43e19f97bf..7481cc53fd 100644 --- a/src/dialogs/settingsdialog.ui +++ b/src/dialogs/settingsdialog.ui @@ -295,6 +295,18 @@ :/icons/breeze-qownnotes/16x16/text-html.svg:/icons/breeze-qownnotes/16x16/text-html.svg + + + AI + + + 22 + + + + :/icons/breeze-dark-qownnotes/16x16/text-html.svg:/icons/breeze-dark-qownnotes/16x16/text-html.svg + + General @@ -421,7 +433,7 @@ - 7 + 22 @@ -6843,6 +6855,78 @@ Just test yourself if you get sync conflicts and set a higher value if so. + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + API keys + + + + 3 + + + + + Create API Key + + + + + + + :/icons/breeze-qownnotes/16x16/text-html.svg:/icons/breeze-qownnotes/16x16/text-html.svg + + + + + + + Groq API key: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + true + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e790a37b32..7b586adb76 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -115,6 +115,7 @@ #include "services/databaseservice.h" #include "services/metricsservice.h" #include "services/nextclouddeckservice.h" +#include "services/openaiservice.h" #include "services/owncloudservice.h" #include "services/updateservice.h" #include "services/webappclientservice.h" @@ -5817,6 +5818,9 @@ void MainWindow::on_action_Find_note_triggered() { changeDistractionFreeMode(false); this->ui->searchLineEdit->setFocus(); this->ui->searchLineEdit->selectAll(); + +// auto openAiService = new OpenAiService(this); +// Q_UNUSED(openAiService) } // diff --git a/src/services/openaiservice.cpp b/src/services/openaiservice.cpp new file mode 100644 index 0000000000..a42d99432b --- /dev/null +++ b/src/services/openaiservice.cpp @@ -0,0 +1,125 @@ +/* +* Copyright (c) 2014-2024 Patrizio Bekerle -- +* +* This program 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; version 2 of the License. +* +* This program 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. +* +*/ + +#include "openaiservice.h" +#include +#include +#include +#include +#include + +using namespace std; + +QT_USE_NAMESPACE + +OpenAiService::OpenAiService(QObject* parent) + : QObject(parent) { + auto apiKey = "secret"; + auto _completer = new OpenAiCompleter( + apiKey, + "llama3-8b-8192", + "https://api.groq.com/openai/v1/chat/completions", + parent); + + _completer->complete("Who am I?"); + + QObject::connect(_completer, &OpenAiCompleter::completed, this, [this](const QString& result) { + qDebug() << "'result': " << result; + }); + + QObject::connect(_completer, &OpenAiCompleter::errorOccurred, this, [this](const QString& errorString) { + qDebug() << "'errorString': " << errorString; + }); +} + +OpenAiCompleter::OpenAiCompleter(QString apiKey, QString modelId, QString apiBaseUrl, QObject* parent) + : QObject(parent), apiKey(std::move(apiKey)), apiBaseUrl(std::move(apiBaseUrl)), modelId(std::move(modelId)) +{ + networkManager = new QNetworkAccessManager(this); + connect(networkManager, &QNetworkAccessManager::finished, this, &OpenAiCompleter::replyFinished); +} + +void OpenAiCompleter::complete(const QString& prompt) +{ + QUrl url(apiBaseUrl); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", ("Bearer " + apiKey).toUtf8()); + + QJsonObject json; + json["model"] = modelId; // Use the modelId set in the constructor or by setModelId + + QJsonArray messagesArray; + QJsonObject messageObject; + messageObject["role"] = "user"; + messageObject["content"] = prompt; + messagesArray.append(messageObject); + + json["messages"] = messagesArray; + + qDebug() << __func__ << " - 'json': " << json; + + QJsonDocument doc(json); + QByteArray data = doc.toJson(); + + networkManager->post(request, data); +} + +void OpenAiCompleter::setApiBaseUrl(const QString& url) { + this->apiBaseUrl = url; +} + +void OpenAiCompleter::setModelId(const QString& id) { + this->modelId = id; +} + +void OpenAiCompleter::replyFinished(QNetworkReply* reply) +{ + if (reply->error()) { + emit errorOccurred(reply->errorString()); + return; + } + + QByteArray response_data = reply->readAll(); + QJsonDocument json = QJsonDocument::fromJson(response_data); + QJsonObject jsonObject = json.object(); + QJsonArray choices = jsonObject["choices"].toArray(); + + // Initializing an empty result string to hold the eventual content. + QString text = ""; + + // Look through the choices (though typically there's only one) + // and parse the nested message content. + if(!choices.isEmpty()) { + QJsonObject firstChoice = choices[0].toObject(); + + // Check if 'message' field exists, ensuring compatibility with the new structure + if(firstChoice.contains("message")) { + QJsonObject message = firstChoice["message"].toObject(); + // Check if the role is "assistant" before extracting content + if(message["role"].toString() == "assistant") { + text = message["content"].toString(); + } + } else { // Fallback to directly parse 'text' if present, for backward compatibility or other responses + text = firstChoice["text"].toString(); + } + + emit completed(text.trimmed()); + } + else { + emit errorOccurred("No choices found in the response"); + } + + reply->deleteLater(); +} diff --git a/src/services/openaiservice.h b/src/services/openaiservice.h new file mode 100644 index 0000000000..a52605af1e --- /dev/null +++ b/src/services/openaiservice.h @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2014-2024 Patrizio Bekerle -- +* +* This program 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; version 2 of the License. +* +* This program 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. +* +*/ + +#pragma once + +#include +#include +#include + +class OpenAiService : public QObject +{ + Q_OBJECT + public: + explicit OpenAiService(QObject* parent = nullptr); +}; + + +class OpenAiCompleter : public QObject +{ + Q_OBJECT + public: + explicit OpenAiCompleter(QString apiKey, QString modelId, QString apiBaseUrl = "https://api.openai.com/v1/completions", QObject* parent = nullptr); + void complete(const QString& prompt); + void setApiBaseUrl(const QString& url); + void setModelId(const QString& id); + + signals: + void completed(QString result); + void errorOccurred(QString errorString); + + private slots: + void replyFinished(QNetworkReply* reply); + + private: + QNetworkAccessManager* networkManager; + QString apiKey; + QString apiBaseUrl; // Store the API base URL + QString modelId; // Model ID used for API requests +};