chat: generate follow-up questions after response (#2634)

* user can configure the prompt and when they appear
* also make the name generation prompt configurable

Signed-off-by: Adam Treat <treat.adam@gmail.com>
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Co-authored-by: Jared Van Bortel <jared@nomic.ai>
This commit is contained in:
AT 2024-07-10 15:45:20 -04:00 committed by GitHub
parent ef4e362d92
commit 66bc04aa8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 621 additions and 138 deletions

View File

@ -210,11 +210,13 @@ qt_add_qml_module(chat
icons/network.svg
icons/nomic_logo.svg
icons/notes.svg
icons/plus.svg
icons/recycle.svg
icons/regenerate.svg
icons/search.svg
icons/send_message.svg
icons/settings.svg
icons/stack.svg
icons/stop_generating.svg
icons/thumbs_down.svg
icons/thumbs_up.svg

View File

@ -58,11 +58,13 @@ void Chat::connectLLM()
connect(m_llmodel, &ChatLLM::modelLoadingPercentageChanged, this, &Chat::handleModelLoadingPercentageChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::responseChanged, this, &Chat::handleResponseChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::promptProcessing, this, &Chat::promptProcessing, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::generatingQuestions, this, &Chat::generatingQuestions, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::responseStopped, this, &Chat::responseStopped, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::modelLoadingError, this, &Chat::handleModelLoadingError, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::modelLoadingWarning, this, &Chat::modelLoadingWarning, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::recalcChanged, this, &Chat::handleRecalculating, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::generatedNameChanged, this, &Chat::generatedNameChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::generatedQuestionFinished, this, &Chat::generatedQuestionFinished, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::reportSpeed, this, &Chat::handleTokenSpeedChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::loadedModelInfoChanged, this, &Chat::loadedModelInfoChanged, Qt::QueuedConnection);
connect(m_llmodel, &ChatLLM::databaseResultsChanged, this, &Chat::handleDatabaseResultsChanged, Qt::QueuedConnection);
@ -113,6 +115,8 @@ void Chat::resetResponseState()
if (m_responseInProgress && m_responseState == Chat::LocalDocsRetrieval)
return;
m_generatedQuestions = QList<QString>();
emit generatedQuestionsChanged();
m_tokenSpeed = QString();
emit tokenSpeedChanged();
m_responseInProgress = true;
@ -189,6 +193,12 @@ void Chat::promptProcessing()
emit responseStateChanged();
}
void Chat::generatingQuestions()
{
m_responseState = Chat::GeneratingQuestions;
emit responseStateChanged();
}
void Chat::responseStopped(qint64 promptResponseMs)
{
m_tokenSpeed = QString();
@ -304,6 +314,12 @@ void Chat::generatedNameChanged(const QString &name)
emit nameChanged();
}
void Chat::generatedQuestionFinished(const QString &question)
{
m_generatedQuestions << question;
emit generatedQuestionsChanged();
}
void Chat::handleRecalculating()
{
Network::globalInstance()->trackChatEvent("recalc_context", { {"length", m_chatModel->count()} });

View File

@ -39,6 +39,7 @@ class Chat : public QObject
Q_PROPERTY(LocalDocsCollectionsModel *collectionModel READ collectionModel NOTIFY collectionModelChanged)
// 0=no, 1=waiting, 2=working
Q_PROPERTY(int trySwitchContextInProgress READ trySwitchContextInProgress NOTIFY trySwitchContextInProgressChanged)
Q_PROPERTY(QList<QString> generatedQuestions READ generatedQuestions NOTIFY generatedQuestionsChanged)
QML_ELEMENT
QML_UNCREATABLE("Only creatable from c++!")
@ -48,6 +49,7 @@ public:
LocalDocsRetrieval,
LocalDocsProcessing,
PromptProcessing,
GeneratingQuestions,
ResponseGeneration
};
Q_ENUM(ResponseState)
@ -119,6 +121,8 @@ public:
int trySwitchContextInProgress() const { return m_trySwitchContextInProgress; }
QList<QString> generatedQuestions() const { return m_generatedQuestions; }
public Q_SLOTS:
void serverNewPromptResponsePair(const QString &prompt);
@ -153,13 +157,16 @@ Q_SIGNALS:
void collectionModelChanged();
void trySwitchContextInProgressChanged();
void loadedModelInfoChanged();
void generatedQuestionsChanged();
private Q_SLOTS:
void handleResponseChanged(const QString &response);
void handleModelLoadingPercentageChanged(float);
void promptProcessing();
void generatingQuestions();
void responseStopped(qint64 promptResponseMs);
void generatedNameChanged(const QString &name);
void generatedQuestionFinished(const QString &question);
void handleRecalculating();
void handleModelLoadingError(const QString &error);
void handleTokenSpeedChanged(const QString &tokenSpeed);
@ -179,6 +186,7 @@ private:
QString m_fallbackReason;
QString m_response;
QList<QString> m_collections;
QList<QString> m_generatedQuestions;
ChatModel *m_chatModel;
bool m_responseInProgress = false;
ResponseState m_responseState;

View File

@ -750,7 +750,7 @@ bool ChatLLM::promptInternal(const QList<QString> &collectionList, const QString
if (!databaseResults.isEmpty()) {
QStringList results;
for (const ResultInfo &info : databaseResults)
results << u"Collection: %1\nPath: %2\nSnippet: %3"_s.arg(info.collection, info.path, info.text);
results << u"Collection: %1\nPath: %2\nExcerpt: %3"_s.arg(info.collection, info.path, info.text);
// FIXME(jared): use a Jinja prompt template instead of hardcoded Alpaca-style localdocs template
docsContext = u"### Context:\n%1\n\n"_s.arg(results.join("\n\n"));
@ -797,7 +797,13 @@ bool ChatLLM::promptInternal(const QList<QString> &collectionList, const QString
m_response = trimmed;
emit responseChanged(QString::fromStdString(m_response));
}
SuggestionMode mode = MySettings::globalInstance()->suggestionMode();
if (mode == SuggestionMode::On || (!databaseResults.isEmpty() && mode == SuggestionMode::LocalDocsOnly))
generateQuestions(elapsed);
else
emit responseStopped(elapsed);
m_pristineLoadedState = false;
return true;
}
@ -875,13 +881,19 @@ void ChatLLM::generateName()
if (!isModelLoaded())
return;
const QString chatNamePrompt = MySettings::globalInstance()->modelChatNamePrompt(m_modelInfo);
if (chatNamePrompt.trimmed().isEmpty()) {
qWarning() << "ChatLLM: not generating chat name because prompt is empty";
return;
}
auto promptTemplate = MySettings::globalInstance()->modelPromptTemplate(m_modelInfo);
auto promptFunc = std::bind(&ChatLLM::handleNamePrompt, this, std::placeholders::_1);
auto responseFunc = std::bind(&ChatLLM::handleNameResponse, this, std::placeholders::_1, std::placeholders::_2);
auto recalcFunc = std::bind(&ChatLLM::handleNameRecalculate, this, std::placeholders::_1);
LLModel::PromptContext ctx = m_ctx;
m_llModelInfo.model->prompt("Describe the above conversation in seven words or less.",
promptTemplate.toStdString(), promptFunc, responseFunc, recalcFunc, ctx);
m_llModelInfo.model->prompt(chatNamePrompt.toStdString(), promptTemplate.toStdString(),
promptFunc, responseFunc, recalcFunc, ctx);
std::string trimmed = trim_whitespace(m_nameResponse);
if (trimmed != m_nameResponse) {
m_nameResponse = trimmed;
@ -901,7 +913,6 @@ bool ChatLLM::handleNamePrompt(int32_t token)
qDebug() << "name prompt" << m_llmThread.objectName() << token;
#endif
Q_UNUSED(token);
qt_noop();
return !m_stopGenerating;
}
@ -925,10 +936,84 @@ bool ChatLLM::handleNameRecalculate(bool isRecalc)
qDebug() << "name recalc" << m_llmThread.objectName() << isRecalc;
#endif
Q_UNUSED(isRecalc);
qt_noop();
return true;
}
bool ChatLLM::handleQuestionPrompt(int32_t token)
{
#if defined(DEBUG)
qDebug() << "question prompt" << m_llmThread.objectName() << token;
#endif
Q_UNUSED(token);
return !m_stopGenerating;
}
bool ChatLLM::handleQuestionResponse(int32_t token, const std::string &response)
{
#if defined(DEBUG)
qDebug() << "question response" << m_llmThread.objectName() << token << response;
#endif
Q_UNUSED(token);
// add token to buffer
m_questionResponse.append(response);
// match whole question sentences
static const QRegularExpression reQuestion(R"(\b(What|Where|How|Why|When|Who|Which|Whose|Whom)\b[^?]*\?)");
// extract all questions from response
int lastMatchEnd = -1;
for (const auto &match : reQuestion.globalMatch(m_questionResponse)) {
lastMatchEnd = match.capturedEnd();
emit generatedQuestionFinished(match.captured());
}
// remove processed input from buffer
if (lastMatchEnd != -1)
m_questionResponse.erase(m_questionResponse.cbegin(), m_questionResponse.cbegin() + lastMatchEnd);
return true;
}
bool ChatLLM::handleQuestionRecalculate(bool isRecalc)
{
#if defined(DEBUG)
qDebug() << "name recalc" << m_llmThread.objectName() << isRecalc;
#endif
Q_UNUSED(isRecalc);
return true;
}
void ChatLLM::generateQuestions(qint64 elapsed)
{
Q_ASSERT(isModelLoaded());
if (!isModelLoaded()) {
emit responseStopped(elapsed);
return;
}
const std::string suggestedFollowUpPrompt = MySettings::globalInstance()->modelSuggestedFollowUpPrompt(m_modelInfo).toStdString();
if (QString::fromStdString(suggestedFollowUpPrompt).trimmed().isEmpty()) {
emit responseStopped(elapsed);
return;
}
emit generatingQuestions();
m_questionResponse.clear();
auto promptTemplate = MySettings::globalInstance()->modelPromptTemplate(m_modelInfo);
auto promptFunc = std::bind(&ChatLLM::handleQuestionPrompt, this, std::placeholders::_1);
auto responseFunc = std::bind(&ChatLLM::handleQuestionResponse, this, std::placeholders::_1, std::placeholders::_2);
auto recalcFunc = std::bind(&ChatLLM::handleQuestionRecalculate, this, std::placeholders::_1);
LLModel::PromptContext ctx = m_ctx;
QElapsedTimer totalTime;
totalTime.start();
m_llModelInfo.model->prompt(suggestedFollowUpPrompt,
promptTemplate.toStdString(), promptFunc, responseFunc, recalcFunc, ctx);
elapsed += totalTime.elapsed();
emit responseStopped(elapsed);
}
bool ChatLLM::handleSystemPrompt(int32_t token)
{
#if defined(DEBUG)

View File

@ -160,6 +160,7 @@ public Q_SLOTS:
void unloadModel();
void reloadModel();
void generateName();
void generateQuestions(qint64 elapsed);
void handleChatIdChanged(const QString &id);
void handleShouldBeLoadedChanged();
void handleThreadStarted();
@ -176,8 +177,10 @@ Q_SIGNALS:
void modelLoadingWarning(const QString &warning);
void responseChanged(const QString &response);
void promptProcessing();
void generatingQuestions();
void responseStopped(qint64 promptResponseMs);
void generatedNameChanged(const QString &name);
void generatedQuestionFinished(const QString &generatedQuestion);
void stateChanged();
void threadStarted();
void shouldBeLoadedChanged();
@ -206,6 +209,9 @@ protected:
bool handleRestoreStateFromTextPrompt(int32_t token);
bool handleRestoreStateFromTextResponse(int32_t token, const std::string &response);
bool handleRestoreStateFromTextRecalculate(bool isRecalc);
bool handleQuestionPrompt(int32_t token);
bool handleQuestionResponse(int32_t token, const std::string &response);
bool handleQuestionRecalculate(bool isRecalc);
void saveState();
void restoreState();
@ -219,6 +225,7 @@ private:
std::string m_response;
std::string m_nameResponse;
QString m_questionResponse;
LLModelInfo m_llModelInfo;
LLModelType m_llModelType;
ModelInfo m_modelInfo;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path></svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M230.91,124A8,8,0,0,1,228,134.91l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,36,121.09l92,53.65,92-53.65A8,8,0,0,1,230.91,124ZM24,80a8,8,0,0,1,4-6.91l96-56a8,8,0,0,1,8.06,0l96,56a8,8,0,0,1,0,13.82l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,24,80Zm23.88,0L128,126.74,208.12,80,128,33.26ZM232,192H216V176a8,8,0,0,0-16,0v16H184a8,8,0,0,0,0,16h16v16a8,8,0,0,0,16,0V208h16a8,8,0,0,0,0-16Zm-92,23.76-12,7L36,169.09A8,8,0,0,0,28,182.91l96,56a8,8,0,0,0,8.06,0l16-9.33A8,8,0,1,0,140,215.76Z"></path></svg>

After

Width:  |  Height:  |  Size: 600 B

View File

@ -334,6 +334,28 @@ void ModelInfo::setSystemPrompt(const QString &p)
m_systemPrompt = p;
}
QString ModelInfo::chatNamePrompt() const
{
return MySettings::globalInstance()->modelChatNamePrompt(*this);
}
void ModelInfo::setChatNamePrompt(const QString &p)
{
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelChatNamePrompt(*this, p, true /*force*/);
m_chatNamePrompt = p;
}
QString ModelInfo::suggestedFollowUpPrompt() const
{
return MySettings::globalInstance()->modelSuggestedFollowUpPrompt(*this);
}
void ModelInfo::setSuggestedFollowUpPrompt(const QString &p)
{
if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSuggestedFollowUpPrompt(*this, p, true /*force*/);
m_suggestedFollowUpPrompt = p;
}
bool ModelInfo::shouldSaveMetadata() const
{
return installed && (isClone() || isDiscovered() || description() == "" /*indicates sideloaded*/);
@ -364,6 +386,8 @@ QVariantMap ModelInfo::getFields() const
{ "repeatPenaltyTokens", m_repeatPenaltyTokens },
{ "promptTemplate", m_promptTemplate },
{ "systemPrompt", m_systemPrompt },
{ "chatNamePrompt", m_chatNamePrompt },
{ "suggestedFollowUpPrompt", m_suggestedFollowUpPrompt },
};
}
@ -758,6 +782,10 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
return info->promptTemplate();
case SystemPromptRole:
return info->systemPrompt();
case ChatNamePromptRole:
return info->chatNamePrompt();
case SuggestedFollowUpPromptRole:
return info->suggestedFollowUpPrompt();
case LikesRole:
return info->likes();
case DownloadsRole:
@ -928,6 +956,10 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
info->setPromptTemplate(value.toString()); break;
case SystemPromptRole:
info->setSystemPrompt(value.toString()); break;
case ChatNamePromptRole:
info->setChatNamePrompt(value.toString()); break;
case SuggestedFollowUpPromptRole:
info->setSuggestedFollowUpPrompt(value.toString()); break;
case LikesRole:
{
if (info->likes() != value.toInt()) {
@ -1077,6 +1109,8 @@ QString ModelList::clone(const ModelInfo &model)
{ ModelList::RepeatPenaltyTokensRole, model.repeatPenaltyTokens() },
{ ModelList::PromptTemplateRole, model.promptTemplate() },
{ ModelList::SystemPromptRole, model.systemPrompt() },
{ ModelList::ChatNamePromptRole, model.chatNamePrompt() },
{ ModelList::SuggestedFollowUpPromptRole, model.suggestedFollowUpPrompt() },
};
updateData(id, data);
return id;
@ -1772,6 +1806,14 @@ void ModelList::updateModelsFromSettings()
const QString systemPrompt = settings.value(g + "/systemPrompt").toString();
data.append({ ModelList::SystemPromptRole, systemPrompt });
}
if (settings.contains(g + "/chatNamePrompt")) {
const QString chatNamePrompt = settings.value(g + "/chatNamePrompt").toString();
data.append({ ModelList::ChatNamePromptRole, chatNamePrompt });
}
if (settings.contains(g + "/suggestedFollowUpPrompt")) {
const QString suggestedFollowUpPrompt = settings.value(g + "/suggestedFollowUpPrompt").toString();
data.append({ ModelList::SuggestedFollowUpPromptRole, suggestedFollowUpPrompt });
}
updateData(id, data);
}
}

View File

@ -68,6 +68,8 @@ struct ModelInfo {
Q_PROPERTY(int repeatPenaltyTokens READ repeatPenaltyTokens WRITE setRepeatPenaltyTokens)
Q_PROPERTY(QString promptTemplate READ promptTemplate WRITE setPromptTemplate)
Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt)
Q_PROPERTY(QString chatNamePrompt READ chatNamePrompt WRITE setChatNamePrompt)
Q_PROPERTY(QString suggestedFollowUpPrompt READ suggestedFollowUpPrompt WRITE setSuggestedFollowUpPrompt)
Q_PROPERTY(int likes READ likes WRITE setLikes)
Q_PROPERTY(int downloads READ downloads WRITE setDownloads)
Q_PROPERTY(QDateTime recency READ recency WRITE setRecency)
@ -167,6 +169,10 @@ public:
void setPromptTemplate(const QString &t);
QString systemPrompt() const;
void setSystemPrompt(const QString &p);
QString chatNamePrompt() const;
void setChatNamePrompt(const QString &p);
QString suggestedFollowUpPrompt() const;
void setSuggestedFollowUpPrompt(const QString &p);
bool shouldSaveMetadata() const;
@ -199,6 +205,8 @@ private:
int m_repeatPenaltyTokens = 64;
QString m_promptTemplate = "### Human:\n%1\n\n### Assistant:\n";
QString m_systemPrompt = "### System:\nYou are an AI assistant who gives a quality response to whatever humans ask of you.\n\n";
QString m_chatNamePrompt = "Describe the above conversation in seven words or less.";
QString m_suggestedFollowUpPrompt = "Suggest three very short factual follow-up questions that have not been answered yet or cannot be found inspired by the previous conversation and excerpts.";
friend class MySettings;
};
Q_DECLARE_METATYPE(ModelInfo)
@ -317,6 +325,8 @@ public:
RepeatPenaltyTokensRole,
PromptTemplateRole,
SystemPromptRole,
ChatNamePromptRole,
SuggestedFollowUpPromptRole,
MinPRole,
LikesRole,
DownloadsRole,
@ -368,6 +378,8 @@ public:
roles[RepeatPenaltyTokensRole] = "repeatPenaltyTokens";
roles[PromptTemplateRole] = "promptTemplate";
roles[SystemPromptRole] = "systemPrompt";
roles[ChatNamePromptRole] = "chatNamePrompt";
roles[SuggestedFollowUpPromptRole] = "suggestedFollowUpPrompt";
roles[LikesRole] = "likes";
roles[DownloadsRole] = "downloads";
roles[RecencyRole] = "recency";

