gpt4all/gpt4all-chat/network.cpp
Jared Van Bortel 01870b4a46
chat: fix blank device in UI and improve Mixpanel reporting (#2409)
Also remove LLModel::hasGPUDevice.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-06-26 15:26:27 -04:00

460 lines
14 KiB
C++

#include "network.h"
#include "chat.h"
#include "chatlistmodel.h"
#include "download.h"
#include "llm.h"
#include "localdocs.h"
#include "localdocsmodel.h"
#include "modellist.h"
#include "mysettings.h"
#include "../gpt4all-backend/llmodel.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QGlobalStatic>
#include <QGuiApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkRequest>
#include <QScreen>
#include <QSettings>
#include <QSize>
#include <QSslConfiguration>
#include <QSslSocket>
#include <QSysInfo>
#include <Qt>
#include <QtGlobal>
#include <QtLogging>
#include <QUrl>
#include <QUuid>
#include <cmath>
#include <cstring>
#include <utility>
using namespace Qt::Literals::StringLiterals;
//#define DEBUG
static const char MIXPANEL_TOKEN[] = "ce362e568ddaee16ed243eaffb5860a2";
#if defined(Q_OS_MAC)
#include <sys/sysctl.h>
static QString getCPUModel()
{
char buffer[256];
size_t bufferlen = sizeof(buffer);
sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferlen, NULL, 0);
return buffer;
}
#elif defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)
#ifndef _MSC_VER
static void get_cpuid(int level, int *regs)
{
asm volatile("cpuid" : "=a" (regs[0]), "=b" (regs[1]), "=c" (regs[2]), "=d" (regs[3]) : "0" (level) : "memory");
}
#else
#define get_cpuid(level, regs) __cpuid(regs, level)
#endif
static QString getCPUModel()
{
int regs[12];
// EAX=800000000h: Get Highest Extended Function Implemented
get_cpuid(0x80000000, regs);
if (regs[0] < 0x80000004)
return "(unknown)";
// EAX=800000002h-800000004h: Processor Brand String
get_cpuid(0x80000002, regs);
get_cpuid(0x80000003, regs + 4);
get_cpuid(0x80000004, regs + 8);
char str[sizeof(regs) + 1];
memcpy(str, regs, sizeof(regs));
str[sizeof(regs)] = 0;
return QString(str).trimmed();
}
#else
static QString getCPUModel() { return "(non-x86)"; }
#endif
class MyNetwork: public Network { };
Q_GLOBAL_STATIC(MyNetwork, networkInstance)
Network *Network::globalInstance()
{
return networkInstance();
}
Network::Network()
: QObject{nullptr}
{
QSettings settings;
m_uniqueId = settings.value("uniqueId", generateUniqueId()).toString();
settings.setValue("uniqueId", m_uniqueId);
m_sessionId = generateUniqueId();
// allow sendMixpanel to be called from any thread
connect(this, &Network::requestMixpanel, this, &Network::sendMixpanel, Qt::QueuedConnection);
const auto *mySettings = MySettings::globalInstance();
connect(mySettings, &MySettings::networkIsActiveChanged, this, &Network::handleIsActiveChanged);
connect(mySettings, &MySettings::networkUsageStatsActiveChanged, this, &Network::handleUsageStatsActiveChanged);
m_hasSentOptIn = !Download::globalInstance()->isFirstStart() && mySettings->networkUsageStatsActive();
m_hasSentOptOut = !Download::globalInstance()->isFirstStart() && !mySettings->networkUsageStatsActive();
if (mySettings->networkIsActive())
sendHealth();
connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this,
&Network::handleSslErrors);
}
// NOTE: this won't be useful until we make it possible to change this via the settings page
void Network::handleUsageStatsActiveChanged()
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
m_sendUsageStats = false;
}
void Network::handleIsActiveChanged()
{
if (MySettings::globalInstance()->networkUsageStatsActive())
sendHealth();
}
QString Network::generateUniqueId() const
{
return QUuid::createUuid().toString(QUuid::WithoutBraces);
}
bool Network::packageAndSendJson(const QString &ingestId, const QString &json)
{
if (!MySettings::globalInstance()->networkIsActive())
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());
Q_ASSERT(ChatListModel::globalInstance()->currentChat());
QJsonObject object = doc.object();
object.insert("source", "gpt4all-chat");
object.insert("agent_id", ChatListModel::globalInstance()->currentChat()->modelInfo().filename());
object.insert("submitter_id", m_uniqueId);
object.insert("ingest_id", ingestId);
QString attribution = MySettings::globalInstance()->networkAttribution();
if (!attribution.isEmpty())
object.insert("network/attribution", attribution);
QString promptTemplate = ChatListModel::globalInstance()->currentChat()->modelInfo().promptTemplate();
object.insert("prompt_template", promptTemplate);
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(QJsonDocument::Compact));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *jsonReply = m_networkManager.post(request, body);
connect(qGuiApp, &QCoreApplication::aboutToQuit, jsonReply, &QNetworkReply::abort);
connect(jsonReply, &QNetworkReply::finished, this, &Network::handleJsonUploadFinished);
m_activeUploads.append(jsonReply);
return true;
}
void Network::handleJsonUploadFinished()
{
QNetworkReply *jsonReply = qobject_cast<QNetworkReply *>(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();
}
#if defined(DEBUG)
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;
}
printf("%s\n", qPrintable(document.toJson(QJsonDocument::Indented)));
fflush(stdout);
#endif
jsonReply->deleteLater();
}
void Network::handleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
{
QUrl url = reply->request().url();
for (const auto &e : errors)
qWarning() << "ERROR: Received ssl error:" << e.errorString() << "for" << url;
}
void Network::sendOptOut()
{
QJsonObject properties;
properties.insert("token", MIXPANEL_TOKEN);
properties.insert("time", QDateTime::currentMSecsSinceEpoch());
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);
emit requestMixpanel(doc.toJson(QJsonDocument::Compact));
#if defined(DEBUG)
printf("%s %s\n", qPrintable("opt_out"), qPrintable(doc.toJson(QJsonDocument::Indented)));
fflush(stdout);
#endif
}
void Network::sendStartup()
{
const auto *mySettings = MySettings::globalInstance();
Q_ASSERT(mySettings->isNetworkUsageStatsActiveSet());
if (!mySettings->networkUsageStatsActive()) {
// send a single opt-out per session after the user has made their selections,
// unless this is a normal start (same version) and the user was already opted out
if (!m_hasSentOptOut) {
sendOptOut();
m_hasSentOptOut = true;
}
return;
}
// only chance to enable usage stats is at the start of a new session
m_sendUsageStats = true;
const auto *display = QGuiApplication::primaryScreen();
trackEvent("startup", {
{"$screen_dpi", std::round(display->physicalDotsPerInch())},
{"display", u"%1x%2"_s.arg(display->size().width()).arg(display->size().height())},
{"ram", LLM::globalInstance()->systemTotalRAMInGB()},
{"cpu", getCPUModel()},
{"cpu_supports_avx2", LLModel::Implementation::cpuSupportsAVX2()},
{"datalake_active", mySettings->networkIsActive()},
});
sendIpify();
// mirror opt-out logic so the ratio can be used to infer totals
if (!m_hasSentOptIn) {
trackEvent("opt_in");
m_hasSentOptIn = true;
}
}
void Network::trackChatEvent(const QString &ev, QVariantMap props)
{
const auto &curChat = ChatListModel::globalInstance()->currentChat();
if (!props.contains("model"))
props.insert("model", curChat->modelInfo().filename());
props.insert("device_backend", curChat->deviceBackend());
props.insert("actualDevice", curChat->device());
props.insert("doc_collections_enabled", curChat->collectionList().count());
props.insert("doc_collections_total", LocalDocs::globalInstance()->localDocsModel()->rowCount());
props.insert("datalake_active", MySettings::globalInstance()->networkIsActive());
props.insert("using_server", ChatListModel::globalInstance()->currentChat()->isServer());
trackEvent(ev, props);
}
void Network::trackEvent(const QString &ev, const QVariantMap &props)
{
if (!m_sendUsageStats)
return;
Q_ASSERT(ChatListModel::globalInstance()->currentChat());
QJsonObject properties;
properties.insert("token", MIXPANEL_TOKEN);
if (!props.contains("time"))
properties.insert("time", QDateTime::currentMSecsSinceEpoch());
properties.insert("distinct_id", m_uniqueId); // effectively a device ID
properties.insert("$insert_id", generateUniqueId());
if (!m_ipify.isEmpty())
properties.insert("ip", m_ipify);
properties.insert("$os", QSysInfo::prettyProductName());
properties.insert("session_id", m_sessionId);
properties.insert("name", QCoreApplication::applicationName() + " v" + QCoreApplication::applicationVersion());
for (const auto &[key, value]: props.asKeyValueRange())
properties.insert(key, QJsonValue::fromVariant(value));
QJsonObject event;
event.insert("event", ev);
event.insert("properties", properties);
QJsonArray array;
array.append(event);
QJsonDocument doc;
doc.setArray(array);
emit requestMixpanel(doc.toJson(QJsonDocument::Compact));
#if defined(DEBUG)
printf("%s %s\n", qPrintable(ev), qPrintable(doc.toJson(QJsonDocument::Indented)));
fflush(stdout);
#endif
}
void Network::sendIpify()
{
if (!m_sendUsageStats || !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(qGuiApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort);
connect(reply, &QNetworkReply::finished, this, &Network::handleIpifyFinished);
}
void Network::sendMixpanel(const QByteArray &json)
{
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(qGuiApp, &QCoreApplication::aboutToQuit, trackReply, &QNetworkReply::abort);
connect(trackReply, &QNetworkReply::finished, this, &Network::handleMixpanelFinished);
}
void Network::handleIpifyFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(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 = reply->readAll();
#if defined(DEBUG)
printf("ipify finished %s\n", qPrintable(m_ipify));
fflush(stdout);
#endif
reply->deleteLater();
trackEvent("ipify_complete");
}
void Network::handleMixpanelFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(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(qGuiApp, &QCoreApplication::aboutToQuit, healthReply, &QNetworkReply::abort);
connect(healthReply, &QNetworkReply::finished, this, &Network::handleHealthFinished);
}
void Network::handleHealthFinished()
{
QNetworkReply *healthReply = qobject_cast<QNetworkReply *>(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);
MySettings::globalInstance()->setNetworkIsActive(false);
}
healthReply->deleteLater();
}