improve mixpanel usage statistics (#2238)

Other changes:
- Always display first start dialog if privacy options are unset (e.g. if the user closed GPT4All without selecting them)
- LocalDocs scanQueue is now always deferred
- Fix a potential crash in magic_match
- LocalDocs indexing is now started after the first start dialog is dismissed so usage stats are included

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
pull/2266/head
Jared Van Bortel 4 weeks ago committed by GitHub
parent 4193533154
commit c622921894
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -785,7 +785,7 @@ const std::vector<LLModel::Token> &GPTJ::endTokens() const
return fres; return fres;
} }
std::string get_arch_name(gguf_context *ctx_gguf) { const char *get_arch_name(gguf_context *ctx_gguf) {
std::string arch_name; std::string arch_name;
const int kid = gguf_find_key(ctx_gguf, "general.architecture"); const int kid = gguf_find_key(ctx_gguf, "general.architecture");
enum gguf_type ktype = gguf_get_kv_type(ctx_gguf, kid); enum gguf_type ktype = gguf_get_kv_type(ctx_gguf, kid);
@ -814,21 +814,25 @@ DLL_EXPORT const char *get_build_variant() {
return GGML_BUILD_VARIANT; return GGML_BUILD_VARIANT;
} }
DLL_EXPORT bool magic_match(const char * fname) { DLL_EXPORT char *get_file_arch(const char *fname) {
struct ggml_context * ctx_meta = NULL; struct ggml_context * ctx_meta = NULL;
struct gguf_init_params params = { struct gguf_init_params params = {
/*.no_alloc = */ true, /*.no_alloc = */ true,
/*.ctx = */ &ctx_meta, /*.ctx = */ &ctx_meta,
}; };
gguf_context *ctx_gguf = gguf_init_from_file(fname, params); gguf_context *ctx_gguf = gguf_init_from_file(fname, params);
if (!ctx_gguf)
return false;
bool isValid = gguf_get_version(ctx_gguf) <= 3; char *arch = nullptr;
isValid = isValid && get_arch_name(ctx_gguf) == "gptj"; if (ctx_gguf && gguf_get_version(ctx_gguf) <= 3) {
arch = strdup(get_arch_name(ctx_gguf));
}
gguf_free(ctx_gguf); gguf_free(ctx_gguf);
return isValid; return arch;
}
DLL_EXPORT bool is_arch_supported(const char *arch) {
return !strcmp(arch, "gptj");
} }
DLL_EXPORT LLModel *construct() { DLL_EXPORT LLModel *construct() {

@ -104,7 +104,7 @@ static int llama_sample_top_p_top_k(
return llama_sample_token(ctx, &candidates_p); return llama_sample_token(ctx, &candidates_p);
} }
std::string get_arch_name(gguf_context *ctx_gguf) { const char *get_arch_name(gguf_context *ctx_gguf) {
std::string arch_name; std::string arch_name;
const int kid = gguf_find_key(ctx_gguf, "general.architecture"); const int kid = gguf_find_key(ctx_gguf, "general.architecture");
enum gguf_type ktype = gguf_get_kv_type(ctx_gguf, kid); enum gguf_type ktype = gguf_get_kv_type(ctx_gguf, kid);
@ -961,25 +961,23 @@ DLL_EXPORT const char *get_build_variant() {
return GGML_BUILD_VARIANT; return GGML_BUILD_VARIANT;
} }
DLL_EXPORT bool magic_match(const char *fname) { DLL_EXPORT char *get_file_arch(const char *fname) {
auto * ctx = load_gguf(fname); auto *ctx = load_gguf(fname);
std::string arch = get_arch_name(ctx); char *arch = nullptr;
if (ctx) {
bool valid = true; std::string archStr = get_arch_name(ctx);
if (is_embedding_arch(archStr) && gguf_find_key(ctx, (archStr + ".pooling_type").c_str()) < 0) {
if (std::find(KNOWN_ARCHES.begin(), KNOWN_ARCHES.end(), arch) == KNOWN_ARCHES.end()) { // old bert.cpp embedding model
// not supported by this version of llama.cpp } else {
if (arch != "gptj") { // we support this via another module arch = strdup(archStr.c_str());
std::cerr << __func__ << ": unsupported model architecture: " << arch << "\n";
} }
valid = false;
} }
if (valid && is_embedding_arch(arch) && gguf_find_key(ctx, (arch + ".pooling_type").c_str()) < 0)
valid = false; // old pre-llama.cpp embedding model, e.g. all-MiniLM-L6-v2-f16.gguf
gguf_free(ctx); gguf_free(ctx);
return valid; return arch;
}
DLL_EXPORT bool is_arch_supported(const char *arch) {
return std::find(KNOWN_ARCHES.begin(), KNOWN_ARCHES.end(), std::string(arch)) < KNOWN_ARCHES.end();
} }
DLL_EXPORT LLModel *construct() { DLL_EXPORT LLModel *construct() {

@ -8,6 +8,7 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <optional>
#include <regex> #include <regex>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -49,14 +50,17 @@ LLModel::Implementation::Implementation(Dlhandle &&dlhandle_)
auto get_build_variant = m_dlhandle->get<const char *()>("get_build_variant"); auto get_build_variant = m_dlhandle->get<const char *()>("get_build_variant");
assert(get_build_variant); assert(get_build_variant);
m_buildVariant = get_build_variant(); m_buildVariant = get_build_variant();
m_magicMatch = m_dlhandle->get<bool(const char*)>("magic_match"); m_getFileArch = m_dlhandle->get<char *(const char *)>("get_file_arch");
assert(m_magicMatch); assert(m_getFileArch);
m_isArchSupported = m_dlhandle->get<bool(const char *)>("is_arch_supported");
assert(m_isArchSupported);
m_construct = m_dlhandle->get<LLModel *()>("construct"); m_construct = m_dlhandle->get<LLModel *()>("construct");
assert(m_construct); assert(m_construct);
} }
LLModel::Implementation::Implementation(Implementation &&o) LLModel::Implementation::Implementation(Implementation &&o)
: m_magicMatch(o.m_magicMatch) : m_getFileArch(o.m_getFileArch)
, m_isArchSupported(o.m_isArchSupported)
, m_construct(o.m_construct) , m_construct(o.m_construct)
, m_modelType(o.m_modelType) , m_modelType(o.m_modelType)
, m_buildVariant(o.m_buildVariant) , m_buildVariant(o.m_buildVariant)
@ -123,18 +127,26 @@ const std::vector<LLModel::Implementation> &LLModel::Implementation::implementat
const LLModel::Implementation* LLModel::Implementation::implementation(const char *fname, const std::string& buildVariant) { const LLModel::Implementation* LLModel::Implementation::implementation(const char *fname, const std::string& buildVariant) {
bool buildVariantMatched = false; bool buildVariantMatched = false;
std::optional<std::string> archName;
for (const auto& i : implementationList()) { for (const auto& i : implementationList()) {
if (buildVariant != i.m_buildVariant) continue; if (buildVariant != i.m_buildVariant) continue;
buildVariantMatched = true; buildVariantMatched = true;
if (!i.m_magicMatch(fname)) continue; char *arch = i.m_getFileArch(fname);
return &i; if (!arch) continue;
archName = arch;
bool archSupported = i.m_isArchSupported(arch);
free(arch);
if (archSupported) return &i;
} }
if (!buildVariantMatched) if (!buildVariantMatched)
throw std::runtime_error("Could not find any implementations for build variant: " + buildVariant); throw MissingImplementationError("Could not find any implementations for build variant: " + buildVariant);
if (!archName)
throw UnsupportedModelError("Unsupported file format");
return nullptr; // unsupported model format throw BadArchError(std::move(*archName));
} }
LLModel *LLModel::Implementation::construct(const std::string &modelPath, std::string buildVariant, int n_ctx) { LLModel *LLModel::Implementation::construct(const std::string &modelPath, std::string buildVariant, int n_ctx) {
@ -144,7 +156,11 @@ LLModel *LLModel::Implementation::construct(const std::string &modelPath, std::s
#if defined(__APPLE__) && defined(__arm64__) // FIXME: See if metal works for intel macs #if defined(__APPLE__) && defined(__arm64__) // FIXME: See if metal works for intel macs
if (buildVariant == "auto") { if (buildVariant == "auto") {
size_t total_mem = getSystemTotalRAMInBytes(); size_t total_mem = getSystemTotalRAMInBytes();
impl = implementation(modelPath.c_str(), "metal"); try {
impl = implementation(modelPath.c_str(), "metal");
} catch (const std::exception &e) {
// fall back to CPU
}
if(impl) { if(impl) {
LLModel* metalimpl = impl->m_construct(); LLModel* metalimpl = impl->m_construct();
metalimpl->m_implementation = impl; metalimpl->m_implementation = impl;
@ -177,7 +193,6 @@ LLModel *LLModel::Implementation::construct(const std::string &modelPath, std::s
} }
} }
impl = implementation(modelPath.c_str(), buildVariant); impl = implementation(modelPath.c_str(), buildVariant);
if (!impl) return nullptr;
} }
// Construct and return llmodel implementation // Construct and return llmodel implementation

@ -17,6 +17,29 @@ class LLModel {
public: public:
using Token = int32_t; using Token = int32_t;
class BadArchError: public std::runtime_error {
public:
BadArchError(std::string arch)
: runtime_error("Unsupported model architecture: " + arch)
, m_arch(std::move(arch))
{}
const std::string &arch() const noexcept { return m_arch; }
private:
std::string m_arch;
};
class MissingImplementationError: public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class UnsupportedModelError: public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
struct GPUDevice { struct GPUDevice {
int index; int index;
int type; int type;
@ -53,7 +76,8 @@ public:
static const Implementation *implementation(const char *fname, const std::string &buildVariant); static const Implementation *implementation(const char *fname, const std::string &buildVariant);
static LLModel *constructDefaultLlama(); static LLModel *constructDefaultLlama();
bool (*m_magicMatch)(const char *fname); char *(*m_getFileArch)(const char *fname);
bool (*m_isArchSupported)(const char *arch);
LLModel *(*m_construct)(); LLModel *(*m_construct)();
std::string_view m_modelType; std::string_view m_modelType;

@ -40,11 +40,6 @@ llmodel_model llmodel_model_create2(const char *model_path, const char *build_va
return nullptr; return nullptr;
} }
if (!llModel) {
llmodel_set_error(error, "Model format not supported (no matching implementation found)");
return nullptr;
}
auto wrapper = new LLModelWrapper; auto wrapper = new LLModelWrapper;
wrapper->llModel = llModel; wrapper->llModel = llModel;
return wrapper; return wrapper;

@ -179,7 +179,7 @@ void Chat::promptProcessing()
emit responseStateChanged(); emit responseStateChanged();
} }
void Chat::responseStopped() void Chat::responseStopped(qint64 promptResponseMs)
{ {
m_tokenSpeed = QString(); m_tokenSpeed = QString();
emit tokenSpeedChanged(); emit tokenSpeedChanged();
@ -228,8 +228,13 @@ void Chat::responseStopped()
emit responseStateChanged(); emit responseStateChanged();
if (m_generatedName.isEmpty()) if (m_generatedName.isEmpty())
emit generateNameRequested(); emit generateNameRequested();
if (chatModel()->count() < 3)
Network::globalInstance()->sendChatStarted(); Network::globalInstance()->trackChatEvent("response_complete", {
{"first", m_firstResponse},
{"message_count", chatModel()->count()},
{"$duration", promptResponseMs / 1000.},
});
m_firstResponse = false;
} }
ModelInfo Chat::modelInfo() const ModelInfo Chat::modelInfo() const
@ -331,7 +336,7 @@ void Chat::generatedNameChanged(const QString &name)
void Chat::handleRecalculating() void Chat::handleRecalculating()
{ {
Network::globalInstance()->sendRecalculatingContext(m_chatModel->count()); Network::globalInstance()->trackChatEvent("recalc_context", { {"length", m_chatModel->count()} });
emit recalcChanged(); emit recalcChanged();
} }

@ -143,7 +143,7 @@ private Q_SLOTS:
void handleResponseChanged(const QString &response); void handleResponseChanged(const QString &response);
void handleModelLoadingPercentageChanged(float); void handleModelLoadingPercentageChanged(float);
void promptProcessing(); void promptProcessing();
void responseStopped(); void responseStopped(qint64 promptResponseMs);
void generatedNameChanged(const QString &name); void generatedNameChanged(const QString &name);
void handleRecalculating(); void handleRecalculating();
void handleModelLoadingError(const QString &error); void handleModelLoadingError(const QString &error);
@ -175,6 +175,7 @@ private:
bool m_shouldDeleteLater = false; bool m_shouldDeleteLater = false;
float m_modelLoadingPercentage = 0.0f; float m_modelLoadingPercentage = 0.0f;
LocalDocsCollectionsModel *m_collectionModel; LocalDocsCollectionsModel *m_collectionModel;
bool m_firstResponse = true;
}; };
#endif // CHAT_H #endif // CHAT_H

@ -7,6 +7,8 @@
#include "mysettings.h" #include "mysettings.h"
#include "../gpt4all-backend/llmodel.h" #include "../gpt4all-backend/llmodel.h"
#include <QElapsedTimer>
//#define DEBUG //#define DEBUG
//#define DEBUG_MODEL_LOADING //#define DEBUG_MODEL_LOADING
@ -74,8 +76,6 @@ ChatLLM::ChatLLM(Chat *parent, bool isServer)
, m_restoreStateFromText(false) , m_restoreStateFromText(false)
{ {
moveToThread(&m_llmThread); moveToThread(&m_llmThread);
connect(this, &ChatLLM::sendStartup, Network::globalInstance(), &Network::sendStartup);
connect(this, &ChatLLM::sendModelLoaded, Network::globalInstance(), &Network::sendModelLoaded);
connect(this, &ChatLLM::shouldBeLoadedChanged, this, &ChatLLM::handleShouldBeLoadedChanged, connect(this, &ChatLLM::shouldBeLoadedChanged, this, &ChatLLM::handleShouldBeLoadedChanged,
Qt::QueuedConnection); // explicitly queued Qt::QueuedConnection); // explicitly queued
connect(this, &ChatLLM::shouldTrySwitchContextChanged, this, &ChatLLM::handleShouldTrySwitchContextChanged, connect(this, &ChatLLM::shouldTrySwitchContextChanged, this, &ChatLLM::handleShouldTrySwitchContextChanged,
@ -278,6 +278,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
m_llModelInfo.fileInfo = fileInfo; m_llModelInfo.fileInfo = fileInfo;
if (fileInfo.exists()) { if (fileInfo.exists()) {
QVariantMap modelLoadProps;
if (modelInfo.isOnline) { if (modelInfo.isOnline) {
QString apiKey; QString apiKey;
QString modelName; QString modelName;
@ -298,6 +299,9 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
model->setAPIKey(apiKey); model->setAPIKey(apiKey);
m_llModelInfo.model = model; m_llModelInfo.model = model;
} else { } else {
QElapsedTimer modelLoadTimer;
modelLoadTimer.start();
auto n_ctx = MySettings::globalInstance()->modelContextLength(modelInfo); auto n_ctx = MySettings::globalInstance()->modelContextLength(modelInfo);
m_ctx.n_ctx = n_ctx; m_ctx.n_ctx = n_ctx;
auto ngl = MySettings::globalInstance()->modelGpuLayers(modelInfo); auto ngl = MySettings::globalInstance()->modelGpuLayers(modelInfo);
@ -307,7 +311,21 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
if (m_forceMetal) if (m_forceMetal)
buildVariant = "metal"; buildVariant = "metal";
#endif #endif
m_llModelInfo.model = LLModel::Implementation::construct(filePath.toStdString(), buildVariant, n_ctx); QString constructError;
m_llModelInfo.model = nullptr;
try {
m_llModelInfo.model = LLModel::Implementation::construct(filePath.toStdString(), buildVariant, n_ctx);
} catch (const LLModel::MissingImplementationError &e) {
modelLoadProps.insert("error", "missing_model_impl");
constructError = e.what();
} catch (const LLModel::UnsupportedModelError &e) {
modelLoadProps.insert("error", "unsupported_model_file");
constructError = e.what();
} catch (const LLModel::BadArchError &e) {
constructError = e.what();
modelLoadProps.insert("error", "unsupported_model_arch");
modelLoadProps.insert("model_arch", QString::fromStdString(e.arch()));
}
if (m_llModelInfo.model) { if (m_llModelInfo.model) {
if (m_llModelInfo.model->isModelBlacklisted(filePath.toStdString())) { if (m_llModelInfo.model->isModelBlacklisted(filePath.toStdString())) {
@ -368,6 +386,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
// llama_init_from_file returned nullptr // llama_init_from_file returned nullptr
emit reportDevice("CPU"); emit reportDevice("CPU");
emit reportFallbackReason("<br>GPU loading failed (out of VRAM?)"); emit reportFallbackReason("<br>GPU loading failed (out of VRAM?)");
modelLoadProps.insert("cpu_fallback_reason", "gpu_load_failed");
success = m_llModelInfo.model->loadModel(filePath.toStdString(), n_ctx, 0); success = m_llModelInfo.model->loadModel(filePath.toStdString(), n_ctx, 0);
} else if (!m_llModelInfo.model->usingGPUDevice()) { } else if (!m_llModelInfo.model->usingGPUDevice()) {
// ggml_vk_init was not called in llama.cpp // ggml_vk_init was not called in llama.cpp
@ -375,6 +394,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
// for instance if the quantization method is not supported on Vulkan yet // for instance if the quantization method is not supported on Vulkan yet
emit reportDevice("CPU"); emit reportDevice("CPU");
emit reportFallbackReason("<br>model or quant has no GPU support"); emit reportFallbackReason("<br>model or quant has no GPU support");
modelLoadProps.insert("cpu_fallback_reason", "gpu_unsupported_model");
} }
if (!success) { if (!success) {
@ -384,6 +404,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store
m_llModelInfo = LLModelInfo(); m_llModelInfo = LLModelInfo();
emit modelLoadingError(QString("Could not load model due to invalid model file for %1").arg(modelInfo.filename())); emit modelLoadingError(QString("Could not load model due to invalid model file for %1").arg(modelInfo.filename()));
modelLoadProps.insert("error", "loadmodel_failed");
} else { } else {
switch (m_llModelInfo.model->implementation().modelType()[0]) { switch (m_llModelInfo.model->implementation().modelType()[0]) {
case 'L': m_llModelType = LLModelType::LLAMA_; break; case 'L': m_llModelType = LLModelType::LLAMA_; break;
@ -398,12 +419,14 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
emit modelLoadingError(QString("Could not determine model type for %1").arg(modelInfo.filename())); emit modelLoadingError(QString("Could not determine model type for %1").arg(modelInfo.filename()));
} }
} }
modelLoadProps.insert("$duration", modelLoadTimer.elapsed() / 1000.);
} }
} else { } else {
if (!m_isServer) if (!m_isServer)
LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store
m_llModelInfo = LLModelInfo(); m_llModelInfo = LLModelInfo();
emit modelLoadingError(QString("Could not load model due to invalid format for %1").arg(modelInfo.filename())); emit modelLoadingError(QString("Error loading %1: %2").arg(modelInfo.filename()).arg(constructError));
} }
} }
#if defined(DEBUG_MODEL_LOADING) #if defined(DEBUG_MODEL_LOADING)
@ -416,12 +439,9 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
#endif #endif
emit modelLoadingPercentageChanged(isModelLoaded() ? 1.0f : 0.0f); emit modelLoadingPercentageChanged(isModelLoaded() ? 1.0f : 0.0f);
static bool isFirstLoad = true; modelLoadProps.insert("requestedDevice", MySettings::globalInstance()->device());
if (isFirstLoad) { modelLoadProps.insert("model", modelInfo.filename());
emit sendStartup(); Network::globalInstance()->trackChatEvent("model_load", modelLoadProps);
isFirstLoad = false;
} else
emit sendModelLoaded();
} else { } else {
if (!m_isServer) if (!m_isServer)
LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store
@ -632,6 +652,8 @@ bool ChatLLM::promptInternal(const QList<QString> &collectionList, const QString
printf("%s", qPrintable(prompt)); printf("%s", qPrintable(prompt));
fflush(stdout); fflush(stdout);
#endif #endif
QElapsedTimer totalTime;
totalTime.start();
m_timer->start(); m_timer->start();
if (!docsContext.isEmpty()) { if (!docsContext.isEmpty()) {
auto old_n_predict = std::exchange(m_ctx.n_predict, 0); // decode localdocs context without a response auto old_n_predict = std::exchange(m_ctx.n_predict, 0); // decode localdocs context without a response
@ -644,12 +666,13 @@ bool ChatLLM::promptInternal(const QList<QString> &collectionList, const QString
fflush(stdout); fflush(stdout);
#endif #endif
m_timer->stop(); m_timer->stop();
qint64 elapsed = totalTime.elapsed();
std::string trimmed = trim_whitespace(m_response); std::string trimmed = trim_whitespace(m_response);
if (trimmed != m_response) { if (trimmed != m_response) {
m_response = trimmed; m_response = trimmed;
emit responseChanged(QString::fromStdString(m_response)); emit responseChanged(QString::fromStdString(m_response));
} }
emit responseStopped(); emit responseStopped(elapsed);
return true; return true;
} }

@ -123,9 +123,7 @@ Q_SIGNALS:
void modelLoadingWarning(const QString &warning); void modelLoadingWarning(const QString &warning);
void responseChanged(const QString &response); void responseChanged(const QString &response);
void promptProcessing(); void promptProcessing();
void responseStopped(); void responseStopped(qint64 promptResponseMs);
void sendStartup();
void sendModelLoaded();
void generatedNameChanged(const QString &name); void generatedNameChanged(const QString &name);
void stateChanged(); void stateChanged();
void threadStarted(); void threadStarted();

@ -1,7 +1,9 @@
#include "database.h" #include "database.h"
#include "mysettings.h"
#include "embllm.h"
#include "embeddings.h" #include "embeddings.h"
#include "embllm.h"
#include "mysettings.h"
#include "network.h"
#include <QTimer> #include <QTimer>
#include <QPdfDocument> #include <QPdfDocument>
@ -490,7 +492,7 @@ QSqlError initDb()
i.collection = collection_name; i.collection = collection_name;
i.folder_path = folder_path; i.folder_path = folder_path;
i.folder_id = folder_id; i.folder_id = folder_id;
emit addCollectionItem(i); emit addCollectionItem(i, false);
// Add a document // Add a document
int document_time = 123456789; int document_time = 123456789;
@ -535,13 +537,13 @@ QSqlError initDb()
Database::Database(int chunkSize) Database::Database(int chunkSize)
: QObject(nullptr) : QObject(nullptr)
, m_watcher(new QFileSystemWatcher(this))
, m_chunkSize(chunkSize) , m_chunkSize(chunkSize)
, m_scanTimer(new QTimer(this))
, m_watcher(new QFileSystemWatcher(this))
, m_embLLM(new EmbeddingLLM) , m_embLLM(new EmbeddingLLM)
, m_embeddings(new Embeddings(this)) , m_embeddings(new Embeddings(this))
{ {
moveToThread(&m_dbThread); moveToThread(&m_dbThread);
connect(&m_dbThread, &QThread::started, this, &Database::start);
m_dbThread.setObjectName("database"); m_dbThread.setObjectName("database");
m_dbThread.start(); m_dbThread.start();
} }
@ -556,11 +558,13 @@ void Database::scheduleNext(int folder_id, size_t countForFolder)
{ {
emit updateCurrentDocsToIndex(folder_id, countForFolder); emit updateCurrentDocsToIndex(folder_id, countForFolder);
if (!countForFolder) { if (!countForFolder) {
emit updateIndexing(folder_id, false); updateFolderStatus(folder_id, FolderStatus::Complete);
emit updateInstalled(folder_id, true); emit updateInstalled(folder_id, true);
} }
if (!m_docsToScan.isEmpty()) if (m_docsToScan.isEmpty()) {
QTimer::singleShot(0, this, &Database::scanQueue); m_scanTimer->stop();
updateIndexingStatus();
}
} }
void Database::handleDocumentError(const QString &errorMessage, void Database::handleDocumentError(const QString &errorMessage,
@ -721,7 +725,6 @@ void Database::removeFolderFromDocumentQueue(int folder_id)
return; return;
m_docsToScan.remove(folder_id); m_docsToScan.remove(folder_id);
emit removeFolderById(folder_id); emit removeFolderById(folder_id);
emit docsToScanChanged();
} }
void Database::enqueueDocumentInternal(const DocumentInfo &info, bool prepend) void Database::enqueueDocumentInternal(const DocumentInfo &info, bool prepend)
@ -745,13 +748,16 @@ void Database::enqueueDocuments(int folder_id, const QVector<DocumentInfo> &info
const size_t bytes = countOfBytes(folder_id); const size_t bytes = countOfBytes(folder_id);
emit updateCurrentBytesToIndex(folder_id, bytes); emit updateCurrentBytesToIndex(folder_id, bytes);
emit updateTotalBytesToIndex(folder_id, bytes); emit updateTotalBytesToIndex(folder_id, bytes);
emit docsToScanChanged(); m_scanTimer->start();
} }
void Database::scanQueue() void Database::scanQueue()
{ {
if (m_docsToScan.isEmpty()) if (m_docsToScan.isEmpty()) {
m_scanTimer->stop();
updateIndexingStatus();
return; return;
}
DocumentInfo info = dequeueDocument(); DocumentInfo info = dequeueDocument();
const size_t countForFolder = countOfDocuments(info.folder); const size_t countForFolder = countOfDocuments(info.folder);
@ -818,6 +824,8 @@ void Database::scanQueue()
QSqlDatabase::database().transaction(); QSqlDatabase::database().transaction();
Q_ASSERT(document_id != -1); Q_ASSERT(document_id != -1);
if (info.isPdf()) { if (info.isPdf()) {
updateFolderStatus(folder_id, FolderStatus::Embedding, -1, info.currentPage == 0);
QPdfDocument doc; QPdfDocument doc;
if (QPdfDocument::Error::None != doc.load(info.doc.canonicalFilePath())) { if (QPdfDocument::Error::None != doc.load(info.doc.canonicalFilePath())) {
handleDocumentError("ERROR: Could not load pdf", handleDocumentError("ERROR: Could not load pdf",
@ -850,6 +858,8 @@ void Database::scanQueue()
emit subtractCurrentBytesToIndex(info.folder, bytes - (bytesPerPage * doc.pageCount())); emit subtractCurrentBytesToIndex(info.folder, bytes - (bytesPerPage * doc.pageCount()));
} }
} else { } else {
updateFolderStatus(folder_id, FolderStatus::Embedding, -1, info.currentPosition == 0);
QFile file(document_path); QFile file(document_path);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
handleDocumentError("ERROR: Cannot open file for scanning", handleDocumentError("ERROR: Cannot open file for scanning",
@ -884,7 +894,7 @@ void Database::scanQueue()
return scheduleNext(folder_id, countForFolder); return scheduleNext(folder_id, countForFolder);
} }
void Database::scanDocuments(int folder_id, const QString &folder_path) void Database::scanDocuments(int folder_id, const QString &folder_path, bool isNew)
{ {
#if defined(DEBUG) #if defined(DEBUG)
qDebug() << "scanning folder for documents" << folder_path; qDebug() << "scanning folder for documents" << folder_path;
@ -915,7 +925,7 @@ void Database::scanDocuments(int folder_id, const QString &folder_path)
} }
if (!infos.isEmpty()) { if (!infos.isEmpty()) {
emit updateIndexing(folder_id, true); updateFolderStatus(folder_id, FolderStatus::Started, infos.count(), false, isNew);
enqueueDocuments(folder_id, infos); enqueueDocuments(folder_id, infos);
} }
} }
@ -925,7 +935,7 @@ void Database::start()
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &Database::directoryChanged); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &Database::directoryChanged);
connect(m_embLLM, &EmbeddingLLM::embeddingsGenerated, this, &Database::handleEmbeddingsGenerated); connect(m_embLLM, &EmbeddingLLM::embeddingsGenerated, this, &Database::handleEmbeddingsGenerated);
connect(m_embLLM, &EmbeddingLLM::errorGenerated, this, &Database::handleErrorGenerated); connect(m_embLLM, &EmbeddingLLM::errorGenerated, this, &Database::handleErrorGenerated);
connect(this, &Database::docsToScanChanged, this, &Database::scanQueue); m_scanTimer->callOnTimeout(this, &Database::scanQueue);
if (!QSqlDatabase::drivers().contains("QSQLITE")) { if (!QSqlDatabase::drivers().contains("QSQLITE")) {
qWarning() << "ERROR: missing sqllite driver"; qWarning() << "ERROR: missing sqllite driver";
} else { } else {
@ -937,10 +947,11 @@ void Database::start()
if (m_embeddings->fileExists() && !m_embeddings->load()) if (m_embeddings->fileExists() && !m_embeddings->load())
qWarning() << "ERROR: Could not load embeddings"; qWarning() << "ERROR: Could not load embeddings";
addCurrentFolders(); int nAdded = addCurrentFolders();
Network::globalInstance()->trackEvent("localdocs_startup", { {"doc_collections_total", nAdded} });
} }
void Database::addCurrentFolders() int Database::addCurrentFolders()
{ {
#if defined(DEBUG) #if defined(DEBUG)
qDebug() << "addCurrentFolders"; qDebug() << "addCurrentFolders";
@ -950,21 +961,26 @@ void Database::addCurrentFolders()
QList<CollectionItem> collections; QList<CollectionItem> collections;
if (!selectAllFromCollections(q, &collections)) { if (!selectAllFromCollections(q, &collections)) {
qWarning() << "ERROR: Cannot select collections" << q.lastError(); qWarning() << "ERROR: Cannot select collections" << q.lastError();
return; return 0;
} }
emit collectionListUpdated(collections); emit collectionListUpdated(collections);
int nAdded = 0;
for (const auto &i : collections) for (const auto &i : collections)
addFolder(i.collection, i.folder_path); nAdded += addFolder(i.collection, i.folder_path, true);
updateIndexingStatus();
return nAdded;
} }
void Database::addFolder(const QString &collection, const QString &path) bool Database::addFolder(const QString &collection, const QString &path, bool fromDb)
{ {
QFileInfo info(path); QFileInfo info(path);
if (!info.exists() || !info.isReadable()) { if (!info.exists() || !info.isReadable()) {
qWarning() << "ERROR: Cannot add folder that doesn't exist or not readable" << path; qWarning() << "ERROR: Cannot add folder that doesn't exist or not readable" << path;
return; return false;
} }
QSqlQuery q; QSqlQuery q;
@ -973,13 +989,13 @@ void Database::addFolder(const QString &collection, const QString &path)
// See if the folder exists in the db // See if the folder exists in the db
if (!selectFolder(q, path, &folder_id)) { if (!selectFolder(q, path, &folder_id)) {
qWarning() << "ERROR: Cannot select folder from path" << path << q.lastError(); qWarning() << "ERROR: Cannot select folder from path" << path << q.lastError();
return; return false;
} }
// Add the folder // Add the folder
if (folder_id == -1 && !addFolderToDB(q, path, &folder_id)) { if (folder_id == -1 && !addFolderToDB(q, path, &folder_id)) {
qWarning() << "ERROR: Cannot add folder to db with path" << path << q.lastError(); qWarning() << "ERROR: Cannot add folder to db with path" << path << q.lastError();
return; return false;
} }
Q_ASSERT(folder_id != -1); Q_ASSERT(folder_id != -1);
@ -988,24 +1004,32 @@ void Database::addFolder(const QString &collection, const QString &path)
QList<int> folders; QList<int> folders;
if (!selectFoldersFromCollection(q, collection, &folders)) { if (!selectFoldersFromCollection(q, collection, &folders)) {
qWarning() << "ERROR: Cannot select folders from collections" << collection << q.lastError(); qWarning() << "ERROR: Cannot select folders from collections" << collection << q.lastError();
return; return false;
} }
bool added = false;
if (!folders.contains(folder_id)) { if (!folders.contains(folder_id)) {
if (!addCollection(q, collection, folder_id)) { if (!addCollection(q, collection, folder_id)) {
qWarning() << "ERROR: Cannot add folder to collection" << collection << path << q.lastError(); qWarning() << "ERROR: Cannot add folder to collection" << collection << path << q.lastError();
return; return false;
} }
CollectionItem i; CollectionItem i;
i.collection = collection; i.collection = collection;
i.folder_path = path; i.folder_path = path;
i.folder_id = folder_id; i.folder_id = folder_id;
emit addCollectionItem(i); emit addCollectionItem(i, fromDb);
added = true;
} }
addFolderToWatch(path); addFolderToWatch(path);
scanDocuments(folder_id, path); scanDocuments(folder_id, path, !fromDb);
if (!fromDb) {
updateIndexingStatus();
}
return added;
} }
void Database::removeFolder(const QString &collection, const QString &path) void Database::removeFolder(const QString &collection, const QString &path)
@ -1285,5 +1309,69 @@ void Database::directoryChanged(const QString &path)
cleanDB(); cleanDB();
// Rescan the documents associated with the folder // Rescan the documents associated with the folder
scanDocuments(folder_id, path); scanDocuments(folder_id, path, false);
updateIndexingStatus();
}
void Database::updateIndexingStatus() {
Q_ASSERT(m_scanTimer->isActive() || m_docsToScan.isEmpty());
if (!m_indexingTimer.isValid() && m_scanTimer->isActive()) {
Network::globalInstance()->trackEvent("localdocs_indexing_start");
m_indexingTimer.start();
} else if (m_indexingTimer.isValid() && !m_scanTimer->isActive()) {
qint64 durationMs = m_indexingTimer.elapsed();
Network::globalInstance()->trackEvent("localdocs_indexing_complete", { {"$duration", durationMs / 1000.} });
m_indexingTimer.invalidate();
}
}
void Database::updateFolderStatus(int folder_id, Database::FolderStatus status, int numDocs, bool atStart, bool isNew) {
FolderStatusRecord *lastRecord = nullptr;
if (m_foldersBeingIndexed.contains(folder_id)) {
lastRecord = &m_foldersBeingIndexed[folder_id];
}
Q_ASSERT(lastRecord || status == FolderStatus::Started);
switch (status) {
case FolderStatus::Started:
if (lastRecord == nullptr) {
// record timestamp but don't send an event yet
m_foldersBeingIndexed.insert(folder_id, { QDateTime::currentMSecsSinceEpoch(), isNew, numDocs });
emit updateIndexing(folder_id, true);
}
break;
case FolderStatus::Embedding:
if (!lastRecord->docsChanged) {
Q_ASSERT(atStart);
// send start event with the original timestamp for folders that need updating
const auto *embeddingModels = ModelList::globalInstance()->installedEmbeddingModels();
Network::globalInstance()->trackEvent("localdocs_folder_indexing", {
{"folder_id", folder_id},
{"is_new_collection", lastRecord->isNew},
{"document_count", lastRecord->numDocs},
{"embedding_model", embeddingModels->defaultModelInfo().filename()},
{"chunk_size", m_chunkSize},
{"time", lastRecord->startTime},
});
}
lastRecord->docsChanged += atStart;
lastRecord->chunksRead++;
break;
case FolderStatus::Complete:
if (lastRecord->docsChanged) {
// send complete event for folders that were updated
qint64 durationMs = QDateTime::currentMSecsSinceEpoch() - lastRecord->startTime;
Network::globalInstance()->trackEvent("localdocs_folder_complete", {
{"folder_id", folder_id},
{"is_new_collection", lastRecord->isNew},
{"documents_total", lastRecord->numDocs},
{"documents_changed", lastRecord->docsChanged},
{"chunks_read", lastRecord->chunksRead},
{"$duration", durationMs / 1000.},
});
}
m_foldersBeingIndexed.remove(folder_id);
emit updateIndexing(folder_id, false);
break;
}
} }

@ -1,16 +1,19 @@
#ifndef DATABASE_H #ifndef DATABASE_H
#define DATABASE_H #define DATABASE_H
#include <QElapsedTimer>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QObject> #include <QObject>
#include <QtSql>
#include <QQueue> #include <QQueue>
#include <QFileInfo>
#include <QThread> #include <QThread>
#include <QFileSystemWatcher> #include <QtSql>
#include "embllm.h" #include "embllm.h"
class Embeddings; class Embeddings;
class QTimer;
struct DocumentInfo struct DocumentInfo
{ {
int folder; int folder;
@ -58,9 +61,10 @@ public:
virtual ~Database(); virtual ~Database();
public Q_SLOTS: public Q_SLOTS:
void start();
void scanQueue(); void scanQueue();
void scanDocuments(int folder_id, const QString &folder_path); void scanDocuments(int folder_id, const QString &folder_path, bool isNew);
void addFolder(const QString &collection, const QString &path); bool addFolder(const QString &collection, const QString &path, bool fromDb);
void removeFolder(const QString &collection, const QString &path); void removeFolder(const QString &collection, const QString &path);
void retrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize, QList<ResultInfo> *results); void retrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize, QList<ResultInfo> *results);
void cleanDB(); void cleanDB();
@ -78,21 +82,22 @@ Q_SIGNALS:
void updateTotalBytesToIndex(int folder_id, size_t totalBytesToIndex); void updateTotalBytesToIndex(int folder_id, size_t totalBytesToIndex);
void updateCurrentEmbeddingsToIndex(int folder_id, size_t currentBytesToIndex); void updateCurrentEmbeddingsToIndex(int folder_id, size_t currentBytesToIndex);
void updateTotalEmbeddingsToIndex(int folder_id, size_t totalBytesToIndex); void updateTotalEmbeddingsToIndex(int folder_id, size_t totalBytesToIndex);
void addCollectionItem(const CollectionItem &item); void addCollectionItem(const CollectionItem &item, bool fromDb);
void removeFolderById(int folder_id); void removeFolderById(int folder_id);
void removeCollectionItem(const QString &collectionName);
void collectionListUpdated(const QList<CollectionItem> &collectionList); void collectionListUpdated(const QList<CollectionItem> &collectionList);
private Q_SLOTS: private Q_SLOTS:
void start();
void directoryChanged(const QString &path); void directoryChanged(const QString &path);
bool addFolderToWatch(const QString &path); bool addFolderToWatch(const QString &path);
bool removeFolderFromWatch(const QString &path); bool removeFolderFromWatch(const QString &path);
void addCurrentFolders(); int addCurrentFolders();
void handleEmbeddingsGenerated(const QVector<EmbeddingResult> &embeddings); void handleEmbeddingsGenerated(const QVector<EmbeddingResult> &embeddings);
void handleErrorGenerated(int folder_id, const QString &error); void handleErrorGenerated(int folder_id, const QString &error);
private: private:
enum class FolderStatus { Started, Embedding, Complete };
struct FolderStatusRecord { qint64 startTime; bool isNew; int numDocs, docsChanged, chunksRead; };
void removeFolderInternal(const QString &collection, int folder_id, const QString &path); void removeFolderInternal(const QString &collection, int folder_id, const QString &path);
size_t chunkStream(QTextStream &stream, int folder_id, int document_id, const QString &file, size_t chunkStream(QTextStream &stream, int folder_id, int document_id, const QString &file,
const QString &title, const QString &author, const QString &subject, const QString &keywords, int page, const QString &title, const QString &author, const QString &subject, const QString &keywords, int page,
@ -107,10 +112,15 @@ private:
void removeFolderFromDocumentQueue(int folder_id); void removeFolderFromDocumentQueue(int folder_id);
void enqueueDocumentInternal(const DocumentInfo &info, bool prepend = false); void enqueueDocumentInternal(const DocumentInfo &info, bool prepend = false);
void enqueueDocuments(int folder_id, const QVector<DocumentInfo> &infos); void enqueueDocuments(int folder_id, const QVector<DocumentInfo> &infos);
void updateIndexingStatus();
void updateFolderStatus(int folder_id, FolderStatus status, int numDocs = -1, bool atStart = false, bool isNew = false);
private: private:
int m_chunkSize; int m_chunkSize;
QTimer *m_scanTimer;
QMap<int, QQueue<DocumentInfo>> m_docsToScan; QMap<int, QQueue<DocumentInfo>> m_docsToScan;
QElapsedTimer m_indexingTimer;
QMap<int, FolderStatusRecord> m_foldersBeingIndexed;
QList<ResultInfo> m_retrieve; QList<ResultInfo> m_retrieve;
QThread m_dbThread; QThread m_dbThread;
QFileSystemWatcher *m_watcher; QFileSystemWatcher *m_watcher;

@ -75,15 +75,25 @@ bool Download::hasNewerRelease() const
return compareVersions(versions.first(), currentVersion); return compareVersions(versions.first(), currentVersion);
} }
bool Download::isFirstStart() const bool Download::isFirstStart(bool writeVersion) const
{ {
auto *mySettings = MySettings::globalInstance();
QSettings settings; QSettings settings;
settings.sync(); settings.sync();
QString lastVersionStarted = settings.value("download/lastVersionStarted").toString(); QString lastVersionStarted = settings.value("download/lastVersionStarted").toString();
bool first = lastVersionStarted != QCoreApplication::applicationVersion(); bool first = lastVersionStarted != QCoreApplication::applicationVersion();
settings.setValue("download/lastVersionStarted", QCoreApplication::applicationVersion()); if (first && writeVersion) {
settings.sync(); settings.setValue("download/lastVersionStarted", QCoreApplication::applicationVersion());
return first; // let the user select these again
settings.remove("network/usageStatsActive");
settings.remove("network/isActive");
settings.sync();
emit mySettings->networkUsageStatsActiveChanged();
emit mySettings->networkIsActiveChanged();
}
return first || !mySettings->isNetworkUsageStatsActiveSet() || !mySettings->isNetworkIsActiveSet();
} }
void Download::updateReleaseNotes() void Download::updateReleaseNotes()
@ -131,7 +141,7 @@ void Download::downloadModel(const QString &modelFile)
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::DownloadingRole, true }}); ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::DownloadingRole, true }});
ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile); ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
QString url = !info.url().isEmpty() ? info.url() : "http://gpt4all.io/models/gguf/" + modelFile; QString url = !info.url().isEmpty() ? info.url() : "http://gpt4all.io/models/gguf/" + modelFile;
Network::globalInstance()->sendDownloadStarted(modelFile); Network::globalInstance()->trackEvent("download_started", { {"model", modelFile} });
QNetworkRequest request(url); QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::User, modelFile); request.setAttribute(QNetworkRequest::User, modelFile);
request.setRawHeader("range", QString("bytes=%1-").arg(tempFile->pos()).toUtf8()); request.setRawHeader("range", QString("bytes=%1-").arg(tempFile->pos()).toUtf8());
@ -153,7 +163,7 @@ void Download::cancelDownload(const QString &modelFile)
QNetworkReply *modelReply = m_activeDownloads.keys().at(i); QNetworkReply *modelReply = m_activeDownloads.keys().at(i);
QUrl url = modelReply->request().url(); QUrl url = modelReply->request().url();
if (url.toString().endsWith(modelFile)) { if (url.toString().endsWith(modelFile)) {
Network::globalInstance()->sendDownloadCanceled(modelFile); Network::globalInstance()->trackEvent("download_canceled", { {"model", modelFile} });
// Disconnect the signals // Disconnect the signals
disconnect(modelReply, &QNetworkReply::downloadProgress, this, &Download::handleDownloadProgress); disconnect(modelReply, &QNetworkReply::downloadProgress, this, &Download::handleDownloadProgress);
@ -178,7 +188,8 @@ void Download::installModel(const QString &modelFile, const QString &apiKey)
if (apiKey.isEmpty()) if (apiKey.isEmpty())
return; return;
Network::globalInstance()->sendInstallModel(modelFile); Network::globalInstance()->trackEvent("install_model", { {"model", modelFile} });
QString filePath = MySettings::globalInstance()->modelPath() + modelFile; QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
QFile file(filePath); QFile file(filePath);
if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) { if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) {
@ -216,7 +227,7 @@ void Download::removeModel(const QString &modelFile)
shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.description() == "" /*indicates sideloaded*/); shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.description() == "" /*indicates sideloaded*/);
if (shouldRemoveInstalled) if (shouldRemoveInstalled)
ModelList::globalInstance()->removeInstalled(info); ModelList::globalInstance()->removeInstalled(info);
Network::globalInstance()->sendRemoveModel(modelFile); Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
file.remove(); file.remove();
} }
@ -332,7 +343,11 @@ void Download::handleErrorOccurred(QNetworkReply::NetworkError code)
.arg(modelReply->errorString()); .arg(modelReply->errorString());
qWarning() << error; qWarning() << error;
ModelList::globalInstance()->updateDataByFilename(modelFilename, {{ ModelList::DownloadErrorRole, error }}); ModelList::globalInstance()->updateDataByFilename(modelFilename, {{ ModelList::DownloadErrorRole, error }});
Network::globalInstance()->sendDownloadError(modelFilename, (int)code, modelReply->errorString()); Network::globalInstance()->trackEvent("download_error", {
{"model", modelFilename},
{"code", (int)code},
{"error", modelReply->errorString()},
});
cancelDownload(modelFilename); cancelDownload(modelFilename);
} }
@ -515,7 +530,7 @@ void Download::handleHashAndSaveFinished(bool success, const QString &error,
// The hash and save should send back with tempfile closed // The hash and save should send back with tempfile closed
Q_ASSERT(!tempFile->isOpen()); Q_ASSERT(!tempFile->isOpen());
QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString(); QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString();
Network::globalInstance()->sendDownloadFinished(modelFilename, success); Network::globalInstance()->trackEvent("download_finished", { {"model", modelFilename}, {"success", success} });
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::CalcHashRole, false }, { ModelList::CalcHashRole, false },

@ -54,7 +54,7 @@ public:
Q_INVOKABLE void cancelDownload(const QString &modelFile); Q_INVOKABLE void cancelDownload(const QString &modelFile);
Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey); Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey);
Q_INVOKABLE void removeModel(const QString &modelFile); Q_INVOKABLE void removeModel(const QString &modelFile);
Q_INVOKABLE bool isFirstStart() const; Q_INVOKABLE bool isFirstStart(bool writeVersion = false) const;
public Q_SLOTS: public Q_SLOTS:
void updateReleaseNotes(); void updateReleaseNotes();

@ -57,7 +57,14 @@ bool EmbeddingLLMWorker::loadModel()
return true; return true;
} }
m_model = LLModel::Implementation::construct(filePath.toStdString()); try {
m_model = LLModel::Implementation::construct(filePath.toStdString());
} catch (const std::exception &e) {
qWarning() << "WARNING: Could not load embedding model:" << e.what();
m_model = nullptr;
return false;
}
// NOTE: explicitly loads model on CPU to avoid GPU OOM // NOTE: explicitly loads model on CPU to avoid GPU OOM
// TODO(cebtenzzre): support GPU-accelerated embeddings // TODO(cebtenzzre): support GPU-accelerated embeddings
bool success = m_model->loadModel(filePath.toStdString(), 2048, 0); bool success = m_model->loadModel(filePath.toStdString(), 2048, 0);

@ -49,7 +49,7 @@ bool LLM::checkForUpdates() const
#pragma message "offline installer build will not check for updates!" #pragma message "offline installer build will not check for updates!"
return QDesktopServices::openUrl(QUrl("https://gpt4all.io/")); return QDesktopServices::openUrl(QUrl("https://gpt4all.io/"));
#else #else
Network::globalInstance()->sendCheckForUpdates(); Network::globalInstance()->trackEvent("check_for_updates");
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
QString tool("maintenancetool"); QString tool("maintenancetool");

@ -18,6 +18,8 @@ LocalDocs::LocalDocs()
// Create the DB with the chunk size from settings // Create the DB with the chunk size from settings
m_database = new Database(MySettings::globalInstance()->localDocsChunkSize()); m_database = new Database(MySettings::globalInstance()->localDocsChunkSize());
connect(this, &LocalDocs::requestStart, m_database,
&Database::start, Qt::QueuedConnection);
connect(this, &LocalDocs::requestAddFolder, m_database, connect(this, &LocalDocs::requestAddFolder, m_database,
&Database::addFolder, Qt::QueuedConnection); &Database::addFolder, Qt::QueuedConnection);
connect(this, &LocalDocs::requestRemoveFolder, m_database, connect(this, &LocalDocs::requestRemoveFolder, m_database,
@ -50,8 +52,6 @@ LocalDocs::LocalDocs()
m_localDocsModel, &LocalDocsModel::addCollectionItem, Qt::QueuedConnection); m_localDocsModel, &LocalDocsModel::addCollectionItem, Qt::QueuedConnection);
connect(m_database, &Database::removeFolderById, connect(m_database, &Database::removeFolderById,
m_localDocsModel, &LocalDocsModel::removeFolderById, Qt::QueuedConnection); m_localDocsModel, &LocalDocsModel::removeFolderById, Qt::QueuedConnection);
connect(m_database, &Database::removeCollectionItem,
m_localDocsModel, &LocalDocsModel::removeCollectionItem, Qt::QueuedConnection);
connect(m_database, &Database::collectionListUpdated, connect(m_database, &Database::collectionListUpdated,
m_localDocsModel, &LocalDocsModel::collectionListUpdated, Qt::QueuedConnection); m_localDocsModel, &LocalDocsModel::collectionListUpdated, Qt::QueuedConnection);
@ -68,7 +68,7 @@ void LocalDocs::addFolder(const QString &collection, const QString &path)
{ {
const QUrl url(path); const QUrl url(path);
const QString localPath = url.isLocalFile() ? url.toLocalFile() : path; const QString localPath = url.isLocalFile() ? url.toLocalFile() : path;
emit requestAddFolder(collection, localPath); emit requestAddFolder(collection, localPath, false);
} }
void LocalDocs::removeFolder(const QString &collection, const QString &path) void LocalDocs::removeFolder(const QString &collection, const QString &path)

