rough pass on AI platform CRUD

This commit is contained in:
Martin McKeaveney 2024-09-02 20:16:37 +01:00
parent 1a57e37d38
commit 99035ad8ae
10 changed files with 220 additions and 57 deletions

View File

@ -1,4 +1,5 @@
import {
AIConfig, AIInnerConfig,
Config,
ConfigType,
GoogleConfig,
@ -17,6 +18,7 @@ import { DocumentType, SEPARATOR } from "../constants"
import { CacheKey, TTL, withCache } from "../cache"
import * as context from "../context"
import env from "../environment"
import { getConfigParams } from "@budibase/server/src/migrations/functions/backfill/global/configs"
// UTILS
@ -254,3 +256,42 @@ export async function getSCIMConfig(): Promise<SCIMInnerConfig | undefined> {
const config = await getConfig<SCIMConfig>(ConfigType.SCIM)
return config?.config
}
// AI
// TODO: Can we assume that you are licensed when you hit this endpoint?
export async function getAIConfig(): Promise<AIConfig | undefined> {
if (!env.SELF_HOSTED) {
// always use the env vars in cloud
// TODO: Licensing stuff - make this right
if (env.OPENAI_API_KEY) {
return getDefaultBudibaseAIConfig()
}
}
// prefer the config in self-host
let config = await getConfig<AIConfig>(ConfigType.AI)
// fallback to env vars
if (!config || !config.activated) {
config = getDefaultBudibaseAIConfig()
}
return config
}
export function getDefaultBudibaseAIConfig(): AIInnerConfig | undefined {
if (env.OPENAI_API_KEY) {
return {
provider: "",
isDefault: true,
name: "Budibase AI",
active: true,
baseUrl: "",
apiKey: env.OPENAI_API_KEY,
// TODO: should be enum
defaultModel: ""
}
}
}

View File

@ -5,15 +5,14 @@
Input,
Select,
Toggle,
Dropzone,
Body,
notifications,
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { plugins } from "stores/portal"
import { PluginSource } from "constants"
import { ConfigMap } from "./constants"
import { API } from "api"
const options = [
// TODO: Update these
const providers = [
"OpenAI",
"Anthropic",
"Together AI",
@ -21,21 +20,69 @@
"Custom"
]
let source = PluginSource.GITHUB
let dynamicValues = {}
const models = [
"gpt4o-mini",
]
let name
let baseUrl
let apiKey
let active
let isDefault
let formValues = {}
let validation
$: {
const { provider, model, name, apiKey } = formValues
validation = provider && model && name && apiKey
}
function prefillConfig(evt) {
const provider = evt.detail
// grab the preset config from the constants for that provider and fill it in
if (ConfigMap[provider]) {
formValues = {
...formValues,
...ConfigMap[provider]
}
} else {
formValues = {
provider
}
}
}
async function saveConfig() {
try {
const savedConfig = await API.saveConfig(formValues)
formValues._rev = savedConfig._rev
formValues._id = savedConfig._id
notifications.success(`Configuration saved`)
} catch (error) {
notifications.error(
`Failed to save AI Configuration, reason: ${error?.message || "Unknown"}`
)
}
}
async function deleteConfig() {
// Delete a configuration
try {
// await API.deleteConfig({
// id: smtpConfig._id,
// rev: smtpConfig._rev,
// })
notifications.success(`Deleted config`)
} catch (error) {
notifications.error(
`Failed to clear email settings, reason: ${error?.message || "Unknown"}`
)
}
}
</script>
<ModalContent
confirmText={"Save"}
onConfirm={() => {}}
cancelText={"Delete"}
onConfirm={saveConfig}
onCancel={deleteConfig}
disabled={!validation}
size="M"
title="Custom AI Configuration"
@ -43,39 +90,40 @@
<div class="form-row">
<Label size="M">Provider</Label>
<Select
placeholder={null}
bind:value={source}
options={options}
placeholder={null}
bind:value={formValues.provider}
options={providers}
on:change={prefillConfig}
/>
</div>
<div class="form-row">
<Label size="M">Model</Label>
<Select
placeholder={null}
bind:value={source}
options={Object.values(PluginSource)}
placeholder={null}
bind:value={formValues.model}
options={models}
/>
</div>
<div class="form-row">
<Label size="M">Name</Label>
<Input placeholder={"Test 1"} bind:value={name} />
<Input placeholder={"Test 1"} bind:value={formValues.name}/>
</div>
<div class="form-row">
<Label size="M">Base URL</Label>
<Input placeholder={"www.google.com"} bind:value={baseUrl} />
<Input placeholder={"www.google.com"} bind:value={formValues.baseUrl}/>
</div>
<div class="form-row">
<Label size="M">API Key</Label>
<Input bind:value={apiKey} />
<Input bind:value={formValues.apiKey}/>
</div>
<Toggle text="Active" bind:value={active} />
<Toggle text="Set as default" bind:value={isDefault} />
<Toggle text="Active" bind:value={formValues.active}/>
<Toggle text="Set as default" bind:value={formValues.isDefault}/>
</ModalContent>
<style>
.form-row {
display: grid;
grid-gap: var(--spacing-s);
align-items: center;
}
.form-row {
display: grid;
grid-gap: var(--spacing-s);
align-items: center;
}
</style>

View File

@ -0,0 +1,8 @@
export const ConfigMap = {
OpenAI: {
baseUrl: "https://api.openai.com"
},
Anthropic: {
baseUrl: "https://api.together.xyz/v1"
},
}

View File

@ -8,57 +8,76 @@
Divider,
notifications,
Modal,
ModalContent,
Tags,
Tag
} from "@budibase/bbui"
import { auth, admin } from "stores/portal"
import { redirect } from "@roxi/routify"
import { admin, licensing } from "stores/portal"
import { API } from "api"
import AIConfigModal from "./ConfigModal.svelte"
import { onMount } from "svelte"
import { sdk } from "@budibase/shared-core"
let diagnosticInfo = ""
let modal
const ConfigTypes = {
AI: "ai",
}
// async function fetchSystemDebugInfo() {
// const diagnostics = await API.fetchSystemDebugInfo()
// diagnosticInfo = {
// browser: {
// language: navigator.language || navigator.userLanguage,
// userAgent: navigator.userAgent,
// platform: navigator.platform,
// vendor: navigator.vendor,
// },
// server: diagnostics,
// }
// }
//
// const copyToClipboard = async () => {
// await Helpers.copyToClipboard(JSON.stringify(diagnosticInfo, undefined, 2))
// notifications.success("Copied")
// }
let modal
let aiConfig
let loading = false
$: isCloud = $admin.cloud
$: budibaseAIEnabled = $licensing.budibaseAIEnabled
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
async function fetchAIConfig() {
loading = true
try {
// Fetch the configs for smtp
const aiDoc = await API.getConfig(ConfigTypes.AI)
if (aiDoc._id) {
aiConfig = aiDoc
}
loading = false
} catch (error) {
notifications.error("Error fetching AI config")
}
}
onMount(async () => {
// await fetchSystemDebugInfo()
await fetchAIConfig()
})
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Modal bind:this={modal}>
<AIConfigModal />
</Modal>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading size="M">AI</Heading>
{#if isCloud && !budibaseAIEnabled}
<Tags>
<Tag icon="LockClosed">Premium</Tag>
</Tags>
{/if}
<Body>Configure your AI settings within this section:</Body>
</Layout>
<Divider />
<Layout noPadding>
<div class="config-heading">
<Heading size="S">AI Configurations</Heading>
<Button size="S" cta on:click={modal.show}>Add configuration</Button>
{#if !isCloud && !customAIConfigsEnabled}
<Tags>
<Tag icon="LockClosed">Premium</Tag>
</Tags>
{:else if isCloud && !customAIConfigsEnabled}
<Tags>
<Tag icon="LockClosed">Enterprise</Tag>
</Tags>
{:else}
<Button size="S" cta on:click={modal.show}>Add configuration</Button>
{/if}
</div>
<Body size="S">Use the following interface to select your preferred AI configuration.</Body>
<Body size="S">Select your AI Model:</Body>

View File

@ -22,6 +22,8 @@ export const createLicensingStore = () => {
backupsEnabled: false,
brandingEnabled: false,
scimEnabled: false,
budibaseAIEnabled: false,
customAIConfigsEnabled: false,
// the currently used quotas from the db
quotaUsage: undefined,
// derived quota metrics for percentages used
@ -142,6 +144,14 @@ export const createLicensingStore = () => {
Constants.Features.VIEW_READONLY_COLUMNS
)
const budibaseAIEnabled = license.features.includes(
Constants.Features.BUDIBASE_AI
)
const customAIConfigsEnabled = license.features.includes(
Constants.Features.AI_CUSTOM_CONFIGS
)
store.update(state => {
return {
...state,
@ -153,6 +163,8 @@ export const createLicensingStore = () => {
groupsEnabled,
backupsEnabled,
brandingEnabled,
budibaseAIEnabled,
customAIConfigsEnabled,
scimEnabled,
environmentVariablesEnabled,
auditLogsEnabled,

View File

@ -15,11 +15,12 @@ import {
} from "@budibase/types"
import env from "./../../../../environment"
export function getConfigParams(): DatabaseQueryOpts {
export function getConfigParams(type?: ConfigType): DatabaseQueryOpts {
const configType = type || ""
return {
include_docs: true,
startkey: `${DocumentType.CONFIG}${SEPARATOR}`,
endkey: `${DocumentType.CONFIG}${SEPARATOR}${UNICODE_MAX}`,
startkey: `${DocumentType.CONFIG}${SEPARATOR}${configType}`,
endkey: `${DocumentType.CONFIG}${SEPARATOR}${configType}${UNICODE_MAX}`,
}
}

View File

@ -111,6 +111,20 @@ export interface SCIMInnerConfig {
export interface SCIMConfig extends Config<SCIMInnerConfig> {}
export interface AIInnerConfig {
// TODO: should be enum
provider: string
isDefault: boolean
name: string
active: boolean
baseUrl: string
apiKey: string
// TODO: should be enum
defaultModel: string
}
export interface AIConfig extends Config<AIInnerConfig> {}
export const isSettingsConfig = (config: Config): config is SettingsConfig =>
config.type === ConfigType.SETTINGS
@ -126,6 +140,9 @@ export const isOIDCConfig = (config: Config): config is OIDCConfig =>
export const isSCIMConfig = (config: Config): config is SCIMConfig =>
config.type === ConfigType.SCIM
export const isAIConfig = (config: Config): config is AIConfig =>
config.type === ConfigType.AI
export enum ConfigType {
SETTINGS = "settings",
ACCOUNT = "account",
@ -134,4 +151,5 @@ export enum ConfigType {
OIDC = "oidc",
OIDC_LOGOS = "logos_oidc",
SCIM = "scim",
AI = "ai"
}

View File

@ -15,6 +15,8 @@ export enum Feature {
EXPANDED_PUBLIC_API = "expandedPublicApi",
VIEW_PERMISSIONS = "viewPermissions",
VIEW_READONLY_COLUMNS = "viewReadonlyColumns",
BUDIBASE_AI = "budibaseAI",
AI_CUSTOM_CONFIGS = "customAIConfigs"
}
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }

View File

@ -197,6 +197,12 @@ 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`
return true
}
export async function save(ctx: UserCtx<Config>) {
const body = ctx.request.body
const type = body.type
@ -224,6 +230,9 @@ export async function save(ctx: UserCtx<Config>) {
case ConfigType.OIDC:
await verifyOIDCConfig(config)
break
case ConfigType.AI:
await verifyAIConfig(config)
break
}
} catch (err: any) {
ctx.throw(400, err)
@ -314,6 +323,11 @@ export async function find(ctx: UserCtx) {
if (type === ConfigType.OIDC_LOGOS) {
enrichOIDCLogos(scopedConfig)
}
if (type === ConfigType.AI) {
// TODO: strip the keys from the configs here
// TODO: do the licensing checks here and return the right things based on the license
}
ctx.body = scopedConfig
} else {
// don't throw an error, there simply is nothing to return