diff --git a/packages/backend-core/src/configs/configs.ts b/packages/backend-core/src/configs/configs.ts index 0d189e3f7d..6062d3624f 100644 --- a/packages/backend-core/src/configs/configs.ts +++ b/packages/backend-core/src/configs/configs.ts @@ -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 { const config = await getConfig(ConfigType.SCIM) return config?.config } + + +// AI + +// TODO: Can we assume that you are licensed when you hit this endpoint? +export async function getAIConfig(): Promise { + 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(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: "" + } + } +} diff --git a/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte b/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte index 408e71bd1f..30830cff1a 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte @@ -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"}` + ) + } + } + {}} + cancelText={"Delete"} + onConfirm={saveConfig} + onCancel={deleteConfig} disabled={!validation} size="M" title="Custom AI Configuration" @@ -43,39 +90,40 @@
- +
- +
- +
- - + +
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/ai.spec.ts b/packages/builder/src/pages/builder/portal/settings/ai/ai.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/builder/src/pages/builder/portal/settings/ai/constants.ts b/packages/builder/src/pages/builder/portal/settings/ai/constants.ts new file mode 100644 index 0000000000..df5508109d --- /dev/null +++ b/packages/builder/src/pages/builder/portal/settings/ai/constants.ts @@ -0,0 +1,8 @@ +export const ConfigMap = { + OpenAI: { + baseUrl: "https://api.openai.com" + }, + Anthropic: { + baseUrl: "https://api.together.xyz/v1" + }, +} \ No newline at end of file diff --git a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte index e40a6cf4ff..427b09a7f2 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte @@ -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() }) - AI + {#if isCloud && !budibaseAIEnabled} + + Premium + + {/if} Configure your AI settings within this section:
AI Configurations - + {#if !isCloud && !customAIConfigsEnabled} + + Premium + + {:else if isCloud && !customAIConfigsEnabled} + + Enterprise + + {:else} + + {/if}
Use the following interface to select your preferred AI configuration. Select your AI Model: diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index 0e44650479..d48377207c 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -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, diff --git a/packages/server/src/migrations/functions/backfill/global/configs.ts b/packages/server/src/migrations/functions/backfill/global/configs.ts index 04eb9caff2..bb0781eba3 100644 --- a/packages/server/src/migrations/functions/backfill/global/configs.ts +++ b/packages/server/src/migrations/functions/backfill/global/configs.ts @@ -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}`, } } diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index 3fd352aada..246874d0f1 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -111,6 +111,20 @@ export interface SCIMInnerConfig { export interface SCIMConfig extends Config {} +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 {} + 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" } diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 0d8db8258e..bf2bcb72b6 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -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 } diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts index a44b173869..43aac19b18 100644 --- a/packages/worker/src/api/controllers/global/configs.ts +++ b/packages/worker/src/api/controllers/global/configs.ts @@ -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) { const body = ctx.request.body const type = body.type @@ -224,6 +230,9 @@ export async function save(ctx: UserCtx) { 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