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>
import {Body, Label, Icon, Tag} from "@budibase/bbui"
import { Body, Label, Icon } from "@budibase/bbui"
import OpenAILogo from "./logos/OpenAI.svelte"
import AnthropicLogo from "./logos/Anthropic.svelte"
import { Providers } from "./constants"

View File

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

View File

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

View File

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

View File

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