rough pass on AI platform CRUD
This commit is contained in:
parent
1a57e37d38
commit
99035ad8ae
|
@ -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: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue