#include "network.h" #include "llm.h" #include #include #include #include #include #include #include //#define DEBUG class MyNetwork: public Network { }; Q_GLOBAL_STATIC(MyNetwork, networkInstance) Network *Network::globalInstance() { return networkInstance(); } Network::Network() : QObject{nullptr} , m_isActive(false) , m_usageStatsActive(false) , m_shouldSendStartup(false) { QSettings settings; settings.sync(); m_uniqueId = settings.value("uniqueId", generateUniqueId()).toString(); settings.setValue("uniqueId", m_uniqueId); settings.sync(); m_isActive = settings.value("network/isActive", false).toBool(); if (m_isActive) sendHealth(); m_usageStatsActive = settings.value("network/usageStatsActive", false).toBool(); if (m_usageStatsActive) sendIpify(); connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this, &Network::handleSslErrors); } void Network::setActive(bool b) { QSettings settings; settings.setValue("network/isActive", b); settings.sync(); m_isActive = b; emit activeChanged(); if (m_isActive) sendHealth(); } void Network::setUsageStatsActive(bool b) { QSettings settings; settings.setValue("network/usageStatsActive", b); settings.sync(); m_usageStatsActive = b; emit usageStatsActiveChanged(); if (!m_usageStatsActive) sendOptOut(); else { // model might be loaded already when user opt-in for first time sendStartup(); sendIpify(); } } QString Network::generateUniqueId() const { return QUuid::createUuid().toString(QUuid::WithoutBraces); } bool Network::packageAndSendJson(const QString &ingestId, const QString &json) { if (!m_isActive) return false; QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8(), &err); if (err.error != QJsonParseError::NoError) { qDebug() << "Couldn't parse: " << json << err.errorString(); return false; } Q_ASSERT(doc.isObject()); QJsonObject object = doc.object(); object.insert("source", "gpt4all-chat"); object.insert("agent_id", LLM::globalInstance()->currentChat()->modelName()); object.insert("submitter_id", m_uniqueId); object.insert("ingest_id", ingestId); QSettings settings; settings.sync(); QString attribution = settings.value("network/attribution", QString()).toString(); if (!attribution.isEmpty()) object.insert("network/attribution", attribution); QJsonDocument newDoc; newDoc.setObject(object); #if defined(DEBUG) printf("%s\n", qPrintable(newDoc.toJson(QJsonDocument::Indented))); fflush(stdout); #endif QUrl jsonUrl("https://api.gpt4all.io/v1/ingest/chat"); QNetworkRequest request(jsonUrl); QSslConfiguration conf = request.sslConfiguration(); conf.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(conf); QByteArray body(newDoc.toJson()); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QNetworkReply *jsonReply = m_networkManager.post(request, body); connect(jsonReply, &QNetworkReply::finished, this, &Network::handleJsonUploadFinished); m_activeUploads.append(jsonReply); return true; } void Network::handleJsonUploadFinished() { QNetworkReply *jsonReply = qobject_cast(sender()); if (!jsonReply) return; m_activeUploads.removeAll(jsonReply); QVariant response = jsonReply->attribute(QNetworkRequest::HttpStatusCodeAttribute); Q_ASSERT(response.isValid()); bool ok; int code = response.toInt(&ok); if (!ok) qWarning() << "ERROR: ingest invalid response."; if (code != 200) { qWarning() << "ERROR: ingest response != 200 code:" << code; sendHealth(); } QByteArray jsonData = jsonReply->readAll(); QJsonParseError err; QJsonDocument document = QJsonDocument::fromJson(jsonData, &err); if (err.error != QJsonParseError::NoError) { qDebug() << "ERROR: Couldn't parse: " << jsonData << err.errorString(); return; } #if defined(DEBUG) printf("%s\n", qPrintable(document.toJson(QJsonDocument::Indented))); fflush(stdout); #endif jsonReply->deleteLater(); } void Network::handleSslErrors(QNetworkReply *reply, const QList &errors) { QUrl url = reply->request().url(); for (auto e : errors) qWarning() << "ERROR: Received ssl error:" << e.errorString() << "for" << url; } void Network::sendOptOut() { QJsonObject properties; properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2"); properties.insert("time", QDateTime::currentSecsSinceEpoch()); properties.insert("distinct_id", m_uniqueId); properties.insert("$insert_id", generateUniqueId()); QJsonObject event; event.insert("event", "opt_out"); event.insert("properties", properties); QJsonArray array; array.append(event); QJsonDocument doc; doc.setArray(array); sendMixpanel(doc.toJson(), true /*isOptOut*/); #if defined(DEBUG) printf("%s %s\n", qPrintable("opt_out"), qPrintable(doc.toJson(QJsonDocument::Indented))); fflush(stdout); #endif } void Network::sendModelLoaded() { if (!m_usageStatsActive) return; sendMixpanelEvent("model_load"); } void Network::sendResetContext() { if (!m_usageStatsActive) return; sendMixpanelEvent("reset_context"); } void Network::sendStartup() { if (!m_usageStatsActive) return; m_shouldSendStartup = true; if (m_ipify.isEmpty()) return; // when it completes it will send sendMixpanelEvent("startup"); } void Network::sendCheckForUpdates() { if (!m_usageStatsActive) return; sendMixpanelEvent("check_for_updates"); } void Network::sendMixpanelEvent(const QString &ev) { if (!m_usageStatsActive) return; QJsonObject properties; properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2"); properties.insert("time", QDateTime::currentSecsSinceEpoch()); properties.insert("distinct_id", m_uniqueId); properties.insert("$insert_id", generateUniqueId()); properties.insert("$os", QSysInfo::prettyProductName()); if (!m_ipify.isEmpty()) properties.insert("ip", m_ipify); properties.insert("name", QCoreApplication::applicationName() + " v" + QCoreApplication::applicationVersion()); properties.insert("model", LLM::globalInstance()->currentChat()->modelName()); QJsonObject event; event.insert("event", ev); event.insert("properties", properties); QJsonArray array; array.append(event); QJsonDocument doc; doc.setArray(array); sendMixpanel(doc.toJson()); #if defined(DEBUG) printf("%s %s\n", qPrintable(ev), qPrintable(doc.toJson(QJsonDocument::Indented))); fflush(stdout); #endif } void Network::sendIpify() { if (!m_usageStatsActive || !m_ipify.isEmpty()) return; QUrl ipifyUrl("https://api.ipify.org"); QNetworkRequest request(ipifyUrl); QSslConfiguration conf = request.sslConfiguration(); conf.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(conf); QNetworkReply *reply = m_networkManager.get(request); connect(reply, &QNetworkReply::finished, this, &Network::handleIpifyFinished); } void Network::sendMixpanel(const QByteArray &json, bool isOptOut) { if (!m_usageStatsActive && !isOptOut) return; QUrl trackUrl("https://api.mixpanel.com/track"); QNetworkRequest request(trackUrl); QSslConfiguration conf = request.sslConfiguration(); conf.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(conf); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QNetworkReply *trackReply = m_networkManager.post(request, json); connect(trackReply, &QNetworkReply::finished, this, &Network::handleMixpanelFinished); } void Network::handleIpifyFinished() { Q_ASSERT(m_usageStatsActive); QNetworkReply *reply = qobject_cast(sender()); if (!reply) return; QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); Q_ASSERT(response.isValid()); bool ok; int code = response.toInt(&ok); if (!ok) qWarning() << "ERROR: ipify invalid response."; if (code != 200) qWarning() << "ERROR: ipify response != 200 code:" << code; m_ipify = qPrintable(reply->readAll()); #if defined(DEBUG) printf("ipify finished %s\n", m_ipify.toLatin1().constData()); fflush(stdout); #endif reply->deleteLater(); if (m_shouldSendStartup) sendStartup(); } void Network::handleMixpanelFinished() { Q_ASSERT(m_usageStatsActive); QNetworkReply *reply = qobject_cast(sender()); if (!reply) return; QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); Q_ASSERT(response.isValid()); bool ok; int code = response.toInt(&ok); if (!ok) qWarning() << "ERROR: track invalid response."; if (code != 200) qWarning() << "ERROR: track response != 200 code:" << code; #if defined(DEBUG) printf("mixpanel finished %s\n", qPrintable(reply->readAll())); fflush(stdout); #endif reply->deleteLater(); } bool Network::sendConversation(const QString &ingestId, const QString &conversation) { return packageAndSendJson(ingestId, conversation); } void Network::sendHealth() { QUrl healthUrl("https://api.gpt4all.io/v1/health"); QNetworkRequest request(healthUrl); QSslConfiguration conf = request.sslConfiguration(); conf.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(conf); QNetworkReply *healthReply = m_networkManager.get(request); connect(healthReply, &QNetworkReply::finished, this, &Network::handleHealthFinished); } void Network::handleHealthFinished() { QNetworkReply *healthReply = qobject_cast(sender()); if (!healthReply) return; QVariant response = healthReply->attribute(QNetworkRequest::HttpStatusCodeAttribute); Q_ASSERT(response.isValid()); bool ok; int code = response.toInt(&ok); if (!ok) qWarning() << "ERROR: health invalid response."; if (code != 200) { qWarning() << "ERROR: health response != 200 code:" << code; emit healthCheckFailed(code); setActive(false); } healthReply->deleteLater(); }