add datagrid field type

This commit is contained in:
Henry
2023-10-31 22:12:09 +00:00
parent 5ff720d66c
commit 82074ee7a1
8 changed files with 217 additions and 86 deletions
@@ -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<any> {
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.')
}
}
@@ -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 }
+2
View File
@@ -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<INodeOptionsValue>
datagrid?: Array<ICommonObject>
credentialNames?: Array<string>
optional?: boolean | INodeDisplay
step?: number
+28
View File
@@ -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)
}
}
@@ -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) => [
<GridActionsCellItem key={'Delete'} icon={<DeleteIcon />} 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 && (
<div style={{ marginTop: 10, height: 210, width: '100%', ...style }}>
<MUIDataGrid
processRowUpdate={handleProcessRowUpdate}
isCellEditable={() => {
return !disabled
}}
hideFooter={hideFooter}
onProcessRowUpdateError={(error) => console.error(error)}
rows={rowValues}
columns={colValues}
/>
</div>
)}
{!disabled && (
<Button sx={{ mt: 1 }} variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
Add Item
</Button>
)}
</>
)
}
DataGrid.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array,
style: PropTypes.any,
disabled: PropTypes.bool,
hideFooter: PropTypes.bool,
onChange: PropTypes.func
}
+15
View File
@@ -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 []
}
}
@@ -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' && (
<DataGrid
disabled={disabled}
columns={inputParam.datagrid}
hideFooter={true}
rows={data.inputs[inputParam.name] ?? JSON.stringify(inputParam.default) ?? []}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
/>
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
key={data.inputs[inputParam.name]}
+5 -19
View File
@@ -28,7 +28,7 @@ import useApi from 'hooks/useApi'
// utils
import useNotifier from 'utils/useNotifier'
import { generateRandomGradient } from 'utils/genericHelper'
import { generateRandomGradient, formatDataGridRows } from 'utils/genericHelper'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const exampleAPIFunc = `/*
@@ -142,20 +142,6 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
[deleteItem]
)
const formatSchema = (schema) => {
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') {