From 76ae562bd2fb54746bf1bb37d865a813c227389a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 15 Apr 2025 14:32:37 +0100 Subject: [PATCH 01/18] Remove LLM options that aren't OpenAI. --- packages/pro | 2 +- .../server/src/api/routes/tests/ai.spec.ts | 189 ++++++++---------- .../src/tests/utilities/mocks/ai/anthropic.ts | 48 ----- packages/types/src/documents/global/config.ts | 8 +- 4 files changed, 88 insertions(+), 159 deletions(-) delete mode 100644 packages/server/src/tests/utilities/mocks/ai/anthropic.ts diff --git a/packages/pro b/packages/pro index 8bcb90edac..1104c58a8c 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 8bcb90edac3d44c48eaecf526cedc82418035438 +Subproject commit 1104c58a8c225b4dfc10998ef87ebdba347f7863 diff --git a/packages/server/src/api/routes/tests/ai.spec.ts b/packages/server/src/api/routes/tests/ai.spec.ts index 9e041a619e..58b538a2b1 100644 --- a/packages/server/src/api/routes/tests/ai.spec.ts +++ b/packages/server/src/api/routes/tests/ai.spec.ts @@ -13,8 +13,6 @@ import { } from "@budibase/types" import { context } from "@budibase/backend-core" 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" function dedent(str: string) { @@ -30,7 +28,6 @@ type SetupFn = ( interface TestSetup { name: string setup: SetupFn - mockLLMResponse: MockLLMResponseFn } function budibaseAI(): SetupFn { @@ -89,25 +86,14 @@ const allProviders: TestSetup[] = [ OPENAI_API_KEY: "test-key", }) }, - mockLLMResponse: mockChatGPTResponse, }, { name: "OpenAI API key with custom config", 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", setup: budibaseAI(), - mockLLMResponse: mockChatGPTResponse, }, ] @@ -126,56 +112,54 @@ describe("AI", () => { nock.cleanAll() }) - describe.each(allProviders)( - "provider: $name", - ({ setup, mockLLMResponse }: TestSetup) => { - let cleanup: () => Promise | void - beforeAll(async () => { - cleanup = await setup(config) + describe.each(allProviders)("provider: $name", ({ setup }: TestSetup) => { + let cleanup: () => Promise | void + beforeAll(async () => { + cleanup = await setup(config) + }) + + afterAll(async () => { + const maybePromise = cleanup() + if (maybePromise) { + await maybePromise + } + }) + + describe("POST /api/ai/js", () => { + let cleanup: () => void + beforeAll(() => { + cleanup = features.testutils.setFeatureFlags("*", { + AI_JS_GENERATION: true, + }) }) - afterAll(async () => { - const maybePromise = cleanup() - if (maybePromise) { - await maybePromise - } + afterAll(() => { + cleanup() }) - describe("POST /api/ai/js", () => { - let cleanup: () => void - beforeAll(() => { - cleanup = features.testutils.setFeatureFlags("*", { - AI_JS_GENERATION: true, - }) - }) + it("handles correct plain code response", async () => { + mockChatGPTResponse(`return 42`) - afterAll(() => { - cleanup() - }) + const { code } = await config.api.ai.generateJs({ prompt: "test" }) + expect(code).toBe("return 42") + }) - it("handles correct plain code response", async () => { - mockLLMResponse(`return 42`) - - const { code } = await config.api.ai.generateJs({ prompt: "test" }) - expect(code).toBe("return 42") - }) - - it("handles correct markdown code response", async () => { - mockLLMResponse( - dedent(` + it("handles correct markdown code response", async () => { + mockChatGPTResponse( + dedent(` \`\`\`js return 42 \`\`\` `) - ) + ) - const { code } = await config.api.ai.generateJs({ prompt: "test" }) - expect(code).toBe("return 42") - }) + const { code } = await config.api.ai.generateJs({ prompt: "test" }) + expect(code).toBe("return 42") + }) - it("handles multiple markdown code blocks returned", async () => { - mockLLMResponse( - dedent(` + it("handles multiple markdown code blocks returned", async () => { + mockChatGPTResponse( + dedent(` This: \`\`\`js @@ -188,63 +172,62 @@ describe("AI", () => { return 10 \`\`\` `) - ) + ) - const { code } = await config.api.ai.generateJs({ prompt: "test" }) - expect(code).toBe("return 42") - }) - - // TODO: handle when this happens - it.skip("handles no code response", async () => { - mockLLMResponse("I'm sorry, you're quite right, etc.") - const { code } = await config.api.ai.generateJs({ prompt: "test" }) - expect(code).toBe("") - }) - - it("handles LLM errors", async () => { - mockLLMResponse(() => { - throw new Error("LLM error") - }) - await config.api.ai.generateJs({ prompt: "test" }, { status: 500 }) - }) + const { code } = await config.api.ai.generateJs({ prompt: "test" }) + expect(code).toBe("return 42") }) - describe("POST /api/ai/cron", () => { - it("handles correct cron response", async () => { - mockLLMResponse("0 0 * * *") + // TODO: handle when this happens + it.skip("handles no code response", async () => { + mockChatGPTResponse("I'm sorry, you're quite right, etc.") + const { code } = await config.api.ai.generateJs({ prompt: "test" }) + expect(code).toBe("") + }) - const { message } = await config.api.ai.generateCron({ + it("handles LLM errors", async () => { + mockChatGPTResponse(() => { + throw new Error("LLM error") + }) + await config.api.ai.generateJs({ prompt: "test" }, { status: 500 }) + }) + }) + + describe("POST /api/ai/cron", () => { + it("handles correct cron response", async () => { + mockChatGPTResponse("0 0 * * *") + + const { message } = await config.api.ai.generateCron({ + prompt: "test", + }) + expect(message).toBe("0 0 * * *") + }) + + it("handles expected LLM error", async () => { + mockChatGPTResponse("Error generating cron: skill issue") + + await config.api.ai.generateCron( + { prompt: "test", - }) - expect(message).toBe("0 0 * * *") - }) - - it("handles expected LLM error", async () => { - mockLLMResponse("Error generating cron: skill issue") - - await config.api.ai.generateCron( - { - prompt: "test", - }, - { status: 400 } - ) - }) - - it("handles unexpected LLM error", async () => { - mockLLMResponse(() => { - throw new Error("LLM error") - }) - - await config.api.ai.generateCron( - { - prompt: "test", - }, - { status: 500 } - ) - }) + }, + { status: 400 } + ) }) - } - ) + + it("handles unexpected LLM error", async () => { + mockChatGPTResponse(() => { + throw new Error("LLM error") + }) + + await config.api.ai.generateCron( + { + prompt: "test", + }, + { status: 500 } + ) + }) + }) + }) }) describe("BudibaseAI", () => { diff --git a/packages/server/src/tests/utilities/mocks/ai/anthropic.ts b/packages/server/src/tests/utilities/mocks/ai/anthropic.ts deleted file mode 100644 index ff0413aee1..0000000000 --- a/packages/server/src/tests/utilities/mocks/ai/anthropic.ts +++ /dev/null @@ -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() -} diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index 422486e30f..3c311c5a86 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -111,13 +111,7 @@ export interface SCIMInnerConfig { export interface SCIMConfig extends Config {} -export type AIProvider = - | "OpenAI" - | "Anthropic" - | "AzureOpenAI" - | "TogetherAI" - | "Custom" - | "BudibaseAI" +export type AIProvider = "OpenAI" | "AzureOpenAI" | "BudibaseAI" export interface ProviderConfig { provider: AIProvider From 9272f3dc5f4aa4e208084cdfcbe608bd6d40ec47 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 15 Apr 2025 15:35:06 +0100 Subject: [PATCH 02/18] Update pro reference. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 1104c58a8c..f27612865c 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1104c58a8c225b4dfc10998ef87ebdba347f7863 +Subproject commit f27612865cd5f689b75b8f4e148293dff3b77bc4 From a12a85df8530c92c31b023381c50d378cdd2ca3f Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Tue, 15 Apr 2025 16:02:13 +0100 Subject: [PATCH 03/18] Typing of AI Configuration files --- .../portal/settings/ai/AIConfigTile.svelte | 12 ++--- .../builder/portal/settings/ai/index.svelte | 44 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte b/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte index fb040204c2..294f777186 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/AIConfigTile.svelte @@ -1,4 +1,4 @@ - 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 ae36fd5b98..e9b2058c74 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte @@ -1,4 +1,4 @@ -