From 1d1bd4f5562a34f91ca0bb390ee08f4337e420cb Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sun, 10 Dec 2023 22:38:18 +0530 Subject: [PATCH 01/16] Environment Variables: Dashboard along with CRUD Operations on variables. --- packages/server/src/Interface.ts | 9 + .../server/src/database/entities/Variable.ts | 25 ++ .../server/src/database/entities/index.ts | 4 +- .../mysql/1702200925471-AddVariableEntity.ts | 21 ++ .../src/database/migrations/mysql/index.ts | 4 +- .../1702200925471-AddVariableEntity.ts | 21 ++ .../src/database/migrations/postgres/index.ts | 4 +- .../sqlite/1702200925471-AddVariableEntity.ts | 13 + .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 42 +++ packages/ui/src/api/variables.js | 16 + packages/ui/src/menu-items/dashboard.js | 12 +- packages/ui/src/routes/MainRoutes.js | 7 + .../views/variables/AddEditVariableDialog.js | 265 +++++++++++++++ packages/ui/src/views/variables/index.js | 301 ++++++++++++++++++ 15 files changed, 742 insertions(+), 6 deletions(-) create mode 100644 packages/server/src/database/entities/Variable.ts create mode 100644 packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts create mode 100644 packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts create mode 100644 packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts create mode 100644 packages/ui/src/api/variables.js create mode 100644 packages/ui/src/views/variables/AddEditVariableDialog.js create mode 100644 packages/ui/src/views/variables/index.js diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index f82c66902..126aac38d 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -68,6 +68,15 @@ export interface ICredential { createdDate: Date } +export interface IVariable { + id: string + name: string + value: string + type: string + updatedDate: Date + createdDate: Date +} + export interface IComponentNodes { [key: string]: INode } diff --git a/packages/server/src/database/entities/Variable.ts b/packages/server/src/database/entities/Variable.ts new file mode 100644 index 000000000..88e0587d1 --- /dev/null +++ b/packages/server/src/database/entities/Variable.ts @@ -0,0 +1,25 @@ +/* eslint-disable */ +import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm' +import { IVariable } from "../../Interface"; + +@Entity() +export class Variable implements IVariable{ + @PrimaryGeneratedColumn('uuid') + id: string + + @Column() + name: string + + @Column({ nullable: true, type: 'text' }) + value: string + + @Column({default: 'string', type: 'text'}) + type: string + + + @CreateDateColumn() + createdDate: Date + + @UpdateDateColumn() + updatedDate: Date +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index 58447a1f5..af5c559f5 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -3,11 +3,13 @@ import { ChatMessage } from './ChatMessage' import { Credential } from './Credential' import { Tool } from './Tool' import { Assistant } from './Assistant' +import { Variable } from './Variable' export const entities = { ChatFlow, ChatMessage, Credential, Tool, - Assistant + Assistant, + Variable } diff --git a/packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts new file mode 100644 index 000000000..a6c818874 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1702200925471-AddVariableEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVariableEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS \`variable\` ( + \`id\` varchar(36) NOT NULL, + \`name\` varchar(255) NOT NULL, + \`value\` text NOT NULL, + \`type\` varchar(255) DEFAULT NULL, + \`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE variable`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 8f9824a86..a5220ad88 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' export const mysqlMigrations = [ Init1693840429259, @@ -23,5 +24,6 @@ export const mysqlMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddVariableEntity1699325775451 ] diff --git a/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts new file mode 100644 index 000000000..d4a1d7be7 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVariableEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS variable ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + "name" varchar NOT NULL, + "value" text NOT NULL, + "type" text NULL, + "createdDate" timestamp NOT NULL DEFAULT now(), + "updatedDate" timestamp NOT NULL DEFAULT now(), + CONSTRAINT "PK_3c7cea7a044ac4c92764576cdbf" PRIMARY KEY (id) + );` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE variable`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index d196fbc16..3c3fa3966 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' export const postgresMigrations = [ Init1693891895163, @@ -23,5 +24,6 @@ export const postgresMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddVariableEntity1699325775451 ] diff --git a/packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts new file mode 100644 index 000000000..63ec709fa --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1702200925471-AddVariableEntity.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddVariableEntity1699325775451 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "variable" ("id" varchar PRIMARY KEY NOT NULL, "name" text NOT NULL, "value" text NOT NULL, "type" varchar, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "updatedDate" datetime NOT NULL DEFAULT (datetime('now')));` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE variable`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index fdd83064a..c0ade080d 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' export const sqliteMigrations = [ Init1693835579790, @@ -23,5 +24,6 @@ export const sqliteMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddVariableEntity1699325775451 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2ab454ad7..2b4e64366 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -62,6 +62,7 @@ import { sanitizeMiddleware } from './utils/XSS' import axios from 'axios' import { Client } from 'langchainhub' import { parsePrompt } from './utils/hub' +import { Variable } from "./database/entities/Variable"; export class App { app: express.Application @@ -1150,6 +1151,47 @@ export class App { return res.json(templates) }) + // ---------------------------------------- + // Variables + // ---------------------------------------- + this.app.get('/api/v1/variables', async (req: Request, res: Response) => { + const variables = await getDataSource().getRepository(Variable).find() + return res.json(variables) + }) + + // Create new variable + this.app.post('/api/v1/variables', async (req: Request, res: Response) => { + const body = req.body + const newVariable = new Variable() + Object.assign(newVariable, body) + const variable = this.AppDataSource.getRepository(Variable).create(newVariable) + const results = await this.AppDataSource.getRepository(Variable).save(variable) + return res.json(results) + }) + + // Update variable + this.app.put('/api/v1/variables/:id', async (req: Request, res: Response) => { + const variable = await this.AppDataSource.getRepository(Variable).findOneBy({ + id: req.params.id + }) + + if (!variable) return res.status(404).send(`Variable ${req.params.id} not found`) + + const body = req.body + const updateVariable = new Variable() + Object.assign(updateVariable, body) + this.AppDataSource.getRepository(Variable).merge(variable, updateVariable) + const result = await this.AppDataSource.getRepository(Variable).save(variable) + + return res.json(result) + }) + + // Delete variable via id + this.app.delete('/api/v1/variables/:id', async (req: Request, res: Response) => { + const results = await this.AppDataSource.getRepository(Variable).delete({ id: req.params.id }) + return res.json(results) + }) + // ---------------------------------------- // API Keys // ---------------------------------------- diff --git a/packages/ui/src/api/variables.js b/packages/ui/src/api/variables.js new file mode 100644 index 000000000..944b83198 --- /dev/null +++ b/packages/ui/src/api/variables.js @@ -0,0 +1,16 @@ +import client from './client' + +const getAllVariables = () => client.get('/variables') + +const createVariable = (body) => client.post(`/variables`, body) + +const updateVariable = (id, body) => client.put(`/variables/${id}`, body) + +const deleteVariable = (id) => client.delete(`/variables/${id}`) + +export default { + getAllVariables, + createVariable, + updateVariable, + deleteVariable +} diff --git a/packages/ui/src/menu-items/dashboard.js b/packages/ui/src/menu-items/dashboard.js index 8bf5b3924..b0e5d66e6 100644 --- a/packages/ui/src/menu-items/dashboard.js +++ b/packages/ui/src/menu-items/dashboard.js @@ -1,8 +1,8 @@ // assets -import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } from '@tabler/icons' +import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } from '@tabler/icons' // constant -const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } +const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } // ==============================|| DASHBOARD MENU ITEMS ||============================== // @@ -51,6 +51,14 @@ const dashboard = { icon: icons.IconLock, breadcrumbs: true }, + { + id: 'variables', + title: 'Environment Variables', + type: 'item', + url: '/variables', + icon: icons.IconVariable, + breadcrumbs: true + }, { id: 'apikey', title: 'API Keys', diff --git a/packages/ui/src/routes/MainRoutes.js b/packages/ui/src/routes/MainRoutes.js index bce0de137..08dd721dd 100644 --- a/packages/ui/src/routes/MainRoutes.js +++ b/packages/ui/src/routes/MainRoutes.js @@ -22,6 +22,9 @@ const Assistants = Loadable(lazy(() => import('views/assistants'))) // credentials routing const Credentials = Loadable(lazy(() => import('views/credentials'))) +// variables routing +const Variables = Loadable(lazy(() => import('views/variables'))) + // ==============================|| MAIN ROUTING ||============================== // const MainRoutes = { @@ -55,6 +58,10 @@ const MainRoutes = { { path: '/credentials', element: + }, + { + path: '/variables', + element: } ] } diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.js b/packages/ui/src/views/variables/AddEditVariableDialog.js new file mode 100644 index 000000000..e32ce663a --- /dev/null +++ b/packages/ui/src/views/variables/AddEditVariableDialog.js @@ -0,0 +1,265 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { useState, useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' + +// Material +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, OutlinedInput } from '@mui/material' + +// Project imports +import { StyledButton } from 'ui-component/button/StyledButton' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' + +// Icons +import { IconX, IconVariable } from '@tabler/icons' + +// API +import variablesApi from 'api/variables' + +// Hooks + +// utils +import useNotifier from 'utils/useNotifier' + +// const +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import { TooltipWithParser } from '../../ui-component/tooltip/TooltipWithParser' +import { Dropdown } from '../../ui-component/dropdown/Dropdown' + +const variableTypes = [ + { + label: 'Static Variable', + name: 'static' + }, + { + label: 'Runtime Variable', + name: 'runtime' + } +] + +const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { + const portalElement = document.getElementById('portal') + + const dispatch = useDispatch() + + // ==============================|| Snackbar ||============================== // + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [name, setName] = useState('') + const [value, setValue] = useState('') + const [type, setType] = useState('') + + const [variable, setVariable] = useState({}) + + useEffect(() => { + if (dialogProps.type === 'EDIT' && dialogProps.data) { + // When variable dialog is opened from Variables dashboard + setName(dialogProps.data.name) + setValue(dialogProps.data.value) + setType(dialogProps.data.type) + //setVariable(dialogProps.data) + } else if (dialogProps.type === 'ADD' && dialogProps.data) { + // When variable dialog is to add a new variable + setName('') + setValue('') + setType('static') + //setVariable({ name: '', value: '', type: 'static' }) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const addNewVariable = async () => { + try { + const obj = { + name, + value, + type + } + const createResp = await variablesApi.createVariable(obj) + if (createResp.data) { + enqueueSnackbar({ + message: 'New Variable added', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(createResp.data.id) + } + } catch (err) { + const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}` + enqueueSnackbar({ + message: `Failed to add new Variable: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const saveVariable = async () => { + try { + const saveObj = { + name, + value, + type + } + + const saveResp = await variablesApi.updateVariable(variable.id, saveObj) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Variable saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm(saveResp.data.id) + } + } catch (error) { + const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}` + enqueueSnackbar({ + message: `Failed to save Variable: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() + } + } + + const component = show ? ( + + +
+
+ +
+ {dialogProps.type === 'ADD' ? 'Add Variable' : 'Edit Variable'} +
+
+ + +
+ + Variable Name * + + +
+
+ setName(e.target.value)} value={name ?? ''} /> +
+ +
+ + Value + + +
+
+ setValue(e.target.value)} value={value ?? ''} /> + Leave the value empty for runtime variables. Will be populated at runtime. +
+ +
+ + Type * + +
+
+ setType(newValue)} + value={type ?? 'static'} + /> + + Runtime: Value would be populated from env. Static: Value would be used as is. + +
+
+ + (dialogProps.type === 'ADD' ? addNewVariable() : saveVariable())} + > + {dialogProps.confirmButtonName} + + + +
+ ) : null + + return createPortal(component, portalElement) +} + +AddEditVariableDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default AddEditVariableDialog diff --git a/packages/ui/src/views/variables/index.js b/packages/ui/src/views/variables/index.js new file mode 100644 index 000000000..ccec36d30 --- /dev/null +++ b/packages/ui/src/views/variables/index.js @@ -0,0 +1,301 @@ +import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' +import moment from 'moment' + +// material-ui +import { + Button, + Box, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Toolbar, + TextField, + InputAdornment, + ButtonGroup +} from '@mui/material' +import { useTheme } from '@mui/material/styles' + +// project imports +import MainCard from 'ui-component/cards/MainCard' +import { StyledButton } from 'ui-component/button/StyledButton' +import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' + +// API +import variablesApi from 'api/variables' + +// Hooks +import useApi from 'hooks/useApi' +import useConfirm from 'hooks/useConfirm' + +// utils +import useNotifier from 'utils/useNotifier' + +// Icons +import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons' +import CredentialEmptySVG from 'assets/images/credential_empty.svg' + +// const +import AddEditVariableDialog from './AddEditVariableDialog' + +// ==============================|| Credentials ||============================== // + +const Variables = () => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const dispatch = useDispatch() + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [showVariableDialog, setShowVariableDialog] = useState(false) + const [variableDialogProps, setVariableDialogProps] = useState({}) + const [variables, setVariables] = useState([]) + + const { confirm } = useConfirm() + + const getAllVariables = useApi(variablesApi.getAllVariables) + + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterVariables(data) { + return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + + const addNew = () => { + const dialogProp = { + type: 'ADD', + cancelButtonName: 'Cancel', + confirmButtonName: 'Add', + data: {} + } + setVariableDialogProps(dialogProp) + setShowVariableDialog(true) + } + + const edit = (variable) => { + const dialogProp = { + type: 'EDIT', + cancelButtonName: 'Cancel', + confirmButtonName: 'Save', + data: variable + } + setVariableDialogProps(dialogProp) + setShowVariableDialog(true) + } + + const deleteVariable = async (credential) => { + const confirmPayload = { + title: `Delete`, + description: `Delete variable ${variable.name}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + const deleteResp = await variablesApi.deleteVariable(credential.id) + if (deleteResp.data) { + enqueueSnackbar({ + message: 'Variable deleted', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm() + } + } catch (error) { + const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}` + enqueueSnackbar({ + message: `Failed to delete Variable: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + } + + const onConfirm = () => { + setShowVariableDialog(false) + getAllVariables.request() + } + + useEffect(() => { + getAllVariables.request() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (getAllVariables.data) { + setVariables(getAllVariables.data) + } + }, [getAllVariables.data]) + + return ( + <> + + + + +

Environment Variables 

+ + + + ) + }} + /> + + + + } + > + Add Variable + + + +
+
+
+ {variables.length <= 0 && ( + + + CredentialEmptySVG + +
No Variables Yet
+
+ )} + {variables.length > 0 && ( + + + + + Name + Value + Type + Last Updated + Created + + + + + + {variables.filter(filterVariables).map((variable, index) => ( + + +
+
+ +
+ {variable.name} +
+
+ {variable.value} + {variable.type === 'static' ? 'Static Variable' : 'Runtime Variable'} + {moment(variable.updatedDate).format('DD-MMM-YY')} + {moment(variable.createdDate).format('DD-MMM-YY')} + + edit(variable)}> + + + + + deleteVariable(variable)}> + + + +
+ ))} +
+
+
+ )} +
+ setShowVariableDialog(false)} + onConfirm={onConfirm} + > + + + ) +} + +export default Variables From 0bf5536095a31c8493e13e30e371258232052d58 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sun, 10 Dec 2023 22:39:06 +0530 Subject: [PATCH 02/16] Environment Variables: injection of variables into the custom script --- packages/components/nodes/tools/CustomTool/core.ts | 3 ++- packages/server/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 12dd72f19..343acafdf 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -80,7 +80,8 @@ export class DynamicStructuredTool< sandbox[`$${item}`] = arg[item] } } - + sandbox['$env'] = { USER: 'VINOD' } + console.log('sandbox === ' + JSON.stringify(sandbox)) const defaultAllowBuiltInDep = [ 'assert', 'buffer', diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2b4e64366..39baaf8e1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -62,7 +62,7 @@ import { sanitizeMiddleware } from './utils/XSS' import axios from 'axios' import { Client } from 'langchainhub' import { parsePrompt } from './utils/hub' -import { Variable } from "./database/entities/Variable"; +import { Variable } from './database/entities/Variable' export class App { app: express.Application From bfa870e56b0371e661ef5d980ded8140b78de42a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 12:19:35 +0530 Subject: [PATCH 03/16] Environment Variables: injection of variables ($env) into the custom tool and addition of ($flow) object --- .../nodes/tools/CustomTool/CustomTool.ts | 27 ++++++++++++++- .../components/nodes/tools/CustomTool/core.ts | 33 +++++++++++++++++-- packages/components/src/Interface.ts | 1 + packages/server/src/utils/index.ts | 4 ++- .../views/variables/AddEditVariableDialog.js | 16 ++++----- 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index 541edcf07..aba803e4c 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -80,7 +80,32 @@ class CustomTool_Tools implements INode { code: tool.func } if (customToolFunc) obj.code = customToolFunc - return new DynamicStructuredTool(obj) + + const variables = await appDataSource.getRepository(databaseEntities['Variable']).find() + + // override variables defined in overrideConfig + // nodeData.inputs.variables is an Object, check each property and override the variable + if (nodeData?.inputs?.variables) { + for (const propertyName of Object.getOwnPropertyNames(nodeData.inputs.variables)) { + const foundVar = variables.find((v) => v.name === propertyName) + if (foundVar) { + // even if the variable was defined as runtime, we override it with static value + foundVar.type = 'static' + foundVar.value = nodeData.inputs.variables[propertyName] + } else { + // add it the variables, if not found locally in the db + variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.variables[propertyName] }) + } + } + } + const flow = { + chatId: options.chatId, // id is uppercase (I) + chatflowId: options.chatflowid // id is lowercase (i) + } + let dynamicStructuredTool = new DynamicStructuredTool(obj) + dynamicStructuredTool.setVariables(variables) + dynamicStructuredTool.setFlowObject(flow) + return dynamicStructuredTool } catch (e) { throw new Error(e) } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 343acafdf..b7b1f6a63 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -2,6 +2,7 @@ import { z } from 'zod' import { CallbackManagerForToolRun } from 'langchain/callbacks' import { StructuredTool, ToolParams } from 'langchain/tools' import { NodeVM } from 'vm2' +import { logger } from "@zilliz/milvus2-sdk-node"; /* * List of dependencies allowed to be import in vm2 @@ -62,6 +63,8 @@ export class DynamicStructuredTool< func: DynamicStructuredToolInput['func'] schema: T + private variables: any[] + private flowObj: any constructor(fields: DynamicStructuredToolInput) { super(fields) @@ -80,8 +83,26 @@ export class DynamicStructuredTool< sandbox[`$${item}`] = arg[item] } } - sandbox['$env'] = { USER: 'VINOD' } - console.log('sandbox === ' + JSON.stringify(sandbox)) + //inject variables + let env = {} + if (this.variables) { + for (const item of this.variables) { + let value = item.value + if (item.type === 'runtime') { + value = process.env[item.name] + } + Object.defineProperty(env, item.name, { + enumerable: true, + configurable: true, + writable: true, + value: value + }) + } + } + sandbox['$env'] = env + if (this.flowObj) { + sandbox['$flow'] = this.flowObj + } const defaultAllowBuiltInDep = [ 'assert', 'buffer', @@ -118,4 +139,12 @@ export class DynamicStructuredTool< return response } + + setVariables(variables: any[]) { + this.variables = variables + } + + setFlowObject(flow: any) { + this.flowObj = flow + } } diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 6752f9440..bc50155c3 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -73,6 +73,7 @@ export interface INodeParams { additionalParams?: boolean loadMethod?: string hidden?: boolean + variables?: ICommonObject[] } export interface INodeExecutionData { diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 2bf1c04a4..adec01cde 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -37,6 +37,7 @@ import { Tool } from '../database/entities/Tool' import { Assistant } from '../database/entities/Assistant' import { DataSource } from 'typeorm' import { CachePool } from '../CachePool' +import { Variable } from '../database/entities/Variable' const QUESTION_VAR_PREFIX = 'question' const CHAT_HISTORY_VAR_PREFIX = 'chat_history' @@ -47,7 +48,8 @@ export const databaseEntities: IDatabaseEntity = { ChatMessage: ChatMessage, Tool: Tool, Credential: Credential, - Assistant: Assistant + Assistant: Assistant, + Variable: Variable } /** diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.js b/packages/ui/src/views/variables/AddEditVariableDialog.js index e32ce663a..db2116b04 100644 --- a/packages/ui/src/views/variables/AddEditVariableDialog.js +++ b/packages/ui/src/views/variables/AddEditVariableDialog.js @@ -62,13 +62,13 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { setName(dialogProps.data.name) setValue(dialogProps.data.value) setType(dialogProps.data.type) - //setVariable(dialogProps.data) + setVariable(dialogProps.data) } else if (dialogProps.type === 'ADD' && dialogProps.data) { // When variable dialog is to add a new variable setName('') setValue('') setType('static') - //setVariable({ name: '', value: '', type: 'static' }) + setVariable({ name: '', value: '', type: 'static' }) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -83,9 +83,9 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const addNewVariable = async () => { try { const obj = { - name, - value, - type + name: name, + value: value, + type: type } const createResp = await variablesApi.createVariable(obj) if (createResp.data) { @@ -125,9 +125,9 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const saveVariable = async () => { try { const saveObj = { - name, - value, - type + name: name, + value: value, + type: type } const saveResp = await variablesApi.updateVariable(variable.id, saveObj) From 947ab9dae656a472a3cba0f470409a86926009dc Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 14:15:41 +0530 Subject: [PATCH 04/16] Environment Variables: handling of environment variables in user input --- packages/server/src/index.ts | 7 ++++++- packages/server/src/utils/index.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 39baaf8e1..8f728f19b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -43,7 +43,8 @@ import { checkMemorySessionId, clearSessionMemoryFromViewMessageDialog, getUserHome, - replaceChatHistory + replaceChatHistory, + replaceEnvVariables } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -1381,6 +1382,10 @@ export class App { const chatflowid = req.params.id let incomingInput: IncomingInput = req.body + if (incomingInput.question) { + incomingInput.question = await replaceEnvVariables(incomingInput.question, this.AppDataSource) + } + let nodeToExecuteData: INodeData const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index adec01cde..d35753881 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -954,3 +954,29 @@ export const getAllValuesFromJson = (obj: any): any[] => { extractValues(obj) return values } + +export const replaceEnvVariables = async (question: string, appDataSource: DataSource): Promise => { + // the incoming question can have more than one env variable with the pattern {{ env.VARIABLE_NAME }} + // extract all the env variables from the question and iterate through them + const envVariables = question.match(/{{[^}]*}}/g) + if (envVariables) { + for (const envVariable of envVariables) { + // this is needed as the user can have spaces between the curly braces and the env keyword + // extract the variable name from the env variable + const variableName = envVariable.replace(/{{\s*env.|\s*}}/g, '') + // get the value of the env variable from the database + const variable = await appDataSource.getRepository(Variable).findOneBy({ + name: variableName + }) + if (variable) { + let value = variable.value + if (variable.type === 'runtime') { + value = process.env[variable.name] as string + } + // replace the env variable with the value from the database + question = question.replace(envVariable, value) + } + } + } + return question +} From c2e97b54acfa741f091881287e744a38b713c522 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 16:19:52 +0530 Subject: [PATCH 05/16] Environment Variables: Validations and minor fixes --- .../components/nodes/tools/CustomTool/core.ts | 1 - .../views/variables/AddEditVariableDialog.js | 70 +++++++++++-------- packages/ui/src/views/variables/index.js | 4 +- 3 files changed, 43 insertions(+), 32 deletions(-) diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index b7b1f6a63..8bae726c9 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -2,7 +2,6 @@ import { z } from 'zod' import { CallbackManagerForToolRun } from 'langchain/callbacks' import { StructuredTool, ToolParams } from 'langchain/tools' import { NodeVM } from 'vm2' -import { logger } from "@zilliz/milvus2-sdk-node"; /* * List of dependencies allowed to be import in vm2 diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.js b/packages/ui/src/views/variables/AddEditVariableDialog.js index db2116b04..85c40e448 100644 --- a/packages/ui/src/views/variables/AddEditVariableDialog.js +++ b/packages/ui/src/views/variables/AddEditVariableDialog.js @@ -26,6 +26,7 @@ import useNotifier from 'utils/useNotifier' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' import { TooltipWithParser } from '../../ui-component/tooltip/TooltipWithParser' import { Dropdown } from '../../ui-component/dropdown/Dropdown' +import { SwitchInput } from '../../ui-component/switch/Switch' const variableTypes = [ { @@ -50,29 +51,29 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [name, setName] = useState('') - const [value, setValue] = useState('') - const [type, setType] = useState('') + const [variableName, setVariableName] = useState('') + const [variableValue, setVariableValue] = useState('') + const [variableType, setVariableType] = useState('static') + const [dialogType, setDialogType] = useState('ADD') const [variable, setVariable] = useState({}) useEffect(() => { - if (dialogProps.type === 'EDIT' && dialogProps.data) { + if (dialogProps.type === 'EDIT') { // When variable dialog is opened from Variables dashboard - setName(dialogProps.data.name) - setValue(dialogProps.data.value) - setType(dialogProps.data.type) + setVariableName(dialogProps.data.name) + setVariableValue(dialogProps.data.value) + setVariableType(dialogProps.data.type) setVariable(dialogProps.data) - } else if (dialogProps.type === 'ADD' && dialogProps.data) { + setDialogType('EDIT') + } else if (dialogProps.type === 'ADD') { // When variable dialog is to add a new variable - setName('') - setValue('') - setType('static') - setVariable({ name: '', value: '', type: 'static' }) + setVariableName('') + setVariableValue('') + setVariableType('static') + setDialogType('ADD') } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dialogProps]) + }, [dialogProps.data, dialogProps.type]) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -83,9 +84,9 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const addNewVariable = async () => { try { const obj = { - name: name, - value: value, - type: type + name: variableName, + value: variableValue, + type: variableType } const createResp = await variablesApi.createVariable(obj) if (createResp.data) { @@ -125,9 +126,9 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const saveVariable = async () => { try { const saveObj = { - name: name, - value: value, - type: type + name: variableName, + value: variableValue, + type: variableType } const saveResp = await variablesApi.updateVariable(variable.id, saveObj) @@ -207,7 +208,13 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
- setName(e.target.value)} value={name ?? ''} /> + setVariableName(e.target.value)} + value={variableName ?? ''} + />
@@ -217,7 +224,13 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
- setValue(e.target.value)} value={value ?? ''} /> + setVariableValue(e.target.value)} + value={variableValue ?? ''} + /> Leave the value empty for runtime variables. Will be populated at runtime.
@@ -228,11 +241,10 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
setType(newValue)} - value={type ?? 'static'} + onSelect={(newValue) => setVariableType(newValue)} + value={variableType} /> Runtime: Value would be populated from env. Static: Value would be used as is. @@ -241,9 +253,9 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { (dialogProps.type === 'ADD' ? addNewVariable() : saveVariable())} + onClick={() => (dialogType === 'ADD' ? addNewVariable() : saveVariable())} > {dialogProps.confirmButtonName} diff --git a/packages/ui/src/views/variables/index.js b/packages/ui/src/views/variables/index.js index ccec36d30..9399eb207 100644 --- a/packages/ui/src/views/variables/index.js +++ b/packages/ui/src/views/variables/index.js @@ -95,7 +95,7 @@ const Variables = () => { setShowVariableDialog(true) } - const deleteVariable = async (credential) => { + const deleteVariable = async (variable) => { const confirmPayload = { title: `Delete`, description: `Delete variable ${variable.name}?`, @@ -106,7 +106,7 @@ const Variables = () => { if (isConfirmed) { try { - const deleteResp = await variablesApi.deleteVariable(credential.id) + const deleteResp = await variablesApi.deleteVariable(variable.id) if (deleteResp.data) { enqueueSnackbar({ message: 'Variable deleted', From 790c7eabeb7d218639700ce265abda081f7cf813 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 16:20:32 +0530 Subject: [PATCH 06/16] Environment Variables: Validations and minor fixes --- packages/ui/src/views/variables/AddEditVariableDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.js b/packages/ui/src/views/variables/AddEditVariableDialog.js index 85c40e448..86a121f67 100644 --- a/packages/ui/src/views/variables/AddEditVariableDialog.js +++ b/packages/ui/src/views/variables/AddEditVariableDialog.js @@ -26,7 +26,6 @@ import useNotifier from 'utils/useNotifier' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' import { TooltipWithParser } from '../../ui-component/tooltip/TooltipWithParser' import { Dropdown } from '../../ui-component/dropdown/Dropdown' -import { SwitchInput } from '../../ui-component/switch/Switch' const variableTypes = [ { From ef3f1b34b149edb2064bc862abf6397522738cc1 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 22:22:25 +0530 Subject: [PATCH 07/16] Environment Variables: renaming overrideConfig node to envVars --- packages/components/nodes/tools/CustomTool/CustomTool.ts | 8 ++++---- packages/server/src/index.ts | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index aba803e4c..6b0397c84 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -85,16 +85,16 @@ class CustomTool_Tools implements INode { // override variables defined in overrideConfig // nodeData.inputs.variables is an Object, check each property and override the variable - if (nodeData?.inputs?.variables) { - for (const propertyName of Object.getOwnPropertyNames(nodeData.inputs.variables)) { + if (nodeData?.inputs?.envVars) { + for (const propertyName of Object.getOwnPropertyNames(nodeData.inputs.envVars)) { const foundVar = variables.find((v) => v.name === propertyName) if (foundVar) { // even if the variable was defined as runtime, we override it with static value foundVar.type = 'static' - foundVar.value = nodeData.inputs.variables[propertyName] + foundVar.value = nodeData.inputs.envVars[propertyName] } else { // add it the variables, if not found locally in the db - variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.variables[propertyName] }) + variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.envVars[propertyName] }) } } } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8f728f19b..34e461f75 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1382,10 +1382,6 @@ export class App { const chatflowid = req.params.id let incomingInput: IncomingInput = req.body - if (incomingInput.question) { - incomingInput.question = await replaceEnvVariables(incomingInput.question, this.AppDataSource) - } - let nodeToExecuteData: INodeData const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ From 48b17fc78554c02a7ab0ea3f0edaa8578d8441bf Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 20 Dec 2023 23:59:04 +0000 Subject: [PATCH 08/16] pass sessionId to custom tool --- .../OpenAIFunctionAgent.ts | 342 ++++++++++++++++-- .../RedisBackedChatMemory.ts | 74 +++- .../components/nodes/tools/CustomTool/core.ts | 58 ++- packages/components/src/utils.ts | 30 +- packages/server/src/index.ts | 11 +- 5 files changed, 464 insertions(+), 51 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index c019ca5a9..63749fd2e 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,10 +1,18 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' -import { BaseLanguageModel } from 'langchain/base_language' +import { AgentExecutor, AgentExecutorInput } from 'langchain/agents' +import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' +import { OutputParserException } from 'langchain/schema/output_parser' +import { CallbackManagerForChainRun } from 'langchain/callbacks' +import { formatToOpenAIFunction } from 'langchain/tools' +import { ToolInputParsingException, Tool } from '@langchain/core/tools' +import { getBaseClasses } from '../../../src/utils' import { flatten } from 'lodash' import { BaseChatMemory } from 'langchain/memory' +import { RunnableSequence } from 'langchain/schema/runnable' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { OpenAIFunctionsAgentOutputParser } from 'langchain/agents/openai/output_parser' class OpenAIFunctionAgent_Agents implements INode { label: string @@ -16,8 +24,9 @@ class OpenAIFunctionAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields: { sessionId?: string }) { this.label = 'OpenAI Function Agent' this.name = 'openAIFunctionAgent' this.version = 3.0 @@ -52,54 +61,323 @@ class OpenAIFunctionAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseLanguageModel const memory = nodeData.inputs?.memory as BaseChatMemory - const systemMessage = nodeData.inputs?.systemMessage as string - let tools = nodeData.inputs?.tools - tools = flatten(tools) - - const executor = await initializeAgentExecutorWithOptions(tools, model, { - agentType: 'openai-functions', - verbose: process.env.DEBUG === 'true' ? true : false, - agentArgs: { - prefix: systemMessage ?? `You are a helpful AI assistant.` - } - }) + const executor = prepareAgent(nodeData, this.sessionId) if (memory) executor.memory = memory return executor } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - const memory = nodeData.inputs?.memory as BaseChatMemory + const memory = nodeData.inputs?.memory - if (options && options.chatHistory) { - const chatHistoryClassName = memory.chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - memory.chatHistory = mapChatHistory(options) - executor.memory = memory - } - } - - ;(executor.memory as any).returnMessages = true // Return true for BaseChatModel + const executor = prepareAgent(nodeData, this.sessionId) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) + let res: ChainValues = {} + if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const result = await executor.run(input, [loggerHandler, handler, ...callbacks]) - return result + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const result = await executor.run(input, [loggerHandler, ...callbacks]) - return result + res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res?.output, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res?.output + } +} + +const formatAgentSteps = (steps: AgentStep[]): BaseMessage[] => + steps.flatMap(({ action, observation }) => { + if ('messageLog' in action && action.messageLog !== undefined) { + const log = action.messageLog as BaseMessage[] + return log.concat(new FunctionMessage(observation, action.tool)) + } else { + return [new AIMessage(action.log)] + } + }) + +const prepareAgent = (nodeData: INodeData, sessionId?: string) => { + const model = nodeData.inputs?.model as ChatOpenAI + const memory = nodeData.inputs?.memory + const systemMessage = nodeData.inputs?.systemMessage as string + let tools = nodeData.inputs?.tools + tools = flatten(tools) + const memoryKey = memory.memoryKey ?? 'chat_history' + + const prompt = ChatPromptTemplate.fromMessages([ + ['ai', systemMessage ?? `You are a helpful AI assistant.`], + new MessagesPlaceholder(memoryKey), + ['human', '{input}'], + new MessagesPlaceholder('agent_scratchpad') + ]) + + const modelWithFunctions = model.bind({ + functions: [...tools.map((tool: any) => formatToOpenAIFunction(tool))] + }) + + const runnableAgent = RunnableSequence.from([ + { + input: (i: { input: string; steps: AgentStep[] }) => i.input, + agent_scratchpad: (i: { input: string; steps: AgentStep[] }) => formatAgentSteps(i.steps), + [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { + const messages: BaseMessage[] = await memory.getChatMessages(sessionId, true) + return messages ?? [] + } + }, + prompt, + modelWithFunctions, + new OpenAIFunctionsAgentOutputParser() + ]) + + const executor = AgentExecutorExtended.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId + }) + + return executor +} + +type AgentExecutorOutput = ChainValues + +class AgentExecutorExtended extends AgentExecutor { + sessionId?: string + + static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string }): AgentExecutorExtended { + const newInstance = new AgentExecutorExtended(fields) + if (fields.sessionId) newInstance.sessionId = fields.sessionId + return newInstance + } + + shouldContinueIteration(iterations: number): boolean { + return this.maxIterations === undefined || iterations < this.maxIterations + } + + async _call(inputs: ChainValues, runManager?: CallbackManagerForChainRun): Promise { + const toolsByName = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + + const steps: AgentStep[] = [] + let iterations = 0 + + const getOutput = async (finishStep: AgentFinish): Promise => { + const { returnValues } = finishStep + const additional = await this.agent.prepareForOutput(returnValues, steps) + + if (this.returnIntermediateSteps) { + return { ...returnValues, intermediateSteps: steps, ...additional } + } + await runManager?.handleAgentEnd(finishStep) + return { ...returnValues, ...additional } + } + + while (this.shouldContinueIteration(iterations)) { + let output + try { + output = await this.agent.plan(steps, inputs, runManager?.getChild()) + } catch (e) { + if (e instanceof OutputParserException) { + let observation + let text = e.message + if (this.handleParsingErrors === true) { + if (e.sendToLLM) { + observation = e.observation + text = e.llmOutput ?? '' + } else { + observation = 'Invalid or incomplete response' + } + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + output = { + tool: '_Exception', + toolInput: observation, + log: text + } as AgentAction + } else { + throw e + } + } + // Check if the agent has finished + if ('returnValues' in output) { + return getOutput(output) + } + + let actions: AgentAction[] + if (Array.isArray(output)) { + actions = output as AgentAction[] + } else { + actions = [output as AgentAction] + } + + const newSteps = await Promise.all( + actions.map(async (action) => { + await runManager?.handleAgentAction(action) + const tool = action.tool === '_Exception' ? new ExceptionTool() : toolsByName[action.tool?.toLowerCase()] + let observation + try { + // here we need to override Tool call method to include sessionId as parameter + observation = tool + ? // @ts-ignore + await tool.call(action.toolInput, runManager?.getChild(), undefined, this.sessionId) + : `${action.tool} is not a valid tool, try another one.` + } catch (e) { + if (e instanceof ToolInputParsingException) { + if (this.handleParsingErrors === true) { + observation = 'Invalid or incomplete tool input. Please try again.' + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + observation = await new ExceptionTool().call(observation, runManager?.getChild()) + return { action, observation: observation ?? '' } + } + } + return { action, observation: observation ?? '' } + }) + ) + + steps.push(...newSteps) + + const lastStep = steps[steps.length - 1] + const lastTool = toolsByName[lastStep.action.tool?.toLowerCase()] + + if (lastTool?.returnDirect) { + return getOutput({ + returnValues: { [this.agent.returnValues[0]]: lastStep.observation }, + log: '' + }) + } + + iterations += 1 + } + + const finish = await this.agent.returnStoppedResponse(this.earlyStoppingMethod, steps, inputs) + + return getOutput(finish) + } + + async _takeNextStep( + nameToolMap: Record, + inputs: ChainValues, + intermediateSteps: AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise { + let output + try { + output = await this.agent.plan(intermediateSteps, inputs, runManager?.getChild()) + } catch (e) { + if (e instanceof OutputParserException) { + let observation + let text = e.message + if (this.handleParsingErrors === true) { + if (e.sendToLLM) { + observation = e.observation + text = e.llmOutput ?? '' + } else { + observation = 'Invalid or incomplete response' + } + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + output = { + tool: '_Exception', + toolInput: observation, + log: text + } as AgentAction + } else { + throw e + } + } + + if ('returnValues' in output) { + return output + } + + let actions: AgentAction[] + if (Array.isArray(output)) { + actions = output as AgentAction[] + } else { + actions = [output as AgentAction] + } + + const result: AgentStep[] = [] + for (const agentAction of actions) { + let observation = '' + if (runManager) { + await runManager?.handleAgentAction(agentAction) + } + if (agentAction.tool in nameToolMap) { + const tool = nameToolMap[agentAction.tool] + try { + // here we need to override Tool call method to include sessionId as parameter + // @ts-ignore + observation = await tool.call(agentAction.toolInput, runManager?.getChild(), undefined, this.sessionId) + } catch (e) { + if (e instanceof ToolInputParsingException) { + if (this.handleParsingErrors === true) { + observation = 'Invalid or incomplete tool input. Please try again.' + } else if (typeof this.handleParsingErrors === 'string') { + observation = this.handleParsingErrors + } else if (typeof this.handleParsingErrors === 'function') { + observation = this.handleParsingErrors(e) + } else { + throw e + } + observation = await new ExceptionTool().call(observation, runManager?.getChild()) + } + } + } else { + observation = `${agentAction.tool} is not a valid tool, try another available tool: ${Object.keys(nameToolMap).join(', ')}` + } + result.push({ + action: agentAction, + observation + }) + } + return result + } +} + +class ExceptionTool extends Tool { + name = '_Exception' + + description = 'Exception tool' + + async _call(query: string) { + return query } } diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index d6ec9a114..72af1cb5d 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -1,8 +1,14 @@ -import { INode, INodeData, INodeParams, ICommonObject } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory } from '../../../src/utils' +import { INode, INodeData, INodeParams, ICommonObject, IMessage, MessageType } from '../../../src/Interface' +import { + convertBaseMessagetoIMessage, + getBaseClasses, + getCredentialData, + getCredentialParam, + serializeChatHistory +} from '../../../src/utils' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis' -import { mapStoredMessageToChatMessage, BaseMessage } from 'langchain/schema' +import { mapStoredMessageToChatMessage, BaseMessage, AIMessage, HumanMessage } from 'langchain/schema' import { Redis } from 'ioredis' class RedisBackedChatMemory_Memory implements INode { @@ -94,14 +100,20 @@ class RedisBackedChatMemory_Memory implements INode { } const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { - const sessionId = nodeData.inputs?.sessionId as string const sessionTTL = nodeData.inputs?.sessionTTL as number const memoryKey = nodeData.inputs?.memoryKey as string const windowSize = nodeData.inputs?.windowSize as number const chatId = options?.chatId as string let isSessionIdUsingChatMessageId = false - if (!sessionId && chatId) isSessionIdUsingChatMessageId = true + let sessionId = '' + + if (!nodeData.inputs?.sessionId && chatId) { + isSessionIdUsingChatMessageId = true + sessionId = chatId + } else { + sessionId = nodeData.inputs?.sessionId + } const credentialData = await getCredentialData(nodeData.credential ?? '', options) const redisUrl = getCredentialParam('redisUrl', credentialData, nodeData) @@ -128,7 +140,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom } let obj: RedisChatMessageHistoryInput = { - sessionId: sessionId ? sessionId : chatId, + sessionId, client } @@ -162,21 +174,67 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId + isSessionIdUsingChatMessageId, + sessionId, + redisClient: client }) return memory } interface BufferMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean + redisClient: Redis + sessionId: string } class BufferMemoryExtended extends BufferMemory { isSessionIdUsingChatMessageId? = false + sessionId = '' + redisClient: Redis - constructor(fields: BufferMemoryInput & Partial) { + constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + this.sessionId = fields.sessionId + this.redisClient = fields.redisClient + } + + async getChatMessages(overrideSessionId = '', returnBaseMessage = false): Promise { + if (!this.redisClient) return [] + + const id = overrideSessionId ?? this.sessionId + const rawStoredMessages = await this.redisClient.lrange(id, 0, -1) + const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) + const baseMessages = orderedMessages.map(mapStoredMessageToChatMessage) + return returnBaseMessage ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + if (!this.redisClient) return + + const id = overrideSessionId ?? this.sessionId + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + if (input) { + const newInputMessage = new HumanMessage(input.text) + const messageToAdd = [newInputMessage].map((msg) => msg.toDict()) + await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0])) + } + + if (output) { + const newOutputMessage = new AIMessage(output.text) + const messageToAdd = [newOutputMessage].map((msg) => msg.toDict()) + await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0])) + } + } + + async clearChatMessages(overrideSessionId = ''): Promise { + if (!this.redisClient) return + + const id = overrideSessionId ?? this.sessionId + await this.redisClient.del(id) + await this.clear() } } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 7e76e37f8..77aa0e6b4 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -1,8 +1,18 @@ import { z } from 'zod' -import { CallbackManagerForToolRun } from 'langchain/callbacks' -import { StructuredTool, ToolParams } from 'langchain/tools' import { NodeVM } from 'vm2' import { availableDependencies } from '../../../src/utils' +import { RunnableConfig } from '@langchain/core/runnables' +import { StructuredTool, ToolParams } from '@langchain/core/tools' +import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' + +class ToolInputParsingException extends Error { + output?: string + + constructor(message: string, output?: string) { + super(message) + this.output = output + } +} export interface BaseDynamicToolInput extends ToolParams { name: string @@ -45,7 +55,47 @@ export class DynamicStructuredTool< this.schema = fields.schema } - protected async _call(arg: z.output): Promise { + async call(arg: z.output, configArg?: RunnableConfig | Callbacks, tags?: string[], overrideSessionId?: string): Promise { + const config = parseCallbackConfigArg(configArg) + if (config.runName === undefined) { + config.runName = this.name + } + let parsed + try { + parsed = await this.schema.parseAsync(arg) + } catch (e) { + throw new ToolInputParsingException(`Received tool input did not match expected schema`, JSON.stringify(arg)) + } + const callbackManager_ = await CallbackManager.configure( + config.callbacks, + this.callbacks, + config.tags || tags, + this.tags, + config.metadata, + this.metadata, + { verbose: this.verbose } + ) + const runManager = await callbackManager_?.handleToolStart( + this.toJSON(), + typeof parsed === 'string' ? parsed : JSON.stringify(parsed), + undefined, + undefined, + undefined, + undefined, + config.runName + ) + let result + try { + result = await this._call(parsed, runManager, overrideSessionId) + } catch (e) { + await runManager?.handleToolError(e) + throw e + } + await runManager?.handleToolEnd(result) + return result + } + + protected async _call(arg: z.output, _?: CallbackManagerForToolRun, overrideSessionId?: string): Promise { let sandbox: any = {} if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { @@ -70,7 +120,7 @@ export class DynamicStructuredTool< } sandbox['$env'] = env if (this.flowObj) { - sandbox['$flow'] = this.flowObj + sandbox['$flow'] = { ...this.flowObj, sessionId: overrideSessionId } } const defaultAllowBuiltInDep = [ 'assert', diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 239b13ca8..2f6855d70 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -8,7 +8,7 @@ import { DataSource } from 'typeorm' import { ICommonObject, IDatabaseEntity, IMessage, INodeData } from './Interface' import { AES, enc } from 'crypto-js' import { ChatMessageHistory } from 'langchain/memory' -import { AIMessage, HumanMessage } from 'langchain/schema' +import { AIMessage, HumanMessage, BaseMessage } from 'langchain/schema' export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}} export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank @@ -644,3 +644,31 @@ export const convertSchemaToZod = (schema: string | object): ICommonObject => { throw new Error(e) } } + +/** + * Convert BaseMessage to IMessage + * @param {BaseMessage[]} messages + * @returns {IMessage[]} + */ +export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[] => { + const formatmessages: IMessage[] = [] + for (const m of messages) { + if (m._getType() === 'human') { + formatmessages.push({ + message: m.content as string, + type: 'userMessage' + }) + } else if (m._getType() === 'ai') { + formatmessages.push({ + message: m.content as string, + type: 'apiMessage' + }) + } else if (m._getType() === 'system') { + formatmessages.push({ + message: m.content as string, + type: 'apiMessage' + }) + } + } + return formatmessages +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 99fcba70e..531533c1b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -44,8 +44,7 @@ import { checkMemorySessionId, clearSessionMemoryFromViewMessageDialog, getUserHome, - replaceChatHistory, - replaceEnvVariables + replaceChatHistory } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -1617,10 +1616,6 @@ export class App { this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) } - const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const nodeInstance = new nodeModule.nodeClass() - logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) let sessionId = undefined @@ -1634,6 +1629,10 @@ export class App { chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) } + const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const nodeInstance = new nodeModule.nodeClass({ sessionId }) + let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatflowid, From aac0546a7f1ad824beeaf392c50ce3993aae96db Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 21 Dec 2023 00:50:24 +0000 Subject: [PATCH 09/16] update all memory nodes --- .../nodes/memory/BufferMemory/BufferMemory.ts | 46 +++- .../BufferWindowMemory/BufferWindowMemory.ts | 44 +++- .../ConversationSummaryMemory.ts | 48 ++++- .../nodes/memory/DynamoDb/DynamoDb.ts | 204 ++++++++++++++++-- .../memory/MongoDBMemory/MongoDBMemory.ts | 96 +++++++-- .../memory/MotorheadMemory/MotorheadMemory.ts | 68 ++++-- .../UpstashRedisBackedChatMemory.ts | 89 ++++++-- .../nodes/memory/ZepMemory/ZepMemory.ts | 31 ++- packages/components/src/Interface.ts | 31 +++ 9 files changed, 579 insertions(+), 78 deletions(-) diff --git a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts index 7793d96d4..0ad8adec9 100644 --- a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts +++ b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts @@ -1,6 +1,7 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' -import { BufferMemory } from 'langchain/memory' +import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' +import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class BufferMemory_Memory implements INode { label: string @@ -41,7 +42,7 @@ class BufferMemory_Memory implements INode { async init(nodeData: INodeData): Promise { const memoryKey = nodeData.inputs?.memoryKey as string const inputKey = nodeData.inputs?.inputKey as string - return new BufferMemory({ + return new BufferMemoryExtended({ returnMessages: true, memoryKey, inputKey @@ -49,4 +50,41 @@ class BufferMemory_Memory implements INode { } } +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { + constructor(fields: BufferMemoryInput) { + super(fields) + } + + async getChatMessages(_?: string, returnBaseMessages = false): Promise { + const memoryResult = await this.loadMemoryVariables({}) + const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise { + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + const inputValues = { [this.inputKey ?? 'input']: input?.text } + const outputValues = { output: output?.text } + + await this.saveContext(inputValues, outputValues) + } + + async clearChatMessages(): Promise { + await this.clear() + } + + async resumeMessages(messages: IMessage[]): Promise { + // Clear existing chatHistory to avoid duplication + if (messages.length) await this.clear() + + // Insert into chatHistory + for (const msg of messages) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + } +} + module.exports = { nodeClass: BufferMemory_Memory } diff --git a/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts b/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts index 84e607e54..ca8d0ddfd 100644 --- a/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts +++ b/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts @@ -1,6 +1,7 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' +import { FlowiseWindowMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferWindowMemory, BufferWindowMemoryInput } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class BufferWindowMemory_Memory implements INode { label: string @@ -57,7 +58,44 @@ class BufferWindowMemory_Memory implements INode { k: parseInt(k, 10) } - return new BufferWindowMemory(obj) + return new BufferWindowMemoryExtended(obj) + } +} + +class BufferWindowMemoryExtended extends FlowiseWindowMemory implements MemoryMethods { + constructor(fields: BufferWindowMemoryInput) { + super(fields) + } + + async getChatMessages(_?: string, returnBaseMessages = false): Promise { + const memoryResult = await this.loadMemoryVariables({}) + const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise { + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + const inputValues = { [this.inputKey ?? 'input']: input?.text } + const outputValues = { output: output?.text } + + await this.saveContext(inputValues, outputValues) + } + + async clearChatMessages(): Promise { + await this.clear() + } + + async resumeMessages(messages: IMessage[]): Promise { + // Clear existing chatHistory to avoid duplication + if (messages.length) await this.clear() + + // Insert into chatHistory + for (const msg of messages) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } } } diff --git a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts index 332d73aa9..107ab7db9 100644 --- a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts +++ b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts @@ -1,7 +1,8 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' +import { FlowiseSummaryMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { ConversationSummaryMemory, ConversationSummaryMemoryInput } from 'langchain/memory' import { BaseLanguageModel } from 'langchain/base_language' +import { BaseMessage } from 'langchain/schema' class ConversationSummaryMemory_Memory implements INode { label: string @@ -56,7 +57,48 @@ class ConversationSummaryMemory_Memory implements INode { inputKey } - return new ConversationSummaryMemory(obj) + return new ConversationSummaryMemoryExtended(obj) + } +} + +class ConversationSummaryMemoryExtended extends FlowiseSummaryMemory implements MemoryMethods { + constructor(fields: ConversationSummaryMemoryInput) { + super(fields) + } + + async getChatMessages(_?: string, returnBaseMessages = false): Promise { + const memoryResult = await this.loadMemoryVariables({}) + const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise { + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + const inputValues = { [this.inputKey ?? 'input']: input?.text } + const outputValues = { output: output?.text } + + await this.saveContext(inputValues, outputValues) + } + + async clearChatMessages(): Promise { + await this.clear() + } + + async resumeMessages(messages: IMessage[]): Promise { + // Clear existing chatHistory to avoid duplication + if (messages.length) await this.clear() + + // Insert into chatHistory + for (const msg of messages) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + // Replace buffer + const chatMessages = await this.chatHistory.getMessages() + this.buffer = await this.predictNewSummary(chatMessages.slice(-2), this.buffer) } } diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 8ca6cf9e5..15b00d335 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -1,15 +1,25 @@ import { - ICommonObject, - INode, - INodeData, - INodeParams, + DynamoDBClient, + DynamoDBClientConfig, + GetItemCommand, + GetItemCommandInput, + UpdateItemCommand, + UpdateItemCommandInput, + DeleteItemCommand, + DeleteItemCommandInput, + AttributeValue +} from '@aws-sdk/client-dynamodb' +import { DynamoDBChatMessageHistory } from 'langchain/stores/message/dynamodb' +import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage, BaseMessage } from 'langchain/schema' +import { + convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory -} from '../../../src' -import { DynamoDBChatMessageHistory } from 'langchain/stores/message/dynamodb' -import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +} from '../../../src/utils' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class DynamoDb_Memory implements INode { label: string @@ -102,49 +112,199 @@ class DynamoDb_Memory implements INode { const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { const tableName = nodeData.inputs?.tableName as string const partitionKey = nodeData.inputs?.partitionKey as string - const sessionId = nodeData.inputs?.sessionId as string const region = nodeData.inputs?.region as string const memoryKey = nodeData.inputs?.memoryKey as string const chatId = options.chatId let isSessionIdUsingChatMessageId = false - if (!sessionId && chatId) isSessionIdUsingChatMessageId = true + let sessionId = '' + + if (!nodeData.inputs?.sessionId && chatId) { + isSessionIdUsingChatMessageId = true + sessionId = chatId + } else { + sessionId = nodeData.inputs?.sessionId + } const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessKeyId = getCredentialParam('accessKey', credentialData, nodeData) const secretAccessKey = getCredentialParam('secretAccessKey', credentialData, nodeData) + const config: DynamoDBClientConfig = { + region, + credentials: { + accessKeyId, + secretAccessKey + } + } + + const client = new DynamoDBClient(config ?? {}) + const dynamoDb = new DynamoDBChatMessageHistory({ tableName, partitionKey, - sessionId: sessionId ? sessionId : chatId, - config: { - region, - credentials: { - accessKeyId, - secretAccessKey - } - } + sessionId, + config }) const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: dynamoDb, - isSessionIdUsingChatMessageId + isSessionIdUsingChatMessageId, + sessionId, + dynamodbClient: client }) return memory } interface BufferMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean + dynamodbClient: DynamoDBClient + sessionId: string } -class BufferMemoryExtended extends BufferMemory { - isSessionIdUsingChatMessageId? = false +interface DynamoDBSerializedChatMessage { + M: { + type: { + S: string + } + text: { + S: string + } + role?: { + S: string + } + } +} - constructor(fields: BufferMemoryInput & Partial) { +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { + isSessionIdUsingChatMessageId = false + sessionId = '' + dynamodbClient: DynamoDBClient + + constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + this.sessionId = fields.sessionId + this.dynamodbClient = fields.dynamodbClient + } + + overrideDynamoKey(overrideSessionId = '') { + const existingDynamoKey = (this as any).dynamoKey + const partitionKey = (this as any).partitionKey + + let newDynamoKey: Record = {} + + if (Object.keys(existingDynamoKey).includes(partitionKey)) { + newDynamoKey[partitionKey] = { S: overrideSessionId } + } + + return Object.keys(newDynamoKey).length ? newDynamoKey : existingDynamoKey + } + + async addNewMessage( + messages: StoredMessage[], + client: DynamoDBClient, + tableName = '', + dynamoKey: Record = {}, + messageAttributeName = 'messages' + ) { + const params: UpdateItemCommandInput = { + TableName: tableName, + Key: dynamoKey, + ExpressionAttributeNames: { + '#m': messageAttributeName + }, + ExpressionAttributeValues: { + ':empty_list': { + L: [] + }, + ':m': { + L: messages.map((message) => { + const dynamoSerializedMessage: DynamoDBSerializedChatMessage = { + M: { + type: { + S: message.type + }, + text: { + S: message.data.content + } + } + } + if (message.data.role) { + dynamoSerializedMessage.M.role = { S: message.data.role } + } + return dynamoSerializedMessage + }) + } + }, + UpdateExpression: 'SET #m = list_append(if_not_exists(#m, :empty_list), :m)' + } + + await client.send(new UpdateItemCommand(params)) + } + + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { + if (!this.dynamodbClient) return [] + + const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey + const tableName = (this as any).tableName + const messageAttributeName = (this as any).messageAttributeName + + const params: GetItemCommandInput = { + TableName: tableName, + Key: dynamoKey + } + + const response = await this.dynamodbClient.send(new GetItemCommand(params)) + const items = response.Item ? response.Item[messageAttributeName]?.L ?? [] : [] + const messages = items + .map((item) => ({ + type: item.M?.type.S, + data: { + role: item.M?.role?.S, + content: item.M?.text.S + } + })) + .filter((x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined) + const baseMessages = messages.map(mapStoredMessageToChatMessage) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + if (!this.dynamodbClient) return + + const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey + const tableName = (this as any).tableName + const messageAttributeName = (this as any).messageAttributeName + + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + if (input) { + const newInputMessage = new HumanMessage(input.text) + const messageToAdd = [newInputMessage].map((msg) => msg.toDict()) + await this.addNewMessage(messageToAdd, this.dynamodbClient, tableName, dynamoKey, messageAttributeName) + } + + if (output) { + const newOutputMessage = new AIMessage(output.text) + const messageToAdd = [newOutputMessage].map((msg) => msg.toDict()) + await this.addNewMessage(messageToAdd, this.dynamodbClient, tableName, dynamoKey, messageAttributeName) + } + } + + async clearChatMessages(overrideSessionId = ''): Promise { + if (!this.dynamodbClient) return + + const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey + const tableName = (this as any).tableName + + const params: DeleteItemCommandInput = { + TableName: tableName, + Key: dynamoKey + } + await this.dynamodbClient.send(new DeleteItemCommand(params)) + await this.clear() } } diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index b654a5b20..681e9042c 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,17 +1,15 @@ +import { MongoClient, Collection, Document } from 'mongodb' +import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' +import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, BaseMessage } from 'langchain/schema' import { + convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam, - ICommonObject, - INode, - INodeData, - INodeParams, serializeChatHistory -} from '../../../src' -import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' -import { BufferMemory, BufferMemoryInput } from 'langchain/memory' -import { BaseMessage, mapStoredMessageToChatMessage } from 'langchain/schema' -import { MongoClient } from 'mongodb' +} from '../../../src/utils' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class MongoDB_Memory implements INode { label: string @@ -99,23 +97,30 @@ class MongoDB_Memory implements INode { const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { const databaseName = nodeData.inputs?.databaseName as string const collectionName = nodeData.inputs?.collectionName as string - const sessionId = nodeData.inputs?.sessionId as string const memoryKey = nodeData.inputs?.memoryKey as string const chatId = options?.chatId as string let isSessionIdUsingChatMessageId = false - if (!sessionId && chatId) isSessionIdUsingChatMessageId = true + let sessionId = '' + + if (!nodeData.inputs?.sessionId && chatId) { + isSessionIdUsingChatMessageId = true + sessionId = chatId + } else { + sessionId = nodeData.inputs?.sessionId + } const credentialData = await getCredentialData(nodeData.credential ?? '', options) - let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) const client = new MongoClient(mongoDBConnectUrl) await client.connect() + const collection = client.db(databaseName).collection(collectionName) const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, - sessionId: sessionId ? sessionId : chatId + sessionId }) mongoDBChatMessageHistory.getMessages = async (): Promise => { @@ -144,20 +149,77 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, - isSessionIdUsingChatMessageId + isSessionIdUsingChatMessageId, + sessionId, + collection }) } interface BufferMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean + collection: Collection + sessionId: string } -class BufferMemoryExtended extends BufferMemory { +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { + sessionId = '' + collection: Collection isSessionIdUsingChatMessageId? = false - constructor(fields: BufferMemoryInput & Partial) { + constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + this.sessionId = fields.sessionId + this.collection = fields.collection + } + + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { + if (!this.collection) return [] + + const id = overrideSessionId ?? this.sessionId + const document = await this.collection.findOne({ sessionId: id }) + const messages = document?.messages || [] + const baseMessages = messages.map(mapStoredMessageToChatMessage) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + if (!this.collection) return + + const id = overrideSessionId ?? this.sessionId + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + if (input) { + const newInputMessage = new HumanMessage(input.text) + const messageToAdd = [newInputMessage].map((msg) => msg.toDict()) + await this.collection.updateOne( + { sessionId: id }, + { + $push: { messages: { $each: messageToAdd } } + }, + { upsert: true } + ) + } + + if (output) { + const newOutputMessage = new AIMessage(output.text) + const messageToAdd = [newOutputMessage].map((msg) => msg.toDict()) + await this.collection.updateOne( + { sessionId: id }, + { + $push: { messages: { $each: messageToAdd } } + }, + { upsert: true } + ) + } + } + + async clearChatMessages(overrideSessionId = ''): Promise { + if (!this.collection) return + + const id = overrideSessionId ?? this.sessionId + await this.collection.deleteOne({ sessionId: id }) + await this.clear() } } diff --git a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts index fc4a06dcc..97f25ba3f 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -1,9 +1,9 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src' -import { MotorheadMemory, MotorheadMemoryInput } from 'langchain/memory' +import { MotorheadMemory, MotorheadMemoryInput, InputValues, MemoryVariables, OutputValues, getBufferString } from 'langchain/memory' import fetch from 'node-fetch' -import { getBufferString } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class MotorMemory_Memory implements INode { label: string @@ -88,19 +88,26 @@ class MotorMemory_Memory implements INode { const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): Promise => { const memoryKey = nodeData.inputs?.memoryKey as string const baseURL = nodeData.inputs?.baseURL as string - const sessionId = nodeData.inputs?.sessionId as string const chatId = options?.chatId as string let isSessionIdUsingChatMessageId = false - if (!sessionId && chatId) isSessionIdUsingChatMessageId = true + let sessionId = '' + + if (!nodeData.inputs?.sessionId && chatId) { + isSessionIdUsingChatMessageId = true + sessionId = chatId + } else { + sessionId = nodeData.inputs?.sessionId + } const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const clientId = getCredentialParam('clientId', credentialData, nodeData) - let obj: MotorheadMemoryInput & Partial = { + let obj: MotorheadMemoryInput & MotorheadMemoryExtendedInput = { returnMessages: true, - sessionId: sessionId ? sessionId : chatId, + isSessionIdUsingChatMessageId, + sessionId, memoryKey } @@ -117,8 +124,6 @@ const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): } } - if (isSessionIdUsingChatMessageId) obj.isSessionIdUsingChatMessageId = true - const motorheadMemory = new MotorheadMemoryExtended(obj) // Get messages from sessionId @@ -134,12 +139,29 @@ interface MotorheadMemoryExtendedInput { class MotorheadMemoryExtended extends MotorheadMemory { isSessionIdUsingChatMessageId? = false - constructor(fields: MotorheadMemoryInput & Partial) { + constructor(fields: MotorheadMemoryInput & MotorheadMemoryExtendedInput) { super(fields) this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId } - async clear(): Promise { + async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + this.sessionId = overrideSessionId + } + return super.loadMemoryVariables({ values }) + } + + async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + this.sessionId = overrideSessionId + } + return super.saveContext(inputValues, outputValues) + } + + async clear(overrideSessionId = ''): Promise { + if (overrideSessionId) { + this.sessionId = overrideSessionId + } try { await this.caller.call(fetch, `${this.url}/sessions/${this.sessionId}/memory`, { //@ts-ignore @@ -155,6 +177,28 @@ class MotorheadMemoryExtended extends MotorheadMemory { await this.chatHistory.clear() await super.clear() } + + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { + const id = overrideSessionId ?? this.sessionId + const memoryVariables = await this.loadMemoryVariables({}, id) + const baseMessages = memoryVariables[this.memoryKey] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + const id = overrideSessionId ?? this.sessionId + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + const inputValues = { [this.inputKey ?? 'input']: input?.text } + const outputValues = { output: output?.text } + + await this.saveContext(inputValues, outputValues, id) + } + + async clearChatMessages(overrideSessionId = ''): Promise { + const id = overrideSessionId ?? this.sessionId + await this.clear(id) + } } module.exports = { nodeClass: MotorMemory_Memory } diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 8bca04404..3ff20a882 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -1,8 +1,16 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory } from '../../../src/utils' -import { ICommonObject } from '../../../src' +import { Redis } from '@upstash/redis' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { UpstashRedisChatMessageHistory } from 'langchain/stores/message/upstash_redis' +import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage, BaseMessage } from 'langchain/schema' +import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { + convertBaseMessagetoIMessage, + getBaseClasses, + getCredentialData, + getCredentialParam, + serializeChatHistory +} from '../../../src/utils' +import { ICommonObject } from '../../../src/Interface' class UpstashRedisBackedChatMemory_Memory implements INode { label: string @@ -84,29 +92,39 @@ class UpstashRedisBackedChatMemory_Memory implements INode { const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const baseURL = nodeData.inputs?.baseURL as string - const sessionId = nodeData.inputs?.sessionId as string const sessionTTL = nodeData.inputs?.sessionTTL as string const chatId = options?.chatId as string let isSessionIdUsingChatMessageId = false - if (!sessionId && chatId) isSessionIdUsingChatMessageId = true + let sessionId = '' + + if (!nodeData.inputs?.sessionId && chatId) { + isSessionIdUsingChatMessageId = true + sessionId = chatId + } else { + sessionId = nodeData.inputs?.sessionId + } const credentialData = await getCredentialData(nodeData.credential ?? '', options) const upstashRestToken = getCredentialParam('upstashRestToken', credentialData, nodeData) + const client = new Redis({ + url: baseURL, + token: upstashRestToken + }) + const redisChatMessageHistory = new UpstashRedisChatMessageHistory({ - sessionId: sessionId ? sessionId : chatId, + sessionId, sessionTTL: sessionTTL ? parseInt(sessionTTL, 10) : undefined, - config: { - url: baseURL, - token: upstashRestToken - } + client }) const memory = new BufferMemoryExtended({ memoryKey: 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId + isSessionIdUsingChatMessageId, + sessionId, + redisClient: client }) return memory @@ -114,14 +132,59 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject interface BufferMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean + redisClient: Redis + sessionId: string } -class BufferMemoryExtended extends BufferMemory { +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { isSessionIdUsingChatMessageId? = false + sessionId = '' + redisClient: Redis - constructor(fields: BufferMemoryInput & Partial) { + constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + this.sessionId = fields.sessionId + this.redisClient = fields.redisClient + } + + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { + if (!this.redisClient) return [] + + const id = overrideSessionId ?? this.sessionId + const rawStoredMessages: StoredMessage[] = await this.redisClient.lrange(id, 0, -1) + const orderedMessages = rawStoredMessages.reverse() + const previousMessages = orderedMessages.filter((x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined) + const baseMessages = previousMessages.map(mapStoredMessageToChatMessage) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + if (!this.redisClient) return + + const id = overrideSessionId ?? this.sessionId + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + + if (input) { + const newInputMessage = new HumanMessage(input.text) + const messageToAdd = [newInputMessage].map((msg) => msg.toDict()) + await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0])) + } + + if (output) { + const newOutputMessage = new AIMessage(output.text) + const messageToAdd = [newOutputMessage].map((msg) => msg.toDict()) + await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0])) + } + } + + async clearChatMessages(overrideSessionId = ''): Promise { + if (!this.redisClient) return + + const id = overrideSessionId ?? this.sessionId + await this.redisClient.del(id) + await this.clear() } } diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index ea52cb0b3..3da35db27 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -1,8 +1,9 @@ +import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' -import { getBufferString, InputValues, MemoryVariables, OutputValues } from 'langchain/memory' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src' +import { InputValues, MemoryVariables, OutputValues, getBufferString } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' class ZepMemory_Memory implements INode { label: string @@ -147,7 +148,7 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis const obj: ZepMemoryInput & ZepMemoryExtendedInput = { baseURL, - sessionId: sessionId ? sessionId : chatId, + sessionId, aiPrefix, humanPrefix, returnMessages: true, @@ -196,6 +197,28 @@ class ZepMemoryExtended extends ZepMemory { } return super.clear() } + + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { + const id = overrideSessionId ?? this.sessionId + const memoryVariables = await this.loadMemoryVariables({}, id) + const baseMessages = memoryVariables[this.memoryKey] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + const id = overrideSessionId ?? this.sessionId + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + const inputValues = { [this.inputKey ?? 'input']: input?.text } + const outputValues = { output: output?.text } + + await this.saveContext(inputValues, outputValues, id) + } + + async clearChatMessages(overrideSessionId = ''): Promise { + const id = overrideSessionId ?? this.sessionId + await this.clear(id) + } } module.exports = { nodeClass: ZepMemory_Memory } diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index bc50155c3..e508ebeee 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -196,3 +196,34 @@ export class VectorStoreRetriever { this.vectorStore = fields.vectorStore } } + +/** + * Implement abstract classes and interface for memory + */ +import { BaseMessage } from 'langchain/schema' +import { BufferMemory, BufferWindowMemory, ConversationSummaryMemory } from 'langchain/memory' + +export interface MemoryMethods { + getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise + addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + clearChatMessages(overrideSessionId?: string): Promise + resumeMessages?(messages: IMessage[]): Promise +} + +export abstract class FlowiseMemory extends BufferMemory implements MemoryMethods { + abstract getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise + abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + abstract clearChatMessages(overrideSessionId?: string): Promise +} + +export abstract class FlowiseWindowMemory extends BufferWindowMemory implements MemoryMethods { + abstract getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise + abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + abstract clearChatMessages(overrideSessionId?: string): Promise +} + +export abstract class FlowiseSummaryMemory extends ConversationSummaryMemory implements MemoryMethods { + abstract getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise + abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise + abstract clearChatMessages(overrideSessionId?: string): Promise +} From 19f2989c8317b88e1ec3cb57cb6513069273fdad Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 21 Dec 2023 01:14:53 +0000 Subject: [PATCH 10/16] rename agent executor name --- .../OpenAIFunctionAgent.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 63749fd2e..275eb950f 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,5 +1,5 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { AgentExecutor, AgentExecutorInput } from 'langchain/agents' +import { AgentExecutor as LCAgentExecutor, AgentExecutorInput } from 'langchain/agents' import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' import { OutputParserException } from 'langchain/schema/output_parser' import { CallbackManagerForChainRun } from 'langchain/callbacks' @@ -34,7 +34,7 @@ class OpenAIFunctionAgent_Agents implements INode { this.category = 'Agents' this.icon = 'function.svg' this.description = `An agent that uses Function Calling to pick the tool and args to call` - this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] + this.baseClasses = [this.type, ...getBaseClasses(LCAgentExecutor)] this.inputs = [ { label: 'Allowed Tools', @@ -124,12 +124,13 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { const systemMessage = nodeData.inputs?.systemMessage as string let tools = nodeData.inputs?.tools tools = flatten(tools) - const memoryKey = memory.memoryKey ?? 'chat_history' + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' const prompt = ChatPromptTemplate.fromMessages([ - ['ai', systemMessage ?? `You are a helpful AI assistant.`], + ['ai', systemMessage ? systemMessage : `You are a helpful AI assistant.`], new MessagesPlaceholder(memoryKey), - ['human', '{input}'], + ['human', `{${inputKey}}`], new MessagesPlaceholder('agent_scratchpad') ]) @@ -139,7 +140,7 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { const runnableAgent = RunnableSequence.from([ { - input: (i: { input: string; steps: AgentStep[] }) => i.input, + [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, agent_scratchpad: (i: { input: string; steps: AgentStep[] }) => formatAgentSteps(i.steps), [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { const messages: BaseMessage[] = await memory.getChatMessages(sessionId, true) @@ -151,7 +152,7 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { new OpenAIFunctionsAgentOutputParser() ]) - const executor = AgentExecutorExtended.fromAgentAndTools({ + const executor = AgentExecutor.fromAgentAndTools({ agent: runnableAgent, tools, sessionId @@ -162,11 +163,11 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { type AgentExecutorOutput = ChainValues -class AgentExecutorExtended extends AgentExecutor { +class AgentExecutor extends LCAgentExecutor { sessionId?: string - static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string }): AgentExecutorExtended { - const newInstance = new AgentExecutorExtended(fields) + static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string }): AgentExecutor { + const newInstance = new AgentExecutor(fields) if (fields.sessionId) newInstance.sessionId = fields.sessionId return newInstance } From cc7debda0e1275c4c5a01e4222526dda873cf7da Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 21 Dec 2023 01:52:03 +0000 Subject: [PATCH 11/16] update openai assistant tool --- .../components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 42686ae0f..c2d0e782f 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -265,7 +265,7 @@ class OpenAIAssistant_Agents implements INode { // Start tool analytics const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) - const toolOutput = await tool.call(actions[i].toolInput) + const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, threadId) // End tool analytics await analyticHandlers.onToolEnd(toolIds, toolOutput) From f0cdf48d54fb73af7440744dd303aa8d0d6ff465 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 21 Dec 2023 17:28:25 +0000 Subject: [PATCH 12/16] update ui changes --- .../nodes/tools/CustomTool/CustomTool.ts | 11 +-- .../components/nodes/tools/CustomTool/core.ts | 15 ++-- .../ui/src/assets/images/variables_empty.svg | 1 + packages/ui/src/menu-items/dashboard.js | 2 +- .../ui/src/views/canvas/NodeInputHandler.js | 7 +- .../src/views/tools/HowToUseFunctionDialog.js | 65 +++++++++++++++++ packages/ui/src/views/tools/ToolDialog.js | 13 ++++ .../views/variables/AddEditVariableDialog.js | 54 +++++++------- .../variables/HowToUseVariablesDialog.js | 72 +++++++++++++++++++ packages/ui/src/views/variables/index.js | 25 +++++-- 10 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 packages/ui/src/assets/images/variables_empty.svg create mode 100644 packages/ui/src/views/tools/HowToUseFunctionDialog.js create mode 100644 packages/ui/src/views/variables/HowToUseVariablesDialog.js diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index 6b0397c84..9ceda9199 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -85,26 +85,29 @@ class CustomTool_Tools implements INode { // override variables defined in overrideConfig // nodeData.inputs.variables is an Object, check each property and override the variable - if (nodeData?.inputs?.envVars) { - for (const propertyName of Object.getOwnPropertyNames(nodeData.inputs.envVars)) { + if (nodeData?.inputs?.vars) { + for (const propertyName of Object.getOwnPropertyNames(nodeData.inputs.vars)) { const foundVar = variables.find((v) => v.name === propertyName) if (foundVar) { // even if the variable was defined as runtime, we override it with static value foundVar.type = 'static' - foundVar.value = nodeData.inputs.envVars[propertyName] + foundVar.value = nodeData.inputs.vars[propertyName] } else { // add it the variables, if not found locally in the db - variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.envVars[propertyName] }) + variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.vars[propertyName] }) } } } + const flow = { chatId: options.chatId, // id is uppercase (I) chatflowId: options.chatflowid // id is lowercase (i) } + let dynamicStructuredTool = new DynamicStructuredTool(obj) dynamicStructuredTool.setVariables(variables) dynamicStructuredTool.setFlowObject(flow) + return dynamicStructuredTool } catch (e) { throw new Error(e) diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 77aa0e6b4..338b0ae9a 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -102,15 +102,19 @@ export class DynamicStructuredTool< sandbox[`$${item}`] = arg[item] } } - //inject variables - let env = {} + + // inject variables + let vars = {} if (this.variables) { for (const item of this.variables) { let value = item.value + + // read from .env file if (item.type === 'runtime') { value = process.env[item.name] } - Object.defineProperty(env, item.name, { + + Object.defineProperty(vars, item.name, { enumerable: true, configurable: true, writable: true, @@ -118,10 +122,13 @@ export class DynamicStructuredTool< }) } } - sandbox['$env'] = env + sandbox['$vars'] = vars + + // inject flow properties if (this.flowObj) { sandbox['$flow'] = { ...this.flowObj, sessionId: overrideSessionId } } + const defaultAllowBuiltInDep = [ 'assert', 'buffer', diff --git a/packages/ui/src/assets/images/variables_empty.svg b/packages/ui/src/assets/images/variables_empty.svg new file mode 100644 index 000000000..eb461e39f --- /dev/null +++ b/packages/ui/src/assets/images/variables_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/menu-items/dashboard.js b/packages/ui/src/menu-items/dashboard.js index b0e5d66e6..793bc290c 100644 --- a/packages/ui/src/menu-items/dashboard.js +++ b/packages/ui/src/menu-items/dashboard.js @@ -53,7 +53,7 @@ const dashboard = { }, { id: 'variables', - title: 'Environment Variables', + title: 'Variables', type: 'item', url: '/variables', icon: icons.IconVariable, diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 33e997362..617d1066c 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -369,7 +369,12 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam?.acceptVariable && ( <> {dialogProps.type !== 'TEMPLATE' && ( { CredentialEmptySVG
No Variables Yet
@@ -267,7 +273,13 @@ const Variables = () => { {variable.value} - {variable.type === 'static' ? 'Static Variable' : 'Runtime Variable'} + + + {moment(variable.updatedDate).format('DD-MMM-YY')} {moment(variable.createdDate).format('DD-MMM-YY')} @@ -293,6 +305,7 @@ const Variables = () => { onCancel={() => setShowVariableDialog(false)} onConfirm={onConfirm} > + setShowHowToDialog(false)}> ) From 3126442e67cfccdf14772eb6f539c765c2b9171d Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 22 Dec 2023 16:53:20 +0000 Subject: [PATCH 13/16] update variables option --- .../views/variables/AddEditVariableDialog.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.js b/packages/ui/src/views/variables/AddEditVariableDialog.js index b84d0525f..933039e72 100644 --- a/packages/ui/src/views/variables/AddEditVariableDialog.js +++ b/packages/ui/src/views/variables/AddEditVariableDialog.js @@ -55,25 +55,31 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const [variableValue, setVariableValue] = useState('') const [variableType, setVariableType] = useState('static') const [dialogType, setDialogType] = useState('ADD') - const [variable, setVariable] = useState({}) useEffect(() => { - if (dialogProps.type === 'EDIT') { - // When variable dialog is opened from Variables dashboard + if (dialogProps.type === 'EDIT' && dialogProps.data) { setVariableName(dialogProps.data.name) setVariableValue(dialogProps.data.value) setVariableType(dialogProps.data.type) - setVariable(dialogProps.data) setDialogType('EDIT') + setVariable(dialogProps.data) } else if (dialogProps.type === 'ADD') { - // When variable dialog is to add a new variable setVariableName('') setVariableValue('') setVariableType('static') setDialogType('ADD') + setVariable({}) } - }, [dialogProps.data, dialogProps.type]) + + return () => { + setVariableName('') + setVariableValue('') + setVariableType('static') + setDialogType('ADD') + setVariable({}) + } + }, [dialogProps]) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -226,10 +232,11 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
setVariableType(newValue)} - value={variableType} + value={variableType ?? 'choose an option'} />
{variableType === 'static' && ( From 6306904cfcf02facee9a7896aad0f35ee628c9b8 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 26 Dec 2023 15:54:50 +0000 Subject: [PATCH 14/16] add missing methods, abstract classes --- .../OpenAIFunctionAgent.ts | 11 ++++---- .../memory/MotorheadMemory/MotorheadMemory.ts | 4 +-- .../nodes/memory/ZepMemory/ZepMemory.ts | 4 +-- packages/components/src/Interface.ts | 3 +++ packages/server/src/utils/index.ts | 26 ------------------- packages/ui/src/views/variables/index.js | 2 +- 6 files changed, 13 insertions(+), 37 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 275eb950f..c0095cee1 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,4 +1,4 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { AgentExecutor as LCAgentExecutor, AgentExecutorInput } from 'langchain/agents' import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' import { OutputParserException } from 'langchain/schema/output_parser' @@ -7,7 +7,6 @@ import { formatToOpenAIFunction } from 'langchain/tools' import { ToolInputParsingException, Tool } from '@langchain/core/tools' import { getBaseClasses } from '../../../src/utils' import { flatten } from 'lodash' -import { BaseChatMemory } from 'langchain/memory' import { RunnableSequence } from 'langchain/schema/runnable' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' @@ -65,7 +64,7 @@ class OpenAIFunctionAgent_Agents implements INode { } async init(nodeData: INodeData): Promise { - const memory = nodeData.inputs?.memory as BaseChatMemory + const memory = nodeData.inputs?.memory as FlowiseMemory const executor = prepareAgent(nodeData, this.sessionId) if (memory) executor.memory = memory @@ -74,7 +73,7 @@ class OpenAIFunctionAgent_Agents implements INode { } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const memory = nodeData.inputs?.memory + const memory = nodeData.inputs?.memory as FlowiseMemory const executor = prepareAgent(nodeData, this.sessionId) @@ -120,7 +119,7 @@ const formatAgentSteps = (steps: AgentStep[]): BaseMessage[] => const prepareAgent = (nodeData: INodeData, sessionId?: string) => { const model = nodeData.inputs?.model as ChatOpenAI - const memory = nodeData.inputs?.memory + const memory = nodeData.inputs?.memory as FlowiseMemory const systemMessage = nodeData.inputs?.systemMessage as string let tools = nodeData.inputs?.tools tools = flatten(tools) @@ -143,7 +142,7 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, agent_scratchpad: (i: { input: string; steps: AgentStep[] }) => formatAgentSteps(i.steps), [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { - const messages: BaseMessage[] = await memory.getChatMessages(sessionId, true) + const messages = (await memory.getChatMessages(sessionId, true)) as BaseMessage[] return messages ?? [] } }, diff --git a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts index 97f25ba3f..938cc8731 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -1,4 +1,4 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src' import { MotorheadMemory, MotorheadMemoryInput, InputValues, MemoryVariables, OutputValues, getBufferString } from 'langchain/memory' @@ -136,7 +136,7 @@ interface MotorheadMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean } -class MotorheadMemoryExtended extends MotorheadMemory { +class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { isSessionIdUsingChatMessageId? = false constructor(fields: MotorheadMemoryInput & MotorheadMemoryExtendedInput) { diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index 3da35db27..4dda76df1 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -1,4 +1,4 @@ -import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface' +import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' @@ -167,7 +167,7 @@ interface ZepMemoryExtendedInput { k?: number } -class ZepMemoryExtended extends ZepMemory { +class ZepMemoryExtended extends ZepMemory implements MemoryMethods { isSessionIdUsingChatMessageId? = false lastN?: number diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index e508ebeee..2a625ff6a 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -214,16 +214,19 @@ export abstract class FlowiseMemory extends BufferMemory implements MemoryMethod abstract getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise abstract clearChatMessages(overrideSessionId?: string): Promise + abstract resumeMessages(messages: IMessage[]): Promise } export abstract class FlowiseWindowMemory extends BufferWindowMemory implements MemoryMethods { abstract getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise abstract clearChatMessages(overrideSessionId?: string): Promise + abstract resumeMessages(messages: IMessage[]): Promise } export abstract class FlowiseSummaryMemory extends ConversationSummaryMemory implements MemoryMethods { abstract getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise abstract addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId?: string): Promise abstract clearChatMessages(overrideSessionId?: string): Promise + abstract resumeMessages(messages: IMessage[]): Promise } diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 41e97cc13..99e3813b1 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1052,29 +1052,3 @@ export const getAllValuesFromJson = (obj: any): any[] => { extractValues(obj) return values } - -export const replaceEnvVariables = async (question: string, appDataSource: DataSource): Promise => { - // the incoming question can have more than one env variable with the pattern {{ env.VARIABLE_NAME }} - // extract all the env variables from the question and iterate through them - const envVariables = question.match(/{{[^}]*}}/g) - if (envVariables) { - for (const envVariable of envVariables) { - // this is needed as the user can have spaces between the curly braces and the env keyword - // extract the variable name from the env variable - const variableName = envVariable.replace(/{{\s*env.|\s*}}/g, '') - // get the value of the env variable from the database - const variable = await appDataSource.getRepository(Variable).findOneBy({ - name: variableName - }) - if (variable) { - let value = variable.value - if (variable.type === 'runtime') { - value = process.env[variable.name] as string - } - // replace the env variable with the value from the database - question = question.replace(envVariable, value) - } - } - } - return question -} diff --git a/packages/ui/src/views/variables/index.js b/packages/ui/src/views/variables/index.js index ca8a3deef..9d0b2e3fe 100644 --- a/packages/ui/src/views/variables/index.js +++ b/packages/ui/src/views/variables/index.js @@ -215,7 +215,7 @@ const Variables = () => { - {variables.length <= 0 && ( + {variables.length === 0 && ( Date: Tue, 26 Dec 2023 16:00:46 +0000 Subject: [PATCH 15/16] ability to use flow.input within custom tool --- packages/components/nodes/tools/CustomTool/CustomTool.ts | 5 +++-- packages/ui/src/views/tools/HowToUseFunctionDialog.js | 3 +++ packages/ui/src/views/tools/ToolDialog.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index 9ceda9199..6ffcc0e21 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -60,7 +60,7 @@ class CustomTool_Tools implements INode { } } - async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { const selectedToolId = nodeData.inputs?.selectedTool as string const customToolFunc = nodeData.inputs?.customToolFunc as string @@ -101,7 +101,8 @@ class CustomTool_Tools implements INode { const flow = { chatId: options.chatId, // id is uppercase (I) - chatflowId: options.chatflowid // id is lowercase (i) + chatflowId: options.chatflowid, // id is lowercase (i) + input } let dynamicStructuredTool = new DynamicStructuredTool(obj) diff --git a/packages/ui/src/views/tools/HowToUseFunctionDialog.js b/packages/ui/src/views/tools/HowToUseFunctionDialog.js index 00acef0d2..47ecee89c 100644 --- a/packages/ui/src/views/tools/HowToUseFunctionDialog.js +++ b/packages/ui/src/views/tools/HowToUseFunctionDialog.js @@ -43,6 +43,9 @@ const HowToUseFunctionDialog = ({ show, onCancel }) => {
  • $flow.chatflowId
  • +
  • + $flow.input +
  • diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js index b011c07ed..ab6d6aa01 100644 --- a/packages/ui/src/views/tools/ToolDialog.js +++ b/packages/ui/src/views/tools/ToolDialog.js @@ -33,7 +33,7 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' const exampleAPIFunc = `/* * You can use any libraries imported in Flowise * You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid -* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId +* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input * You can get custom variables: $vars. * Must return a string value at the end of function */ From b6d08268d48b2ff3122f90342e5f45d89cac09c3 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 26 Dec 2023 16:13:14 +0000 Subject: [PATCH 16/16] add abstract methods --- packages/components/nodes/memory/DynamoDb/DynamoDb.ts | 4 ++++ .../nodes/memory/MongoDBMemory/MongoDBMemory.ts | 4 ++++ .../memory/RedisBackedChatMemory/RedisBackedChatMemory.ts | 8 ++++++-- .../UpstashRedisBackedChatMemory.ts | 4 ++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 15b00d335..872ec0b51 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -306,6 +306,10 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { await this.dynamodbClient.send(new DeleteItemCommand(params)) await this.clear() } + + async resumeMessages(): Promise { + return + } } module.exports = { nodeClass: DynamoDb_Memory } diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 681e9042c..b422921e6 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -221,6 +221,10 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { await this.collection.deleteOne({ sessionId: id }) await this.clear() } + + async resumeMessages(): Promise { + return + } } module.exports = { nodeClass: MongoDB_Memory } diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index 72af1cb5d..a02df3ea2 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -1,4 +1,4 @@ -import { INode, INodeData, INodeParams, ICommonObject, IMessage, MessageType } from '../../../src/Interface' +import { INode, INodeData, INodeParams, ICommonObject, IMessage, MessageType, FlowiseMemory, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, @@ -187,7 +187,7 @@ interface BufferMemoryExtendedInput { sessionId: string } -class BufferMemoryExtended extends BufferMemory { +class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { isSessionIdUsingChatMessageId? = false sessionId = '' redisClient: Redis @@ -236,6 +236,10 @@ class BufferMemoryExtended extends BufferMemory { await this.redisClient.del(id) await this.clear() } + + async resumeMessages(): Promise { + return + } } module.exports = { nodeClass: RedisBackedChatMemory_Memory } diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 3ff20a882..c3f971231 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -186,6 +186,10 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { await this.redisClient.del(id) await this.clear() } + + async resumeMessages(): Promise { + return + } } module.exports = { nodeClass: UpstashRedisBackedChatMemory_Memory }