@ -26,7 +26,8 @@ public Q_SLOTS:
void aboutToQuit(); void aboutToQuit();
Q_SIGNALS: Q_SIGNALS:
void requestAddFolder(const QString &collection, const QString &path); void requestStart();
void requestAddFolder(const QString &collection, const QString &path, bool fromDb);
void requestRemoveFolder(const QString &collection, const QString &path); void requestRemoveFolder(const QString &collection, const QString &path);
void requestChunkSizeChange(int chunkSize); void requestChunkSizeChange(int chunkSize);
void localDocsModelChanged(); void localDocsModelChanged();

@ -1,105 +0,0 @@
#ifndef LOCALDOCS_H
#define LOCALDOCS_H
#include "localdocsmodel.h"
#include <QObject>
#include <QtSql>
#include <QQueue>
#include <QFileInfo>
#include <QThread>
#include <QFileSystemWatcher>
struct DocumentInfo
{
int folder;
QFileInfo doc;
};
struct CollectionItem {
QString collection;
QString folder_path;
int folder_id = -1;
};
Q_DECLARE_METATYPE(CollectionItem)
class Database : public QObject
{
Q_OBJECT
public:
Database();
public Q_SLOTS:
void scanQueue();
void scanDocuments(int folder_id, const QString &folder_path);
void addFolder(const QString &collection, const QString &path);
void removeFolder(const QString &collection, const QString &path);
void retrieveFromDB(const QList<QString> &collections, const QString &text);
void cleanDB();
Q_SIGNALS:
void docsToScanChanged();
void retrieveResult(const QList<QString> &result);
void collectionListUpdated(const QList<CollectionItem> &collectionList);
private Q_SLOTS:
void start();
void directoryChanged(const QString &path);
bool addFolderToWatch(const QString &path);
bool removeFolderFromWatch(const QString &path);
void addCurrentFolders();
void updateCollectionList();
private:
void removeFolderInternal(const QString &collection, int folder_id, const QString &path);
void chunkStream(QTextStream &stream, int document_id);
void handleDocumentErrorAndScheduleNext(const QString &errorMessage,
int document_id, const QString &document_path, const QSqlError &error);
private:
QQueue<DocumentInfo> m_docsToScan;
QList<QString> m_retrieve;
QThread m_dbThread;
QFileSystemWatcher *m_watcher;
};
class LocalDocs : public QObject
{
Q_OBJECT
Q_PROPERTY(LocalDocsModel *localDocsModel READ localDocsModel NOTIFY localDocsModelChanged)
public:
static LocalDocs *globalInstance();
LocalDocsModel *localDocsModel() const { return m_localDocsModel; }
void addFolder(const QString &collection, const QString &path);
void removeFolder(const QString &collection, const QString &path);
QList<QString> result() const { return m_retrieveResult; }
void requestRetrieve(const QList<QString> &collections, const QString &text);
Q_SIGNALS:
void requestAddFolder(const QString &collection, const QString &path);
void requestRemoveFolder(const QString &collection, const QString &path);
void requestRetrieveFromDB(const QList<QString> &collections, const QString &text);
void receivedResult();
void localDocsModelChanged();
private Q_SLOTS:
void handleRetrieveResult(const QList<QString> &result);
void handleCollectionListUpdated(const QList<CollectionItem> &collectionList);
private:
LocalDocsModel *m_localDocsModel;
Database *m_database;
QList<QString> m_retrieveResult;
QList<CollectionItem> m_collectionList;
private:
explicit LocalDocs();
~LocalDocs() {}
friend class MyLocalDocs;
};
#endif // LOCALDOCS_H

