mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2024-10-01 01:06:10 -04:00
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>
This commit is contained in:
parent
4193533154
commit
c622921894
@ -785,7 +785,7 @@ const std::vector<LLModel::Token> &GPTJ::endTokens() const
|
||||
return fres;
|
||||
}
|
||||
|
||||
std::string get_arch_name(gguf_context *ctx_gguf) {
|
||||
const char *get_arch_name(gguf_context *ctx_gguf) {
|
||||
std::string arch_name;
|
||||
const int kid = gguf_find_key(ctx_gguf, "general.architecture");
|
||||
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;
|
||||
}
|
||||
|
||||
DLL_EXPORT bool magic_match(const char * fname) {
|
||||
DLL_EXPORT char *get_file_arch(const char *fname) {
|
||||
struct ggml_context * ctx_meta = NULL;
|
||||
struct gguf_init_params params = {
|
||||
/*.no_alloc = */ true,
|
||||
/*.ctx = */ &ctx_meta,
|
||||
};
|
||||
gguf_context *ctx_gguf = gguf_init_from_file(fname, params);
|
||||
if (!ctx_gguf)
|
||||
return false;
|
||||
|
||||
bool isValid = gguf_get_version(ctx_gguf) <= 3;
|
||||
isValid = isValid && get_arch_name(ctx_gguf) == "gptj";
|
||||
char *arch = nullptr;
|
||||
if (ctx_gguf && gguf_get_version(ctx_gguf) <= 3) {
|
||||
arch = strdup(get_arch_name(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() {
|
||||
|
@ -104,7 +104,7 @@ static int llama_sample_top_p_top_k(
|
||||
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;
|
||||
const int kid = gguf_find_key(ctx_gguf, "general.architecture");
|
||||
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;
|
||||
}
|
||||
|
||||
DLL_EXPORT bool magic_match(const char *fname) {
|
||||
auto * ctx = load_gguf(fname);
|
||||
std::string arch = get_arch_name(ctx);
|
||||
|
||||
bool valid = true;
|
||||
|
||||
if (std::find(KNOWN_ARCHES.begin(), KNOWN_ARCHES.end(), arch) == KNOWN_ARCHES.end()) {
|
||||
// not supported by this version of llama.cpp
|
||||
if (arch != "gptj") { // we support this via another module
|
||||
std::cerr << __func__ << ": unsupported model architecture: " << arch << "\n";
|
||||
DLL_EXPORT char *get_file_arch(const char *fname) {
|
||||
auto *ctx = load_gguf(fname);
|
||||
char *arch = nullptr;
|
||||
if (ctx) {
|
||||
std::string archStr = get_arch_name(ctx);
|
||||
if (is_embedding_arch(archStr) && gguf_find_key(ctx, (archStr + ".pooling_type").c_str()) < 0) {
|
||||
// old bert.cpp embedding model
|
||||
} else {
|
||||
arch = strdup(archStr.c_str());
|
||||
}
|
||||
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);
|
||||
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() {
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@ -49,14 +50,17 @@ LLModel::Implementation::Implementation(Dlhandle &&dlhandle_)
|
||||
auto get_build_variant = m_dlhandle->get<const char *()>("get_build_variant");
|
||||
assert(get_build_variant);
|
||||
m_buildVariant = get_build_variant();
|
||||
m_magicMatch = m_dlhandle->get<bool(const char*)>("magic_match");
|
||||
assert(m_magicMatch);
|
||||
m_getFileArch = m_dlhandle->get<char *(const char *)>("get_file_arch");
|
||||
assert(m_getFileArch);
|
||||
m_isArchSupported = m_dlhandle->get<bool(const char *)>("is_arch_supported");
|
||||
assert(m_isArchSupported);
|
||||
m_construct = m_dlhandle->get<LLModel *()>("construct");
|
||||
assert(m_construct);
|
||||
}
|
||||
|
||||
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_modelType(o.m_modelType)
|
||||
, 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) {
|
||||
bool buildVariantMatched = false;
|
||||
std::optional<std::string> archName;
|
||||
for (const auto& i : implementationList()) {
|
||||
if (buildVariant != i.m_buildVariant) continue;
|
||||
buildVariantMatched = true;
|
||||
|
||||
if (!i.m_magicMatch(fname)) continue;
|
||||
return &i;
|
||||
char *arch = i.m_getFileArch(fname);
|
||||
if (!arch) continue;
|
||||
archName = arch;
|
||||
|
||||
bool archSupported = i.m_isArchSupported(arch);
|
||||
free(arch);
|
||||
if (archSupported) return &i;
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -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 (buildVariant == "auto") {
|
||||
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) {
|
||||
LLModel* metalimpl = impl->m_construct();
|
||||
metalimpl->m_implementation = impl;
|
||||
@ -177,7 +193,6 @@ LLModel *LLModel::Implementation::construct(const std::string &modelPath, std::s
|
||||
}
|
||||
}
|
||||
impl = implementation(modelPath.c_str(), buildVariant);
|
||||
if (!impl) return nullptr;
|
||||
}
|
||||
|
||||
// Construct and return llmodel implementation
|
||||
|
@ -17,6 +17,29 @@ class LLModel {
|
||||
public:
|
||||
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 {
|
||||
int index;
|
||||
int type;
|
||||
@ -53,7 +76,8 @@ public:
|
||||
static const Implementation *implementation(const char *fname, const std::string &buildVariant);
|
||||
static LLModel *constructDefaultLlama();
|
||||
|
||||
bool (*m_magicMatch)(const char *fname);
|
||||
char *(*m_getFileArch)(const char *fname);
|
||||
bool (*m_isArchSupported)(const char *arch);
|
||||
LLModel *(*m_construct)();
|
||||
|
||||
std::string_view m_modelType;
|
||||
|
@ -40,11 +40,6 @@ llmodel_model llmodel_model_create2(const char *model_path, const char *build_va
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!llModel) {
|
||||
llmodel_set_error(error, "Model format not supported (no matching implementation found)");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto wrapper = new LLModelWrapper;
|
||||
wrapper->llModel = llModel;
|
||||
return wrapper;
|
||||
|
@ -179,7 +179,7 @@ void Chat::promptProcessing()
|
||||
emit responseStateChanged();
|
||||
}
|
||||
|
||||
void Chat::responseStopped()
|
||||
void Chat::responseStopped(qint64 promptResponseMs)
|
||||
{
|
||||
m_tokenSpeed = QString();
|
||||
emit tokenSpeedChanged();
|
||||
@ -228,8 +228,13 @@ void Chat::responseStopped()
|
||||
emit responseStateChanged();
|
||||
if (m_generatedName.isEmpty())
|
||||
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
|
||||
@ -331,7 +336,7 @@ void Chat::generatedNameChanged(const QString &name)
|
||||
|
||||
void Chat::handleRecalculating()
|
||||
{
|
||||
Network::globalInstance()->sendRecalculatingContext(m_chatModel->count());
|
||||
Network::globalInstance()->trackChatEvent("recalc_context", { {"length", m_chatModel->count()} });
|
||||
emit recalcChanged();
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@ private Q_SLOTS:
|
||||
void handleResponseChanged(const QString &response);
|
||||
void handleModelLoadingPercentageChanged(float);
|
||||
void promptProcessing();
|
||||
void responseStopped();
|
||||
void responseStopped(qint64 promptResponseMs);
|
||||
void generatedNameChanged(const QString &name);
|
||||
void handleRecalculating();
|
||||
void handleModelLoadingError(const QString &error);
|
||||
@ -175,6 +175,7 @@ private:
|
||||
bool m_shouldDeleteLater = false;
|
||||
float m_modelLoadingPercentage = 0.0f;
|
||||
LocalDocsCollectionsModel *m_collectionModel;
|
||||
bool m_firstResponse = true;
|
||||
};
|
||||
|
||||
#endif // CHAT_H
|
||||
|
@ -7,6 +7,8 @@
|
||||
#include "mysettings.h"
|
||||
#include "../gpt4all-backend/llmodel.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
//#define DEBUG
|
||||
//#define DEBUG_MODEL_LOADING
|
||||
|
||||
@ -74,8 +76,6 @@ ChatLLM::ChatLLM(Chat *parent, bool isServer)
|
||||
, m_restoreStateFromText(false)
|
||||
{
|
||||
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,
|
||||
Qt::QueuedConnection); // explicitly queued
|
||||
connect(this, &ChatLLM::shouldTrySwitchContextChanged, this, &ChatLLM::handleShouldTrySwitchContextChanged,
|
||||
@ -278,6 +278,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
m_llModelInfo.fileInfo = fileInfo;
|
||||
|
||||
if (fileInfo.exists()) {
|
||||
QVariantMap modelLoadProps;
|
||||
if (modelInfo.isOnline) {
|
||||
QString apiKey;
|
||||
QString modelName;
|
||||
@ -298,6 +299,9 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
model->setAPIKey(apiKey);
|
||||
m_llModelInfo.model = model;
|
||||
} else {
|
||||
QElapsedTimer modelLoadTimer;
|
||||
modelLoadTimer.start();
|
||||
|
||||
auto n_ctx = MySettings::globalInstance()->modelContextLength(modelInfo);
|
||||
m_ctx.n_ctx = n_ctx;
|
||||
auto ngl = MySettings::globalInstance()->modelGpuLayers(modelInfo);
|
||||
@ -307,7 +311,21 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
if (m_forceMetal)
|
||||
buildVariant = "metal";
|
||||
#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->isModelBlacklisted(filePath.toStdString())) {
|
||||
@ -368,6 +386,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
// llama_init_from_file returned nullptr
|
||||
emit reportDevice("CPU");
|
||||
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);
|
||||
} else if (!m_llModelInfo.model->usingGPUDevice()) {
|
||||
// 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
|
||||
emit reportDevice("CPU");
|
||||
emit reportFallbackReason("<br>model or quant has no GPU support");
|
||||
modelLoadProps.insert("cpu_fallback_reason", "gpu_unsupported_model");
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
@ -384,6 +404,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store
|
||||
m_llModelInfo = LLModelInfo();
|
||||
emit modelLoadingError(QString("Could not load model due to invalid model file for %1").arg(modelInfo.filename()));
|
||||
modelLoadProps.insert("error", "loadmodel_failed");
|
||||
} else {
|
||||
switch (m_llModelInfo.model->implementation().modelType()[0]) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
modelLoadProps.insert("$duration", modelLoadTimer.elapsed() / 1000.);
|
||||
}
|
||||
} else {
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(m_llModelInfo); // release back into the store
|
||||
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)
|
||||
@ -416,12 +439,9 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
#endif
|
||||
emit modelLoadingPercentageChanged(isModelLoaded() ? 1.0f : 0.0f);
|
||||
|
||||
static bool isFirstLoad = true;
|
||||
if (isFirstLoad) {
|
||||
emit sendStartup();
|
||||
isFirstLoad = false;
|
||||
} else
|
||||
emit sendModelLoaded();
|
||||
modelLoadProps.insert("requestedDevice", MySettings::globalInstance()->device());
|
||||
modelLoadProps.insert("model", modelInfo.filename());
|
||||
Network::globalInstance()->trackChatEvent("model_load", modelLoadProps);
|
||||
} else {
|
||||
if (!m_isServer)
|
||||
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));
|
||||
fflush(stdout);
|
||||
#endif
|
||||
QElapsedTimer totalTime;
|
||||
totalTime.start();
|
||||
m_timer->start();
|
||||
if (!docsContext.isEmpty()) {
|
||||
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);
|
||||
#endif
|
||||
m_timer->stop();
|
||||
qint64 elapsed = totalTime.elapsed();
|
||||
std::string trimmed = trim_whitespace(m_response);
|
||||
if (trimmed != m_response) {
|
||||
m_response = trimmed;
|
||||
emit responseChanged(QString::fromStdString(m_response));
|
||||
}
|
||||
emit responseStopped();
|
||||
emit responseStopped(elapsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -123,9 +123,7 @@ Q_SIGNALS:
|
||||
void modelLoadingWarning(const QString &warning);
|
||||
void responseChanged(const QString &response);
|
||||
void promptProcessing();
|
||||
void responseStopped();
|
||||
void sendStartup();
|
||||
void sendModelLoaded();
|
||||
void responseStopped(qint64 promptResponseMs);
|
||||
void generatedNameChanged(const QString &name);
|
||||
void stateChanged();
|
||||
void threadStarted();
|
||||
|
@ -1,7 +1,9 @@
|
||||
#include "database.h"
|
||||
#include "mysettings.h"
|
||||
#include "embllm.h"
|
||||
|
||||
#include "embeddings.h"
|
||||
#include "embllm.h"
|
||||
#include "mysettings.h"
|
||||
#include "network.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QPdfDocument>
|
||||
@ -490,7 +492,7 @@ QSqlError initDb()
|
||||
i.collection = collection_name;
|
||||
i.folder_path = folder_path;
|
||||
i.folder_id = folder_id;
|
||||
emit addCollectionItem(i);
|
||||
emit addCollectionItem(i, false);
|
||||
|
||||
// Add a document
|
||||
int document_time = 123456789;
|
||||
@ -535,13 +537,13 @@ QSqlError initDb()
|
||||
|
||||
Database::Database(int chunkSize)
|
||||
: QObject(nullptr)
|
||||
, m_watcher(new QFileSystemWatcher(this))
|
||||
, m_chunkSize(chunkSize)
|
||||
, m_scanTimer(new QTimer(this))
|
||||
, m_watcher(new QFileSystemWatcher(this))
|
||||
, m_embLLM(new EmbeddingLLM)
|
||||
, m_embeddings(new Embeddings(this))
|
||||
{
|
||||
moveToThread(&m_dbThread);
|
||||
connect(&m_dbThread, &QThread::started, this, &Database::start);
|
||||
m_dbThread.setObjectName("database");
|
||||
m_dbThread.start();
|
||||
}
|
||||
@ -556,11 +558,13 @@ void Database::scheduleNext(int folder_id, size_t countForFolder)
|
||||
{
|
||||
emit updateCurrentDocsToIndex(folder_id, countForFolder);
|
||||
if (!countForFolder) {
|
||||
emit updateIndexing(folder_id, false);
|
||||
updateFolderStatus(folder_id, FolderStatus::Complete);
|
||||
emit updateInstalled(folder_id, true);
|
||||
}
|
||||
if (!m_docsToScan.isEmpty())
|
||||
QTimer::singleShot(0, this, &Database::scanQueue);
|
||||
if (m_docsToScan.isEmpty()) {
|
||||
m_scanTimer->stop();
|
||||
updateIndexingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
void Database::handleDocumentError(const QString &errorMessage,
|
||||
@ -721,7 +725,6 @@ void Database::removeFolderFromDocumentQueue(int folder_id)
|
||||
return;
|
||||
m_docsToScan.remove(folder_id);
|
||||
emit removeFolderById(folder_id);
|
||||
emit docsToScanChanged();
|
||||
}
|
||||
|
||||
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);
|
||||
emit updateCurrentBytesToIndex(folder_id, bytes);
|
||||
emit updateTotalBytesToIndex(folder_id, bytes);
|
||||
emit docsToScanChanged();
|
||||
m_scanTimer->start();
|
||||
}
|
||||
|
||||
void Database::scanQueue()
|
||||
{
|
||||
if (m_docsToScan.isEmpty())
|
||||
if (m_docsToScan.isEmpty()) {
|
||||
m_scanTimer->stop();
|
||||
updateIndexingStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentInfo info = dequeueDocument();
|
||||
const size_t countForFolder = countOfDocuments(info.folder);
|
||||
@ -818,6 +824,8 @@ void Database::scanQueue()
|
||||
QSqlDatabase::database().transaction();
|
||||
Q_ASSERT(document_id != -1);
|
||||
if (info.isPdf()) {
|
||||
updateFolderStatus(folder_id, FolderStatus::Embedding, -1, info.currentPage == 0);
|
||||
|
||||
QPdfDocument doc;
|
||||
if (QPdfDocument::Error::None != doc.load(info.doc.canonicalFilePath())) {
|
||||
handleDocumentError("ERROR: Could not load pdf",
|
||||
@ -850,6 +858,8 @@ void Database::scanQueue()
|
||||
emit subtractCurrentBytesToIndex(info.folder, bytes - (bytesPerPage * doc.pageCount()));
|
||||
}
|
||||
} else {
|
||||
updateFolderStatus(folder_id, FolderStatus::Embedding, -1, info.currentPosition == 0);
|
||||
|
||||
QFile file(document_path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
handleDocumentError("ERROR: Cannot open file for scanning",
|
||||
@ -884,7 +894,7 @@ void Database::scanQueue()
|
||||
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)
|
||||
qDebug() << "scanning folder for documents" << folder_path;
|
||||
@ -915,7 +925,7 @@ void Database::scanDocuments(int folder_id, const QString &folder_path)
|
||||
}
|
||||
|
||||
if (!infos.isEmpty()) {
|
||||
emit updateIndexing(folder_id, true);
|
||||
updateFolderStatus(folder_id, FolderStatus::Started, infos.count(), false, isNew);
|
||||
enqueueDocuments(folder_id, infos);
|
||||
}
|
||||
}
|
||||
@ -925,7 +935,7 @@ void Database::start()
|
||||
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &Database::directoryChanged);
|
||||
connect(m_embLLM, &EmbeddingLLM::embeddingsGenerated, this, &Database::handleEmbeddingsGenerated);
|
||||
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")) {
|
||||
qWarning() << "ERROR: missing sqllite driver";
|
||||
} else {
|
||||
@ -937,10 +947,11 @@ void Database::start()
|
||||
if (m_embeddings->fileExists() && !m_embeddings->load())
|
||||
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)
|
||||
qDebug() << "addCurrentFolders";
|
||||
@ -950,21 +961,26 @@ void Database::addCurrentFolders()
|
||||
QList<CollectionItem> collections;
|
||||
if (!selectAllFromCollections(q, &collections)) {
|
||||
qWarning() << "ERROR: Cannot select collections" << q.lastError();
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
emit collectionListUpdated(collections);
|
||||
|
||||
int nAdded = 0;
|
||||
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);
|
||||
if (!info.exists() || !info.isReadable()) {
|
||||
qWarning() << "ERROR: Cannot add folder that doesn't exist or not readable" << path;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery q;
|
||||
@ -973,13 +989,13 @@ void Database::addFolder(const QString &collection, const QString &path)
|
||||
// See if the folder exists in the db
|
||||
if (!selectFolder(q, path, &folder_id)) {
|
||||
qWarning() << "ERROR: Cannot select folder from path" << path << q.lastError();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the folder
|
||||
if (folder_id == -1 && !addFolderToDB(q, path, &folder_id)) {
|
||||
qWarning() << "ERROR: Cannot add folder to db with path" << path << q.lastError();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Q_ASSERT(folder_id != -1);
|
||||
@ -988,24 +1004,32 @@ void Database::addFolder(const QString &collection, const QString &path)
|
||||
QList<int> folders;
|
||||
if (!selectFoldersFromCollection(q, collection, &folders)) {
|
||||
qWarning() << "ERROR: Cannot select folders from collections" << collection << q.lastError();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool added = false;
|
||||
if (!folders.contains(folder_id)) {
|
||||
if (!addCollection(q, collection, folder_id)) {
|
||||
qWarning() << "ERROR: Cannot add folder to collection" << collection << path << q.lastError();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
CollectionItem i;
|
||||
i.collection = collection;
|
||||
i.folder_path = path;
|
||||
i.folder_id = folder_id;
|
||||
emit addCollectionItem(i);
|
||||
emit addCollectionItem(i, fromDb);
|
||||
added = true;
|
||||
}
|
||||
|
||||
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)
|
||||
@ -1285,5 +1309,69 @@ void Database::directoryChanged(const QString &path)
|
||||
cleanDB();
|
||||
|
||||
// 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
|
||||
#define DATABASE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QtSql>
|
||||
#include <QQueue>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFileInfo>
|
||||
#include <QThread>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QThread>
|
||||
#include <QtSql>
|
||||
|
||||
#include "embllm.h"
|
||||
|
||||
class Embeddings;
|
||||
class QTimer;
|
||||
|
||||
struct DocumentInfo
|
||||
{
|
||||
int folder;
|
||||
@ -58,9 +61,10 @@ public:
|
||||
virtual ~Database();
|
||||
|
||||
public Q_SLOTS:
|
||||
void start();
|
||||
void scanQueue();
|
||||
void scanDocuments(int folder_id, const QString &folder_path);
|
||||
void addFolder(const QString &collection, const QString &path);
|
||||
void scanDocuments(int folder_id, const QString &folder_path, bool isNew);
|
||||
bool addFolder(const QString &collection, const QString &path, bool fromDb);
|
||||
void removeFolder(const QString &collection, const QString &path);
|
||||
void retrieveFromDB(const QList<QString> &collections, const QString &text, int retrievalSize, QList<ResultInfo> *results);
|
||||
void cleanDB();
|
||||
@ -78,21 +82,22 @@ Q_SIGNALS:
|
||||
void updateTotalBytesToIndex(int folder_id, size_t totalBytesToIndex);
|
||||
void updateCurrentEmbeddingsToIndex(int folder_id, size_t currentBytesToIndex);
|
||||
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 removeCollectionItem(const QString &collectionName);
|
||||
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();
|
||||
int addCurrentFolders();
|
||||
void handleEmbeddingsGenerated(const QVector<EmbeddingResult> &embeddings);
|
||||
void handleErrorGenerated(int folder_id, const QString &error);
|
||||
|
||||
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);
|
||||
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,
|
||||
@ -107,10 +112,15 @@ private:
|
||||
void removeFolderFromDocumentQueue(int folder_id);
|
||||
void enqueueDocumentInternal(const DocumentInfo &info, bool prepend = false);
|
||||
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:
|
||||
int m_chunkSize;
|
||||
QTimer *m_scanTimer;
|
||||
QMap<int, QQueue<DocumentInfo>> m_docsToScan;
|
||||
QElapsedTimer m_indexingTimer;
|
||||
QMap<int, FolderStatusRecord> m_foldersBeingIndexed;
|
||||
QList<ResultInfo> m_retrieve;
|
||||
QThread m_dbThread;
|
||||
QFileSystemWatcher *m_watcher;
|
||||
|
@ -75,15 +75,25 @@ bool Download::hasNewerRelease() const
|
||||
return compareVersions(versions.first(), currentVersion);
|
||||
}
|
||||
|
||||
bool Download::isFirstStart() const
|
||||
bool Download::isFirstStart(bool writeVersion) const
|
||||
{
|
||||
auto *mySettings = MySettings::globalInstance();
|
||||
|
||||
QSettings settings;
|
||||
settings.sync();
|
||||
QString lastVersionStarted = settings.value("download/lastVersionStarted").toString();
|
||||
bool first = lastVersionStarted != QCoreApplication::applicationVersion();
|
||||
settings.setValue("download/lastVersionStarted", QCoreApplication::applicationVersion());
|
||||
settings.sync();
|
||||
return first;
|
||||
if (first && writeVersion) {
|
||||
settings.setValue("download/lastVersionStarted", QCoreApplication::applicationVersion());
|
||||
// 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()
|
||||
@ -131,7 +141,7 @@ void Download::downloadModel(const QString &modelFile)
|
||||
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::DownloadingRole, true }});
|
||||
ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(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);
|
||||
request.setAttribute(QNetworkRequest::User, modelFile);
|
||||
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);
|
||||
QUrl url = modelReply->request().url();
|
||||
if (url.toString().endsWith(modelFile)) {
|
||||
Network::globalInstance()->sendDownloadCanceled(modelFile);
|
||||
Network::globalInstance()->trackEvent("download_canceled", { {"model", modelFile} });
|
||||
|
||||
// Disconnect the signals
|
||||
disconnect(modelReply, &QNetworkReply::downloadProgress, this, &Download::handleDownloadProgress);
|
||||
@ -178,7 +188,8 @@ void Download::installModel(const QString &modelFile, const QString &apiKey)
|
||||
if (apiKey.isEmpty())
|
||||
return;
|
||||
|
||||
Network::globalInstance()->sendInstallModel(modelFile);
|
||||
Network::globalInstance()->trackEvent("install_model", { {"model", modelFile} });
|
||||
|
||||
QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
|
||||
QFile file(filePath);
|
||||
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*/);
|
||||
if (shouldRemoveInstalled)
|
||||
ModelList::globalInstance()->removeInstalled(info);
|
||||
Network::globalInstance()->sendRemoveModel(modelFile);
|
||||
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
|
||||
file.remove();
|
||||
}
|
||||
|
||||
@ -332,7 +343,11 @@ void Download::handleErrorOccurred(QNetworkReply::NetworkError code)
|
||||
.arg(modelReply->errorString());
|
||||
qWarning() << 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);
|
||||
}
|
||||
|
||||
@ -515,7 +530,7 @@ void Download::handleHashAndSaveFinished(bool success, const QString &error,
|
||||
// The hash and save should send back with tempfile closed
|
||||
Q_ASSERT(!tempFile->isOpen());
|
||||
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 {
|
||||
{ ModelList::CalcHashRole, false },
|
||||
|
@ -54,7 +54,7 @@ public:
|
||||
Q_INVOKABLE void cancelDownload(const QString &modelFile);
|
||||
Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey);
|
||||
Q_INVOKABLE void removeModel(const QString &modelFile);
|
||||
Q_INVOKABLE bool isFirstStart() const;
|
||||
Q_INVOKABLE bool isFirstStart(bool writeVersion = false) const;
|
||||
|
||||
public Q_SLOTS:
|
||||
void updateReleaseNotes();
|
||||
|
@ -57,7 +57,14 @@ bool EmbeddingLLMWorker::loadModel()
|
||||
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
|
||||
// TODO(cebtenzzre): support GPU-accelerated embeddings
|
||||
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!"
|
||||
return QDesktopServices::openUrl(QUrl("https://gpt4all.io/"));
|
||||
#else
|
||||
Network::globalInstance()->sendCheckForUpdates();
|
||||
Network::globalInstance()->trackEvent("check_for_updates");
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
QString tool("maintenancetool");
|
||||
|
@ -18,6 +18,8 @@ LocalDocs::LocalDocs()
|
||||
// Create the DB with the chunk size from settings
|
||||
m_database = new Database(MySettings::globalInstance()->localDocsChunkSize());
|
||||
|
||||
connect(this, &LocalDocs::requestStart, m_database,
|
||||
&Database::start, Qt::QueuedConnection);
|
||||
connect(this, &LocalDocs::requestAddFolder, m_database,
|
||||
&Database::addFolder, Qt::QueuedConnection);
|
||||
connect(this, &LocalDocs::requestRemoveFolder, m_database,
|
||||
@ -50,8 +52,6 @@ LocalDocs::LocalDocs()
|
||||
m_localDocsModel, &LocalDocsModel::addCollectionItem, Qt::QueuedConnection);
|
||||
connect(m_database, &Database::removeFolderById,
|
||||
m_localDocsModel, &LocalDocsModel::removeFolderById, Qt::QueuedConnection);
|
||||
connect(m_database, &Database::removeCollectionItem,
|
||||
m_localDocsModel, &LocalDocsModel::removeCollectionItem, Qt::QueuedConnection);
|
||||
connect(m_database, &Database::collectionListUpdated,
|
||||
m_localDocsModel, &LocalDocsModel::collectionListUpdated, Qt::QueuedConnection);
|
||||
|
||||
@ -68,7 +68,7 @@ void LocalDocs::addFolder(const QString &collection, const QString &path)
|
||||
{
|
||||
const QUrl url(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)
|
||||
|
@ -26,7 +26,8 @@ public Q_SLOTS:
|
||||
void aboutToQuit();
|
||||
|
||||
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 requestChunkSizeChange(int chunkSize);
|
||||
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 "localdocs.h"
|
||||
#include "network.h"
|
||||
|
||||
LocalDocsCollectionsModel::LocalDocsCollectionsModel(QObject *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});
|
||||
}
|
||||
|
||||
void LocalDocsModel::addCollectionItem(const CollectionItem &item)
|
||||
void LocalDocsModel::addCollectionItem(const CollectionItem &item, bool fromDb)
|
||||
{
|
||||
beginInsertRows(QModelIndex(), m_collectionList.size(), m_collectionList.size());
|
||||
m_collectionList.append(item);
|
||||
endInsertRows();
|
||||
|
||||
if (!fromDb) {
|
||||
Network::globalInstance()->trackEvent("doc_collection_add", {
|
||||
{"collection_count", m_collectionList.count()},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void LocalDocsModel::removeCollectionIf(std::function<bool(CollectionItem)> const &predicate) {
|
||||
for (int i = 0; i < m_collectionList.size();) {
|
||||
if (predicate(m_collectionList.at(i))) {
|
||||
beginRemoveRows(QModelIndex(), i, i);
|
||||
m_collectionList.removeAt(i);
|
||||
endRemoveRows();
|
||||
|
||||
Network::globalInstance()->trackEvent("doc_collection_remove", {
|
||||
{"collection_count", m_collectionList.count()},
|
||||
});
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LocalDocsModel::removeFolderById(int folder_id)
|
||||
{
|
||||
for (int i = 0; i < m_collectionList.size();) {
|
||||
if (m_collectionList.at(i).folder_id == folder_id) {
|
||||
beginRemoveRows(QModelIndex(), i, i);
|
||||
m_collectionList.removeAt(i);
|
||||
endRemoveRows();
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
removeCollectionIf([folder_id](const auto &c) { return c.folder_id == folder_id; });
|
||||
}
|
||||
|
||||
void LocalDocsModel::removeCollectionPath(const QString &name, const QString &path)
|
||||
{
|
||||
for (int i = 0; i < m_collectionList.size();) {
|
||||
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)
|
||||
{
|
||||
for (int i = 0; i < m_collectionList.size();) {
|
||||
if (m_collectionList.at(i).collection == collectionName) {
|
||||
beginRemoveRows(QModelIndex(), i, i);
|
||||
m_collectionList.removeAt(i);
|
||||
endRemoveRows();
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
removeCollectionIf([&name, &path](const auto &c) { return c.collection == name && c.folder_path == path; });
|
||||
}
|
||||
|
||||
void LocalDocsModel::collectionListUpdated(const QList<CollectionItem> &collectionList)
|
||||
|
@ -55,10 +55,9 @@ public Q_SLOTS:
|
||||
void updateTotalBytesToIndex(int folder_id, size_t totalBytesToIndex);
|
||||
void updateCurrentEmbeddingsToIndex(int folder_id, size_t currentBytesToIndex);
|
||||
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 removeCollectionPath(const QString &name, const QString &path);
|
||||
void removeCollectionItem(const QString &collectionName);
|
||||
void collectionListUpdated(const QList<CollectionItem> &collectionList);
|
||||
|
||||
private:
|
||||
@ -66,6 +65,7 @@ private:
|
||||
void updateField(int folder_id, T value,
|
||||
const std::function<void(CollectionItem&, T)>& updater,
|
||||
const QVector<int>& roles);
|
||||
void removeCollectionIf(std::function<bool(CollectionItem)> const &predicate);
|
||||
|
||||
private:
|
||||
QList<CollectionItem> m_collectionList;
|
||||
|
@ -910,15 +910,23 @@ bool MySettings::networkIsActive() const
|
||||
return setting.value("network/isActive", default_networkIsActive).toBool();
|
||||
}
|
||||
|
||||
bool MySettings::isNetworkIsActiveSet() const
|
||||
{
|
||||
QSettings setting;
|
||||
setting.sync();
|
||||
return setting.value("network/isActive").isValid();
|
||||
}
|
||||
|
||||
void MySettings::setNetworkIsActive(bool b)
|
||||
{
|
||||
if (networkIsActive() == b)
|
||||
return;
|
||||
|
||||
QSettings setting;
|
||||
setting.setValue("network/isActive", b);
|
||||
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
|
||||
@ -928,13 +936,21 @@ bool MySettings::networkUsageStatsActive() const
|
||||
return setting.value("network/usageStatsActive", default_networkUsageStatsActive).toBool();
|
||||
}
|
||||
|
||||
bool MySettings::isNetworkUsageStatsActiveSet() const
|
||||
{
|
||||
QSettings setting;
|
||||
setting.sync();
|
||||
return setting.value("network/usageStatsActive").isValid();
|
||||
}
|
||||
|
||||
void MySettings::setNetworkUsageStatsActive(bool b)
|
||||
{
|
||||
if (networkUsageStatsActive() == b)
|
||||
return;
|
||||
|
||||
QSettings setting;
|
||||
setting.setValue("network/usageStatsActive", b);
|
||||
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;
|
||||
void setNetworkAttribution(const QString &a);
|
||||
bool networkIsActive() const;
|
||||
Q_INVOKABLE bool isNetworkIsActiveSet() const;
|
||||
void setNetworkIsActive(bool b);
|
||||
bool networkUsageStatsActive() const;
|
||||
Q_INVOKABLE bool isNetworkUsageStatsActiveSet() const;
|
||||
void setNetworkUsageStatsActive(bool b);
|
||||
int networkPort() const;
|
||||
void setNetworkPort(int c);
|
||||
|
@ -1,8 +1,13 @@
|
||||
#include "network.h"
|
||||
#include "llm.h"
|
||||
|
||||
#include "chatlistmodel.h"
|
||||
#include "download.h"
|
||||
#include "llm.h"
|
||||
#include "localdocs.h"
|
||||
#include "mysettings.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QUuid>
|
||||
@ -14,16 +19,49 @@
|
||||
|
||||
//#define DEBUG
|
||||
|
||||
static const char MIXPANEL_TOKEN[] = "ce362e568ddaee16ed243eaffb5860a2";
|
||||
|
||||
#if defined(Q_OS_MAC)
|
||||
|
||||
#include <sys/sysctl.h>
|
||||
std::string getCPUModel() {
|
||||
static QString getCPUModel() {
|
||||
char buffer[256];
|
||||
size_t bufferlen = sizeof(buffer);
|
||||
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
|
||||
|
||||
|
||||
class MyNetwork: public Network { };
|
||||
Q_GLOBAL_STATIC(MyNetwork, networkInstance)
|
||||
Network *Network::globalInstance()
|
||||
@ -33,40 +71,43 @@ Network *Network::globalInstance()
|
||||
|
||||
Network::Network()
|
||||
: QObject{nullptr}
|
||||
, m_shouldSendStartup(false)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.sync();
|
||||
m_uniqueId = settings.value("uniqueId", generateUniqueId()).toString();
|
||||
settings.setValue("uniqueId", m_uniqueId);
|
||||
settings.sync();
|
||||
connect(MySettings::globalInstance(), &MySettings::networkIsActiveChanged, this, &Network::handleIsActiveChanged);
|
||||
connect(MySettings::globalInstance(), &MySettings::networkUsageStatsActiveChanged, this, &Network::handleUsageStatsActiveChanged);
|
||||
if (MySettings::globalInstance()->networkIsActive())
|
||||
m_sessionId = generateUniqueId();
|
||||
|
||||
// allow sendMixpanel to be called from any thread
|
||||
connect(this, &Network::requestMixpanel, this, &Network::sendMixpanel, Qt::QueuedConnection);
|
||||
|
||||
const auto *mySettings = MySettings::globalInstance();
|
||||
connect(mySettings, &MySettings::networkIsActiveChanged, this, &Network::handleIsActiveChanged);
|
||||
connect(mySettings, &MySettings::networkUsageStatsActiveChanged, this, &Network::handleUsageStatsActiveChanged);
|
||||
|
||||
m_hasSentOptIn = !Download::globalInstance()->isFirstStart() && mySettings->networkUsageStatsActive();
|
||||
m_hasSentOptOut = !Download::globalInstance()->isFirstStart() && !mySettings->networkUsageStatsActive();
|
||||
|
||||
if (mySettings->networkIsActive())
|
||||
sendHealth();
|
||||
if (MySettings::globalInstance()->networkUsageStatsActive())
|
||||
sendIpify();
|
||||
connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this,
|
||||
&Network::handleSslErrors);
|
||||
}
|
||||
|
||||
// NOTE: this won't be useful until we make it possible to change this via the settings page
|
||||
void Network::handleUsageStatsActiveChanged()
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive())
|
||||
m_sendUsageStats = false;
|
||||
}
|
||||
|
||||
void Network::handleIsActiveChanged()
|
||||
{
|
||||
if (MySettings::globalInstance()->networkUsageStatsActive())
|
||||
sendHealth();
|
||||
}
|
||||
|
||||
void Network::handleUsageStatsActiveChanged()
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive())
|
||||
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);
|
||||
@ -167,8 +208,8 @@ void Network::handleSslErrors(QNetworkReply *reply, const QList<QSslError> &erro
|
||||
void Network::sendOptOut()
|
||||
{
|
||||
QJsonObject properties;
|
||||
properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2");
|
||||
properties.insert("time", QDateTime::currentSecsSinceEpoch());
|
||||
properties.insert("token", MIXPANEL_TOKEN);
|
||||
properties.insert("time", QDateTime::currentMSecsSinceEpoch());
|
||||
properties.insert("distinct_id", m_uniqueId);
|
||||
properties.insert("$insert_id", generateUniqueId());
|
||||
|
||||
@ -181,7 +222,7 @@ void Network::sendOptOut()
|
||||
|
||||
QJsonDocument doc;
|
||||
doc.setArray(array);
|
||||
sendMixpanel(doc.toJson(QJsonDocument::Compact), true /*isOptOut*/);
|
||||
emit requestMixpanel(doc.toJson(QJsonDocument::Compact), true /*isOptOut*/);
|
||||
|
||||
#if defined(DEBUG)
|
||||
printf("%s %s\n", qPrintable("opt_out"), qPrintable(doc.toJson(QJsonDocument::Indented)));
|
||||
@ -189,215 +230,76 @@ void Network::sendOptOut()
|
||||
#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()
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive())
|
||||
const auto *mySettings = MySettings::globalInstance();
|
||||
Q_ASSERT(mySettings->isNetworkUsageStatsActiveSet());
|
||||
if (!mySettings->networkUsageStatsActive()) {
|
||||
// send a single opt-out per session after the user has made their selections,
|
||||
// unless this is a normal start (same version) and the user was already opted out
|
||||
if (!m_hasSentOptOut) {
|
||||
sendOptOut();
|
||||
m_hasSentOptOut = true;
|
||||
}
|
||||
return;
|
||||
m_shouldSendStartup = true;
|
||||
if (m_ipify.isEmpty())
|
||||
return; // when it completes it will send
|
||||
sendMixpanelEvent("startup");
|
||||
}
|
||||
|
||||
// only chance to enable usage stats is at the start of a new session
|
||||
m_sendUsageStats = true;
|
||||
|
||||
const auto *display = QGuiApplication::primaryScreen();
|
||||
trackEvent("startup", {
|
||||
{"$screen_dpi", std::round(display->physicalDotsPerInch())},
|
||||
{"display", 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::sendCheckForUpdates()
|
||||
void Network::trackChatEvent(const QString &ev, QVariantMap props)
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive())
|
||||
return;
|
||||
sendMixpanelEvent("check_for_updates");
|
||||
const auto &curChat = ChatListModel::globalInstance()->currentChat();
|
||||
if (!props.contains("model"))
|
||||
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::sendModelDownloaderDialog()
|
||||
void Network::trackEvent(const QString &ev, const QVariantMap &props)
|
||||
{
|
||||
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;
|
||||
|
||||
KeyValue kv;
|
||||
kv.key = QString("length");
|
||||
kv.value = QJsonValue(conversationLength);
|
||||
sendMixpanelEvent("recalc_context", QVector<KeyValue>{kv});
|
||||
}
|
||||
|
||||
void Network::sendNonCompatHardware()
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive())
|
||||
return;
|
||||
sendMixpanelEvent("noncompat_hardware");
|
||||
}
|
||||
|
||||
void Network::sendMixpanelEvent(const QString &ev, const QVector<KeyValue> &values)
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive())
|
||||
if (!m_sendUsageStats)
|
||||
return;
|
||||
|
||||
Q_ASSERT(ChatListModel::globalInstance()->currentChat());
|
||||
QJsonObject properties;
|
||||
properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2");
|
||||
properties.insert("time", QDateTime::currentSecsSinceEpoch());
|
||||
properties.insert("distinct_id", m_uniqueId);
|
||||
|
||||
properties.insert("token", MIXPANEL_TOKEN);
|
||||
if (!props.contains("time"))
|
||||
properties.insert("time", QDateTime::currentMSecsSinceEpoch());
|
||||
properties.insert("distinct_id", m_uniqueId); // effectively a device ID
|
||||
properties.insert("$insert_id", generateUniqueId());
|
||||
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", 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
|
||||
}
|
||||
properties.insert("$os", QSysInfo::prettyProductName());
|
||||
properties.insert("session_id", m_sessionId);
|
||||
properties.insert("name", QCoreApplication::applicationName() + " v" + QCoreApplication::applicationVersion());
|
||||
|
||||
for (const auto& p : values)
|
||||
properties.insert(p.key, p.value);
|
||||
for (const auto &[key, value]: props.asKeyValueRange())
|
||||
properties.insert(key, QJsonValue::fromVariant(value));
|
||||
|
||||
QJsonObject event;
|
||||
event.insert("event", ev);
|
||||
@ -408,7 +310,7 @@ void Network::sendMixpanelEvent(const QString &ev, const QVector<KeyValue> &valu
|
||||
|
||||
QJsonDocument doc;
|
||||
doc.setArray(array);
|
||||
sendMixpanel(doc.toJson(QJsonDocument::Compact));
|
||||
emit requestMixpanel(doc.toJson(QJsonDocument::Compact));
|
||||
|
||||
#if defined(DEBUG)
|
||||
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()
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive() || !m_ipify.isEmpty())
|
||||
if (!m_sendUsageStats || !m_ipify.isEmpty())
|
||||
return;
|
||||
|
||||
QUrl ipifyUrl("https://api.ipify.org");
|
||||
@ -433,7 +335,7 @@ void Network::sendIpify()
|
||||
|
||||
void Network::sendMixpanel(const QByteArray &json, bool isOptOut)
|
||||
{
|
||||
if (!MySettings::globalInstance()->networkUsageStatsActive() && !isOptOut)
|
||||
if (!m_sendUsageStats)
|
||||
return;
|
||||
|
||||
QUrl trackUrl("https://api.mixpanel.com/track");
|
||||
@ -449,7 +351,6 @@ void Network::sendMixpanel(const QByteArray &json, bool isOptOut)
|
||||
|
||||
void Network::handleIpifyFinished()
|
||||
{
|
||||
Q_ASSERT(MySettings::globalInstance()->networkUsageStatsActive());
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||
if (!reply)
|
||||
return;
|
||||
@ -469,8 +370,7 @@ void Network::handleIpifyFinished()
|
||||
#endif
|
||||
reply->deleteLater();
|
||||
|
||||
if (m_shouldSendStartup)
|
||||
sendStartup();
|
||||
trackEvent("ipify_complete");
|
||||
}
|
||||
|
||||
void Network::handleMixpanelFinished()
|
||||
|
@ -19,31 +19,15 @@ public:
|
||||
|
||||
Q_INVOKABLE QString generateUniqueId() const;
|
||||
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:
|
||||
void healthCheckFailed(int code);
|
||||
void requestMixpanel(const QByteArray &json, bool isOptOut = false);
|
||||
|
||||
public Q_SLOTS:
|
||||
void sendOptOut();
|
||||
void sendModelLoaded();
|
||||
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:
|
||||
void handleIpifyFinished();
|
||||
@ -53,18 +37,21 @@ private Q_SLOTS:
|
||||
void handleMixpanelFinished();
|
||||
void handleIsActiveChanged();
|
||||
void handleUsageStatsActiveChanged();
|
||||
void sendMixpanel(const QByteArray &json, bool isOptOut);
|
||||
|
||||
private:
|
||||
void sendOptOut();
|
||||
void sendHealth();
|
||||
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);
|
||||
|
||||
private:
|
||||
bool m_shouldSendStartup;
|
||||
bool m_sendUsageStats = false;
|
||||
bool m_hasSentOptIn;
|
||||
bool m_hasSentOptOut;
|
||||
QString m_ipify;
|
||||
QString m_uniqueId;
|
||||
QString m_sessionId;
|
||||
QNetworkAccessManager m_networkManager;
|
||||
QVector<QNetworkReply*> m_activeUploads;
|
||||
|
||||
|
@ -40,7 +40,7 @@ Rectangle {
|
||||
Accessible.description: qsTr("Create a new chat")
|
||||
onClicked: {
|
||||
ChatListModel.addChat();
|
||||
Network.sendNewChat(ChatListModel.count)
|
||||
Network.trackEvent("new_chat", {"number_of_chats": ChatListModel.count})
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,8 +110,8 @@ Rectangle {
|
||||
// having focus
|
||||
if (chatName.readOnly)
|
||||
return;
|
||||
Network.trackChatEvent("rename_chat")
|
||||
changeName();
|
||||
Network.sendRenameChat()
|
||||
}
|
||||
function changeName() {
|
||||
ChatListModel.get(index).name = chatName.text
|
||||
@ -194,8 +194,8 @@ Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
Network.trackChatEvent("remove_chat")
|
||||
ChatListModel.removeChat(ChatListModel.get(index))
|
||||
Network.sendRemoveChat()
|
||||
}
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Confirm chat deletion")
|
||||
|
@ -1,16 +1,18 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import llm
|
||||
|
||||
import chatlistmodel
|
||||
import download
|
||||
import modellist
|
||||
import network
|
||||
import gpt4all
|
||||
import llm
|
||||
import localdocs
|
||||
import modellist
|
||||
import mysettings
|
||||
import network
|
||||
|
||||
Rectangle {
|
||||
id: window
|
||||
@ -29,6 +31,10 @@ Rectangle {
|
||||
startupDialogs();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
Network.trackEvent("session_end")
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: firstStartDialog
|
||||
function onClosed() {
|
||||
@ -66,12 +72,12 @@ Rectangle {
|
||||
}
|
||||
|
||||
property bool hasShownModelDownload: false
|
||||
property bool hasShownFirstStart: false
|
||||
property bool hasCheckedFirstStart: false
|
||||
property bool hasShownSettingsAccess: false
|
||||
|
||||
function startupDialogs() {
|
||||
if (!LLM.compatHardware()) {
|
||||
Network.sendNonCompatHardware();
|
||||
Network.trackEvent("noncompat_hardware")
|
||||
errorCompatHardware.open();
|
||||
return;
|
||||
}
|
||||
@ -84,10 +90,18 @@ Rectangle {
|
||||
}
|
||||
|
||||
// check for first time start of this version
|
||||
if (!hasShownFirstStart && Download.isFirstStart()) {
|
||||
firstStartDialog.open();
|
||||
hasShownFirstStart = true;
|
||||
return;
|
||||
if (!hasCheckedFirstStart) {
|
||||
if (Download.isFirstStart(/*writeVersion*/ true)) {
|
||||
firstStartDialog.open();
|
||||
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
|
||||
@ -547,7 +561,6 @@ Rectangle {
|
||||
onClicked: {
|
||||
if (MySettings.networkIsActive) {
|
||||
MySettings.networkIsActive = false
|
||||
Network.sendNetworkToggled(false);
|
||||
} else
|
||||
networkDialog.open()
|
||||
}
|
||||
@ -733,7 +746,7 @@ Rectangle {
|
||||
Accessible.description: qsTr("Reset the context and erase current conversation")
|
||||
|
||||
onClicked: {
|
||||
Network.sendResetContext(chatModel.count)
|
||||
Network.trackChatEvent("reset_context", { "length": chatModel.count })
|
||||
currentChat.reset();
|
||||
currentChat.processSystemPrompt();
|
||||
}
|
||||
@ -1288,9 +1301,11 @@ Rectangle {
|
||||
var listElement = chatModel.get(index);
|
||||
|
||||
if (currentChat.responseInProgress) {
|
||||
Network.trackChatEvent("stop_generating_clicked")
|
||||
listElement.stopped = true
|
||||
currentChat.stopGenerating()
|
||||
} else {
|
||||
Network.trackChatEvent("regenerate_clicked")
|
||||
currentChat.regenerateResponse()
|
||||
if (chatModel.count) {
|
||||
if (listElement.name === qsTr("Response: ")) {
|
||||
@ -1405,6 +1420,7 @@ Rectangle {
|
||||
if (textInput.text === "")
|
||||
return
|
||||
|
||||
Network.trackChatEvent("send_message")
|
||||
currentChat.stopGenerating()
|
||||
currentChat.newPromptResponsePair(textInput.text);
|
||||
currentChat.prompt(textInput.text,
|
||||
|
@ -19,7 +19,7 @@ MyDialog {
|
||||
property bool showEmbeddingModels: false
|
||||
|
||||
onOpened: {
|
||||
Network.sendModelDownloaderDialog();
|
||||
Network.trackEvent("download_dialog")
|
||||
|
||||
if (showEmbeddingModels) {
|
||||
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: {
|
||||
if (MySettings.networkIsActive)
|
||||
return
|
||||
MySettings.networkIsActive = true;
|
||||
Network.sendNetworkToggled(true);
|
||||
MySettings.networkIsActive = true
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
if (!MySettings.networkIsActive)
|
||||
return
|
||||
MySettings.networkIsActive = false;
|
||||
Network.sendNetworkToggled(false);
|
||||
MySettings.networkIsActive = false
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ MyDialog {
|
||||
modal: true
|
||||
padding: 20
|
||||
onOpened: {
|
||||
Network.sendSettingsDialog();
|
||||
Network.trackEvent("settings_dialog")
|
||||
}
|
||||
|
||||
signal downloadClicked
|
||||
|
@ -123,8 +123,6 @@ model release that uses your data!")
|
||||
buttons: optInStatisticsRadio.children
|
||||
onClicked: {
|
||||
MySettings.networkUsageStatsActive = optInStatisticsRadio.checked
|
||||
if (!optInStatisticsRadio.checked)
|
||||
Network.sendOptOut();
|
||||
if (optInNetworkRadio.choiceMade && optInStatisticsRadio.choiceMade)
|
||||
startupDialog.close();
|
||||
}
|
||||
@ -140,7 +138,7 @@ model release that uses your data!")
|
||||
|
||||
RadioButton {
|
||||
id: optInStatisticsRadioYes
|
||||
checked: false
|
||||
checked: MySettings.networkUsageStatsActive
|
||||
text: qsTr("Yes")
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
Accessible.role: Accessible.RadioButton
|
||||
@ -182,6 +180,7 @@ model release that uses your data!")
|
||||
}
|
||||
RadioButton {
|
||||
id: optInStatisticsRadioNo
|
||||
checked: MySettings.isNetworkUsageStatsActiveSet() && !MySettings.networkUsageStatsActive
|
||||
text: qsTr("No")
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
Accessible.role: Accessible.RadioButton
|
||||
@ -254,7 +253,7 @@ model release that uses your data!")
|
||||
|
||||
RadioButton {
|
||||
id: optInNetworkRadioYes
|
||||
checked: false
|
||||
checked: MySettings.networkIsActive
|
||||
text: qsTr("Yes")
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
Accessible.role: Accessible.RadioButton
|
||||
@ -296,6 +295,7 @@ model release that uses your data!")
|
||||
}
|
||||
RadioButton {
|
||||
id: optInNetworkRadioNo
|
||||
checked: MySettings.isNetworkIsActiveSet() && !MySettings.networkIsActive
|
||||
text: qsTr("No")
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
Accessible.role: Accessible.RadioButton
|
||||
|
Loading…
Reference in New Issue
Block a user