diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 2b20938981..8c97b33418 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -1141,7 +1141,8 @@ class InternalBuilder { schema.constraints?.presence === true || schema.type === FieldType.FORMULA || schema.type === FieldType.AUTO || - schema.type === FieldType.LINK + schema.type === FieldType.LINK || + schema.type === FieldType.AI ) { continue } diff --git a/packages/backend-core/src/sql/sqlTable.ts b/packages/backend-core/src/sql/sqlTable.ts index f5b02cc4e4..84f4e290aa 100644 --- a/packages/backend-core/src/sql/sqlTable.ts +++ b/packages/backend-core/src/sql/sqlTable.ts @@ -17,7 +17,7 @@ import SchemaBuilder = Knex.SchemaBuilder import CreateTableBuilder = Knex.CreateTableBuilder function isIgnoredType(type: FieldType) { - const ignored = [FieldType.LINK, FieldType.FORMULA] + const ignored = [FieldType.LINK, FieldType.FORMULA, FieldType.AI] return ignored.indexOf(type) !== -1 } @@ -144,6 +144,9 @@ function generateSchema( case FieldType.FORMULA: // This is allowed, but nothing to do on the external datasource break + case FieldType.AI: + // This is allowed, but nothing to do on the external datasource + break case FieldType.ATTACHMENTS: case FieldType.ATTACHMENT_SINGLE: case FieldType.SIGNATURE_SINGLE: diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 0130c39715..da68b4182b 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -33,6 +33,7 @@ } from "constants/backend" import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils" import ConfirmDialog from "components/common/ConfirmDialog.svelte" + import AIFieldConfiguration from "components/common/AIFieldConfiguration.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import { getBindings } from "components/backend/DataTable/formula" import JSONSchemaModal from "./JSONSchemaModal.svelte" @@ -54,6 +55,7 @@ const NUMBER_TYPE = FieldType.NUMBER const JSON_TYPE = FieldType.JSON const DATE_TYPE = FieldType.DATETIME + const AI_TYPE = FieldType.AI const dispatch = createEventDispatcher() const { dispatch: gridDispatch, rows } = getContext("grid") @@ -421,6 +423,7 @@ FIELDS.ATTACHMENT_SINGLE, FIELDS.ATTACHMENTS, FIELDS.FORMULA, + FIELDS.AI, FIELDS.JSON, FIELDS.BARCODEQR, FIELDS.SIGNATURE_SINGLE, @@ -732,6 +735,13 @@ /> + {:else if editableColumn.type === AI_TYPE} + {:else if editableColumn.type === JSON_TYPE} + import { Input, Multiselect, Select, TextArea } from "@budibase/bbui" + import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte" + import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" + + const AIOperations = { + SUMMARISE_TEXT: { + label: "Summarise Text", + value: "SUMMARISE_TEXT" + }, + CLEAN_DATA: { + label: "Clean Data", + value: "CLEAN_DATA" + }, + TRANSLATE: { + label: "Translate", + value: "TRANSLATE" + }, + CATEGORISE_TEXT: { + label: "Categorise Text", + value: "CATEGORISE_TEXT" + }, + SENTIMENT_ANALYSIS: { + label: "Sentiment Analysis", + value: "SENTIMENT_ANALYSIS" + }, + PROMPT: { + label: "Prompt", + value: "PROMPT" + }, + SEARCH_WEB: { + label: "Search Web", + value: "SEARCH_WEB" + } + } + + const OperationFieldTypes = { + MULTI_COLUMN: "columns", + COLUMN: "column", + BINDABLE_TEXT: "prompt", + // LANGUAGE: "language", + } + + const OperationFields = { + SUMMARISE_TEXT: { + columns: OperationFieldTypes.MULTI_COLUMN, + prompt: OperationFieldTypes.BINDABLE_TEXT, + }, + CLEAN_DATA: { + columns: OperationFieldTypes.MULTI_COLUMN, + prompt: OperationFieldTypes.BINDABLE_TEXT, + }, + TRANSLATE: { + columns: OperationFieldTypes.MULTI_COLUMN, + language: OperationFieldTypes.BINDABLE_TEXT, + prompt: OperationFieldTypes.BINDABLE_TEXT, + }, + CATEGORISE_TEXT: { + columns: OperationFieldTypes.MULTI_COLUMN, + categories: OperationFieldTypes.BINDABLE_TEXT, + prompt: OperationFieldTypes.BINDABLE_TEXT, + }, + SENTIMENT_ANALYSIS: { + column: OperationFieldTypes.COLUMN, + prompt: OperationFieldTypes.BINDABLE_TEXT, + }, + PROMPT: { + prompt: OperationFieldTypes.BINDABLE_TEXT, + }, + SEARCH_WEB: { + columns: OperationFieldTypes.MULTI_COLUMN, + prompt: OperationFieldTypes.BINDABLE_TEXT, + } + } + + const AIFieldConfigOptions = Object.keys(AIOperations).map(key => ({ + label: AIOperations[key].label, + value: AIOperations[key].value + })) + + export let bindings + export let context + export let schema + export let aiField = {} + + $: OperationField = OperationFields[aiField.operation] || null + $: console.log(aiField) + $: console.log(schema) + $: schemaWithoutRelations = Object.keys(schema).filter(key => schema[key].type !== "link") + + + + {/if} + {/each} +{/if} \ No newline at end of file diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index 6fbc36afe2..6ddf4c2138 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -159,6 +159,12 @@ export const FIELDS = { icon: TypeIconMap[FieldType.FORMULA], constraints: {}, }, + AI: { + name: "AI", + type: FieldType.AI, + icon: TypeIconMap[FieldType.AI], + constraints: {}, + }, JSON: { name: "JSON", type: FieldType.JSON, diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte index 3a0c789b9e..d255d022bf 100644 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ b/packages/frontend-core/src/components/FilterBuilder.svelte @@ -262,7 +262,7 @@ {/if} {#if allowBindings && filter.field && filter.valueType === "Binding"} - {:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)} + {:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI].includes(filter.type)} {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === ArrayOperator.ONE_OF)} { diff --git a/packages/server/src/api/controllers/row/aiColumn.ts b/packages/server/src/api/controllers/row/aiColumn.ts new file mode 100644 index 0000000000..fc543c3487 --- /dev/null +++ b/packages/server/src/api/controllers/row/aiColumn.ts @@ -0,0 +1,43 @@ +import { getRowParams } from "../../../db/utils" +import { + outputProcessing, + processAIColumns, +} from "../../../utilities/rowProcessor" +import { context } from "@budibase/backend-core" +import { Table, Row } from "@budibase/types" +import isEqual from "lodash/isEqual" +import { cloneDeep } from "lodash/fp" + +export async function updateAllAIColumnsInTable(table: Table) { + const db = context.getAppDB() + // start by getting the raw rows (which will be written back to DB after update) + let rows = ( + await db.allDocs( + getRowParams(table._id, null, { + include_docs: true, + }) + ) + ).rows.map(row => row.doc!) + // now enrich the rows, note the clone so that we have the base state of the + // rows so that we don't write any of the enriched information back + let enrichedRows = await outputProcessing(table, cloneDeep(rows), { + squash: false, + }) + const updatedRows = [] + for (let row of rows) { + // find the enriched row, if found process the formulas + const enrichedRow = enrichedRows.find( + (enriched: Row) => enriched._id === row._id + ) + if (enrichedRow) { + let processed = await processAIColumns(table, cloneDeep(row), { + contextRows: [enrichedRow], + }) + // values have changed, need to add to bulk docs to update + if (!isEqual(processed, row)) { + updatedRows.push(processed) + } + } + } + await db.bulkDocs(updatedRows) +} diff --git a/packages/server/src/api/controllers/row/staticFormula.ts b/packages/server/src/api/controllers/row/staticFormula.ts index 777379db14..54cb7d0728 100644 --- a/packages/server/src/api/controllers/row/staticFormula.ts +++ b/packages/server/src/api/controllers/row/staticFormula.ts @@ -1,6 +1,6 @@ import { getRowParams } from "../../../db/utils" import { - outputProcessing, + outputProcessing, processAIColumns, processFormulas, } from "../../../utilities/rowProcessor" import { context } from "@budibase/backend-core" @@ -101,7 +101,7 @@ export async function updateAllFormulasInTable(table: Table) { (enriched: Row) => enriched._id === row._id ) if (enrichedRow) { - const processed = await processFormulas(table, cloneDeep(row), { + let processed = await processFormulas(table, cloneDeep(row), { dynamic: false, contextRows: [enrichedRow], }) @@ -142,6 +142,10 @@ export async function finaliseRow( dynamic: false, contextRows: [enrichedRow], }) + row = await processAIColumns(table, row, { + contextRows: [enrichedRow], + }) + // don't worry about rev, tables handle rev/lastID updates // if another row has been written since processing this will // handle the auto ID clash @@ -154,6 +158,10 @@ export async function finaliseRow( enrichedRow = await processFormulas(table, enrichedRow, { dynamic: false, }) + enrichedRow = await processAIColumns(table, row, { + contextRows: [enrichedRow], + }) + // this updates the related formulas in other rows based on the relations to this row if (updateFormula) { await updateRelatedFormula(table, enrichedRow) diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts index 249bb43bbc..22b34e1720 100644 --- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts +++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts @@ -119,6 +119,7 @@ export function buildSqlFieldList( ([columnName, column]) => column.type !== FieldType.LINK && column.type !== FieldType.FORMULA && + column.type !== FieldType.AI && !existing.find((field: string) => field === columnName) ) .map(column => `${table.name}.${column[0]}`) diff --git a/packages/server/src/api/controllers/table/bulkFormula.ts b/packages/server/src/api/controllers/table/bulkFormula.ts index 060b67e8ce..6e47f30233 100644 --- a/packages/server/src/api/controllers/table/bulkFormula.ts +++ b/packages/server/src/api/controllers/table/bulkFormula.ts @@ -5,15 +5,10 @@ import isEqual from "lodash/isEqual" import uniq from "lodash/uniq" import { updateAllFormulasInTable } from "../row/staticFormula" import { context } from "@budibase/backend-core" -import { - FormulaType, - FieldSchema, - FieldType, - FormulaFieldMetadata, - Table, -} from "@budibase/types" +import { FieldSchema, FieldType, FormulaFieldMetadata, FormulaType, Table, } from "@budibase/types" import sdk from "../../../sdk" import { isRelationshipColumn } from "../../../db/utils" +import { updateAllAIColumnsInTable } from "../row/aiColumn" function isStaticFormula( column: FieldSchema @@ -198,3 +193,21 @@ export async function runStaticFormulaChecks( await checkIfFormulaUpdated(table, { oldTable }) } } + +export async function runAIColumnChecks( + table: Table, + { oldTable }: { oldTable?: Table } +) { + // look to see if any formula values have changed + const shouldUpdate = Object.values(table.schema).find( + column => + column.type === FieldType.AI && + (!oldTable || + !oldTable.schema[column.name] || + !isEqual(oldTable.schema[column.name], column)) + ) + // if a static formula column has updated, then need to run the update + if (shouldUpdate != null) { + await updateAllAIColumnsInTable(table) + } +} \ No newline at end of file diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts index 269f079ae8..b916cb9c04 100644 --- a/packages/server/src/api/controllers/table/utils.ts +++ b/packages/server/src/api/controllers/table/utils.ts @@ -33,7 +33,7 @@ import { } from "@budibase/types" import sdk from "../../../sdk" import env from "../../../environment" -import { runStaticFormulaChecks } from "./bulkFormula" +import { runAIColumnChecks, runStaticFormulaChecks } from "./bulkFormula" export async function clearColumns(table: Table, columnNames: string[]) { const db = context.getAppDB() diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts index 2f7f7fd44c..f0e6c69563 100644 --- a/packages/server/src/db/linkedRows/index.ts +++ b/packages/server/src/db/linkedRows/index.ts @@ -296,7 +296,7 @@ export async function squashLinks( return false } if ( - [FieldType.LINK, FieldType.FORMULA].includes(tableColumn.type) + [FieldType.LINK, FieldType.FORMULA, FieldType.AI].includes(tableColumn.type) ) { return false } diff --git a/packages/server/src/integrations/googlesheets.ts b/packages/server/src/integrations/googlesheets.ts index 831528f84d..5f61791683 100644 --- a/packages/server/src/integrations/googlesheets.ts +++ b/packages/server/src/integrations/googlesheets.ts @@ -56,6 +56,7 @@ interface AuthTokenResponse { const isTypeAllowed: Record = { [FieldType.STRING]: true, [FieldType.FORMULA]: true, + [FieldType.AI]: true, [FieldType.NUMBER]: true, [FieldType.LONGFORM]: true, [FieldType.DATETIME]: true, @@ -490,7 +491,8 @@ export class GoogleSheetsIntegration implements DatasourcePlus { } if ( !sheet.headerValues.includes(key) && - column.type !== FieldType.FORMULA + column.type !== FieldType.FORMULA && + column.type !== FieldType.AI ) { updatedHeaderValues.push(key) } diff --git a/packages/server/src/sdk/app/tables/internal/index.ts b/packages/server/src/sdk/app/tables/internal/index.ts index c0beed0db8..c372b65bd0 100644 --- a/packages/server/src/sdk/app/tables/internal/index.ts +++ b/packages/server/src/sdk/app/tables/internal/index.ts @@ -15,7 +15,7 @@ import { import { EventType, updateLinks } from "../../../../db/linkedRows" import { cloneDeep } from "lodash/fp" import isEqual from "lodash/isEqual" -import { runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula" +import { runAIColumnChecks, runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula" import { context } from "@budibase/backend-core" import { findDuplicateInternalColumns } from "@budibase/shared-core" import { getTable } from "../getters" @@ -133,6 +133,7 @@ export async function save( } // has to run after, make sure it has _id await runStaticFormulaChecks(table, { oldTable, deletion: false }) + await runAIColumnChecks(table, { oldTable, deletion: false }) return { table } } diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts index bd71644537..7533e2b22a 100644 --- a/packages/server/src/sdk/app/tables/internal/sqs.ts +++ b/packages/server/src/sdk/app/tables/internal/sqs.ts @@ -19,6 +19,7 @@ const FieldTypeMap: Record = { [FieldType.BOOLEAN]: SQLiteType.NUMERIC, [FieldType.DATETIME]: SQLiteType.TEXT, [FieldType.FORMULA]: SQLiteType.TEXT, + [FieldType.AI]: SQLiteType.TEXT, [FieldType.LONGFORM]: SQLiteType.TEXT, [FieldType.NUMBER]: SQLiteType.REAL, [FieldType.STRING]: SQLiteType.TEXT, diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 269158e61e..f2f6f764d1 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -172,7 +172,7 @@ export async function enrichSchema( for (const relTableFieldName of Object.keys(relTable.schema)) { const relTableField = relTable.schema[relTableFieldName] - if ([FieldType.LINK, FieldType.FORMULA].includes(relTableField.type)) { + if ([FieldType.LINK, FieldType.FORMULA, FieldType.AI].includes(relTableField.type)) { continue } diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 2f227dd646..c4b8130966 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -189,6 +189,10 @@ export async function inputProcessing( if (field.type === FieldType.FORMULA) { delete clonedRow[key] } + // remove any AI values, they are to be generated + if (field.type === FieldType.AI) { + delete clonedRow[key] + } // otherwise coerce what is there to correct types else { clonedRow[key] = coerce(value, field.type) diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts index 0fa6f62807..30942edc93 100644 --- a/packages/server/src/utilities/rowProcessor/utils.ts +++ b/packages/server/src/utilities/rowProcessor/utils.ts @@ -11,6 +11,7 @@ import { } from "@budibase/types" import tracer from "dd-trace" import { context } from "@budibase/backend-core" +import * as pro from "@budibase/pro" interface FormulaOpts { dynamic?: boolean @@ -91,6 +92,56 @@ export async function processFormulas( }) } +/** + * Looks through the rows provided and finds AI columns - which it then processes. + */ +export async function processAIColumns( + table: Table, + inputRows: T, + { contextRows }: FormulaOpts +): Promise { + return tracer.trace("processAIColumns", {}, async span => { + const numRows = Array.isArray(inputRows) ? inputRows.length : 1 + span?.addTags({ table_id: table._id }) + const rows = Array.isArray(inputRows) ? inputRows : [inputRows] + if (rows) { + // Ensure we have snippet context + await context.ensureSnippetContext() + + for (let [column, schema] of Object.entries(table.schema)) { + if (schema.type !== FieldType.AI) { + continue + } + + // const llm = pro.ai.LargeLanguageModel() + // if ( + // schema.formula == null || + // (dynamic && isStatic) || + // (!dynamic && !isStatic) + // ) { + // continue + // } + // iterate through rows and process formula + for (let i = 0; i < rows.length; i++) { + let row = rows[i] + // let context = contextRows ? contextRows[i] : row + // let formula = schema.prompt + rows[i] = { + ...row, + [column]: tracer.trace("processAIColumn", {}, span => { + span?.addTags({ table_id: table._id, column }) + // return processStringSync(formula, context) + // TODO: Add the AI stuff in to this + return "YEET AI" + }), + } + } + } + } + return Array.isArray(inputRows) ? rows : rows[0] + }) +} + /** * Processes any date columns and ensures that those without the ignoreTimezones * flag set are parsed as UTC rather than local time. diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index 45e9a7c6d0..6400b5852b 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -87,6 +87,8 @@ export const getValidOperatorsForType = ( ops = numOps } else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) { ops = stringOps.concat([Op.MoreThan, Op.LessThan]) + } else if (type === FieldType.AI) { + ops = stringOps.concat([Op.MoreThan, Op.LessThan]) } else if ( type === FieldType.BB_REFERENCE_SINGLE || schema.isDeprecatedSingleUserColumn(fieldType) diff --git a/packages/shared-core/src/table.ts b/packages/shared-core/src/table.ts index 8a8069ce4d..4067e251d1 100644 --- a/packages/shared-core/src/table.ts +++ b/packages/shared-core/src/table.ts @@ -8,6 +8,7 @@ const allowDisplayColumnByType: Record = { [FieldType.NUMBER]: true, [FieldType.DATETIME]: true, [FieldType.FORMULA]: true, + [FieldType.AI]: true, [FieldType.AUTO]: true, [FieldType.INTERNAL]: true, [FieldType.BARCODEQR]: true, @@ -38,6 +39,7 @@ const allowSortColumnByType: Record = { [FieldType.JSON]: true, [FieldType.FORMULA]: false, + [FieldType.AI]: false, [FieldType.ATTACHMENTS]: false, [FieldType.ATTACHMENT_SINGLE]: false, [FieldType.SIGNATURE_SINGLE]: false, @@ -61,6 +63,7 @@ const allowDefaultColumnByType: Record = { [FieldType.BIGINT]: false, [FieldType.BOOLEAN]: false, [FieldType.FORMULA]: false, + [FieldType.AI]: false, [FieldType.ATTACHMENTS]: false, [FieldType.ATTACHMENT_SINGLE]: false, [FieldType.SIGNATURE_SINGLE]: false, diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index 42440b2988..cb4b1d560d 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -76,6 +76,10 @@ export enum FieldType { * that is part of the initial formula definition, the formula will be live evaluated in the browser. */ AUTO = "auto", + /** + * A complex type, called an AI column within Budibase. This type has a... TODO: fill out + */ + AI = "ai", /** * a JSON type, called JSON within Budibase. This type allows any arbitrary JSON to be input to this column * type, which will be represented as a JSON object in the row. This type depends on a schema being diff --git a/packages/types/src/documents/app/table/constants.ts b/packages/types/src/documents/app/table/constants.ts index fffaddc5df..6ba23eba25 100644 --- a/packages/types/src/documents/app/table/constants.ts +++ b/packages/types/src/documents/app/table/constants.ts @@ -30,6 +30,7 @@ export enum JsonFieldSubType { export enum FormulaType { STATIC = "static", DYNAMIC = "dynamic", + AI = "ai" } export enum BBReferenceFieldSubType { diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index 6078f73d1d..b0c78f9dc5 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -116,6 +116,17 @@ export interface FormulaFieldMetadata extends BaseFieldSchema { formulaType?: FormulaType } +export interface AIFieldMetadata extends BaseFieldSchema { + type: FieldType.AI + formula: string + // TODO: needs better types + operation: string + columns?: string[] + column?: string + prompt?: string + language?: string +} + export interface BBReferenceFieldMetadata extends Omit { type: FieldType.BB_REFERENCE @@ -190,6 +201,7 @@ interface OtherFieldMetadata extends BaseFieldSchema { | FieldType.LINK | FieldType.AUTO | FieldType.FORMULA + | FieldType.AI | FieldType.NUMBER | FieldType.LONGFORM | FieldType.BB_REFERENCE @@ -207,6 +219,7 @@ export type FieldSchema = | RelationshipFieldMetadata | AutoColumnFieldMetadata | FormulaFieldMetadata + | AIFieldMetadata | NumberFieldMetadata | LongFormFieldMetadata | StringFieldMetadata