rough pass on AI platform CRUD
This commit is contained in:
parent
1a57e37d38
commit
99035ad8ae
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AIConfig, AIInnerConfig,
|
||||||
Config,
|
Config,
|
||||||
ConfigType,
|
ConfigType,
|
||||||
GoogleConfig,
|
GoogleConfig,
|
||||||
|
@ -17,6 +18,7 @@ import { DocumentType, SEPARATOR } from "../constants"
|
||||||
import { CacheKey, TTL, withCache } from "../cache"
|
import { CacheKey, TTL, withCache } from "../cache"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { getConfigParams } from "@budibase/server/src/migrations/functions/backfill/global/configs"
|
||||||
|
|
||||||
// UTILS
|
// UTILS
|
||||||
|
|
||||||
|
@ -254,3 +256,42 @@ export async function getSCIMConfig(): Promise<SCIMInnerConfig | undefined> {
|
||||||
const config = await getConfig<SCIMConfig>(ConfigType.SCIM)
|
const config = await getConfig<SCIMConfig>(ConfigType.SCIM)
|
||||||
return config?.config
|
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: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,15 +5,14 @@
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
Toggle,
|
||||||
Dropzone,
|
|
||||||
Body,
|
Body,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import { ConfigMap } from "./constants"
|
||||||
import { plugins } from "stores/portal"
|
import { API } from "api"
|
||||||
import { PluginSource } from "constants"
|
|
||||||
|
|
||||||
const options = [
|
// TODO: Update these
|
||||||
|
const providers = [
|
||||||
"OpenAI",
|
"OpenAI",
|
||||||
"Anthropic",
|
"Anthropic",
|
||||||
"Together AI",
|
"Together AI",
|
||||||
|
@ -21,21 +20,69 @@
|
||||||
"Custom"
|
"Custom"
|
||||||
]
|
]
|
||||||
|
|
||||||
let source = PluginSource.GITHUB
|
const models = [
|
||||||
let dynamicValues = {}
|
"gpt4o-mini",
|
||||||
|
]
|
||||||
|
|
||||||
let name
|
let formValues = {}
|
||||||
let baseUrl
|
|
||||||
let apiKey
|
|
||||||
let active
|
|
||||||
let isDefault
|
|
||||||
|
|
||||||
let validation
|
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>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
confirmText={"Save"}
|
confirmText={"Save"}
|
||||||
onConfirm={() => {}}
|
cancelText={"Delete"}
|
||||||
|
onConfirm={saveConfig}
|
||||||
|
onCancel={deleteConfig}
|
||||||
disabled={!validation}
|
disabled={!validation}
|
||||||
size="M"
|
size="M"
|
||||||
title="Custom AI Configuration"
|
title="Custom AI Configuration"
|
||||||
|
@ -43,39 +90,40 @@
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="M">Provider</Label>
|
<Label size="M">Provider</Label>
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
bind:value={source}
|
bind:value={formValues.provider}
|
||||||
options={options}
|
options={providers}
|
||||||
|
on:change={prefillConfig}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="M">Model</Label>
|
<Label size="M">Model</Label>
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
bind:value={source}
|
bind:value={formValues.model}
|
||||||
options={Object.values(PluginSource)}
|
options={models}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="M">Name</Label>
|
<Label size="M">Name</Label>
|
||||||
<Input placeholder={"Test 1"} bind:value={name} />
|
<Input placeholder={"Test 1"} bind:value={formValues.name}/>
|
||||||
</div>
|
</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={baseUrl} />
|
<Input placeholder={"www.google.com"} bind:value={formValues.baseUrl}/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label size="M">API Key</Label>
|
<Label size="M">API Key</Label>
|
||||||
<Input bind:value={apiKey} />
|
<Input bind:value={formValues.apiKey}/>
|
||||||
</div>
|
</div>
|
||||||
<Toggle text="Active" bind:value={active} />
|
<Toggle text="Active" bind:value={formValues.active}/>
|
||||||
<Toggle text="Set as default" bind:value={isDefault} />
|
<Toggle text="Set as default" bind:value={formValues.isDefault}/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.form-row {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-s);
|
grid-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const ConfigMap = {
|
||||||
|
OpenAI: {
|
||||||
|
baseUrl: "https://api.openai.com"
|
||||||
|
},
|
||||||
|
Anthropic: {
|
||||||
|
baseUrl: "https://api.together.xyz/v1"
|
||||||
|
},
|
||||||
|
}
|
|
@ -8,57 +8,76 @@
|
||||||
Divider,
|
Divider,
|
||||||
notifications,
|
notifications,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
Tags,
|
||||||
|
Tag
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth, admin } from "stores/portal"
|
import { admin, licensing } from "stores/portal"
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import AIConfigModal from "./ConfigModal.svelte"
|
import AIConfigModal from "./ConfigModal.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let diagnosticInfo = ""
|
const ConfigTypes = {
|
||||||
let modal
|
AI: "ai",
|
||||||
|
}
|
||||||
|
|
||||||
// async function fetchSystemDebugInfo() {
|
let modal
|
||||||
// const diagnostics = await API.fetchSystemDebugInfo()
|
let aiConfig
|
||||||
// diagnosticInfo = {
|
let loading = false
|
||||||
// browser: {
|
|
||||||
// language: navigator.language || navigator.userLanguage,
|
$: isCloud = $admin.cloud
|
||||||
// userAgent: navigator.userAgent,
|
$: budibaseAIEnabled = $licensing.budibaseAIEnabled
|
||||||
// platform: navigator.platform,
|
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
|
||||||
// vendor: navigator.vendor,
|
|
||||||
// },
|
async function fetchAIConfig() {
|
||||||
// server: diagnostics,
|
loading = true
|
||||||
// }
|
try {
|
||||||
// }
|
// Fetch the configs for smtp
|
||||||
//
|
const aiDoc = await API.getConfig(ConfigTypes.AI)
|
||||||
// const copyToClipboard = async () => {
|
if (aiDoc._id) {
|
||||||
// await Helpers.copyToClipboard(JSON.stringify(diagnosticInfo, undefined, 2))
|
aiConfig = aiDoc
|
||||||
// notifications.success("Copied")
|
}
|
||||||
// }
|
loading = false
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error fetching AI config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// await fetchSystemDebugInfo()
|
await fetchAIConfig()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<AIConfigModal />
|
<AIConfigModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="M">AI</Heading>
|
<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>
|
<Body>Configure your AI settings within this section:</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<div class="config-heading">
|
<div class="config-heading">
|
||||||
<Heading size="S">AI Configurations</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>
|
</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>
|
||||||
|
|
|
@ -22,6 +22,8 @@ export const createLicensingStore = () => {
|
||||||
backupsEnabled: false,
|
backupsEnabled: false,
|
||||||
brandingEnabled: false,
|
brandingEnabled: false,
|
||||||
scimEnabled: false,
|
scimEnabled: false,
|
||||||
|
budibaseAIEnabled: false,
|
||||||
|
customAIConfigsEnabled: false,
|
||||||
// the currently used quotas from the db
|
// the currently used quotas from the db
|
||||||
quotaUsage: undefined,
|
quotaUsage: undefined,
|
||||||
// derived quota metrics for percentages used
|
// derived quota metrics for percentages used
|
||||||
|
@ -142,6 +144,14 @@ export const createLicensingStore = () => {
|
||||||
Constants.Features.VIEW_READONLY_COLUMNS
|
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 => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -153,6 +163,8 @@ export const createLicensingStore = () => {
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
brandingEnabled,
|
brandingEnabled,
|
||||||
|
budibaseAIEnabled,
|
||||||
|
customAIConfigsEnabled,
|
||||||
scimEnabled,
|
scimEnabled,
|
||||||
environmentVariablesEnabled,
|
environmentVariablesEnabled,
|
||||||
auditLogsEnabled,
|
auditLogsEnabled,
|
||||||
|
|
|
@ -15,11 +15,12 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import env from "./../../../../environment"
|
import env from "./../../../../environment"
|
||||||
|
|
||||||
export function getConfigParams(): DatabaseQueryOpts {
|
export function getConfigParams(type?: ConfigType): DatabaseQueryOpts {
|
||||||
|
const configType = type || ""
|
||||||
return {
|
return {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
startkey: `${DocumentType.CONFIG}${SEPARATOR}`,
|
startkey: `${DocumentType.CONFIG}${SEPARATOR}${configType}`,
|
||||||
endkey: `${DocumentType.CONFIG}${SEPARATOR}${UNICODE_MAX}`,
|
endkey: `${DocumentType.CONFIG}${SEPARATOR}${configType}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,20 @@ export interface SCIMInnerConfig {
|
||||||
|
|
||||||
export interface SCIMConfig extends Config<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 =>
|
export const isSettingsConfig = (config: Config): config is SettingsConfig =>
|
||||||
config.type === ConfigType.SETTINGS
|
config.type === ConfigType.SETTINGS
|
||||||
|
|
||||||
|
@ -126,6 +140,9 @@ export const isOIDCConfig = (config: Config): config is OIDCConfig =>
|
||||||
export const isSCIMConfig = (config: Config): config is SCIMConfig =>
|
export const isSCIMConfig = (config: Config): config is SCIMConfig =>
|
||||||
config.type === ConfigType.SCIM
|
config.type === ConfigType.SCIM
|
||||||
|
|
||||||
|
export const isAIConfig = (config: Config): config is AIConfig =>
|
||||||
|
config.type === ConfigType.AI
|
||||||
|
|
||||||
export enum ConfigType {
|
export enum ConfigType {
|
||||||
SETTINGS = "settings",
|
SETTINGS = "settings",
|
||||||
ACCOUNT = "account",
|
ACCOUNT = "account",
|
||||||
|
@ -134,4 +151,5 @@ export enum ConfigType {
|
||||||
OIDC = "oidc",
|
OIDC = "oidc",
|
||||||
OIDC_LOGOS = "logos_oidc",
|
OIDC_LOGOS = "logos_oidc",
|
||||||
SCIM = "scim",
|
SCIM = "scim",
|
||||||
|
AI = "ai"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ export enum Feature {
|
||||||
EXPANDED_PUBLIC_API = "expandedPublicApi",
|
EXPANDED_PUBLIC_API = "expandedPublicApi",
|
||||||
VIEW_PERMISSIONS = "viewPermissions",
|
VIEW_PERMISSIONS = "viewPermissions",
|
||||||
VIEW_READONLY_COLUMNS = "viewReadonlyColumns",
|
VIEW_READONLY_COLUMNS = "viewReadonlyColumns",
|
||||||
|
BUDIBASE_AI = "budibaseAI",
|
||||||
|
AI_CUSTOM_CONFIGS = "customAIConfigs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
||||||
|
|
|
@ -197,6 +197,12 @@ async function verifyOIDCConfig(config: OIDCConfigs) {
|
||||||
await verifySSOConfig(ConfigType.OIDC, config.configs[0])
|
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>) {
|
export async function save(ctx: UserCtx<Config>) {
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
const type = body.type
|
const type = body.type
|
||||||
|
@ -224,6 +230,9 @@ export async function save(ctx: UserCtx<Config>) {
|
||||||
case ConfigType.OIDC:
|
case ConfigType.OIDC:
|
||||||
await verifyOIDCConfig(config)
|
await verifyOIDCConfig(config)
|
||||||
break
|
break
|
||||||
|
case ConfigType.AI:
|
||||||
|
await verifyAIConfig(config)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
|
@ -314,6 +323,11 @@ export async function find(ctx: UserCtx) {
|
||||||
if (type === ConfigType.OIDC_LOGOS) {
|
if (type === ConfigType.OIDC_LOGOS) {
|
||||||
enrichOIDCLogos(scopedConfig)
|
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
|
ctx.body = scopedConfig
|
||||||
} else {
|
} else {
|
||||||
// don't throw an error, there simply is nothing to return
|
// don't throw an error, there simply is nothing to return
|
||||||
|
|
Loading…
Reference in New Issue