From 78f45b94d0670d74ab895fb5dffacedba1e7c4a2 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Mon, 12 Feb 2024 08:30:16 +0100 Subject: [PATCH] Adding ability to configure OpenAI API key from UI and store it to MF config #1514 --- app/src/qt/dialogs/configuration_dialog.cpp | 44 ++++++++++++++++--- app/src/qt/dialogs/configuration_dialog.h | 3 ++ lib/src/config/configuration.cpp | 27 +++++++++--- lib/src/config/configuration.h | 16 ++++--- .../markdown_configuration_representation.cpp | 6 +++ 5 files changed, 79 insertions(+), 17 deletions(-) diff --git a/app/src/qt/dialogs/configuration_dialog.cpp b/app/src/qt/dialogs/configuration_dialog.cpp index 8ffba9c7..0db15217 100644 --- a/app/src/qt/dialogs/configuration_dialog.cpp +++ b/app/src/qt/dialogs/configuration_dialog.cpp @@ -692,18 +692,32 @@ ConfigurationDialog::WingmanTab::WingmanTab(QWidget* parent) llmHelpLabel = new QLabel( tr( - "To configure OpenAI LLM provider, " - "generate OpenAI API key " - "and set" - "
" - "%1 shell environment variable with the key." + "Configure OpenAI LLM provider:\n" + "" ).arg(ENV_VAR_OPENAI_API_KEY)); + llmHelpLabel->setVisible(!config.canWingmanOpenAi()); + openAiApiKeyEdit = new QLineEdit(this); + openAiApiKeyEdit->setVisible(!config.canWingmanOpenAi()); + clearOpenAiApiKeyButton = new QPushButton(tr("Clear OpenAI API Key"), this); + if(config.getWingmanOpenAiApiKey().size() == 0) { + clearOpenAiApiKeyButton->setVisible(false); + } // assembly QVBoxLayout* nLayout = new QVBoxLayout{this}; nLayout->addWidget(llmProvidersLabel); nLayout->addWidget(llmProvidersCombo); nLayout->addWidget(llmHelpLabel); + nLayout->addWidget(openAiApiKeyEdit); + nLayout->addWidget(clearOpenAiApiKeyButton); QGroupBox* nGroup = new QGroupBox{tr("Large language model (LLM) providers"), this}; nGroup->setLayout(nLayout); @@ -711,6 +725,24 @@ ConfigurationDialog::WingmanTab::WingmanTab(QWidget* parent) boxesLayout->addWidget(nGroup); boxesLayout->addStretch(); setLayout(boxesLayout); + + QObject::connect( + clearOpenAiApiKeyButton, SIGNAL(clicked()), + this, SLOT(clearOpenAiApiKeySlot())); + +} + +void ConfigurationDialog::WingmanTab::clearOpenAiApiKeySlot() +{ + openAiApiKeyEdit->clear(); + QMessageBox::information( + this, + tr("OpenAI API Key Cleared"), + tr( + "API key has been cleared from the configuration. " + "Please close the configuration dialog with the OK button " + "and restart MindForger to apply this change.") + ); } void ConfigurationDialog::WingmanTab::handleComboBoxChanged(int index) { @@ -749,6 +781,7 @@ void ConfigurationDialog::WingmanTab::refresh() llmProvidersCombo->addItem( QString::fromStdString(openAiComboLabel), WingmanLlmProviders::WINGMAN_PROVIDER_OPENAI); } + openAiApiKeyEdit->setText(QString::fromStdString(config.getWingmanOpenAiApiKey())); // set the last selected provider llmProvidersCombo->setCurrentIndex( llmProvidersCombo->findData(config.getWingmanLlmProvider())); @@ -761,6 +794,7 @@ void ConfigurationDialog::WingmanTab::save() WingmanLlmProviders llmProvider = static_cast( llmProvidersCombo->itemData(llmProvidersCombo->currentIndex()).toInt()); config.setWingmanLlmProvider(llmProvider); + config.setWingmanOpenAiApiKey(openAiApiKeyEdit->text().toStdString()); } /* diff --git a/app/src/qt/dialogs/configuration_dialog.h b/app/src/qt/dialogs/configuration_dialog.h index 02dc027f..203d27d5 100644 --- a/app/src/qt/dialogs/configuration_dialog.h +++ b/app/src/qt/dialogs/configuration_dialog.h @@ -87,6 +87,8 @@ class ConfigurationDialog::WingmanTab : public QWidget QComboBox* llmProvidersCombo; QLabel* llmHelpLabel; + QLineEdit* openAiApiKeyEdit; + QPushButton* clearOpenAiApiKeyButton; public: explicit WingmanTab(QWidget* parent); @@ -98,6 +100,7 @@ class ConfigurationDialog::WingmanTab : public QWidget private slots: void handleComboBoxChanged(int index); + void clearOpenAiApiKeySlot(); }; /** diff --git a/lib/src/config/configuration.cpp b/lib/src/config/configuration.cpp index ea622348..b7b167e6 100644 --- a/lib/src/config/configuration.cpp +++ b/lib/src/config/configuration.cpp @@ -52,6 +52,7 @@ Configuration::Configuration() autolinkingCaseInsensitive{}, wingmanProvider{DEFAULT_WINGMAN_LLM_PROVIDER}, wingmanApiKey{}, + wingmanOpenAiApiKey{}, wingmanLlmModel{DEFAULT_WINGMAN_LLM_MODEL_OPENAI}, md2HtmlOptions{}, distributorSleepInterval{DEFAULT_DISTRIBUTOR_SLEEP_INTERVAL}, @@ -150,6 +151,7 @@ void Configuration::clear() autolinkingCaseInsensitive = DEFAULT_AUTOLINKING_CASE_INSENSITIVE; wingmanProvider = DEFAULT_WINGMAN_LLM_PROVIDER; wingmanApiKey.clear(); + wingmanOpenAiApiKey.clear(); wingmanLlmModel.clear(); timeScopeAsString.assign(DEFAULT_TIME_SCOPE); tagsScope.clear(); @@ -391,7 +393,14 @@ const char* Configuration::getEditorFromEnv() bool Configuration::canWingmanOpenAi() { - return std::getenv(ENV_VAR_OPENAI_API_KEY) != nullptr?true:false; + if ( + this->wingmanOpenAiApiKey.size() > 0 + || std::getenv(ENV_VAR_OPENAI_API_KEY) != nullptr + ) { + return true; + } + + return false; } void Configuration::setWingmanLlmProvider(WingmanLlmProviders provider) @@ -429,18 +438,22 @@ bool Configuration::initWingmanOpenAi() { if(canWingmanOpenAi()) { MF_DEBUG( " Wingman OpenAI API key found in the shell environment variable " - "MINDFORGER_OPENAI_API_KEY" << endl); - const char* apiKeyEnv = std::getenv(ENV_VAR_OPENAI_API_KEY); - MF_DEBUG(" Wingman API key loaded from the env: " << apiKeyEnv << endl); - wingmanApiKey = apiKeyEnv; + "MINDFORGER_OPENAI_API_KEY or set in MF config" << endl); + if(wingmanOpenAiApiKey.size() > 0) { + wingmanApiKey = wingmanOpenAiApiKey; + } else { + const char* apiKeyEnv = std::getenv(ENV_VAR_OPENAI_API_KEY); + MF_DEBUG(" Wingman API key loaded from the env: " << apiKeyEnv << endl); + wingmanApiKey = apiKeyEnv; + } wingmanLlmModel = DEFAULT_WINGMAN_LLM_MODEL_OPENAI; wingmanProvider = WingmanLlmProviders::WINGMAN_PROVIDER_OPENAI; return true; } MF_DEBUG( - " Wingman OpenAI API key NOT found in the environment variable " - "MINDFORGER_OPENAI_API_KEY" << endl); + " Wingman OpenAI API key NEITHER found in the environment variable " + "MINDFORGER_OPENAI_API_KEY, NOR set in MF configuration" << endl); wingmanApiKey.clear(); wingmanLlmModel.clear(); wingmanProvider = WingmanLlmProviders::WINGMAN_PROVIDER_NONE; diff --git a/lib/src/config/configuration.h b/lib/src/config/configuration.h index c8579aad..08790b19 100644 --- a/lib/src/config/configuration.h +++ b/lib/src/config/configuration.h @@ -371,9 +371,10 @@ class Configuration { - appWindow.llmProvider used to detect configuration change - on change: re-init Wingman DIALOG (refresh pre-defined prompts) */ - WingmanLlmProviders wingmanProvider; // "OpenAI", "Google", "Mock" - std::string wingmanApiKey; // OpenAI/Bard/... API key loaded from the shell environment - std::string wingmanLlmModel; // gpt-3.5-turbo + WingmanLlmProviders wingmanProvider; // "none", "Mock", "OpenAI", ... + std::string wingmanApiKey; // API key of the currently configured Wingman LLM provider + std::string wingmanOpenAiApiKey; // OpenAI API specified by user in the config, env or UI + std::string wingmanLlmModel; // preferred LLM model the currently configured provider, like "gpt-3.5-turbo" TimeScope timeScope; std::string timeScopeAsString; @@ -568,13 +569,18 @@ class Configuration { */ bool initWingman(); public: + std::string getWingmanOpenAiApiKey() const { return wingmanOpenAiApiKey; } + void setWingmanOpenAiApiKey(std::string apiKey) { wingmanOpenAiApiKey = apiKey; } + /** + * @brief Get API key of the currently configured Wingman LLM provider. + */ std::string getWingmanApiKey() const { return wingmanApiKey; } /** - * @brief Get preferred Wingman's LLM provider model name. + * @brief Get preferred Wingman LLM provider model name. */ std::string getWingmanLlmModel() const { return wingmanLlmModel; } /** - * @brief Check whether Wingman's LLM provider is ready from + * @brief Check whether a Wingman LLM provider is ready from * the configuration perspective. */ bool isWingman(); diff --git a/lib/src/representations/markdown/markdown_configuration_representation.cpp b/lib/src/representations/markdown/markdown_configuration_representation.cpp index 29db2d37..3c6c6e84 100644 --- a/lib/src/representations/markdown/markdown_configuration_representation.cpp +++ b/lib/src/representations/markdown/markdown_configuration_representation.cpp @@ -35,6 +35,7 @@ constexpr const auto CONFIG_SETTING_MIND_TAGS_SCOPE_LABEL = "* Tags scope: "; constexpr const auto CONFIG_SETTING_MIND_DISTRIBUTOR_INTERVAL = "* Async refresh interval (ms): "; constexpr const auto CONFIG_SETTING_MIND_AUTOLINKING = "* Autolinking: "; constexpr const auto CONFIG_SETTING_MIND_WINGMAN_PROVIDER = "* Wingman LLM provider: "; +constexpr const auto CONFIG_SETTING_MIND_OPENAI_KEY = "* Wingman's OpenAI API key: "; // application constexpr const auto CONFIG_SETTING_STARTUP_VIEW_LABEL = "* Startup view: "; @@ -410,6 +411,9 @@ void MarkdownConfigurationRepresentation::configurationSection( } else { c.setWingmanLlmProvider(WingmanLlmProviders::WINGMAN_PROVIDER_NONE); } + } else if(line->find(CONFIG_SETTING_MIND_OPENAI_KEY) != std::string::npos) { + string k = line->substr(strlen(CONFIG_SETTING_MIND_OPENAI_KEY)); + c.setWingmanOpenAiApiKey(k); } } } @@ -503,6 +507,8 @@ string& MarkdownConfigurationRepresentation::to(Configuration* c, string& md) " * Examples: yes, no" << endl << CONFIG_SETTING_MIND_WINGMAN_PROVIDER << Configuration::getWingmanLlmProviderAsString(c?c->getWingmanLlmProvider():Configuration::DEFAULT_WINGMAN_LLM_PROVIDER) << endl << " * Examples: none, openai" << endl << + CONFIG_SETTING_MIND_OPENAI_KEY << (c?c->getWingmanOpenAiApiKey():"") << endl << + " * OpenAI API key generated at https://platform.openai.com/api-keys to be used by Wingman as LLM provider" << endl << endl << "# " << CONFIG_SECTION_APP << endl <<