From e7a94288ba92e0b4a7ae066465c0b946b4db242b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20K=C3=BCffer?= <101799433+rkuffer@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:56:18 +0100 Subject: [PATCH] [Tock Studio] Export and import of ia gen settings (#1760) * Wip * Rag settings export * Rag settings import * Sentence generation settings export and import * Observability settings export and import * Vector DB settings export and import --- .../models/providers-configuration.ts | 3 +- .../observability-settings.component.html | 133 +++++++++++ .../observability-settings.component.scss | 7 + .../observability-settings.component.ts | 173 ++++++++++++++- .../models/engines-configuration.ts | 16 +- ...entence-generation-settings.component.html | 131 +++++++++++ ...entence-generation-settings.component.scss | 7 + .../sentence-generation-settings.component.ts | 176 ++++++++++++++- .../models/providers-configuration.ts | 5 +- .../vector-db-settings.component.html | 133 +++++++++++ .../vector-db-settings.component.scss | 7 + .../vector-db-settings.component.ts | 172 ++++++++++++++- .../models/engines-configurations.ts | 25 ++- .../rag-settings/rag-settings.component.html | 141 +++++++++++- .../rag-settings/rag-settings.component.ts | 208 ++++++++++++++++-- bot/admin/web/src/app/rag/rag.module.ts | 3 +- .../web/src/app/shared/model/ai-settings.ts | 18 +- 17 files changed, 1273 insertions(+), 85 deletions(-) diff --git a/bot/admin/web/src/app/configuration/observability-settings/models/providers-configuration.ts b/bot/admin/web/src/app/configuration/observability-settings/models/providers-configuration.ts index bc98c880ba..5682374ab8 100644 --- a/bot/admin/web/src/app/configuration/observability-settings/models/providers-configuration.ts +++ b/bot/admin/web/src/app/configuration/observability-settings/models/providers-configuration.ts @@ -9,6 +9,7 @@ export interface ProvidersConfigurationParam { source?: string[]; inputScale?: 'default' | 'fullwidth'; defaultValue?: string; + confirmExport?: boolean; } export interface ProvidersConfiguration { @@ -23,7 +24,7 @@ export const ProvidersConfigurations: ProvidersConfiguration[] = [ key: ObservabilityProvider.Langfuse, params: [ { key: 'publicKey', label: 'Public key', type: 'obfuscated' }, - { key: 'secretKey', label: 'Secret key', type: 'obfuscated' }, + { key: 'secretKey', label: 'Secret key', type: 'obfuscated', confirmExport: true }, { key: 'url', label: 'Url', type: 'obfuscated' } ] } diff --git a/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.html b/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.html index ce1587d0ea..50d492c03f 100644 --- a/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.html +++ b/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.html @@ -3,6 +3,26 @@

Observability settings

+ + + + + + +
Include the following sensitive data:
+
+ + {{ sensitiveParam.label }} + {{ sensitiveParam.param.label }} + +
+
+ + + + + + + + + + + + Import observability settings dump + + + + +
+ + + +
+
+ + + + + +
+
+ + diff --git a/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.scss b/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.scss index e69de29bb2..0541689588 100644 --- a/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.scss +++ b/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.scss @@ -0,0 +1,7 @@ +.grid-actions { + display: grid; + grid-gap: 0.5rem; + grid-auto-flow: column; + align-items: center; + justify-content: end; +} diff --git a/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.ts b/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.ts index fa0193162e..915870e46e 100644 --- a/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.ts +++ b/bot/admin/web/src/app/configuration/observability-settings/observability-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { StateService } from '../../core-nlp/state.service'; import { RestService } from '../../core-nlp/rest/rest.service'; import { NbDialogService, NbToastrService, NbWindowService } from '@nebular/theme'; @@ -6,10 +6,17 @@ import { BotConfigurationService } from '../../core/bot-configuration.service'; import { Observable, Subject, debounceTime, takeUntil } from 'rxjs'; import { BotApplicationConfiguration } from '../../core/model/configuration'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { ObservabilityProvider, ProvidersConfiguration, ProvidersConfigurations } from './models/providers-configuration'; +import { + ObservabilityProvider, + ProvidersConfiguration, + ProvidersConfigurationParam, + ProvidersConfigurations +} from './models/providers-configuration'; import { ObservabilitySettings } from './models/observability-settings'; -import { deepCopy } from '../../shared/utils'; +import { deepCopy, getExportFileName, readFileAsText } from '../../shared/utils'; import { ChoiceDialogComponent, DebugViewerWindowComponent } from '../../shared/components'; +import { saveAs } from 'file-saver-es'; +import { FileValidators } from '../../shared/validators'; interface ObservabilitySettingsForm { id: FormControl; @@ -36,6 +43,9 @@ export class ObservabilitySettingsComponent implements OnInit, OnDestroy { settingsBackup: ObservabilitySettings; + @ViewChild('exportConfirmationModal') exportConfirmationModal: TemplateRef; + @ViewChild('importModal') importModal: TemplateRef; + constructor( private state: StateService, private rest: RestService, @@ -59,9 +69,14 @@ export class ObservabilitySettingsComponent implements OnInit, OnDestroy { this.botConfiguration.configurations.pipe(takeUntil(this.destroy$)).subscribe((confs: BotApplicationConfiguration[]) => { delete this.settingsBackup; + + // Reset form on configuration change + this.form.reset(); + // Reset formGroup control too, if any + this.resetFormGroupControls(); + this.loading = true; this.configurations = confs; - this.form.reset(); if (confs.length) { this.getObservabilitySettingsLoader().subscribe((res) => { @@ -136,10 +151,7 @@ export class ObservabilitySettingsComponent implements OnInit, OnDestroy { if (requiredConfiguration) { // Purge existing controls that may contain values incompatible with a new control with the same name if provider change - const existingGroupKeys = Object.keys(this.form.controls['setting'].controls); - existingGroupKeys.forEach((key) => { - this.form.controls['setting'].removeControl(key); - }); + this.resetFormGroupControls(); requiredConfiguration.params.forEach((param) => { this.form.controls['setting'].addControl(param.key, new FormControl(param.defaultValue, Validators.required)); @@ -149,6 +161,13 @@ export class ObservabilitySettingsComponent implements OnInit, OnDestroy { } } + resetFormGroupControls() { + const existingGroupKeys = Object.keys(this.form.controls['setting'].controls); + existingGroupKeys.forEach((key) => { + this.form.controls['setting'].removeControl(key); + }); + } + cancel(): void { this.initForm(this.settingsBackup); } @@ -196,6 +215,144 @@ export class ObservabilitySettingsComponent implements OnInit, OnDestroy { } } + get hasExportableData(): boolean { + if (this.observabilityProvider.value) return true; + + const formValue: ObservabilitySettings = deepCopy(this.form.value) as unknown as ObservabilitySettings; + + return Object.values(formValue).some((entry) => { + return entry && (typeof entry !== 'object' || Object.keys(entry).length !== 0); + }); + } + + sensitiveParams: { label: string; key: string; include: boolean; param: ProvidersConfigurationParam }[]; + + exportSettings() { + this.sensitiveParams = []; + + const shouldConfirm = + this.observabilityProvider.value && + this.currentObservabilityProvider.params.some((entry) => { + return entry.confirmExport; + }); + + if (shouldConfirm) { + this.currentObservabilityProvider.params.forEach((entry) => { + if (entry.confirmExport) { + this.sensitiveParams.push({ label: 'Observability provider', key: 'setting', include: false, param: entry }); + } + }); + + this.exportConfirmationModalRef = this.nbDialogService.open(this.exportConfirmationModal); + } else { + this.downloadSettings(); + } + } + + exportConfirmationModalRef; + + closeExportConfirmationModal() { + this.exportConfirmationModalRef.close(); + } + + confirmExportSettings() { + this.downloadSettings(); + this.closeExportConfirmationModal(); + } + + downloadSettings() { + const formValue: ObservabilitySettings = deepCopy(this.form.value) as unknown as ObservabilitySettings; + delete formValue['observabilityProvider']; + delete formValue['id']; + delete formValue['enabled']; + + if (this.sensitiveParams?.length) { + this.sensitiveParams.forEach((sensitiveParam) => { + if (!sensitiveParam.include) { + delete formValue[sensitiveParam.key][sensitiveParam.param.key]; + } + }); + } + + const jsonBlob = new Blob([JSON.stringify(formValue)], { + type: 'application/json' + }); + + const exportFileName = getExportFileName( + this.state.currentApplication.namespace, + this.state.currentApplication.name, + 'Observability settings', + 'json' + ); + + saveAs(jsonBlob, exportFileName); + + this.toastrService.show(`Observability settings dump provided`, 'Observability settings dump', { + duration: 3000, + status: 'success' + }); + } + + importModalRef; + + importSettings() { + this.isImportSubmitted = false; + this.importForm.reset(); + this.importModalRef = this.nbDialogService.open(this.importModal); + } + + closeImportModal() { + this.importModalRef.close(); + } + + isImportSubmitted: boolean = false; + + importForm: FormGroup = new FormGroup({ + fileSource: new FormControl([], { + nonNullable: true, + validators: [Validators.required, FileValidators.mimeTypeSupported(['application/json'])] + }) + }); + + get fileSource(): FormControl { + return this.importForm.get('fileSource') as FormControl; + } + + get canSaveImport(): boolean { + return this.isImportSubmitted ? this.importForm.valid : this.importForm.dirty; + } + + submitImportSettings() { + this.isImportSubmitted = true; + if (this.canSaveImport) { + const file = this.fileSource.value[0]; + + readFileAsText(file).then((fileContent) => { + const settings = JSON.parse(fileContent.data); + + const hasCompatibleProvider = + settings.setting?.provider && Object.values(ObservabilityProvider).includes(settings.setting.provider); + + if (!hasCompatibleProvider) { + this.toastrService.show( + `The file supplied does not reference a compatible provider. Please check the file.`, + 'Observability settings import fails', + { + duration: 6000, + status: 'danger' + } + ); + return; + } + + this.initForm(settings); + this.form.markAsDirty(); + + this.closeImportModal(); + }); + } + } + confirmSettingsDeletion() { const confirmAction = 'Delete'; const cancelAction = 'Cancel'; diff --git a/bot/admin/web/src/app/configuration/sentence-generation-settings/models/engines-configuration.ts b/bot/admin/web/src/app/configuration/sentence-generation-settings/models/engines-configuration.ts index 1390dc9a13..9b7384053a 100644 --- a/bot/admin/web/src/app/configuration/sentence-generation-settings/models/engines-configuration.ts +++ b/bot/admin/web/src/app/configuration/sentence-generation-settings/models/engines-configuration.ts @@ -1,7 +1,7 @@ import { AzureOpenAiApiVersionsList, EnginesConfiguration, - LLMProvider, + AiEngineProvider, OllamaLlmModelsList, OpenAIModelsList } from '../../../shared/model/ai-settings'; @@ -40,10 +40,10 @@ Answer in '{{locale}}' (language locale). export const EngineConfigurations: EnginesConfiguration[] = [ { - label: 'OpenAI', - key: LLMProvider.OpenAI, + label: 'OpenAi', + key: AiEngineProvider.OpenAI, params: [ - { key: 'apiKey', label: 'Api key', type: 'obfuscated' }, + { key: 'apiKey', label: 'Api key', type: 'obfuscated', confirmExport: true }, { key: 'baseUrl', label: 'Base url', type: 'text', defaultValue: 'https://api.openai.com/v1' }, { key: 'model', label: 'Model name', type: 'openlist', source: OpenAIModelsList }, { key: 'temperature', label: 'Temperature', type: 'number', inputScale: 'fullwidth' }, @@ -51,10 +51,10 @@ export const EngineConfigurations: EnginesConfiguration[] = [ ] }, { - label: 'Azure OpenAI', - key: LLMProvider.AzureOpenAIService, + label: 'Azure OpenAi', + key: AiEngineProvider.AzureOpenAIService, params: [ - { key: 'apiKey', label: 'Api key', type: 'obfuscated' }, + { key: 'apiKey', label: 'Api key', type: 'obfuscated', confirmExport: true }, { key: 'apiVersion', label: 'Api version', type: 'openlist', source: AzureOpenAiApiVersionsList }, { key: 'deploymentName', label: 'Deployment name', type: 'text' }, { key: 'apiBase', label: 'Base url', type: 'obfuscated' }, @@ -64,7 +64,7 @@ export const EngineConfigurations: EnginesConfiguration[] = [ }, { label: 'Ollama', - key: LLMProvider.Ollama, + key: AiEngineProvider.Ollama, params: [ { key: 'baseUrl', label: 'Base url', type: 'text', defaultValue: 'http://localhost:11434' }, { key: 'model', label: 'Model', type: 'openlist', source: OllamaLlmModelsList, defaultValue: 'llama2' }, diff --git a/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.html b/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.html index 236857d932..b0a3800600 100644 --- a/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.html +++ b/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.html @@ -19,6 +19,26 @@

Sentence generation settings

+ + + + + + +
Include the following sensitive data:
+
+ + {{ sensitiveParam.label }} + {{ sensitiveParam.param.label }} + +
+
+ + + + + + + + + + + + Import sentence generation settings dump + + + + +
+ + + +
+
+ + + + + +
+
+ diff --git a/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.scss b/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.scss index e69de29bb2..0541689588 100644 --- a/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.scss +++ b/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.scss @@ -0,0 +1,7 @@ +.grid-actions { + display: grid; + grid-gap: 0.5rem; + grid-auto-flow: column; + align-items: center; + justify-content: end; +} diff --git a/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.ts b/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.ts index 01d40c8651..82890d8b54 100644 --- a/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.ts +++ b/bot/admin/web/src/app/configuration/sentence-generation-settings/sentence-generation-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { Observable, Subject, debounceTime, takeUntil } from 'rxjs'; import { BotApplicationConfiguration } from '../../core/model/configuration'; import { DefaultPrompt, EngineConfigurations } from './models/engines-configuration'; @@ -7,16 +7,18 @@ import { StateService } from '../../core-nlp/state.service'; import { RestService } from '../../core-nlp/rest/rest.service'; import { NbDialogService, NbToastrService, NbWindowService } from '@nebular/theme'; import { BotConfigurationService } from '../../core/bot-configuration.service'; -import { EnginesConfiguration, LLMProvider } from '../../shared/model/ai-settings'; -import { deepCopy } from '../../shared/utils'; +import { AiEngineSettingKeyName, EnginesConfiguration, EnginesConfigurationParam, AiEngineProvider } from '../../shared/model/ai-settings'; +import { deepCopy, getExportFileName, readFileAsText } from '../../shared/utils'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { ChoiceDialogComponent, DebugViewerWindowComponent } from '../../shared/components'; +import { saveAs } from 'file-saver-es'; +import { FileValidators } from '../../shared/validators'; interface GenAiSettingsForm { id: FormControl; enabled: FormControl; nbSentences: FormControl; - llmEngine: FormControl; + llmEngine: FormControl; llmSetting: FormGroup; } @@ -40,6 +42,9 @@ export class SentenceGenerationSettingsComponent implements OnInit, OnDestroy { loading: boolean = false; + @ViewChild('exportConfirmationModal') exportConfirmationModal: TemplateRef; + @ViewChild('importModal') importModal: TemplateRef; + constructor( private state: StateService, private rest: RestService, @@ -57,15 +62,20 @@ export class SentenceGenerationSettingsComponent implements OnInit, OnDestroy { this.form .get('llmEngine') .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((engine: LLMProvider) => { + .subscribe((engine: AiEngineProvider) => { this.initFormSettings(engine); }); this.botConfiguration.configurations.pipe(takeUntil(this.destroy$)).subscribe((confs: BotApplicationConfiguration[]) => { delete this.settingsBackup; + + // Reset form on configuration change + this.form.reset(); + // Reset formGroup control too, if any + this.resetFormGroupControls(); + this.loading = true; this.configurations = confs; - this.form.reset(); if (confs.length) { this.getSentenceGenerationSettingsLoader().subscribe((res) => { @@ -108,15 +118,12 @@ export class SentenceGenerationSettingsComponent implements OnInit, OnDestroy { return this.isSubmitted ? this.form.valid : this.form.dirty; } - initFormSettings(provider: LLMProvider): void { + initFormSettings(provider: AiEngineProvider): void { let requiredConfiguration: EnginesConfiguration = EngineConfigurations.find((c) => c.key === provider); if (requiredConfiguration) { // Purge existing controls that may contain values incompatible with a new control with the same name after engine change - const existingGroupKeys = Object.keys(this.form.controls['llmSetting'].controls); - existingGroupKeys.forEach((key) => { - this.form.controls['llmSetting'].removeControl(key); - }); + this.resetFormGroupControls(); requiredConfiguration.params.forEach((param) => { this.form.controls['llmSetting'].addControl(param.key, new FormControl(param.defaultValue, Validators.required)); @@ -126,6 +133,13 @@ export class SentenceGenerationSettingsComponent implements OnInit, OnDestroy { } } + resetFormGroupControls() { + const existingGroupKeys = Object.keys(this.form.controls['llmSetting'].controls); + existingGroupKeys.forEach((key) => { + this.form.controls['llmSetting'].removeControl(key); + }); + } + private getSentenceGenerationSettingsLoader(): Observable { const url = `/configuration/bots/${this.state.currentApplication.name}/sentence-generation/configuration`; return this.rest.get(url, (settings: SentenceGenerationSettings) => settings); @@ -203,6 +217,146 @@ export class SentenceGenerationSettingsComponent implements OnInit, OnDestroy { } } + get hasExportableData(): boolean { + if (this.llmEngine.value) return true; + + const formValue: SentenceGenerationSettings = deepCopy(this.form.value) as unknown as SentenceGenerationSettings; + + return Object.values(formValue).some((entry) => { + return entry && (typeof entry !== 'object' || Object.keys(entry).length !== 0); + }); + } + + sensitiveParams: { label: string; key: string; include: boolean; param: EnginesConfigurationParam }[]; + + exportSettings() { + this.sensitiveParams = []; + + const shouldConfirm = + this.llmEngine.value && + this.currentLlmEngine.params.some((entry) => { + return entry.confirmExport; + }); + + if (shouldConfirm) { + [{ label: 'LLM engine', key: AiEngineSettingKeyName.llmSetting, params: this.currentLlmEngine.params }].forEach((engine) => { + this.currentLlmEngine.params.forEach((entry) => { + if (entry.confirmExport) { + this.sensitiveParams.push({ label: 'LLM engine', key: engine.key, include: false, param: entry }); + } + }); + }); + + this.exportConfirmationModalRef = this.nbDialogService.open(this.exportConfirmationModal); + } else { + this.downloadSettings(); + } + } + + exportConfirmationModalRef; + + closeExportConfirmationModal() { + this.exportConfirmationModalRef.close(); + } + + confirmExportSettings() { + this.downloadSettings(); + this.closeExportConfirmationModal(); + } + + downloadSettings() { + const formValue: SentenceGenerationSettings = deepCopy(this.form.value) as unknown as SentenceGenerationSettings; + delete formValue['llmEngine']; + delete formValue['id']; + delete formValue['enabled']; + + if (this.sensitiveParams?.length) { + this.sensitiveParams.forEach((sensitiveParam) => { + if (!sensitiveParam.include) { + delete formValue[sensitiveParam.key][sensitiveParam.param.key]; + } + }); + } + + const jsonBlob = new Blob([JSON.stringify(formValue)], { + type: 'application/json' + }); + + const exportFileName = getExportFileName( + this.state.currentApplication.namespace, + this.state.currentApplication.name, + 'Sentence generation settings', + 'json' + ); + + saveAs(jsonBlob, exportFileName); + + this.toastrService.show(`Sentence generation settings dump provided`, 'Sentence generation settings dump', { + duration: 3000, + status: 'success' + }); + } + + importModalRef; + + importSettings() { + this.isImportSubmitted = false; + this.importForm.reset(); + this.importModalRef = this.nbDialogService.open(this.importModal); + } + + closeImportModal() { + this.importModalRef.close(); + } + + isImportSubmitted: boolean = false; + + importForm: FormGroup = new FormGroup({ + fileSource: new FormControl([], { + nonNullable: true, + validators: [Validators.required, FileValidators.mimeTypeSupported(['application/json'])] + }) + }); + + get fileSource(): FormControl { + return this.importForm.get('fileSource') as FormControl; + } + + get canSaveImport(): boolean { + return this.isImportSubmitted ? this.importForm.valid : this.importForm.dirty; + } + + submitImportSettings() { + this.isImportSubmitted = true; + if (this.canSaveImport) { + const file = this.fileSource.value[0]; + + readFileAsText(file).then((fileContent) => { + const settings = JSON.parse(fileContent.data); + + const hasCompatibleProvider = + settings.llmSetting?.provider && Object.values(AiEngineProvider).includes(settings.llmSetting.provider); + + if (!hasCompatibleProvider) { + this.toastrService.show( + `The file supplied does not reference a compatible provider. Please check the file.`, + 'Sentence generation settings import fails', + { + duration: 6000, + status: 'danger' + } + ); + return; + } + + this.initForm(settings); + this.form.markAsDirty(); + + this.closeImportModal(); + }); + } + } + confirmSettingsDeletion() { const confirmAction = 'Delete'; const cancelAction = 'Cancel'; diff --git a/bot/admin/web/src/app/configuration/vector-db-settings/models/providers-configuration.ts b/bot/admin/web/src/app/configuration/vector-db-settings/models/providers-configuration.ts index 9894b579f4..13e046ea84 100644 --- a/bot/admin/web/src/app/configuration/vector-db-settings/models/providers-configuration.ts +++ b/bot/admin/web/src/app/configuration/vector-db-settings/models/providers-configuration.ts @@ -17,6 +17,7 @@ export interface ProvidersConfigurationParam { step?: number; readonly?: boolean; disabled?: boolean; + confirmExport?: boolean; } export interface ProvidersConfiguration { @@ -33,7 +34,7 @@ export const ProvidersConfigurations: ProvidersConfiguration[] = [ { key: 'host', label: 'Host', type: 'text', defaultValue: 'localhost' }, { key: 'port', label: 'Port', type: 'number', min: 1, max: 65535, step: 1, defaultValue: '9200' }, { key: 'username', label: 'User name', type: 'obfuscated', defaultValue: 'admin' }, - { key: 'password', label: 'Password', type: 'obfuscated', defaultValue: 'admin' }, + { key: 'password', label: 'Password', type: 'obfuscated', defaultValue: 'admin', confirmExport: true }, { key: 'k', label: 'k-nearest neighbors', @@ -53,7 +54,7 @@ export const ProvidersConfigurations: ProvidersConfiguration[] = [ { key: 'host', label: 'Host', type: 'text', defaultValue: 'localhost' }, { key: 'port', label: 'Port', type: 'number', min: 1, max: 65535, step: 1, defaultValue: '5432' }, { key: 'username', label: 'User name', type: 'obfuscated', defaultValue: 'postgres' }, - { key: 'password', label: 'Password', type: 'obfuscated', defaultValue: 'ChangeMe' }, + { key: 'password', label: 'Password', type: 'obfuscated', defaultValue: 'ChangeMe', confirmExport: true }, { key: 'k', label: 'k-nearest neighbors', diff --git a/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.html b/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.html index 7a676324e2..8d492280cf 100644 --- a/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.html +++ b/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.html @@ -3,6 +3,26 @@

Application vector database settings

+ + + + + + +
Include the following sensitive data:
+
+ + {{ sensitiveParam.label }} + {{ sensitiveParam.param.label }} + +
+
+ + + + + + + + + + + + Import Vector DB settings dump + + + + +
+ + + +
+
+ + + + + +
+
+ + diff --git a/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.scss b/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.scss index e69de29bb2..0541689588 100644 --- a/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.scss +++ b/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.scss @@ -0,0 +1,7 @@ +.grid-actions { + display: grid; + grid-gap: 0.5rem; + grid-auto-flow: column; + align-items: center; + justify-content: end; +} diff --git a/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.ts b/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.ts index 7d8e17da85..ca3477445d 100644 --- a/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.ts +++ b/bot/admin/web/src/app/configuration/vector-db-settings/vector-db-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { StateService } from '../../core-nlp/state.service'; import { RestService } from '../../core-nlp/rest/rest.service'; import { NbDialogService, NbToastrService, NbWindowService } from '@nebular/theme'; @@ -6,10 +6,17 @@ import { BotConfigurationService } from '../../core/bot-configuration.service'; import { Observable, Subject, debounceTime, takeUntil } from 'rxjs'; import { BotApplicationConfiguration } from '../../core/model/configuration'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { VectorDbProvider, ProvidersConfiguration, ProvidersConfigurations } from './models/providers-configuration'; +import { + VectorDbProvider, + ProvidersConfiguration, + ProvidersConfigurations, + ProvidersConfigurationParam +} from './models/providers-configuration'; import { VectorDbSettings } from './models/vector-db-settings'; -import { deepCopy } from '../../shared/utils'; +import { deepCopy, getExportFileName, readFileAsText } from '../../shared/utils'; import { ChoiceDialogComponent, DebugViewerWindowComponent } from '../../shared/components'; +import { saveAs } from 'file-saver-es'; +import { FileValidators } from '../../shared/validators'; interface VectorDbSettingsForm { id: FormControl; @@ -36,6 +43,9 @@ export class VectorDbSettingsComponent implements OnInit { settingsBackup: VectorDbSettings; + @ViewChild('exportConfirmationModal') exportConfirmationModal: TemplateRef; + @ViewChild('importModal') importModal: TemplateRef; + constructor( public state: StateService, private rest: RestService, @@ -59,9 +69,14 @@ export class VectorDbSettingsComponent implements OnInit { this.botConfiguration.configurations.pipe(takeUntil(this.destroy$)).subscribe((confs: BotApplicationConfiguration[]) => { delete this.settingsBackup; + + // Reset form on configuration change + this.form.reset(); + // Reset formGroup control too, if any + this.resetFormGroupControls(); + this.loading = true; this.configurations = confs; - this.form.reset(); if (confs.length) { this.getVectorDbSettingsLoader().subscribe((res) => { @@ -136,10 +151,7 @@ export class VectorDbSettingsComponent implements OnInit { if (requiredConfiguration) { // Purge existing controls that may contain values incompatible with a new control with the same name if provider change - const existingGroupKeys = Object.keys(this.form.controls['setting'].controls); - existingGroupKeys.forEach((key) => { - this.form.controls['setting'].removeControl(key); - }); + this.resetFormGroupControls(); requiredConfiguration.params.forEach((param) => { let defaultValue = param.defaultValue; @@ -160,6 +172,13 @@ export class VectorDbSettingsComponent implements OnInit { } } + resetFormGroupControls() { + const existingGroupKeys = Object.keys(this.form.controls['setting'].controls); + existingGroupKeys.forEach((key) => { + this.form.controls['setting'].removeControl(key); + }); + } + cancel(): void { this.initForm(this.settingsBackup); } @@ -207,6 +226,143 @@ export class VectorDbSettingsComponent implements OnInit { } } + get hasExportableData(): boolean { + if (this.vectorDbProvider.value) return true; + + const formValue: VectorDbSettings = deepCopy(this.form.value) as unknown as VectorDbSettings; + + return Object.values(formValue).some((entry) => { + return entry && (typeof entry !== 'object' || Object.keys(entry).length !== 0); + }); + } + + sensitiveParams: { label: string; key: string; include: boolean; param: ProvidersConfigurationParam }[]; + + exportSettings() { + this.sensitiveParams = []; + + const shouldConfirm = + this.vectorDbProvider.value && + this.currentVectorDbProvider.params.some((entry) => { + return entry.confirmExport; + }); + + if (shouldConfirm) { + this.currentVectorDbProvider.params.forEach((entry) => { + if (entry.confirmExport) { + this.sensitiveParams.push({ label: 'Vector DB provider', key: 'setting', include: false, param: entry }); + } + }); + + this.exportConfirmationModalRef = this.nbDialogService.open(this.exportConfirmationModal); + } else { + this.downloadSettings(); + } + } + + exportConfirmationModalRef; + + closeExportConfirmationModal() { + this.exportConfirmationModalRef.close(); + } + + confirmExportSettings() { + this.downloadSettings(); + this.closeExportConfirmationModal(); + } + + downloadSettings() { + const formValue: VectorDbSettings = deepCopy(this.form.value) as unknown as VectorDbSettings; + delete formValue['vectorDbProvider']; + delete formValue['id']; + delete formValue['enabled']; + + if (this.sensitiveParams?.length) { + this.sensitiveParams.forEach((sensitiveParam) => { + if (!sensitiveParam.include) { + delete formValue[sensitiveParam.key][sensitiveParam.param.key]; + } + }); + } + + const jsonBlob = new Blob([JSON.stringify(formValue)], { + type: 'application/json' + }); + + const exportFileName = getExportFileName( + this.state.currentApplication.namespace, + this.state.currentApplication.name, + 'Vector DB settings', + 'json' + ); + + saveAs(jsonBlob, exportFileName); + + this.toastrService.show(`Vector DB settings dump provided`, 'Vector DB settings dump', { + duration: 3000, + status: 'success' + }); + } + + importModalRef; + + importSettings() { + this.isImportSubmitted = false; + this.importForm.reset(); + this.importModalRef = this.nbDialogService.open(this.importModal); + } + + closeImportModal() { + this.importModalRef.close(); + } + + isImportSubmitted: boolean = false; + + importForm: FormGroup = new FormGroup({ + fileSource: new FormControl([], { + nonNullable: true, + validators: [Validators.required, FileValidators.mimeTypeSupported(['application/json'])] + }) + }); + + get fileSource(): FormControl { + return this.importForm.get('fileSource') as FormControl; + } + + get canSaveImport(): boolean { + return this.isImportSubmitted ? this.importForm.valid : this.importForm.dirty; + } + + submitImportSettings() { + this.isImportSubmitted = true; + if (this.canSaveImport) { + const file = this.fileSource.value[0]; + + readFileAsText(file).then((fileContent) => { + const settings = JSON.parse(fileContent.data); + + const hasCompatibleProvider = settings.setting?.provider && Object.values(VectorDbProvider).includes(settings.setting.provider); + + if (!hasCompatibleProvider) { + this.toastrService.show( + `The file supplied does not reference a compatible provider. Please check the file.`, + 'Vector DB settings import fails', + { + duration: 6000, + status: 'danger' + } + ); + return; + } + + this.initForm(settings); + this.form.markAsDirty(); + + this.closeImportModal(); + }); + } + } + confirmSettingsDeletion() { const confirmAction = 'Delete'; const cancelAction = 'Cancel'; diff --git a/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts b/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts index 8fba6a2e2f..f52c37bc4d 100644 --- a/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts +++ b/bot/admin/web/src/app/rag/rag-settings/models/engines-configurations.ts @@ -1,7 +1,8 @@ import { AzureOpenAiApiVersionsList, + AiEngineSettingKeyName, EnginesConfiguration, - LLMProvider, + AiEngineProvider, OllamaEmModelsList, OllamaLlmModelsList, OpenAIEmbeddingModel, @@ -40,9 +41,9 @@ Answer in {locale}. const EnginesConfigurations_Llm: EnginesConfiguration[] = [ { label: 'OpenAI', - key: LLMProvider.OpenAI, + key: AiEngineProvider.OpenAI, params: [ - { key: 'apiKey', label: 'Api key', type: 'obfuscated' }, + { key: 'apiKey', label: 'Api key', type: 'obfuscated', confirmExport: true }, { key: 'baseUrl', label: 'Base url', type: 'text', defaultValue: 'https://api.openai.com/v1' }, { key: 'model', label: 'Model name', type: 'openlist', source: OpenAIModelsList }, { key: 'temperature', label: 'Temperature', type: 'number', inputScale: 'fullwidth' }, @@ -51,9 +52,9 @@ const EnginesConfigurations_Llm: EnginesConfiguration[] = [ }, { label: 'Azure OpenAI', - key: LLMProvider.AzureOpenAIService, + key: AiEngineProvider.AzureOpenAIService, params: [ - { key: 'apiKey', label: 'Api key', type: 'obfuscated' }, + { key: 'apiKey', label: 'Api key', type: 'obfuscated', confirmExport: true }, { key: 'apiVersion', label: 'Api version', type: 'openlist', source: AzureOpenAiApiVersionsList }, { key: 'deploymentName', label: 'Deployment name', type: 'text' }, { key: 'apiBase', label: 'Base url', type: 'obfuscated' }, @@ -63,7 +64,7 @@ const EnginesConfigurations_Llm: EnginesConfiguration[] = [ }, { label: 'Ollama', - key: LLMProvider.Ollama, + key: AiEngineProvider.Ollama, params: [ { key: 'baseUrl', label: 'BaseUrl', type: 'text', defaultValue: 'http://localhost:11434' }, { key: 'model', label: 'Model', type: 'openlist', source: OllamaLlmModelsList, defaultValue: 'llama2' }, @@ -76,18 +77,18 @@ const EnginesConfigurations_Llm: EnginesConfiguration[] = [ const EnginesConfigurations_Embedding: EnginesConfiguration[] = [ { label: 'OpenAI', - key: LLMProvider.OpenAI, + key: AiEngineProvider.OpenAI, params: [ - { key: 'apiKey', label: 'Api key', type: 'obfuscated' }, + { key: 'apiKey', label: 'Api key', type: 'obfuscated', confirmExport: true }, { key: 'baseUrl', label: 'Base url', type: 'text', defaultValue: 'https://api.openai.com/v1' }, { key: 'model', label: 'Model name', type: 'openlist', source: OpenAIEmbeddingModel } ] }, { label: 'Azure OpenAI', - key: LLMProvider.AzureOpenAIService, + key: AiEngineProvider.AzureOpenAIService, params: [ - { key: 'apiKey', label: 'Api key', type: 'obfuscated' }, + { key: 'apiKey', label: 'Api key', type: 'obfuscated', confirmExport: true }, { key: 'apiVersion', label: 'Api version', type: 'openlist', source: AzureOpenAiApiVersionsList }, { key: 'deploymentName', label: 'Deployment name', type: 'text' }, { key: 'apiBase', label: 'Base url', type: 'obfuscated' } @@ -95,7 +96,7 @@ const EnginesConfigurations_Embedding: EnginesConfiguration[] = [ }, { label: 'Ollama', - key: LLMProvider.Ollama, + key: AiEngineProvider.Ollama, params: [ { key: 'baseUrl', label: 'BaseUrl', type: 'text', defaultValue: 'http://localhost:11434' }, { key: 'model', label: 'Model', type: 'openlist', source: OllamaEmModelsList, defaultValue: 'all-minilm' } @@ -103,7 +104,7 @@ const EnginesConfigurations_Embedding: EnginesConfiguration[] = [ } ]; -export const EnginesConfigurations: { [key: string]: EnginesConfiguration[] } = { +export const EnginesConfigurations: { [K in AiEngineSettingKeyName]: EnginesConfiguration[] } = { llmSetting: EnginesConfigurations_Llm, emSetting: EnginesConfigurations_Embedding }; diff --git a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html index f07c95dc44..3607171e6c 100644 --- a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html +++ b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.html @@ -19,6 +19,26 @@

Rag settings

+ + + + + + +
Include the following sensitive data:
+
+ + {{ sensitiveParam.label }} + {{ sensitiveParam.param.label }} + +
+
+ + + + + + + + + + + + Import Rag settings dump + + + + +
+ + + +
+
+ + + + + +
+
+ diff --git a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts index 1d0828e7e9..6f5be53672 100644 --- a/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts +++ b/bot/admin/web/src/app/rag/rag-settings/rag-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { debounceTime, forkJoin, Observable, of, Subject, take, takeUntil } from 'rxjs'; import { BotService } from '../../bot/bot-service'; @@ -9,11 +9,13 @@ import { DefaultPrompt, EnginesConfigurations } from './models/engines-configura import { RagSettings } from './models'; import { NbDialogService, NbToastrService, NbWindowService } from '@nebular/theme'; import { BotConfigurationService } from '../../core/bot-configuration.service'; -import { deepCopy } from '../../shared/utils'; +import { deepCopy, getExportFileName, readFileAsText } from '../../shared/utils'; import { BotApplicationConfiguration } from '../../core/model/configuration'; import { DebugViewerWindowComponent } from '../../shared/components/debug-viewer-window/debug-viewer-window.component'; -import { EnginesConfiguration, LLMProvider } from '../../shared/model/ai-settings'; +import { AiEngineSettingKeyName, EnginesConfiguration, EnginesConfigurationParam, AiEngineProvider } from '../../shared/model/ai-settings'; import { ChoiceDialogComponent } from '../../shared/components'; +import { saveAs } from 'file-saver-es'; +import { FileValidators } from '../../shared/validators'; interface RagSettingsForm { id: FormControl; @@ -25,9 +27,9 @@ interface RagSettingsForm { indexSessionId: FormControl; indexName: FormControl; - llmEngine: FormControl; + llmEngine: FormControl; llmSetting: FormGroup; - emEngine: FormControl; + emEngine: FormControl; emSetting: FormGroup; } @@ -43,6 +45,8 @@ export class RagSettingsComponent implements OnInit, OnDestroy { enginesConfigurations = EnginesConfigurations; + engineSettingKeyName = AiEngineSettingKeyName; + defaultPrompt = DefaultPrompt; availableStories: StoryDefinitionConfiguration[]; @@ -55,6 +59,9 @@ export class RagSettingsComponent implements OnInit, OnDestroy { loading: boolean = false; + @ViewChild('exportConfirmationModal') exportConfirmationModal: TemplateRef; + @ViewChild('importModal') importModal: TemplateRef; + constructor( private botService: BotService, private state: StateService, @@ -73,22 +80,29 @@ export class RagSettingsComponent implements OnInit, OnDestroy { this.form .get('llmEngine') .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((engine: LLMProvider) => { - this.initFormSettings('llmSetting', engine); + .subscribe((engine: AiEngineProvider) => { + this.initFormSettings(AiEngineSettingKeyName.llmSetting, engine); }); this.form .get('emEngine') .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((engine: LLMProvider) => { - this.initFormSettings('emSetting', engine); + .subscribe((engine: AiEngineProvider) => { + this.initFormSettings(AiEngineSettingKeyName.emSetting, engine); }); this.botConfiguration.configurations.pipe(takeUntil(this.destroy$)).subscribe((confs: BotApplicationConfiguration[]) => { delete this.settingsBackup; + + // Reset form on configuration change + this.form.reset(); + // Reset formGroup controls too, if any + this.resetFormGroupControls(AiEngineSettingKeyName.llmSetting); + this.resetFormGroupControls(AiEngineSettingKeyName.emSetting); + this.loading = true; this.configurations = confs; - this.form.reset(); + if (confs.length) { forkJoin([this.getStoriesLoader(), this.getRagSettingsLoader()]).subscribe((res) => { this.availableStories = res[0]; @@ -202,15 +216,12 @@ export class RagSettingsComponent implements OnInit, OnDestroy { }, 100); } - initFormSettings(group: 'llmSetting' | 'emSetting', provider: LLMProvider): void { + initFormSettings(group: AiEngineSettingKeyName, provider: AiEngineProvider): void { let requiredConfiguration: EnginesConfiguration = EnginesConfigurations[group].find((c) => c.key === provider); if (requiredConfiguration) { // Purge existing controls that may contain values incompatible with a new control with the same name after engine change - const existingGroupKeys = Object.keys(this.form.controls[group].controls); - existingGroupKeys.forEach((key) => { - this.form.controls[group].removeControl(key); - }); + this.resetFormGroupControls(group); requiredConfiguration.params.forEach((param) => { this.form.controls[group].addControl(param.key, new FormControl(param.defaultValue, Validators.required)); @@ -220,17 +231,24 @@ export class RagSettingsComponent implements OnInit, OnDestroy { } } + resetFormGroupControls(group: AiEngineSettingKeyName) { + const existingGroupKeys = Object.keys(this.form.controls[group].controls); + existingGroupKeys.forEach((key) => { + this.form.controls[group].removeControl(key); + }); + } + private getRagSettingsLoader(): Observable { const url = `/configuration/bots/${this.state.currentApplication.name}/rag`; return this.rest.get(url, (settings: RagSettings) => settings); } initForm(settings: RagSettings) { - this.initFormSettings('llmSetting', settings.llmSetting.provider); - this.initFormSettings('emSetting', settings.emSetting.provider); + this.initFormSettings(AiEngineSettingKeyName.llmSetting, settings.llmSetting?.provider); + this.initFormSettings(AiEngineSettingKeyName.emSetting, settings.emSetting?.provider); this.form.patchValue({ - llmEngine: settings.llmSetting.provider, - emEngine: settings.emSetting.provider + llmEngine: settings.llmSetting?.provider, + emEngine: settings.emSetting?.provider }); this.form.patchValue(settings); this.form.markAsPristine(); @@ -249,11 +267,11 @@ export class RagSettingsComponent implements OnInit, OnDestroy { } get currentLlmEngine(): EnginesConfiguration { - return EnginesConfigurations['llmSetting'].find((e) => e.key === this.llmEngine.value); + return EnginesConfigurations[AiEngineSettingKeyName.llmSetting].find((e) => e.key === this.llmEngine.value); } get currentEmEngine(): EnginesConfiguration { - return EnginesConfigurations['emSetting'].find((e) => e.key === this.emEngine.value); + return EnginesConfigurations[AiEngineSettingKeyName.emSetting].find((e) => e.key === this.emEngine.value); } private getStoriesLoader(): Observable { @@ -282,11 +300,11 @@ export class RagSettingsComponent implements OnInit, OnDestroy { if (this.canSave && this.form.dirty) { this.loading = true; const formValue: RagSettings = deepCopy(this.form.value) as unknown as RagSettings; + delete formValue['llmEngine']; + delete formValue['emEngine']; formValue.namespace = this.state.currentApplication.namespace; formValue.botId = this.state.currentApplication.name; formValue.noAnswerStoryId = this.noAnswerStoryId.value === 'null' ? null : this.noAnswerStoryId.value; - delete formValue['llmEngine']; - delete formValue['emEngine']; const url = `/configuration/bots/${this.state.currentApplication.name}/rag`; this.rest.post(url, formValue, null, null, true).subscribe({ @@ -328,6 +346,150 @@ export class RagSettingsComponent implements OnInit, OnDestroy { } } + get hasExportableData(): boolean { + if (this.llmEngine.value || this.emEngine.value) return true; + + const formValue: RagSettings = deepCopy(this.form.value) as unknown as RagSettings; + + return Object.values(formValue).some((entry) => { + return entry && (typeof entry !== 'object' || Object.keys(entry).length !== 0); + }); + } + + sensitiveParams: { label: string; key: string; include: boolean; param: EnginesConfigurationParam }[]; + + exportSettings() { + this.sensitiveParams = []; + + const shouldConfirm = + (this.llmEngine.value || this.emEngine.value) && + [(this.currentLlmEngine.params, this.currentEmEngine.params)].some((engine) => { + return engine.some((entry) => { + return entry.confirmExport; + }); + }); + + if (shouldConfirm) { + [ + { label: 'LLM engine', key: AiEngineSettingKeyName.llmSetting, params: this.currentLlmEngine.params }, + { label: 'Embedding engine', key: AiEngineSettingKeyName.emSetting, params: this.currentEmEngine.params } + ].forEach((engine) => { + engine.params.forEach((entry) => { + if (entry.confirmExport) { + this.sensitiveParams.push({ label: engine.label, key: engine.key, include: false, param: entry }); + } + }); + }); + + this.exportConfirmationModalRef = this.nbDialogService.open(this.exportConfirmationModal); + } else { + this.downloadSettings(); + } + } + + exportConfirmationModalRef; + + closeExportConfirmationModal() { + this.exportConfirmationModalRef.close(); + } + + confirmExportSettings() { + this.downloadSettings(); + this.closeExportConfirmationModal(); + } + + downloadSettings() { + const formValue: RagSettings = deepCopy(this.form.value) as unknown as RagSettings; + delete formValue['llmEngine']; + delete formValue['emEngine']; + delete formValue['id']; + delete formValue['enabled']; + + if (this.sensitiveParams?.length) { + this.sensitiveParams.forEach((sensitiveParam) => { + if (!sensitiveParam.include) { + delete formValue[sensitiveParam.key][sensitiveParam.param.key]; + } + }); + } + + const jsonBlob = new Blob([JSON.stringify(formValue)], { + type: 'application/json' + }); + + const exportFileName = getExportFileName( + this.state.currentApplication.namespace, + this.state.currentApplication.name, + 'Rag settings', + 'json' + ); + + saveAs(jsonBlob, exportFileName); + + this.toastrService.show(`Rag settings dump provided`, 'Rag settings dump', { duration: 3000, status: 'success' }); + } + + importModalRef; + + importSettings() { + this.isImportSubmitted = false; + this.importForm.reset(); + this.importModalRef = this.nbDialogService.open(this.importModal); + } + + closeImportModal() { + this.importModalRef.close(); + } + + isImportSubmitted: boolean = false; + + importForm: FormGroup = new FormGroup({ + fileSource: new FormControl([], { + nonNullable: true, + validators: [Validators.required, FileValidators.mimeTypeSupported(['application/json'])] + }) + }); + + get fileSource(): FormControl { + return this.importForm.get('fileSource') as FormControl; + } + + get canSaveImport(): boolean { + return this.isImportSubmitted ? this.importForm.valid : this.importForm.dirty; + } + + submitImportSettings() { + this.isImportSubmitted = true; + if (this.canSaveImport) { + const file = this.fileSource.value[0]; + + readFileAsText(file).then((fileContent) => { + const settings = JSON.parse(fileContent.data); + + const hasCompatibleProvider = Object.values(AiEngineSettingKeyName).some((ekn) => { + return settings[ekn]?.provider && Object.values(AiEngineProvider).includes(settings[ekn].provider); + }); + + if (!hasCompatibleProvider) { + this.toastrService.show( + `The file supplied does not reference a compatible provider. Please check the file.`, + 'Rag settings import fails', + { + duration: 6000, + status: 'danger' + } + ); + return; + } + + this.initForm(settings); + this.form.markAsDirty(); + + this.closeImportModal(); + }); + } + } + confirmSettingsDeletion() { const confirmAction = 'Delete'; const cancelAction = 'Cancel'; diff --git a/bot/admin/web/src/app/rag/rag.module.ts b/bot/admin/web/src/app/rag/rag.module.ts index 7d12894e8d..eeeb0f04b5 100644 --- a/bot/admin/web/src/app/rag/rag.module.ts +++ b/bot/admin/web/src/app/rag/rag.module.ts @@ -21,7 +21,7 @@ import { import { RagSettingsComponent } from './rag-settings/rag-settings.component'; import { BotSharedModule } from '../shared/bot-shared.module'; import { RagRoutingModule } from './rag-routing.module'; -import { ReactiveFormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RagExcludedComponent } from './rag-excluded/rag-excluded.component'; import { RagSourcesBoardComponent } from './rag-sources/rag-sources-board.component'; import { NewSourceComponent } from './rag-sources/new-source/new-source.component'; @@ -38,6 +38,7 @@ import { SourceManagementApiService } from './rag-sources/source-management.api. CommonModule, BotSharedModule, RagRoutingModule, + FormsModule, ReactiveFormsModule, NbRouteTabsetModule, NbSelectModule, diff --git a/bot/admin/web/src/app/shared/model/ai-settings.ts b/bot/admin/web/src/app/shared/model/ai-settings.ts index bba79a5af0..519e7035b3 100644 --- a/bot/admin/web/src/app/shared/model/ai-settings.ts +++ b/bot/admin/web/src/app/shared/model/ai-settings.ts @@ -1,15 +1,20 @@ -export enum LLMProvider { +export enum AiEngineProvider { OpenAI = 'OpenAI', AzureOpenAIService = 'AzureOpenAIService', Ollama = 'Ollama' } +export enum AiEngineSettingKeyName { + llmSetting = 'llmSetting', + emSetting = 'emSetting' +} + export interface llmSetting { - provider: LLMProvider; + provider: AiEngineProvider; - apiKey: String; model: String; + apiKey?: String; deploymentName?: String; apiBase?: String; apiVersion?: String; @@ -19,11 +24,11 @@ export interface llmSetting { } export interface emSetting { - provider: LLMProvider; + provider: AiEngineProvider; - apiKey: String; model: String; + apiKey?: String; deploymentName?: String; apiBase?: String; apiVersion?: String; @@ -31,7 +36,7 @@ export interface emSetting { export interface EnginesConfiguration { label: string; - key: LLMProvider; + key: AiEngineProvider; params: EnginesConfigurationParam[]; } @@ -42,6 +47,7 @@ export interface EnginesConfigurationParam { source?: string[]; inputScale?: 'default' | 'fullwidth'; defaultValue?: string | number; + confirmExport?: boolean; } export const AzureOpenAiApiVersionsList: string[] = [