AI Config CRUD complete

This commit is contained in:
Martin McKeaveney 2024-09-03 17:18:46 +01:00
parent ca4b17bc9b
commit 43135e4274
5 changed files with 91 additions and 87 deletions

View File

@ -1,5 +1,5 @@
<script> <script>
import {Body, Label, Icon, Tag} from "@budibase/bbui" import { Body, Label, Icon } from "@budibase/bbui"
import OpenAILogo from "./logos/OpenAI.svelte" import OpenAILogo from "./logos/OpenAI.svelte"
import AnthropicLogo from "./logos/Anthropic.svelte" import AnthropicLogo from "./logos/Anthropic.svelte"
import { Providers } from "./constants" import { Providers } from "./constants"

View File

@ -5,13 +5,10 @@
Input, Input,
Select, Select,
Toggle, Toggle,
Body,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { ConfigMap, Providers } from "./constants" import {ConfigMap, Providers} from "./constants"
import { API } from "api"
export let defaultConfig = { export let config = {
active: false, active: false,
isDefault: false, isDefault: false,
} }
@ -19,11 +16,10 @@
export let saveHandler export let saveHandler
export let deleteHandler export let deleteHandler
let aiConfig = defaultConfig
let validation let validation
$: { $: {
const { provider, defaultModel, name, apiKey } = aiConfig const {provider, defaultModel, name, apiKey} = config
validation = provider && defaultModel && name && apiKey validation = provider && defaultModel && name && apiKey
} }
@ -31,20 +27,15 @@
const provider = evt.detail const provider = evt.detail
// grab the preset config from the constants for that provider and fill it in // grab the preset config from the constants for that provider and fill it in
if (ConfigMap[provider]) { if (ConfigMap[provider]) {
aiConfig = { config = {
...aiConfig, ...config,
...ConfigMap[provider], ...ConfigMap[provider],
provider provider,
} }
} else { } else {
aiConfig.provider = provider config.provider = provider
// aiConfig = {
// ...aiConfig,
// provider
// }
} }
} }
</script> </script>
<ModalContent <ModalContent
@ -57,10 +48,14 @@
title="Custom AI Configuration" title="Custom AI Configuration"
> >
<div class="form-row"> <div class="form-row">
<div class="form-row">
<Label size="M">Name</Label>
<Input placeholder={"Test 1"} bind:value={config.name}/>
</div>
<Label size="M">Provider</Label> <Label size="M">Provider</Label>
<Select <Select
placeholder={null} placeholder={null}
bind:value={aiConfig.provider} bind:value={config.provider}
options={Object.keys(Providers)} options={Object.keys(Providers)}
on:change={prefillConfig} on:change={prefillConfig}
/> />
@ -68,25 +63,21 @@
<div class="form-row"> <div class="form-row">
<Label size="M">Default Model</Label> <Label size="M">Default Model</Label>
<Select <Select
placeholder={aiConfig.provider ? "Choose an option" : "Select a provider first"} placeholder={config.provider ? "Choose an option" : "Select a provider first"}
bind:value={aiConfig.defaultModel} bind:value={config.defaultModel}
options={aiConfig.provider ? Providers[aiConfig.provider].models : []} options={config.provider ? Providers[config.provider].models : []}
/> />
</div> </div>
<div class="form-row">
<Label size="M">Name</Label>
<Input placeholder={"Test 1"} bind:value={aiConfig.name}/>
</div>
<div class="form-row"> <div class="form-row">
<Label size="M">Base URL</Label> <Label size="M">Base URL</Label>
<Input placeholder={"www.google.com"} bind:value={aiConfig.baseUrl}/> <Input placeholder={"www.google.com"} bind:value={config.baseUrl}/>
</div> </div>
<div class="form-row"> <div class="form-row">
<Label size="M">API Key</Label> <Label size="M">API Key</Label>
<Input type="password" bind:value={aiConfig.apiKey}/> <Input type="password" bind:value={config.apiKey}/>
</div> </div>
<Toggle text="Active" bind:value={aiConfig.active}/> <Toggle text="Active" bind:value={config.active}/>
<Toggle text="Set as default" bind:value={aiConfig.isDefault}/> <Toggle text="Set as default" bind:value={config.isDefault}/>
</ModalContent> </ModalContent>
<style> <style>

View File

