From 82074ee7a1f3766adb2817737944ae417f71b25b Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 31 Oct 2023 22:12:09 +0000 Subject: [PATCH] add datagrid field type --- .../StructuredOutputParser.ts | 89 +++++++------- .../nodes/tools/CustomTool/CustomTool.ts | 24 +--- packages/components/src/Interface.ts | 2 + packages/components/src/utils.ts | 28 +++++ packages/ui/src/ui-component/grid/DataGrid.js | 111 ++++++++++++++++++ packages/ui/src/utils/genericHelper.js | 15 +++ .../ui/src/views/canvas/NodeInputHandler.js | 10 ++ packages/ui/src/views/tools/ToolDialog.js | 24 +--- 8 files changed, 217 insertions(+), 86 deletions(-) create mode 100644 packages/ui/src/ui-component/grid/DataGrid.js diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts index a0927106..10a5f0bb 100644 --- a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts +++ b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts @@ -1,7 +1,8 @@ -import { getBaseClasses, INode, INodeData, INodeParams } from '../../../src' +import { convertSchemaToZod, getBaseClasses, INode, INodeData, INodeParams } from '../../../src' import { BaseOutputParser } from 'langchain/schema/output_parser' import { StructuredOutputParser as LangchainStructuredOutputParser } from 'langchain/output_parsers' import { CATEGORY } from '../OutputParserHelpers' +import { z } from 'zod' class StructuredOutputParser implements INode { label: string @@ -24,65 +25,65 @@ class StructuredOutputParser implements INode { this.icon = 'structure.png' this.category = CATEGORY this.baseClasses = [this.type, ...getBaseClasses(BaseOutputParser)] - //TODO: To extend the structureType to ZodSchema this.inputs = [ - { - label: 'Structure Type', - name: 'structureType', - type: 'options', - options: [ - { - label: 'Names And Descriptions', - name: 'fromNamesAndDescriptions' - } - ], - default: 'fromNamesAndDescriptions' - }, - { - label: 'Structure', - name: 'structure', - type: 'string', - rows: 4, - placeholder: - '{' + - ' answer: "answer to the question",\n' + - ' source: "source used to answer the question, should be a website.",\n' + - '}' - }, { label: 'Autofix', name: 'autofixParser', type: 'boolean', optional: true, description: 'In the event that the first call fails, will make another call to the model to fix any errors.' + }, + { + label: 'JSON Structure', + name: 'jsonStructure', + type: 'datagrid', + description: 'JSON structure for LLM to return', + datagrid: [ + { field: 'property', headerName: 'Property', editable: true }, + { + field: 'type', + headerName: 'Type', + type: 'singleSelect', + valueOptions: ['string', 'number', 'boolean'], + editable: true + }, + { field: 'description', headerName: 'Description', editable: true, flex: 1 } + ], + default: [ + { + property: 'answer', + type: 'string', + description: `answer to the user's question` + }, + { + property: 'source', + type: 'string', + description: `sources used to answer the question, should be websites` + } + ], + additionalParams: true } ] } async init(nodeData: INodeData): Promise { - const structureType = nodeData.inputs?.structureType as string - const structure = nodeData.inputs?.structure as string + const jsonStructure = nodeData.inputs?.jsonStructure as string const autoFix = nodeData.inputs?.autofixParser as boolean - let parsedStructure: any | undefined = undefined - if (structure && structureType === 'fromNamesAndDescriptions') { - try { - parsedStructure = JSON.parse(structure) + try { + const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(z.object(convertSchemaToZod(jsonStructure))) - // NOTE: When we change Flowise to return a json response, the following has to be changed to: JsonStructuredOutputParser - let structuredOutputParser = LangchainStructuredOutputParser.fromNamesAndDescriptions(parsedStructure) - Object.defineProperty(structuredOutputParser, 'autoFix', { - enumerable: true, - configurable: true, - writable: true, - value: autoFix - }) - return structuredOutputParser - } catch (exception) { - throw new Error('Invalid JSON in StructuredOutputParser: ' + exception) - } + // NOTE: When we change Flowise to return a json response, the following has to be changed to: JsonStructuredOutputParser + Object.defineProperty(structuredOutputParser, 'autoFix', { + enumerable: true, + configurable: true, + writable: true, + value: autoFix + }) + return structuredOutputParser + } catch (exception) { + throw new Error('Invalid JSON in StructuredOutputParser: ' + exception) } - throw new Error('Error creating OutputParser.') } } diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index c070df31..541edcf0 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -1,5 +1,5 @@ import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' +import { convertSchemaToZod, getBaseClasses } from '../../../src/utils' import { DynamicStructuredTool } from './core' import { z } from 'zod' import { DataSource } from 'typeorm' @@ -87,26 +87,4 @@ class CustomTool_Tools implements INode { } } -const convertSchemaToZod = (schema: string) => { - try { - const parsedSchema = JSON.parse(schema) - const zodObj: any = {} - for (const sch of parsedSchema) { - if (sch.type === 'string') { - if (sch.required) z.string({ required_error: `${sch.property} required` }).describe(sch.description) - zodObj[sch.property] = z.string().describe(sch.description) - } else if (sch.type === 'number') { - if (sch.required) z.number({ required_error: `${sch.property} required` }).describe(sch.description) - zodObj[sch.property] = z.number().describe(sch.description) - } else if (sch.type === 'boolean') { - if (sch.required) z.boolean({ required_error: `${sch.property} required` }).describe(sch.description) - zodObj[sch.property] = z.boolean().describe(sch.description) - } - } - return zodObj - } catch (e) { - throw new Error(e) - } -} - module.exports = { nodeClass: CustomTool_Tools } diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index d0694d6f..5008813b 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -6,6 +6,7 @@ export type NodeParamsType = | 'asyncOptions' | 'options' | 'multiOptions' + | 'datagrid' | 'string' | 'number' | 'boolean' @@ -60,6 +61,7 @@ export interface INodeParams { description?: string warning?: string options?: Array + datagrid?: Array credentialNames?: Array optional?: boolean | INodeDisplay step?: number diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 8f33683c..9ef30a0e 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -3,6 +3,7 @@ import { load } from 'cheerio' import * as fs from 'fs' import * as path from 'path' import { JSDOM } from 'jsdom' +import { z } from 'zod' import { DataSource } from 'typeorm' import { ICommonObject, IDatabaseEntity, IMessage, INodeData } from './Interface' import { AES, enc } from 'crypto-js' @@ -546,3 +547,30 @@ export const convertChatHistoryToText = (chatHistory: IMessage[] = []): string = }) .join('\n') } + +/** + * Convert schema to zod schema + * @param {string} schema + * @returns {ICommonObject} + */ +export const convertSchemaToZod = (schema: string) => { + try { + const parsedSchema = JSON.parse(schema) + const zodObj: ICommonObject = {} + for (const sch of parsedSchema) { + if (sch.type === 'string') { + if (sch.required) z.string({ required_error: `${sch.property} required` }).describe(sch.description) + zodObj[sch.property] = z.string().describe(sch.description) + } else if (sch.type === 'number') { + if (sch.required) z.number({ required_error: `${sch.property} required` }).describe(sch.description) + zodObj[sch.property] = z.number().describe(sch.description) + } else if (sch.type === 'boolean') { + if (sch.required) z.boolean({ required_error: `${sch.property} required` }).describe(sch.description) + zodObj[sch.property] = z.boolean().describe(sch.description) + } + } + return zodObj + } catch (e) { + throw new Error(e) + } +} diff --git a/packages/ui/src/ui-component/grid/DataGrid.js b/packages/ui/src/ui-component/grid/DataGrid.js new file mode 100644 index 00000000..7c2a2670 --- /dev/null +++ b/packages/ui/src/ui-component/grid/DataGrid.js @@ -0,0 +1,111 @@ +import PropTypes from 'prop-types' +import { useState, useCallback } from 'react' +import { DataGrid as MUIDataGrid, GridActionsCellItem } from '@mui/x-data-grid' +import { IconPlus } from '@tabler/icons' +import { Button } from '@mui/material' +import DeleteIcon from '@mui/icons-material/Delete' +import { cloneDeep } from 'lodash' +import { formatDataGridRows } from 'utils/genericHelper' + +export const DataGrid = ({ columns, rows, style, disabled = false, hideFooter = false, onChange }) => { + const [rowValues, setRowValues] = useState(formatDataGridRows(rows) ?? []) + + const deleteItem = useCallback( + (id) => () => { + let updatedRows = [] + setRowValues((prevRows) => { + let allRows = [...cloneDeep(prevRows)] + allRows = allRows.filter((row) => row.id !== id) + updatedRows = allRows + return allRows + }) + onChange(JSON.stringify(updatedRows)) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + + const addCols = (columns) => { + return [ + ...columns, + { + field: 'actions', + type: 'actions', + width: 80, + getActions: (params) => [ + } label='Delete' onClick={deleteItem(params.id)} /> + ] + } + ] + } + + const colValues = addCols(columns) + + const handleProcessRowUpdate = (newRow) => { + let updatedRows = [] + setRowValues((prevRows) => { + let allRows = [...cloneDeep(prevRows)] + const indexToUpdate = allRows.findIndex((row) => row.id === newRow.id) + if (indexToUpdate >= 0) { + allRows[indexToUpdate] = { ...newRow } + } + updatedRows = allRows + return allRows + }) + onChange(JSON.stringify(updatedRows)) + return newRow + } + + const getEmptyJsonObj = () => { + const obj = {} + for (let i = 0; i < colValues.length; i += 1) { + obj[colValues[i]?.field] = '' + } + return obj + } + + const addNewRow = () => { + setRowValues((prevRows) => { + let allRows = [...cloneDeep(prevRows)] + const lastRowId = allRows.length ? allRows[allRows.length - 1].id + 1 : 1 + allRows.push({ + ...getEmptyJsonObj(), + id: lastRowId + }) + return allRows + }) + } + + return ( + <> + {rowValues && colValues && ( +
+ { + return !disabled + }} + hideFooter={hideFooter} + onProcessRowUpdateError={(error) => console.error(error)} + rows={rowValues} + columns={colValues} + /> +
+ )} + {!disabled && ( + + )} + + ) +} + +DataGrid.propTypes = { + rows: PropTypes.array, + columns: PropTypes.array, + style: PropTypes.any, + disabled: PropTypes.bool, + hideFooter: PropTypes.bool, + onChange: PropTypes.func +} diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index af7f4353..18230a55 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -43,6 +43,7 @@ export const initNode = (nodeData, newNodeId) => { 'asyncOptions', 'options', 'multiOptions', + 'datagrid', 'string', 'number', 'boolean', @@ -422,3 +423,17 @@ export const isValidURL = (url) => { return undefined } } + +export const formatDataGridRows = (rows) => { + try { + const parsedRows = typeof rows === 'string' ? JSON.parse(rows) : rows + return parsedRows.map((sch, index) => { + return { + ...sch, + id: index + } + }) + } catch (e) { + return [] + } +} diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 3a13a3b5..906ad1d8 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -14,6 +14,7 @@ import { Dropdown } from 'ui-component/dropdown/Dropdown' import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown' import { Input } from 'ui-component/input/Input' +import { DataGrid } from 'ui-component/grid/DataGrid' import { File } from 'ui-component/file/File' import { SwitchInput } from 'ui-component/switch/Switch' import { flowContext } from 'store/context/ReactFlowContext' @@ -258,6 +259,15 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA value={data.inputs[inputParam.name] ?? inputParam.default ?? false} /> )} + {inputParam.type === 'datagrid' && ( + (data.inputs[inputParam.name] = newValue)} + /> + )} {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( { - try { - const parsedSchema = JSON.parse(schema) - return parsedSchema.map((sch, index) => { - return { - ...sch, - id: index - } - }) - } catch (e) { - return [] - } - } - useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) else dispatch({ type: HIDE_CANVAS_DIALOG }) @@ -167,7 +153,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = setToolId(getSpecificToolApi.data.id) setToolName(getSpecificToolApi.data.name) setToolDesc(getSpecificToolApi.data.description) - setToolSchema(formatSchema(getSpecificToolApi.data.schema)) + setToolSchema(formatDataGridRows(getSpecificToolApi.data.schema)) if (getSpecificToolApi.data.func) setToolFunc(getSpecificToolApi.data.func) else setToolFunc('') } @@ -180,7 +166,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) setToolIcon(dialogProps.data.iconSrc) - setToolSchema(formatSchema(dialogProps.data.schema)) + setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') } else if (dialogProps.type === 'EDIT' && dialogProps.toolId) { @@ -191,7 +177,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) setToolIcon(dialogProps.data.iconSrc) - setToolSchema(formatSchema(dialogProps.data.schema)) + setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') } else if (dialogProps.type === 'TEMPLATE' && dialogProps.data) { @@ -199,7 +185,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) setToolIcon(dialogProps.data.iconSrc) - setToolSchema(formatSchema(dialogProps.data.schema)) + setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') } else if (dialogProps.type === 'ADD') {