diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index aee099e10a..e2fd975e40 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -28,6 +28,7 @@ export enum Config { OIDC = "oidc", OIDC_LOGOS = "logos_oidc", SCIM = "scim", + AI = "AI", } export const MIN_VALID_DATE = new Date(-2147483647000) diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index bc9a3b635c..5ba6fb36a1 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -102,6 +102,14 @@ export const useAppBuilders = () => { return useFeature(Feature.APP_BUILDERS) } +export const useBudibaseAI = () => { + return useFeature(Feature.BUDIBASE_AI) +} + +export const useAICustomConfigs = () => { + return useFeature(Feature.AI_CUSTOM_CONFIGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte index 57a57518df..ba023cf2ca 100644 --- a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte +++ b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte @@ -26,7 +26,9 @@ $: style = makeStyle($menu) $: isNewRow = $focusedRowId === NewRowID - $: budibaseAIEnabled = $config.licensing?.budibaseAIEnabled || $config.licensing?.customAIConfigsEnabled + $: budibaseAIEnabled = + $config.licensing?.budibaseAIEnabled || + $config.licensing?.customAIConfigsEnabled const makeStyle = menu => { return `left:${menu.left}px; top:${menu.top}px;` diff --git a/packages/pro b/packages/pro index fc4c7f4925..a55a58fc96 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit fc4c7f4925139af078480217965c3d6338dc0a7f +Subproject commit a55a58fc96291ae021e61ad369a2077992fddfcd diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 5222069460..843abd43c0 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -1,10 +1,6 @@ import * as setup from "./utilities" -import { - DatabaseName, - getDatasource, - knexClient, -} from "../../../integrations/tests/utils" +import { DatabaseName, knexClient } from "../../../integrations/tests/utils" import tk from "timekeeper" import emitter from "../../../../src/events" @@ -12,36 +8,37 @@ import { outputProcessing } from "../../../utilities/rowProcessor" import { context, InternalTable, + setEnv as setCoreEnv, tenancy, withEnv as withCoreEnv, - setEnv as setCoreEnv, } from "@budibase/backend-core" import { quotas } from "@budibase/pro" import { + AIOperationEnum, AttachmentFieldMetadata, AutoFieldSubType, + BBReferenceFieldSubType, Datasource, DateFieldMetadata, DeleteRow, + FeatureFlag, FieldSchema, FieldType, - BBReferenceFieldSubType, FormulaType, INTERNAL_TABLE_SOURCE_ID, + JsonFieldSubType, NumberFieldMetadata, QuotaUsageType, + RelationSchemaField, RelationshipType, Row, + RowExportFormat, SaveTableRequest, StaticQuotaName, Table, + TableSchema, TableSourceType, UpdatedRowEventEmitter, - TableSchema, - JsonFieldSubType, - RowExportFormat, - FeatureFlag, - RelationSchemaField, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import _, { merge } from "lodash" @@ -51,6 +48,18 @@ import { InternalTables } from "../../../db/utils" import { withEnv } from "../../../environment" import { JsTimeoutError } from "@budibase/string-templates" +jest.mock("@budibase/pro", () => ({ + ...jest.requireActual("@budibase/pro"), + ai: { + LargeLanguageModel: { + forCurrentTenant: async () => ({ + run: jest.fn(() => `Mock LLM Response`), + buildPromptFromAIOperation: jest.fn(), + }), + }, + }, +})) + const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() tk.freeze(timestamp) interface WaitOptions { @@ -77,13 +86,13 @@ async function waitForEvent( } describe.each([ - ["lucene", undefined], + // ["lucene", undefined], ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], + // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + // [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], ])("/rows (%s)", (providerType, dsProvider) => { const isInternal = dsProvider === undefined const isLucene = providerType === "lucene" @@ -1986,6 +1995,7 @@ describe.each([ [FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(), [FieldType.FORMULA]: undefined, // generated field [FieldType.AUTO]: undefined, // generated field + [FieldType.AI]: undefined, // generated field [FieldType.JSON]: { name: generator.guid() }, [FieldType.INTERNAL]: generator.guid(), [FieldType.BARCODEQR]: generator.guid(), @@ -2016,6 +2026,7 @@ describe.each([ url: expect.any(String), }), [FieldType.FORMULA]: fullSchema[FieldType.FORMULA].formula, + [FieldType.AI]: fullSchema[FieldType.AI].prompt, [FieldType.AUTO]: expect.any(Number), [FieldType.JSON]: rowValues[FieldType.JSON], [FieldType.INTERNAL]: rowValues[FieldType.INTERNAL], @@ -2880,6 +2891,57 @@ describe.each([ ) }) + isSqs && + describe("AI fields", () => { + let table: Table + + beforeAll(async () => { + mocks.licenses.useBudibaseAI() + mocks.licenses.useAICustomConfigs() + table = await config.api.table.save( + saveTableRequest({ + schema: { + ai: { + name: "ai", + type: FieldType.AI, + operation: AIOperationEnum.PROMPT, + prompt: "Convert the following to German: '{{ product }}'", + }, + product: { + name: "product", + type: FieldType.STRING, + }, + }, + }) + ) + + await config.api.row.save(table._id!, { + product: generator.word(), + }) + }) + + afterAll(() => { + jest.unmock("@budibase/pro") + }) + + it("should be able to save a row with an AI column", async () => { + const { rows } = await config.api.row.search(table._id!) + expect(rows.length).toBe(1) + expect(rows[0].ai).toEqual("Mock LLM Response") + }) + + it("should be able to update a row with an AI column", async () => { + const { rows } = await config.api.row.search(table._id!) + expect(rows.length).toBe(1) + await config.api.row.save(table._id!, { + product: generator.word(), + ...rows[0], + }) + expect(rows.length).toBe(1) + expect(rows[0].ai).toEqual("Mock LLM Response") + }) + }) + describe("Formula fields", () => { let table: Table let otherTable: Table diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 110899e292..fb8bc084fd 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -17,6 +17,7 @@ import { import * as setup from "./utilities" import { + AIOperationEnum, AutoFieldSubType, BBReferenceFieldSubType, Datasource, @@ -41,11 +42,23 @@ import tk from "timekeeper" import { encodeJSBinding } from "@budibase/string-templates" import { dataFilters } from "@budibase/shared-core" import { Knex } from "knex" -import { generator, structures } from "@budibase/backend-core/tests" +import { generator, structures, mocks } from "@budibase/backend-core/tests" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" import { generateRowIdField } from "../../../integrations/utils" import { cloneDeep } from "lodash/fp" +jest.mock("@budibase/pro", () => ({ + ...jest.requireActual("@budibase/pro"), + ai: { + LargeLanguageModel: { + forCurrentTenant: async () => ({ + run: jest.fn(() => `Mock LLM Response`), + buildPromptFromAIOperation: jest.fn(), + }), + }, + }, +})) + describe.each([ ["in-memory", undefined], ["lucene", undefined], @@ -1606,6 +1619,79 @@ describe.each([ }) }) + isSqs && + describe("AI Column", () => { + const UNEXISTING_AI_COLUMN = "Real LLM Response" + + beforeAll(async () => { + mocks.licenses.useBudibaseAI() + mocks.licenses.useAICustomConfigs() + + tableOrViewId = await createTableOrView({ + product: { name: "product", type: FieldType.STRING }, + ai: { + name: "AI", + type: FieldType.AI, + operation: AIOperationEnum.PROMPT, + prompt: "Translate '{{ product }}' into German", + }, + }) + + await createRows([{ product: "Big Mac" }, { product: "McCrispy" }]) + }) + + describe("equal", () => { + it("successfully finds rows based on AI column", async () => { + await expectQuery({ + equal: { ai: "Mock LLM Response" }, + }).toContainExactly([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { ai: UNEXISTING_AI_COLUMN }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("Returns nothing when searching notEqual on the mock AI response", async () => { + await expectQuery({ + notEqual: { ai: "Mock LLM Response" }, + }).toContainExactly([]) + }) + + it("return all when requesting non-existing response", async () => { + await expectQuery({ + notEqual: { ai: "Real LLM Response" }, + }).toContainExactly([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { ai: ["Mock LLM Response", "Other LLM Response"] }, + }).toContainExactly([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { ai: ["Whopper"] }, + }).toFindNothing() + }) + }) + }) + describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { beforeAll(async () => { tableOrViewId = await createTableOrView({ diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 72cd31e383..16803f19cd 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -7,6 +7,7 @@ import { TRIGGER_DEFINITIONS, } from "../../automations" import { + AIOperationEnum, Automation, AutomationActionStepId, AutomationResults, @@ -666,6 +667,12 @@ export function fullSchemaWithoutLinks({ presence: allRequired, }, }, + [FieldType.AI]: { + name: "ai", + type: FieldType.AI, + operation: AIOperationEnum.PROMPT, + prompt: "Translate this into German :'{{ product }}'", + }, [FieldType.BARCODEQR]: { name: "barcodeqr", type: FieldType.BARCODEQR,