Merge branch 'master' into cheeks-fixes
This commit is contained in:
commit
eb0ea6e878
|
@ -1,5 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionButton, Icon, notifications } from "@budibase/bbui"
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Icon,
|
||||||
|
notifications,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Body,
|
||||||
|
Link,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { auth, admin, licensing } from "@/stores/portal"
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import type { EnrichedBinding } from "@budibase/types"
|
import type { EnrichedBinding } from "@budibase/types"
|
||||||
|
@ -8,6 +19,8 @@
|
||||||
|
|
||||||
export let bindings: EnrichedBinding[] = []
|
export let bindings: EnrichedBinding[] = []
|
||||||
export let value: string | null = ""
|
export let value: string | null = ""
|
||||||
|
export let expandedOnly: boolean = false
|
||||||
|
|
||||||
export let parentWidth: number | null = null
|
export let parentWidth: number | null = null
|
||||||
export const dispatch = createEventDispatcher<{
|
export const dispatch = createEventDispatcher<{
|
||||||
update: { code: string }
|
update: { code: string }
|
||||||
|
@ -15,28 +28,27 @@
|
||||||
reject: { code: string | null }
|
reject: { code: string | null }
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let buttonContainer: HTMLElement
|
let promptInput: HTMLInputElement
|
||||||
let promptInput: HTMLTextAreaElement
|
|
||||||
let buttonElement: HTMLButtonElement
|
let buttonElement: HTMLButtonElement
|
||||||
let promptLoading = false
|
let promptLoading = false
|
||||||
let suggestedCode: string | null = null
|
let suggestedCode: string | null = null
|
||||||
let previousContents: string | null = null
|
let previousContents: string | null = null
|
||||||
let expanded = false
|
let expanded = false
|
||||||
let containerWidth = "auto"
|
let containerWidth = "auto"
|
||||||
let containerHeight = "40px"
|
|
||||||
let promptText = ""
|
let promptText = ""
|
||||||
let animateBorder = false
|
let animateBorder = false
|
||||||
|
let switchOnAIModal: Modal
|
||||||
|
let addCreditsModal: Modal
|
||||||
|
|
||||||
function adjustContainerHeight() {
|
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||||
if (promptInput && buttonElement) {
|
$: accountPortal = $admin.accountPortalUrl
|
||||||
promptInput.style.height = "0px"
|
$: aiEnabled = $auth?.user?.llm
|
||||||
const newHeight = Math.min(promptInput.scrollHeight, 100)
|
$: expanded = expandedOnly ? true : expanded
|
||||||
promptInput.style.height = `${newHeight}px`
|
$: creditsExceeded = $licensing.aiCreditsExceeded
|
||||||
containerHeight = `${Math.max(40, newHeight + 20)}px`
|
$: disabled = suggestedCode !== null || !aiEnabled || creditsExceeded
|
||||||
|
$: if (expandedOnly) {
|
||||||
|
containerWidth = calculateExpandedWidth()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$: if (promptInput && promptText) adjustContainerHeight()
|
|
||||||
|
|
||||||
async function generateJs(prompt: string) {
|
async function generateJs(prompt: string) {
|
||||||
if (!prompt.trim()) return
|
if (!prompt.trim()) return
|
||||||
|
@ -85,22 +97,23 @@
|
||||||
function resetExpand() {
|
function resetExpand() {
|
||||||
expanded = false
|
expanded = false
|
||||||
containerWidth = "auto"
|
containerWidth = "auto"
|
||||||
containerHeight = "40px"
|
|
||||||
promptText = ""
|
promptText = ""
|
||||||
suggestedCode = null
|
suggestedCode = null
|
||||||
previousContents = null
|
previousContents = null
|
||||||
animateBorder = false
|
animateBorder = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateExpandedWidth() {
|
||||||
|
return parentWidth
|
||||||
|
? `${Math.min(Math.max(parentWidth * 0.8, 300), 600)}px`
|
||||||
|
: "300px"
|
||||||
|
}
|
||||||
|
|
||||||
function toggleExpand() {
|
function toggleExpand() {
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
expanded = true
|
expanded = true
|
||||||
animateBorder = true
|
animateBorder = true
|
||||||
// Calculate width based on size of CodeEditor parent
|
containerWidth = calculateExpandedWidth()
|
||||||
containerWidth = parentWidth
|
|
||||||
? `${Math.min(Math.max(parentWidth * 0.8, 300), 600)}px`
|
|
||||||
: "300px"
|
|
||||||
containerHeight = "40px"
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
promptInput?.focus()
|
promptInput?.focus()
|
||||||
}, 250)
|
}, 250)
|
||||||
|
@ -125,11 +138,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
class="ai-gen-container"
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
style="--container-width: {containerWidth}; --container-height: {containerHeight}"
|
<div class="ai-gen-container" style="--container-width: {containerWidth}">
|
||||||
bind:this={buttonContainer}
|
|
||||||
>
|
|
||||||
{#if suggestedCode !== null}
|
{#if suggestedCode !== null}
|
||||||
<div class="floating-actions">
|
<div class="floating-actions">
|
||||||
<ActionButton size="S" icon="CheckmarkCircle" on:click={acceptSuggestion}>
|
<ActionButton size="S" icon="CheckmarkCircle" on:click={acceptSuggestion}>
|
||||||
|
@ -140,7 +151,6 @@
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
bind:this={buttonElement}
|
bind:this={buttonElement}
|
||||||
class="spectrum-ActionButton fade"
|
class="spectrum-ActionButton fade"
|
||||||
|
@ -149,18 +159,28 @@
|
||||||
on:click={!expanded ? toggleExpand : undefined}
|
on:click={!expanded ? toggleExpand : undefined}
|
||||||
>
|
>
|
||||||
<div class="button-content-wrapper">
|
<div class="button-content-wrapper">
|
||||||
<img src={BBAI} alt="AI" class="ai-icon" />
|
<img
|
||||||
|
src={BBAI}
|
||||||
|
alt="AI"
|
||||||
|
class="ai-icon"
|
||||||
|
class:disabled={expanded && disabled}
|
||||||
|
on:click={!expandedOnly
|
||||||
|
? e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
toggleExpand()
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
/>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<textarea
|
<input
|
||||||
|
type="text"
|
||||||
bind:this={promptInput}
|
bind:this={promptInput}
|
||||||
bind:value={promptText}
|
bind:value={promptText}
|
||||||
class="prompt-input"
|
class="prompt-input"
|
||||||
placeholder="Generate Javascript..."
|
placeholder="Generate Javascript..."
|
||||||
on:keydown={handleKeyPress}
|
on:keydown={handleKeyPress}
|
||||||
on:input={adjustContainerHeight}
|
{disabled}
|
||||||
disabled={suggestedCode !== null}
|
|
||||||
readonly={suggestedCode !== null}
|
readonly={suggestedCode !== null}
|
||||||
rows="1"
|
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="spectrum-ActionButton-label ai-gen-text">
|
<span class="spectrum-ActionButton-label ai-gen-text">
|
||||||
|
@ -168,9 +188,46 @@
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
|
{#if !aiEnabled}
|
||||||
|
<Button cta size="S" on:click={() => switchOnAIModal.show()}>
|
||||||
|
Switch on AI
|
||||||
|
</Button>
|
||||||
|
<Modal bind:this={switchOnAIModal}>
|
||||||
|
<ModalContent title="Switch on AI" showConfirmButton={false}>
|
||||||
|
<div class="enable-ai">
|
||||||
|
<p>To enable BB AI:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Add your Budibase license key:
|
||||||
|
<Link href={accountPortal}>Budibase account portal</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Go to the portal settings page, click AI and switch on BB AI
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
{:else if creditsExceeded}
|
||||||
|
<Button cta size="S" on:click={() => addCreditsModal.show()}>
|
||||||
|
Add AI credits
|
||||||
|
</Button>
|
||||||
|
<Modal bind:this={addCreditsModal}>
|
||||||
|
<ModalContent title="Add AI credits" showConfirmButton={false}>
|
||||||
|
<Body size="S">
|
||||||
|
{#if accountPortalAccess}
|
||||||
|
<Link href={"https://budibase.com/contact/"}
|
||||||
|
>Contact sales</Link
|
||||||
|
> to unlock additional BB AI credits
|
||||||
|
{:else}
|
||||||
|
Contact your account holder to unlock additional BB AI credits
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
{:else}
|
||||||
<Icon
|
<Icon
|
||||||
color={promptLoading
|
color={promptLoading
|
||||||
? "#6E56FF"
|
? "#6E56FF"
|
||||||
|
@ -181,16 +238,7 @@
|
||||||
name={promptLoading ? "StopCircle" : "PlayCircle"}
|
name={promptLoading ? "StopCircle" : "PlayCircle"}
|
||||||
on:click={() => generateJs(promptText)}
|
on:click={() => generateJs(promptText)}
|
||||||
/>
|
/>
|
||||||
<Icon
|
{/if}
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
name="Close"
|
|
||||||
hoverColor="#6E56FF"
|
|
||||||
on:click={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (!suggestedCode && !promptLoading) toggleExpand()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
@ -198,13 +246,12 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ai-gen-container {
|
.ai-gen-container {
|
||||||
|
height: 40px;
|
||||||
--container-width: auto;
|
--container-width: auto;
|
||||||
--container-height: 40px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
width: var(--container-width);
|
width: var(--container-width);
|
||||||
height: var(--container-height);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
@ -238,7 +285,7 @@
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
125deg,
|
125deg,
|
||||||
transparent -10%,
|
transparent -10%,
|
||||||
#6e56ff 5%,
|
#6e56ff 2%,
|
||||||
#9f8fff 15%,
|
#9f8fff 15%,
|
||||||
#9f8fff 25%,
|
#9f8fff 25%,
|
||||||
transparent 35%,
|
transparent 35%,
|
||||||
|
@ -320,6 +367,7 @@
|
||||||
height: 18px;
|
height: 18px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
cursor: var(--ai-icon-cursor, pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-gen-text {
|
.ai-gen-text {
|
||||||
|
@ -335,18 +383,16 @@
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: var(--font-size-s);
|
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
color: var(--spectrum-alias-text-color);
|
color: var(--spectrum-alias-text-color);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
resize: none;
|
resize: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 1.2;
|
|
||||||
min-height: 10px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input::placeholder {
|
.prompt-input::placeholder {
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
|
@ -354,6 +400,7 @@
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-content-wrapper {
|
.button-content-wrapper {
|
||||||
|
@ -366,4 +413,15 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-right: var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt-input:disabled,
|
||||||
|
.prompt-input[readonly] {
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-icon.disabled {
|
||||||
|
filter: grayscale(1) brightness(1.5);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -469,6 +469,7 @@
|
||||||
|
|
||||||
{#if aiGenEnabled}
|
{#if aiGenEnabled}
|
||||||
<AIGen
|
<AIGen
|
||||||
|
expandedOnly={true}
|
||||||
{bindings}
|
{bindings}
|
||||||
{value}
|
{value}
|
||||||
parentWidth={editorWidth}
|
parentWidth={editorWidth}
|
||||||
|
|
|
@ -56,7 +56,9 @@ interface LicensingState {
|
||||||
// user limits
|
// user limits
|
||||||
userCount?: number
|
userCount?: number
|
||||||
userLimit?: number
|
userLimit?: number
|
||||||
|
aiCreditsLimit?: number
|
||||||
userLimitReached: boolean
|
userLimitReached: boolean
|
||||||
|
aiCreditsExceeded: boolean
|
||||||
errUserLimit: boolean
|
errUserLimit: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +104,8 @@ class LicensingStore extends BudiStore<LicensingState> {
|
||||||
userLimit: undefined,
|
userLimit: undefined,
|
||||||
userLimitReached: false,
|
userLimitReached: false,
|
||||||
errUserLimit: false,
|
errUserLimit: false,
|
||||||
|
// AI Limits
|
||||||
|
aiCreditsExceeded: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +123,16 @@ class LicensingStore extends BudiStore<LicensingState> {
|
||||||
return userCount > userLimit
|
return userCount > userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aiCreditsExceeded(
|
||||||
|
aiCredits: number,
|
||||||
|
aiCreditsLimit = get(this.store).aiCreditsLimit
|
||||||
|
) {
|
||||||
|
if (aiCreditsLimit === UNLIMITED || aiCreditsLimit === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return aiCredits > aiCreditsLimit
|
||||||
|
}
|
||||||
|
|
||||||
async isCloud() {
|
async isCloud() {
|
||||||
let adminStore = get(admin)
|
let adminStore = get(admin)
|
||||||
if (!adminStore.loaded) {
|
if (!adminStore.loaded) {
|
||||||
|
@ -291,9 +305,15 @@ class LicensingStore extends BudiStore<LicensingState> {
|
||||||
|
|
||||||
const userQuota = license.quotas.usage.static.users
|
const userQuota = license.quotas.usage.static.users
|
||||||
const userLimit = userQuota.value
|
const userLimit = userQuota.value
|
||||||
|
const aiCreditsQuota = license.quotas.usage.monthly.budibaseAICredits
|
||||||
|
const aiCreditsLimit = aiCreditsQuota.value
|
||||||
const userCount = usage.usageQuota.users
|
const userCount = usage.usageQuota.users
|
||||||
const userLimitReached = this.usersLimitReached(userCount, userLimit)
|
const userLimitReached = this.usersLimitReached(userCount, userLimit)
|
||||||
const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
|
const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
|
||||||
|
const aiCreditsExceeded = this.aiCreditsExceeded(
|
||||||
|
usage.monthly.current.budibaseAICredits,
|
||||||
|
aiCreditsLimit
|
||||||
|
)
|
||||||
const isCloudAccount = await this.isCloud()
|
const isCloudAccount = await this.isCloud()
|
||||||
const errUserLimit =
|
const errUserLimit =
|
||||||
isCloudAccount &&
|
isCloudAccount &&
|
||||||
|
@ -315,6 +335,8 @@ class LicensingStore extends BudiStore<LicensingState> {
|
||||||
userLimit,
|
userLimit,
|
||||||
userLimitReached,
|
userLimitReached,
|
||||||
errUserLimit,
|
errUserLimit,
|
||||||
|
aiCreditsLimit,
|
||||||
|
aiCreditsExceeded,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8bcb90edac3d44c48eaecf526cedc82418035438
|
Subproject commit f27612865cd5f689b75b8f4e148293dff3b77bc4
|
|
@ -13,8 +13,6 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { mocks } from "@budibase/backend-core/tests"
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
import { MockLLMResponseFn } from "../../../tests/utilities/mocks/ai"
|
|
||||||
import { mockAnthropicResponse } from "../../../tests/utilities/mocks/ai/anthropic"
|
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
|
|
||||||
function dedent(str: string) {
|
function dedent(str: string) {
|
||||||
|
@ -30,7 +28,6 @@ type SetupFn = (
|
||||||
interface TestSetup {
|
interface TestSetup {
|
||||||
name: string
|
name: string
|
||||||
setup: SetupFn
|
setup: SetupFn
|
||||||
mockLLMResponse: MockLLMResponseFn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function budibaseAI(): SetupFn {
|
function budibaseAI(): SetupFn {
|
||||||
|
@ -89,25 +86,14 @@ const allProviders: TestSetup[] = [
|
||||||
OPENAI_API_KEY: "test-key",
|
OPENAI_API_KEY: "test-key",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mockLLMResponse: mockChatGPTResponse,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OpenAI API key with custom config",
|
name: "OpenAI API key with custom config",
|
||||||
setup: customAIConfig({ provider: "OpenAI", defaultModel: "gpt-4o-mini" }),
|
setup: customAIConfig({ provider: "OpenAI", defaultModel: "gpt-4o-mini" }),
|
||||||
mockLLMResponse: mockChatGPTResponse,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Anthropic API key with custom config",
|
|
||||||
setup: customAIConfig({
|
|
||||||
provider: "Anthropic",
|
|
||||||
defaultModel: "claude-3-5-sonnet-20240620",
|
|
||||||
}),
|
|
||||||
mockLLMResponse: mockAnthropicResponse,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "BudibaseAI",
|
name: "BudibaseAI",
|
||||||
setup: budibaseAI(),
|
setup: budibaseAI(),
|
||||||
mockLLMResponse: mockChatGPTResponse,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -126,9 +112,7 @@ describe("AI", () => {
|
||||||
nock.cleanAll()
|
nock.cleanAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.each(allProviders)(
|
describe.each(allProviders)("provider: $name", ({ setup }: TestSetup) => {
|
||||||
"provider: $name",
|
|
||||||
({ setup, mockLLMResponse }: TestSetup) => {
|
|
||||||
let cleanup: () => Promise<void> | void
|
let cleanup: () => Promise<void> | void
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
cleanup = await setup(config)
|
cleanup = await setup(config)
|
||||||
|
@ -154,14 +138,14 @@ describe("AI", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles correct plain code response", async () => {
|
it("handles correct plain code response", async () => {
|
||||||
mockLLMResponse(`return 42`)
|
mockChatGPTResponse(`return 42`)
|
||||||
|
|
||||||
const { code } = await config.api.ai.generateJs({ prompt: "test" })
|
const { code } = await config.api.ai.generateJs({ prompt: "test" })
|
||||||
expect(code).toBe("return 42")
|
expect(code).toBe("return 42")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles correct markdown code response", async () => {
|
it("handles correct markdown code response", async () => {
|
||||||
mockLLMResponse(
|
mockChatGPTResponse(
|
||||||
dedent(`
|
dedent(`
|
||||||
\`\`\`js
|
\`\`\`js
|
||||||
return 42
|
return 42
|
||||||
|
@ -174,7 +158,7 @@ describe("AI", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles multiple markdown code blocks returned", async () => {
|
it("handles multiple markdown code blocks returned", async () => {
|
||||||
mockLLMResponse(
|
mockChatGPTResponse(
|
||||||
dedent(`
|
dedent(`
|
||||||
This:
|
This:
|
||||||
|
|
||||||
|
@ -196,13 +180,13 @@ describe("AI", () => {
|
||||||
|
|
||||||
// TODO: handle when this happens
|
// TODO: handle when this happens
|
||||||
it.skip("handles no code response", async () => {
|
it.skip("handles no code response", async () => {
|
||||||
mockLLMResponse("I'm sorry, you're quite right, etc.")
|
mockChatGPTResponse("I'm sorry, you're quite right, etc.")
|
||||||
const { code } = await config.api.ai.generateJs({ prompt: "test" })
|
const { code } = await config.api.ai.generateJs({ prompt: "test" })
|
||||||
expect(code).toBe("")
|
expect(code).toBe("")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles LLM errors", async () => {
|
it("handles LLM errors", async () => {
|
||||||
mockLLMResponse(() => {
|
mockChatGPTResponse(() => {
|
||||||
throw new Error("LLM error")
|
throw new Error("LLM error")
|
||||||
})
|
})
|
||||||
await config.api.ai.generateJs({ prompt: "test" }, { status: 500 })
|
await config.api.ai.generateJs({ prompt: "test" }, { status: 500 })
|
||||||
|
@ -211,7 +195,7 @@ describe("AI", () => {
|
||||||
|
|
||||||
describe("POST /api/ai/cron", () => {
|
describe("POST /api/ai/cron", () => {
|
||||||
it("handles correct cron response", async () => {
|
it("handles correct cron response", async () => {
|
||||||
mockLLMResponse("0 0 * * *")
|
mockChatGPTResponse("0 0 * * *")
|
||||||
|
|
||||||
const { message } = await config.api.ai.generateCron({
|
const { message } = await config.api.ai.generateCron({
|
||||||
prompt: "test",
|
prompt: "test",
|
||||||
|
@ -220,7 +204,7 @@ describe("AI", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles expected LLM error", async () => {
|
it("handles expected LLM error", async () => {
|
||||||
mockLLMResponse("Error generating cron: skill issue")
|
mockChatGPTResponse("Error generating cron: skill issue")
|
||||||
|
|
||||||
await config.api.ai.generateCron(
|
await config.api.ai.generateCron(
|
||||||
{
|
{
|
||||||
|
@ -231,7 +215,7 @@ describe("AI", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles unexpected LLM error", async () => {
|
it("handles unexpected LLM error", async () => {
|
||||||
mockLLMResponse(() => {
|
mockChatGPTResponse(() => {
|
||||||
throw new Error("LLM error")
|
throw new Error("LLM error")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -243,8 +227,7 @@ describe("AI", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("BudibaseAI", () => {
|
describe("BudibaseAI", () => {
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import AnthropicClient from "@anthropic-ai/sdk"
|
|
||||||
import nock from "nock"
|
|
||||||
import { MockLLMResponseFn, MockLLMResponseOpts } from "."
|
|
||||||
|
|
||||||
let chatID = 1
|
|
||||||
const SPACE_REGEX = /\s+/g
|
|
||||||
|
|
||||||
export const mockAnthropicResponse: MockLLMResponseFn = (
|
|
||||||
answer: string | ((prompt: string) => string),
|
|
||||||
opts?: MockLLMResponseOpts
|
|
||||||
) => {
|
|
||||||
return nock(opts?.host || "https://api.anthropic.com")
|
|
||||||
.post("/v1/messages")
|
|
||||||
.reply((uri: string, body: nock.Body) => {
|
|
||||||
const req = body as AnthropicClient.MessageCreateParamsNonStreaming
|
|
||||||
const prompt = req.messages[0].content
|
|
||||||
if (typeof prompt !== "string") {
|
|
||||||
throw new Error("Anthropic mock only supports string prompts")
|
|
||||||
}
|
|
||||||
|
|
||||||
let content
|
|
||||||
if (typeof answer === "function") {
|
|
||||||
try {
|
|
||||||
content = answer(prompt)
|
|
||||||
} catch (e) {
|
|
||||||
return [500, "Internal Server Error"]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content = answer
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp: AnthropicClient.Messages.Message = {
|
|
||||||
id: `${chatID++}`,
|
|
||||||
type: "message",
|
|
||||||
role: "assistant",
|
|
||||||
model: req.model,
|
|
||||||
stop_reason: "end_turn",
|
|
||||||
usage: {
|
|
||||||
input_tokens: prompt.split(SPACE_REGEX).length,
|
|
||||||
output_tokens: content.split(SPACE_REGEX).length,
|
|
||||||
},
|
|
||||||
stop_sequence: null,
|
|
||||||
content: [{ type: "text", text: content }],
|
|
||||||
}
|
|
||||||
return [200, resp]
|
|
||||||
})
|
|
||||||
.persist()
|
|
||||||
}
|
|
|
@ -111,13 +111,7 @@ export interface SCIMInnerConfig {
|
||||||
|
|
||||||
export interface SCIMConfig extends Config<SCIMInnerConfig> {}
|
export interface SCIMConfig extends Config<SCIMInnerConfig> {}
|
||||||
|
|
||||||
export type AIProvider =
|
export type AIProvider = "OpenAI" | "AzureOpenAI" | "BudibaseAI"
|
||||||
| "OpenAI"
|
|
||||||
| "Anthropic"
|
|
||||||
| "AzureOpenAI"
|
|
||||||
| "TogetherAI"
|
|
||||||
| "Custom"
|
|
||||||
| "BudibaseAI"
|
|
||||||
|
|
||||||
export interface ProviderConfig {
|
export interface ProviderConfig {
|
||||||
provider: AIProvider
|
provider: AIProvider
|
||||||
|
|
Loading…
Reference in New Issue