@ -1,6 +1,7 @@
#include "localdocsmodel.h" #include "localdocsmodel.h"
#include "localdocs.h" #include "localdocs.h"
#include "network.h"
LocalDocsCollectionsModel::LocalDocsCollectionsModel(QObject *parent) LocalDocsCollectionsModel::LocalDocsCollectionsModel(QObject *parent)
: QSortFilterProxyModel(parent) : QSortFilterProxyModel(parent)
@ -158,50 +159,43 @@ void LocalDocsModel::updateTotalEmbeddingsToIndex(int folder_id, size_t totalEmb
[](CollectionItem& item, size_t val) { item.totalEmbeddingsToIndex += val; }, {TotalEmbeddingsToIndexRole}); [](CollectionItem& item, size_t val) { item.totalEmbeddingsToIndex += val; }, {TotalEmbeddingsToIndexRole});
} }
void LocalDocsModel::addCollectionItem(const CollectionItem &item) void LocalDocsModel::addCollectionItem(const CollectionItem &item, bool fromDb)
{ {
beginInsertRows(QModelIndex(), m_collectionList.size(), m_collectionList.size()); beginInsertRows(QModelIndex(), m_collectionList.size(), m_collectionList.size());
m_collectionList.append(item); m_collectionList.append(item);
endInsertRows(); endInsertRows();
if (!fromDb) {
Network::globalInstance()->trackEvent("doc_collection_add", {
{"collection_count", m_collectionList.count()},
});
}
} }
void LocalDocsModel::removeFolderById(int folder_id) void LocalDocsModel::removeCollectionIf(std::function<bool(CollectionItem)> const &predicate) {
{
for (int i = 0; i < m_collectionList.size();) { for (int i = 0; i < m_collectionList.size();) {
if (m_collectionList.at(i).folder_id == folder_id) { if (predicate(m_collectionList.at(i))) {
beginRemoveRows(QModelIndex(), i, i); beginRemoveRows(QModelIndex(), i, i);
m_collectionList.removeAt(i); m_collectionList.removeAt(i);
endRemoveRows(); endRemoveRows();
Network::globalInstance()->trackEvent("doc_collection_remove", {
{"collection_count", m_collectionList.count()},
});
} else { } else {
++i; ++i;
} }
} }
} }
void LocalDocsModel::removeCollectionPath(const QString &name, const QString &path) void LocalDocsModel::removeFolderById(int folder_id)
{ {
for (int i = 0; i < m_collectionList.size();) { removeCollectionIf([folder_id](const auto &c) { return c.folder_id == folder_id; });
if (m_collectionList.at(i).collection == name && m_collectionList.at(i).folder_path == path) {
beginRemoveRows(QModelIndex(), i, i);
m_collectionList.removeAt(i);
endRemoveRows();
} else {
++i;
}
}
} }
void LocalDocsModel::removeCollectionItem(const QString &collectionName) void LocalDocsModel::removeCollectionPath(const QString &name, const QString &path)
{ {
for (int i = 0; i < m_collectionList.size();) { removeCollectionIf([&name, &path](const auto &c) { return c.collection == name && c.folder_path == path; });
if (m_collectionList.at(i).collection == collectionName) {
beginRemoveRows(QModelIndex(), i, i);
m_collectionList.removeAt(i);
endRemoveRows();
} else {
++i;
}
}
} }
void LocalDocsModel::collectionListUpdated(const QList<CollectionItem> &collectionList) void LocalDocsModel::collectionListUpdated(const QList<CollectionItem> &collectionList)

@ -55,10 +55,9 @@ public Q_SLOTS:
void updateTotalBytesToIndex(int folder_id, size_t totalBytesToIndex); void updateTotalBytesToIndex(int folder_id, size_t totalBytesToIndex);
void updateCurrentEmbeddingsToIndex(int folder_id, size_t currentBytesToIndex); void updateCurrentEmbeddingsToIndex(int folder_id, size_t currentBytesToIndex);
void updateTotalEmbeddingsToIndex(int folder_id, size_t totalBytesToIndex); void updateTotalEmbeddingsToIndex(int folder_id, size_t totalBytesToIndex);
void addCollectionItem(const CollectionItem &item); void addCollectionItem(const CollectionItem &item, bool fromDb);
void removeFolderById(int folder_id); void removeFolderById(int folder_id);
void removeCollectionPath(const QString &name, const QString &path); void removeCollectionPath(const QString &name, const QString &path);
void removeCollectionItem(const QString &collectionName);
void collectionListUpdated(const QList<CollectionItem> &collectionList); void collectionListUpdated(const QList<CollectionItem> &collectionList);
private: private:
@ -66,6 +65,7 @@ private:
void updateField(int folder_id, T value, void updateField(int folder_id, T value,
const std::function<void(CollectionItem&, T)>& updater, const std::function<void(CollectionItem&, T)>& updater,
const QVector<int>& roles); const QVector<int>& roles);
void removeCollectionIf(std::function<bool(CollectionItem)> const &predicate);
private: private:
QList<CollectionItem> m_collectionList; QList<CollectionItem> m_collectionList;

@ -910,15 +910,23 @@ bool MySettings::networkIsActive() const
return setting.value("network/isActive", default_networkIsActive).toBool(); return setting.value("network/isActive", default_networkIsActive).toBool();
} }
void MySettings::setNetworkIsActive(bool b) bool MySettings::isNetworkIsActiveSet() const
{ {
if (networkIsActive() == b) QSettings setting;
return; setting.sync();
return setting.value("network/isActive").isValid();
}
void MySettings::setNetworkIsActive(bool b)
{
QSettings setting; QSettings setting;
setting.setValue("network/isActive", b);
setting.sync(); setting.sync();
emit networkIsActiveChanged(); auto cur = setting.value("network/isActive");
if (!cur.isValid() || cur.toBool() != b) {
setting.setValue("network/isActive", b);
setting.sync();
emit networkIsActiveChanged();
}
} }
bool MySettings::networkUsageStatsActive() const bool MySettings::networkUsageStatsActive() const
@ -928,13 +936,21 @@ bool MySettings::networkUsageStatsActive() const
return setting.value("network/usageStatsActive", default_networkUsageStatsActive).toBool(); return setting.value("network/usageStatsActive", default_networkUsageStatsActive).toBool();
} }
void MySettings::setNetworkUsageStatsActive(bool b) bool MySettings::isNetworkUsageStatsActiveSet() const
{ {
if (networkUsageStatsActive() == b) QSettings setting;
return; setting.sync();
return setting.value("network/usageStatsActive").isValid();
}
void MySettings::setNetworkUsageStatsActive(bool b)
{
QSettings setting; QSettings setting;
setting.setValue("network/usageStatsActive", b);
setting.sync(); setting.sync();
emit networkUsageStatsActiveChanged(); auto cur = setting.value("network/usageStatsActive");
if (!cur.isValid() || cur.toBool() != b) {
setting.setValue("network/usageStatsActive", b);
setting.sync();
emit networkUsageStatsActiveChanged();
}
} }

@ -129,8 +129,10 @@ public:
QString networkAttribution() const; QString networkAttribution() const;
void setNetworkAttribution(const QString &a); void setNetworkAttribution(const QString &a);
bool networkIsActive() const; bool networkIsActive() const;
Q_INVOKABLE bool isNetworkIsActiveSet() const;
void setNetworkIsActive(bool b); void setNetworkIsActive(bool b);
bool networkUsageStatsActive() const; bool networkUsageStatsActive() const;
Q_INVOKABLE bool isNetworkUsageStatsActiveSet() const;
void setNetworkUsageStatsActive(bool b); void setNetworkUsageStatsActive(bool b);
int networkPort() const; int networkPort() const;
void setNetworkPort(int c); void setNetworkPort(int c);