@ -22,8 +22,9 @@
} }
let modal let modal
let aiConfig let fullAIConfig
let currentlyEditingConfig let editingAIConfig = {}
let editingUuid
$: isCloud = $admin.cloud $: isCloud = $admin.cloud
$: budibaseAIEnabled = $licensing.budibaseAIEnabled $: budibaseAIEnabled = $licensing.budibaseAIEnabled
@ -34,7 +35,7 @@
// Fetch the AI configs // Fetch the AI configs
const aiDoc = await API.getConfig(ConfigTypes.AI) const aiDoc = await API.getConfig(ConfigTypes.AI)
if (aiDoc._id) { if (aiDoc._id) {
aiConfig = aiDoc fullAIConfig = aiDoc
} }
} catch (error) { } catch (error) {
notifications.error("Error fetching AI config") notifications.error("Error fetching AI config")
@ -42,22 +43,27 @@
} }
async function saveConfig() { async function saveConfig() {
// Update the config that was changed // Use existing key or generate new one
const updateConfigs = aiConfig.config const id = editingUuid || Helpers.uuid()
const config = { // Creating first custom AI Config
if (!fullAIConfig) {
fullAIConfig = {
type: ConfigTypes.AI, type: ConfigTypes.AI,
config: [ config: {
// TODO: include the ones that are already there, or just handle this in the backend [id]: editingAIConfig
aiConfig,
]
} }
}
} else {
// Add new or update existing custom AI Config
fullAIConfig.config[id] = editingAIConfig
}
try { try {
const isNew = !!aiConfig._rev const savedConfig = await API.saveConfig(fullAIConfig)
const savedConfig = await API.saveConfig(config) fullAIConfig._rev = savedConfig._rev
aiConfig._rev = savedConfig._rev fullAIConfig._id = savedConfig._id
aiConfig._id = savedConfig._id notifications.success(`Successfully saved and activated AI Configuration`)
notifications.success(`Successfully saved and activated ${isNew ? "new" : ""} AI Configuration`)
} catch (error) { } catch (error) {
notifications.error( notifications.error(
`Failed to save AI Configuration, reason: ${error?.message || "Unknown"}` `Failed to save AI Configuration, reason: ${error?.message || "Unknown"}`
@ -65,15 +71,14 @@
} }
} }
async function deleteConfig(name) { async function deleteConfig(key) {
// Delete a configuration // Delete a configuration
const idx = aiConfig.config.findIndex(config => config.name === currentlyEditingConfig?.name || name) delete fullAIConfig.config[key]
aiConfig.config.splice(idx, 1)
try { try {
const savedConfig = await API.saveConfig(aiConfig) const savedConfig = await API.saveConfig(fullAIConfig)
aiConfig._rev = savedConfig._rev fullAIConfig._rev = savedConfig._rev
aiConfig._id = savedConfig._id fullAIConfig._id = savedConfig._id
notifications.success(`Deleted config`) notifications.success(`Deleted config`)
} catch (error) { } catch (error) {
notifications.error( notifications.error(
@ -82,13 +87,15 @@
} }
} }
function editConfig(config) { function editConfig(uuid) {
currentlyEditingConfig = config editingUuid = uuid
editingAIConfig = fullAIConfig?.config[editingUuid]
modal.show() modal.show()
} }
function newConfig() { function newConfig() {
currentlyEditingConfig = undefined editingUuid = undefined
editingAIConfig = undefined
modal.show() modal.show()
} }
@ -103,7 +110,7 @@
<AIConfigModal <AIConfigModal
saveHandler={saveConfig} saveHandler={saveConfig}
deleteHandler={deleteConfig} deleteHandler={deleteConfig}
defaultConfig={currentlyEditingConfig} bind:config={editingAIConfig}
/> />
</Modal> </Modal>
<Layout noPadding> <Layout noPadding>
@ -134,12 +141,12 @@
</div> </div>
<Body size="S">Use the following interface to select your preferred AI configuration.</Body> <Body size="S">Use the following interface to select your preferred AI configuration.</Body>
<Body size="S">Select your AI Model:</Body> <Body size="S">Select your AI Model:</Body>
{#if aiConfig} {#if fullAIConfig?.config}
{#each aiConfig.config as config} {#each Object.keys(fullAIConfig.config) as key}
<AIConfigTile <AIConfigTile
{config} config={fullAIConfig.config[key]}
editHandler={() => editConfig(config)} editHandler={() => editConfig(key)}
deleteHandler={modal.show} deleteHandler={() => deleteConfig(key)}
/> />
{/each} {/each}
{/if} {/if}

View File

@ -28,7 +28,9 @@ import {
SSOConfig, SSOConfig,
SSOConfigType, SSOConfigType,
UserCtx, UserCtx,
OIDCLogosConfig, AIConfig, OIDCLogosConfig,
AIConfig,
PASSWORD_REPLACEMENT,
} from "@budibase/types" } from "@budibase/types"
import * as pro from "@budibase/pro" import * as pro from "@budibase/pro"
@ -197,9 +199,11 @@ async function verifyOIDCConfig(config: OIDCConfigs) {
await verifySSOConfig(ConfigType.OIDC, config.configs[0]) await verifySSOConfig(ConfigType.OIDC, config.configs[0])
} }
async function verifyAIConfig(config: OIDCConfigs) { async function verifyAIConfig(config: AIConfig, existingConfig?: AIConfig) {
// await verifySSOConfig(ConfigType.OIDC, config.configs[0]) // ensure that the redacted API keys are not overwritten in the DB
// Shape should be `config_ai` // for (let aiConfig of existingConfig) {
//
// }
return true return true
} }
@ -231,7 +235,7 @@ export async function save(ctx: UserCtx<Config>) {
await verifyOIDCConfig(config) await verifyOIDCConfig(config)
break break
case ConfigType.AI: case ConfigType.AI:
await verifyAIConfig(config) await verifyAIConfig(config, existingConfig)
break break
} }
} catch (err: any) { } catch (err: any) {
@ -314,8 +318,10 @@ function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
} }
function sanitizeAIConfig(aiConfig: AIConfig) { function sanitizeAIConfig(aiConfig: AIConfig) {
for (let providerConfig of aiConfig.config) { for (let key in aiConfig.config) {
delete providerConfig.apiKey if (aiConfig.config[key].apiKey) {
aiConfig.config[key].apiKey = PASSWORD_REPLACEMENT
}
} }
return aiConfig return aiConfig
} }

View File

@ -67,7 +67,8 @@ function scimValidation() {
function aiValidation() { function aiValidation() {
// prettier-ignore // prettier-ignore
return Joi.array().items( return Joi.object().pattern(
Joi.string(),
Joi.object({ Joi.object({
provider: Joi.string().required(), provider: Joi.string().required(),
isDefault: Joi.boolean().required(), isDefault: Joi.boolean().required(),
@ -77,9 +78,8 @@ function aiValidation() {
apiKey: Joi.string().required(), apiKey: Joi.string().required(),
// TODO: should be enum // TODO: should be enum
defaultModel: Joi.string().optional(), defaultModel: Joi.string().optional(),
}).required()
}) )
).required()
} }
function buildConfigSaveValidation() { function buildConfigSaveValidation() {