gpt4all/network.cpp
Adam Treat 01e582f15b First attempt at providing a persistent chat list experience.
Limitations:

1) Context is not restored for gpt-j models
2) When you switch between different model types in an existing chat
   the context and all the conversation is lost
3) The settings are not chat or conversation specific
4) The sizes of the chat persisted files are very large due to how much
   data the llama.cpp backend tries to persist. Need to investigate how
   we can shrink this.
2023-05-04 15:31:41 -04:00

515 lines
14 KiB
C++

#include "network.h"
#include "llm.h"
#include "sysinfo.h"
#include <QCoreApplication>
#include <QGuiApplication>
#include <QUuid>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QSettings>
#include <QNetworkRequest>
#include <QScreen>
//#define DEBUG
#if defined(Q_OS_MAC)
#include <sys/sysctl.h>
std::string getCPUModel() {
char buffer[256];
size_t bufferlen = sizeof(buffer);
sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferlen, NULL, 0);
return std::string(buffer);
}
#endif
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());
Q_ASSERT(LLM::globalInstance()->chatListModel()->currentChat());
QJsonObject object = doc.object();
object.insert("source", "gpt4all-chat");
object.insert("agent_id", LLM::globalInstance()->chatListModel()->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<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();
}
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<QSslError> &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(int conversationLength)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("length");
kv.value = QJsonValue(conversationLength);
sendMixpanelEvent("reset_context", QVector<KeyValue>{kv});
}
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::sendModelDownloaderDialog()
{
if (!m_usageStatsActive)
return;
sendMixpanelEvent("download_dialog");
}
void Network::sendDownloadStarted(const QString &model)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
sendMixpanelEvent("download_started", QVector<KeyValue>{kv});
}
void Network::sendDownloadCanceled(const QString &model)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
sendMixpanelEvent("download_canceled", QVector<KeyValue>{kv});
}
void Network::sendDownloadError(const QString &model, int code, const QString &errorString)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
KeyValue kvCode;
kvCode.key = QString("code");
kvCode.value = QJsonValue(code);
KeyValue kvError;
kvError.key = QString("error");
kvError.value = QJsonValue(errorString);
sendMixpanelEvent("download_error", QVector<KeyValue>{kv, kvCode, kvError});
}
void Network::sendDownloadFinished(const QString &model, bool success)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
KeyValue kvSuccess;
kvSuccess.key = QString("success");
kvSuccess.value = QJsonValue(success);
sendMixpanelEvent("download_finished", QVector<KeyValue>{kv, kvSuccess});
}
void Network::sendSettingsDialog()
{
if (!m_usageStatsActive)
return;
sendMixpanelEvent("settings_dialog");
}
void Network::sendNetworkToggled(bool isActive)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("isActive");
kv.value = QJsonValue(isActive);
sendMixpanelEvent("network_toggled", QVector<KeyValue>{kv});
}
void Network::sendNewChat(int count)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("number_of_chats");
kv.value = QJsonValue(count);
sendMixpanelEvent("new_chat", QVector<KeyValue>{kv});
}
void Network::sendRemoveChat()
{
if (!m_usageStatsActive)
return;
sendMixpanelEvent("remove_chat");
}
void Network::sendRenameChat()
{
if (!m_usageStatsActive)
return;
sendMixpanelEvent("rename_chat");
}
void Network::sendChatStarted()
{
if (!m_usageStatsActive)
return;
sendMixpanelEvent("chat_started");
}
void Network::sendRecalculatingContext(int conversationLength)
{
if (!m_usageStatsActive)
return;
KeyValue kv;
kv.key = QString("length");
kv.value = QJsonValue(conversationLength);
sendMixpanelEvent("recalc_context", QVector<KeyValue>{kv});
}
void Network::sendMixpanelEvent(const QString &ev, const QVector<KeyValue> &values)
{
if (!m_usageStatsActive)
return;
Q_ASSERT(LLM::globalInstance()->chatListModel()->currentChat());
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()->chatListModel()->currentChat()->modelName());
// Some additional startup information
if (ev == "startup") {
const QSize display = QGuiApplication::primaryScreen()->size();
properties.insert("display", QString("%1x%2").arg(display.width()).arg(display.height()));
properties.insert("ram", getSystemTotalRAM());
#if defined(__x86_64__) || defined(__i386__)
properties.insert("avx", bool(__builtin_cpu_supports("avx")));
properties.insert("avx2", bool(__builtin_cpu_supports("avx2")));
properties.insert("fma", bool(__builtin_cpu_supports("fma")));
#endif
#if defined(Q_OS_MAC)
properties.insert("cpu", QString::fromStdString(getCPUModel()));
#endif
}
for (auto p : values)
properties.insert(p.key, p.value);
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<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 = 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()
{
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(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);
setActive(false);
}
healthReply->deleteLater();
}