This commit is contained in:
Martin McKeaveney 2024-10-01 12:52:11 +01:00
parent b1462b4c19
commit 2d21afbe10
27 changed files with 314 additions and 19 deletions

View File

@ -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
}

View File

@ -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:

View File

@ -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 @@
/>
</div>
</div>
{:else if editableColumn.type === AI_TYPE}
<AIFieldConfiguration
aiField={editableColumn}
context={rowGoldenSample}
bindings={getBindings({ table })}
schema={table.schema}
/>
{:else if editableColumn.type === JSON_TYPE}
<Button primary text on:click={openJsonSchemaEditor}
>Open schema editor</Button

View File

@ -0,0 +1,123 @@
<script>
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")
</script>
<Select
options={AIFieldConfigOptions}
bind:value={aiField.operation}
/>
{#if aiField.operation}
{#each Object.keys(OperationField) as key}
{#if OperationField[key] === OperationFieldTypes.BINDABLE_TEXT}
<ModalBindableInput
label={key}
panel={ServerBindingPanel}
title="Prompt"
on:change={e => aiField[key] = e.detail}
value={aiField[key]}
{bindings}
allowJS
{context}
/>
{:else if OperationField[key] === OperationFieldTypes.MULTI_COLUMN}
<Multiselect
bind:value={aiField[key]}
label={key}
options={schemaWithoutRelations}
/>
{:else if OperationField[key] === OperationFieldTypes.COLUMN}
<Select
bind:value={aiField[key]}
label={key}
options={schemaWithoutRelations}
/>
{/if}
{/each}
{/if}

View File

@ -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,

View File

@ -262,7 +262,7 @@
{/if}
{#if allowBindings && filter.field && filter.valueType === "Binding"}
<slot name="binding" {filter} />
{: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)}
<Input disabled={filter.noValue} bind:value={filter.value} />
{:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === ArrayOperator.ONE_OF)}
<Multiselect

View File

@ -95,7 +95,8 @@
const { type, formulaType } = col.schema
return (
searchableTypes.includes(type) ||
(type === FieldType.FORMULA && formulaType === FormulaType.STATIC)
(type === FieldType.FORMULA && formulaType === FormulaType.STATIC) ||
type === FieldType.AI
)
}

View File

@ -30,6 +30,8 @@ const TypeComponentMap = {
[FieldType.ATTACHMENT_SINGLE]: AttachmentSingleCell,
[FieldType.LINK]: RelationshipCell,
[FieldType.FORMULA]: FormulaCell,
// TODO: fix
[FieldType.AI]: FormulaCell,
[FieldType.JSON]: JSONCell,
[FieldType.BB_REFERENCE]: BBReferenceCell,
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,

View File

@ -160,6 +160,7 @@ export const TypeIconMap = {
[FieldType.ATTACHMENT_SINGLE]: "DocumentFragment",
[FieldType.LINK]: "DataCorrelated",
[FieldType.FORMULA]: "Calculator",
[FieldType.AI]: "MagicWand",
[FieldType.JSON]: "Brackets",
[FieldType.BIGINT]: "TagBold",
[FieldType.AUTO]: "MagicWand",

View File

@ -154,7 +154,8 @@ function isEditableColumn(column: FieldSchema) {
column.autoReason !== AutoReason.FOREIGN_KEY &&
column.subtype !== AutoFieldSubType.AUTO_ID
const isFormula = column.type === FieldType.FORMULA
return !(isExternalAutoColumn || isFormula)
const isAIColumn = column.type === FieldType.AI
return !(isExternalAutoColumn || isFormula || isAIColumn)
}
export class ExternalRequest<T extends Operation> {

View File

@ -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<Row>(
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)
}

View File

@ -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)

View File

@ -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]}`)

View File

@ -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)
}
}

View File

@ -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()

View File

@ -296,7 +296,7 @@ export async function squashLinks<T = Row[] | Row>(
return false
}
if (
[FieldType.LINK, FieldType.FORMULA].includes(tableColumn.type)
[FieldType.LINK, FieldType.FORMULA, FieldType.AI].includes(tableColumn.type)
) {
return false
}

View File

@ -56,6 +56,7 @@ interface AuthTokenResponse {
const isTypeAllowed: Record<FieldType, boolean> = {
[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)
}

View File

@ -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 }
}

View File

@ -19,6 +19,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
[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,

View File

@ -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
}

View File

@ -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)

View File

@ -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<T extends Row | Row[]>(
})
}
/**
* Looks through the rows provided and finds AI columns - which it then processes.
*/
export async function processAIColumns<T extends Row | Row[]>(
table: Table,
inputRows: T,
{ contextRows }: FormulaOpts
): Promise<T> {
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.

View File

@ -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)

View File

@ -8,6 +8,7 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
[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, boolean> = {
[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, boolean> = {
[FieldType.BIGINT]: false,
[FieldType.BOOLEAN]: false,
[FieldType.FORMULA]: false,
[FieldType.AI]: false,
[FieldType.ATTACHMENTS]: false,
[FieldType.ATTACHMENT_SINGLE]: false,
[FieldType.SIGNATURE_SINGLE]: false,

View File

@ -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

View File

@ -30,6 +30,7 @@ export enum JsonFieldSubType {
export enum FormulaType {
STATIC = "static",
DYNAMIC = "dynamic",
AI = "ai"
}
export enum BBReferenceFieldSubType {

View File

@ -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<BaseFieldSchema, "subtype"> {
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