View File

@ -41,6 +41,7 @@ static const QVariantMap basicDefaults {
{ "saveChatsContext", false },
{ "serverChat", false },
{ "userDefaultModel", "Application default" },
{ "suggestionMode", QVariant::fromValue(SuggestionMode::LocalDocsOnly) },
{ "localdocs/chunkSize", 512 },
{ "localdocs/retrievalSize", 3 },
{ "localdocs/showReferences", true },
@ -136,6 +137,8 @@ void MySettings::restoreModelDefaults(const ModelInfo &info)
setModelRepeatPenaltyTokens(info, info.m_repeatPenaltyTokens);
setModelPromptTemplate(info, info.m_promptTemplate);
setModelSystemPrompt(info, info.m_systemPrompt);
setModelChatNamePrompt(info, info.m_chatNamePrompt);
setModelSuggestedFollowUpPrompt(info, info.m_suggestedFollowUpPrompt);
}
void MySettings::restoreApplicationDefaults()
@ -150,6 +153,7 @@ void MySettings::restoreApplicationDefaults()
setModelPath(defaultLocalModelsPath());
setUserDefaultModel(basicDefaults.value("userDefaultModel").toString());
setForceMetal(defaults::forceMetal);
setSuggestionMode(basicDefaults.value("suggestionMode").value<SuggestionMode>());
}
void MySettings::restoreLocalDocsDefaults()
@ -234,6 +238,8 @@ double MySettings::modelRepeatPenalty (const ModelInfo &info) const { re
int MySettings::modelRepeatPenaltyTokens (const ModelInfo &info) const { return getModelSetting("repeatPenaltyTokens", info).toInt(); }
QString MySettings::modelPromptTemplate (const ModelInfo &info) const { return getModelSetting("promptTemplate", info).toString(); }
QString MySettings::modelSystemPrompt (const ModelInfo &info) const { return getModelSetting("systemPrompt", info).toString(); }
QString MySettings::modelChatNamePrompt (const ModelInfo &info) const { return getModelSetting("chatNamePrompt", info).toString(); }
QString MySettings::modelSuggestedFollowUpPrompt(const ModelInfo &info) const { return getModelSetting("suggestedFollowUpPrompt", info).toString(); }
void MySettings::setModelFilename(const ModelInfo &info, const QString &value, bool force)
{
@ -345,6 +351,16 @@ void MySettings::setModelSystemPrompt(const ModelInfo &info, const QString &valu
setModelSetting("systemPrompt", info, value, force, true);
}
void MySettings::setModelChatNamePrompt(const ModelInfo &info, const QString &value, bool force)
{
setModelSetting("chatNamePrompt", info, value, force, true);
}
void MySettings::setModelSuggestedFollowUpPrompt(const ModelInfo &info, const QString &value, bool force)
{
setModelSetting("suggestedFollowUpPrompt", info, value, force, true);
}
int MySettings::threadCount() const
{
int c = m_settings.value("threadCount", defaults::threadCount).toInt();
@ -383,6 +399,7 @@ bool MySettings::localDocsUseRemoteEmbed() const { return getBasicSetting
QString MySettings::localDocsNomicAPIKey() const { return getBasicSetting("localdocs/nomicAPIKey" ).toString(); }
QString MySettings::localDocsEmbedDevice() const { return getBasicSetting("localdocs/embedDevice" ).toString(); }
QString MySettings::networkAttribution() const { return getBasicSetting("network/attribution" ).toString(); }
SuggestionMode MySettings::suggestionMode() const { return getBasicSetting("suggestionMode").value<SuggestionMode>(); };
void MySettings::setSaveChatsContext(bool value) { setBasicSetting("saveChatsContext", value); }
void MySettings::setServerChat(bool value) { setBasicSetting("serverChat", value); }
@ -399,6 +416,7 @@ void MySettings::setLocalDocsUseRemoteEmbed(bool value) { setBasic
void MySettings::setLocalDocsNomicAPIKey(const QString &value) { setBasicSetting("localdocs/nomicAPIKey", value, "localDocsNomicAPIKey"); }
void MySettings::setLocalDocsEmbedDevice(const QString &value) { setBasicSetting("localdocs/embedDevice", value, "localDocsEmbedDevice"); }
void MySettings::setNetworkAttribution(const QString &value) { setBasicSetting("network/attribution", value, "networkAttribution"); }
void MySettings::setSuggestionMode(SuggestionMode value) { setBasicSetting("suggestionMode", int(value)); }
QString MySettings::modelPath()
{

View File

@ -13,6 +13,18 @@
#include <cstdint>
#include <optional>
namespace MySettingsEnums {
Q_NAMESPACE
enum class SuggestionMode {
LocalDocsOnly = 0,
On = 1,
Off = 2,
};
Q_ENUM_NS(SuggestionMode)
}
using namespace MySettingsEnums;
class MySettings : public QObject
{
Q_OBJECT
@ -39,6 +51,7 @@ class MySettings : public QObject
Q_PROPERTY(QStringList deviceList MEMBER m_deviceList CONSTANT)
Q_PROPERTY(QStringList embeddingsDeviceList MEMBER m_embeddingsDeviceList CONSTANT)
Q_PROPERTY(int networkPort READ networkPort WRITE setNetworkPort NOTIFY networkPortChanged)
Q_PROPERTY(SuggestionMode suggestionMode READ suggestionMode WRITE setSuggestionMode NOTIFY suggestionModeChanged)
public:
static MySettings *globalInstance();
@ -98,6 +111,10 @@ public:
Q_INVOKABLE void setModelContextLength(const ModelInfo &info, int value, bool force = false);
int modelGpuLayers(const ModelInfo &info) const;
Q_INVOKABLE void setModelGpuLayers(const ModelInfo &info, int value, bool force = false);
QString modelChatNamePrompt(const ModelInfo &info) const;
Q_INVOKABLE void setModelChatNamePrompt(const ModelInfo &info, const QString &value, bool force = false);
QString modelSuggestedFollowUpPrompt(const ModelInfo &info) const;
Q_INVOKABLE void setModelSuggestedFollowUpPrompt(const ModelInfo &info, const QString &value, bool force = false);
// Application settings
int threadCount() const;
@ -122,6 +139,8 @@ public:
void setContextLength(int32_t value);
int32_t gpuLayers() const;
void setGpuLayers(int32_t value);
SuggestionMode suggestionMode() const;
void setSuggestionMode(SuggestionMode mode);
// Release/Download settings
QString lastVersionStarted() const;
@ -171,6 +190,8 @@ Q_SIGNALS:
void repeatPenaltyTokensChanged(const ModelInfo &info);
void promptTemplateChanged(const ModelInfo &info);
void systemPromptChanged(const ModelInfo &info);
void chatNamePromptChanged(const ModelInfo &info);
void suggestedFollowUpPromptChanged(const ModelInfo &info);
void threadCountChanged();
void saveChatsContextChanged();
void serverChatChanged();
@ -193,6 +214,7 @@ Q_SIGNALS:
void networkUsageStatsActiveChanged();
void attemptModelLoadChanged();
void deviceChanged();
void suggestionModeChanged();
private:
QSettings m_settings;

View File

@ -227,16 +227,40 @@ MySettingsTab {
MySettings.userDefaultModel = comboBox.currentText
}
}
MySettingsLabel {
id: suggestionModeLabel
text: qsTr("Suggestion Mode")
helpText: qsTr("Generate suggested follow-up questions at the end of responses.")
Layout.row: 6
Layout.column: 0
}
MyComboBox {
id: suggestionModeBox
Layout.row: 6
Layout.column: 2
Layout.minimumWidth: 400
Layout.maximumWidth: 400
Layout.alignment: Qt.AlignRight
model: [ qsTr("When chatting with LocalDocs"), qsTr("Whenever possible"), qsTr("Never") ]
Accessible.name: suggestionModeLabel.text
Accessible.description: suggestionModeLabel.helpText
onActivated: {
MySettings.suggestionMode = suggestionModeBox.currentIndex;
}
Component.onCompleted: {
suggestionModeBox.currentIndex = MySettings.suggestionMode;
}
}
MySettingsLabel {
id: modelPathLabel
text: qsTr("Download Path")
helpText: qsTr("Where to store local models and the LocalDocs database.")
Layout.row: 6
Layout.row: 7
Layout.column: 0
}
RowLayout {
Layout.row: 6
Layout.row: 7
Layout.column: 2
Layout.alignment: Qt.AlignRight
Layout.minimumWidth: 400
@ -273,12 +297,12 @@ MySettingsTab {
id: dataLakeLabel
text: qsTr("Enable Datalake")
helpText: qsTr("Send chats and feedback to the GPT4All Open-Source Datalake.")
Layout.row: 7
Layout.row: 8
Layout.column: 0
}
MyCheckBox {
id: dataLakeBox
Layout.row: 7
Layout.row: 8
Layout.column: 2
Layout.alignment: Qt.AlignRight
Component.onCompleted: { dataLakeBox.checked = MySettings.networkIsActive; }
@ -296,7 +320,7 @@ MySettingsTab {
}
ColumnLayout {
Layout.row: 8
Layout.row: 9
Layout.column: 0
Layout.columnSpan: 3
Layout.fillWidth: true
@ -319,7 +343,7 @@ MySettingsTab {
id: nThreadsLabel
text: qsTr("CPU Threads")
helpText: qsTr("The number of CPU threads used for inference and embedding.")
Layout.row: 9
Layout.row: 10
Layout.column: 0
}
MyTextField {
@ -327,7 +351,7 @@ MySettingsTab {
color: theme.textColor
font.pixelSize: theme.fontSizeLarge
Layout.alignment: Qt.AlignRight
Layout.row: 9
Layout.row: 10
Layout.column: 2
Layout.minimumWidth: 200
Layout.maximumWidth: 200
@ -351,12 +375,12 @@ MySettingsTab {
id: saveChatsContextLabel
text: qsTr("Save Chat Context")
helpText: qsTr("Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat.")
Layout.row: 10
Layout.row: 11
Layout.column: 0
}
MyCheckBox {
id: saveChatsContextBox
Layout.row: 10
Layout.row: 11
Layout.column: 2
Layout.alignment: Qt.AlignRight
checked: MySettings.saveChatsContext
@ -368,12 +392,12 @@ MySettingsTab {
id: serverChatLabel
text: qsTr("Enable Local Server")
helpText: qsTr("Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage.")
Layout.row: 11
Layout.row: 12
Layout.column: 0
}
MyCheckBox {
id: serverChatBox
Layout.row: 11
Layout.row: 12
Layout.column: 2
Layout.alignment: Qt.AlignRight
checked: MySettings.serverChat
@ -385,7 +409,7 @@ MySettingsTab {
id: serverPortLabel
text: qsTr("API Server Port")
helpText: qsTr("The port to use for the local server. Requires restart.")
Layout.row: 12
Layout.row: 13
Layout.column: 0
}
MyTextField {
@ -393,7 +417,7 @@ MySettingsTab {
text: MySettings.networkPort
color: theme.textColor
font.pixelSize: theme.fontSizeLarge
Layout.row: 12
Layout.row: 13
Layout.column: 2
Layout.minimumWidth: 200
Layout.maximumWidth: 200

View File

@ -797,7 +797,7 @@ Rectangle {
delegate: GridLayout {
width: listView.contentItem.width - 15
rows: 3
rows: 5
columns: 2
Item {
@ -850,6 +850,8 @@ Rectangle {
font.pixelSize: theme.fontSizeLarger
font.bold: true
color: theme.conversationHeader
enabled: false
focus: false
readOnly: true
}
Text {
@ -872,6 +874,7 @@ Rectangle {
case Chat.LocalDocsProcessing: return qsTr("searching localdocs: ") + currentChat.collectionList.join(", ") + " ...";
case Chat.PromptProcessing: return qsTr("processing ...")
case Chat.ResponseGeneration: return qsTr("generating response ...");
case Chat.GeneratingQuestions: return qsTr("generating questions ...");
default: return ""; // handle unexpected values
}
}
@ -1094,7 +1097,16 @@ Rectangle {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences && (!currentResponse || !currentChat.responseInProgress)
visible: {
if (consolidatedSources.length === 0)
return false
if (!MySettings.localDocsShowReferences)
return false
if (currentResponse && currentChat.responseInProgress
&& currentChat.responseState !== Chat.GeneratingQuestions )
return false
return true
}
MyButton {
backgroundColor: theme.sourcesBackground
@ -1171,7 +1183,16 @@ Rectangle {
Layout.row: 3
Layout.column: 1
Layout.topMargin: 5
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences && (!currentResponse || !currentChat.responseInProgress)
visible: {
if (consolidatedSources.length === 0)
return false
if (!MySettings.localDocsShowReferences)
return false
if (currentResponse && currentChat.responseInProgress
&& currentChat.responseState !== Chat.GeneratingQuestions )
return false
return true
}
clip: true
Layout.fillWidth: true
Layout.preferredHeight: 0
@ -1310,45 +1331,250 @@ Rectangle {
}
}
}
function shouldShowSuggestions() {
if (!currentResponse)
return false;
if (MySettings.suggestionMode === 2) // Off
return false;
if (MySettings.suggestionMode === 0 && consolidatedSources.length === 0) // LocalDocs only
return false;
return currentChat.responseState === Chat.GeneratingQuestions || currentChat.generatedQuestions.length !== 0;
}
property bool shouldAutoScroll: true
property bool isAutoScrolling: false
Item {
visible: shouldShowSuggestions()
Layout.row: 4
Layout.column: 0
Layout.topMargin: 20
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredWidth: 28
Layout.preferredHeight: 28
Image {
id: stack
sourceSize: Qt.size(28, 28)
fillMode: Image.PreserveAspectFit
mipmap: true
visible: false
source: "qrc:/gpt4all/icons/stack.svg"
}
Connections {
target: currentChat
function onResponseChanged() {
listView.scrollToEnd()
ColorOverlay {
anchors.fill: stack
source: stack
color: theme.conversationHeader
}
}
Item {
visible: shouldShowSuggestions()
Layout.row: 4
Layout.column: 1
Layout.topMargin: 20
Layout.fillWidth: true
Layout.preferredHeight: 38
RowLayout {
spacing: 5
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
TextArea {
text: qsTr("Suggested follow-ups")
padding: 0
font.pixelSize: theme.fontSizeLarger
font.bold: true
color: theme.conversationHeader
enabled: false
focus: false
readOnly: true
}
}
}
ColumnLayout {
visible: shouldShowSuggestions()
Layout.row: 5
Layout.column: 1
Layout.fillWidth: true
Layout.minimumHeight: 1
spacing: 10
Repeater {
model: currentChat.generatedQuestions
TextArea {
id: followUpText
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
rightPadding: 40
topPadding: 10
leftPadding: 20
bottomPadding: 10
text: modelData
focus: false
readOnly: true
wrapMode: Text.WordWrap
hoverEnabled: !currentChat.responseInProgress
color: theme.textColor
font.pixelSize: theme.fontSizeLarge
background: Rectangle {
color: hovered ? theme.sourcesBackgroundHovered : theme.sourcesBackground
radius: 10
}
MouseArea {
id: maFollowUp
anchors.fill: parent
enabled: !currentChat.responseInProgress
onClicked: function() {
var chat = window.currentChat
var followup = modelData
chat.stopGenerating()
chat.newPromptResponsePair(followup);
chat.prompt(followup,
MySettings.promptTemplate,
MySettings.maxLength,
MySettings.topK,
MySettings.topP,
MySettings.minP,
MySettings.temperature,
MySettings.promptBatchSize,
MySettings.repeatPenalty,
MySettings.repeatPenaltyTokens)
}
}
Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
visible: !currentChat.responseInProgress
Image {
id: plusImage
anchors.verticalCenter: parent.verticalCenter
sourceSize.width: 20
sourceSize.height: 20
mipmap: true
visible: false
source: "qrc:/gpt4all/icons/plus.svg"
}
ColorOverlay {
anchors.fill: plusImage
source: plusImage
color: theme.styledTextColor
}
}
}
}
Rectangle {
Layout.fillWidth: true
color: "transparent"
radius: 10
Layout.preferredHeight: currentChat.responseInProgress ? 40 : 0
clip: true
ColumnLayout {
id: followUpLayout
anchors.fill: parent
Rectangle {
id: myRect1
Layout.preferredWidth: 0
Layout.minimumWidth: 0
Layout.maximumWidth: parent.width
height: 12
color: theme.sourcesBackgroundHovered
}
Rectangle {
id: myRect2
Layout.preferredWidth: 0
Layout.minimumWidth: 0
Layout.maximumWidth: parent.width
height: 12
color: theme.sourcesBackgroundHovered
}
SequentialAnimation {
id: followUpProgressAnimation
ParallelAnimation {
PropertyAnimation {
target: myRect1
property: "Layout.preferredWidth"
from: 0
to: followUpLayout.width
duration: 1000
}
PropertyAnimation {
target: myRect2
property: "Layout.preferredWidth"
from: 0
to: followUpLayout.width / 2
duration: 1000
}
}
SequentialAnimation {
loops: Animation.Infinite
ParallelAnimation {
PropertyAnimation {
target: myRect1
property: "opacity"
from: 1
to: 0.2
duration: 1500
}
PropertyAnimation {
target: myRect2
property: "opacity"
from: 1
to: 0.2
duration: 1500
}
}
ParallelAnimation {
PropertyAnimation {
target: myRect1
property: "opacity"
from: 0.2
to: 1
duration: 1500
}
PropertyAnimation {
target: myRect2
property: "opacity"
from: 0.2
to: 1
duration: 1500
}
}
}
}
onVisibleChanged: {
if (visible)
followUpProgressAnimation.start();
}
}
Behavior on Layout.preferredHeight {
NumberAnimation {
duration: 300
easing.type: Easing.InOutQuad
}
}
}
}
}
function scrollToEnd() {
if (listView.shouldAutoScroll) {
listView.isAutoScrolling = true
listView.positionViewAtEnd()
listView.isAutoScrolling = false
}
}
onContentYChanged: {
if (!isAutoScrolling)
shouldAutoScroll = atYEnd
}
Component.onCompleted: {
shouldAutoScroll = true
positionViewAtEnd()
}
footer: Item {
id: bottomPadding
width: parent.width
height: 0
onContentHeightChanged: {
if (atYEnd)
scrollToEnd()
}
}
}
}
}
Rectangle {

View File

@ -250,45 +250,64 @@ MySettingsTab {
}
}
MySettingsLabel {
id: chatNamePromptLabel
text: qsTr("Chat Name Prompt")
helpText: qsTr("Prompt used to automatically generate chat names.")
Layout.row: 11
Layout.column: 0
Layout.topMargin: 15
}
Rectangle {
id: optionalImageRect
visible: false // FIXME: for later
Layout.row: 2
Layout.column: 1
Layout.rowSpan: 5
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.maximumWidth: height
Layout.topMargin: 35
Layout.bottomMargin: 35
Layout.leftMargin: 35
width: 3000
radius: 10
id: chatNamePrompt
Layout.row: 12
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.minimumHeight: Math.max(100, chatNamePromptTextArea.contentHeight + 20)
color: "transparent"
Item {
anchors.centerIn: parent
height: childrenRect.height
Image {
id: img
anchors.horizontalCenter: parent.horizontalCenter
width: 100
height: 100
source: "qrc:/gpt4all/icons/image.svg"
clip: true
MyTextArea {
id: chatNamePromptTextArea
anchors.fill: parent
text: root.currentModelInfo.chatNamePrompt
Accessible.role: Accessible.EditableText
Accessible.name: chatNamePromptLabel.text
Accessible.description: chatNamePromptLabel.text
}
Text {
text: qsTr("Add\noptional image")
font.pixelSize: theme.fontSizeLarge
anchors.top: img.bottom
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: TextArea.Wrap
horizontalAlignment: Qt.AlignHCenter
color: theme.mutedTextColor
}
MySettingsLabel {
id: suggestedFollowUpPromptLabel
text: qsTr("Suggested FollowUp Prompt")
helpText: qsTr("Prompt used to generate suggested follow-up questions.")
Layout.row: 13
Layout.column: 0
Layout.topMargin: 15
}
Rectangle {
id: suggestedFollowUpPrompt
Layout.row: 14
Layout.column: 0
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.minimumHeight: Math.max(100, suggestedFollowUpPromptTextArea.contentHeight + 20)
color: "transparent"
clip: true
MyTextArea {
id: suggestedFollowUpPromptTextArea
anchors.fill: parent
text: root.currentModelInfo.suggestedFollowUpPrompt
Accessible.role: Accessible.EditableText
Accessible.name: suggestedFollowUpPromptLabel.text
Accessible.description: suggestedFollowUpPromptLabel.text
}
}
GridLayout {
Layout.row: 11
Layout.row: 15
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15
@ -784,7 +803,7 @@ MySettingsTab {
}
Rectangle {
Layout.row: 12
Layout.row: 16
Layout.column: 0
Layout.columnSpan: 2
Layout.topMargin: 15