@ -1,8 +1,13 @@
#include "network.h" #include "network.h"
#include "llm.h"
#include "chatlistmodel.h" #include "chatlistmodel.h"
#include "download.h"
#include "llm.h"
#include "localdocs.h"
#include "mysettings.h" #include "mysettings.h"
#include <cmath>
#include <QCoreApplication> #include <QCoreApplication>
#include <QGuiApplication> #include <QGuiApplication>
#include <QUuid> #include <QUuid>
@ -14,16 +19,49 @@
//#define DEBUG //#define DEBUG
static const char MIXPANEL_TOKEN[] = "ce362e568ddaee16ed243eaffb5860a2";
#if defined(Q_OS_MAC) #if defined(Q_OS_MAC)
#include <sys/sysctl.h> #include <sys/sysctl.h>
std::string getCPUModel() { static QString getCPUModel() {
char buffer[256]; char buffer[256];
size_t bufferlen = sizeof(buffer); size_t bufferlen = sizeof(buffer);
sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferlen, NULL, 0); sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferlen, NULL, 0);
return std::string(buffer); return buffer;
} }
#elif defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)
#define get_cpuid(level, a, b, c, d) asm volatile("cpuid" : "=a" (a), "=b" (b), "=c" (c), "=d" (d) : "0" (level) : "memory")
static QString getCPUModel() {
unsigned regs[12];
// EAX=800000000h: Get Highest Extended Function Implemented
get_cpuid(0x80000000, regs[0], regs[1], regs[2], regs[3]);
if (regs[0] < 0x80000004)
return "(unknown)";
// EAX=800000002h-800000004h: Processor Brand String
get_cpuid(0x80000002, regs[0], regs[1], regs[ 2], regs[ 3]);
get_cpuid(0x80000003, regs[4], regs[5], regs[ 6], regs[ 7]);
get_cpuid(0x80000004, regs[8], regs[9], regs[10], regs[11]);
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 #endif
class MyNetwork: public Network { }; class MyNetwork: public Network { };
Q_GLOBAL_STATIC(MyNetwork, networkInstance) Q_GLOBAL_STATIC(MyNetwork, networkInstance)
Network *Network::globalInstance() Network *Network::globalInstance()
@ -33,38 +71,41 @@ Network *Network::globalInstance()
Network::Network() Network::Network()
: QObject{nullptr} : QObject{nullptr}
, m_shouldSendStartup(false)
{ {
QSettings settings; QSettings settings;
settings.sync(); settings.sync();
m_uniqueId = settings.value("uniqueId", generateUniqueId()).toString(); m_uniqueId = settings.value("uniqueId", generateUniqueId()).toString();
settings.setValue("uniqueId", m_uniqueId); settings.setValue("uniqueId", m_uniqueId);
settings.sync(); settings.sync();
connect(MySettings::globalInstance(), &MySettings::networkIsActiveChanged, this, &Network::handleIsActiveChanged); m_sessionId = generateUniqueId();
connect(MySettings::globalInstance(), &MySettings::networkUsageStatsActiveChanged, this, &Network::handleUsageStatsActiveChanged);
if (MySettings::globalInstance()->networkIsActive()) // 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(); sendHealth();
if (MySettings::globalInstance()->networkUsageStatsActive())
sendIpify();
connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this, connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this,
&Network::handleSslErrors); &Network::handleSslErrors);
} }
void Network::handleIsActiveChanged() // 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()) if (!MySettings::globalInstance()->networkUsageStatsActive())
sendHealth(); m_sendUsageStats = false;
} }
void Network::handleUsageStatsActiveChanged() void Network::handleIsActiveChanged()
{ {
if (!MySettings::globalInstance()->networkUsageStatsActive()) if (MySettings::globalInstance()->networkUsageStatsActive())
sendOptOut(); sendHealth();
else {
// model might be loaded already when user opt-in for first time
sendStartup();
sendIpify();
}
} }
QString Network::generateUniqueId() const QString Network::generateUniqueId() const
@ -167,8 +208,8 @@ void Network::handleSslErrors(QNetworkReply *reply, const QList<QSslError> &erro
void Network::sendOptOut() void Network::sendOptOut()
{ {
QJsonObject properties; QJsonObject properties;
properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2"); properties.insert("token", MIXPANEL_TOKEN);
properties.insert("time", QDateTime::currentSecsSinceEpoch()); properties.insert("time", QDateTime::currentMSecsSinceEpoch());
properties.insert("distinct_id", m_uniqueId); properties.insert("distinct_id", m_uniqueId);
properties.insert("$insert_id", generateUniqueId()); properties.insert("$insert_id", generateUniqueId());
@ -181,7 +222,7 @@ void Network::sendOptOut()
QJsonDocument doc; QJsonDocument doc;
doc.setArray(array); doc.setArray(array);
sendMixpanel(doc.toJson(QJsonDocument::Compact), true /*isOptOut*/); emit requestMixpanel(doc.toJson(QJsonDocument::Compact), true /*isOptOut*/);
#if defined(DEBUG) #if defined(DEBUG)
printf("%s %s\n", qPrintable("opt_out"), qPrintable(doc.toJson(QJsonDocument::Indented))); printf("%s %s\n", qPrintable("opt_out"), qPrintable(doc.toJson(QJsonDocument::Indented)));
@ -189,215 +230,76 @@ void Network::sendOptOut()
#endif #endif
} }
void Network::sendModelLoaded()
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("model_load");
}
void Network::sendResetContext(int conversationLength)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
KeyValue kv;
kv.key = QString("length");
kv.value = QJsonValue(conversationLength);
sendMixpanelEvent("reset_context", QVector<KeyValue>{kv});
}
void Network::sendStartup() void Network::sendStartup()
{ {
if (!MySettings::globalInstance()->networkUsageStatsActive()) const auto *mySettings = MySettings::globalInstance();
return; Q_ASSERT(mySettings->isNetworkUsageStatsActiveSet());
m_shouldSendStartup = true; if (!mySettings->networkUsageStatsActive()) {
if (m_ipify.isEmpty()) // send a single opt-out per session after the user has made their selections,
return; // when it completes it will send // unless this is a normal start (same version) and the user was already opted out
sendMixpanelEvent("startup"); if (!m_hasSentOptOut) {
} sendOptOut();
m_hasSentOptOut = true;
void Network::sendCheckForUpdates() }
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("check_for_updates");
}
void Network::sendModelDownloaderDialog()
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("download_dialog");
}
void Network::sendInstallModel(const QString &model)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
sendMixpanelEvent("install_model", QVector<KeyValue>{kv});
}
void Network::sendRemoveModel(const QString &model)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
sendMixpanelEvent("remove_model", QVector<KeyValue>{kv});
}
void Network::sendDownloadStarted(const QString &model)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
KeyValue kv;
kv.key = QString("model");
kv.value = QJsonValue(model);
sendMixpanelEvent("download_started", QVector<KeyValue>{kv});
}
void Network::sendDownloadCanceled(const QString &model)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
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 (!MySettings::globalInstance()->networkUsageStatsActive())
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 (!MySettings::globalInstance()->networkUsageStatsActive())
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 (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("settings_dialog");
}
void Network::sendNetworkToggled(bool isActive)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
KeyValue kv;
kv.key = QString("isActive");
kv.value = QJsonValue(isActive);
sendMixpanelEvent("network_toggled", QVector<KeyValue>{kv});
}
void Network::sendNewChat(int count)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
KeyValue kv;
kv.key = QString("number_of_chats");
kv.value = QJsonValue(count);
sendMixpanelEvent("new_chat", QVector<KeyValue>{kv});
}
void Network::sendRemoveChat()
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("remove_chat");
}
void Network::sendRenameChat()
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("rename_chat");
}
void Network::sendChatStarted()
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return;
sendMixpanelEvent("chat_started");
}
void Network::sendRecalculatingContext(int conversationLength)
{
if (!MySettings::globalInstance()->networkUsageStatsActive())
return; return;
}
KeyValue kv; // only chance to enable usage stats is at the start of a new session
kv.key = QString("length"); m_sendUsageStats = true;
kv.value = QJsonValue(conversationLength);
sendMixpanelEvent("recalc_context", QVector<KeyValue>{kv}); const auto *display = QGuiApplication::primaryScreen();
trackEvent("startup", {
{"$screen_dpi", std::round(display->physicalDotsPerInch())},
{"display", QString("%1x%2").arg(display->size().width()).arg(display->size().height())},
{"ram", LLM::globalInstance()->systemTotalRAMInGB()},
{"cpu", getCPUModel()},
{"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::sendNonCompatHardware() void Network::trackChatEvent(const QString &ev, QVariantMap props)
{ {
if (!MySettings::globalInstance()->networkUsageStatsActive()) const auto &curChat = ChatListModel::globalInstance()->currentChat();
return; if (!props.contains("model"))
sendMixpanelEvent("noncompat_hardware"); props.insert("model", curChat->modelInfo().filename());
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::sendMixpanelEvent(const QString &ev, const QVector<KeyValue> &values) void Network::trackEvent(const QString &ev, const QVariantMap &props)
{ {
if (!MySettings::globalInstance()->networkUsageStatsActive()) if (!m_sendUsageStats)
return; return;
Q_ASSERT(ChatListModel::globalInstance()->currentChat()); Q_ASSERT(ChatListModel::globalInstance()->currentChat());
QJsonObject properties; QJsonObject properties;
properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2");
properties.insert("time", QDateTime::currentSecsSinceEpoch()); properties.insert("token", MIXPANEL_TOKEN);
properties.insert("distinct_id", m_uniqueId); if (!props.contains("time"))
properties.insert("time", QDateTime::currentMSecsSinceEpoch());
properties.insert("distinct_id", m_uniqueId); // effectively a device ID
properties.insert("$insert_id", generateUniqueId()); properties.insert("$insert_id", generateUniqueId());
properties.insert("$os", QSysInfo::prettyProductName());
if (!m_ipify.isEmpty()) if (!m_ipify.isEmpty())
properties.insert("ip", m_ipify); properties.insert("ip", m_ipify);
properties.insert("name", QCoreApplication::applicationName() + " v"
+ QCoreApplication::applicationVersion());
properties.insert("model", ChatListModel::globalInstance()->currentChat()->modelInfo().filename());
properties.insert("requestedDevice", MySettings::globalInstance()->device());
properties.insert("actualDevice", ChatListModel::globalInstance()->currentChat()->device());
// 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", LLM::globalInstance()->systemTotalRAMInGB());
#if defined(Q_OS_MAC)
properties.insert("cpu", QString::fromStdString(getCPUModel()));
#endif
}
for (const auto& p : values) properties.insert("$os", QSysInfo::prettyProductName());
properties.insert(p.key, p.value); 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; QJsonObject event;
event.insert("event", ev); event.insert("event", ev);
@ -408,7 +310,7 @@ void Network::sendMixpanelEvent(const QString &ev, const QVector<KeyValue> &valu
QJsonDocument doc; QJsonDocument doc;
doc.setArray(array); doc.setArray(array);
sendMixpanel(doc.toJson(QJsonDocument::Compact)); emit requestMixpanel(doc.toJson(QJsonDocument::Compact));
#if defined(DEBUG) #if defined(DEBUG)
printf("%s %s\n", qPrintable(ev), qPrintable(doc.toJson(QJsonDocument::Indented))); printf("%s %s\n", qPrintable(ev), qPrintable(doc.toJson(QJsonDocument::Indented)));
@ -418,7 +320,7 @@ void Network::sendMixpanelEvent(const QString &ev, const QVector<KeyValue> &valu
void Network::sendIpify() void Network::sendIpify()
{ {
if (!MySettings::globalInstance()->networkUsageStatsActive() || !m_ipify.isEmpty()) if (!m_sendUsageStats || !m_ipify.isEmpty())
return; return;
QUrl ipifyUrl("https://api.ipify.org"); QUrl ipifyUrl("https://api.ipify.org");
@ -433,7 +335,7 @@ void Network::sendIpify()
void Network::sendMixpanel(const QByteArray &json, bool isOptOut) void Network::sendMixpanel(const QByteArray &json, bool isOptOut)
{ {
if (!MySettings::globalInstance()->networkUsageStatsActive() && !isOptOut) if (!m_sendUsageStats)
return; return;
QUrl trackUrl("https://api.mixpanel.com/track"); QUrl trackUrl("https://api.mixpanel.com/track");
@ -449,7 +351,6 @@ void Network::sendMixpanel(const QByteArray &json, bool isOptOut)
void Network::handleIpifyFinished() void Network::handleIpifyFinished()
{ {
Q_ASSERT(MySettings::globalInstance()->networkUsageStatsActive());
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender()); QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) if (!reply)
return; return;
@ -469,8 +370,7 @@ void Network::handleIpifyFinished()
#endif #endif
reply->deleteLater(); reply->deleteLater();
if (m_shouldSendStartup) trackEvent("ipify_complete");
sendStartup();
} }
void Network::handleMixpanelFinished() void Network::handleMixpanelFinished()

@ -19,31 +19,15 @@ public:
Q_INVOKABLE QString generateUniqueId() const; Q_INVOKABLE QString generateUniqueId() const;
Q_INVOKABLE bool sendConversation(const QString &ingestId, const QString &conversation); Q_INVOKABLE bool sendConversation(const QString &ingestId, const QString &conversation);
Q_INVOKABLE void trackChatEvent(const QString &event, QVariantMap props = QVariantMap());
Q_INVOKABLE void trackEvent(const QString &event, const QVariantMap &props = QVariantMap());
Q_SIGNALS: Q_SIGNALS:
void healthCheckFailed(int code); void healthCheckFailed(int code);
void requestMixpanel(const QByteArray &json, bool isOptOut = false);
public Q_SLOTS: public Q_SLOTS:
void sendOptOut();
void sendModelLoaded();
void sendStartup(); void sendStartup();
void sendCheckForUpdates();
Q_INVOKABLE void sendModelDownloaderDialog();
Q_INVOKABLE void sendResetContext(int conversationLength);
void sendInstallModel(const QString &model);
void sendRemoveModel(const QString &model);
void sendDownloadStarted(const QString &model);
void sendDownloadCanceled(const QString &model);
void sendDownloadError(const QString &model, int code, const QString &errorString);
void sendDownloadFinished(const QString &model, bool success);
Q_INVOKABLE void sendSettingsDialog();
Q_INVOKABLE void sendNetworkToggled(bool active);
Q_INVOKABLE void sendNewChat(int count);
Q_INVOKABLE void sendRemoveChat();
Q_INVOKABLE void sendRenameChat();
Q_INVOKABLE void sendNonCompatHardware();
void sendChatStarted();
void sendRecalculatingContext(int conversationLength);
private Q_SLOTS: private Q_SLOTS:
void handleIpifyFinished(); void handleIpifyFinished();
@ -53,18 +37,21 @@ private Q_SLOTS:
void handleMixpanelFinished(); void handleMixpanelFinished();
void handleIsActiveChanged(); void handleIsActiveChanged();
void handleUsageStatsActiveChanged(); void handleUsageStatsActiveChanged();
void sendMixpanel(const QByteArray &json, bool isOptOut);
private: private:
void sendOptOut();
void sendHealth(); void sendHealth();
void sendIpify(); void sendIpify();
void sendMixpanelEvent(const QString &event, const QVector<KeyValue> &values = QVector<KeyValue>());
void sendMixpanel(const QByteArray &json, bool isOptOut = false);
bool packageAndSendJson(const QString &ingestId, const QString &json); bool packageAndSendJson(const QString &ingestId, const QString &json);
private: private:
bool m_shouldSendStartup; bool m_sendUsageStats = false;
bool m_hasSentOptIn;
bool m_hasSentOptOut;
QString m_ipify; QString m_ipify;
QString m_uniqueId; QString m_uniqueId;
QString m_sessionId;
QNetworkAccessManager m_networkManager; QNetworkAccessManager m_networkManager;
QVector<QNetworkReply*> m_activeUploads; QVector<QNetworkReply*> m_activeUploads;

@ -40,7 +40,7 @@ Rectangle {
Accessible.description: qsTr("Create a new chat") Accessible.description: qsTr("Create a new chat")
onClicked: { onClicked: {
ChatListModel.addChat(); ChatListModel.addChat();
Network.sendNewChat(ChatListModel.count) Network.trackEvent("new_chat", {"number_of_chats": ChatListModel.count})
} }
} }
@ -110,8 +110,8 @@ Rectangle {
// having focus // having focus
if (chatName.readOnly) if (chatName.readOnly)
return; return;
Network.trackChatEvent("rename_chat")
changeName(); changeName();
Network.sendRenameChat()
} }
function changeName() { function changeName() {
ChatListModel.get(index).name = chatName.text ChatListModel.get(index).name = chatName.text
@ -194,8 +194,8 @@ Rectangle {
color: "transparent" color: "transparent"
} }
onClicked: { onClicked: {
Network.trackChatEvent("remove_chat")
ChatListModel.removeChat(ChatListModel.get(index)) ChatListModel.removeChat(ChatListModel.get(index))
Network.sendRemoveChat()
} }
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: qsTr("Confirm chat deletion") Accessible.name: qsTr("Confirm chat deletion")

@ -1,16 +1,18 @@
import Qt5Compat.GraphicalEffects
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.Basic import QtQuick.Controls.Basic
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import llm
import chatlistmodel import chatlistmodel
import download import download
import modellist
import network
import gpt4all import gpt4all
import llm
import localdocs
import modellist
import mysettings import mysettings
import network
Rectangle { Rectangle {
id: window id: window
@ -29,6 +31,10 @@ Rectangle {
startupDialogs(); startupDialogs();
} }
Component.onDestruction: {
Network.trackEvent("session_end")
}
Connections { Connections {
target: firstStartDialog target: firstStartDialog
function onClosed() { function onClosed() {
@ -66,12 +72,12 @@ Rectangle {
} }
property bool hasShownModelDownload: false property bool hasShownModelDownload: false
property bool hasShownFirstStart: false property bool hasCheckedFirstStart: false
property bool hasShownSettingsAccess: false property bool hasShownSettingsAccess: false
function startupDialogs() { function startupDialogs() {
if (!LLM.compatHardware()) { if (!LLM.compatHardware()) {
Network.sendNonCompatHardware(); Network.trackEvent("noncompat_hardware")
errorCompatHardware.open(); errorCompatHardware.open();
return; return;
} }
@ -84,10 +90,18 @@ Rectangle {
} }
// check for first time start of this version // check for first time start of this version
if (!hasShownFirstStart && Download.isFirstStart()) { if (!hasCheckedFirstStart) {
firstStartDialog.open(); if (Download.isFirstStart(/*writeVersion*/ true)) {
hasShownFirstStart = true; firstStartDialog.open();
return; return;
}
// send startup or opt-out now that the user has made their choice
Network.sendStartup()
// start localdocs
LocalDocs.requestStart()
hasCheckedFirstStart = true
} }
// check for any current models and if not, open download dialog once // check for any current models and if not, open download dialog once
@ -547,7 +561,6 @@ Rectangle {
onClicked: { onClicked: {
if (MySettings.networkIsActive) { if (MySettings.networkIsActive) {
MySettings.networkIsActive = false MySettings.networkIsActive = false
Network.sendNetworkToggled(false);
} else } else
networkDialog.open() networkDialog.open()
} }
@ -733,7 +746,7 @@ Rectangle {
Accessible.description: qsTr("Reset the context and erase current conversation") Accessible.description: qsTr("Reset the context and erase current conversation")
onClicked: { onClicked: {
Network.sendResetContext(chatModel.count) Network.trackChatEvent("reset_context", { "length": chatModel.count })
currentChat.reset(); currentChat.reset();
currentChat.processSystemPrompt(); currentChat.processSystemPrompt();
} }
@ -1288,9 +1301,11 @@ Rectangle {
var listElement = chatModel.get(index); var listElement = chatModel.get(index);
if (currentChat.responseInProgress) { if (currentChat.responseInProgress) {
Network.trackChatEvent("stop_generating_clicked")
listElement.stopped = true listElement.stopped = true
currentChat.stopGenerating() currentChat.stopGenerating()
} else { } else {
Network.trackChatEvent("regenerate_clicked")
currentChat.regenerateResponse() currentChat.regenerateResponse()
if (chatModel.count) { if (chatModel.count) {
if (listElement.name === qsTr("Response: ")) { if (listElement.name === qsTr("Response: ")) {
@ -1405,6 +1420,7 @@ Rectangle {
if (textInput.text === "") if (textInput.text === "")
return return
Network.trackChatEvent("send_message")
currentChat.stopGenerating() currentChat.stopGenerating()
currentChat.newPromptResponsePair(textInput.text); currentChat.newPromptResponsePair(textInput.text);
currentChat.prompt(textInput.text, currentChat.prompt(textInput.text,

@ -19,7 +19,7 @@ MyDialog {
property bool showEmbeddingModels: false property bool showEmbeddingModels: false
onOpened: { onOpened: {
Network.sendModelDownloaderDialog(); Network.trackEvent("download_dialog")
if (showEmbeddingModels) { if (showEmbeddingModels) {
ModelList.downloadableModels.expanded = true ModelList.downloadableModels.expanded = true

@ -100,16 +100,10 @@ NOTE: By turning on this feature, you will be sending your data to the GPT4All O
} }
onAccepted: { onAccepted: {
if (MySettings.networkIsActive) MySettings.networkIsActive = true
return
MySettings.networkIsActive = true;
Network.sendNetworkToggled(true);
} }
onRejected: { onRejected: {
if (!MySettings.networkIsActive) MySettings.networkIsActive = false
return
MySettings.networkIsActive = false;
Network.sendNetworkToggled(false);
} }
} }

@ -16,7 +16,7 @@ MyDialog {
modal: true modal: true
padding: 20 padding: 20
onOpened: { onOpened: {
Network.sendSettingsDialog(); Network.trackEvent("settings_dialog")
} }
signal downloadClicked signal downloadClicked

@ -123,8 +123,6 @@ model release that uses your data!")
buttons: optInStatisticsRadio.children buttons: optInStatisticsRadio.children
onClicked: { onClicked: {
MySettings.networkUsageStatsActive = optInStatisticsRadio.checked MySettings.networkUsageStatsActive = optInStatisticsRadio.checked
if (!optInStatisticsRadio.checked)
Network.sendOptOut();
if (optInNetworkRadio.choiceMade && optInStatisticsRadio.choiceMade) if (optInNetworkRadio.choiceMade && optInStatisticsRadio.choiceMade)
startupDialog.close(); startupDialog.close();
} }
@ -140,7 +138,7 @@ model release that uses your data!")
RadioButton { RadioButton {
id: optInStatisticsRadioYes id: optInStatisticsRadioYes
checked: false checked: MySettings.networkUsageStatsActive
text: qsTr("Yes") text: qsTr("Yes")
font.pixelSize: theme.fontSizeLarge font.pixelSize: theme.fontSizeLarge
Accessible.role: Accessible.RadioButton Accessible.role: Accessible.RadioButton
@ -182,6 +180,7 @@ model release that uses your data!")
} }
RadioButton { RadioButton {
id: optInStatisticsRadioNo id: optInStatisticsRadioNo
checked: MySettings.isNetworkUsageStatsActiveSet() && !MySettings.networkUsageStatsActive
text: qsTr("No") text: qsTr("No")
font.pixelSize: theme.fontSizeLarge font.pixelSize: theme.fontSizeLarge
Accessible.role: Accessible.RadioButton Accessible.role: Accessible.RadioButton
@ -254,7 +253,7 @@ model release that uses your data!")
RadioButton { RadioButton {
id: optInNetworkRadioYes id: optInNetworkRadioYes
checked: false checked: MySettings.networkIsActive
text: qsTr("Yes") text: qsTr("Yes")
font.pixelSize: theme.fontSizeLarge font.pixelSize: theme.fontSizeLarge
Accessible.role: Accessible.RadioButton Accessible.role: Accessible.RadioButton
@ -296,6 +295,7 @@ model release that uses your data!")
} }
RadioButton { RadioButton {
id: optInNetworkRadioNo id: optInNetworkRadioNo
checked: MySettings.isNetworkIsActiveSet() && !MySettings.networkIsActive
text: qsTr("No") text: qsTr("No")
font.pixelSize: theme.fontSizeLarge font.pixelSize: theme.fontSizeLarge
Accessible.role: Accessible.RadioButton Accessible.role: Accessible.RadioButton

Loading…
Cancel
Save