From 0f464fddb8c787334385320db768f991fbc6bb2d Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 07:22:08 +0530 Subject: [PATCH 001/502] Conversation Starters: Initial commit --- .../cards/StarterConversationCard.js | 49 ++++++++++++++++ .../ui/src/views/chatflows/Configuration.js | 57 ++++++++++++++----- .../ui/src/views/chatmessage/ChatMessage.js | 15 ++++- 3 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 packages/ui/src/ui-component/cards/StarterConversationCard.js diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.js b/packages/ui/src/ui-component/cards/StarterConversationCard.js new file mode 100644 index 00000000..946c4c11 --- /dev/null +++ b/packages/ui/src/ui-component/cards/StarterConversationCard.js @@ -0,0 +1,49 @@ +import Chip from '@mui/material/Chip' +import Box from '@mui/material/Box' +import PropTypes from 'prop-types' +import { MenuItem, Select } from '@mui/material' + +const StarterConversationCard = ({ isGrid, chipsData, onChipClick }) => { + if (isGrid) { + const chipStyle = { + margin: '5px', + width: 'calc(50% - 10px)' + } + + return ( + + {chipsData.map((chipLabel, index) => ( + onChipClick(chipLabel)} /> + ))} + + ) + } else { + return ( + + ) + } +} + +StarterConversationCard.propTypes = { + isGrid: PropTypes.bool, + chipsData: PropTypes.arrayOf(PropTypes.string), + onChipClick: PropTypes.func +} + +export default StarterConversationCard diff --git a/packages/ui/src/views/chatflows/Configuration.js b/packages/ui/src/views/chatflows/Configuration.js index d569020b..9dc74090 100644 --- a/packages/ui/src/views/chatflows/Configuration.js +++ b/packages/ui/src/views/chatflows/Configuration.js @@ -4,6 +4,10 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import PropTypes from 'prop-types' import { Box, Typography, Button, OutlinedInput } from '@mui/material' +import Accordion from '@mui/material/Accordion' +import AccordionSummary from '@mui/material/AccordionSummary' +import AccordionDetails from '@mui/material/AccordionDetails' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' // Project import import { StyledButton } from 'ui-component/button/StyledButton' @@ -32,6 +36,10 @@ const Configuration = () => { const [limitMax, setLimitMax] = useState(apiConfig?.rateLimit?.limitMax ?? '') const [limitDuration, setLimitDuration] = useState(apiConfig?.rateLimit?.limitDuration ?? '') const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? '') + const [prompt1, setPrompt1] = useState(apiConfig?.prompt?.prompt1 ?? '') + const [prompt2, setPrompt2] = useState(apiConfig?.prompt?.prompt2 ?? '') + const [prompt3, setPrompt3] = useState(apiConfig?.prompt?.prompt3 ?? '') + const [prompt4, setPrompt4] = useState(apiConfig?.prompt?.prompt4 ?? '') const formatObj = () => { const obj = { @@ -111,7 +119,7 @@ const Configuration = () => { return (
- {fieldLabel} + {fieldLabel && {fieldLabel}} { return ( <> - {/*Rate Limit*/} - - Rate Limit{' '} - Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' - } - /> - - {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} - {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} - {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} + + } aria-controls='panel1a-content' id='panel1a-header'> + + Rate Limit{' '} + Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' + } + /> + + + + + {/*Rate Limit*/} + {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} + {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} + {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} + + + + + } aria-controls='panel2a-content' id='panel2a-header'> + Conversation Starters + + + + {textField(prompt1, 'prompt1', 'Starter Prompts', 'string')} + {textField(prompt2, 'prompt2', '', 'string')} + {textField(prompt3, 'prompt3', '', 'string')} + {textField(prompt4, 'prompt4', '', 'string')} + + + onSave()}> Save Changes diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0cf5695b..d047a3aa 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -32,6 +32,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' +import StarterConversationCard from '../../ui-component/cards/StarterConversationCard' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -103,9 +104,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, 100) } + const handlePromptClick = async (prompt) => { + setUserInput(prompt) + await handleSubmit() + } + // Handle form submission const handleSubmit = async (e) => { - e.preventDefault() + if (e) e.preventDefault() if (userInput.trim() === '') { return @@ -369,6 +375,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
+ {messages && messages.length === 1 && ( + + )} Date: Tue, 21 Nov 2023 17:15:39 +0530 Subject: [PATCH 002/502] Conversation Starters: Initial Implementation --- .../server/src/database/entities/ChatFlow.ts | 3 + ...00565042576-AddStarterPromptsToChatFlow.ts | 12 ++ .../src/database/migrations/mysql/index.ts | 4 +- ...00565042576-AddStarterPromptsToChatFlow.ts | 11 + .../src/database/migrations/postgres/index.ts | 4 +- ...00565042576-AddStarterPromptsToChatFlow.ts | 11 + .../src/database/migrations/sqlite/index.ts | 4 +- packages/ui/src/menu-items/settings.js | 11 +- .../cards/StarterConversationCard.css | 16 ++ .../cards/StarterConversationCard.js | 53 ++--- .../dialog/ConversationStarterDialog.js | 188 ++++++++++++++++++ packages/ui/src/views/canvas/CanvasHeader.js | 14 ++ .../ui/src/views/chatflows/Configuration.js | 57 ++---- .../ui/src/views/chatmessage/ChatMessage.js | 24 ++- 14 files changed, 319 insertions(+), 93 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts create mode 100644 packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts create mode 100644 packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts create mode 100644 packages/ui/src/ui-component/cards/StarterConversationCard.css create mode 100644 packages/ui/src/ui-component/dialog/ConversationStarterDialog.js diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 376a100b..626f743a 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -28,6 +28,9 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true, type: 'text' }) apiConfig?: string + @Column({ nullable: true, type: 'text' }) + starterPrompt?: string + @Column({ nullable: true, type: 'text' }) analytic?: string diff --git a/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts new file mode 100644 index 00000000..1f7a3c54 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddStarterPrompt1700565042576 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'starterPrompt') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`starterPrompt\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`starterPrompt\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a95..f38a9cfe 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddStarterPrompt1700565042576 ] diff --git a/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts new file mode 100644 index 00000000..ac2694b9 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddStarterPrompt1700565042576 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "starterPrompt" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "starterPrompt";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b..e9018265 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddStarterPrompt1700565042576 ] diff --git a/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts new file mode 100644 index 00000000..3d180797 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddStarterPrompt1700565042576 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "starterPrompt" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "starterPrompt";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc40..414d755e 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddStarterPrompt1700565042576 ] diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 307bd0bd..9224b78f 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,8 +1,8 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } from '@tabler/icons' // constant -const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } +const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -11,6 +11,13 @@ const settings = { title: '', type: 'group', children: [ + { + id: 'conversationStarters', + title: 'Set Starter Prompts', + type: 'item', + url: '', + icon: icons.IconPictureInPictureOff + }, { id: 'viewMessages', title: 'View Messages', diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.css b/packages/ui/src/ui-component/cards/StarterConversationCard.css new file mode 100644 index 00000000..8eb3b0ad --- /dev/null +++ b/packages/ui/src/ui-component/cards/StarterConversationCard.css @@ -0,0 +1,16 @@ +.button-container { + display: flex; + overflow-x: auto; + -webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */ + scrollbar-width: none; /* For Firefox */ +} + +/*.button-container::-webkit-scrollbar {*/ +/* display: none; !* For Chrome, Safari, and Opera *!*/ +/*}*/ + +.button { + flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ + margin: 5px; /* Adjust as needed for spacing between buttons */ + /* Add additional button styling here */ +} diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.js b/packages/ui/src/ui-component/cards/StarterConversationCard.js index 946c4c11..aa50f266 100644 --- a/packages/ui/src/ui-component/cards/StarterConversationCard.js +++ b/packages/ui/src/ui-component/cards/StarterConversationCard.js @@ -1,49 +1,24 @@ -import Chip from '@mui/material/Chip' import Box from '@mui/material/Box' import PropTypes from 'prop-types' -import { MenuItem, Select } from '@mui/material' +import { Button } from '@mui/material' +import './StarterConversationCard.css' -const StarterConversationCard = ({ isGrid, chipsData, onChipClick }) => { - if (isGrid) { - const chipStyle = { - margin: '5px', - width: 'calc(50% - 10px)' - } - - return ( - - {chipsData.map((chipLabel, index) => ( - onChipClick(chipLabel)} /> - ))} - - ) - } else { - return ( - - ) - } +const StarterConversationCard = ({ isGrid, starterPrompts, onPromptClick }) => { + return ( + + {starterPrompts.map((sp, index) => ( + + ))} + + ) } StarterConversationCard.propTypes = { isGrid: PropTypes.bool, - chipsData: PropTypes.arrayOf(PropTypes.string), - onChipClick: PropTypes.func + starterPrompts: PropTypes.arrayOf(PropTypes.string), + onPromptClick: PropTypes.func } export default StarterConversationCard diff --git a/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js b/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js new file mode 100644 index 00000000..1139f21e --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js @@ -0,0 +1,188 @@ +import { createPortal } from 'react-dom' +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' + +// material-ui +import { Button, IconButton, Dialog, DialogContent, OutlinedInput, DialogTitle, DialogActions, Box, List, Divider } from '@mui/material' +import { IconX, IconTrash, IconPlus } from '@tabler/icons' + +// Project import +import { StyledButton } from 'ui-component/button/StyledButton' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import useNotifier from 'utils/useNotifier' + +// API +import chatflowsApi from 'api/chatflows' + +const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [inputFields, setInputFields] = useState([ + { + prompt: '' + } + ]) + + const addInputField = () => { + setInputFields([ + ...inputFields, + { + prompt: '' + } + ]) + } + const removeInputFields = (index) => { + const rows = [...inputFields] + rows.splice(index, 1) + setInputFields(rows) + } + + const handleChange = (index, evnt) => { + const { name, value } = evnt.target + const list = [...inputFields] + list[index][name] = value + setInputFields(list) + } + + const onSave = async () => { + try { + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + starterPrompt: JSON.stringify(inputFields) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Conversation Starter Prompts Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + onCancel() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Conversation Starter Prompts: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.starterPrompt) { + try { + setInputFields(JSON.parse(dialogProps.chatflow.starterPrompt)) + } catch (e) { + setInputFields([ + { + prompt: '' + } + ]) + console.error(e) + } + } + + return () => {} + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + Set Conversation Starter Prompts + + + + + {inputFields.map((data, index) => { + return ( +
+ + handleChange(index, e)} + value={data.prompt} + name='prompt' + /> + + + {inputFields.length !== 1 && ( + + + + )} + + + {index === inputFields.length - 1 && ( + + + + )} + +
+ ) + })} +
+
+
+ + + + Cancel + + + Save + + +
+ ) : null + + return createPortal(component, portalElement) +} + +ConversationStarterDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default ConversationStarterDialog diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index 56365ba8..d931ee26 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -16,6 +16,7 @@ import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog' import APICodeDialog from 'views/chatflows/APICodeDialog' import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog' import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog' +import ConversationStarterDialog from 'ui-component/dialog/ConversationStarterDialog' // API import chatflowsApi from 'api/chatflows' @@ -45,6 +46,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [apiDialogProps, setAPIDialogProps] = useState({}) const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false) const [analyseDialogProps, setAnalyseDialogProps] = useState({}) + const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false) + const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({}) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) @@ -56,6 +59,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl if (setting === 'deleteChatflow') { handleDeleteFlow() + } else if (setting === 'conversationStarters') { + setConversationStartersDialogProps({ + title: 'Set Conversation Starters - ' + chatflow.name, + chatflow: chatflow + }) + setConversationStartersDialogOpen(true) } else if (setting === 'analyseChatflow') { setAnalyseDialogProps({ title: 'Analyse Chatflow', @@ -376,6 +385,11 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl /> setAPIDialogOpen(false)} /> setAnalyseDialogOpen(false)} /> + setConversationStartersDialogOpen(false)} + /> { const [limitMax, setLimitMax] = useState(apiConfig?.rateLimit?.limitMax ?? '') const [limitDuration, setLimitDuration] = useState(apiConfig?.rateLimit?.limitDuration ?? '') const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? '') - const [prompt1, setPrompt1] = useState(apiConfig?.prompt?.prompt1 ?? '') - const [prompt2, setPrompt2] = useState(apiConfig?.prompt?.prompt2 ?? '') - const [prompt3, setPrompt3] = useState(apiConfig?.prompt?.prompt3 ?? '') - const [prompt4, setPrompt4] = useState(apiConfig?.prompt?.prompt4 ?? '') const formatObj = () => { const obj = { @@ -119,7 +111,7 @@ const Configuration = () => { return (
- {fieldLabel && {fieldLabel}} + {fieldLabel} { return ( <> - - } aria-controls='panel1a-content' id='panel1a-header'> - - Rate Limit{' '} - Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' - } - /> - - - - - {/*Rate Limit*/} - {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} - {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} - {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} - - - + {/*Rate Limit*/} + + Rate Limit{' '} + Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' + } + /> + + {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} + {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} + {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} - - } aria-controls='panel2a-content' id='panel2a-header'> - Conversation Starters - - - - {textField(prompt1, 'prompt1', 'Starter Prompts', 'string')} - {textField(prompt2, 'prompt2', '', 'string')} - {textField(prompt3, 'prompt3', '', 'string')} - {textField(prompt4, 'prompt4', '', 'string')} - - - onSave()}> Save Changes diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index d047a3aa..8e9b753b 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -57,6 +57,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const inputRef = useRef(null) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) + + const [starterPrompts, setStarterPrompts] = useState([]) const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) @@ -104,14 +107,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, 100) } - const handlePromptClick = async (prompt) => { + const handlePromptClick = async (prompt, e) => { setUserInput(prompt) - await handleSubmit() + await handleSubmit(e) } // Handle form submission const handleSubmit = async (e) => { - if (e) e.preventDefault() + e.preventDefault() if (userInput.trim() === '') { return @@ -202,10 +205,18 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (getIsChatflowStreamingApi.data) { setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [getIsChatflowStreamingApi.data]) + useEffect(() => { + if (getChatflowConfig.data) { + if (getChatflowConfig.data?.starterPrompt && JSON.parse(getChatflowConfig.data?.starterPrompt)) { + setStarterPrompts(JSON.parse(getChatflowConfig.data?.starterPrompt)) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.data]) + // Auto scroll chat to bottom useEffect(() => { scrollToBottom() @@ -224,6 +235,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (open && chatflowid) { getChatmessageApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid) + getChatflowConfig.request(chatflowid) scrollToBottom() socket = socketIOClient(baseURL) @@ -377,8 +389,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {messages && messages.length === 1 && ( )} From a27da2375ba8519287c4a11f8b0a3b522ca420a0 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 23 Nov 2023 10:30:18 +0530 Subject: [PATCH 003/502] Conversation Starters: - Updates to use the chatbot config for persistence. --- packages/server/src/database/entities/ChatFlow.ts | 3 --- .../1700565042576-AddStarterPromptsToChatFlow.ts | 12 ------------ .../server/src/database/migrations/mysql/index.ts | 4 +--- .../1700565042576-AddStarterPromptsToChatFlow.ts | 11 ----------- .../server/src/database/migrations/postgres/index.ts | 4 +--- .../1700565042576-AddStarterPromptsToChatFlow.ts | 11 ----------- .../server/src/database/migrations/sqlite/index.ts | 4 +--- packages/ui/src/menu-items/settings.js | 2 +- .../ui-component/dialog/ConversationStarterDialog.js | 10 ++++++---- packages/ui/src/views/canvas/CanvasHeader.js | 2 +- packages/ui/src/views/chatmessage/ChatMessage.js | 7 +++++-- 11 files changed, 16 insertions(+), 54 deletions(-) delete mode 100644 packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts delete mode 100644 packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts delete mode 100644 packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 626f743a..376a100b 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -28,9 +28,6 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true, type: 'text' }) apiConfig?: string - @Column({ nullable: true, type: 'text' }) - starterPrompt?: string - @Column({ nullable: true, type: 'text' }) analytic?: string diff --git a/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts deleted file mode 100644 index 1f7a3c54..00000000 --- a/packages/server/src/database/migrations/mysql/1700565042576-AddStarterPromptsToChatFlow.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddStarterPrompt1700565042576 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const columnExists = await queryRunner.hasColumn('chat_flow', 'starterPrompt') - if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`starterPrompt\` TEXT;`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`starterPrompt\`;`) - } -} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index f38a9cfe..4b7b8a95 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,7 +8,6 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' -import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const mysqlMigrations = [ Init1693840429259, @@ -20,6 +19,5 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341, - AddStarterPrompt1700565042576 + AddUsedToolsToChatMessage1699481607341 ] diff --git a/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts deleted file mode 100644 index ac2694b9..00000000 --- a/packages/server/src/database/migrations/postgres/1700565042576-AddStarterPromptsToChatFlow.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddStarterPrompt1700565042576 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "starterPrompt" TEXT;`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "starterPrompt";`) - } -} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index e9018265..75562c0b 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,7 +8,6 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' -import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const postgresMigrations = [ Init1693891895163, @@ -20,6 +19,5 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341, - AddStarterPrompt1700565042576 + AddUsedToolsToChatMessage1699481607341 ] diff --git a/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts deleted file mode 100644 index 3d180797..00000000 --- a/packages/server/src/database/migrations/sqlite/1700565042576-AddStarterPromptsToChatFlow.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddStarterPrompt1700565042576 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "starterPrompt" TEXT;`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "starterPrompt";`) - } -} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 414d755e..4a14fc40 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,7 +8,6 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' -import { AddStarterPrompt1700565042576 } from './1700565042576-AddStarterPromptsToChatFlow' export const sqliteMigrations = [ Init1693835579790, @@ -20,6 +19,5 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341, - AddStarterPrompt1700565042576 + AddUsedToolsToChatMessage1699481607341 ] diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 9224b78f..1e0f58dd 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -13,7 +13,7 @@ const settings = { children: [ { id: 'conversationStarters', - title: 'Set Starter Prompts', + title: 'Starter Prompts', type: 'item', url: '', icon: icons.IconPictureInPictureOff diff --git a/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js b/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js index 1139f21e..979a4526 100644 --- a/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js +++ b/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js @@ -57,7 +57,9 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { const onSave = async () => { try { const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { - starterPrompt: JSON.stringify(inputFields) + chatbotConfig: { + starterPrompts: JSON.stringify(inputFields) + } }) if (saveResp.data) { enqueueSnackbar({ @@ -94,9 +96,9 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { } useEffect(() => { - if (dialogProps.chatflow && dialogProps.chatflow.starterPrompt) { + if (dialogProps.chatflow && dialogProps.chatbotConfig.starterPrompts) { try { - setInputFields(JSON.parse(dialogProps.chatflow.starterPrompt)) + setInputFields(JSON.parse(dialogProps.chatbotConfig.starterPrompts)) } catch (e) { setInputFields([ { @@ -126,7 +128,7 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { aria-describedby='alert-dialog-description' > - Set Conversation Starter Prompts + {dialogProps.title || 'Conversation Starter Prompts'} diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index d931ee26..b08eb6ab 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -61,7 +61,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl handleDeleteFlow() } else if (setting === 'conversationStarters') { setConversationStartersDialogProps({ - title: 'Set Conversation Starters - ' + chatflow.name, + title: 'Starter Prompts - ' + chatflow.name, chatflow: chatflow }) setConversationStartersDialogOpen(true) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 8e9b753b..e006ba49 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -210,8 +210,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { useEffect(() => { if (getChatflowConfig.data) { - if (getChatflowConfig.data?.starterPrompt && JSON.parse(getChatflowConfig.data?.starterPrompt)) { - setStarterPrompts(JSON.parse(getChatflowConfig.data?.starterPrompt)) + if ( + getChatflowConfig.data?.chatbotConfig?.starterPrompts && + JSON.parse(getChatflowConfig.data?.chatbotConfig?.starterPrompts) + ) { + setStarterPrompts(JSON.parse(getChatflowConfig.data?.chatbotConfig?.starterPrompts)) } } // eslint-disable-next-line react-hooks/exhaustive-deps From 6881231ea40c73018199b4450e7566b333e81fb5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 23 Nov 2023 22:38:45 +0530 Subject: [PATCH 004/502] Conversation Starter: Setup of prompts from the chatflow dashboard and other refactorings. --- .../src/ui-component/button/FlowListMenu.js | 22 +++++ ...rsationCard.css => StarterPromptsCard.css} | 0 ...versationCard.js => StarterPromptsCard.js} | 8 +- ...arterDialog.js => StarterPromptsDialog.js} | 86 +++++++++++++------ packages/ui/src/views/canvas/CanvasHeader.js | 4 +- .../ui/src/views/chatmessage/ChatMessage.css | 2 +- .../ui/src/views/chatmessage/ChatMessage.js | 24 +++--- 7 files changed, 104 insertions(+), 42 deletions(-) rename packages/ui/src/ui-component/cards/{StarterConversationCard.css => StarterPromptsCard.css} (100%) rename packages/ui/src/ui-component/cards/{StarterConversationCard.js => StarterPromptsCard.js} (75%) rename packages/ui/src/ui-component/dialog/{ConversationStarterDialog.js => StarterPromptsDialog.js} (66%) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index b242d2cb..3f94c400 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -11,6 +11,7 @@ import FileCopyIcon from '@mui/icons-material/FileCopy' import FileDownloadIcon from '@mui/icons-material/Downloading' import FileDeleteIcon from '@mui/icons-material/Delete' import FileCategoryIcon from '@mui/icons-material/Category' +import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt' import Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import { IconX } from '@tabler/icons' @@ -28,6 +29,7 @@ import TagDialog from '../dialog/TagDialog' import { generateExportFlowData } from '../../utils/genericHelper' import useNotifier from '../../utils/useNotifier' +import StarterPromptsDialog from '../dialog/StarterPromptsDialog' const StyledMenu = styled((props) => ( { setAnchorEl(event.currentTarget) @@ -93,6 +97,15 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { setFlowDialogOpen(true) } + const handleFlowStarterPrompts = () => { + setAnchorEl(null) + setConversationStartersDialogProps({ + title: 'Starter Prompts - ' + chatflow.name, + chatflow: chatflow + }) + setConversationStartersDialogOpen(true) + } + const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -254,6 +267,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Export + + + Starter Prompts + Update Category @@ -281,6 +298,11 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { onClose={() => setCategoryDialogOpen(false)} onSubmit={saveFlowCategory} /> + setConversationStartersDialogOpen(false)} + />
) } diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.css b/packages/ui/src/ui-component/cards/StarterPromptsCard.css similarity index 100% rename from packages/ui/src/ui-component/cards/StarterConversationCard.css rename to packages/ui/src/ui-component/cards/StarterPromptsCard.css diff --git a/packages/ui/src/ui-component/cards/StarterConversationCard.js b/packages/ui/src/ui-component/cards/StarterPromptsCard.js similarity index 75% rename from packages/ui/src/ui-component/cards/StarterConversationCard.js rename to packages/ui/src/ui-component/cards/StarterPromptsCard.js index aa50f266..caf8a219 100644 --- a/packages/ui/src/ui-component/cards/StarterConversationCard.js +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.js @@ -1,9 +1,9 @@ import Box from '@mui/material/Box' import PropTypes from 'prop-types' import { Button } from '@mui/material' -import './StarterConversationCard.css' +import './StarterPromptsCard.css' -const StarterConversationCard = ({ isGrid, starterPrompts, onPromptClick }) => { +const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => { return ( {starterPrompts.map((sp, index) => ( @@ -15,10 +15,10 @@ const StarterConversationCard = ({ isGrid, starterPrompts, onPromptClick }) => { ) } -StarterConversationCard.propTypes = { +StarterPromptsCard.propTypes = { isGrid: PropTypes.bool, starterPrompts: PropTypes.arrayOf(PropTypes.string), onPromptClick: PropTypes.func } -export default StarterConversationCard +export default StarterPromptsCard diff --git a/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js similarity index 66% rename from packages/ui/src/ui-component/dialog/ConversationStarterDialog.js rename to packages/ui/src/ui-component/dialog/StarterPromptsDialog.js index 979a4526..7752e32f 100644 --- a/packages/ui/src/ui-component/dialog/ConversationStarterDialog.js +++ b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js @@ -5,7 +5,18 @@ import PropTypes from 'prop-types' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' // material-ui -import { Button, IconButton, Dialog, DialogContent, OutlinedInput, DialogTitle, DialogActions, Box, List, Divider } from '@mui/material' +import { + Button, + IconButton, + Dialog, + DialogContent, + OutlinedInput, + DialogTitle, + DialogActions, + Box, + List, + InputAdornment +} from '@mui/material' import { IconX, IconTrash, IconPlus } from '@tabler/icons' // Project import @@ -18,7 +29,7 @@ import useNotifier from 'utils/useNotifier' // API import chatflowsApi from 'api/chatflows' -const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { +const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() @@ -56,10 +67,13 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { const onSave = async () => { try { - const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { - chatbotConfig: { - starterPrompts: JSON.stringify(inputFields) + let value = { + starterPrompts: { + ...inputFields } + } + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(value) }) if (saveResp.data) { enqueueSnackbar({ @@ -96,16 +110,30 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { } useEffect(() => { - if (dialogProps.chatflow && dialogProps.chatbotConfig.starterPrompts) { + if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { try { - setInputFields(JSON.parse(dialogProps.chatbotConfig.starterPrompts)) + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + if (chatbotConfig.starterPrompts) { + let inputFields = [] + Object.getOwnPropertyNames(chatbotConfig.starterPrompts).forEach((key) => { + if (chatbotConfig.starterPrompts[key]) { + inputFields.push(chatbotConfig.starterPrompts[key]) + } + }) + setInputFields(inputFields) + } else { + setInputFields([ + { + prompt: '' + } + ]) + } } catch (e) { setInputFields([ { prompt: '' } ]) - console.error(e) } } @@ -131,28 +159,34 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { {dialogProps.title || 'Conversation Starter Prompts'} - + :not(style)': { m: 1 }, pt: 2 }}> {inputFields.map((data, index) => { return ( -
- +
+ handleChange(index, e)} + size='small' value={data.prompt} name='prompt' + endAdornment={ + + + + + + } /> - - {inputFields.length !== 1 && ( - - - - )} - {index === inputFields.length - 1 && ( @@ -160,6 +194,13 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { )} + {/**/} + {/* {inputFields.length !== 1 && (*/} + {/* */} + {/* */} + {/* */} + {/* )}*/} + {/**/}
) })} @@ -167,10 +208,7 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => {
- - - Cancel - + Save @@ -181,10 +219,10 @@ const ConversationStarterDialog = ({ show, dialogProps, onCancel }) => { return createPortal(component, portalElement) } -ConversationStarterDialog.propTypes = { +StarterPromptsDialog.propTypes = { show: PropTypes.bool, dialogProps: PropTypes.object, onCancel: PropTypes.func } -export default ConversationStarterDialog +export default StarterPromptsDialog diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index b08eb6ab..2c3bdfe4 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -16,7 +16,7 @@ import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog' import APICodeDialog from 'views/chatflows/APICodeDialog' import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog' import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog' -import ConversationStarterDialog from 'ui-component/dialog/ConversationStarterDialog' +import StarterPromptsDialog from 'ui-component/dialog/StarterPromptsDialog' // API import chatflowsApi from 'api/chatflows' @@ -385,7 +385,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl /> setAPIDialogOpen(false)} /> setAnalyseDialogOpen(false)} /> - setConversationStartersDialogOpen(false)} diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 2298fee6..2c627988 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -118,7 +118,7 @@ .cloud { width: 400px; - height: calc(100vh - 260px); + height: calc(90vh - 260px); border-radius: 0.5rem; display: flex; justify-content: center; diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 81ba9512..962ecb36 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -33,7 +33,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' -import StarterConversationCard from '../../ui-component/cards/StarterConversationCard' +import StarterPromptsCard from '../../ui-component/cards/StarterPromptsCard' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -238,11 +238,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { useEffect(() => { if (getChatflowConfig.data) { - if ( - getChatflowConfig.data?.chatbotConfig?.starterPrompts && - JSON.parse(getChatflowConfig.data?.chatbotConfig?.starterPrompts) - ) { - setStarterPrompts(JSON.parse(getChatflowConfig.data?.chatbotConfig?.starterPrompts)) + if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { + let config = JSON.parse(getChatflowConfig.data?.chatbotConfig) + if (config.starterPrompts) { + let inputFields = [] + Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { + if (config.starterPrompts[key]) { + inputFields.push(config.starterPrompts[key]) + } + }) + setStarterPrompts(inputFields) + } } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -439,11 +445,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
{messages && messages.length === 1 && ( - + )} Date: Fri, 24 Nov 2023 11:20:35 +0530 Subject: [PATCH 005/502] Conversation Starter: Changes to ensure that the chatbotConfig is not overwritten between the starter prompts and share chatbot dialogs --- .../ui/src/ui-component/button/FlowListMenu.js | 6 ++++++ .../ui-component/dialog/StarterPromptsDialog.js | 15 +++++++++++---- packages/ui/src/views/chatflows/ShareChatbot.js | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index 3f94c400..2f5bdd5d 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -106,6 +106,11 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { setConversationStartersDialogOpen(true) } + const saveFlowStarterPrompts = async () => { + setConversationStartersDialogOpen(false) + await updateFlowsApi.request() + } + const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -301,6 +306,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { setConversationStartersDialogOpen(false)} />
diff --git a/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js index 7752e32f..286399a6 100644 --- a/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js +++ b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.js @@ -29,7 +29,7 @@ import useNotifier from 'utils/useNotifier' // API import chatflowsApi from 'api/chatflows' -const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { +const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm = undefined }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() @@ -44,6 +44,8 @@ const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { } ]) + const [chatbotConfig, setChatbotConfig] = useState({}) + const addInputField = () => { setInputFields([ ...inputFields, @@ -72,8 +74,9 @@ const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { ...inputFields } } + chatbotConfig.starterPrompts = value.starterPrompts const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { - chatbotConfig: JSON.stringify(value) + chatbotConfig: JSON.stringify(chatbotConfig) }) if (saveResp.data) { enqueueSnackbar({ @@ -90,7 +93,9 @@ const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { }) dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } - onCancel() + if (onConfirm) { + onConfirm() + } } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ @@ -113,6 +118,7 @@ const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { try { let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) if (chatbotConfig.starterPrompts) { let inputFields = [] Object.getOwnPropertyNames(chatbotConfig.starterPrompts).forEach((key) => { @@ -222,7 +228,8 @@ const StarterPromptsDialog = ({ show, dialogProps, onCancel }) => { StarterPromptsDialog.propTypes = { show: PropTypes.bool, dialogProps: PropTypes.object, - onCancel: PropTypes.func + onCancel: PropTypes.func, + onConfirm: PropTypes.func } export default StarterPromptsDialog diff --git a/packages/ui/src/views/chatflows/ShareChatbot.js b/packages/ui/src/views/chatflows/ShareChatbot.js index dc6c0621..0bf5fc39 100644 --- a/packages/ui/src/views/chatflows/ShareChatbot.js +++ b/packages/ui/src/views/chatflows/ShareChatbot.js @@ -135,6 +135,8 @@ const ShareChatbot = ({ isSessionMemory }) => { if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession + if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts + return obj } From c96572e10ff8fa954d973f10748c603a00d49ef9 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 25 Nov 2023 16:39:02 +0530 Subject: [PATCH 006/502] GPT Vision - OpenAIVisionChain --- .../chains/VisionChain/OpenAIVisionChain.ts | 216 ++++++++++++++++++ .../nodes/chains/VisionChain/VLLMChain.ts | 146 ++++++++++++ packages/server/src/index.ts | 26 +++ 3 files changed, 388 insertions(+) create mode 100644 packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts create mode 100644 packages/components/nodes/chains/VisionChain/VLLMChain.ts diff --git a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts new file mode 100644 index 00000000..f2260a76 --- /dev/null +++ b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts @@ -0,0 +1,216 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' +import { VLLMChain } from './VLLMChain' +import { BaseLanguageModel } from 'langchain/base_language' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { formatResponse } from '../../outputparsers/OutputParserHelpers' +import { ChatOpenAI } from 'langchain/chat_models/openai' + +class OpenAIVisionChain_Chains implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + baseClasses: string[] + description: string + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Open AI Vision Chain' + this.name = 'openAIVisionChain' + this.version = 3.0 + this.type = 'OpenAIVisionChain' + this.icon = 'chain.svg' + this.category = 'Chains' + this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .' + this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] + this.inputs = [ + { + label: 'Language Model (Works only with Open AI [gpt-4-vision-preview])', + name: 'model', + type: 'BaseLanguageModel' + }, + { + label: 'Prompt', + name: 'prompt', + type: 'BasePromptTemplate', + optional: true + }, + { + label: 'Image Resolution', + description: 'This parameter controls the resolution in which the model views the image.', + name: 'imageResolution', + type: 'options', + options: [ + { + label: 'Low', + name: 'low' + }, + { + label: 'High', + name: 'high' + } + ], + default: 'low', + optional: false + }, + { + label: 'Chain Name', + name: 'chainName', + type: 'string', + placeholder: 'Name Your Chain', + optional: true + } + ] + this.outputs = [ + { + label: 'Open AI Vision Chain', + name: 'openAIVisionChain', + baseClasses: [this.type, ...getBaseClasses(VLLMChain)] + }, + { + label: 'Output Prediction', + name: 'outputPrediction', + baseClasses: ['string', 'json'] + } + ] + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const model = nodeData.inputs?.model as BaseLanguageModel + const prompt = nodeData.inputs?.prompt + const output = nodeData.outputs?.output as string + const imageResolution = nodeData.inputs?.imageResolution + const promptValues = prompt.promptValues as ICommonObject + if (!(model as any).openAIApiKey || (model as any).modelName !== 'gpt-4-vision-preview') { + throw new Error('Chain works with OpenAI Vision model only') + } + const openAIModel = model as ChatOpenAI + const fields = { + openAIApiKey: openAIModel.openAIApiKey, + imageResolution: imageResolution, + verbose: process.env.DEBUG === 'true', + imageUrls: options.url, + openAIModel: openAIModel + } + if (output === this.name) { + const chain = new VLLMChain({ + ...fields, + prompt: prompt + }) + return chain + } else if (output === 'outputPrediction') { + const chain = new VLLMChain({ + ...fields + }) + const inputVariables: string[] = prompt.inputVariables as string[] // ["product"] + const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) + // eslint-disable-next-line no-console + console.log('\x1b[92m\x1b[1m\n*****OUTPUT PREDICTION*****\n\x1b[0m\x1b[0m') + // eslint-disable-next-line no-console + console.log(res) + /** + * Apply string transformation to convert special chars: + * FROM: hello i am ben\n\n\thow are you? + * TO: hello i am benFLOWISE_NEWLINEFLOWISE_NEWLINEFLOWISE_TABhow are you? + */ + return handleEscapeCharacters(res, false) + } + } + + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const prompt = nodeData.inputs?.prompt + const inputVariables: string[] = prompt.inputVariables as string[] // ["product"] + const chain = nodeData.instance as VLLMChain + let promptValues: ICommonObject | undefined = nodeData.inputs?.prompt.promptValues as ICommonObject + const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) + // eslint-disable-next-line no-console + console.log('\x1b[93m\x1b[1m\n*****FINAL RESULT*****\n\x1b[0m\x1b[0m') + // eslint-disable-next-line no-console + console.log(res) + return res + } +} + +const runPrediction = async ( + inputVariables: string[], + chain: VLLMChain, + input: string, + promptValuesRaw: ICommonObject | undefined, + options: ICommonObject, + nodeData: INodeData +) => { + const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) + + const isStreaming = options.socketIO && options.socketIOClientId + const socketIO = isStreaming ? options.socketIO : undefined + const socketIOClientId = isStreaming ? options.socketIOClientId : '' + + /** + * Apply string transformation to reverse converted special chars: + * FROM: { "value": "hello i am benFLOWISE_NEWLINEFLOWISE_NEWLINEFLOWISE_TABhow are you?" } + * TO: { "value": "hello i am ben\n\n\thow are you?" } + */ + const promptValues = handleEscapeCharacters(promptValuesRaw, true) + if (options?.url) { + chain.imageUrls = options.url + } + if (promptValues && inputVariables.length > 0) { + let seen: string[] = [] + + for (const variable of inputVariables) { + seen.push(variable) + if (promptValues[variable]) { + chain.inputKey = variable + seen.pop() + } + } + + if (seen.length === 0) { + // All inputVariables have fixed values specified + const options = { ...promptValues } + if (isStreaming) { + const handler = new CustomChainHandler(socketIO, socketIOClientId) + const res = await chain.call(options, [loggerHandler, handler, ...callbacks]) + return formatResponse(res?.text) + } else { + const res = await chain.call(options, [loggerHandler, ...callbacks]) + return formatResponse(res?.text) + } + } else if (seen.length === 1) { + // If one inputVariable is not specify, use input (user's question) as value + const lastValue = seen.pop() + if (!lastValue) throw new Error('Please provide Prompt Values') + chain.inputKey = lastValue as string + const options = { + ...promptValues, + [lastValue]: input + } + if (isStreaming) { + const handler = new CustomChainHandler(socketIO, socketIOClientId) + const res = await chain.call(options, [loggerHandler, handler, ...callbacks]) + return formatResponse(res?.text) + } else { + const res = await chain.call(options, [loggerHandler, ...callbacks]) + return formatResponse(res?.text) + } + } else { + throw new Error(`Please provide Prompt Values for: ${seen.join(', ')}`) + } + } else { + if (isStreaming) { + const handler = new CustomChainHandler(socketIO, socketIOClientId) + const res = await chain.run(input, [loggerHandler, handler, ...callbacks]) + return formatResponse(res) + } else { + const res = await chain.run(input, [loggerHandler, ...callbacks]) + return formatResponse(res) + } + } +} + +module.exports = { nodeClass: OpenAIVisionChain_Chains } diff --git a/packages/components/nodes/chains/VisionChain/VLLMChain.ts b/packages/components/nodes/chains/VisionChain/VLLMChain.ts new file mode 100644 index 00000000..17260be2 --- /dev/null +++ b/packages/components/nodes/chains/VisionChain/VLLMChain.ts @@ -0,0 +1,146 @@ +import { OpenAI as OpenAIClient, ClientOptions } from 'openai' +import { BaseChain, ChainInputs } from 'langchain/chains' +import { ChainValues } from 'langchain/schema' +import { BasePromptTemplate, ChatPromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' +import { ChatOpenAI } from 'langchain/chat_models/openai' + +/** + * Interface for the input parameters of the OpenAIVisionChain class. + */ +export interface OpenAIVisionChainInput extends ChainInputs { + openAIApiKey?: string + openAIOrganization?: string + throwError?: boolean + prompt?: BasePromptTemplate + configuration?: ClientOptions + imageUrls?: [] + imageResolution?: string + openAIModel: ChatOpenAI +} + +/** + * Class representing a chain for generating text from an image using the OpenAI + * Vision API. It extends the BaseChain class and implements the + * OpenAIVisionChainInput interface. + */ +export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { + static lc_name() { + return 'VLLMChain' + } + + get lc_secrets(): { [key: string]: string } | undefined { + return { + openAIApiKey: 'OPENAI_API_KEY' + } + } + prompt: BasePromptTemplate | undefined + + inputKey = 'input' + outputKey = 'text' + imageUrls?: [] + imageResolution: string = 'low' + openAIApiKey?: string + openAIOrganization?: string + openAIModel: ChatOpenAI + clientConfig: ClientOptions + client: OpenAIClient + throwError: boolean + + constructor(fields: OpenAIVisionChainInput) { + super(fields) + this.throwError = fields?.throwError ?? false + this.imageResolution = fields?.imageResolution ?? 'low' + this.openAIApiKey = fields?.openAIApiKey + this.prompt = fields?.prompt + this.imageUrls = fields?.imageUrls ?? [] + if (!this.openAIApiKey) { + throw new Error('OpenAI API key not found') + } + + this.openAIOrganization = fields?.openAIOrganization + this.openAIModel = fields.openAIModel + + this.clientConfig = { + ...fields?.configuration, + apiKey: this.openAIApiKey, + organization: this.openAIOrganization + } + + this.client = new OpenAIClient(this.clientConfig) + } + + async _call(values: ChainValues): Promise { + const userInput = values[this.inputKey] + + const vRequest: any = { + model: 'gpt-4-vision-preview', + temperature: this.openAIModel.temperature, + top_p: this.openAIModel.topP, + messages: [] + } + if (this.openAIModel.maxTokens) vRequest.max_tokens = this.openAIModel.maxTokens + + const userRole: any = { role: 'user' } + userRole.content = [] + userRole.content.push({ + type: 'text', + text: userInput + }) + if (this.imageUrls && this.imageUrls.length > 0) { + this.imageUrls.forEach((imageUrl: any) => { + userRole.content.push({ + type: 'image_url', + image_url: { + url: imageUrl?.data, + detail: this.imageResolution + } + }) + }) + } + vRequest.messages.push(userRole) + if (this.prompt && this.prompt instanceof ChatPromptTemplate) { + let chatPrompt = this.prompt as ChatPromptTemplate + chatPrompt.promptMessages.forEach((message: any) => { + if (message instanceof SystemMessagePromptTemplate) { + vRequest.messages.push({ + role: 'system', + content: [ + { + type: 'text', + text: (message.prompt as any).template + } + ] + }) + } + }) + } + + let response + try { + // @ts-ignore + response = await this.client.chat.completions.create(vRequest) + } catch (error) { + if (error instanceof Error) { + throw error + } else { + throw new Error(error as string) + } + } + const output = response.choices[0] + return { + [this.outputKey]: output.message.content + } + } + + _chainType() { + return 'vision_chain' + } + + get inputKeys() { + return this.prompt?.inputVariables ?? [this.inputKey] + } + + get outputKeys(): string[] { + return [this.outputKey] + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 91de4f4c..9bc3eb3a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -403,6 +403,19 @@ export class App { return res.json(obj) }) + // Check if chatflow valid for uploads + this.app.get('/api/v1/chatflows-uploads/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + + const obj = { + allowUploads: this.shouldAllowUploads(chatflow) + } + return res.json(obj) + }) + // ---------------------------------------- // ChatMessage // ---------------------------------------- @@ -1241,6 +1254,19 @@ export class App { }) } + private uploadAllowedNodes = ['OpenAIVisionChain'] + private shouldAllowUploads(result: ChatFlow): boolean { + const flowObj = JSON.parse(result.flowData) + let allowUploads = false + flowObj.nodes.forEach((node: IReactFlowNode) => { + if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) { + logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) + allowUploads = true + } + }) + return allowUploads + } + /** * Validate API Key * @param {Request} req From 4a3e1784d8f8b4532587bfb5ddce61eb29c35977 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 27 Nov 2023 06:06:18 +0530 Subject: [PATCH 007/502] LS Prompt Hub: Initial Commit --- .../dialog/PromptLangsmithHubDialog.js | 368 ++++++++++++++++++ .../ui/src/views/canvas/NodeInputHandler.js | 29 ++ 2 files changed, 397 insertions(+) create mode 100644 packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js new file mode 100644 index 00000000..16d5c30f --- /dev/null +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -0,0 +1,368 @@ +import { createPortal } from 'react-dom' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { + Box, + Button, + Card, + CardContent, + Chip, + Dialog, + DialogContent, + DialogTitle, + Divider, + Grid, + InputLabel, + List, + ListItemButton, + ListItemText, + OutlinedInput, + Select, + Typography +} from '@mui/material' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '../../store/actions' +import { useDispatch } from 'react-redux' +import FormControl from '@mui/material/FormControl' +import Checkbox from '@mui/material/Checkbox' +import MenuItem from '@mui/material/MenuItem' +import axios from 'axios' +import ReactMarkdown from 'react-markdown' +import CredentialInputHandler from '../../views/canvas/CredentialInputHandler' + +const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show, dispatch]) + + const ITEM_HEIGHT = 48 + const ITEM_PADDING_TOP = 8 + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } + } + + const models = [ + { id: 101, name: 'anthropic:claude-instant-1' }, + { id: 102, name: 'anthropic:claude-instant-1.2' }, + { id: 103, name: 'anthropic:claude-2' }, + { id: 104, name: 'google:palm-2-chat-bison' }, + { id: 105, name: 'google:palm-2-codechat-bison' }, + { id: 106, name: 'google:palm-2-text-bison' }, + { id: 107, name: 'meta:llama-2-13b-chat' }, + { id: 108, name: 'meta:llama-2-70b-chat' }, + { id: 109, name: 'openai:gpt-3.5-turbo' }, + { id: 110, name: 'openai:gpt-4' }, + { id: 111, name: 'openai:text-davinci-003' } + ] + const [modelName, setModelName] = useState([]) + + const usecases = [ + { id: 201, name: 'Agents' }, + { id: 202, name: 'Agent Stimulation' }, + { id: 203, name: 'Autonomous agents' }, + { id: 204, name: 'Classification' }, + { id: 205, name: 'Chatbots' }, + { id: 206, name: 'Code understanding' }, + { id: 207, name: 'Code writing' }, + { id: 208, name: 'Evaluation' }, + { id: 209, name: 'Extraction' }, + { id: 210, name: 'Interacting with APIs' }, + { id: 211, name: 'Multi-modal' }, + { id: 212, name: 'QA over documents' }, + { id: 213, name: 'Self-checking' }, + { id: 214, name: 'SQL' }, + { id: 215, name: 'Summarization' }, + { id: 216, name: 'Tagging' } + ] + const [usecase, setUsecase] = useState([]) + + const languages = [ + { id: 301, name: 'Chinese' }, + { id: 302, name: 'English' }, + { id: 303, name: 'French' }, + { id: 304, name: 'German' }, + { id: 305, name: 'Russian' }, + { id: 306, name: 'Spanish' } + ] + const [language, setLanguage] = useState([]) + const [prompts, setPrompts] = useState([]) + const [selectedPrompt, setSelectedPrompt] = useState({}) + + const [credentialId, setCredentialId] = useState('') + + const handleListItemClick = (event, index) => { + setSelectedPrompt(prompts[index]) + } + + const fetchPrompts = async () => { + let tags = promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' + modelName.forEach((item) => { + tags += `tags=${item.name}&` + }) + usecase.forEach((item) => { + tags += `tags=${item.name}&` + }) + language.forEach((item) => { + tags += `tags=${item.name}&` + }) + const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + axios.get(url).then((response) => { + if (response.data.repos) { + setPrompts(response.data.repos) + // response.data.repos.forEach((item) => { + // console.log(item) + // }) + } + }) + // latestReleaseReq.then() + } + const removeDuplicates = (value) => { + let duplicateRemoved = [] + + value.forEach((item) => { + if (value.filter((o) => o.id === item.id).length === 1) { + duplicateRemoved.push(item) + } + }) + return duplicateRemoved + } + + const handleModelChange = (event) => { + const { + target: { value } + } = event + + setModelName(removeDuplicates(value)) + } + + const handleUsecaseChange = (event) => { + const { + target: { value } + } = event + + setUsecase(removeDuplicates(value)) + } + const handleLanguageChange = (event) => { + const { + target: { value } + } = event + + setLanguage(removeDuplicates(value)) + } + + const component = show ? ( + + + Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) + + + + + Langsmith Credential + * + + { + setCredentialId(newValue) + }} + /> + + + + + Model + + + + + + Usecase + + + + + + Language + + + + + + + + + + + + + + + Available Prompts + + + {prompts.map((item, index) => ( + handleListItemClick(event, index)} + > + {item.full_name} + + ))} + + + + + + + + + + + Description + + {selectedPrompt?.description} + + + + + + Tags + + {selectedPrompt?.tags?.map((item) => ( + + ))} + + + + + + Readme + + + {selectedPrompt?.readme} + + + + + + + + + ) : null + + return createPortal(component, portalElement) +} + +PromptLangsmithHubDialog.propTypes = { + promptType: PropTypes.string, + show: PropTypes.bool, + onCancel: PropTypes.func +} + +export default PromptLangsmithHubDialog diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 7eb31bdb..162455df 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -6,6 +6,7 @@ import { useSelector } from 'react-redux' // material-ui import { useTheme, styled } from '@mui/material/styles' import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material' +import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh' import { tooltipClasses } from '@mui/material/Tooltip' import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons' @@ -31,6 +32,7 @@ import { getInputVariables } from 'utils/genericHelper' // const import { FLOWISE_CREDENTIAL_ID } from 'store/constant' +import PromptLangsmithHubDialog from '../../ui-component/dialog/PromptLangsmithHubDialog' const EDITABLE_OPTIONS = ['selectedTool', 'selectedAssistant'] @@ -56,6 +58,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString()) const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false) const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({}) + const [showPromptHubDialog, setShowPromptHubDialog] = useState(false) const onExpandDialogClicked = (value, inputParam) => { const dialogProp = { @@ -69,6 +72,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA setShowExpandDialog(true) } + const onShowPromptHubButtonClicked = () => { + setShowPromptHubDialog(true) + } const onFormatPromptValuesClicked = (value, inputParam) => { // Preset values if the field is format prompt values let inputValue = value @@ -209,6 +215,28 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA )} + {(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( + <> + + setShowPromptHubDialog(false)} + > + + )}
{inputParam.label} @@ -260,6 +288,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA }} /> )} + {inputParam.type === 'file' && ( Date: Mon, 27 Nov 2023 22:42:04 +0530 Subject: [PATCH 008/502] LS Prompt Hub: Moving calls to server side and adding functionality to show the detailed prompt --- packages/server/src/index.ts | 40 +++++++ packages/server/src/utils/hub.ts | 27 +++++ packages/ui/src/api/prompt.js | 9 ++ .../dialog/PromptLangsmithHubDialog.js | 111 +++++++++++------- 4 files changed, 144 insertions(+), 43 deletions(-) create mode 100644 packages/server/src/utils/hub.ts create mode 100644 packages/ui/src/api/prompt.js diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 91de4f4c..92b32b59 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -64,6 +64,9 @@ import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' import { ICommonObject, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' +import axios from 'axios' +import { Client } from 'langchainhub' +import { parsePrompt } from './utils/hub' export class App { app: express.Application @@ -1093,6 +1096,43 @@ export class App { await this.buildChatflow(req, res, undefined, true, true) }) + // ---------------------------------------- + // Prompt from Hub + // ---------------------------------------- + this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => { + try { + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: req.body.credential + }) + + if (!credential) return res.status(404).json({ error: `Credential ${req.body.credential} not found` }) + + // Decrypt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData, credential.credentialName, undefined) + let hub = new Client({ apiKey: decryptedCredentialData.langsmithApiKey, apiUrl: decryptedCredentialData.langsmithEndpoint }) + const prompt = await hub.pull(req.body.promptName) + const templates = parsePrompt(prompt) + + return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates }) + } catch (e: any) { + return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message }) + } + }) + + this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => { + try { + const tags = req.body.tags ? `tags=${req.body.tags}` : '' + const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + axios.get(url).then((response) => { + if (response.data.repos) { + return res.json({ status: 'OK', repos: response.data.repos }) + } + }) + } catch (e: any) { + return res.json({ status: 'ERROR', repos: [] }) + } + }) + // ---------------------------------------- // Prediction // ---------------------------------------- diff --git a/packages/server/src/utils/hub.ts b/packages/server/src/utils/hub.ts new file mode 100644 index 00000000..79e7136f --- /dev/null +++ b/packages/server/src/utils/hub.ts @@ -0,0 +1,27 @@ +export function parsePrompt(prompt: string): any[] { + const promptObj = JSON.parse(prompt) + let response = [] + if (promptObj.kwargs.messages) { + promptObj.kwargs.messages.forEach((message: any) => { + let messageType = message.id.includes('SystemMessagePromptTemplate') + ? 'systemMessagePrompt' + : message.id.includes('HumanMessagePromptTemplate') + ? 'humanMessagePrompt' + : message.id.includes('AIMessagePromptTemplate') + ? 'aiMessagePrompt' + : 'template' + let template = message.kwargs.prompt.kwargs.template + response.push({ + type: messageType, + template: template + }) + }) + } else if (promptObj.kwargs.template) { + let template = promptObj.kwargs.template + response.push({ + type: 'template', + template: template + }) + } + return response +} diff --git a/packages/ui/src/api/prompt.js b/packages/ui/src/api/prompt.js new file mode 100644 index 00000000..42b1bdbc --- /dev/null +++ b/packages/ui/src/api/prompt.js @@ -0,0 +1,9 @@ +import client from './client' + +const getAvailablePrompts = (body) => client.post(`/prompts-list`, body) +const getPrompt = (body) => client.post(`/load-prompt`, body) + +export default { + getAvailablePrompts, + getPrompt +} diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index 16d5c30f..4db61633 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -25,9 +25,9 @@ import { useDispatch } from 'react-redux' import FormControl from '@mui/material/FormControl' import Checkbox from '@mui/material/Checkbox' import MenuItem from '@mui/material/MenuItem' -import axios from 'axios' import ReactMarkdown from 'react-markdown' import CredentialInputHandler from '../../views/canvas/CredentialInputHandler' +import promptApi from '../../api/prompt' const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { const portalElement = document.getElementById('portal') @@ -96,13 +96,24 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { { id: 306, name: 'Spanish' } ] const [language, setLanguage] = useState([]) - const [prompts, setPrompts] = useState([]) + const [availablePrompNameList, setAvailablePrompNameList] = useState([]) const [selectedPrompt, setSelectedPrompt] = useState({}) const [credentialId, setCredentialId] = useState('') - const handleListItemClick = (event, index) => { - setSelectedPrompt(prompts[index]) + const handleListItemClick = async (event, index) => { + const prompt = availablePrompNameList[index] + + if (!prompt.detailed) { + const createResp = await promptApi.getPrompt({ + credential: credentialId, + promptName: selectedPrompt.full_name + }) + if (createResp.data) { + prompt.detailed = createResp.data.templates + } + } + setSelectedPrompt(prompt) } const fetchPrompts = async () => { @@ -116,17 +127,15 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { language.forEach((item) => { tags += `tags=${item.name}&` }) - const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` - axios.get(url).then((response) => { - if (response.data.repos) { - setPrompts(response.data.repos) - // response.data.repos.forEach((item) => { - // console.log(item) - // }) - } + const createResp = await promptApi.getAvailablePrompts({ + credential: credentialId, + tags: tags }) - // latestReleaseReq.then() + if (createResp.data) { + setAvailablePrompNameList(createResp.data.repos) + } } + const removeDuplicates = (value) => { let duplicateRemoved = [] @@ -173,26 +182,29 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel }) => { Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) - - + + Langsmith Credential * - { - setCredentialId(newValue) - }} - /> + + { + setCredentialId(newValue) + }} + /> + { Model { } - renderValue={(selected) => selected.map((x) => x.name).join(', ')} - MenuProps={MenuProps} - > - {models.map((variant) => ( - - item.id === variant.id) >= 0} /> - - - ))} - - - - - Usecase - - - - - - Language - - - - - - - - - - - - - - - Available Prompts - - - {availablePrompNameList.map((item, index) => ( - handleListItemClick(event, index)} - > - {item.full_name} - - ))} - - - + {credentialId && ( + + + + Model + + + + + + Usecase + + + + + + Language + + + + + + + + )} + {availablePrompNameList && availablePrompNameList.length == 0 && ( + + + promptEmptySVG - - - - - - handleAccordionChange('panel1')}> - } - id='panel1d-header' - > - Description - - - - {selectedPrompt?.description} - - - - handleAccordionChange('panel2')}> - } - id='panel2d-header' - > - Prompt - - - - {selectedPrompt?.detailed?.map((item) => ( - <> - - {item.typeDisplay.toUpperCase()} +
No Available Prompts
+ + )} + {availablePrompNameList && availablePrompNameList.length > 0 && ( + + + + + + + + + Available Prompts + + + {availablePrompNameList.map((item, index) => ( + handleListItemClick(index)} + > +
+ + {item.full_name} + +
+ {item.tags.map((tag, index) => ( + + ))} +
+
+
+ ))} +
+
+
+
+
+ + + + + + } + id='panel2d-header' + > + Prompt + + + + {selectedPrompt?.detailed?.map((item) => ( + <> + + {item.typeDisplay.toUpperCase()} + + +

+ {item.template} +

+
+ + ))}
- -

+ + + } + id='panel1d-header' + > + Description + + + + {selectedPrompt?.description} + + + + + } + aria-controls='panel3d-content' + id='panel3d-header' + > + Readme + + +

+ + ) : ( + + {children} + + ) + } }} > - {item.template} -

- - - ))} - - - - handleAccordionChange('panel3')}> - } - aria-controls='panel3d-content' - id='panel3d-header' - > - Readme - - - - {selectedPrompt?.readme} - - - - - + {selectedPrompt?.readme} +
+
+
+
+
+
+
+
+
-
-
+ + )}
- - - onSubmit(selectedPrompt.detailed)} variant='contained'> - Submit - - + {availablePrompNameList && availablePrompNameList.length > 0 && ( + + + onSubmit(selectedPrompt.detailed)} + variant='contained' + > + Load + + + )} ) : null diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 24ba3c92..7920af6a 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -231,7 +231,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA flexDirection: 'row', width: '100%' }} - sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 2 }} + sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }} variant='outlined' onClick={() => onShowPromptHubButtonClicked()} endIcon={} From bfeebcd229477b95bf67b404a234e1d9416704fa Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 5 Dec 2023 19:02:33 +0000 Subject: [PATCH 020/502] update Zep version --- .../nodes/memory/ZepMemory/ZepMemory.ts | 104 ++++++++---------- packages/components/package.json | 2 +- .../chatflows/Long Term Memory.json | 20 +--- 3 files changed, 51 insertions(+), 75 deletions(-) diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index ced871a1..e72a6704 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -1,9 +1,8 @@ -import { SystemMessage } from 'langchain/schema' +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 { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' -import { getBufferString } from 'langchain/memory' class ZepMemory_Memory implements INode { label: string @@ -20,7 +19,7 @@ class ZepMemory_Memory implements INode { constructor() { this.label = 'Zep Memory' this.name = 'ZepMemory' - this.version = 1.0 + this.version = 2.0 this.type = 'ZepMemory' this.icon = 'zep.png' this.category = 'Memory' @@ -41,17 +40,12 @@ class ZepMemory_Memory implements INode { type: 'string', default: 'http://127.0.0.1:8000' }, - { - label: 'Auto Summary', - name: 'autoSummary', - type: 'boolean', - default: true - }, { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -60,15 +54,10 @@ class ZepMemory_Memory implements INode { label: 'Size', name: 'k', type: 'number', - default: '10', - description: 'Window of size k to surface the last k back-and-forth to use as memory.' - }, - { - label: 'Auto Summary Template', - name: 'autoSummaryTemplate', - type: 'string', - default: 'This is the summary of the following conversation:\n{summary}', - additionalParams: true + placeholder: '10', + description: 'Window of size k to surface the last k back-and-forth to use as memory.', + additionalParams: true, + optional: true }, { label: 'AI Prefix', @@ -109,36 +98,7 @@ class ZepMemory_Memory implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const autoSummaryTemplate = nodeData.inputs?.autoSummaryTemplate as string - const autoSummary = nodeData.inputs?.autoSummary as boolean - - const k = nodeData.inputs?.k as string - - let zep = await initalizeZep(nodeData, options) - - // hack to support summary - let tmpFunc = zep.loadMemoryVariables - zep.loadMemoryVariables = async (values) => { - let data = await tmpFunc.bind(zep, values)() - if (autoSummary && zep.returnMessages && data[zep.memoryKey] && data[zep.memoryKey].length) { - const zepClient = await zep.zepClientPromise - const memory = await zepClient.memory.getMemory(zep.sessionId, parseInt(k, 10) ?? 10) - if (memory?.summary) { - let summary = autoSummaryTemplate.replace(/{summary}/g, memory.summary.content) - // eslint-disable-next-line no-console - console.log('[ZepMemory] auto summary:', summary) - data[zep.memoryKey].unshift(new SystemMessage(summary)) - } - } - // for langchain zep memory compatibility, or we will get "Missing value for input variable chat_history" - if (data instanceof Array) { - data = { - [zep.memoryKey]: data - } - } - return data - } - return zep + return await initalizeZep(nodeData, options) } //@ts-ignore @@ -169,40 +129,72 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis const humanPrefix = nodeData.inputs?.humanPrefix as string const memoryKey = nodeData.inputs?.memoryKey as string const inputKey = nodeData.inputs?.inputKey as string - const sessionId = nodeData.inputs?.sessionId as string + const k = nodeData.inputs?.k 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 obj: ZepMemoryInput & Partial = { + const obj: ZepMemoryInput & ZepMemoryExtendedInput = { baseURL, sessionId: sessionId ? sessionId : chatId, aiPrefix, humanPrefix, returnMessages: true, memoryKey, - inputKey + inputKey, + isSessionIdUsingChatMessageId, + k: k ? parseInt(k, 10) : undefined } if (apiKey) obj.apiKey = apiKey - if (isSessionIdUsingChatMessageId) obj.isSessionIdUsingChatMessageId = true return new ZepMemoryExtended(obj) } interface ZepMemoryExtendedInput { isSessionIdUsingChatMessageId: boolean + k?: number } class ZepMemoryExtended extends ZepMemory { isSessionIdUsingChatMessageId? = false + lastN?: number - constructor(fields: ZepMemoryInput & Partial) { + constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) { super(fields) this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + this.lastN = fields.k + } + + async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + super.sessionId = overrideSessionId + } + return super.loadMemoryVariables({ ...values, lastN: this.lastN }) + } + + async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + super.sessionId = overrideSessionId + } + return super.saveContext(inputValues, outputValues) + } + + async clear(overrideSessionId = ''): Promise { + if (overrideSessionId) { + super.sessionId = overrideSessionId + } + return super.clear() } } diff --git a/packages/components/package.json b/packages/components/package.json index bea9a7a0..dd87754d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -21,7 +21,7 @@ "@aws-sdk/client-s3": "^3.427.0", "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", - "@getzep/zep-js": "^0.6.3", + "@getzep/zep-js": "^0.9.0", "@gomomento/sdk": "^1.51.1", "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index c508b480..c39f746a 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -205,7 +205,7 @@ "data": { "id": "ZepMemory_0", "label": "Zep Memory", - "version": 1, + "version": 2, "name": "ZepMemory", "type": "ZepMemory", "baseClasses": ["ZepMemory", "BaseChatMemory", "BaseMemory"], @@ -228,13 +228,6 @@ "default": "http://127.0.0.1:8000", "id": "ZepMemory_0-input-baseURL-string" }, - { - "label": "Auto Summary", - "name": "autoSummary", - "type": "boolean", - "default": true, - "id": "ZepMemory_0-input-autoSummary-boolean" - }, { "label": "Session Id", "name": "sessionId", @@ -251,17 +244,10 @@ "type": "number", "default": "10", "step": 1, + "additionalParams": true, "description": "Window of size k to surface the last k back-and-forths to use as memory.", "id": "ZepMemory_0-input-k-number" }, - { - "label": "Auto Summary Template", - "name": "autoSummaryTemplate", - "type": "string", - "default": "This is the summary of the following conversation:\n{summary}", - "additionalParams": true, - "id": "ZepMemory_0-input-autoSummaryTemplate-string" - }, { "label": "AI Prefix", "name": "aiPrefix", @@ -306,10 +292,8 @@ "inputAnchors": [], "inputs": { "baseURL": "http://127.0.0.1:8000", - "autoSummary": true, "sessionId": "", "k": "10", - "autoSummaryTemplate": "This is the summary of the following conversation:\n{summary}", "aiPrefix": "ai", "humanPrefix": "human", "memoryKey": "chat_history", From 59308665c2b8c83b2b77ecd2a0753a86847ea037 Mon Sep 17 00:00:00 2001 From: takuabonn Date: Wed, 6 Dec 2023 07:32:45 +0900 Subject: [PATCH 021/502] remove_props --- packages/ui/src/views/chatflows/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 0ace4033..c87ad306 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -161,7 +161,6 @@ const Chatflows = () => { variant='contained' value='card' title='Card View' - selectedcolor='#00abc0' > From d397adb47a1c363e44b1ddcd3b2965ad8466cf09 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 6 Dec 2023 00:30:51 +0000 Subject: [PATCH 022/502] avoid button showing up for other systemprompt like conversation chain --- .../ui/src/views/canvas/NodeInputHandler.js | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 7920af6a..fc2e7ac8 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -223,29 +223,30 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA )} - {(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( - <> - - setShowPromptHubDialog(false)} - onSubmit={onShowPromptHubButtonSubmit} - > - - )} + {(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') && + (inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && ( + <> + + setShowPromptHubDialog(false)} + onSubmit={onShowPromptHubButtonSubmit} + > + + )}
{inputParam.label} From 2da6f598340593556bc40860babbf474a52fae62 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 6 Dec 2023 01:51:14 +0000 Subject: [PATCH 023/502] fix ttl parseInt error --- packages/components/nodes/cache/RedisCache/RedisCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index 3b68cf12..8128b6e3 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -89,7 +89,7 @@ class RedisCache implements INode { redisClient.update = async (prompt: string, llmKey: string, value: Generation[]) => { for (let i = 0; i < value.length; i += 1) { const key = getCacheKey(prompt, llmKey, String(i)) - if (ttl !== undefined) { + if (ttl) { await client.set(key, JSON.stringify(serializeGeneration(value[i])), 'EX', parseInt(ttl, 10)) } else { await client.set(key, JSON.stringify(serializeGeneration(value[i]))) From 73f7046316ac8cf6645515332b0c3f3b8aed7c95 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 12:31:33 +0530 Subject: [PATCH 024/502] GPT Vision: Initial implementation of the OpenAI Vision API --- .../chains/VisionChain/OpenAIVisionChain.ts | 24 +- .../nodes/chains/VisionChain/VLLMChain.ts | 1 + .../nodes/chains/VisionChain/chain.svg | 6 + packages/server/src/Interface.ts | 2 + .../src/database/entities/ChatMessage.ts | 3 + ...01788586491-AddFileUploadsToChatMessage.ts | 12 + .../src/database/migrations/mysql/index.ts | 4 +- ...01788586491-AddFileUploadsToChatMessage.ts | 11 + .../src/database/migrations/postgres/index.ts | 4 +- ...01788586491-AddFileUploadsToChatMessage.ts | 20 ++ .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 44 ++- packages/ui/src/api/chatflows.js | 4 +- .../ui/src/views/chatmessage/ChatMessage.css | 29 ++ .../ui/src/views/chatmessage/ChatMessage.js | 301 +++++++++++++++++- 15 files changed, 447 insertions(+), 22 deletions(-) create mode 100644 packages/components/nodes/chains/VisionChain/chain.svg create mode 100644 packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts diff --git a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts index f2260a76..7745f05d 100644 --- a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts +++ b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts @@ -12,6 +12,7 @@ class OpenAIVisionChain_Chains implements INode { version: number type: string icon: string + badge: string category: string baseClasses: string[] description: string @@ -21,10 +22,11 @@ class OpenAIVisionChain_Chains implements INode { constructor() { this.label = 'Open AI Vision Chain' this.name = 'openAIVisionChain' - this.version = 3.0 + this.version = 1.0 this.type = 'OpenAIVisionChain' this.icon = 'chain.svg' this.category = 'Chains' + this.badge = 'EXPERIMENTAL' this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .' this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] this.inputs = [ @@ -63,6 +65,20 @@ class OpenAIVisionChain_Chains implements INode { type: 'string', placeholder: 'Name Your Chain', optional: true + }, + { + label: 'Accepted Upload Types', + name: 'allowedUploadTypes', + type: 'string', + default: 'image/gif;image/jpeg;image/png;image/webp', + hidden: true + }, + { + label: 'Maximum Upload Size (MB)', + name: 'maxUploadSize', + type: 'number', + default: '5', + hidden: true } ] this.outputs = [ @@ -93,7 +109,7 @@ class OpenAIVisionChain_Chains implements INode { openAIApiKey: openAIModel.openAIApiKey, imageResolution: imageResolution, verbose: process.env.DEBUG === 'true', - imageUrls: options.url, + imageUrls: options.uploads, openAIModel: openAIModel } if (output === this.name) { @@ -156,8 +172,8 @@ const runPrediction = async ( * TO: { "value": "hello i am ben\n\n\thow are you?" } */ const promptValues = handleEscapeCharacters(promptValuesRaw, true) - if (options?.url) { - chain.imageUrls = options.url + if (options?.uploads) { + chain.imageUrls = options.uploads } if (promptValues && inputVariables.length > 0) { let seen: string[] = [] diff --git a/packages/components/nodes/chains/VisionChain/VLLMChain.ts b/packages/components/nodes/chains/VisionChain/VLLMChain.ts index 17260be2..59a2483a 100644 --- a/packages/components/nodes/chains/VisionChain/VLLMChain.ts +++ b/packages/components/nodes/chains/VisionChain/VLLMChain.ts @@ -79,6 +79,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { messages: [] } if (this.openAIModel.maxTokens) vRequest.max_tokens = this.openAIModel.maxTokens + else vRequest.max_tokens = 1024 const userRole: any = { role: 'user' } userRole.content = [] diff --git a/packages/components/nodes/chains/VisionChain/chain.svg b/packages/components/nodes/chains/VisionChain/chain.svg new file mode 100644 index 00000000..a5b32f90 --- /dev/null +++ b/packages/components/nodes/chains/VisionChain/chain.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index c562b4ee..30b4bd35 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -31,6 +31,7 @@ export interface IChatMessage { sourceDocuments?: string usedTools?: string fileAnnotations?: string + fileUploads?: string chatType: string chatId: string memoryType?: string @@ -167,6 +168,7 @@ export interface IncomingInput { socketIOClientId?: string chatId?: string stopNodeId?: string + uploads?: string } export interface IActiveChatflows { diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index 4054a26d..c803ce50 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -26,6 +26,9 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) fileAnnotations?: string + @Column({ nullable: true, type: 'text' }) + fileUploads?: string + @Column() chatType: string diff --git a/packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts b/packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts new file mode 100644 index 00000000..d896066b --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_message', 'fileUploads') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileUploads\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileUploads\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 8f9824a8..f5adff64 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 { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' export const mysqlMigrations = [ Init1693840429259, @@ -23,5 +24,6 @@ export const mysqlMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddFileUploadsToChatMessage1701788586491 ] diff --git a/packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts new file mode 100644 index 00000000..6574ac81 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileUploads" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileUploads";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index d196fbc1..f80335a0 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 { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' export const postgresMigrations = [ Init1693891895163, @@ -23,5 +24,6 @@ export const postgresMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddFileUploadsToChatMessage1701788586491 ] diff --git a/packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts new file mode 100644 index 00000000..68e33220 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "fileUploads" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "fileAnnotations", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "fileAnnotations", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileUploads";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index fdd83064..bae0cec8 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 { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -23,5 +24,6 @@ export const sqliteMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddFileUploadsToChatMessage1701788586491 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9bc3eb3a..195eaf1d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -410,9 +410,7 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const obj = { - allowUploads: this.shouldAllowUploads(chatflow) - } + const obj = this.shouldAllowUploads(chatflow) return res.json(obj) }) @@ -1255,16 +1253,30 @@ export class App { } private uploadAllowedNodes = ['OpenAIVisionChain'] - private shouldAllowUploads(result: ChatFlow): boolean { + private shouldAllowUploads(result: ChatFlow): any { const flowObj = JSON.parse(result.flowData) let allowUploads = false + let allowedTypes: string[] = [] + let maxUploadSize: number = -1 flowObj.nodes.forEach((node: IReactFlowNode) => { if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) { logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) allowUploads = true + node.data.inputParams.map((param: any) => { + if (param.name === 'allowedUploadTypes') { + allowedTypes = param.default.split(';') + } + if (param.name === 'maxUploadSize') { + maxUploadSize = parseInt(param.default ? param.default : '0') + } + }) } }) - return allowUploads + return { + allowUploads, + allowedTypes, + maxUploadSize + } } /** @@ -1392,6 +1404,23 @@ export class App { if (!isKeyValidated) return res.status(401).send('Unauthorized') } + if (incomingInput.uploads) { + // @ts-ignore + ;(incomingInput.uploads as any[]).forEach((url: any) => { + if (url.type === 'file') { + const filename = url.name + const bf = url.data + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', filename) + if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'gptvision'))) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + } + if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, bf) + fs.unlinkSync(filePath) + url.data = bf.toString('base64') + } + }) + } + let isStreamValid = false const files = (req.files as any[]) || [] @@ -1534,6 +1563,7 @@ export class App { let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + uploads: incomingInput.uploads, chatHistory: incomingInput.history, socketIO, socketIOClientId: incomingInput.socketIOClientId, @@ -1544,6 +1574,7 @@ export class App { chatId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + uploads: incomingInput.uploads, chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, @@ -1567,7 +1598,8 @@ export class App { chatId, memoryType, sessionId, - createdDate: userMessageDateTime + createdDate: userMessageDateTime, + fileUploads: incomingInput.uploads ? JSON.stringify(incomingInput.uploads) : '' } await this.addChatMessage(userMessage) diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index 8810b5a5..c02ca5cd 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -13,6 +13,7 @@ const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body) const deleteChatflow = (id) => client.delete(`/chatflows/${id}`) const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`) +const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`) export default { getAllChatflows, @@ -21,5 +22,6 @@ export default { createNewChatflow, updateChatflow, deleteChatflow, - getIsChatflowStreaming + getIsChatflowStreaming, + getAllowChatflowUploads } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 2298fee6..f1831d39 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -144,3 +144,32 @@ justify-content: center; align-items: center; } + +.file-drop-field { + position: relative; /* Needed to position the icon correctly */ + /* Other styling for the field */ +} + +.drop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(137, 134, 134, 0.83); /* Semi-transparent white */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 10; /* Ensure it's above other content */ + border: 2px dashed #0094ff; /* Example style */ +} + +.preview-container { + +} + +.button { + flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ + margin: 5px; /* Adjust as needed for spacing between buttons */ +} \ No newline at end of file diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index c610f944..0243f252 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' import PropTypes from 'prop-types' import socketIOClient from 'socket.io-client' @@ -9,9 +9,23 @@ import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import axios from 'axios' -import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material' +import { + Box, + Button, + Card, + CardActions, + CardMedia, + Chip, + CircularProgress, + Divider, + Grid, + IconButton, + InputAdornment, + OutlinedInput, + Typography +} from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconSend, IconDownload } from '@tabler/icons' +import { IconDownload, IconSend, IconUpload } from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' @@ -33,6 +47,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' +import DeleteIcon from '@mui/icons-material/Delete' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -58,6 +73,185 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + const fileUploadRef = useRef(null) + const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) + const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) + const [previews, setPreviews] = useState([]) + const [isDragOver, setIsDragOver] = useState(false) + const handleDragOver = (e) => { + if (!isChatFlowAvailableForUploads) { + return + } + e.preventDefault() + } + const isFileAllowedForUpload = (file) => { + // check if file type is allowed + if (getAllowChatFlowUploads.data?.allowedTypes?.length > 0) { + const allowedFileTypes = getAllowChatFlowUploads.data?.allowedTypes + if (!allowedFileTypes.includes(file.type)) { + alert(`File ${file.name} is not allowed.\nAllowed file types are ${allowedFileTypes.join(', ')}.`) + return false + } + } + // check if file size is allowed + if (getAllowChatFlowUploads.data?.maxUploadSize > 0) { + const sizeInMB = file.size / 1024 / 1024 + if (sizeInMB > getAllowChatFlowUploads.data?.maxUploadSize) { + alert(`File ${file.name} is too large.\nMaximum allowed size is ${getAllowChatFlowUploads.data?.maxUploadSize} MB.`) + return false + } + } + return true + } + const handleDrop = async (e) => { + if (!isChatFlowAvailableForUploads) { + return + } + e.preventDefault() + setIsDragOver(false) + let files = [] + if (e.dataTransfer.files.length > 0) { + for (const file of e.dataTransfer.files) { + if (isFileAllowedForUpload(file) === false) { + return + } + const reader = new FileReader() + const { name } = file + files.push( + new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return + } + const { result } = evt.target + resolve({ + data: result, + preview: URL.createObjectURL(file), + type: 'file', + name: name + }) + } + reader.readAsDataURL(file) + }) + ) + } + + const newFiles = await Promise.all(files) + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) + } + if (e.dataTransfer.items) { + const newUploads = [] + for (const item of e.dataTransfer.items) { + if (item.kind === 'string' && item.type.match('^text/uri-list')) { + item.getAsString((s) => { + let upload = { + data: s, + preview: s, + type: 'url', + name: s.substring(s.lastIndexOf('/') + 1) + } + setPreviews((prevPreviews) => [...prevPreviews, upload]) + }) + } else if (item.kind === 'string' && item.type.match('^text/html')) { + item.getAsString((s) => { + if (s.indexOf('href') === -1) return + //extract href + let start = s.substring(s.indexOf('href') + 6) + let hrefStr = start.substring(0, start.indexOf('"')) + + let upload = { + data: hrefStr, + preview: hrefStr, + type: 'url', + name: hrefStr.substring(hrefStr.lastIndexOf('/') + 1) + } + setPreviews((prevPreviews) => [...prevPreviews, upload]) + }) + } + } + } + } + const handleFileChange = async (event) => { + const fileObj = event.target.files && event.target.files[0] + if (!fileObj) { + return + } + let files = [] + for (const file of event.target.files) { + if (isFileAllowedForUpload(file) === false) { + return + } + const reader = new FileReader() + const { name } = file + files.push( + new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return + } + const { result } = evt.target + resolve({ + data: result, + preview: URL.createObjectURL(file), + type: 'file', + name: name + }) + } + reader.readAsDataURL(file) + }) + ) + } + + const newFiles = await Promise.all(files) + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) + // 👇️ reset file input + event.target.value = null + } + + const handleDragEnter = (e) => { + if (isChatFlowAvailableForUploads) { + e.preventDefault() + setIsDragOver(true) + } + } + + const handleDragLeave = (e) => { + if (isChatFlowAvailableForUploads) { + e.preventDefault() + if (e.originalEvent?.pageX !== 0 || e.originalEvent?.pageY !== 0) { + return false + } + setIsDragOver(false) // Set the drag over state to false when the drag leaves + } + } + const handleDeletePreview = (itemToDelete) => { + if (itemToDelete.type === 'file') { + URL.revokeObjectURL(itemToDelete.preview) // Clean up for file + } + setPreviews(previews.filter((item) => item !== itemToDelete)) + } + const handleUploadClick = () => { + // 👇️ open file input box on click of another element + fileUploadRef.current.click() + } + + const previewStyle = { + width: '64px', + height: '64px', + objectFit: 'cover' // This makes the image cover the area, cropping it if necessary + } + const messageImageStyle = { + width: '128px', + height: '128px', + objectFit: 'cover' // This makes the image cover the area, cropping it if necessary + } + + const clearPreviews = () => { + // Revoke the data uris to avoid memory leaks + previews.forEach((file) => URL.revokeObjectURL(file.preview)) + setPreviews([]) + } + const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -113,7 +307,16 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } setLoading(true) - setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }]) + const urls = [] + previews.map((item) => { + urls.push({ + data: item.data, + type: item.type, + name: item.name + }) + }) + clearPreviews() + setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage', fileUploads: urls }]) // Send user question and history to API try { @@ -122,6 +325,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'), chatId } + if (urls) params.uploads = urls if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) @@ -209,6 +413,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) + if (message.fileUploads) obj.fileUploads = JSON.parse(message.fileUploads) return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) @@ -227,6 +432,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getIsChatflowStreamingApi.data]) + // Get chatflow uploads capability + useEffect(() => { + if (getAllowChatFlowUploads.data) { + setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.allowUploads ?? false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllowChatFlowUploads.data]) + // Auto scroll chat to bottom useEffect(() => { scrollToBottom() @@ -245,6 +458,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (open && chatflowid) { getChatmessageApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid) + getAllowChatFlowUploads.request(chatflowid) scrollToBottom() socket = socketIOClient(baseURL) @@ -281,9 +495,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, [open, chatflowid]) return ( - <> -
-
+
+ {isDragOver && ( + + Drop here to upload + {getAllowChatFlowUploads.data?.allowedTypes?.join(', ')} + Max Allowed Size: {getAllowChatFlowUploads.data?.maxUploadSize} MB + + )} +
+
{messages && messages.map((message, index) => { return ( @@ -375,6 +602,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })}
)} + {message.fileUploads && + message.fileUploads.map((item, index) => { + return ( + + + + ) + })} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { @@ -430,6 +671,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { onChange={onChange} multiline={true} maxRows={isDialog ? 7 : 2} + startAdornment={ + isChatFlowAvailableForUploads && ( + + + + + + ) + } endAdornment={ @@ -447,11 +704,39 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } /> + {isChatFlowAvailableForUploads && ( + + )}
+ {previews && previews.length > 0 && ( + + {previews.map((item, index) => ( + + + + +
) } From 8122377bbb794564d89fa79096fcff6a209a2f9e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 12:53:43 +0530 Subject: [PATCH 025/502] LS Prompt Hub: Minor fixes --- packages/server/src/index.ts | 2 +- .../ui/src/ui-component/dialog/PromptLangsmithHubDialog.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 42fb326d..9df016d0 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1077,7 +1077,7 @@ export class App { headers['x-api-key'] = decryptedCredentialData.langsmithApiKey const tags = req.body.tags ? `tags=${req.body.tags}` : '' - const url = `https://web.hub.langchain.com/repos/?${tags}offset=0&limit=20&has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + const url = `https://web.hub.langchain.com/repos/?${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` axios.get(url, headers).then((response) => { if (response.data.repos) { return res.json({ status: 'OK', repos: response.data.repos }) diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index e1cfaaa9..e6f06e20 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -181,7 +181,6 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { } } setSelectedPrompt(prompt) - await new Promise((resolve) => setTimeout(resolve, 500)) } const fetchPrompts = async () => { @@ -201,7 +200,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { }) if (createResp.data) { setAvailablePrompNameList(createResp.data.repos) - if (createResp.data.repos?.length) handleListItemClick(0, createResp.data.repos) + if (createResp.data.repos?.length) await handleListItemClick(0, createResp.data.repos) } } From cc1a3101e26d22642ac4d74d63528f474f0bd43f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 15:01:30 +0530 Subject: [PATCH 026/502] S3 File Loader: Region missing fix --- packages/components/nodes/documentloaders/S3File/S3File.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/documentloaders/S3File/S3File.ts b/packages/components/nodes/documentloaders/S3File/S3File.ts index 07295aba..58ffd8af 100644 --- a/packages/components/nodes/documentloaders/S3File/S3File.ts +++ b/packages/components/nodes/documentloaders/S3File/S3File.ts @@ -162,8 +162,11 @@ class S3_DocumentLoaders implements INode { accessKeyId?: string secretAccessKey?: string } = { - accessKeyId, - secretAccessKey + region, + credentials: { + accessKeyId, + secretAccessKey + } } loader.load = async () => { From 275540d1830575c56679666e759ba35f74699f91 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 6 Dec 2023 17:39:18 +0000 Subject: [PATCH 027/502] add default limit to 100 --- packages/server/src/index.ts | 3 ++- packages/ui/src/views/canvas/NodeInputHandler.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9df016d0..3d8208f9 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1077,7 +1077,8 @@ export class App { headers['x-api-key'] = decryptedCredentialData.langsmithApiKey const tags = req.body.tags ? `tags=${req.body.tags}` : '' - const url = `https://web.hub.langchain.com/repos/?${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + // Default to 100, TODO: add pagination and use offset & limit + const url = `https://web.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` axios.get(url, headers).then((response) => { if (response.data.repos) { return res.json({ status: 'OK', repos: response.data.repos }) diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index fc2e7ac8..103af6b4 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -232,6 +232,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA flexDirection: 'row', width: '100%' }} + disabled={disabled} sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }} variant='outlined' onClick={() => onShowPromptHubButtonClicked()} From b492153f8a6f4ec642e5c0efabb38a80908171a2 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 7 Dec 2023 14:26:17 +0530 Subject: [PATCH 028/502] GPT Vision: Storing filenames only in chat message --- .../nodes/chains/VisionChain/VLLMChain.ts | 13 +++- packages/server/src/index.ts | 15 +++-- .../ui/src/views/chatmessage/ChatMessage.js | 65 ++++++++++--------- 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/components/nodes/chains/VisionChain/VLLMChain.ts b/packages/components/nodes/chains/VisionChain/VLLMChain.ts index 59a2483a..f9b92e53 100644 --- a/packages/components/nodes/chains/VisionChain/VLLMChain.ts +++ b/packages/components/nodes/chains/VisionChain/VLLMChain.ts @@ -3,6 +3,9 @@ import { BaseChain, ChainInputs } from 'langchain/chains' import { ChainValues } from 'langchain/schema' import { BasePromptTemplate, ChatPromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' import { ChatOpenAI } from 'langchain/chat_models/openai' +import path from 'path' +import { getUserHome } from '../../../src/utils' +import fs from 'fs' /** * Interface for the input parameters of the OpenAIVisionChain class. @@ -89,10 +92,18 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { }) if (this.imageUrls && this.imageUrls.length > 0) { this.imageUrls.forEach((imageUrl: any) => { + let bf = imageUrl?.data + if (imageUrl.type == 'stored-file') { + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', imageUrl.data) + + // as the image is stored in the server, read the file and convert it to base64 + const contents = fs.readFileSync(filePath) + bf = 'data:' + imageUrl.mime + ';base64,' + contents.toString('base64') + } userRole.content.push({ type: 'image_url', image_url: { - url: imageUrl?.data, + url: bf, detail: this.imageResolution } }) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 70978c6a..83b018d5 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1349,17 +1349,20 @@ export class App { if (incomingInput.uploads) { // @ts-ignore - ;(incomingInput.uploads as any[]).forEach((url: any) => { - if (url.type === 'file') { - const filename = url.name - const bf = url.data + ;(incomingInput.uploads as any[]).forEach((upload: any) => { + if (upload.type === 'file') { + const filename = upload.name const filePath = path.join(getUserHome(), '.flowise', 'gptvision', filename) if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'gptvision'))) { fs.mkdirSync(path.dirname(filePath), { recursive: true }) } + const splitDataURI = upload.data.split(',') + //const fname = splitDataURI.pop()?.split(':')[1] ?? '' + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, bf) - fs.unlinkSync(filePath) - url.data = bf.toString('base64') + // don't need to store the file contents in chatmessage, just the filename + upload.data = filename //bf.toString('base64') + upload.type = 'stored-file' } }) } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0243f252..92c73699 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -128,7 +128,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { data: result, preview: URL.createObjectURL(file), type: 'file', - name: name + name: name, + mime: file.type }) } reader.readAsDataURL(file) @@ -138,9 +139,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const newFiles = await Promise.all(files) setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) + // if (newFiles.length > 0) { + // document.getElementById('messagelist').style.height = '80%' + // } } if (e.dataTransfer.items) { - const newUploads = [] for (const item of e.dataTransfer.items) { if (item.kind === 'string' && item.type.match('^text/uri-list')) { item.getAsString((s) => { @@ -194,7 +197,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { data: result, preview: URL.createObjectURL(file), type: 'file', - name: name + name: name, + mime: file.type }) } reader.readAsDataURL(file) @@ -312,7 +316,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { urls.push({ data: item.data, type: item.type, - name: item.name + name: item.name, + mime: item.mime }) }) clearPreviews() @@ -510,7 +515,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { )}
-
+
{messages && messages.map((message, index) => { return ( @@ -710,31 +715,33 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
- {previews && previews.length > 0 && ( - - {previews.map((item, index) => ( - - - - -
setSourceDialogOpen(false)} />
) From e67c43157a53cc208776431c1fad829f5170d9fd Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 7 Dec 2023 16:06:32 +0530 Subject: [PATCH 029/502] XSS: Simplified by adding XSS middleware --- packages/server/package.json | 2 +- packages/server/src/index.ts | 421 ++++++++++--------------------- packages/server/src/utils/XSS.ts | 11 + 3 files changed, 142 insertions(+), 292 deletions(-) create mode 100644 packages/server/src/utils/XSS.ts diff --git a/packages/server/package.json b/packages/server/package.json index 38c20389..97a95d43 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,7 +54,6 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-rate-limit": "^6.9.0", - "express-validator": "^7.0.1", "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", @@ -64,6 +63,7 @@ "reflect-metadata": "^0.1.13", "socket.io": "^4.6.1", "sqlite3": "^5.1.6", + "strip-js": "^1.2.0", "typeorm": "^0.3.6", "uuid": "^9.0.1", "winston": "^3.9.0" diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 61f34e92..d40b42bf 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -58,7 +58,7 @@ import { CachePool } from './CachePool' import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' -import { body, param, query, validationResult } from 'express-validator' +import { sanitizeMiddleware } from './utils/XSS' export class App { app: express.Application @@ -122,6 +122,9 @@ export class App { // Add the expressRequestLogger middleware to log all requests this.app.use(expressRequestLogger) + // Add the sanitizeMiddleware to guard against XSS + this.app.use(sanitizeMiddleware) + if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) { const username = process.env.FLOWISE_USERNAME const password = process.env.FLOWISE_PASSWORD @@ -184,27 +187,17 @@ export class App { }) // Get specific component node via name - this.app.get('/api/v1/nodes/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { return res.json(this.nodesPool.componentNodes[req.params.name]) } else { - throw new Error(`Node ${name} not found`) + throw new Error(`Node ${req.params.name} not found`) } }) // Get component credential via name - this.app.get('/api/v1/components-credentials/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Credential ${name} not found`) - } - if (!req.params.name.includes('&')) { + this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -212,7 +205,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { @@ -224,14 +217,9 @@ export class App { }) // Returns specific component node icon via name - this.app.get('/api/v1/node-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { - const nodeInstance = this.nodesPool.componentNodes[name] + this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + const nodeInstance = this.nodesPool.componentNodes[req.params.name] if (nodeInstance.icon === undefined) { throw new Error(`Node ${req.params.name} icon not found`) } @@ -240,48 +228,38 @@ export class App { const filepath = nodeInstance.icon res.sendFile(filepath) } else { - throw new Error(`Node ${name} icon is missing icon`) + throw new Error(`Node ${req.params.name} icon is missing icon`) } } else { - throw new Error(`Node ${name} not found`) + throw new Error(`Node ${req.params.name} not found`) } }) // Returns specific component credential icon via name - this.app.get('/api/v1/components-credentials-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Credential ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { - const credInstance = this.nodesPool.componentCredentials[name] + this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { + const credInstance = this.nodesPool.componentCredentials[req.params.name] if (credInstance.icon === undefined) { - throw new Error(`Credential ${name} icon not found`) + throw new Error(`Credential ${req.params.name} icon not found`) } if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { const filepath = credInstance.icon res.sendFile(filepath) } else { - throw new Error(`Credential ${name} is missing icon`) + throw new Error(`Credential ${req.params.name} icon is missing icon`) } } else { - throw new Error(`Credential ${name} not found`) + throw new Error(`Credential ${req.params.name} not found`) } }) // load async options - this.app.post('/api/v1/node-load-method/:name', param('name').notEmpty().escape(), async (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } + this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { const nodeData: INodeData = req.body - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { try { - const nodeInstance = this.nodesPool.componentNodes[name] + const nodeInstance = this.nodesPool.componentNodes[req.params.name] const methodName = nodeData.loadMethod || '' const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { @@ -294,7 +272,7 @@ export class App { return res.json([]) } } else { - res.status(404).send(`Node ${name} not found`) + res.status(404).send(`Node ${req.params.name} not found`) return } }) @@ -310,11 +288,7 @@ export class App { }) // Get specific chatflow via api key - this.app.get('/api/v1/chatflows/apikey/:apiKey', param('apiKey').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(401).send('Unauthorized') - } + this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => { try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') @@ -326,19 +300,14 @@ export class App { .orderBy('cf.name', 'ASC') .getMany() if (chatflows.length >= 1) return res.status(200).send(chatflows) - return res.status(404).send('APIKey not found') + return res.status(404).send('Chatflow not found') } catch (err: any) { return res.status(500).send(err?.message) } }) // Get specific chatflow via id - this.app.get('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -347,12 +316,7 @@ export class App { }) // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) - this.app.get('/api/v1/public-chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -374,69 +338,48 @@ export class App { }) // Update chatflow - this.app.put( - '/api/v1/chatflows/:id', - body('chatflow.id').notEmpty(), - param('id').notEmpty().escape(), - async (req: Request, res: Response) => { - const chatflowId = req.params.id - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowId - }) + this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) - if (!chatflow) { - res.status(404).send(`Chatflow ${chatflowId} not found`) - return - } - - const body = req.body - const updateChatFlow = new ChatFlow() - Object.assign(updateChatFlow, body) - - updateChatFlow.id = chatflow.id - createRateLimiter(updateChatFlow) - - this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) - const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - // chatFlowPool is initialized only when a flow is opened - // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined - if (this.chatflowPool) { - // Update chatflowpool inSync to false, to build Langchain again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) - } - - return res.json(result) + if (!chatflow) { + res.status(404).send(`Chatflow ${req.params.id} not found`) + return } - ) + + const body = req.body + const updateChatFlow = new ChatFlow() + Object.assign(updateChatFlow, body) + + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + + this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) + const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + + // chatFlowPool is initialized only when a flow is opened + // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined + if (this.chatflowPool) { + // Update chatflowpool inSync to false, to build Langchain again because data has been changed + this.chatflowPool.updateInSync(chatflow.id, false) + } + + return res.json(result) + }) // Delete chatflow via id - this.app.delete('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) return res.json(results) }) // Check if chatflow valid for streaming - this.app.get('/api/v1/chatflows-streaming/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } - + this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowId + id: req.params.id }) - if (!chatflow) return res.status(404).send(`Chatflow ${chatflowId} not found`) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) /*** Get Ending Node with Directed Graph ***/ const flowData = chatflow.flowData @@ -466,84 +409,58 @@ export class App { // ---------------------------------------- // Get all chatmessages from chatflowid - this.app.get( - '/api/v1/chatmessage/:id', - query('chatId').notEmpty().escape(), - query('sortOrder').notEmpty().escape(), - query('memoryType').notEmpty().escape(), - query('sessionId').notEmpty().escape(), - query('startDate').notEmpty().escape(), - query('endDate').notEmpty().escape(), - query('chatTypeFilter').notEmpty().escape(), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - let chatTypeFilter = req.query?.chatType as chatType | undefined + this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + let chatTypeFilter = req.query?.chatType as chatType | undefined - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL } + } catch (e) { + return res.status(500).send(e) } - - const chatmessages = await this.getChatMessage( - req.params.id, - chatTypeFilter, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate - ) - return res.json(chatmessages) } - ) + + const chatmessages = await this.getChatMessage( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate + ) + return res.json(chatmessages) + }) // Get internal chatmessages from chatflowid - this.app.get('/api/v1/internal-chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL) return res.json(chatmessages) }) // Add chatmessages for chatflowid - this.app.post('/api/v1/chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const body = req.body const results = await this.addChatMessage(body) return res.json(results) }) // Delete all chatmessages from chatId - this.app.delete('/api/v1/chatmessage/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const chatflowid = req.params.id const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -627,11 +544,7 @@ export class App { }) // Get specific credential - this.app.get('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => { const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -652,11 +565,7 @@ export class App { }) // Update credential - this.app.put('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => { const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -672,11 +581,7 @@ export class App { }) // Delete all chatmessages from chatflowid - this.app.delete('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) }) @@ -692,11 +597,7 @@ export class App { }) // Get specific tool - this.app.get('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -716,11 +617,7 @@ export class App { }) // Update tool - this.app.put('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -741,11 +638,7 @@ export class App { }) // Delete tool - this.app.delete('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) return res.json(results) }) @@ -761,11 +654,7 @@ export class App { }) // Get specific assistant - this.app.get('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -773,46 +662,33 @@ export class App { }) // Get assistant object - this.app.get( - '/api/v1/openai-assistants/:id', - param('id').notEmpty().escape(), - query('credential').notEmpty().escape(), - async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant or Credential not found`) - } - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) + this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) - const resp = await openai.files.list() - const existingFiles = resp.data ?? [] + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) + const resp = await openai.files.list() + const existingFiles = resp.data ?? [] - if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { - ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) - } - - return res.json(retrievedAssistant) + if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { + ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) } - ) + + return res.json(retrievedAssistant) + }) // List available assistants - this.app.get('/api/v1/openai-assistants', query('credential').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant or Credential not found`) - } + this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { const credentialId = req.query.credential as string const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: credentialId @@ -947,11 +823,7 @@ export class App { }) // Update assistant - this.app.put('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -1059,11 +931,7 @@ export class App { }) // Delete assistant - this.app.delete('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -1118,11 +986,7 @@ export class App { // Configuration // ---------------------------------------- - this.app.get('/api/v1/flow-config/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Chatflow ${req.params.id} not found`) - } + this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -1181,11 +1045,7 @@ export class App { } ) - this.app.post('/api/v1/vector/internal-upsert/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Upsert ${req.params.id} not found`) - } + this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { await this.buildChatflow(req, res, undefined, true, true) }) @@ -1196,24 +1056,15 @@ export class App { // Send input message and get prediction result (External) this.app.post( '/api/v1/prediction/:id', - param('id').notEmpty().escape(), upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Prediction`) - } await this.buildChatflow(req, res, socketIO) } ) // Send input message and get prediction result (Internal) - this.app.post('/api/v1/internal-prediction/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Prediction`) - } + this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { await this.buildChatflow(req, res, socketIO, true) }) @@ -1308,31 +1159,19 @@ export class App { }) // Update api key - this.app.put('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Update API Key`) - } + this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await updateAPIKey(req.params.id, req.body.keyName) return addChatflowsCount(keys, res) }) // Delete new api key - this.app.delete('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Update API Key`) - } + this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await deleteAPIKey(req.params.id) return addChatflowsCount(keys, res) }) // Verify api key - this.app.get('/api/v1/verify/apikey/:apiKey', param('apikey').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing API Key`) - } + this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => { try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts new file mode 100644 index 00000000..a69cde21 --- /dev/null +++ b/packages/server/src/utils/XSS.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from 'express' +let stripJs = require('strip-js') + +export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { + req.url = stripJs(req.url) + for (let p in req.query) { + req.query[p] = stripJs(req.query[p]) + } + + next() +} From 68fbe0ea12c89632fa50fc1f58c80e352edc0dc8 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 7 Dec 2023 22:32:07 +0530 Subject: [PATCH 030/502] GPT Vision: Vision Chain Node update along with addition of chatid folder on the server side when saving messages. --- .../chains/VisionChain/OpenAIVisionChain.ts | 77 +++++++++++++++---- .../nodes/chains/VisionChain/VLLMChain.ts | 32 ++++---- packages/server/src/index.ts | 36 ++++++--- .../ui/src/views/chatmessage/ChatMessage.js | 9 ++- 4 files changed, 112 insertions(+), 42 deletions(-) diff --git a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts index 7745f05d..6d19235c 100644 --- a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts +++ b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts @@ -1,10 +1,8 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' -import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' -import { VLLMChain } from './VLLMChain' -import { BaseLanguageModel } from 'langchain/base_language' +import { getBaseClasses, getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils' +import { OpenAIVisionChainInput, VLLMChain } from './VLLMChain' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { formatResponse } from '../../outputparsers/OutputParserHelpers' -import { ChatOpenAI } from 'langchain/chat_models/openai' class OpenAIVisionChain_Chains implements INode { label: string @@ -18,6 +16,7 @@ class OpenAIVisionChain_Chains implements INode { description: string inputs: INodeParams[] outputs: INodeOutputsValue[] + credential: INodeParams constructor() { this.label = 'Open AI Vision Chain' @@ -26,14 +25,28 @@ class OpenAIVisionChain_Chains implements INode { this.type = 'OpenAIVisionChain' this.icon = 'chain.svg' this.category = 'Chains' - this.badge = 'EXPERIMENTAL' + this.badge = 'BETA' this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .' this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['openAIApi'] + } this.inputs = [ { - label: 'Language Model (Works only with Open AI [gpt-4-vision-preview])', - name: 'model', - type: 'BaseLanguageModel' + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'gpt-4-vision-preview', + name: 'gpt-4-vision-preview' + } + ], + default: 'gpt-4-vision-preview', + optional: true }, { label: 'Prompt', @@ -57,7 +70,33 @@ class OpenAIVisionChain_Chains implements INode { } ], default: 'low', - optional: false + optional: false, + additionalParams: true + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 0.9, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Max Tokens', + name: 'maxTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true }, { label: 'Chain Name', @@ -96,22 +135,26 @@ class OpenAIVisionChain_Chains implements INode { } async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const model = nodeData.inputs?.model as BaseLanguageModel const prompt = nodeData.inputs?.prompt const output = nodeData.outputs?.output as string const imageResolution = nodeData.inputs?.imageResolution const promptValues = prompt.promptValues as ICommonObject - if (!(model as any).openAIApiKey || (model as any).modelName !== 'gpt-4-vision-preview') { - throw new Error('Chain works with OpenAI Vision model only') - } - const openAIModel = model as ChatOpenAI - const fields = { - openAIApiKey: openAIModel.openAIApiKey, + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxTokens = nodeData.inputs?.maxTokens as string + const topP = nodeData.inputs?.topP as string + const fields: OpenAIVisionChainInput = { + openAIApiKey: openAIApiKey, imageResolution: imageResolution, verbose: process.env.DEBUG === 'true', imageUrls: options.uploads, - openAIModel: openAIModel + modelName: modelName } + if (temperature) fields.temperature = parseFloat(temperature) + if (maxTokens) fields.maxTokens = parseInt(maxTokens, 10) + if (topP) fields.topP = parseFloat(topP) if (output === this.name) { const chain = new VLLMChain({ ...fields, diff --git a/packages/components/nodes/chains/VisionChain/VLLMChain.ts b/packages/components/nodes/chains/VisionChain/VLLMChain.ts index f9b92e53..2849cf63 100644 --- a/packages/components/nodes/chains/VisionChain/VLLMChain.ts +++ b/packages/components/nodes/chains/VisionChain/VLLMChain.ts @@ -2,7 +2,6 @@ import { OpenAI as OpenAIClient, ClientOptions } from 'openai' import { BaseChain, ChainInputs } from 'langchain/chains' import { ChainValues } from 'langchain/schema' import { BasePromptTemplate, ChatPromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' -import { ChatOpenAI } from 'langchain/chat_models/openai' import path from 'path' import { getUserHome } from '../../../src/utils' import fs from 'fs' @@ -18,7 +17,10 @@ export interface OpenAIVisionChainInput extends ChainInputs { configuration?: ClientOptions imageUrls?: [] imageResolution?: string - openAIModel: ChatOpenAI + temperature?: number + modelName?: string + maxTokens?: number + topP?: number } /** @@ -30,12 +32,6 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { static lc_name() { return 'VLLMChain' } - - get lc_secrets(): { [key: string]: string } | undefined { - return { - openAIApiKey: 'OPENAI_API_KEY' - } - } prompt: BasePromptTemplate | undefined inputKey = 'input' @@ -44,10 +40,13 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { imageResolution: string = 'low' openAIApiKey?: string openAIOrganization?: string - openAIModel: ChatOpenAI clientConfig: ClientOptions client: OpenAIClient throwError: boolean + temperature?: number + modelName?: string + maxTokens?: number + topP?: number constructor(fields: OpenAIVisionChainInput) { super(fields) @@ -55,13 +54,16 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { this.imageResolution = fields?.imageResolution ?? 'low' this.openAIApiKey = fields?.openAIApiKey this.prompt = fields?.prompt + this.temperature = fields?.temperature + this.modelName = fields?.modelName + this.maxTokens = fields?.maxTokens + this.topP = fields?.topP this.imageUrls = fields?.imageUrls ?? [] if (!this.openAIApiKey) { throw new Error('OpenAI API key not found') } this.openAIOrganization = fields?.openAIOrganization - this.openAIModel = fields.openAIModel this.clientConfig = { ...fields?.configuration, @@ -76,12 +78,12 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { const userInput = values[this.inputKey] const vRequest: any = { - model: 'gpt-4-vision-preview', - temperature: this.openAIModel.temperature, - top_p: this.openAIModel.topP, + model: this.modelName, + temperature: this.temperature, + top_p: this.topP, messages: [] } - if (this.openAIModel.maxTokens) vRequest.max_tokens = this.openAIModel.maxTokens + if (this.maxTokens) vRequest.max_tokens = this.maxTokens else vRequest.max_tokens = 1024 const userRole: any = { role: 'user' } @@ -94,7 +96,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { this.imageUrls.forEach((imageUrl: any) => { let bf = imageUrl?.data if (imageUrl.type == 'stored-file') { - const filePath = path.join(getUserHome(), '.flowise', 'gptvision', imageUrl.data) + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', imageUrl.data, imageUrl.name) // as the image is stored in the server, read the file and convert it to base64 const contents = fs.readFileSync(filePath) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 83b018d5..84e76c6e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -972,12 +972,29 @@ export class App { } }) + function streamFileToUser(res: Response, filePath: string) { + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + } + // Download file from assistant this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) - const fileStream = fs.createReadStream(filePath) - fileStream.pipe(res) + streamFileToUser(res, filePath) + }) + + // stream uploaded image + this.app.get('/api/v1/get-upload-file/:id', async (req: Request, res: Response) => { + if (!req.params.id || !req.query.chatId) { + return res.status(500).send(`Invalid file path`) + } + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', req.query.chatId as string, req.params.id) + console.log(filePath) + if (!path.isAbsolute(filePath) || !fs.existsSync(filePath)) { + return res.status(500).send(`Invalid file path`) + } + streamFileToUser(res, filePath) }) // ---------------------------------------- @@ -1352,16 +1369,17 @@ export class App { ;(incomingInput.uploads as any[]).forEach((upload: any) => { if (upload.type === 'file') { const filename = upload.name - const filePath = path.join(getUserHome(), '.flowise', 'gptvision', filename) - if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'gptvision'))) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }) + const dir = path.join(getUserHome(), '.flowise', 'gptvision', chatId) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) } + const filePath = path.join(dir, filename) const splitDataURI = upload.data.split(',') - //const fname = splitDataURI.pop()?.split(':')[1] ?? '' const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, bf) - // don't need to store the file contents in chatmessage, just the filename - upload.data = filename //bf.toString('base64') + //TODO: check if file exists, what should we do if it exists? + fs.writeFileSync(filePath, bf) + // don't need to store the file contents in chatmessage, just the filename and chatId + upload.data = chatId upload.type = 'stored-file' } }) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 92c73699..d2ff51d8 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -418,7 +418,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) - if (message.fileUploads) obj.fileUploads = JSON.parse(message.fileUploads) + if (message.fileUploads) { + obj.fileUploads = JSON.parse(message.fileUploads) + obj.fileUploads.forEach((file) => { + if (file.type === 'stored-file') { + file.data = `${baseURL}/api/v1/get-upload-file/${file.name}?chatId=${chatId}` + } + }) + } return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) From 7578183ac25e74518e07f06a61b3eec8609ac2ed Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Dec 2023 18:46:03 +0000 Subject: [PATCH 031/502] add custom analytics --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 32 +- packages/components/package.json | 3 +- packages/components/src/handler.ts | 489 ++++++++++++++++++ packages/server/src/index.ts | 2 + 4 files changed, 522 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 7f2377bd..d4426394 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -8,6 +8,7 @@ import * as path from 'node:path' import fetch from 'node-fetch' import { flatten, uniqWith, isEqual } from 'lodash' import { zodToJsonSchema } from 'zod-to-json-schema' +import { AnalyticHandler } from '../../../src/handler' class OpenAIAssistant_Agents implements INode { label: string @@ -149,6 +150,11 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) + // Start analytics + const analyticHandlers = new AnalyticHandler(nodeData, options) + await analyticHandlers.init() + const parentIds = await analyticHandlers.onChainStart('OpenAIAssistant', input) + try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id @@ -171,7 +177,8 @@ class OpenAIAssistant_Agents implements INode { } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: options.chatId + chatId: options.chatId, + chatflowid: options.chatflowid }) let threadId = '' @@ -185,7 +192,7 @@ class OpenAIAssistant_Agents implements INode { threadId = thread.id } - // List all runs + // List all runs, in case existing thread is still running if (!isNewThread) { const promise = (threadId: string) => { return new Promise((resolve) => { @@ -221,6 +228,7 @@ class OpenAIAssistant_Agents implements INode { }) // Run assistant thread + const llmIds = await analyticHandlers.onLLMStart('ChatOpenAI', input, parentIds) const runThread = await openai.beta.threads.runs.create(threadId, { assistant_id: retrievedAssistant.id }) @@ -253,7 +261,15 @@ class OpenAIAssistant_Agents implements INode { for (let i = 0; i < actions.length; i += 1) { const tool = tools.find((tool: any) => tool.name === actions[i].tool) if (!tool) continue + + // Start tool analytics + const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) + const toolOutput = await tool.call(actions[i].toolInput) + + // End tool analytics + await analyticHandlers.onToolEnd(toolIds, toolOutput) + submitToolOutputs.push({ tool_call_id: actions[i].toolCallId, output: toolOutput @@ -302,7 +318,9 @@ class OpenAIAssistant_Agents implements INode { runThreadId = newRunThread.id state = await promise(threadId, newRunThread.id) } else { - throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + const errMsg = `Error processing thread: ${state}, Thread ID: ${threadId}` + await analyticHandlers.onChainError(parentIds, errMsg) + throw new Error(errMsg) } } @@ -387,11 +405,18 @@ class OpenAIAssistant_Agents implements INode { const bitmap = fsDefault.readFileSync(filePath) const base64String = Buffer.from(bitmap).toString('base64') + // TODO: Use a file path and retrieve image on the fly. Storing as base64 to localStorage and database will easily hit limits const imgHTML = `${fileObj.filename}
` returnVal += imgHTML } } + const imageRegex = /]*\/>/g + let llmOutput = returnVal.replace(imageRegex, '') + llmOutput = llmOutput.replace('
', '') + await analyticHandlers.onLLMEnd(llmIds, llmOutput) + await analyticHandlers.onChainEnd(parentIds, messageData, true) + return { text: returnVal, usedTools, @@ -399,6 +424,7 @@ class OpenAIAssistant_Agents implements INode { assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { + await analyticHandlers.onChainError(parentIds, error, true) throw new Error(error) } } diff --git a/packages/components/package.json b/packages/components/package.json index dd87754d..a775e630 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -51,8 +51,9 @@ "husky": "^8.0.3", "ioredis": "^5.3.2", "langchain": "^0.0.196", + "langfuse": "^1.2.0", "langfuse-langchain": "^1.0.31", - "langsmith": "^0.0.32", + "langsmith": "^0.0.49", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", "mammoth": "^1.5.1", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 456cf39c..ae5a9de0 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -8,6 +8,10 @@ import { LLMonitorHandler } from 'langchain/callbacks/handlers/llmonitor' import { getCredentialData, getCredentialParam } from './utils' import { ICommonObject, INodeData } from './Interface' import CallbackHandler from 'langfuse-langchain' +import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith' +import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' // or "langfuse-node" +import monitor from 'llmonitor' +import { v4 as uuidv4 } from 'uuid' interface AgentRun extends Run { actions: AgentAction[] @@ -273,3 +277,488 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO throw new Error(e) } } + +export class AnalyticHandler { + nodeData: INodeData + options: ICommonObject = {} + handlers: ICommonObject = {} + + constructor(nodeData: INodeData, options: ICommonObject) { + this.options = options + this.nodeData = nodeData + this.init() + } + + async init() { + try { + if (!this.options.analytic) return + + const analytic = JSON.parse(this.options.analytic) + + for (const provider in analytic) { + const providerStatus = analytic[provider].status as boolean + + if (providerStatus) { + const credentialId = analytic[provider].credentialId as string + const credentialData = await getCredentialData(credentialId ?? '', this.options) + if (provider === 'langSmith') { + const langSmithProject = analytic[provider].projectName as string + const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData) + const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData) + + const client = new LangsmithClient({ + apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com', + apiKey: langSmithApiKey + }) + + this.handlers['langSmith'] = { client, langSmithProject } + } else if (provider === 'langFuse') { + const release = analytic[provider].release as string + const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData) + const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData) + const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData) + + const langfuse = new Langfuse({ + secretKey: langFuseSecretKey, + publicKey: langFusePublicKey, + baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com', + release + }) + this.handlers['langFuse'] = { client: langfuse } + } else if (provider === 'llmonitor') { + const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, this.nodeData) + const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, this.nodeData) + + monitor.init({ + appId: llmonitorAppId, + apiUrl: llmonitorEndpoint + }) + + this.handlers['llmonitor'] = { client: monitor } + } + } + } + } catch (e) { + throw new Error(e) + } + } + + async onChainStart(name: string, input: string, parentIds?: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + if (!parentIds || !Object.keys(parentIds).length) { + const parentRunConfig: RunTreeConfig = { + name, + run_type: 'chain', + inputs: { + text: input + }, + serialized: {}, + project_name: this.handlers['langSmith'].langSmithProject, + client: this.handlers['langSmith'].client + } + const parentRun = new RunTree(parentRunConfig) + await parentRun.postRun() + this.handlers['langSmith'].chainRun = { [parentRun.id]: parentRun } + returnIds['langSmith'].chainRun = parentRun.id + } else { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childChainRun = await parentRun.createChild({ + name, + run_type: 'chain', + inputs: { + text: input + } + }) + await childChainRun.postRun() + this.handlers['langSmith'].chainRun = { [childChainRun.id]: childChainRun } + returnIds['langSmith'].chainRun = childChainRun.id + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + let langfuseTraceClient: LangfuseTraceClient + + if (!parentIds || !Object.keys(parentIds).length) { + const langfuse: Langfuse = this.handlers['langFuse'].client + langfuseTraceClient = langfuse.trace({ + name, + userId: this.options.chatId, + metadata: { tags: ['openai-assistant'] } + }) + } else { + langfuseTraceClient = this.handlers['langFuse'].trace[parentIds['langFuse']] + } + + if (langfuseTraceClient) { + const span = langfuseTraceClient.span({ + name, + input: { + text: input + } + }) + this.handlers['langFuse'].trace = { [langfuseTraceClient.id]: langfuseTraceClient } + this.handlers['langFuse'].span = { [span.id]: span } + returnIds['langFuse'].trace = langfuseTraceClient.id + returnIds['langFuse'].span = span.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + + if (monitor) { + const runId = uuidv4() + await monitor.trackEvent('chain', 'start', { + runId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].chainEvent = { [runId]: runId } + returnIds['llmonitor'].chainEvent = runId + } + } + + return returnIds + } + + async onChainEnd(returnIds: ICommonObject, output: string | object, shutdown = false) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun] + if (chainRun) { + await chainRun.end({ + outputs: { + output + } + }) + await chainRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span] + if (span) { + span.end({ + output + }) + if (shutdown) { + const langfuse: Langfuse = this.handlers['langFuse'].client + await langfuse.shutdownAsync() + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const chainEventId = returnIds['llmonitor'].chainEvent + const monitor = this.handlers['llmonitor'].client + + if (monitor && chainEventId) { + await monitor.trackEvent('chain', 'end', { + runId: chainEventId, + output + }) + } + } + } + + async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun] + if (chainRun) { + await chainRun.end({ + error: { + error + } + }) + await chainRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span] + if (span) { + span.end({ + output: { + error + } + }) + if (shutdown) { + const langfuse: Langfuse = this.handlers['langFuse'].client + await langfuse.shutdownAsync() + } + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const chainEventId = returnIds['llmonitor'].chainEvent + const monitor = this.handlers['llmonitor'].client + + if (monitor && chainEventId) { + await monitor.trackEvent('chain', 'end', { + runId: chainEventId, + output: error + }) + } + } + } + + async onLLMStart(name: string, input: string, parentIds: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childLLMRun = await parentRun.createChild({ + name, + run_type: 'llm', + inputs: { + prompts: [input] + } + }) + await childLLMRun.postRun() + this.handlers['langSmith'].llmRun = { [childLLMRun.id]: childLLMRun } + returnIds['langSmith'].llmRun = childLLMRun.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] + if (trace) { + const generation = trace.generation({ + name, + prompt: input + }) + this.handlers['langFuse'].generation = { [generation.id]: generation } + returnIds['langFuse'].generation = generation.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent] + + if (monitor && chainEventId) { + const runId = uuidv4() + await monitor.trackEvent('llm', 'start', { + runId, + parentRunId: chainEventId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].llmEvent = { [runId]: runId } + returnIds['llmonitor'].llmEvent = runId + } + } + + return returnIds + } + + async onLLMEnd(returnIds: ICommonObject, output: string) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun] + if (llmRun) { + await llmRun.end({ + outputs: { + generations: [output] + } + }) + await llmRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] + if (generation) { + generation.end({ + completion: output + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && llmEventId) { + await monitor.trackEvent('llm', 'end', { + runId: llmEventId, + output + }) + } + } + } + + async onLLMError(returnIds: ICommonObject, error: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun] + if (llmRun) { + await llmRun.end({ + error: { + error + } + }) + await llmRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] + if (generation) { + generation.end({ + completion: error + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && llmEventId) { + await monitor.trackEvent('llm', 'end', { + runId: llmEventId, + output: error + }) + } + } + } + + async onToolStart(name: string, input: string | object, parentIds: ICommonObject) { + const returnIds: ICommonObject = { + langSmith: {}, + langFuse: {}, + llmonitor: {} + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun] + if (parentRun) { + const childToolRun = await parentRun.createChild({ + name, + run_type: 'tool', + inputs: { + input + } + }) + await childToolRun.postRun() + this.handlers['langSmith'].toolRun = { [childToolRun.id]: childToolRun } + returnIds['langSmith'].toolRun = childToolRun.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] + if (trace) { + const toolSpan = trace.span({ + name, + input + }) + this.handlers['langFuse'].toolSpan = { [toolSpan.id]: toolSpan } + returnIds['langFuse'].toolSpan = toolSpan.id + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const monitor = this.handlers['llmonitor'].client + const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent] + + if (monitor && chainEventId) { + const runId = uuidv4() + await monitor.trackEvent('tool', 'start', { + runId, + parentRunId: chainEventId, + name, + userId: this.options.chatId, + input + }) + this.handlers['llmonitor'].toolEvent = { [runId]: runId } + returnIds['llmonitor'].toolEvent = runId + } + } + + return returnIds + } + + async onToolEnd(returnIds: ICommonObject, output: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun] + if (toolRun) { + await toolRun.end({ + outputs: { + output + } + }) + await toolRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan] + if (toolSpan) { + toolSpan.end({ + output + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const toolEventId: string = this.handlers['llmonitor'].toolEvent[returnIds['llmonitor'].toolEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && toolEventId) { + await monitor.trackEvent('tool', 'end', { + runId: toolEventId, + output + }) + } + } + } + + async onToolError(returnIds: ICommonObject, error: string | object) { + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) { + const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun] + if (toolRun) { + await toolRun.end({ + error: { + error + } + }) + await toolRun.patchRun() + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { + const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan] + if (toolSpan) { + toolSpan.end({ + output: error + }) + } + } + + if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) { + const toolEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].toolEvent] + const monitor = this.handlers['llmonitor'].client + + if (monitor && toolEventId) { + await monitor.trackEvent('tool', 'end', { + runId: toolEventId, + output: error + }) + } + } + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d87d2c0a..61e55159 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1470,6 +1470,7 @@ export class App { let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatflowid, chatHistory, socketIO, socketIOClientId: incomingInput.socketIOClientId, @@ -1480,6 +1481,7 @@ export class App { chatId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatflowid, chatHistory, logger, appDataSource: this.AppDataSource, From c9a7ee2ad4c1d23471b926babf848e18c22d7f1b Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Dec 2023 23:17:27 +0000 Subject: [PATCH 032/502] update hyde retriever --- .../retrievers/HydeRetriever/HydeRetriever.ts | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts index 9ec7ada0..10d9a6e7 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts +++ b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts @@ -18,7 +18,7 @@ class HydeRetriever_Retrievers implements INode { constructor() { this.label = 'Hyde Retriever' this.name = 'HydeRetriever' - this.version = 1.0 + this.version = 2.0 this.type = 'HydeRetriever' this.icon = 'hyderetriever.svg' this.category = 'Retrievers' @@ -36,41 +36,66 @@ class HydeRetriever_Retrievers implements INode { type: 'VectorStore' }, { - label: 'Prompt Key', + label: 'Select Defined Prompt', name: 'promptKey', + description: 'Select a pre-defined prompt', type: 'options', options: [ { label: 'websearch', - name: 'websearch' + name: 'websearch', + description: `Please write a passage to answer the question +Question: {question} +Passage:` }, { label: 'scifact', - name: 'scifact' + name: 'scifact', + description: `Please write a scientific paper passage to support/refute the claim +Claim: {question} +Passage:` }, { label: 'arguana', - name: 'arguana' + name: 'arguana', + description: `Please write a counter argument for the passage +Passage: {question} +Counter Argument:` }, { label: 'trec-covid', - name: 'trec-covid' + name: 'trec-covid', + description: `Please write a scientific paper passage to answer the question +Question: {question} +Passage:` }, { label: 'fiqa', - name: 'fiqa' + name: 'fiqa', + description: `Please write a financial article passage to answer the question +Question: {question} +Passage:` }, { label: 'dbpedia-entity', - name: 'dbpedia-entity' + name: 'dbpedia-entity', + description: `Please write a passage to answer the question. +Question: {question} +Passage:` }, { label: 'trec-news', - name: 'trec-news' + name: 'trec-news', + description: `Please write a news passage about the topic. +Topic: {question} +Passage:` }, { label: 'mr-tydi', - name: 'mr-tydi' + name: 'mr-tydi', + description: `Please write a passage in Swahili/Korean/Japanese/Bengali to answer the question in detail. +Question: {question} +Passage:` } ], default: 'websearch' @@ -78,7 +103,7 @@ class HydeRetriever_Retrievers implements INode { { label: 'Custom Prompt', name: 'customPrompt', - description: 'If custom prompt is used, this will override Prompt Key', + description: 'If custom prompt is used, this will override Defined Prompt', placeholder: 'Please write a passage to answer the question\nQuestion: {question}\nPassage:', type: 'string', rows: 4, From 32575828cdf49b976480912b50d9391477e7bed1 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 8 Dec 2023 17:21:53 +0530 Subject: [PATCH 033/502] GPT Vision: Converting vision into Multi Modal. Base Changes. --- .../nodes/multimodal/OpenAI/AudioWhisper.ts | 61 ++++++++++++++++++ .../OpenAI}/OpenAIVisionChain.ts | 41 +++++++----- .../OpenAI}/VLLMChain.ts | 0 .../nodes/multimodal/OpenAI/audio.svg | 1 + .../OpenAI}/chain.svg | 0 .../nodes/multimodal/OpenAI/list.png | Bin 0 -> 5002 bytes packages/server/src/index.ts | 16 +++-- packages/ui/src/assets/images/wave-sound.jpg | Bin 0 -> 330686 bytes .../ui/src/views/chatmessage/ChatMessage.js | 55 +++++++++------- 9 files changed, 129 insertions(+), 45 deletions(-) create mode 100644 packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts rename packages/components/nodes/{chains/VisionChain => multimodal/OpenAI}/OpenAIVisionChain.ts (94%) rename packages/components/nodes/{chains/VisionChain => multimodal/OpenAI}/VLLMChain.ts (100%) create mode 100644 packages/components/nodes/multimodal/OpenAI/audio.svg rename packages/components/nodes/{chains/VisionChain => multimodal/OpenAI}/chain.svg (100%) create mode 100644 packages/components/nodes/multimodal/OpenAI/list.png create mode 100644 packages/ui/src/assets/images/wave-sound.jpg diff --git a/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts b/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts new file mode 100644 index 00000000..b308a7c5 --- /dev/null +++ b/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts @@ -0,0 +1,61 @@ +import { INode, INodeData, INodeParams } from '../../../src' + +class OpenAIAudioWhisper implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'Open AI Whisper' + this.name = 'openAIAudioWhisper' + this.version = 1.0 + this.type = 'OpenAIWhisper' + this.description = 'Speech to text using OpenAI Whisper API' + this.icon = 'audio.svg' + this.category = 'MultiModal' + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'Purpose', + name: 'purpose', + type: 'options', + options: [ + { + label: 'transcription', + name: 'transcription' + }, + { + label: 'translation', + name: 'translation' + } + ] + }, + { + label: 'Accepted Upload Types', + name: 'allowedUploadTypes', + type: 'string', + default: 'audio/mpeg;audio/x-wav;audio/mp4', + hidden: true + }, + { + label: 'Maximum Upload Size (MB)', + name: 'maxUploadSize', + type: 'number', + default: '5', + hidden: true + } + ] + } + + async init(nodeData: INodeData): Promise { + return {} + } +} + +module.exports = { nodeClass: OpenAIAudioWhisper } diff --git a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts b/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts similarity index 94% rename from packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts rename to packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts index 6d19235c..4151b4b0 100644 --- a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts +++ b/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts @@ -19,14 +19,14 @@ class OpenAIVisionChain_Chains implements INode { credential: INodeParams constructor() { - this.label = 'Open AI Vision Chain' - this.name = 'openAIVisionChain' + this.label = 'Open AI MultiModal Chain' + this.name = 'openAIMultiModalChain' this.version = 1.0 - this.type = 'OpenAIVisionChain' + this.type = 'OpenAIMultiModalChain' this.icon = 'chain.svg' this.category = 'Chains' this.badge = 'BETA' - this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .' + this.description = 'Chain to query against Image and Audio Input.' this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] this.credential = { label: 'Connect Credential', @@ -36,16 +36,9 @@ class OpenAIVisionChain_Chains implements INode { } this.inputs = [ { - label: 'Model Name', - name: 'modelName', - type: 'options', - options: [ - { - label: 'gpt-4-vision-preview', - name: 'gpt-4-vision-preview' - } - ], - default: 'gpt-4-vision-preview', + label: 'Audio Input', + name: 'audioInput', + type: 'OpenAIWhisper', optional: true }, { @@ -54,6 +47,22 @@ class OpenAIVisionChain_Chains implements INode { type: 'BasePromptTemplate', optional: true }, + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'gpt-4-vision-preview', + name: 'gpt-4-vision-preview' + }, + { + label: 'whisper-1', + name: 'whisper-1' + } + ], + default: 'gpt-4-vision-preview' + }, { label: 'Image Resolution', description: 'This parameter controls the resolution in which the model views the image.', @@ -122,8 +131,8 @@ class OpenAIVisionChain_Chains implements INode { ] this.outputs = [ { - label: 'Open AI Vision Chain', - name: 'openAIVisionChain', + label: 'Open AI MultiModal Chain', + name: 'OpenAIMultiModalChain', baseClasses: [this.type, ...getBaseClasses(VLLMChain)] }, { diff --git a/packages/components/nodes/chains/VisionChain/VLLMChain.ts b/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts similarity index 100% rename from packages/components/nodes/chains/VisionChain/VLLMChain.ts rename to packages/components/nodes/multimodal/OpenAI/VLLMChain.ts diff --git a/packages/components/nodes/multimodal/OpenAI/audio.svg b/packages/components/nodes/multimodal/OpenAI/audio.svg new file mode 100644 index 00000000..3bcbbdcd --- /dev/null +++ b/packages/components/nodes/multimodal/OpenAI/audio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chains/VisionChain/chain.svg b/packages/components/nodes/multimodal/OpenAI/chain.svg similarity index 100% rename from packages/components/nodes/chains/VisionChain/chain.svg rename to packages/components/nodes/multimodal/OpenAI/chain.svg diff --git a/packages/components/nodes/multimodal/OpenAI/list.png b/packages/components/nodes/multimodal/OpenAI/list.png new file mode 100644 index 0000000000000000000000000000000000000000..acb4e5d68f200207a97e10ee63125eb4e040fcec GIT binary patch literal 5002 zcmd5<2{_c-`~QwX$Tk=|b*-5x5|dmh$v)YQu`66;%_w`J8`(7^WhzOw8AjF;6AD?9 zt+9+HvPP0U!u+R3|J(Ds&vWnd|3CNspZ}R>&ikG3`=0lG&pDs-dCyS$siOe5fsVco z00IF3i1q=fgTOU_Z4V2C2g<|4!_ULb&Br4I<$*xBAzVB>T!K(pA!(=}zo@i=lBhU* zpP-Ki5Tzx^00w=_^+(&o!obAL2>zzk;s!uq5CaSAUPcB65Ysmc5CfQz3Bt?+ z6;L!!?b^fti?XSOe@YoIpAzDf8RjCkr1YF$R<}gt5lu-cX_uSXG%tKVd#6YF_7?I} z%-y#XfE`TB2LgrwCxJT=0HBDfUIXvU1mzr=$?Av zr^3Z4(hY_R&KYZY3vo43i7l^BK>3|f0$736v|mlw7O&pcHT~^%iViW1(_*R9ap#Cq z4sZX2A)EEDY|Yb2s@Zn#6V48=WGjfsPNJZxUh^#8*~zX+FF&?3!)HeycQp?`a5JB9 z1=jrbi>(rtcUwXNG(g{w7lv%8Idl(d>-C6I{Arme*ec-ze#g~UsLrZ}ck1SF{E4QC zp0kGqBEpaZMkCmR)#mLy&}gj)|P46p`XO}6aW zGW)UsF8@9VbS;CA&|92K*tk>U#vlDtt`Q|cvh8xA{8wb3ccxkMB^zmb`CzWhkj?9_ zhIOZxxV;aV&pf5#LIvtI2s}+2b|0irp(B(2Lz5&_TdVWbq3sjbiRvsad#<;mo~YuB z{DW<#h_Huw6;Ne4_s0%J%cfmz661QCR`=I0lDZz^w|(K}EBs1eu}e$>t=eM`M4%M~ zsaUrtfeP2fw1P{Yy|YIw+1g-eeb|Po>Te;NDl@MfJHJc>BQRd?p6*5NnjEQ+f6h@J z+FF5~Ah(oV8KaBQL-Lk6ef5>eL9K6}eL32-0PEFFMa#5*ji!r;@+zBjM`nupINWS* zbWpd3U@dNFhg10^L>CO*lOFbABJS%6Mabgcy~US`T+2nF6!1r9O zIi4eU+s&)iMS6ry-PU*!IbOrr`F;PGdECv4ZJuWoPq{AMwGmR5Ll-$3qL-C|-Y_(d z(m{29&_FuO453_=~!60>4!hGIs8 z_KwL55ow5T74I4{vBs+r89{Dyb$D`nA{7JlxL{KZo56-DI{GU6IgaWJNex9P4@kNE zp=#GPj!EAa=x#14;BEqs7GBvaj|cjUqX zBPn#y|IP9GHq-OUF}+oDRDl26k&&J%X57e8aO!AS9fVVXt3EBXE|P_E7fvz8wi#iy znU7=(myB20hD+X^w1)*NWqEyF^x_SEt<_M)zG%XUPmT&jC3`+vGS`XUIkOZLH?@&7mpJ^gV`^!6{imGa>de!~~09Lzo8V=Mhnz!sShqtV&66&M|tRaNE`R2R> z=ee-tFy9m@44xpI=x>U;oIZ0rxH8#Z)-OV5p))6;ZpHC%_J?J?8s%fZ%5!AtR=9+v zC<>&-B|53Cpp4JSb!#Pf+QfWovCT0t>2?u12vHHU@hZPuT6KY^Z{cz*caq<3O!eYM za5cjX#F9LXCGQ!A1MNjBB$eRmXz%l+57q5M20Ml%aLbbiEZIlPM^=WXJ_%LiuIDbO z_`x@hu5ihWL`Wo1dO~{waZg=$Jrz$3b{@xt0C7s5h6{!xK4Ul=-w>bWK%UxE-@gpO z84fNyFymyEjjqB^25#VCBef-+)tc8l_6I~UJpB{avHbseAcDjme1|`~RA-Ju#bt!! z>-O zEVc!P7-lQdrs4K<YO~`ls1VK8muN?4Ax*CRuRwtGbPCr;sh5 z;iddhrlx*XQ2x1c!D-IsIn!{ph}rs9H~M4rg^x)U0?SW13q^BxA9pqXd!t4Q-{%4T zMs-_$8@EcUuGS&fcL7I2x%|ryKwtieSz{jmiGS11>6r}EvHm8zK*LS7+;^aZmM35&Em9-cW}7LYuFw9IR_zuI@%uBvgp07vO)n);Z2?L>kb+Ho@#Dvq z90J(f=)H?Ich4k?doXjHBo_s>yq{uBJ680=NN@=!@Mp5d1LcmD0;X+>8>4jX!}7!Y-Y_Wh^Nw|YP;6&S^phL7f@{-|t}c2u;55z1;L<{mumfh9{1 zmc641VU7)kszQH-a&;}`RUcRzE2qAL$sib!!IoC`Tb0@Q@tB(-(cAVlEZIqGeYWY<`&LB*E1wOK z`l&zxe32*F9`uMkI$#p>q! zk_YSJjxBg!stPTk0;aD*J&W3glQ&apAqQ4DHmigFKAGu-T~qPv#%xT6d$oH4WgiD$DG=4Uw@JRY&uH` zk8>28(2lOzDXVlW`$p{S^ekyMgtl zSEOF(Fm$xI?kO`W)2V6wfiQbx-epLs2TlIvpLa%EuBacbdu>JKD5x_R{VHKPRr3DX zy3cIW`;#zB;g4!KTm|*|`YcuPNgl7Cz%Y3d61;nu z81+3q`|eG%l-1{;0;?8F3C5Hii_MH0ikck`F=FTwOUPjl8@LB(4wMH}9rdhn?V?N! z5SQGST`zJSb<^N}?kz?%%vcbjYcq}y+yl4Y>10&E>3XKRRyWbLGRZVk+6d6zsqe!V z>DrcTnkj7rpt { if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) { logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) allowUploads = true + const allowance: any = {} node.data.inputParams.map((param: any) => { if (param.name === 'allowedUploadTypes') { - allowedTypes = param.default.split(';') + allowance.allowedTypes = param.default.split(';') } if (param.name === 'maxUploadSize') { - maxUploadSize = parseInt(param.default ? param.default : '0') + allowance.maxUploadSize = parseInt(param.default ? param.default : '0') } }) + if (allowance.allowedTypes && allowance.maxUploadSize) { + allowances.push(allowance) + } } }) return { allowUploads, - allowedTypes, - maxUploadSize + allowed: allowances } } diff --git a/packages/ui/src/assets/images/wave-sound.jpg b/packages/ui/src/assets/images/wave-sound.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9f56d67d968643d91c1dce1689a84711d977220d GIT binary patch literal 330686 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

m2M)*TRE&r3t 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 literal 0 HcmV?d00001 diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index d2ff51d8..79a9b6e0 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -8,6 +8,7 @@ import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import axios from 'axios' +import audioUploadSVG from 'assets/images/wave-sound.jpg' import { Box, @@ -85,23 +86,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { e.preventDefault() } const isFileAllowedForUpload = (file) => { - // check if file type is allowed - if (getAllowChatFlowUploads.data?.allowedTypes?.length > 0) { - const allowedFileTypes = getAllowChatFlowUploads.data?.allowedTypes - if (!allowedFileTypes.includes(file.type)) { - alert(`File ${file.name} is not allowed.\nAllowed file types are ${allowedFileTypes.join(', ')}.`) - return false - } - } - // check if file size is allowed - if (getAllowChatFlowUploads.data?.maxUploadSize > 0) { + const constraints = getAllowChatFlowUploads.data + let acceptFile = false + if (constraints.allowUploads) { + const fileType = file.type const sizeInMB = file.size / 1024 / 1024 - if (sizeInMB > getAllowChatFlowUploads.data?.maxUploadSize) { - alert(`File ${file.name} is too large.\nMaximum allowed size is ${getAllowChatFlowUploads.data?.maxUploadSize} MB.`) - return false - } + constraints.allowed.map((allowed) => { + if (allowed.allowedTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { + acceptFile = true + } + }) } - return true + if (!acceptFile) { + alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`) + } + return acceptFile } const handleDrop = async (e) => { if (!isChatFlowAvailableForUploads) { @@ -124,9 +123,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { return } const { result } = evt.target + let previewUrl + if (file.type.startsWith('audio/')) { + previewUrl = audioUploadSVG + } else if (file.type.startsWith('image/')) { + previewUrl = URL.createObjectURL(file) + } resolve({ data: result, - preview: URL.createObjectURL(file), + preview: previewUrl, type: 'file', name: name, mime: file.type @@ -240,7 +245,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } const previewStyle = { - width: '64px', + width: '128px', height: '64px', objectFit: 'cover' // This makes the image cover the area, cropping it if necessary } @@ -514,11 +519,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { onDrop={handleDrop} className={`file-drop-field`} > - {isDragOver && ( + {isDragOver && getAllowChatFlowUploads.data?.allowUploads && ( Drop here to upload - {getAllowChatFlowUploads.data?.allowedTypes?.join(', ')} - Max Allowed Size: {getAllowChatFlowUploads.data?.maxUploadSize} MB + {getAllowChatFlowUploads.data.allowed.map((allowed) => { + return ( + <> + {allowed.allowedTypes?.join(', ')} + Max Allowed Size: {allowed.maxUploadSize} MB + + ) + })} )}

@@ -727,7 +738,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {previews.map((item, index) => ( - + { alt={`preview ${index}`} style={previewStyle} /> - + - {credentialId && ( - - - - Model - - - - - - Usecase - - - - - - Language - - - - - - - - )} + {availablePrompNameList && availablePrompNameList.length == 0 && ( diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 103af6b4..892a6273 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -238,7 +238,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA onClick={() => onShowPromptHubButtonClicked()} endIcon={} > - Langsmith Prompt Hub + Langchain Hub Date: Sat, 9 Dec 2023 14:12:30 +0000 Subject: [PATCH 041/502] add sanitize html types --- packages/server/package.json | 1 + packages/server/src/utils/XSS.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 013e6007..1eeb43f1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -72,6 +72,7 @@ "@types/cors": "^2.8.12", "@types/crypto-js": "^4.1.1", "@types/multer": "^1.4.7", + "@types/sanitize-html": "^2.9.5", "concurrently": "^7.1.0", "nodemon": "^2.0.15", "oclif": "^3", diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index 329c2ed2..3e96e6c8 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,12 +1,12 @@ import { Request, Response, NextFunction } from 'express' -const sanitizeHtml = require('sanitize-html') +import sanitizeHtml from 'sanitize-html' export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { // decoding is necessary as the url is encoded by the browser const decodedURI = decodeURI(req.url) req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = sanitizeHtml(req.query[p]) + req.query[p] = sanitizeHtml(req.query[p] as string) } next() From c26e37a923b39a07ac81b2da23409b8efbc8c85c Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 14:44:01 +0000 Subject: [PATCH 042/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.3=20relea?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 2b1f49e9..7a739978 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.2", + "version": "1.4.3", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From bac91eed00bd6a97567c245798818f93b681a152 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 14:44:53 +0000 Subject: [PATCH 043/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.5=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1993c7e5..804c3c96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.4", + "version": "1.4.5", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index a7315999..ab1f6149 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.4", + "version": "1.4.5", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 1b308a8b54d02c37efb20dd2071a39be0e704216 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 9 Dec 2023 22:07:16 +0530 Subject: [PATCH 044/502] making the chain multi-modal. now we accept audio and image uploads and can run inference --- .../nodes/multimodal/OpenAI/AudioWhisper.ts | 13 ++-- .../multimodal/OpenAI/OpenAIVisionChain.ts | 6 +- .../nodes/multimodal/OpenAI/VLLMChain.ts | 53 ++++++++++++++-- .../ui/src/views/chatmessage/ChatMessage.js | 62 +++++++++++-------- 4 files changed, 96 insertions(+), 38 deletions(-) diff --git a/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts b/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts index b308a7c5..aa2c71e1 100644 --- a/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts +++ b/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts @@ -7,6 +7,7 @@ class OpenAIAudioWhisper implements INode { description: string type: string icon: string + badge: string category: string baseClasses: string[] inputs: INodeParams[] @@ -18,6 +19,7 @@ class OpenAIAudioWhisper implements INode { this.type = 'OpenAIWhisper' this.description = 'Speech to text using OpenAI Whisper API' this.icon = 'audio.svg' + this.badge = 'BETA' this.category = 'MultiModal' this.baseClasses = [this.type] this.inputs = [ @@ -27,14 +29,15 @@ class OpenAIAudioWhisper implements INode { type: 'options', options: [ { - label: 'transcription', + label: 'Transcription', name: 'transcription' }, { - label: 'translation', + label: 'Translation', name: 'translation' } - ] + ], + default: 'transcription' }, { label: 'Accepted Upload Types', @@ -54,7 +57,9 @@ class OpenAIAudioWhisper implements INode { } async init(nodeData: INodeData): Promise { - return {} + const purpose = nodeData.inputs?.purpose as string + + return { purpose } } } diff --git a/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts b/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts index 4151b4b0..dcaa96e2 100644 --- a/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts +++ b/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts @@ -132,7 +132,7 @@ class OpenAIVisionChain_Chains implements INode { this.outputs = [ { label: 'Open AI MultiModal Chain', - name: 'OpenAIMultiModalChain', + name: 'openAIMultiModalChain', baseClasses: [this.type, ...getBaseClasses(VLLMChain)] }, { @@ -154,6 +154,8 @@ class OpenAIVisionChain_Chains implements INode { const modelName = nodeData.inputs?.modelName as string const maxTokens = nodeData.inputs?.maxTokens as string const topP = nodeData.inputs?.topP as string + const whisperConfig = nodeData.inputs?.audioInput + const fields: OpenAIVisionChainInput = { openAIApiKey: openAIApiKey, imageResolution: imageResolution, @@ -164,6 +166,8 @@ class OpenAIVisionChain_Chains implements INode { if (temperature) fields.temperature = parseFloat(temperature) if (maxTokens) fields.maxTokens = parseInt(maxTokens, 10) if (topP) fields.topP = parseFloat(topP) + if (whisperConfig) fields.whisperConfig = whisperConfig + if (output === this.name) { const chain = new VLLMChain({ ...fields, diff --git a/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts b/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts index 2849cf63..dd44ebb5 100644 --- a/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts +++ b/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts @@ -21,6 +21,7 @@ export interface OpenAIVisionChainInput extends ChainInputs { modelName?: string maxTokens?: number topP?: number + whisperConfig?: any } /** @@ -48,6 +49,8 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { maxTokens?: number topP?: number + whisperConfig?: any + constructor(fields: OpenAIVisionChainInput) { super(fields) this.throwError = fields?.throwError ?? false @@ -59,6 +62,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { this.maxTokens = fields?.maxTokens this.topP = fields?.topP this.imageUrls = fields?.imageUrls ?? [] + this.whisperConfig = fields?.whisperConfig ?? {} if (!this.openAIApiKey) { throw new Error('OpenAI API key not found') } @@ -92,15 +96,44 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { type: 'text', text: userInput }) + if (this.whisperConfig && this.imageUrls && this.imageUrls.length > 0) { + const audioUploads = this.getAudioUploads(this.imageUrls) + for (const url of audioUploads) { + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', url.data, url.name) + + // as the image is stored in the server, read the file and convert it to base64 + const audio_file = fs.createReadStream(filePath) + if (this.whisperConfig.purpose === 'transcription') { + const transcription = await this.client.audio.transcriptions.create({ + file: audio_file, + model: 'whisper-1' + }) + userRole.content.push({ + type: 'text', + text: transcription.text + }) + } else if (this.whisperConfig.purpose === 'translation') { + const translation = await this.client.audio.translations.create({ + file: audio_file, + model: 'whisper-1' + }) + userRole.content.push({ + type: 'text', + text: translation.text + }) + } + } + } if (this.imageUrls && this.imageUrls.length > 0) { - this.imageUrls.forEach((imageUrl: any) => { - let bf = imageUrl?.data - if (imageUrl.type == 'stored-file') { - const filePath = path.join(getUserHome(), '.flowise', 'gptvision', imageUrl.data, imageUrl.name) + const imageUploads = this.getImageUploads(this.imageUrls) + for (const url of imageUploads) { + let bf = url.data + if (url.type == 'stored-file') { + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', url.data, url.name) // as the image is stored in the server, read the file and convert it to base64 const contents = fs.readFileSync(filePath) - bf = 'data:' + imageUrl.mime + ';base64,' + contents.toString('base64') + bf = 'data:' + url.mime + ';base64,' + contents.toString('base64') } userRole.content.push({ type: 'image_url', @@ -109,7 +142,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { detail: this.imageResolution } }) - }) + } } vRequest.messages.push(userRole) if (this.prompt && this.prompt instanceof ChatPromptTemplate) { @@ -146,6 +179,14 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { } } + getAudioUploads = (urls: any[]) => { + return urls.filter((url: any) => url.mime.startsWith('audio/')) + } + + getImageUploads = (urls: any[]) => { + return urls.filter((url: any) => url.mime.startsWith('image/')) + } + _chainType() { return 'vision_chain' } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 79a9b6e0..37b45bd5 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -14,7 +14,6 @@ import { Box, Button, Card, - CardActions, CardMedia, Chip, CircularProgress, @@ -48,7 +47,6 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' -import DeleteIcon from '@mui/icons-material/Delete' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -628,15 +626,25 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.fileUploads && message.fileUploads.map((item, index) => { return ( - - - + <> + {item.mime.startsWith('image/') ? ( + + + + ) : ( + // eslint-disable-next-line jsx-a11y/media-has-caption + + )} + ) })} {message.sourceDocuments && ( @@ -738,23 +746,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {previews.map((item, index) => ( - - - - + ) + } + }) + 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 00000000..ccec36d3 --- /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 046/502] 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 12dd72f1..343acafd 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 2b4e6436..39baaf8e 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 f51c1d5b7a53cd877b000786d221298ed1766187 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Dec 2023 20:35:30 +0000 Subject: [PATCH 047/502] check for array query parameter --- packages/server/src/utils/XSS.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index 3e96e6c8..5d8b81e9 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -6,8 +6,15 @@ export function sanitizeMiddleware(req: Request, res: Response, next: NextFuncti const decodedURI = decodeURI(req.url) req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = sanitizeHtml(req.query[p] as string) + if (Array.isArray(req.query[p])) { + const sanitizedQ = [] + for (const q of req.query[p] as string[]) { + sanitizedQ.push(sanitizeHtml(q)) + } + req.query[p] = sanitizedQ + } else { + req.query[p] = sanitizeHtml(req.query[p] as string) + } } - next() } From bfa870e56b0371e661ef5d980ded8140b78de42a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 12:19:35 +0530 Subject: [PATCH 048/502] 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 541edcf0..aba803e4 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 343acafd..b7b1f6a6 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 6752f944..bc50155c 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 2bf1c04a..adec01cd 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 e32ce663..db2116b0 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 049/502] 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 39baaf8e..8f728f19 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 adec01cd..d3575388 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 050/502] 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 b7b1f6a6..8bae726c 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 db2116b0..85c40e44 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 ccec36d3..9399eb20 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 051/502] 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 85c40e44..86a121f6 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 8e1ef2d5337f8140e8789830b7c5844ee854cc24 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Dec 2023 12:44:42 +0000 Subject: [PATCH 052/502] fix sanitized & --- packages/server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2ab454ad..2d40f32e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -200,7 +200,7 @@ export class App { // Get component credential via name this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { - if (!req.params.name.includes('&')) { + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -208,7 +208,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { From ef3f1b34b149edb2064bc862abf6397522738cc1 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 12 Dec 2023 22:22:25 +0530 Subject: [PATCH 053/502] 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 aba803e4..6b0397c8 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 8f728f19..34e461f7 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 2110e1146b5d7ca024411f5ec26be3dcee540972 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Dec 2023 23:14:52 +0000 Subject: [PATCH 054/502] add a public endpoint to retrieve chatbotconfig --- packages/server/src/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2d40f32e..fb4a5f5a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -138,6 +138,7 @@ export class App { '/api/v1/verify/apikey/', '/api/v1/chatflows/apikey/', '/api/v1/public-chatflows', + '/api/v1/public-chatbotConfig', '/api/v1/prediction/', '/api/v1/vector/upsert/', '/api/v1/node-icon/', @@ -328,6 +329,23 @@ export class App { return res.status(404).send(`Chatflow ${req.params.id} not found`) }) + // Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat) + // Safe as public endpoint as chatbotConfig doesn't contain sensitive credential + this.app.get('/api/v1/public-chatbotConfig/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) + if (chatflow && chatflow.chatbotConfig) { + try { + const parsedConfig = JSON.parse(chatflow.chatbotConfig) + return res.json(parsedConfig) + } catch (e) { + return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) + } + } + return res.status(404).send(`Chatbot Config for Chatflow ${req.params.id} not found`) + }) + // Save chatflow this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => { const body = req.body From 1bd1fd58280c2a524baea346ab9e21e817484033 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 13 Dec 2023 10:47:45 +0530 Subject: [PATCH 055/502] MultiModal: Minor adjustments to layout and categorization of node --- .../multimodal/OpenAI/OpenAIVisionChain.ts | 2 +- packages/server/src/index.ts | 4 +- .../ui/src/views/chatmessage/ChatMessage.css | 12 ++++ .../ui/src/views/chatmessage/ChatMessage.js | 71 +++++++++++-------- 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts b/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts index dcaa96e2..1ff4f4c9 100644 --- a/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts +++ b/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts @@ -24,7 +24,7 @@ class OpenAIVisionChain_Chains implements INode { this.version = 1.0 this.type = 'OpenAIMultiModalChain' this.icon = 'chain.svg' - this.category = 'Chains' + this.category = 'MultiModal' this.badge = 'BETA' this.description = 'Chain to query against Image and Audio Input.' this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index eb03f47e..673ba278 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1456,7 +1456,9 @@ export class App { if (!endingNodeData) return res.status(500).send(`Ending node ${endingNodeId} data not found`) if (endingNodeData && endingNodeData.category !== 'Chains' && endingNodeData.category !== 'Agents' && !isUpsert) { - return res.status(500).send(`Ending node must be either a Chain or Agent`) + if (endingNodeData.type !== 'OpenAIMultiModalChain') { + return res.status(500).send(`Ending node must be either a Chain or Agent`) + } } if ( diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index f1831d39..3b0bb9e3 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -119,6 +119,7 @@ .cloud { width: 400px; height: calc(100vh - 260px); + overflow-y: scroll; border-radius: 0.5rem; display: flex; justify-content: center; @@ -169,6 +170,17 @@ } +.preview-card { + border: 2px solid #E7EDF3; + border-radius: 16%; + transition: 0.4s; +} + +.preview-card&:hover { + border-color: #5B9FED; +} + + .button { flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ margin: 5px; /* Adjust as needed for spacing between buttons */ diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 37b45bd5..a4a13df0 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -14,6 +14,7 @@ import { Box, Button, Card, + CardActions, CardMedia, Chip, CircularProgress, @@ -47,6 +48,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' +import DeleteIcon from '@mui/icons-material/Delete' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -245,7 +247,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const previewStyle = { width: '128px', height: '64px', - objectFit: 'cover' // This makes the image cover the area, cropping it if necessary + objectFit: 'fit' // This makes the image cover the area, cropping it if necessary } const messageImageStyle = { width: '128px', @@ -685,13 +687,50 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
+
+ {previews && previews.length > 0 && ( +
+
+ + {previews.map((item, index) => ( + <> + {item.mime.startsWith('image/') ? ( + + + + + handleDeletePreview(item)} size='small'> + + + + + + ) : ( + + + + + handleDeletePreview(item)} size='small'> + + + + + + )} + + ))} + +
+
+ )} +
{
-
- {previews && previews.length > 0 && ( - - {previews.map((item, index) => ( - - {item.mime.startsWith('image/') ? ( - - - - ) : ( - // eslint-disable-next-line jsx-a11y/media-has-caption - - )} - - ))} - - )} -
+ setSourceDialogOpen(false)} />
) From a504e4b38f72345d5be9c4f527f7f959eaa9d292 Mon Sep 17 00:00:00 2001 From: kzhang Date: Tue, 12 Dec 2023 23:46:15 -0600 Subject: [PATCH 056/502] Add windowSize input to avoid "token exceeded error" --- .../RedisBackedChatMemory/RedisBackedChatMemory.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index 7fe447ad..a92d9bbb 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -57,6 +57,14 @@ class RedisBackedChatMemory_Memory implements INode { type: 'string', default: 'chat_history', additionalParams: true + }, + { + label: 'Window Size', + name: 'windowSize', + type: 'number', + description: 'Window of size k to surface the last k back-and-forth to use as memory.', + additionalParams: true, + optional: true } ] } @@ -89,6 +97,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom 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 @@ -129,7 +138,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const redisChatMessageHistory = new RedisChatMessageHistory(obj) redisChatMessageHistory.getMessages = async (): Promise => { - const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, 0, -1) + const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, windowSize ? -windowSize : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) return orderedMessages.map(mapStoredMessageToChatMessage) } From 8f4dd8b50926842d6efc4b9600f4a52ef7892de2 Mon Sep 17 00:00:00 2001 From: tirongi Date: Wed, 13 Dec 2023 10:47:39 +0100 Subject: [PATCH 057/502] Fix wrong elasti client setup when use custom URL --- .../Elasticsearch/Elasticsearch.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts index 5f3cf206..04c90c6b 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch.ts @@ -183,13 +183,26 @@ const prepareConnectionOptions = ( } else if (cloudId) { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) - elasticSearchClientOptions = { - cloud: { - id: cloudId - }, - auth: { - username: username, - password: password + if (cloudId.startsWith('http')) { + elasticSearchClientOptions = { + node: cloudId, + auth: { + username: username, + password: password + }, + tls: { + rejectUnauthorized: false + } + } + } else { + elasticSearchClientOptions = { + cloud: { + id: cloudId + }, + auth: { + username: username, + password: password + } } } } From c609c63f44f61267ba97dba8a8924f4692320681 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 13 Dec 2023 22:10:00 +0530 Subject: [PATCH 058/502] MultiModal: start integration of audio input (live recording) for MultiModal. --- .../ui/src/views/chatmessage/ChatMessage.js | 537 +++++++++++++++++- .../src/views/chatmessage/audio-recording.css | 278 +++++++++ .../src/views/chatmessage/audio-recording.js | 433 ++++++++++++++ 3 files changed, 1232 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/views/chatmessage/audio-recording.css create mode 100644 packages/ui/src/views/chatmessage/audio-recording.js diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index a4a13df0..d7bbaf9e 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -26,13 +26,23 @@ import { Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconDownload, IconSend, IconUpload } from '@tabler/icons' +import { + IconDownload, + IconSend, + IconUpload, + IconMicrophone, + IconPhotoPlus, + IconPlayerStop, + IconPlayerRecord, + IconCircleDot +} from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' import SourceDocDialog from 'ui-component/dialog/SourceDocDialog' import './ChatMessage.css' +import './audio-recording.css' // api import chatmessageApi from 'api/chatmessage' @@ -477,6 +487,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { getIsChatflowStreamingApi.request(chatflowid) getAllowChatFlowUploads.request(chatflowid) scrollToBottom() + initAudioRecording() socket = socketIOClient(baseURL) @@ -519,6 +530,39 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { onDrop={handleDrop} className={`file-drop-field`} > +
+
+ +
+ +

+
+ +
+
+

+ Audio is playing. + . + . +

+
+
+
+
+

To record audio, use browsers like Chrome and Firefox that support audio recording.

+ +
+
+ {/* eslint-disable-next-line jsx-a11y/media-has-caption */} + {isDragOver && getAllowChatFlowUploads.data?.allowUploads && ( Drop here to upload @@ -750,7 +794,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { disabled={loading || !chatflowid} edge='start' > - @@ -758,20 +802,35 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { ) } endAdornment={ - - - {loading ? ( -
- -
- ) : ( - // Send icon SVG in input field - - )} -
-
+ <> + {isChatFlowAvailableForUploads && ( + + + + + + )} + + + {loading ? ( +
+ +
+ ) : ( + // Send icon SVG in input field + + )} +
+
+ } /> {isChatFlowAvailableForUploads && ( @@ -791,3 +850,449 @@ ChatMessage.propTypes = { chatflowid: PropTypes.string, isDialog: PropTypes.bool } + +// audio-recording.js --------------- +//View +let microphoneButton = document.getElementsByClassName('start-recording-button')[0] +let recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0] +let stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0] +let cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0] +let elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0] +let closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0] +let overlay = document.getElementsByClassName('overlay')[0] +let audioElement = document.getElementsByClassName('audio-element')[0] +let audioElementSource = audioElement?.getElementsByTagName('source')[0] +let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] + +const initAudioRecording = () => { + microphoneButton = document.getElementsByClassName('start-recording-button')[0] + recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0] + stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0] + cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0] + elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0] + closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0] + overlay = document.getElementsByClassName('overlay')[0] + audioElement = document.getElementsByClassName('audio-element')[0] + audioElementSource = audioElement?.getElementsByTagName('source')[0] + textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] + //Listeners + + //Listen to start recording button + if (microphoneButton) microphoneButton.onclick = startAudioRecording + + //Listen to stop recording button + if (stopRecordingButton) stopRecordingButton.onclick = stopAudioRecording + + //Listen to cancel recording button + if (cancelRecordingButton) cancelRecordingButton.onclick = cancelAudioRecording + + //Listen to when the ok button is clicked in the browser not supporting audio recording box + if (closeBrowserNotSupportedBoxButton) closeBrowserNotSupportedBoxButton.onclick = hideBrowserNotSupportedOverlay + + //Listen to when the audio being played ends + if (audioElement) audioElement.onended = hideTextIndicatorOfAudioPlaying +} + +/** Displays recording control buttons */ +function handleDisplayingRecordingControlButtons() { + //Hide the microphone button that starts audio recording + microphoneButton.style.display = 'none' + + //Display the recording control buttons + recordingControlButtonsContainer.classList.remove('hide') + + //Handle the displaying of the elapsed recording time + handleElapsedRecordingTime() +} + +/** Hide the displayed recording control buttons */ +function handleHidingRecordingControlButtons() { + //Display the microphone button that starts audio recording + microphoneButton.style.display = 'block' + + //Hide the recording control buttons + recordingControlButtonsContainer.classList.add('hide') + + //stop interval that handles both time elapsed and the red dot + clearInterval(elapsedTimeTimer) +} + +/** Displays browser not supported info box for the user*/ +function displayBrowserNotSupportedOverlay() { + overlay.classList.remove('hide') +} + +/** Displays browser not supported info box for the user*/ +function hideBrowserNotSupportedOverlay() { + overlay.classList.add('hide') +} + +/** Creates a source element for the audio element in the HTML document*/ +function createSourceForAudioElement() { + let sourceElement = document.createElement('source') + audioElement.appendChild(sourceElement) + + audioElementSource = sourceElement +} + +/** Display the text indicator of the audio being playing in the background */ +function displayTextIndicatorOfAudioPlaying() { + textIndicatorOfAudiPlaying.classList.remove('hide') +} + +/** Hide the text indicator of the audio being playing in the background */ +function hideTextIndicatorOfAudioPlaying() { + textIndicatorOfAudiPlaying.classList.add('hide') +} + +//Controller + +/** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/ +let audioRecordStartTime + +/** Stores the maximum recording time in hours to stop recording once maximum recording hour has been reached */ +let maximumRecordingTimeInHours = 1 + +/** Stores the reference of the setInterval function that controls the timer in audio recording*/ +let elapsedTimeTimer + +/** Starts the audio recording*/ +function startAudioRecording() { + console.log('Recording Audio...') + + //If a previous audio recording is playing, pause it + let recorderAudioIsPlaying = !audioElement.paused // the paused property tells whether the media element is paused or not + console.log('paused?', !recorderAudioIsPlaying) + if (recorderAudioIsPlaying) { + audioElement.pause() + //also hide the audio playing indicator displayed on the screen + hideTextIndicatorOfAudioPlaying() + } + + //start recording using the audio recording API + audioRecorder + .start() + .then(() => { + //on success + + //store the recording start time to display the elapsed time according to it + audioRecordStartTime = new Date() + + //display control buttons to offer the functionality of stop and cancel + handleDisplayingRecordingControlButtons() + }) + .catch((error) => { + //on error + //No Browser Support Error + if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) { + console.log('To record audio, use browsers like Chrome and Firefox.') + displayBrowserNotSupportedOverlay() + } + + //Error handling structure + switch (error.name) { + case 'AbortError': //error from navigator.mediaDevices.getUserMedia + console.log('An AbortError has occurred.') + break + case 'NotAllowedError': //error from navigator.mediaDevices.getUserMedia + console.log('A NotAllowedError has occurred. User might have denied permission.') + break + case 'NotFoundError': //error from navigator.mediaDevices.getUserMedia + console.log('A NotFoundError has occurred.') + break + case 'NotReadableError': //error from navigator.mediaDevices.getUserMedia + console.log('A NotReadableError has occurred.') + break + case 'SecurityError': //error from navigator.mediaDevices.getUserMedia or from the MediaRecorder.start + console.log('A SecurityError has occurred.') + break + case 'TypeError': //error from navigator.mediaDevices.getUserMedia + console.log('A TypeError has occurred.') + break + case 'InvalidStateError': //error from the MediaRecorder.start + console.log('An InvalidStateError has occurred.') + break + case 'UnknownError': //error from the MediaRecorder.start + console.log('An UnknownError has occurred.') + break + default: + console.log('An error occurred with the error name ' + error.name) + } + }) +} +/** Stop the currently started audio recording & sends it + */ +function stopAudioRecording() { + console.log('Stopping Audio Recording...') + + //stop the recording using the audio recording API + audioRecorder + .stop() + .then((audioAsblob) => { + //Play recorder audio + playAudio(audioAsblob) + + //hide recording control button & return record icon + handleHidingRecordingControlButtons() + }) + .catch((error) => { + //Error handling structure + switch (error.name) { + case 'InvalidStateError': //error from the MediaRecorder.stop + console.log('An InvalidStateError has occurred.') + break + default: + console.log('An error occurred with the error name ' + error.name) + } + }) +} + +/** Cancel the currently started audio recording */ +function cancelAudioRecording() { + console.log('Canceling audio...') + + //cancel the recording using the audio recording API + audioRecorder.cancel() + + //hide recording control button & return record icon + handleHidingRecordingControlButtons() +} + +/** Plays recorded audio using the audio element in the HTML document + * @param {Blob} recorderAudioAsBlob - recorded audio as a Blob Object + */ +function playAudio(recorderAudioAsBlob) { + //read content of files (Blobs) asynchronously + let reader = new FileReader() + + //once content has been read + reader.onload = (e) => { + //store the base64 URL that represents the URL of the recording audio + let base64URL = e.target.result + + //If this is the first audio playing, create a source element + //as pre-populating the HTML with a source of empty src causes error + if (!audioElementSource) + //if it is not defined create it (happens first time only) + createSourceForAudioElement() + + //set the audio element's source using the base64 URL + audioElementSource.src = base64URL + + //set the type of the audio element based on the recorded audio's Blob type + let BlobType = recorderAudioAsBlob.type.includes(';') + ? recorderAudioAsBlob.type.substr(0, recorderAudioAsBlob.type.indexOf(';')) + : recorderAudioAsBlob.type + audioElementSource.type = BlobType + + //call the load method as it is used to update the audio element after changing the source or other settings + audioElement.load() + + //play the audio after successfully setting new src and type that corresponds to the recorded audio + console.log('Playing audio...') + audioElement.play() + + //Display text indicator of having the audio play in the background + displayTextIndicatorOfAudioPlaying() + } + + //read content and convert it to a URL (base64) + reader.readAsDataURL(recorderAudioAsBlob) +} + +/** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/ +function handleElapsedRecordingTime() { + //display initial time when recording begins + displayElapsedTimeDuringAudioRecording('00:00') + + //create an interval that compute & displays elapsed time, as well as, animate red dot - every second + elapsedTimeTimer = setInterval(() => { + //compute the elapsed time every second + let elapsedTime = computeElapsedTime(audioRecordStartTime) //pass the actual record start time + //display the elapsed time + displayElapsedTimeDuringAudioRecording(elapsedTime) + }, 1000) //every second +} + +/** Display elapsed time during audio recording + * @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss + */ +function displayElapsedTimeDuringAudioRecording(elapsedTime) { + //1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element + elapsedTimeTag.innerHTML = elapsedTime + + //2. Stop the recording when the max number of hours is reached + if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) { + stopAudioRecording() + } +} + +/** + * @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss + * @returns {Boolean} whether the elapsed time reached the maximum number of hours or not + */ +function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) { + //Split the elapsed time by the symbol that separates the hours, minutes and seconds : + let elapsedTimeSplit = elapsedTime.split(':') + + //Turn the maximum recording time in hours to a string and pad it with zero if less than 10 + let maximumRecordingTimeInHoursAsString = + maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString() + + //if the elapsed time reach hours and also reach the maximum recording time in hours return true + if (elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString) return true + //otherwise, return false + else return false +} + +/** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss + * @param {String} startTime - start time to compute the elapsed time since + * @returns {String} elapsed time in mm:ss format or hh:mm:ss format, if elapsed hours are 0. + */ +function computeElapsedTime(startTime) { + //record end time + let endTime = new Date() + + //time difference in ms + let timeDiff = endTime - startTime + + //convert time difference from ms to seconds + timeDiff = timeDiff / 1000 + + //extract integer seconds that don't form a minute using % + let seconds = Math.floor(timeDiff % 60) //ignoring incomplete seconds (floor) + + //pad seconds with a zero if necessary + seconds = seconds < 10 ? '0' + seconds : seconds + + //convert time difference from seconds to minutes using % + timeDiff = Math.floor(timeDiff / 60) + + //extract integer minutes that don't form an hour using % + let minutes = timeDiff % 60 //no need to floor possible incomplete minutes, because they've been handled as seconds + minutes = minutes < 10 ? '0' + minutes : minutes + + //convert time difference from minutes to hours + timeDiff = Math.floor(timeDiff / 60) + + //extract integer hours that don't form a day using % + let hours = timeDiff % 24 //no need to floor possible incomplete hours, because they've been handled as seconds + + //convert time difference from hours to days + timeDiff = Math.floor(timeDiff / 24) + + // the rest of timeDiff is number of days + let days = timeDiff //add days to hours + + let totalHours = hours + days * 24 + totalHours = totalHours < 10 ? '0' + totalHours : totalHours + + if (totalHours === '00') { + return minutes + ':' + seconds + } else { + return totalHours + ':' + minutes + ':' + seconds + } +} + +//API to handle audio recording + +const audioRecorder = { + /** Stores the recorded audio as Blob objects of audio data as the recording continues*/ + audioBlobs: [] /*of type Blob[]*/, + /** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/ + mediaRecorder: null /*of type MediaRecorder*/, + /** Stores the reference to the stream currently capturing the audio*/ + streamBeingCaptured: null /*of type MediaStream*/, + /** Start recording the audio + * @returns {Promise} - returns a promise that resolves if audio recording successfully started + */ + start: function () { + //Feature Detection + if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { + //Feature is not supported in browser + //return a custom error + return Promise.reject(new Error('mediaDevices API or getUserMedia method is not supported in this browser.')) + } else { + //Feature is supported in browser + + //create an audio stream + return ( + navigator.mediaDevices + .getUserMedia({ audio: true } /*of type MediaStreamConstraints*/) + //returns a promise that resolves to the audio stream + .then((stream) /*of type MediaStream*/ => { + //save the reference of the stream to be able to stop it when necessary + audioRecorder.streamBeingCaptured = stream + + //create a media recorder instance by passing that stream into the MediaRecorder constructor + audioRecorder.mediaRecorder = new MediaRecorder(stream) /*the MediaRecorder interface of the MediaStream Recording + API provides functionality to easily record media*/ + + //clear previously saved audio Blobs, if any + audioRecorder.audioBlobs = [] + + //add a dataavailable event listener in order to store the audio data Blobs when recording + audioRecorder.mediaRecorder.addEventListener('dataavailable', (event) => { + //store audio Blob object + audioRecorder.audioBlobs.push(event.data) + }) + + //start the recording by calling the start method on the media recorder + audioRecorder.mediaRecorder.start() + }) + ) + + /* errors are not handled in the API because if its handled and the promise is chained, the .then after the catch will be executed*/ + } + }, + /** Stop the started audio recording + * @returns {Promise} - returns a promise that resolves to the audio as a blob file + */ + stop: function () { + //return a promise that would return the blob or URL of the recording + return new Promise((resolve) => { + //save audio type to pass to set the Blob type + let mimeType = audioRecorder.mediaRecorder.mimeType + + //listen to the stop event in order to create & return a single Blob object + audioRecorder.mediaRecorder.addEventListener('stop', () => { + //create a single blob object, as we might have gathered a few Blob objects that needs to be joined as one + let audioBlob = new Blob(audioRecorder.audioBlobs, { type: mimeType }) + + //resolve promise with the single audio blob representing the recorded audio + resolve(audioBlob) + }) + audioRecorder.cancel() + }) + }, + /** Cancel audio recording*/ + cancel: function () { + //stop the recording feature + audioRecorder.mediaRecorder.stop() + + //stop all the tracks on the active stream in order to stop the stream + audioRecorder.stopStream() + + //reset API properties for next recording + audioRecorder.resetRecordingProperties() + }, + /** Stop all the tracks on the active stream in order to stop the stream and remove + * the red flashing dot showing in the tab + */ + stopStream: function () { + //stopping the capturing request by stopping all the tracks on the active stream + audioRecorder.streamBeingCaptured + .getTracks() //get all tracks from the stream + .forEach((track) /*of type MediaStreamTrack*/ => track.stop()) //stop each one + }, + /** Reset all the recording properties including the media recorder and stream being captured*/ + resetRecordingProperties: function () { + audioRecorder.mediaRecorder = null + audioRecorder.streamBeingCaptured = null + + /*No need to remove event listeners attached to mediaRecorder as + If a DOM element which is removed is reference-free (no references pointing to it), the element itself is picked + up by the garbage collector as well as any event handlers/listeners associated with it. + getEventListeners(audioRecorder.mediaRecorder) will return an empty array of events.*/ + } +} diff --git a/packages/ui/src/views/chatmessage/audio-recording.css b/packages/ui/src/views/chatmessage/audio-recording.css new file mode 100644 index 00000000..5ba0fa50 --- /dev/null +++ b/packages/ui/src/views/chatmessage/audio-recording.css @@ -0,0 +1,278 @@ +/* style.css*/ + +/* Media Queries */ + +/* Small Devices*/ + +@media (min-width: 0px) { + * { + box-sizing: border-box; + } + .audio-recording-container { + width: 100%; + /* view port height*/ + /*targeting Chrome & Safari*/ + display: -webkit-flex; + /*targeting IE10*/ + display: -ms-flex; + display: flex; + flex-direction: column; + justify-content: center; + /*horizontal centering*/ + align-items: center; + } + .start-recording-button { + font-size: 70px; + color: #435f7a; + cursor: pointer; + } + .start-recording-button:hover { + opacity: 1; + } + .recording-control-buttons-container { + /*targeting Chrome & Safari*/ + display: -webkit-flex; + /*targeting IE10*/ + display: -ms-flex; + display: flex; + justify-content: space-evenly; + /*horizontal centering*/ + align-items: center; + width: 334px; + margin-bottom: 30px; + } + .cancel-recording-button, + .stop-recording-button { + font-size: 70px; + cursor: pointer; + } + .cancel-recording-button { + color: red; + opacity: 0.7; + } + .cancel-recording-button:hover { + color: rgb(206, 4, 4); + } + .stop-recording-button { + color: #33cc33; + opacity: 0.7; + } + .stop-recording-button:hover { + color: #27a527; + } + .recording-elapsed-time { + /*targeting Chrome & Safari*/ + display: -webkit-flex; + /*targeting IE10*/ + display: -ms-flex; + display: flex; + justify-content: center; + /*horizontal centering*/ + align-items: center; + } + .red-recording-dot { + font-size: 25px; + color: red; + margin-right: 12px; + /*transitions with Firefox, IE and Opera Support browser support*/ + animation-name: flashing-recording-dot; + -webkit-animation-name: flashing-recording-dot; + -moz-animation-name: flashing-recording-dot; + -o-animation-name: flashing-recording-dot; + animation-duration: 2s; + -webkit-animation-duration: 2s; + -moz-animation-duration: 2s; + -o-animation-duration: 2s; + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + -o-animation-iteration-count: infinite; + } + /* The animation code */ + @keyframes flashing-recording-dot { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @-webkit-keyframes flashing-recording-dot { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @-moz-keyframes flashing-recording-dot { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @-o-keyframes flashing-recording-dot { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + .elapsed-time { + font-size: 32px; + } + .recording-control-buttons-container.hide { + display: none; + } + .overlay { + position: absolute; + top: 0; + width: 100%; + background-color: rgba(82, 76, 76, 0.35); + /*targeting Chrome & Safari*/ + display: -webkit-flex; + /*targeting IE10*/ + display: -ms-flex; + display: flex; + justify-content: center; + /*horizontal centering*/ + align-items: center; + } + .overlay.hide { + display: none; + } + .browser-not-supporting-audio-recording-box { + /*targeting Chrome & Safari*/ + display: -webkit-flex; + /*targeting IE10*/ + display: -ms-flex; + display: flex; + flex-direction: column; + justify-content: space-between; + /*horizontal centering*/ + align-items: center; + width: 317px; + height: 119px; + background-color: white; + border-radius: 10px; + padding: 15px; + font-size: 16px; + } + .close-browser-not-supported-box { + cursor: pointer; + background-color: #abc1c05c; + border-radius: 10px; + font-size: 16px; + border: none; + } + .close-browser-not-supported-box:hover { + background-color: #92a5a45c; + } + .close-browser-not-supported-box:focus { + outline: none; + border: none; + } + .audio-element.hide { + display: none; + } + .text-indication-of-audio-playing-container { + height: 20px; + } + .text-indication-of-audio-playing { + font-size: 20px; + } + .text-indication-of-audio-playing.hide { + display: none; + } + /* 3 Dots animation*/ + .text-indication-of-audio-playing span { + /*transitions with Firefox, IE and Opera Support browser support*/ + animation-name: blinking-dot; + -webkit-animation-name: blinking-dot; + -moz-animation-name: blinking-dot; + -o-animation-name: blinking-dot; + animation-duration: 2s; + -webkit-animation-duration: 2s; + -moz-animation-duration: 2s; + -o-animation-duration: 2s; + animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + -o-animation-iteration-count: infinite; + } + .text-indication-of-audio-playing span:nth-child(2) { + animation-delay: .4s; + -webkit-animation-delay: .4s; + -moz-animation-delay: .4s; + -o-animation-delay: .4s; + } + .text-indication-of-audio-playing span:nth-child(3) { + animation-delay: .8s; + -webkit-animation-delay: .8s; + -moz-animation-delay: .8s; + -o-animation-delay: .8s; + } + /* The animation code */ + @keyframes blinking-dot { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + /* The animation code */ + @-webkit-keyframes blinking-dot { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + /* The animation code */ + @-moz-keyframes blinking-dot { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + /* The animation code */ + @-o-keyframes blinking-dot { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } +} \ No newline at end of file diff --git a/packages/ui/src/views/chatmessage/audio-recording.js b/packages/ui/src/views/chatmessage/audio-recording.js new file mode 100644 index 00000000..395443fe --- /dev/null +++ b/packages/ui/src/views/chatmessage/audio-recording.js @@ -0,0 +1,433 @@ +// audio-recording.js --------------- +//View +let microphoneButton = document.getElementsByClassName('start-recording-button')[0] +let recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0] +let stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0] +let cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0] +let elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0] +let closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0] +let overlay = document.getElementsByClassName('overlay')[0] +let audioElement = document.getElementsByClassName('audio-element')[0] +let audioElementSource = document.getElementsByClassName('audio-element')[0].getElementsByTagName('source')[0] +let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] + +//Listeners + +//Listen to start recording button +microphoneButton.onclick = startAudioRecording + +//Listen to stop recording button +stopRecordingButton.onclick = stopAudioRecording + +//Listen to cancel recording button +cancelRecordingButton.onclick = cancelAudioRecording + +//Listen to when the ok button is clicked in the browser not supporting audio recording box +closeBrowserNotSupportedBoxButton.onclick = hideBrowserNotSupportedOverlay + +//Listen to when the audio being played ends +audioElement.onended = hideTextIndicatorOfAudioPlaying + +/** Displays recording control buttons */ +function handleDisplayingRecordingControlButtons() { + //Hide the microphone button that starts audio recording + microphoneButton.style.display = 'none' + + //Display the recording control buttons + recordingControlButtonsContainer.classList.remove('hide') + + //Handle the displaying of the elapsed recording time + handleElapsedRecordingTime() +} + +/** Hide the displayed recording control buttons */ +function handleHidingRecordingControlButtons() { + //Display the microphone button that starts audio recording + microphoneButton.style.display = 'block' + + //Hide the recording control buttons + recordingControlButtonsContainer.classList.add('hide') + + //stop interval that handles both time elapsed and the red dot + clearInterval(elapsedTimeTimer) +} + +/** Displays browser not supported info box for the user*/ +function displayBrowserNotSupportedOverlay() { + overlay.classList.remove('hide') +} + +/** Displays browser not supported info box for the user*/ +function hideBrowserNotSupportedOverlay() { + overlay.classList.add('hide') +} + +/** Creates a source element for the audio element in the HTML document*/ +function createSourceForAudioElement() { + let sourceElement = document.createElement('source') + audioElement.appendChild(sourceElement) + + audioElementSource = sourceElement +} + +/** Display the text indicator of the audio being playing in the background */ +function displayTextIndicatorOfAudioPlaying() { + textIndicatorOfAudiPlaying.classList.remove('hide') +} + +/** Hide the text indicator of the audio being playing in the background */ +function hideTextIndicatorOfAudioPlaying() { + textIndicatorOfAudiPlaying.classList.add('hide') +} + +//Controller + +/** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/ +let audioRecordStartTime + +/** Stores the maximum recording time in hours to stop recording once maximum recording hour has been reached */ +let maximumRecordingTimeInHours = 1 + +/** Stores the reference of the setInterval function that controls the timer in audio recording*/ +let elapsedTimeTimer + +/** Starts the audio recording*/ +function startAudioRecording() { + console.log('Recording Audio...') + + //If a previous audio recording is playing, pause it + let recorderAudioIsPlaying = !audioElement.paused // the paused property tells whether the media element is paused or not + console.log('paused?', !recorderAudioIsPlaying) + if (recorderAudioIsPlaying) { + audioElement.pause() + //also hide the audio playing indicator displayed on the screen + hideTextIndicatorOfAudioPlaying() + } + + //start recording using the audio recording API + audioRecorder + .start() + .then(() => { + //on success + + //store the recording start time to display the elapsed time according to it + audioRecordStartTime = new Date() + + //display control buttons to offer the functionality of stop and cancel + handleDisplayingRecordingControlButtons() + }) + .catch((error) => { + //on error + //No Browser Support Error + if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) { + console.log('To record audio, use browsers like Chrome and Firefox.') + displayBrowserNotSupportedOverlay() + } + + //Error handling structure + switch (error.name) { + case 'AbortError': //error from navigator.mediaDevices.getUserMedia + console.log('An AbortError has occurred.') + break + case 'NotAllowedError': //error from navigator.mediaDevices.getUserMedia + console.log('A NotAllowedError has occurred. User might have denied permission.') + break + case 'NotFoundError': //error from navigator.mediaDevices.getUserMedia + console.log('A NotFoundError has occurred.') + break + case 'NotReadableError': //error from navigator.mediaDevices.getUserMedia + console.log('A NotReadableError has occurred.') + break + case 'SecurityError': //error from navigator.mediaDevices.getUserMedia or from the MediaRecorder.start + console.log('A SecurityError has occurred.') + break + case 'TypeError': //error from navigator.mediaDevices.getUserMedia + console.log('A TypeError has occurred.') + break + case 'InvalidStateError': //error from the MediaRecorder.start + console.log('An InvalidStateError has occurred.') + break + case 'UnknownError': //error from the MediaRecorder.start + console.log('An UnknownError has occurred.') + break + default: + console.log('An error occurred with the error name ' + error.name) + } + }) +} +/** Stop the currently started audio recording & sends it + */ +function stopAudioRecording() { + console.log('Stopping Audio Recording...') + + //stop the recording using the audio recording API + audioRecorder + .stop() + .then((audioAsblob) => { + //Play recorder audio + playAudio(audioAsblob) + + //hide recording control button & return record icon + handleHidingRecordingControlButtons() + }) + .catch((error) => { + //Error handling structure + switch (error.name) { + case 'InvalidStateError': //error from the MediaRecorder.stop + console.log('An InvalidStateError has occurred.') + break + default: + console.log('An error occurred with the error name ' + error.name) + } + }) +} + +/** Cancel the currently started audio recording */ +function cancelAudioRecording() { + console.log('Canceling audio...') + + //cancel the recording using the audio recording API + audioRecorder.cancel() + + //hide recording control button & return record icon + handleHidingRecordingControlButtons() +} + +/** Plays recorded audio using the audio element in the HTML document + * @param {Blob} recorderAudioAsBlob - recorded audio as a Blob Object + */ +function playAudio(recorderAudioAsBlob) { + //read content of files (Blobs) asynchronously + let reader = new FileReader() + + //once content has been read + reader.onload = (e) => { + //store the base64 URL that represents the URL of the recording audio + let base64URL = e.target.result + + //If this is the first audio playing, create a source element + //as pre-populating the HTML with a source of empty src causes error + if (!audioElementSource) + //if it is not defined create it (happens first time only) + createSourceForAudioElement() + + //set the audio element's source using the base64 URL + audioElementSource.src = base64URL + + //set the type of the audio element based on the recorded audio's Blob type + let BlobType = recorderAudioAsBlob.type.includes(';') + ? recorderAudioAsBlob.type.substr(0, recorderAudioAsBlob.type.indexOf(';')) + : recorderAudioAsBlob.type + audioElementSource.type = BlobType + + //call the load method as it is used to update the audio element after changing the source or other settings + audioElement.load() + + //play the audio after successfully setting new src and type that corresponds to the recorded audio + console.log('Playing audio...') + audioElement.play() + + //Display text indicator of having the audio play in the background + displayTextIndicatorOfAudioPlaying() + } + + //read content and convert it to a URL (base64) + reader.readAsDataURL(recorderAudioAsBlob) +} + +/** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/ +function handleElapsedRecordingTime() { + //display initial time when recording begins + displayElapsedTimeDuringAudioRecording('00:00') + + //create an interval that compute & displays elapsed time, as well as, animate red dot - every second + elapsedTimeTimer = setInterval(() => { + //compute the elapsed time every second + let elapsedTime = computeElapsedTime(audioRecordStartTime) //pass the actual record start time + //display the elapsed time + displayElapsedTimeDuringAudioRecording(elapsedTime) + }, 1000) //every second +} + +/** Display elapsed time during audio recording + * @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss + */ +function displayElapsedTimeDuringAudioRecording(elapsedTime) { + //1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element + elapsedTimeTag.innerHTML = elapsedTime + + //2. Stop the recording when the max number of hours is reached + if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) { + stopAudioRecording() + } +} + +/** + * @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss + * @returns {Boolean} whether the elapsed time reached the maximum number of hours or not + */ +function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) { + //Split the elapsed time by the symbol that separates the hours, minutes and seconds : + let elapsedTimeSplit = elapsedTime.split(':') + + //Turn the maximum recording time in hours to a string and pad it with zero if less than 10 + let maximumRecordingTimeInHoursAsString = + maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString() + + //if the elapsed time reach hours and also reach the maximum recording time in hours return true + if (elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString) return true + //otherwise, return false + else return false +} + +/** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss + * @param {String} startTime - start time to compute the elapsed time since + * @returns {String} elapsed time in mm:ss format or hh:mm:ss format, if elapsed hours are 0. + */ +function computeElapsedTime(startTime) { + //record end time + let endTime = new Date() + + //time difference in ms + let timeDiff = endTime - startTime + + //convert time difference from ms to seconds + timeDiff = timeDiff / 1000 + + //extract integer seconds that don't form a minute using % + let seconds = Math.floor(timeDiff % 60) //ignoring incomplete seconds (floor) + + //pad seconds with a zero if necessary + seconds = seconds < 10 ? '0' + seconds : seconds + + //convert time difference from seconds to minutes using % + timeDiff = Math.floor(timeDiff / 60) + + //extract integer minutes that don't form an hour using % + let minutes = timeDiff % 60 //no need to floor possible incomplete minutes, because they've been handled as seconds + minutes = minutes < 10 ? '0' + minutes : minutes + + //convert time difference from minutes to hours + timeDiff = Math.floor(timeDiff / 60) + + //extract integer hours that don't form a day using % + let hours = timeDiff % 24 //no need to floor possible incomplete hours, because they've been handled as seconds + + //convert time difference from hours to days + timeDiff = Math.floor(timeDiff / 24) + + // the rest of timeDiff is number of days + let days = timeDiff //add days to hours + + let totalHours = hours + days * 24 + totalHours = totalHours < 10 ? '0' + totalHours : totalHours + + if (totalHours === '00') { + return minutes + ':' + seconds + } else { + return totalHours + ':' + minutes + ':' + seconds + } +} + +//API to handle audio recording + +const audioRecorder = { + /** Stores the recorded audio as Blob objects of audio data as the recording continues*/ + audioBlobs: [] /*of type Blob[]*/, + /** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/ + mediaRecorder: null /*of type MediaRecorder*/, + /** Stores the reference to the stream currently capturing the audio*/ + streamBeingCaptured: null /*of type MediaStream*/, + /** Start recording the audio + * @returns {Promise} - returns a promise that resolves if audio recording successfully started + */ + start: function () { + //Feature Detection + if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { + //Feature is not supported in browser + //return a custom error + return Promise.reject(new Error('mediaDevices API or getUserMedia method is not supported in this browser.')) + } else { + //Feature is supported in browser + + //create an audio stream + return ( + navigator.mediaDevices + .getUserMedia({ audio: true } /*of type MediaStreamConstraints*/) + //returns a promise that resolves to the audio stream + .then((stream) /*of type MediaStream*/ => { + //save the reference of the stream to be able to stop it when necessary + audioRecorder.streamBeingCaptured = stream + + //create a media recorder instance by passing that stream into the MediaRecorder constructor + audioRecorder.mediaRecorder = new MediaRecorder(stream) /*the MediaRecorder interface of the MediaStream Recording + API provides functionality to easily record media*/ + + //clear previously saved audio Blobs, if any + audioRecorder.audioBlobs = [] + + //add a dataavailable event listener in order to store the audio data Blobs when recording + audioRecorder.mediaRecorder.addEventListener('dataavailable', (event) => { + //store audio Blob object + audioRecorder.audioBlobs.push(event.data) + }) + + //start the recording by calling the start method on the media recorder + audioRecorder.mediaRecorder.start() + }) + ) + + /* errors are not handled in the API because if its handled and the promise is chained, the .then after the catch will be executed*/ + } + }, + /** Stop the started audio recording + * @returns {Promise} - returns a promise that resolves to the audio as a blob file + */ + stop: function () { + //return a promise that would return the blob or URL of the recording + return new Promise((resolve) => { + //save audio type to pass to set the Blob type + let mimeType = audioRecorder.mediaRecorder.mimeType + + //listen to the stop event in order to create & return a single Blob object + audioRecorder.mediaRecorder.addEventListener('stop', () => { + //create a single blob object, as we might have gathered a few Blob objects that needs to be joined as one + let audioBlob = new Blob(audioRecorder.audioBlobs, { type: mimeType }) + + //resolve promise with the single audio blob representing the recorded audio + resolve(audioBlob) + }) + audioRecorder.cancel() + }) + }, + /** Cancel audio recording*/ + cancel: function () { + //stop the recording feature + audioRecorder.mediaRecorder.stop() + + //stop all the tracks on the active stream in order to stop the stream + audioRecorder.stopStream() + + //reset API properties for next recording + audioRecorder.resetRecordingProperties() + }, + /** Stop all the tracks on the active stream in order to stop the stream and remove + * the red flashing dot showing in the tab + */ + stopStream: function () { + //stopping the capturing request by stopping all the tracks on the active stream + audioRecorder.streamBeingCaptured + .getTracks() //get all tracks from the stream + .forEach((track) /*of type MediaStreamTrack*/ => track.stop()) //stop each one + }, + /** Reset all the recording properties including the media recorder and stream being captured*/ + resetRecordingProperties: function () { + audioRecorder.mediaRecorder = null + audioRecorder.streamBeingCaptured = null + + /*No need to remove event listeners attached to mediaRecorder as + If a DOM element which is removed is reference-free (no references pointing to it), the element itself is picked + up by the garbage collector as well as any event handlers/listeners associated with it. + getEventListeners(audioRecorder.mediaRecorder) will return an empty array of events.*/ + } +} From d214ddfe5bb985d508b6a209d56fd88a9ca6fc15 Mon Sep 17 00:00:00 2001 From: abhishekshankr Date: Wed, 13 Dec 2023 12:45:00 -0500 Subject: [PATCH 059/502] Initial Icon Tests --- .../components/nodes/agents/BabyAGI/BabyAGI.ts | 2 +- .../components/nodes/agents/BabyAGI/babyagi.svg | 7 +++++++ .../components/nodes/chains/LLMChain/LLMChain.ts | 2 +- .../nodes/chains/LLMChain/LLM_Chain.svg | 6 ++++++ .../nodes/chains/RetrievalQAChain/QA_Chain.svg | 7 +++++++ .../chains/RetrievalQAChain/RetrievalQAChain.ts | 2 +- .../components/nodes/documentloaders/Pdf/pdf.svg | 16 +++++++++------- .../nodes/documentloaders/Text/Text.ts | 2 +- .../nodes/documentloaders/Text/Txt.svg | 10 ++++++++++ 9 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 packages/components/nodes/agents/BabyAGI/babyagi.svg create mode 100644 packages/components/nodes/chains/LLMChain/LLM_Chain.svg create mode 100644 packages/components/nodes/chains/RetrievalQAChain/QA_Chain.svg create mode 100644 packages/components/nodes/documentloaders/Text/Txt.svg diff --git a/packages/components/nodes/agents/BabyAGI/BabyAGI.ts b/packages/components/nodes/agents/BabyAGI/BabyAGI.ts index e31f31c6..f82f134b 100644 --- a/packages/components/nodes/agents/BabyAGI/BabyAGI.ts +++ b/packages/components/nodes/agents/BabyAGI/BabyAGI.ts @@ -20,7 +20,7 @@ class BabyAGI_Agents implements INode { this.version = 1.0 this.type = 'BabyAGI' this.category = 'Agents' - this.icon = 'babyagi.jpg' + this.icon = 'babyagi.svg' this.description = 'Task Driven Autonomous Agent which creates new task and reprioritizes task list based on objective' this.baseClasses = ['BabyAGI'] this.inputs = [ diff --git a/packages/components/nodes/agents/BabyAGI/babyagi.svg b/packages/components/nodes/agents/BabyAGI/babyagi.svg new file mode 100644 index 00000000..0dd731cc --- /dev/null +++ b/packages/components/nodes/agents/BabyAGI/babyagi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index fd398151..b7c055e4 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -27,7 +27,7 @@ class LLMChain_Chains implements INode { this.name = 'llmChain' this.version = 3.0 this.type = 'LLMChain' - this.icon = 'chain.svg' + this.icon = 'LLM_Chain.svg' this.category = 'Chains' this.description = 'Chain to run queries against LLMs' this.baseClasses = [this.type, ...getBaseClasses(LLMChain)] diff --git a/packages/components/nodes/chains/LLMChain/LLM_Chain.svg b/packages/components/nodes/chains/LLMChain/LLM_Chain.svg new file mode 100644 index 00000000..f6c79d95 --- /dev/null +++ b/packages/components/nodes/chains/LLMChain/LLM_Chain.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/components/nodes/chains/RetrievalQAChain/QA_Chain.svg b/packages/components/nodes/chains/RetrievalQAChain/QA_Chain.svg new file mode 100644 index 00000000..7bea5c76 --- /dev/null +++ b/packages/components/nodes/chains/RetrievalQAChain/QA_Chain.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts index bff2a0a7..45cacad8 100644 --- a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts +++ b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts @@ -21,7 +21,7 @@ class RetrievalQAChain_Chains implements INode { this.name = 'retrievalQAChain' this.version = 1.0 this.type = 'RetrievalQAChain' - this.icon = 'chain.svg' + this.icon = 'QA_Chain.svg' this.category = 'Chains' this.description = 'QA chain to answer a question based on the retrieved documents' this.baseClasses = [this.type, ...getBaseClasses(RetrievalQAChain)] diff --git a/packages/components/nodes/documentloaders/Pdf/pdf.svg b/packages/components/nodes/documentloaders/Pdf/pdf.svg index 20af94f8..a937431b 100644 --- a/packages/components/nodes/documentloaders/Pdf/pdf.svg +++ b/packages/components/nodes/documentloaders/Pdf/pdf.svg @@ -1,7 +1,9 @@ - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/packages/components/nodes/documentloaders/Text/Text.ts b/packages/components/nodes/documentloaders/Text/Text.ts index 3f12e490..e41c5a9f 100644 --- a/packages/components/nodes/documentloaders/Text/Text.ts +++ b/packages/components/nodes/documentloaders/Text/Text.ts @@ -21,7 +21,7 @@ class Text_DocumentLoaders implements INode { this.name = 'textFile' this.version = 3.0 this.type = 'Document' - this.icon = 'textFile.svg' + this.icon = 'Txt.svg' this.category = 'Document Loaders' this.description = `Load data from text files` this.baseClasses = [this.type] diff --git a/packages/components/nodes/documentloaders/Text/Txt.svg b/packages/components/nodes/documentloaders/Text/Txt.svg new file mode 100644 index 00000000..c91819ba --- /dev/null +++ b/packages/components/nodes/documentloaders/Text/Txt.svg @@ -0,0 +1,10 @@ + + + + + + + + + + From 871dea249cdaa2ce2dcaf1e48577c03de5ff8471 Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Wed, 13 Dec 2023 18:47:25 +0100 Subject: [PATCH 060/502] Added boolean flag which enables ssl connection for redis nodes. This is needed for example connecting to the azure redis sever. --- packages/components/credentials/RedisCacheApi.credential.ts | 5 +++++ packages/components/nodes/cache/RedisCache/RedisCache.ts | 6 +++++- .../nodes/cache/RedisCache/RedisEmbeddingsCache.ts | 6 +++++- .../memory/RedisBackedChatMemory/RedisBackedChatMemory.ts | 6 +++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/components/credentials/RedisCacheApi.credential.ts b/packages/components/credentials/RedisCacheApi.credential.ts index 4d1a2498..2b4ad618 100644 --- a/packages/components/credentials/RedisCacheApi.credential.ts +++ b/packages/components/credentials/RedisCacheApi.credential.ts @@ -35,6 +35,11 @@ class RedisCacheApi implements INodeCredential { name: 'redisCachePwd', type: 'password', placeholder: '' + }, + { + label: 'Use SSL', + name: 'redisCacheSslEnabled', + type: 'boolean' } ] } diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index 8128b6e3..657fd1bc 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -56,12 +56,16 @@ class RedisCache implements INode { const password = getCredentialParam('redisCachePwd', credentialData, nodeData) const portStr = getCredentialParam('redisCachePort', credentialData, nodeData) const host = getCredentialParam('redisCacheHost', credentialData, nodeData) + const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) + + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {}; client = new Redis({ port: portStr ? parseInt(portStr) : 6379, host, username, - password + password, + ...tlsOptions }) } else { client = new Redis(redisUrl) diff --git a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts index f15869d7..5d6c4bfb 100644 --- a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts @@ -71,12 +71,16 @@ class RedisEmbeddingsCache implements INode { const password = getCredentialParam('redisCachePwd', credentialData, nodeData) const portStr = getCredentialParam('redisCachePort', credentialData, nodeData) const host = getCredentialParam('redisCacheHost', credentialData, nodeData) + const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) + + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {}; client = new Redis({ port: portStr ? parseInt(portStr) : 6379, host, username, - password + password, + ...tlsOptions }) } else { client = new Redis(redisUrl) diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index 7fe447ad..08a16631 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -103,12 +103,16 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const password = getCredentialParam('redisCachePwd', credentialData, nodeData) const portStr = getCredentialParam('redisCachePort', credentialData, nodeData) const host = getCredentialParam('redisCacheHost', credentialData, nodeData) + const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) + + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {}; client = new Redis({ port: portStr ? parseInt(portStr) : 6379, host, username, - password + password, + ...tlsOptions }) } else { client = new Redis(redisUrl) From 966a7dc861bbe3ff02bfd0e4b1dc94b961dc9182 Mon Sep 17 00:00:00 2001 From: abhishekshankr Date: Wed, 13 Dec 2023 12:52:27 -0500 Subject: [PATCH 061/502] Added additional test icons --- .../nodes/chains/ConversationChain/ConversationChain.ts | 2 +- .../components/nodes/chains/ConversationChain/chain.svg | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 7887ce97..ee5bdfc5 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -26,7 +26,7 @@ class ConversationChain_Chains implements INode { this.name = 'conversationChain' this.version = 1.0 this.type = 'ConversationChain' - this.icon = 'chain.svg' + this.icon = 'Chain.svg' this.category = 'Chains' this.description = 'Chat models specific conversational chain with memory' this.baseClasses = [this.type, ...getBaseClasses(ConversationChain)] diff --git a/packages/components/nodes/chains/ConversationChain/chain.svg b/packages/components/nodes/chains/ConversationChain/chain.svg index a5b32f90..2bba4889 100644 --- a/packages/components/nodes/chains/ConversationChain/chain.svg +++ b/packages/components/nodes/chains/ConversationChain/chain.svg @@ -1,6 +1,3 @@ - - - - - - \ No newline at end of file + + + From a2404afc41c7dcbbfbf3e3a13a94d5ccb02212f9 Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 14 Dec 2023 10:43:50 +0100 Subject: [PATCH 062/502] #1385 fixed linting --- packages/components/nodes/cache/RedisCache/RedisCache.ts | 2 +- .../components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts | 2 +- .../nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index 657fd1bc..4e61c239 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -58,7 +58,7 @@ class RedisCache implements INode { const host = getCredentialParam('redisCacheHost', credentialData, nodeData) const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) - const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {}; + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} client = new Redis({ port: portStr ? parseInt(portStr) : 6379, diff --git a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts index 5d6c4bfb..fe1b4df8 100644 --- a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts @@ -73,7 +73,7 @@ class RedisEmbeddingsCache implements INode { const host = getCredentialParam('redisCacheHost', credentialData, nodeData) const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) - const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {}; + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} client = new Redis({ port: portStr ? parseInt(portStr) : 6379, diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index 08a16631..f772ae56 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -105,7 +105,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const host = getCredentialParam('redisCacheHost', credentialData, nodeData) const sslEnabled = getCredentialParam('redisCacheSslEnabled', credentialData, nodeData) - const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {}; + const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} client = new Redis({ port: portStr ? parseInt(portStr) : 6379, From 826de70c6c73560b88dce0d7c070fc0947280aeb Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 15 Dec 2023 13:21:10 +0530 Subject: [PATCH 063/502] MultiModal: addition of live recording... --- .../ui/src/views/chatmessage/ChatMessage.js | 579 +++--------------- .../src/views/chatmessage/audio-recording.css | 3 + .../src/views/chatmessage/audio-recording.js | 167 +---- 3 files changed, 118 insertions(+), 631 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index d7bbaf9e..7e0092cd 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -26,16 +26,7 @@ import { Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { - IconDownload, - IconSend, - IconUpload, - IconMicrophone, - IconPhotoPlus, - IconPlayerStop, - IconPlayerRecord, - IconCircleDot -} from '@tabler/icons' +import { IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconCircleDot } from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' @@ -59,6 +50,7 @@ import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' import DeleteIcon from '@mui/icons-material/Delete' +import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -84,11 +76,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + // drag & drop and file input const fileUploadRef = useRef(null) const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [previews, setPreviews] = useState([]) const [isDragOver, setIsDragOver] = useState(false) + + // recording + const [isRecording, setIsRecording] = useState(false) + const [recordingNotSupported, setRecordingNotSupported] = useState(false) + const handleDragOver = (e) => { if (!isChatFlowAvailableForUploads) { return @@ -227,6 +225,24 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { event.target.value = null } + const addRecordingToPreviews = (blob) => { + const mimeType = blob.type.substring(0, blob.type.indexOf(';')) + // read blob and add to previews + const reader = new FileReader() + reader.readAsDataURL(blob) + reader.onloadend = () => { + const base64data = reader.result + const upload = { + data: base64data, + preview: audioUploadSVG, + type: 'audio', + name: 'audio.wav', + mime: mimeType + } + setPreviews((prevPreviews) => [...prevPreviews, upload]) + } + } + const handleDragEnter = (e) => { if (isChatFlowAvailableForUploads) { e.preventDefault() @@ -271,6 +287,21 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setPreviews([]) } + const onMicrophonePressed = () => { + setIsRecording(true) + startAudioRecording(setIsRecording, setRecordingNotSupported) + } + const onRecordingCancelled = () => { + cancelAudioRecording() + setIsRecording(false) + setRecordingNotSupported(false) + } + const onRecordingStopped = () => { + stopAudioRecording(addRecordingToPreviews) + setIsRecording(false) + setRecordingNotSupported(false) + } + const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -487,8 +518,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { getIsChatflowStreamingApi.request(chatflowid) getAllowChatFlowUploads.request(chatflowid) scrollToBottom() - initAudioRecording() - + setIsRecording(false) socket = socketIOClient(baseURL) socket.on('connect', () => { @@ -530,39 +560,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { onDrop={handleDrop} className={`file-drop-field`} > -
-
- -
- -

-
- -
-
-

- Audio is playing. - . - . -

-
-
-
-
-

To record audio, use browsers like Chrome and Firefox that support audio recording.

- -
-
- {/* eslint-disable-next-line jsx-a11y/media-has-caption */} - {isDragOver && getAllowChatFlowUploads.data?.allowUploads && ( Drop here to upload @@ -576,6 +573,41 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })} )} + {isRecording && ( + +
+ Recording +
+ + + +
+ + + +

00:00

+
+ + + +
+
+ {recordingNotSupported && ( +
+
+

To record audio, use browsers like Chrome and Firefox that support audio recording.

+ +
+
+ )} +
+ )}
{messages && @@ -804,9 +836,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { endAdornment={ <> {isChatFlowAvailableForUploads && ( - - + + onMicrophonePressed()} + type='button' + disabled={loading || !chatflowid} + edge='end' + > {
- setSourceDialogOpen(false)} />
) @@ -850,449 +887,3 @@ ChatMessage.propTypes = { chatflowid: PropTypes.string, isDialog: PropTypes.bool } - -// audio-recording.js --------------- -//View -let microphoneButton = document.getElementsByClassName('start-recording-button')[0] -let recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0] -let stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0] -let cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0] -let elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0] -let closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0] -let overlay = document.getElementsByClassName('overlay')[0] -let audioElement = document.getElementsByClassName('audio-element')[0] -let audioElementSource = audioElement?.getElementsByTagName('source')[0] -let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] - -const initAudioRecording = () => { - microphoneButton = document.getElementsByClassName('start-recording-button')[0] - recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0] - stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0] - cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0] - elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0] - closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0] - overlay = document.getElementsByClassName('overlay')[0] - audioElement = document.getElementsByClassName('audio-element')[0] - audioElementSource = audioElement?.getElementsByTagName('source')[0] - textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] - //Listeners - - //Listen to start recording button - if (microphoneButton) microphoneButton.onclick = startAudioRecording - - //Listen to stop recording button - if (stopRecordingButton) stopRecordingButton.onclick = stopAudioRecording - - //Listen to cancel recording button - if (cancelRecordingButton) cancelRecordingButton.onclick = cancelAudioRecording - - //Listen to when the ok button is clicked in the browser not supporting audio recording box - if (closeBrowserNotSupportedBoxButton) closeBrowserNotSupportedBoxButton.onclick = hideBrowserNotSupportedOverlay - - //Listen to when the audio being played ends - if (audioElement) audioElement.onended = hideTextIndicatorOfAudioPlaying -} - -/** Displays recording control buttons */ -function handleDisplayingRecordingControlButtons() { - //Hide the microphone button that starts audio recording - microphoneButton.style.display = 'none' - - //Display the recording control buttons - recordingControlButtonsContainer.classList.remove('hide') - - //Handle the displaying of the elapsed recording time - handleElapsedRecordingTime() -} - -/** Hide the displayed recording control buttons */ -function handleHidingRecordingControlButtons() { - //Display the microphone button that starts audio recording - microphoneButton.style.display = 'block' - - //Hide the recording control buttons - recordingControlButtonsContainer.classList.add('hide') - - //stop interval that handles both time elapsed and the red dot - clearInterval(elapsedTimeTimer) -} - -/** Displays browser not supported info box for the user*/ -function displayBrowserNotSupportedOverlay() { - overlay.classList.remove('hide') -} - -/** Displays browser not supported info box for the user*/ -function hideBrowserNotSupportedOverlay() { - overlay.classList.add('hide') -} - -/** Creates a source element for the audio element in the HTML document*/ -function createSourceForAudioElement() { - let sourceElement = document.createElement('source') - audioElement.appendChild(sourceElement) - - audioElementSource = sourceElement -} - -/** Display the text indicator of the audio being playing in the background */ -function displayTextIndicatorOfAudioPlaying() { - textIndicatorOfAudiPlaying.classList.remove('hide') -} - -/** Hide the text indicator of the audio being playing in the background */ -function hideTextIndicatorOfAudioPlaying() { - textIndicatorOfAudiPlaying.classList.add('hide') -} - -//Controller - -/** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/ -let audioRecordStartTime - -/** Stores the maximum recording time in hours to stop recording once maximum recording hour has been reached */ -let maximumRecordingTimeInHours = 1 - -/** Stores the reference of the setInterval function that controls the timer in audio recording*/ -let elapsedTimeTimer - -/** Starts the audio recording*/ -function startAudioRecording() { - console.log('Recording Audio...') - - //If a previous audio recording is playing, pause it - let recorderAudioIsPlaying = !audioElement.paused // the paused property tells whether the media element is paused or not - console.log('paused?', !recorderAudioIsPlaying) - if (recorderAudioIsPlaying) { - audioElement.pause() - //also hide the audio playing indicator displayed on the screen - hideTextIndicatorOfAudioPlaying() - } - - //start recording using the audio recording API - audioRecorder - .start() - .then(() => { - //on success - - //store the recording start time to display the elapsed time according to it - audioRecordStartTime = new Date() - - //display control buttons to offer the functionality of stop and cancel - handleDisplayingRecordingControlButtons() - }) - .catch((error) => { - //on error - //No Browser Support Error - if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) { - console.log('To record audio, use browsers like Chrome and Firefox.') - displayBrowserNotSupportedOverlay() - } - - //Error handling structure - switch (error.name) { - case 'AbortError': //error from navigator.mediaDevices.getUserMedia - console.log('An AbortError has occurred.') - break - case 'NotAllowedError': //error from navigator.mediaDevices.getUserMedia - console.log('A NotAllowedError has occurred. User might have denied permission.') - break - case 'NotFoundError': //error from navigator.mediaDevices.getUserMedia - console.log('A NotFoundError has occurred.') - break - case 'NotReadableError': //error from navigator.mediaDevices.getUserMedia - console.log('A NotReadableError has occurred.') - break - case 'SecurityError': //error from navigator.mediaDevices.getUserMedia or from the MediaRecorder.start - console.log('A SecurityError has occurred.') - break - case 'TypeError': //error from navigator.mediaDevices.getUserMedia - console.log('A TypeError has occurred.') - break - case 'InvalidStateError': //error from the MediaRecorder.start - console.log('An InvalidStateError has occurred.') - break - case 'UnknownError': //error from the MediaRecorder.start - console.log('An UnknownError has occurred.') - break - default: - console.log('An error occurred with the error name ' + error.name) - } - }) -} -/** Stop the currently started audio recording & sends it - */ -function stopAudioRecording() { - console.log('Stopping Audio Recording...') - - //stop the recording using the audio recording API - audioRecorder - .stop() - .then((audioAsblob) => { - //Play recorder audio - playAudio(audioAsblob) - - //hide recording control button & return record icon - handleHidingRecordingControlButtons() - }) - .catch((error) => { - //Error handling structure - switch (error.name) { - case 'InvalidStateError': //error from the MediaRecorder.stop - console.log('An InvalidStateError has occurred.') - break - default: - console.log('An error occurred with the error name ' + error.name) - } - }) -} - -/** Cancel the currently started audio recording */ -function cancelAudioRecording() { - console.log('Canceling audio...') - - //cancel the recording using the audio recording API - audioRecorder.cancel() - - //hide recording control button & return record icon - handleHidingRecordingControlButtons() -} - -/** Plays recorded audio using the audio element in the HTML document - * @param {Blob} recorderAudioAsBlob - recorded audio as a Blob Object - */ -function playAudio(recorderAudioAsBlob) { - //read content of files (Blobs) asynchronously - let reader = new FileReader() - - //once content has been read - reader.onload = (e) => { - //store the base64 URL that represents the URL of the recording audio - let base64URL = e.target.result - - //If this is the first audio playing, create a source element - //as pre-populating the HTML with a source of empty src causes error - if (!audioElementSource) - //if it is not defined create it (happens first time only) - createSourceForAudioElement() - - //set the audio element's source using the base64 URL - audioElementSource.src = base64URL - - //set the type of the audio element based on the recorded audio's Blob type - let BlobType = recorderAudioAsBlob.type.includes(';') - ? recorderAudioAsBlob.type.substr(0, recorderAudioAsBlob.type.indexOf(';')) - : recorderAudioAsBlob.type - audioElementSource.type = BlobType - - //call the load method as it is used to update the audio element after changing the source or other settings - audioElement.load() - - //play the audio after successfully setting new src and type that corresponds to the recorded audio - console.log('Playing audio...') - audioElement.play() - - //Display text indicator of having the audio play in the background - displayTextIndicatorOfAudioPlaying() - } - - //read content and convert it to a URL (base64) - reader.readAsDataURL(recorderAudioAsBlob) -} - -/** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/ -function handleElapsedRecordingTime() { - //display initial time when recording begins - displayElapsedTimeDuringAudioRecording('00:00') - - //create an interval that compute & displays elapsed time, as well as, animate red dot - every second - elapsedTimeTimer = setInterval(() => { - //compute the elapsed time every second - let elapsedTime = computeElapsedTime(audioRecordStartTime) //pass the actual record start time - //display the elapsed time - displayElapsedTimeDuringAudioRecording(elapsedTime) - }, 1000) //every second -} - -/** Display elapsed time during audio recording - * @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss - */ -function displayElapsedTimeDuringAudioRecording(elapsedTime) { - //1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element - elapsedTimeTag.innerHTML = elapsedTime - - //2. Stop the recording when the max number of hours is reached - if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) { - stopAudioRecording() - } -} - -/** - * @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss - * @returns {Boolean} whether the elapsed time reached the maximum number of hours or not - */ -function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) { - //Split the elapsed time by the symbol that separates the hours, minutes and seconds : - let elapsedTimeSplit = elapsedTime.split(':') - - //Turn the maximum recording time in hours to a string and pad it with zero if less than 10 - let maximumRecordingTimeInHoursAsString = - maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString() - - //if the elapsed time reach hours and also reach the maximum recording time in hours return true - if (elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString) return true - //otherwise, return false - else return false -} - -/** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss - * @param {String} startTime - start time to compute the elapsed time since - * @returns {String} elapsed time in mm:ss format or hh:mm:ss format, if elapsed hours are 0. - */ -function computeElapsedTime(startTime) { - //record end time - let endTime = new Date() - - //time difference in ms - let timeDiff = endTime - startTime - - //convert time difference from ms to seconds - timeDiff = timeDiff / 1000 - - //extract integer seconds that don't form a minute using % - let seconds = Math.floor(timeDiff % 60) //ignoring incomplete seconds (floor) - - //pad seconds with a zero if necessary - seconds = seconds < 10 ? '0' + seconds : seconds - - //convert time difference from seconds to minutes using % - timeDiff = Math.floor(timeDiff / 60) - - //extract integer minutes that don't form an hour using % - let minutes = timeDiff % 60 //no need to floor possible incomplete minutes, because they've been handled as seconds - minutes = minutes < 10 ? '0' + minutes : minutes - - //convert time difference from minutes to hours - timeDiff = Math.floor(timeDiff / 60) - - //extract integer hours that don't form a day using % - let hours = timeDiff % 24 //no need to floor possible incomplete hours, because they've been handled as seconds - - //convert time difference from hours to days - timeDiff = Math.floor(timeDiff / 24) - - // the rest of timeDiff is number of days - let days = timeDiff //add days to hours - - let totalHours = hours + days * 24 - totalHours = totalHours < 10 ? '0' + totalHours : totalHours - - if (totalHours === '00') { - return minutes + ':' + seconds - } else { - return totalHours + ':' + minutes + ':' + seconds - } -} - -//API to handle audio recording - -const audioRecorder = { - /** Stores the recorded audio as Blob objects of audio data as the recording continues*/ - audioBlobs: [] /*of type Blob[]*/, - /** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/ - mediaRecorder: null /*of type MediaRecorder*/, - /** Stores the reference to the stream currently capturing the audio*/ - streamBeingCaptured: null /*of type MediaStream*/, - /** Start recording the audio - * @returns {Promise} - returns a promise that resolves if audio recording successfully started - */ - start: function () { - //Feature Detection - if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { - //Feature is not supported in browser - //return a custom error - return Promise.reject(new Error('mediaDevices API or getUserMedia method is not supported in this browser.')) - } else { - //Feature is supported in browser - - //create an audio stream - return ( - navigator.mediaDevices - .getUserMedia({ audio: true } /*of type MediaStreamConstraints*/) - //returns a promise that resolves to the audio stream - .then((stream) /*of type MediaStream*/ => { - //save the reference of the stream to be able to stop it when necessary - audioRecorder.streamBeingCaptured = stream - - //create a media recorder instance by passing that stream into the MediaRecorder constructor - audioRecorder.mediaRecorder = new MediaRecorder(stream) /*the MediaRecorder interface of the MediaStream Recording - API provides functionality to easily record media*/ - - //clear previously saved audio Blobs, if any - audioRecorder.audioBlobs = [] - - //add a dataavailable event listener in order to store the audio data Blobs when recording - audioRecorder.mediaRecorder.addEventListener('dataavailable', (event) => { - //store audio Blob object - audioRecorder.audioBlobs.push(event.data) - }) - - //start the recording by calling the start method on the media recorder - audioRecorder.mediaRecorder.start() - }) - ) - - /* errors are not handled in the API because if its handled and the promise is chained, the .then after the catch will be executed*/ - } - }, - /** Stop the started audio recording - * @returns {Promise} - returns a promise that resolves to the audio as a blob file - */ - stop: function () { - //return a promise that would return the blob or URL of the recording - return new Promise((resolve) => { - //save audio type to pass to set the Blob type - let mimeType = audioRecorder.mediaRecorder.mimeType - - //listen to the stop event in order to create & return a single Blob object - audioRecorder.mediaRecorder.addEventListener('stop', () => { - //create a single blob object, as we might have gathered a few Blob objects that needs to be joined as one - let audioBlob = new Blob(audioRecorder.audioBlobs, { type: mimeType }) - - //resolve promise with the single audio blob representing the recorded audio - resolve(audioBlob) - }) - audioRecorder.cancel() - }) - }, - /** Cancel audio recording*/ - cancel: function () { - //stop the recording feature - audioRecorder.mediaRecorder.stop() - - //stop all the tracks on the active stream in order to stop the stream - audioRecorder.stopStream() - - //reset API properties for next recording - audioRecorder.resetRecordingProperties() - }, - /** Stop all the tracks on the active stream in order to stop the stream and remove - * the red flashing dot showing in the tab - */ - stopStream: function () { - //stopping the capturing request by stopping all the tracks on the active stream - audioRecorder.streamBeingCaptured - .getTracks() //get all tracks from the stream - .forEach((track) /*of type MediaStreamTrack*/ => track.stop()) //stop each one - }, - /** Reset all the recording properties including the media recorder and stream being captured*/ - resetRecordingProperties: function () { - audioRecorder.mediaRecorder = null - audioRecorder.streamBeingCaptured = null - - /*No need to remove event listeners attached to mediaRecorder as - If a DOM element which is removed is reference-free (no references pointing to it), the element itself is picked - up by the garbage collector as well as any event handlers/listeners associated with it. - getEventListeners(audioRecorder.mediaRecorder) will return an empty array of events.*/ - } -} diff --git a/packages/ui/src/views/chatmessage/audio-recording.css b/packages/ui/src/views/chatmessage/audio-recording.css index 5ba0fa50..fbca2f60 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.css +++ b/packages/ui/src/views/chatmessage/audio-recording.css @@ -20,6 +20,7 @@ justify-content: center; /*horizontal centering*/ align-items: center; + background-color: white; } .start-recording-button { font-size: 70px; @@ -40,6 +41,7 @@ align-items: center; width: 334px; margin-bottom: 30px; + background-color: white; } .cancel-recording-button, .stop-recording-button { @@ -61,6 +63,7 @@ color: #27a527; } .recording-elapsed-time { + font-size: 32px; /*targeting Chrome & Safari*/ display: -webkit-flex; /*targeting IE10*/ diff --git a/packages/ui/src/views/chatmessage/audio-recording.js b/packages/ui/src/views/chatmessage/audio-recording.js index 395443fe..f5cba001 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.js +++ b/packages/ui/src/views/chatmessage/audio-recording.js @@ -1,41 +1,21 @@ +/** + * @fileoverview This file contains the API to handle audio recording. + * Originally from 'https://ralzohairi.medium.com/audio-recording-in-javascript-96eed45b75ee' + */ + // audio-recording.js --------------- -//View -let microphoneButton = document.getElementsByClassName('start-recording-button')[0] -let recordingControlButtonsContainer = document.getElementsByClassName('recording-control-buttons-container')[0] -let stopRecordingButton = document.getElementsByClassName('stop-recording-button')[0] -let cancelRecordingButton = document.getElementsByClassName('cancel-recording-button')[0] -let elapsedTimeTag = document.getElementsByClassName('elapsed-time')[0] -let closeBrowserNotSupportedBoxButton = document.getElementsByClassName('close-browser-not-supported-box')[0] -let overlay = document.getElementsByClassName('overlay')[0] -let audioElement = document.getElementsByClassName('audio-element')[0] -let audioElementSource = document.getElementsByClassName('audio-element')[0].getElementsByTagName('source')[0] -let textIndicatorOfAudiPlaying = document.getElementsByClassName('text-indication-of-audio-playing')[0] +let microphoneButton, elapsedTimeTag -//Listeners - -//Listen to start recording button -microphoneButton.onclick = startAudioRecording - -//Listen to stop recording button -stopRecordingButton.onclick = stopAudioRecording - -//Listen to cancel recording button -cancelRecordingButton.onclick = cancelAudioRecording - -//Listen to when the ok button is clicked in the browser not supporting audio recording box -closeBrowserNotSupportedBoxButton.onclick = hideBrowserNotSupportedOverlay - -//Listen to when the audio being played ends -audioElement.onended = hideTextIndicatorOfAudioPlaying +/** Initialize controls */ +function initializeControls() { + microphoneButton = document.getElementsByClassName('start-recording-button')[0] +} /** Displays recording control buttons */ function handleDisplayingRecordingControlButtons() { //Hide the microphone button that starts audio recording microphoneButton.style.display = 'none' - //Display the recording control buttons - recordingControlButtonsContainer.classList.remove('hide') - //Handle the displaying of the elapsed recording time handleElapsedRecordingTime() } @@ -45,43 +25,10 @@ function handleHidingRecordingControlButtons() { //Display the microphone button that starts audio recording microphoneButton.style.display = 'block' - //Hide the recording control buttons - recordingControlButtonsContainer.classList.add('hide') - //stop interval that handles both time elapsed and the red dot clearInterval(elapsedTimeTimer) } -/** Displays browser not supported info box for the user*/ -function displayBrowserNotSupportedOverlay() { - overlay.classList.remove('hide') -} - -/** Displays browser not supported info box for the user*/ -function hideBrowserNotSupportedOverlay() { - overlay.classList.add('hide') -} - -/** Creates a source element for the audio element in the HTML document*/ -function createSourceForAudioElement() { - let sourceElement = document.createElement('source') - audioElement.appendChild(sourceElement) - - audioElementSource = sourceElement -} - -/** Display the text indicator of the audio being playing in the background */ -function displayTextIndicatorOfAudioPlaying() { - textIndicatorOfAudiPlaying.classList.remove('hide') -} - -/** Hide the text indicator of the audio being playing in the background */ -function hideTextIndicatorOfAudioPlaying() { - textIndicatorOfAudiPlaying.classList.add('hide') -} - -//Controller - /** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/ let audioRecordStartTime @@ -92,24 +39,17 @@ let maximumRecordingTimeInHours = 1 let elapsedTimeTimer /** Starts the audio recording*/ -function startAudioRecording() { - console.log('Recording Audio...') - - //If a previous audio recording is playing, pause it - let recorderAudioIsPlaying = !audioElement.paused // the paused property tells whether the media element is paused or not - console.log('paused?', !recorderAudioIsPlaying) - if (recorderAudioIsPlaying) { - audioElement.pause() - //also hide the audio playing indicator displayed on the screen - hideTextIndicatorOfAudioPlaying() - } +export function startAudioRecording(onRecordingStart, onUnsupportedBrowser) { + initializeControls() //start recording using the audio recording API audioRecorder .start() .then(() => { - //on success - + //on success show the controls to stop and cancel the recording + if (onRecordingStart) { + onRecordingStart(true) + } //store the recording start time to display the elapsed time according to it audioRecordStartTime = new Date() @@ -120,8 +60,9 @@ function startAudioRecording() { //on error //No Browser Support Error if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) { - console.log('To record audio, use browsers like Chrome and Firefox.') - displayBrowserNotSupportedOverlay() + if (onUnsupportedBrowser) { + onUnsupportedBrowser(true) + } } //Error handling structure @@ -157,18 +98,16 @@ function startAudioRecording() { } /** Stop the currently started audio recording & sends it */ -function stopAudioRecording() { - console.log('Stopping Audio Recording...') - +export function stopAudioRecording(addRecordingToPreviews) { //stop the recording using the audio recording API audioRecorder .stop() - .then((audioAsblob) => { - //Play recorder audio - playAudio(audioAsblob) - + .then((audioBlob) => { //hide recording control button & return record icon handleHidingRecordingControlButtons() + if (addRecordingToPreviews) { + addRecordingToPreviews(audioBlob) + } }) .catch((error) => { //Error handling structure @@ -183,9 +122,7 @@ function stopAudioRecording() { } /** Cancel the currently started audio recording */ -function cancelAudioRecording() { - console.log('Canceling audio...') - +export function cancelAudioRecording() { //cancel the recording using the audio recording API audioRecorder.cancel() @@ -193,50 +130,9 @@ function cancelAudioRecording() { handleHidingRecordingControlButtons() } -/** Plays recorded audio using the audio element in the HTML document - * @param {Blob} recorderAudioAsBlob - recorded audio as a Blob Object - */ -function playAudio(recorderAudioAsBlob) { - //read content of files (Blobs) asynchronously - let reader = new FileReader() - - //once content has been read - reader.onload = (e) => { - //store the base64 URL that represents the URL of the recording audio - let base64URL = e.target.result - - //If this is the first audio playing, create a source element - //as pre-populating the HTML with a source of empty src causes error - if (!audioElementSource) - //if it is not defined create it (happens first time only) - createSourceForAudioElement() - - //set the audio element's source using the base64 URL - audioElementSource.src = base64URL - - //set the type of the audio element based on the recorded audio's Blob type - let BlobType = recorderAudioAsBlob.type.includes(';') - ? recorderAudioAsBlob.type.substr(0, recorderAudioAsBlob.type.indexOf(';')) - : recorderAudioAsBlob.type - audioElementSource.type = BlobType - - //call the load method as it is used to update the audio element after changing the source or other settings - audioElement.load() - - //play the audio after successfully setting new src and type that corresponds to the recorded audio - console.log('Playing audio...') - audioElement.play() - - //Display text indicator of having the audio play in the background - displayTextIndicatorOfAudioPlaying() - } - - //read content and convert it to a URL (base64) - reader.readAsDataURL(recorderAudioAsBlob) -} - /** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/ function handleElapsedRecordingTime() { + elapsedTimeTag = document.getElementById('elapsed-time') //display initial time when recording begins displayElapsedTimeDuringAudioRecording('00:00') @@ -255,7 +151,6 @@ function handleElapsedRecordingTime() { function displayElapsedTimeDuringAudioRecording(elapsedTime) { //1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element elapsedTimeTag.innerHTML = elapsedTime - //2. Stop the recording when the max number of hours is reached if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) { stopAudioRecording() @@ -275,9 +170,7 @@ function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) { maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString() //if the elapsed time reach hours and also reach the maximum recording time in hours return true - if (elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString) return true - //otherwise, return false - else return false + return elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString } /** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss @@ -331,7 +224,7 @@ function computeElapsedTime(startTime) { //API to handle audio recording -const audioRecorder = { +export const audioRecorder = { /** Stores the recorded audio as Blob objects of audio data as the recording continues*/ audioBlobs: [] /*of type Blob[]*/, /** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/ @@ -360,8 +253,8 @@ const audioRecorder = { audioRecorder.streamBeingCaptured = stream //create a media recorder instance by passing that stream into the MediaRecorder constructor - audioRecorder.mediaRecorder = new MediaRecorder(stream) /*the MediaRecorder interface of the MediaStream Recording - API provides functionality to easily record media*/ + audioRecorder.mediaRecorder = new MediaRecorder(stream) + /*the MediaRecorder interface of the MediaStream Recording API provides functionality to easily record media*/ //clear previously saved audio Blobs, if any audioRecorder.audioBlobs = [] From e7f13c87d8b76909b31234447802aa7061b09038 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 15 Dec 2023 20:58:08 +0530 Subject: [PATCH 064/502] Addition of Google Gemini Chat Model, Credential and Associated Embeddings --- .../GoogleGenerativeAI.credential.ts | 25 ++++ .../ChatGoogleGenerativeAI.ts | 108 ++++++++++++++++++ .../ChatGoogleGenerativeAI/gemini.png | Bin 0 -> 57143 bytes .../GoogleGenerativeAIEmbedding.ts | 105 +++++++++++++++++ .../GoogleGenerativeAIEmbedding/gemini.png | Bin 0 -> 57143 bytes packages/components/package.json | 1 + 6 files changed, 239 insertions(+) create mode 100644 packages/components/credentials/GoogleGenerativeAI.credential.ts create mode 100644 packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts create mode 100644 packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/gemini.png create mode 100644 packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts create mode 100644 packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/gemini.png diff --git a/packages/components/credentials/GoogleGenerativeAI.credential.ts b/packages/components/credentials/GoogleGenerativeAI.credential.ts new file mode 100644 index 00000000..9a1f3f28 --- /dev/null +++ b/packages/components/credentials/GoogleGenerativeAI.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class GoogleGenerativeAICredential implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Google Generative AI' + this.name = 'googleGenerativeAI' + this.version = 1.0 + this.description = 'Get your API Key here.' + this.inputs = [ + { + label: 'Google AI API Key', + name: 'googleGenerativeAPIKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: GoogleGenerativeAICredential } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts new file mode 100644 index 00000000..26913424 --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -0,0 +1,108 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { BaseCache } from 'langchain/schema' +import { ChatGoogleGenerativeAI } from '@langchain/google-genai' + +class GoogleGenerativeAI_ChatModels implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'ChatGoogleGenerativeAI' + this.name = 'chatGoogleGenerativeAI' + this.version = 2.0 + this.type = 'ChatGoogleGenerativeAI' + this.icon = 'gemini.png' + this.category = 'Chat Models' + this.description = 'Wrapper around Google Gemini large language models that use the Chat endpoint' + this.baseClasses = [this.type, ...getBaseClasses(ChatGoogleGenerativeAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleGenerativeAI'], + optional: true, + description: 'Google Generative AI credential.' + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'gemini-pro', + name: 'gemini-pro' + } + ], + default: 'gemini-pro', + optional: true + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + default: 0.9, + optional: true + }, + { + label: 'Max Output Tokens', + name: 'maxOutputTokens', + type: 'number', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('googleGenerativeAPIKey', credentialData, nodeData) + + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxOutputTokens = nodeData.inputs?.maxOutputTokens as string + const topP = nodeData.inputs?.topP as string + const cache = nodeData.inputs?.cache as BaseCache + + const obj = { + apiKey: apiKey, + modelName: modelName, + maxOutputTokens: 2048 + } + + if (maxOutputTokens) obj.maxOutputTokens = parseInt(maxOutputTokens, 10) + + const model = new ChatGoogleGenerativeAI(obj) + if (topP) model.topP = parseFloat(topP) + if (cache) model.cache = cache + if (temperature) model.temperature = parseInt(temperature) + return model + } +} + +module.exports = { nodeClass: GoogleGenerativeAI_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/gemini.png b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0d60f44817cdbf1bd5483d88c1929831a84eed GIT binary patch literal 57143 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

1U z<~Q(Y3(oE^e9!x)iRwtrmp@6w->dw4i}!JUeDrGiWynMg1t(c_ZHPRfa;!kwc?&mxcbwP~C1JQ{jk% zCzU{y+GMi$ajsO?fv7SI(N1Kt4d??kOYLFSpbrO0_RueEMlD$fgYyQTb5lLem>ol2 z(B{J7^i>NF3PUy5)#+G(|BF8?#E7!Nhn#MhT;tg|t?X(gxV4dVkSB8M;5y9T)O4aB zbFSk7Fq|piUyOX>`A?L05f!;s7AWAkvhLtbbSxLhSO5^k`=LH=7{18a?jbBr!vX;S z4!qvWz0>LEXFJm$e|$du`s#3ci3dAtn9KIQiN1OBtw)*m5`ZGepnQ%f=rs7;M~)8# zG0GsdoIrRv(J{9<5l6LH!SYzZR?u0Fnh7o=W!`Y2V;TuHfk0pdZ+0&=t89BZWQL|foLNtOg=pDCj47HmNQnQj*E!fgvRk8+ux_$X7? zT$JJbdx0`~%@jv1)Y;H6nKc77W#+yom;cVzmgxxaoNl{==RbEb^~UEw+y$Ee#E@yv zN~)aCZGi%wE3kOFhjg3x2IVssj$>HLi=S?LL!%s)Fqe;Q{^N(8=`TP2Iz4^)cG^F| z7eMjs<^+4HX+*=jQ#b7V*5j?{KqmvqBx_DWj;|3IG*}b)@sG|#!H!c&@|ncIgL2=h zjj!{#1tEdiS*D#Gm?=+Xj9b5;GjGdlG)a8ZkM+_Kn;MBBk8zeUvjw$D4WL~9b8wvz zhwf2;EKw=_JHR8FW}GJ1JvW!fyb#ax0O8K0Na&YGkF2iqqb=+jjc}^(;qZx#Z_fVfkm*#(%qb(^PM>!#Z{X%|aJI8Gqi?vlf#JNHC)Q1` z%E1edPKuynr9np$I($1JrZ{znWAhl1ulhp(&WxT%=U@4wVV1@8S>QpY0_)K#xk#%W z<_X8nBI(g+cye8l0ZQo@AlMX!m}mznThoF`Xa71U_%$Box_(C0-dv<|A8bF?y+{B( zdOH#W+GV&ka(Pgvx>?4`TbTDj zv^&++NZH0Q>Iu3oB_e0eSlwPx;$6Jr6wGV7SE0E2 zvsLOgg-)Ow0z}&n$Um>B( zF>PH`mt-@Rf@~1L*l2MY_0o=sK1Es2tVqT%!Mwf5Yx=P?Y{$6l#R!6=B|7K2O=oT4 zQXP|mVsA2dqDs9!WOR*1V;Q5~B(p_l_M^O9m*TXUbqsFGOX8RC1m!r3Co_$r^JAhv z)k1^^4<5|k13I7SG9_-)oRXEjuYz+4w>ee7(|VVe4#Qaxy*J!}FViJvczX5=KHi^CrcY3VgF8b;706Shc3xQi>45 ztXViFIh3Ys*8$7pB2M7(y^dQGGR~XA5IxKF5B#?bc+{8CsYrzVl#e$1|J=5i>zt2d zlG7xgLIWP#Ei%<(E@tnk@1ks+=fH!y6Yxa;4Qj@$UoPYGya=P7QGb-p+dHlU>U~y0 z3Psy>Ei?%t8x6@iL>)f+I*Ysy0-oF^3K|WY^;^`TGicP7S?0wFiuEi4t_M6+i)Gk+ z4=5HPTrA58LtWQ{+Ha&fFIz4%FIQ{X5$J2|QbQdB; zh)TYE<0hj67;)4u^{gXVBscR&PLshquLVY#vO^(1ovzC@0+3Ur6Xl4<@)-*W=4H8a zltwuTESouv&m=4zBM%S_d6c7KFw+A3lNQPx;OY9+{?Jbp)_-Oj zw1N7P$9==IC0IANMGT;mty-4poe;m-Oq!1BxmG5+W*?Hs&aF*}MbV)j==cMn5m+y# z=DJCCHz=<=GMGzc;t%DSGjtba<8?ov+^pitQUp=5?Gs>+QD4;-#^ywYpSDh(DP|Og!L1-)t9>2Q6Bm7ORn-zB`@%^yumI*B_q( ze2%95HGFP*{r+^kfvs((clg`yjy>1whsrHSb+x#v;^>7D)NK_Q^M&88emT9=g}zTM zs5@Iq0DZn;bhAUW<&z!`jWi*@Iin7iNK;<3fX=h2Hi(dX$)v^%O^;M@)+vBab%Q$V zGRiF1&Yf~Y2Ue&`RF9pc%bCj}%VR2lhdgxLRMPp9*Y#tWb!Mq#=*W@hJkZWf1PVNQ zHARNZIghI`7i~9pxJ_Ey;B|09!$tj?iWyJ_JqrnpLps@qu84~t2ihXhg^cL)7zNLm zL6q$mJj>3#uVr)a^>)mu)ptq3Oqb)CYu<0UyTD2Utf3=@hb zZnJqaZ0{yAomflMl(H)-3l#7yxe#=@L_6&gYm+zlX z-~Ie(dbNX35^UlZ6aJ2;O}HaYo#AuS+)GVUV7OhfT#C)V=%S1YAg<>%xXSgf(JkJH za+GmLfXzT?LE>vN$(L`K+YUT11-r}vHV9`bkfR}5A#F6`qzfizs~ml|7-qmI;0c;b z0*?T$bq<7g{T43}D3|+bX6mnfD zTR&8sCcq(VjXh!(^3a$`E0Z;2}--0;u`3vXJaK;Q8fC2B{wCm>p#cyzU@pgau`N{L?$KPH} z&)yzPyT`b-0XBR_bAsnB+L0Jn=lJJJcoQx9l^O8l3lDV#STWl-Aoz7Xj|+Mcc=E3_ zIh-z&yurs`#Bp0N->sK^tV=vpfDR+^s6`eiXTkQ6o-}YAJV{o|C#B2HLqC@tb{|3M zSAJ`oNU0w=9p>!_aG=iC+Y0lStn~@LZ0jMy7O5_jGf(n*^}JA= zV3{WoTj5OJNSJQWWIGQk$D&MqoeHfhB|#2PEkxgpG-IJ(0OP1T=qN*d*o5W!LWfS< zBI>5rS4jnxj{dNIGa0JB>BM?N9?qFA#~X=H;t8%<$%P5o=65N4Ep6flCf@_v#*Wxq zh;Lv-#AiVp?dH)4iuZ`Lr!H)d+vFC#zkV00c8dxNcxIOhFCxBwg9`@0b94mo!Ph$v zfIQD$9!$TzdN=*?@ssHlc8J~w0C9=H5gy)f8yoJS#uq;waPeVe%pD=v0skBi+uQeDP;-yoD^;E_y<;=jfw(Y$%&NMVnJWNOb4 zuV1uLpPdxxn`%uU$@`ScLcJY$kT0>Fr^#swX3a@xpk%v7yGg2gLI#`A_|;yNias30 zC~WdfJOIk_Og0k+F?g3K@mmk;LS4C!M9%AKfTyXatcF#N=Yr1Qwq`I)MW`wIjdi>J zIOle#Z(3!x<#LHx1vXEAVb5Hcl^|VeNHz=WiUwy{)G>ov7Dt|$?&sF)Yb5htCT`3K zTP@*0efsB{2kOHk93AUf5I@Py(U4zI#m*vlKE%MZ17}3W$T#0Rji64=0tGzH5OFD< z0(cIvwGBTj{S4dJ{QTr4{$5Ydu|jPRm~=o#j7>UE@O_+P00~#2F%VPy3n*^==1ft* zdxsY@L%K(uZWp)`3kN!SGMh6BEpOb=pt-@o!&ccb4ja*eb`Wl}EYOU=({%uzte>^? z6+~UFFVoI*xw>+pFQX)b^T=b2ZCwr%wimB0*OTj_{qj{a%B0<_jQkGlhfxH|Bw{Wv zID9_u5^k0V;kHX-HSk!k`0>J`Mg4MFW7SF5K+~_AAQPMucfSpLoFPM_^{+LG^>Z&c z20l0A4@K_{w~fzo-_z^{M??IR!TP-jDd|VsnR-?{G)&WFY)5hKV)5g|a`*>bZVB%DqKlDWjNMJd21nnM8 zsYE2Kfu($g_XQr`c){IdFxkBVWcQ05*~tsoVNo}dgQ@Y5UP+a>ENYNwHm)g}Z~VL! z4C!Kok$DtP!d=EAMAGVrI>j^pF|a=T5B-qN&X)PXL+bp}Dbo&=JtSs3lh-BDE~Jvc zSvV+ryRk|?Jo&}GUH&AjcAA7?rCIg5(uU>D_Qo~~bHHl5OC(Cr!| zVdqH4nLNHLm_r+lnjJr@ASn`Ah|-xh>JlhB5$0<)Gm#{5&93L58@ddAdObmu&RO2P zjfp6tP(c4cNST^yZ}%6c+4$p7@EoUaA& zjLYX@!)9l4h7$e8-zD)EGC3Y$mvRKmg#lP0pm|%aL*l508+7uy&42&!0vmO{onE{< z1TClA2u`@raD{P7jTaH)47~7p5%PJ?ar-qiZrG@qv~smLBaaRwiYTOo3=};t>VeX} z$y_fRl5bwqQ|J(!f0&kpq z-Go@I>S4P^n@BT)4oooog7m@Plh)mJD80tW6a{C@)UL`9sJTR3l_jtI;ku7*H_NglUHhfd@RfUP((A zsU=9m5Z5k#Cw=3yoWVDQ<8&mBwUXSl-UgLLeRAw>Rd8B_Z9#Yr`k`~PLX#n<0-~sz zbbVxlO!K)tbNx^jdcY=~ClsM=p&$8&Y!pV*5!EzF(5o&c=K3)wJ1JF}Ge{sF@@42z zfevhA*4-sm#7zhdS<>pfk7Vnb$e0v+%Bo8b1~w4>&N4;xyzR7 zmt@a`A>r$9k@WLDKRp zn*ieLKI+P6F3;xbIKH6DKrrGT^A}%OwI6YPS;lRRUjyJ_k4CRvZ04Z!*_V^1Lo}MP zm^E$%u+Fgzr@htzFuMR2fDJ}AKfO4detmsBz1ZdF3*bCX52hn5k><0Sb!=a=hKoXP zWDsp?RH7*=QZhLB#*Z@U z9A$Io$hJiZNoF^G@W9FOnN!fQ2FGoS4+0*@p2<^PZqIP4rU^QpLnXJ8$Ht?wMv1K8 zAw_mR-41AzIf`o^$IuThL|X5u-BOfi1~wW4dYHA9Eh+&^60NXcKjA%0`L6 z2<>XqJY?l|OA#ar2lFlR+SXK(939svV~7Y*IxD*s21)QSyX)6v26NCxI?l|`buDds zW6pMtIwfbJuBCks!k8VH*&P=x8~*R={3IM z$?x5~e6=&}>>Z)(c-q5f2M$g*u>y_n+HeJ$mp-%3_;tqXivW^;$uO^9k3E9S6i5T* zL6|aI^ddky4|vGR8$vE%cCjzYSIs>$t9LQ*3`H4>6!~X2bP|~9SfTW5<<`}Lc=Loj z(v4r4a|dh3NeYK?(=7rV(ybGyTb3x6 zxo);E+8E{6{d~QE2iBq~2_!bDJDq6;b}Hi>kL+QI+3xd}S6cXvR8N`0H+R>iZM&_NZWm|jx!? zAxk{t>J!f?0qLBu?1qjy^eJN9Sh%o;_dZ7wvvE_Vx_#qE-Sdkxe(jNqZSHJ{oCiF1 z?4~piWRt$hkgSE9KxJleJ&m5UXGTz2`D8O@hhvev`F#mVY@;1gtBT^Kmm(-0?QPG^ zMCf*(0W2|*Ulq4hoLtwC2Rdf8ApO*ilu7+Sv?wq0WqBcsXUfa^MO#%yTQj2eD}saX zmW$-^A~9d^wDUs#Y}BbXEzmOwK_Sh3G_ED)_j*W|WaPG|Y+b)Ix?I@$xm9cFy#n`( z$`TJX81~~21~TNnAqN8Wkq*>jw>Ut<4u%K%hRzK?C4H}J9qpWW|q0RaAmRE zwxiWrXRs_6l9&N`#G~#!5FZXFfM+hpU@&-Ck48pO6_n39u^Q{q99W~v-Kn^)r%_ig zM}Ch(lG`Pr^Y&VZ3Nn4vZ4S3jRn6?tK zpVign1>MDRgQ}m=C-go1(Mvl7(69~y5x4V+z}>YY#E2+#iF1^H?3uuJ)iSRHc!ap> z!Erf-$!SCWcxBr0Q++Cc(_mVTta7}li3Hx)#AgEzro979jqUAC`v8{xeSiz%JD5V- zJ=zk8c({tKXmn{4Q2g5GF#zWnuyTTru$`(C+sz4N;5@g12WO3P*vW(xC<1RhFDwuGfxK2wf059fC_a-Cg;!Ju6`o+kdp9jr6|3gym4Lmf&(UIR*kyEB&s44W`14w9jG2+L^ zThsn7-ugd01pVQ3aBwgkf{*DpctLX)Zjlf6PlWKN$Y&YGDnKcYP}VUmMuSrxFK4<0 zl;F%bI|p(8F^()g`#I)V2fvC=@aItsIH_6#9?^=w-JS%aurhB{7@szeJQ&B9PNPwO zu=*w8!ZH==aE9~z&dfLToXyurC_=f-5h}~pitDKR4rD|gr5UO%muzvP=D6q*lR?An zAl(2FIy%c*$Is*Q~kJ@2SwgjwzJ(0gcF9 zNv{X=NmbwElEJ#>VFt*$K1@SdXZw_8t&f@*(fR69PX6`YNOW2*&lIB__{G*j*l7kl zNk3i$(bvJ_MxWL1+5t~jm^P-qBV)1OdI@N9fAF66-9OJKz3pjRUMSBOkE)nm2h49e z@#MNhcF66nhcMLA4=Xn1en zVWapL&N(V(cO4#b+KZE2bl!OT|8Q+O0O$~K4i9i9xFEiZX|x^0ckw)CAEO&?O+(kh zQ4puiI5Jt|Ga95%2oBhAGyWu;2MxRJka#JH7Uv@x9=-0-i!U-t;Dux`@|B?y$!1KK z3Ca;g(6vdYz@naZe12>Uq+!8G&dFHbWGxotn`d6;4xi^a6&ICL6x^JK>PH+oA-CT^ zx`52VD=8voZjy2;}1)q-|bE>U%vrRQQnsRPH+|@>%erH91?C|0tNw!iw3wu zns!NpG1UojSiT|LHS8XD5P2d5h0+VXcOGb17fWzmA?+&8u;4j2owGxC)(q{LsjSvY z5g{IJLE3!Qjn9wbG9_VXrjzqSVo*~K|FTYV zCv9hR7I>_O<%JANjkxsd24RU6>1{>M*$=Ien{-WS#2B<&anG{NT|#FWvH3aIV_Ufm zB8RNbwxmDilAMyfVD1FgH9s5C)61?auLCmm498&?SsX=CPAZ89{rLb8^i%*cXCR#W zkoA&gwA1o&ob^eTH;f^FV0rXA z0MZUdQ3SDLfER!Vcyx+y<$x4ZZ5S!7;j_|sX2YpC8I3msr}X%Hw)GvmI})Vvb0`;% z?hTE2UKR*|)Yu$>V{z^r6t0%_oJ&M}5iKM1wH;bowl0X4#gi|Pbn)w=Y|_AD$46e$ zMhjYbI^_#3Zf)~;;xkC?Y%MSH*ygMY)7gRXDzF}d!^2MAJR{FHaZ6KImF4TnWp0NV zd5jxcakgK5kaf)ZHTiO!j%Qt4X+O>MLj|KQ$g~Qb))D59f~<>j(?vtNK?&JeemlLxXQg*AZMTDWdG-$Z!WLf!#XCC~X_?W# zEqYz-)OL|{;OUjK?$9wZtydhCs|&{E;WwNgyp5Wb_KHKo@15rtNT#N3Y#p9iW4kF@ zwLJNPLVnb%3y$;%QZiak+7YLf_1I{;=h`MHd3Z0W2kvqrt-V=%wlVJ;s1Lq!C#VaRjNI9fl;$~Q0RZM#7`>Kghn zFUy|4bAI;atS=PW3Xh2)(<>6lky}pDq8^m1=^Y1*5E~w83;*s zrm&urlkG<>a(Wi;eP%g2qHdlC9poZoq*+4>aX%o-Qd)b-({e~HnTjyi_9e)OC$%@% zGmAGKidYP!vz%5>lgDd_%!(6!L|zh~=0K&tVnIQ_^K2x(b6_me)xi0BZ1C-_?f7#&YfpY2ReHr@7AsWh62LXPto2F+VVrRD4z)Z0uP96FM zscJfrv!fk0kZ4<$G!PM595lG=LWtGRlw1%5?M!jO zmhHmp;XF3ANfpe6MOMh8fF1{Fl+U@4&x%C2*rxf&BlD5m*H!DER2brh{Rx{CremLxq}A(u z<~5zw0jyUg{Y)0s+nSs~A#^3pVzAE6)iUY(+p3F7Ja}+lYD8V6Vm$t`tPcU3%%AJX z`JW^H`GDsGS`&bz{$`)IdxZw?dA=6(s5Yw56dm6C;dI;qGM~RanO?kkH$8g(5}ShV zV`0MymTbed;9EPJc>gDWfoCmd(5DV~ar4qcUF~L4^{fwnf}6I$LG?WW=wFSjfMxu5HZX4{Px@G?&?C1^668EAFJ$`M@OBzhkk4Qlbt+C zmTJhlS|#lya0pxFhw`~~QJ(W6zMig+Oc(NlE}sXT?Wr=^f$bs;+h&=EEfPTIhBmrk z+j*_ZSA&_i=c&idH))F%RCLkH)^vkIo-J~YdaZ9dlKx<@@NZBgww zudiQ6$fkrq`7n>D8_rp`X1|b@8t8X$P^YeQysxf{*!AtKYk21_zHo}A z&-~CFx6R>5hPBH_P=zKlu1V)x&a5dPnTUEmQp^XjUu)p`y%t5sEHz+U|Lji;On^hv z4lc}BK%%F35%nc@Ir!zbr_+tivgBY>wrtBHdZu0-4?yTJVTxfayrL58zO8D+aG~Tdm=3IoJluA@k@>~c;`jPz!Q0K&eNyr zt1y9;0-5ca#6jOPSC7PrMSWviFq5sKZhDT3&Y}JsMUQ&pdX@-O$NIQJ4E_T%EQgr> z=S!dW@C8#nlevqX2)INUDsbwoe@+_;#x0y}6#lJAzw%SA>&k`yj==vP00960k-~Rh z002M$Nkly3RRw>bYA|OR{ELvgJXxWqHIlwlNr67zZ4Tffw75kOac? zg^!Rg--G0Z@8x?RAukDINZ^H-7YKo1;DKiw<4HEQWmz8Nwl$C4YIUpU;ZEnA_y23v z+Esg>eeS)I`bypRtm?jJ*Ra;ARcr6P{#~_eS9OL6a*hU9z0>Kmw}4F>;IG>i0ZAWJ zY%EfGJk=k{fg`8ofrpRC-b1J5*n-H(<*qD&qL05Um-4L+HI_yP$ikoS&x6w$t>;ot z{(CH;!}?OY>e7-XRT<dD4Ky`){9qF0&P^;E6NrU=n zZx3Bn!t_#|yr6OHmI4Q~vA3hHtFfU>b<1s}YB+xjP;BLm6ik+rb~5D7vnR!M2M`Fbg)`VB9l|5&5&cD(+RLBk6KYw0pK+d%Z@Vxp0lC9+g0oM>)7RO4;r5Tvw`V zbvuvIGY@i5iT$&BNYm-yDog4vFE2^2+mp?kHp$G)jLgl>$R@<6XIz@``LA7TiCHrt z9r8T5fAjR2K8~}+nXaczUI&dUTi!-<*5MMdKr|!6?hGIX7sYT1)NV3Anu&N(RzYt%OR?US zfv4be0mv%ll@uDU;$) zpYgl%HVVt59%=_uXyjMbOFB}OtSu5{8U5C)Eb4|2oeh_nBr!MPNJ+y!^I}c2fhdCa~>#xd76j=tr%H)tnqbkJY z>S@C9U8zHu)K#lr{(Ey;W-! zsPNz0$8*NEmo3&RwU=A3i@>PPGc~mA2`W$esoOG_Xq`pfG+0@0ZaazaSIJ|1Ij9mJWj9%zm94K|*+gvwQlyVDJZ&O)^7w0_k*g5qw9<5eB1Px_&c2>4**`hrg#;2yIr3by( zQ(x&pPUWdWw9(4IqqH0JXX67;y5D&IwAaxg=q%$RLV?~O=Awppa?x8&v+)BU;yrT& zJ)wighv)^JkiCbG%YBDW$fCM902X>f!y))Fe8H*_yPnd_#fL{yVuI%`w%oszl~x`*`bFu$$ZgoMOGCV$}$9y?4Nj z5*$_no?2aWoXMF{#yH@i4z7jPTkzDBpb!moF#O}M*|2noPUT0lMY5@9;%ESu_)&5F z92A6w{g_+UrFxqPwX-**q%ld^S(WFess#*e4W7kwR>Q%y@=>2cakXDjd0AEo)(EUu z@mbwop7SA_nTby0Ojhk>c~0cBi2U;6NrEX?&l}IL#(9moRNQ-vYm)jPnnaVmB1^LL zkjNdBtjO%_^sJG+n(65&*|B|_Y~8vQpfgpIr}ZRRT0I2mC0Nw&_1Qg}&!yE#{c?}T z^ETpfV*?LQxSw=f3Fx38$&*ds(Sb7@d5JJ(N5(q?i}M0rLU(x)f73HvnFr;5NVxs} zMS1X{{c`O1qAd4&$eBWK2{*dlB$R{;UaweGj;qoCfn9^DgmT^%3OXe7!s6%`J+sPk z%l8~gQ;7wqM=6+3zu5_~#B_5SlGJ6mJpgb-R)s%GQP+jJ* zSV>3yx~e0}Agi=X$*0w%!Ws|RMjV*i%$!qcLRYtx&3JWA^~~i`(!1ki_2cx*92TpA zu&YB^4FPq1^0_iPmX^%P*%oJt<*pxVZpvpKLzz#hrOC=NB`$u-vhtd!=hHC8ln#{y zUY%8*l2xge^JaP~1&3f^EB>?V#cZSX8Uad>S3KD1`AIdar3yOS#g$HGQ?n~aH#TqC zEW38?!h4%(fC;Wg7>D*DyNXDhWt>@!TPqR!`7(VID-LK$<6g~W8!NW4frsac=eD>S z?3CAU#2P_KoY>vx@%kl*5VjT}=)S`Px&6Kaa^Jqga%wm&r|>Rl8W+wK=z6S^!!lhA z?cs&%5IwD-f)Jmp@TA0gRG`L8Pq26>ZxGM(qxmIP;F?>jth|g{j?yW)@#I{K5dg{( zB&$$!*f2zz1}jFxs)P4PQXJO)Wj8uE{5NGcz@gP5Ae4vb91w@eaCj$zKy+`EdUzV zE+0#ac$`OXn&)MXh`9weKJa);pTN;x^6}>7g^bAs02fX;AOPVNkQ_WVkb53FD*F%3 z>${=(B?8V2=+JG(-*Bog159M#g-1}3dq&1Stl&2O1&^E*Lo{wi@MZqC9l(& z6Jm<`mG4@NeT;oraRLr?dLScZ_BDFeOelr(EV2R*=^c+I!B`eKN%N@s6_h0pTT#hx z$gA=pP~*x-ZR?@3IkM~L4NvNI(kf*niP1n8#$hey{}fryKy!G%k`^%tV{U`kd5C@DeUiyL2(x01*y4^%nt2CkAr&k&|-& zv3c2hbXiWE>R>VgI`Fdt8BBo+&aKPg0iANHw+%1|mp!h~^iWo5!;5y@g{wD}-B}~R zQ%wi1BtcH1WmXw?lIn(KIWtFYRHvi^&V?Cl8odiAPsg%O8^O*poK&5Xw~3hv2pBa0 zA?Q=T@s3jgsgl7uay1XsTt{F+NF=-ayHyh(S}c5=^0cX z%kA>!MO!+i@)pp0$8R8zmlt>|@Dz`@Dw7OW4I&Iw#$x<6dCcGwWoE98-&b@-Z_c0) zLO}@HjzYn@V=w4IQ?^$ff)~Rsx%Wcgp+GOxYV?Pi7FE?~)U?V|=d06a4YWNVt(-RX zbDJkQbzakh4;O0*tf_8`%w^TMbhNz^iFiI8s*h7O?NK`)g-)c;?Z|a9#(J|nI`dpU zj>q_nu(DNRBTpe>Y`toSM94$QRmms>Rj0x0!O?W(34@KQd`y1f!vq*OF`x$`K12@i zUDyzR3No0PHa9mTGgDJY&*_j_Dc@x!k&o%{M0g)lMdu)K0MD9F8t;DpBj`Brc-}OI z-=<`WNnG!X03UphDyQ*x=cDuTwR`r;-u(w;YHqW1r#FEcGvb8L|G4d{D;>x*vAYKIPouv%2pq5r=rDe{D9$ZRmos6p<;4SJY9SgN9d?4t`l*@$})L)j2 zlSd9O)8?r{uB0C+5A|wYZvR{t-=5WNmdnhnkdy3Nl`yMHWF44Qw?$dTdJ3I#w2(9! zE$d8si3jxa=CThSsEaKBk14@(dO72LD7n z$Y&JsJXQNdL&c?LRN7v`&3&vuZ}+DLe5|u=ADP)<`R~9wjc$7y&Jy}>;d*S>QJ+B& zfA@BNA5vBtQ>*n?tmz3yFEKhSN~0e$YXtt?U3zJs+SZpZVk%G`N8zxJp{cRP$xn~> z6TVOsIE~T*o=-sC5Mk7b<&xLn5x)BIDXL4nxAJ|5)y*tTb`(B|)=~%No&B<=+8%mO ze$mul`QIWNQ#a=f)y&l?n#T6{GorzB)3;+y4#GWS>>lj&-lFNn_?yNJ`~LMKZa>BK ztSuBgsMW81$VJm6lOm$^5?)zt#f&lQ8}l@}N}DOBq_Zu%_Mhm0b`caBJ?3ryfsjkX zd1>cAv)+jE;N6_&fP6f$uElrN_~9>9`@RnQ$p#bp@oGLo-ANhH3|$7F9x>}9up~r6 z0oSL|!8XoDKrxzF9ouL}gacbkdpG(%{%3`8bg16V>b=rWsc;Tzzgz#`npB*fqLle$ zWT89YbBi14)gUm_gJ_4zy|}M({0HBrSP=hhnrvs#4@KslZYG7lExw!E|78-Y;jq(( zq#fkNY{~h4X(B81l=gG4sdxrzq|rO)*NSGLu4u2c5WNG9MNJ?2g> z%4%sVw^7$T(rPwr)2ZF@O_x=`YL8mfW^BlsAZQGri1qAYKlZ=#H4G3d?%ps%Pb;mOpuoBjmB81Dz^Q>4jdOTZ5iwOS#iveL8+0pc)Ka8o z^084OaOc_~e{lU<`P-ZEnqvro;wR0XQKeXp_y`pWe|9_cby9V;&9T~H_$8ZuJr-W{ zbaG0Ww6^khyPBfjS_b5omMrkcv~sGtjP^Dwgn^r6;3fmmZDPl2qFquYOiAUak^kLI z^d%uLtzNW=4r_a70k@pclK3JxZC32489B6ah5XOaBd?zjJm$~Kv?A$(7Pge5@y-fg z1gd4+ltpqS<3;6*7cN7{w03>_hAs{zW@cRr;V9BT zx^~zn+fEMSZkExo%G&PdBznZ;tX>Z`->=LwM!{3OBkwID$vCW=Q7@c49k-|t&Q_v` z?RbQX=K#$1RON^gWxh#dYsTD(qSX~NhM^2|fG;I%S&!Z@(-wUQ`gL6d#3fb!5_OXO zC>|QH57jf)N!D_Ud|(cQO63z?L6z@yhlsSB6BWFg`M^OJb*|6(^{ZK~*9p zmZS#pFB6gU@DG_n-l(>`uhqh4vrydhW-=OWM)ZRt!tPrPKV=Dekeu^Wy{u0NvTd_f z8PCD{=+R;m+U}=|G2zNmZ<03TDAv(aPqQi#XSMY-)nUw4lna|#%R>ahF z($eM6K<$b;X@6N-`-{H0R#h&JXh+T0+wa@(BwJ~2ksn~foL$=-<*|_G3)`L7IGS~B zP0_$dK=m|OA7XDq>-rG>@he03xxB9QVHVa@{0Jlkb(~q4JdZ9UZ%6GnWSs(&u)zM` zB7DU6FWmD(22}nx?r67ujTGy(Ug9)f5`QYlkYUDt@~^{;tPS!yj6BhejJXI?bD-{h zFh(X_iYx!Go^7l_#DuT9e zo0O3;L54+1E5C4hTe~V$cBT@6g8w_o=j*b*DJ@}xxm3Wb6JEYt6NLpQi$tQtebBm_}RloD_P1T`+GV%--$kka{z<#zANV7Vm3nCcxF`;4QA+ zepCpA&vPKP1Qxa+b|ls78wlw|)A7^}YoGRiz?qJxWy2n5R!x^$`+5jGDvhT!&N!yP z{!NEa=hqwjM?T(yV0{C5H~WQvO9azI^BaL!0Yx8Q!&BZ-nSu_#vkW2yTA3CZzIO(j z6Y8cv1#i_YlnISH3jbN<-2VxMQFq*FGb$?a+OTi7T(Pw$MJgh>G(A4U@v>!X0xV>u zx*9SHQ(METsgD7|K6cAISX`UclvCL-Y}>Q+op&zn%AuuX?yV6?r|SPCGjAoE^sBn^ z+XOSnX6qMh-d)<~9O^y4YG8j+4MHJ}L7dw74t#P1cmBBc;@quj7%e5;xP4_!4EJgN zkyy+b(zDV8chGZ6;JOgc+EC)J7Q^cnbkJzqWD}|ZcE`Esbq$_MJ-FdnkJDCeIybdg z4PN89z5#(~Yf{x=;9Z(Ve-@akJ?;ES$DaHr0=k7-EEV zn*J)UYN~CBToC3XY{b?M&(K}@t$BDdGX(i-V!f@5;%KR8;KPp7Z8qyx)!w8zX34(j zN-#@@!wwfQ&B2T5M0|mWMfnD=$=E=!CvPg3+bHePgHF-18TU8_UOkMIE@rNDj&?#I$s*QS6gT^Hy=^eDzb&h z@G5HNGMm|V8xlQk$-e1T$D9dBsV=yIt#e|IrP+k?!#PgacSIvVT~K|luD}6l0|Fg_ z(X6>AOI8*Uffmh|E0WoPO!L)4J|C67P6IDOgi zs&uu@rN|9becLF`Y*W8_z4;eG?u0; ze|YX`&2t`QE6>Z<+*32CG%2%*I>P;Ir>W#>puLK;+!AY`uz%ebq5opQ_ z1m3Pt^UkbvYH%+Asq#RjaL2zdzoY*3Zy1nZ%DPb)2ajd!{N#*r>uw-N?Yk89*p4)8 z@rw|BjV}K!hUxiqx|+H^9H?snkKoIrv}$pr8FG^Xru{=lGR7uE)AtWtw3}yyQ|f&; zo)=9-OWk6q?(dJ<(p?rMBH4+>TMvwd1h>*PbD6t;tFIhQhlC~s>n13|M#%HS3G`tR zR=^Fb5%_T$Gb*fo{t`UaIHd96EgC<`bwE08PA`pSVUM0VwFWLWM?1UH@r#pS*3g~C zxV8UYIR9G8$%I>0<6L92gNo=(=>h;~kLIM1J^&B7a=U;cGS3 zdQD$2<@`45Mk|9{(CD!m1xx7z%kqgTV!^vk5W{N`m_ANV=$NiTq@lUm0q>pHa38#Z zx5w<^4n17W1J3&?fA<;R{uqc8XPHL!(PZ*{@O-tN4f*)8@5V2L5CTM(VskDBA+&<0 zU+|2(_Yqj*#f+-vhqbiLxsHd21B=l8z5NO_-2%RcU=)^izi|CaEZaDJLmvI0pM~BZ zL3BatklVTE#`DO@PRr;L_*N2ko@{AFGAifeizeQDDzxDoD=kYU5oR8~vJwFwj^04< z)6d$|0YM)9DpUS1LbrH;R3U((zS*4tq4vF6N?(nkPi!Bi=+7Nm^y7yJEEV+!tJVA_e7ptU}g(5~6m(#t2Ps+L>mYO$7WyQsDB2CX{WWIi& zAa0;zp`1Y_}+p8VW-#hNMm@*ocXhkYJ4IHcv7H!JJ~m%fSmn`&jPHhXQL z9SmH^xkQEuYqcPoxkssYZH=+D=YK!T)eh>5vdmUJj0skhY<8|p(mR_1RaK}fIe499 zDTJ@p@wL=6y0PtM=l1QoEeCwXkg(~SgBAgd_At=!!n(L$T0{{=z+60gp+uAxilrYrKV1FS2~eOu>tzNKa$aJA zfXluazv2$o77U4XEE`mTJy)^^bGYy6_oA`n>R3lCEATxpzZ=%EcW!pu7qL$F`o@|! zBOFTy^!dCThU_qLO28;2YVGpbKX;i!%Gn+cbf+g&v6aE33O2QyEF!alJn=}4Kik!W z_gtTono{yTDyjf;#}PO($Mic-cAmkXO9NBP?jg_F>%Jl)U52r#71HfCZ;Kas#~a?8fQXwVUZJenp9_nSEo;GzkAaxREg;Z;lst1;-e0YxKbNEfcpG3B7OGTYAM zWb{4mp6SftIf{8Ag&oX3G}FXP5rLYCH{hYh;#m?*o=_xs=?xvO-5u_o!}aVHD5#Hs zdo#WbepL7J8Bye@&8Ex&6ExX{=3B_0tfAUeoJTjv)a2f;#uC@u$M?&3(K2TF!K`+-WS`}#gDPsd>wE;qhIZ6rfan7?|c#xAS)n%7_EvWL*3KQVL{5XFg8BhZs)b!iMp<#8NqPd~mTY zX(C-W=az1yRQy{E*#~&$2yKFmW38++uQjPb?cuu_Eg0Vo$>u>hr$T2BJQ#u+s6k-ieU*C{fb!KpSTu!FmICADTEUJd^6#d#pi32eP}%sBWp_tv zGbw;_0j&83X-tp>dr}OKmJ4jXRjfDvEg&0=2gl^rKF(lD)9x?A2%NqQ0HI^a25v9t zHtrv*DVxNcZJ^5HIwp}iQ}zR%V9&2A>3-_X1-%VC~NIe_`LK5_+ENtIO$KywUQhIVtarY7!i_@1g;4)y*;TXiZ9nB~CJ| z&TjJ-^dZg>@;0=Ob#BkFZjcf5;fuhwc^vq zN&Ar+rf=yHP2xj0Rq!4POciiVqp2fMAgu$5Oou4<;iv;cCopEJ?Y zt|3}VNKe3?lPCuyidKsgS^8*Xgkmc?6JPIH$mvR-tww*QPrz6zmtfp$6aBX(D`0DDtNyI} zub;_!lTN_zI!Ct;;t0LlMMC5nnu|&GF5_N;m78po-+O*>HwzxrOe+uxfE3Lq9N+3x zXLloSGn*t~rZby%^=OgXsVOs^dA*Pd%@4@jV^^zS{DFy{uYrC67HB+h^dK772#96$P%fa;1%2?AEp zr0sk-M=Aiu3%bTS>o7+=^GR=rv(?ViMQm8Q3pW+JRrgOFvHhw_qG=W=H)=nbx+Zh* zy|1V%shH;ifwStLlUUk^4watxYm{)Op~5})<4Tv}2cP{>Sel3_zt};Q6ll^tDo>-~ zdP9j%hzMI;GpR-)lB)`Df^UmViXqi8`h{sscFmG7fNbE@s^}*BdGMK&8KzN)b7h>4 zm>ZFMgT(6|@@^e`jGIGbWpz`*H_TsIj#z7ndw`8a6`SPT@{O^^7S+-6O~o*tcJi^b zP&mS#58JYe(7MySOXuAxTcELGV1=J}zgGPBFD)$d_5TTg3mwug zMw7b;0F!DIzOyt&#iZZVUggGE$ls?oCN~K88g*I(pofKbGpj2gyY86*FbYJcjh!MwdM8UFQz*oZ-cwCf* z1WXBsCEu|rNpd0`ql{S;)q_>OF2f89x|)wd2M||BEn0^pSEP4F#T_Zu+cOHve%Ma! z%--FLl+mY;6sW`{A;Fm-dL8M1J1E=v@`HEt&QubiX5x=c{TBdW(Z{swW4`&KUMH_2 z%Ly}m!U(I0EC-~<$vap9Q5nK!ScNKuf4Zl2%Qstm$4cJ(ae-3Xv!SPLiv&PANOG)YLn|qo{K%4|KyqjJlN!{x*aVQ>`}6fp6Z|8vz?2t)BboaW}21Te4Q85ze zW~e>-9+Ac6e!b^q& zvHXyT_|bgRRotx6o)hsljJHQI`8{-#ew}d5wI*Oy=>he(czCm%opI$m-Hn55g;mSS zZ|uO@#!h=_2Ah>=96`QjxoMfDX&B3z%a5v7S_M4c`IFUm$~oC#i#IoAenA$Ri&R&B zSyeUX&32t)WaS78dXF&yDhu*1Xoh@MR49Ilm=Gd%%H8EKLK}q=OZH`KRmTsOO*6_Q z3=@sk%6Er3d$c)Lgvwd5O}E7l1^aR2H=4dso_1_b>eGlZzuQ=53QwG8T6}i=kNE&W zyt=3G4}iV8q4M3K_VXm%o9v`^p%;{AG{pK8MENRA0io$et1v_O3_b2b9QU=gUgS!F|d`x*e5Km)EV=u1`FXRttzLj0;&<1 zto=)dumLenv^Xi{5XZ`qv_NJXBKE_MT{YsYGq2Ij65@+}CB>GKit1h!I;P0u(<6Vq zGFam;lHYQke@5B^47#+MOZTz8QxIM$oXmprrqYfIMPH65c2}1TnRq(1P`FO_5`X@6 zQ_fV!F-v|IpGV3%i0l&F(fa8lqEpd)?o!^gK1IJ z)TrxoAS~OxA$tV-6dMhH`ik>0m+swumFHF|mbE=rv>~esV2eMS2SnG-eLKPaPjwM{ zD{~Bp^zro`!iJV9A^qS_tO(;AyFA9_){b$K{bhPc;LZh`2?{Na<_uea6SBg7cK1R! zvEEsG9YDRrm-o)Tj5*iw+W)_s3i%ee8Lg)(92LE<`g*DMNli5fAJYhEaCGz(wsneR z#%{_DRfG@AK}!_&GG#}F%iRqfOgt-g7HE`ym<@4*U%UAy+SlJ(zqt4jl42LtiE9u3 z#|pR+Q>%RM#I3L)s9f?MCNW&MER*JLD2e5t#JT0?bBepjh(}?m0v@=A9FbsBg5|w} z>6CD0_6<8e(M>$1)a*s2IqcWOU|8`>Z%=y6gw%hZPYG$cYPo4WQu$aqG)==iv}-Be z#y+MhhEQy~lrDr<4I!z1{h?tUd>|#%5m~uJ6u*Oa;sz-@DiN!d_J%J1CMG+X zir~9+PL(XEgrG6@)PNl3qnyFjk*jb0dwRl8B|x7LOm|HahG-K4lh=W{pwN;T@|K60gjEj~YNrs4 zy9hW>ivUpJUIcU?!k1eB%s#`x=Ms>|W7gWz4QGz3?di>Ni_YJWj8TQ!UGz!pT`V;X#5wq3tGmc98K%eY>&a6$5_^jJt9pv zF&Qo-%INEsy|%x?F;gVXGNk}?`QD%T-3BPtY~$ch^A$qVRXyw)mvpbn;fxiscJ8Ep zsmq64RUt-lvLy^`rE&M(%K6dKv+W$^r#>kI#Z>x`iJmv|*sP1Oh43JQ-3_BHh*(xj-C_t01-@7}iDljni?vU4xeb=2kdKpes zQNetCLfwHCk7eU^&R=h)2v*|ky+oUg3+?n9;FK!Ntf}glt`yXIt9`r69k4G%Hv8z9^k~?IF z8Z?%#bjRQCz4~E}zxrW87EY~h#9WM}Pmg8SUCWP0sk!yAwm#SfoY&|dla1NIB%l%% zR)I1$9oboLqBk)+)l=3CaIL=rwYQaume53hp;lO0k~VHzrWWzak_yMcQw+x-kWBiv zm?`AsVznhhzqspNU3Qa}U5znbcde9H_PZ!gt%=jtzowDyj-y+F>EZkrQ~RpT5v0nq zcEZX(&5ToL8&p+0aO#b>5l`@w8VNh}+M z@Rdca_FMyDdiWauxP%}{*uURkMk2P>iG2bqbu-KFd{bG4_BAA7tVV zu363`qrY<|;1#U>YwDJ|Kt!U&oLN;4;74iqUynSyjayd_hO;?R=Emv52*k)@0tUe; z;_T9qXZtLT-dYF#T^QnMS!a#vU7ErRR|8O{Y-o<|rg8>l8u~q{raCxZ8^Y{$sv%Qw zTJ-7BFOozwNd6;X`P^lST3|LJyVJPyT~BMM&@;rkgUpzM+$X+V-yu_`s;qgw|2YAf z5%-dM3ybGg?~JC{c_@sbR@HhPw|{`F;g&*qigO}~jrf32=b zQjMf2Nvk}si@3NquEw&L?Mukwx1*8a7Cx8=4g9&PTul)b!YX^MmLNpoucl|KFl0!A z@+GI+WUe%WWk-2z964#ZJTcd20;19!n*p$%Si=;;(Zakbq%nqdQ3s81BNN>FvMA^a z?+T^aE?=@_1AFc`ez!q{nMX1{+UNS;{dy2gwE^Hr^CnrfxAl5lhf6QyGYhMNns(Qx z@);wgHME?9JBZf@WAYr%$4QlVG&ji^oQKlH%3kbiAt^loJyvF0z8CPkFmPpkfG4>P2=~o)g z_Bg%EM0adOs_0$pcpkKP-d_SIAt>lIcJKNFRKY`F)eDAciR`C6gnON2f0%H@%1wmj zPc`^n<`xr!etZFblp}=u(p>a+KGRXJs#69Aaci$c&D|cHn7RW-qljz%IoknGRIO=z zsZ#78ye#EroE&Wmz6h23 zGRAaRF^59zCl2D+{-lb0vDv{Qg&Z^we+%9ZzaG&s;Y`n!pnVp%mQl3~L(J=V_qur< z9~d>^#DqiOFk_%!oY(fCZaU&VYp_qEZQ6&9LRiw0VnPAODYs>(XTe?4MjIXAU@P%Z z%!A2RGlg!=vSRiec8yf6s;u-EsGo z5LUAlj=9+e)2+m^D=#*!+y2QICW6AOM=x`P{Wrdcy|%wLGmX?mpUmI`k`Ft`Ty)@E=4eju!3) zOn}vN)v3g?&$VA*kE%8i5MtZvMru&^^l(AFbs8E@xUC#~oRvfZ(8Eim=@hq5S?80A zUPobu4V|1@PgR6tpp4%An1FHY{15I$kxnkAd<_JO21=w-1k`LZ{}e;JS!Q36z=hCy z3K=jAlMlOzJ0s;N5IeEt zFrp6>A7~p)rrG;x;wl1ZDF}3%-&JJEp^C~ue;_xplP)bw#Qg1TUit~Lj5U#d;UlAJ zMmW!r*Jf>9#$^$$T6@$sGY^|Vl((YkqoA%DW%X9!ZXE~pVqm}({A5+AeVFN6GtvBP zj}RB8I44{q>A|hg5PURe+Bg?F+S7iruvErIt+^*U;&QXyjyG;N!9&hv5mx~3=dRQy z?#l^3gR@taqu0CLfo7zEv69kUDy1)dXDIA0gGubOE>!s+ z)2gh?Wx4P^oLUU)uP5FGmiXgWssFsG z&A8X{L0~Buu$KkwP?75AiO_K~Qf44B-5x+){UyY_^-@I$!6!VQsa)XE4N}=!11csj z{}ttlHw$5OdRGrdr=i4|hJm@c`Uy8`&$Pw3Qr%w z!2v@MH6CV)4{$mHvsLZ~#$D55;kM^xrB_6ixm-uqXMAl~3T;g_on8Uth^(FZuq9ed z!O|RI*(AluYcdn8grhbJfZwaX*T~``KkM{@i+Y+=-|yAHfL%2RY@<*vhPch?9+ht1LSSTB{+9A zbU3FhV8}u&Z#+sEzYn!4^jcW92{=;@@LwoeGMdur)WC?l{@R~69 z%GMv-I+@Ws79rj}ZWk?mU;v?t-j*F}4GvyJ^3K3pOG5Ngt>p-E9jTWr(Ge`0|x0r>uXBNWuE~NEj8QS+&gW$TDAcT~7r_zB1obCMH zoeck7biZA1tP4``65iP1BlOmrDO6aJ@T)($Cw%T#tzc;+R!&+jy5tfx+06Y#G-gBh z0H~5wBgX(8z5|DPNJLao>#SvVQ>!CKq5+vawN4p$`}Gl zf{zxlvq~SdoCn@y6MkaCR^LAkA)9=ENNFRelasAX$=zEPA_4t(e3|J+pq9; zPMXIlVmdd;GebZbiFu`h)pw&Zu#@%AJa+$K;8>&k0vvjW{l0R$G~@GiALj<6i4U~m zd~1GrGojlp3q2sCB{%edWPqwP^ilefnmy=p`S5I^OaWK*KT%9?t@tIoB14AZ1k@@* zUqbM8-44;5ej988tli>RwK^~EDXV@1Dr#yI#BzOYSnys7F7cs(dTU|McC{CzzxgMN zbk`@{>@%|5Vh73~HhPy^#Y;C!C^sTr`v zfkh_F5u=;+HG7PU>AlA~Qfy*c=J{%p$(^y9xtl_;rY~tY(05`FnNcwy$^uQuXgaUv+kn(z0c49AgrU40%VmzwH&TphgK1(o&2!D&?8quRfMR^}6SWo14^q)0qXGRMwG&4_v z+1dBVea~ZhlX;%Vc)s)+Qr|Nr6ZM|<0{Z^NZT{H+%Q#{g=s{4fx! zvVc5_1S;4MA9GKquchI+RNg#BWQ$!|GBL4OniqYn`id%yBr5I5xOqagan?A>Q*1!K zei^H@>u2HT8D2*DJEe~y6F!psOE1MF-2*`h!-Rgh)_#|5k5o4 zVYJiWb;oxjYjOs&-1?D~9f8(vKbTfvhtXKlic}|WcvH+cE2S4k&`GCsNioZ=+Vy8`9A+@^O?k;!eO5_r z(B%~M80yVsEg`9y)skBrJEVebg^tWoCc;|O-ftU^}alPca$*h2f z58f`c76fZOhOWkdzP}@ckUayD&{Wr!hhhWw1Gy?Ery$Mmt=GjzB>iR~cmLkdeD(Nu z+w>(FF?j!w`iaMQ`)>JJ8o!;QKS1(cAEzv>Vy?YDiNp*UgKK-+K;60lTTU3$UQ2Xs;N=W5g`b?CL0dw?J9Ros*Ns|4B8KK&D?v0gKfml)J<2$to6(MHck8o-(Cgyn!IlNgR!dAbX3CR+?G2A(f$(0ALuy8t00{^b~@o>02+DL zP*QQA^tFrp^6%K6h28TK0m^MZ*(VYLwpjOmspzJ8G*Hbq>&7}lrorW0h_FgHwtR== zt&2c-C3=U_=${P(ng8gj%oZz%{vzDh3q(g%TW9KCliXW9FboW%(Z8WYM>{VmBJm$~ zvBbgE>wf;Qeq#g0#{%~y0u+9CaOu;T&XBnzR(bNp(1x_Vbai%B2_KYknRHVS-B*ms zOR(o$X>3k53lrA3-5;%1@t^x{xY9{2R)qvI9VnRP-%e5vLBYy@Q~p4+jpKVg_{aEr z4vtKv1^Oh8F@!?0(D$Jh%)s%G@=7xvBNno&hJojQ8DhXGi3VjgBGe{IBzLn16D{|A zdW`*Le=k9zSw5a3f)?@76pDh7foRQC9K7QWn*lQ2#m7w~>80cRt}csbo6vUeeGBT- zjHN)$Dt)0~Tw|GxOx~hxwElL}i?mjo`av4gPgr+D>Um`tUz1CCXa{Duu;=;Neh=As zC63skA;#0TBI+(>AkiEz@g{?kn(b2SoghA_YzbU&Byh=}C=M*<9uQ8`bVE4;`0o_> zKPqjdJZ%zmsQPLPtNfX@Kk*WeeBre}lgs!|_!lF(x30xxs-K@>Dwx;BmU)5gZDFFz zsAYKqtc!W!`Mf?4(+UfA>TttkRh7`>P)uFB$JQopCbJvY1@)I`zQZx#>p!1?RxO|F zfj-b#wf43B2gl@p&?E8-uyC{H^<`AypJl)%%a$k@Ehfqdj(iC`R2+~#U&EDc-+{s4 z>2YXF3{APqMFGX0T@BDL2lQNRxA%?~{{r$>%Z`j$vAR!}oifIyJL{rxv?WM8Nc*?L zUvCf^F=%*==?cFV<~RT6B`yr|`SI%M|`kCX9EzVAYpwWKh3NoS|ld zQ(9h9eWO`&f7y8W8+U8%HRew_@w5{Fu#WuApgL#_hkfNofUu>@(O(Yx*g%zF%f&~M zV#B-Uo`vpV7dZCiSWIC?57oQsvn&oWjN8Fml!A1YePgL442|hem1za%-yQ3Z$An|{ zdEE+^xlF;W4DBRc;^Q7kMENES~>VtMM`uER% z@Aa&~dE#E@S<^!HaKW1Gt@mX~e`H}fY~MB4wBhO`13|;GbO>u0Fq#}83kMH5hcJre zIEev%t_{tIMNJr)fI(4C$t#AxF<>`e`eJ`I{WQg^=gOzaog^lxO3*0`IcKR5LMMUw zF3QS3QLe@^@fML>DiF}#hH}_>yWt!fO) zH<%0&4M9$cMkpO-iW|_}ZylYWKI>jx+{W8J!xD6d87!51 zKikPP*oUsizqf$%yO_L&!ug8k`V9UwBQ~w&&YqGz#^J~C)~`j@L!&T;2v?~oX>|%U z=?hubBc5rQE`>VVXmK~?m~8#BsYk{}v^@GrndTMvamw1s7-i5k z>9I#TIFsCoLVjAm(+NKdWT(MgGa3RuN*U8D(IL;G<*8=%t_lC6q@pj)(~W?@sk!PP z_BM0xgmuyE%hTlYC%9)n+)&Xte>S9l0itu%7Fi{q0bA74#VXRaQ=4IMj1Vb;v3&W~%R)vey;u-IZuz5c|F|p0#dY=7okmH@fiJKE%KWAds__vf zp(#pZpKP3w)sacX+Jt|bH%T`8W2n{YIYa!4#PbGAU@6 zbYUXqIcvf{LT%eXIdcH5L030g3AP5ge^s-yYEt(LdZxs|7B`!7ejk*Z`(o@n16&f; zJ;Er{%tK*AAG|R{4!@4xd8_f??`Yt+UQeT82A=hj56GdzsCi*Fs{jCF-@k_%w%8hP z-i05Gq1X2iW$V9e3d4%B<`*!Dt^L#Mw*0~wtX?vosh7 zqBKDVz-7oo|D*pAtfA20y45RvZ)=jQqs1+8WsfN_b4=`e%47QPKnEe}!??}C{lkU* zyV(@?C~1ujQ>d1Io{`VrUksnHm!^L_h;}71z(iljK~p-T-Nj18lRTpiOASQ%ydN)I zk?~h)1W?JAX<2lbsV z`|ui%#$f3%;q_b0@C)btE7ysG`@45<=wB`Bn^o#h7-ozm-nMI^U)MPotrrgk#yLOV)FmLyEp;HjS!r zYPE)L?j69k{sGuuA+!(Z=qb3!X)qiSYWIT%3F9WOXT6754c5USLIFPCrt@#ss>#YHtlvd9TmA^`kP`|MC0En~H`~K$Hed*sJS^a9fo@0vHIR$F& z3{YuI!bH!pe^1hcmZw(2PEH1fjL`*R4T6XIa&bS1`{D7Aunex{4BpHQSkZ`dqaDvRnZXq08wmhZS%l z!j4F>@%#b$V{qi%N4T%O@9W_HDf_=Oa(@(3r3_zx4APPShv|<#TPh~0I}?v>$OA*Q zxZy*-38Ucy7+}HOC1JXU2!oFo*DP@6F`Vh11A0sm?Db5h?Q9QmpNu|Yf5)Ku=nT9u`Mb0&S-&mko4t| z%8t=gPtRT}XBugKuYxCrsu?hR($tEy=W7bSv}4QXu>U_#^fOlUThPU4oj2%^NpkkN zAV=?H_b2dhu277rH{8zxo_csGDIlvN+nxD_UA7qGglfHw9d9uz{2+#v?jKxSq=sGI z283!(s6h##AvdeURc-pjIk||Y#3+mDR;5b9tS}8Dn~T#wI%HiCPzZ`aR6I1@I?(E; z^`khS=>PjyoS{7Yv7%*&Li%IYz|Lk5o>n4mc1}r}%8yD0reG@-h{eE;+hg3v{&C`_ z_Ai;MD`5No_FUWzamb3_-AmW2h1%FjWAa<7D&%r-mA@m z^GPOuSC)5g{3w3Zv~Oh4Tg?=*64|VWuZA0PkE+w7SAL0vQ&w+;j}66S1;i%u^Z~ZN zlMcX7cAj7dbq~?|TrGC*k_+@nw(ac9POjLXWXH6HbFJ34BoPw`8vHMy#;PZAW^ zGvgRT^O4lnsLGjoTmTy4+bk*R061?gfv_EifOp5(W<3V&|#5mUq| zHVUZi(jG2+_2BARI)h^_bhm!%_!C3v(=2NJ!0Smoj3E#iO#x{0`oW*~9<=dq3gtow zm@iG61^cj8-|Lzr`i?N4W(X6^%!xLO2QnWn1F-xAhj<4EtlIWe0sQ)~RGsqi$J`>1vk9U>j_O0A8D=p}9V~d)z zb3mMcZ_mwS?Bw18Cc%Iv{-Ei7vr~J`gk8&Q19}ekS2iXT!za%Y&p!gLmrc4f(|p%1 zKQiQshalG(3axj9_%>k@1f&q$ zOggIjV1DE#9L;_Hd&3xp-ZHh9!m7-!DUz>{Nq5_>ARqlTWDC%{;~7NgJRnO@C*;wE zI(%=>BHD+$KWrd%Ytb3{&4)~~+jvs!-Ar@1GC(}WJnW~4snvFjd4sRovOF){(jSS6 z7-`ty!6b1T{y)`S`8$+v)SnrKG(riHHG8%sYh&NbnzELCEBlgVY%_>Z5tZyBB$cs~ zofumrWt%d@*kz1mjIq4W^nS1Re|YDond^G)GxvSYxzFd^pL3q);TYV_^m(}vCew2t z`X2VuU5E7rkANh4OA_cj+Zw)KD`ta)F|N~`YoTa6q?v9`rI6IGqa#+_Tb96L_fup0 z(&3R{IGqYDpM+P`PX}arD=y&e?CAWHqDz9`Nbu@N+eJiFIJu6)Nd)__I_kY(4S!Go zR5Va99;#4E_5a22ynV~j=i3jDAsVye^r?M*Z6vwC$Y?q{13z6q41H^Q)+xqRwy+26 z!67zF_qc>3DZ;`GhzT>PKkM3oi?}W zP-E%=-X&>8v(%2{#eB8y-cUSQ!r;S5&N~S9tzoy$gog70AgP&o@z&O2^XFEvW<7D) zIZ^pHIP;7X16R9_C%Od4C856`-#P8>mcm4;`(wEia+yZYRS-y!Jp}WAGWcbHQ#ENR z3U>{clr$D5jt?t{k0{MoLkztu6PC1wMVm%}EfMmw*ri#^ER zaohSL&~!X~6VHJ|#QBqNNk1Ly;wrS5lr6eQJ*ZvWZZQe2;0}w_c-k1-m|gZ(E^{D%kb7sVLEKq|{VXRa(yY&bzzfnEv>?$|y2 z=Sx*6uh!jr=~^$X&STIW2~?*|P_4`OeI>;4d|2%g%1X*weD*KdhP=UylW4$g^Uv%W zRiF%(@_3$JaQ1X3|1IHi9=y1En@(ZYd|?#M6QbQp%j4Uwx-MhD;Z<`lreA^FzZsFV z{$-WHZW1eQhN<`%uO}plMgd{F24(v9t>V+Ma@hur?(;L1+?>M$1;49JXGmFO`O|9gkYR!5s1r@{M^cP2uZnD$ zSrH5svaefS6wft?!XV)5?dLnX!zA__^W+Hr3fQY^7qV$s?nNClk+T|9Ek4b}fBhu; z5rKHXmHuHu*ChBE&_^cH3)qD<=)Gw98zy+}&)!f;9`B|ELpp1n;OGF&=%tL%eo;Sc zDjOr(YJ_Fe3GKO*_M4R_w_9_HL7}`k{qt%44YYKgW=#AfuG^MhEEUC9ePxPR8J*ml)VP|2NIo}-w#sd+Pil_?Q^Jo z^qQ%9?36FoHC%~iR&!H70#3^Z`}stE#CqO*?H%ECL83EdBc*{mlkaGF5Feg-@5#&v zL`21L!u-Qmk?#9r?}{8;gf-dOl`vFzeOAz67LyBotk&dl3({}jc;pZ@VobzGaEz`@ zJ$uHKdA{zA!qH?#Ett-?Gw795Vd z75je5>2;i8*@vE`zz^%iw5%P^0eX;2@`&ofK4~3a5b`GA)FW=sP_{-4Jy>V9md7V) z^A+Yl(7{1$J#63IW}W<&KFT!Pwr$?A8jxnhPwCL-bak{n+x?ykwpj z)4GktPj5G0aJP$(9Lm>bxLLt;%f|RiH-weSsWK>!jt+UFz#1s5aBj!oD;xqR6*v}! zd5i543CQCdlGAXHXb@%aj6GLP=3!xuij}vym$}s#} zYQK%n4{4b_T53_tBWvVHkCg`9K{v0QbN6QrL)YyR(hl?dn(f<85>i}Y?q90}-{N-t z#hAQ*_Hpad5?%|;hdKjDCMR2_!nMk7-CLCRt5P2wKRoe_X+cpftman?&M7Oq$6ob7 zpxmOK8;hhL{NtaW9aWmlc8;AL|J~;%5q7e_8z?NoHUwGZ_MMz($h}|X z*1O(K`y0Hjag^YCq3z@LtNhOe_Q`$f&fi~BvYo62nI&SKNL!VOF~KLeT)qLBqrVi_ zlO&s!uio0srL>QLr>1tlQGl4nn17lSATs+!Cc!D`A15C$IoD!beqVODf%);~u$pP` zDGuEsM5VsZ4F}d%`GUK_-YFeEe-H#>wJ3*l(eg4Tzefx1#Ru`zF6UG;jo1RjW4Sn{mu3PvU<42 zb6WI%DUG9$@N5AD+&D!C5lHoMp1lXf~7e@nti|D&+b>@Z~(?(+^Y$mWSu<9)a=)&Y#Uv zqHfS%;spdci3Ap8n4CEYl>#i3SjE00=w$O3?L!D_r8Jz2Ga6~Pp8y}>^9dfRHvuWeS z;Rq?pmTnAN75S{_ynmdAo1+^KPcLg=xw34bLd@Nnz14~Zdi$uSj@mxwO~rFL@1)B) zAKdCSa02@86h2T>$@TK52_JhP@w9u0&-?d)C*5E0rQmTVh3k1sv@|rl!s?LYtvt{d=-FD+(QyE5HMElAY80UTDkAA-4V>C?elSRC@CAnCu}V&FA6V4BAQ z$TGoH$B%Um6o(jz1~{6(wcMstM;^qi5v!W-dn{76z7_n6QtgE>Jl;@8wBEfYbrstk z!Nd5Iv#_cZl>dF^EHj|ZFY+wx2JEw3zGbMhG5bvs3#tX5X*A;SL1_Kdovn|l5N2_` z?%4(N@ctT^jDi>}%nioQlh-|<+@0Ub`JiA_k8^aMv(9zFD4807@J9OAt?ED&xw0)% zQ`9!mE9QVxxwT`i&KSfS>~iJD_zD+c@+}QWf4ae=KIV4x?3v26h8cgf&R`FgiFxFPwI09F=g+Wp!X9rHfefY;-(SHYUlYuIaHh01E&$Oh5 z#+Gh^w3BJSi2c{-o|ifaNsD1nx_t;}#U45V~7Ffpe$;MCvMdxOLg9N8z3Qz2@8 zdYrMI4YGpWBgvjV);k3=DxxlW>w|s{#?~?VzNU%GtK04oD|9Fy01ABZQ4yz5l z1M_E#%khmcI4)>!(q@wM)vOfR4W{Fiq>DnHF16d9ALWYz3NYhKo3FC6?tiH{RfiV-B(S< za>O9NCKLOIQ%{V7>S)s348$#-WC~ckYyzCbD3-d)IZ6GC6W*vtJz#h9P~a{=HKC(z znSo3`C1}eWmyfkc@F~a6WZLx}e=(oN;9Jtut#^)B?0nNMh6D}N-5FyN7e4jTCXb&H z=50XDSo_Afo+4VnLwt|2DwVCv*os1qNKu^I{EECGdPS)y+iGNAIB~$Wj zB)O7FieZ&ir_YaSAwZ^1Pad04`(;HAA&(a) zqzbR!p;1=J(`{OreQ`)^#wGq_tT*&SFll?ime2T%&(({+7};g+!?w>uiHmEMX*^Up zp9uz!pj{9usou!a1@Q$3-)OXX*H+Pa8A_U`ey>()pZE+N{-L_ z8X>z^q=Av+SqZ$Z72LR?DMS_w_f1ktfKCJX*qCo-U!D%^2hxL|9>^ROXM!>!SLpeV1d0fcbP8N6D^dvsU)m^LL-w=w)smqa@z$p#sVblT@VSXD+#aZOu5=1C+GH2 z40P7Y`p1lX&yU;r4oZU+H@jTxh4ZWnaVt9<3kMYngYEmM%zTZ8Cj)ykd=j_?8^W%QC^Q%)F)Gouua!Ta# zkn}Ll;LJsQo?nao47oZ5q_>RsFSUzDYgjuP12eG6$cptmC#msmPGNGrhTvi@-5bKq zzn#&(-9Y>%>;3vlocx(9bA@Ak1M80F(FZ=2&lT+%zI8rP^j2135J^@@fDS(fY&`s! zQ@c+O4Z5xzrNPY6Gsow!pWBNmYA1*`^k@3Uu-I)ET6NDDx653Qh$7=ojxXYMv8gmc z=!dZ;DrTko46t~pWfc%bF4HCd>90b9ndztLkU4F>v4mxR+vC}7`h`k0VmN5TQeKaq z74}Kl&m?+Fzx|Fh+fdk=V@=}&ZCahNEBETTDYw=Qt7O$nRck0N3NURvnCFxi=mWo7Dv(_Ix^5p|3|3;36lBJe4mw?0fGpdZ(KANEnwC0Pw^G zM4lf2I9tf+3yEMM!URWHd?%b8x-qadmwT8Ch0tR}YReK#ze?#{ z-?N$fSJ$?F_?Q0l6!iRYgAHiqFr>nUP%hqc-UST;$NC{o3CHsped-la zjR{;+BmYiS=kF%#HbpKyIh@ z@ccV1rO~lP_}NWEW{%lao(i34C9-ZJnp;+~#f_9_m)=gEmquc~QYn>ez(h47a?nvB z+d|rWzkKe2$~KZ^(~_6NONYa<%@>*W-##MXzfuxgd10gTSzo+_rw=EAG24^tY(H2Z zpqiHwiF~+b{lAn04!pzBIxb_ao|CYDJzu&|l&U_o0oI&xl{sLpZdUptYdcI3+DNL~;bg-}3O+mGJR@Mnj)^_>$eiCCQSwU9A#6ZJ!MP zgW{(i|LXjcg#x}VwzEpvyO|CxR(ro&^`rNF6bHz(xuE=~;nl@=)~HS54;2G$qt8nE zL!Rz`%iMgS&(~-*6EYv#_lY-Q!ug$#VD7YMw*|GnSk|#@j&JfnWtfrY9ZnK*!}iZM zZOP*UBbu4FKWR^d-J{CTIXsR=8mtIwPGzpE}^>K)f}T%$rIV z67ime<#yJkOHL>@ssUpIio_&m+4%{Pb8XFUW0vDv*287eq$Xb{RZc-^v`HS$Vo>~2LNE$_MUJptoffS!tnW>e9_ zmRdJt2QJ=&AD|hT3(`FtLQ1^z|Gp`UlQBqnQ*VO7;B0AiECG9)0jO1WRad7AG7qri zx}m%=w-FeQh4%(>XjU^`nBLX}H(BtlafIFJQTP0c0$ zSl^oENp1a|6bcV$iieIQrKH9my&r#F@;`8!NL9P=es^=;_VVBJCKy^BbiJCem zgU3B{^73A;*yne$cgTfLXxHv10c%)Fnv!W0{Q{xQfAmO~=Pq!G!0~M$W&gJ{FDkW_ zJ#=0t!xtrh@(9pWTIl6Xjh4%Pa;`&e#Y(^Sq>9>xP-QE*EczXy%|F})L3zAJiiU;* zSqd@zv7S1Z>WSca=-Prc64?zr!cTb#Zu>c!suy)^!UBMr{E{r}oe`;74_5cH{nKImZ4E6GX-{ literal 0 HcmV?d00001 diff --git a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts new file mode 100644 index 00000000..7682b280 --- /dev/null +++ b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts @@ -0,0 +1,105 @@ +import { GoogleVertexAIEmbeddings } from 'langchain/embeddings/googlevertexai' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { GoogleGenerativeAIEmbeddings, GoogleGenerativeAIEmbeddingsParams } from '@langchain/google-genai' +import { TaskType } from '@google/generative-ai' + +class GoogleGenerativeAIEmbedding_Embeddings implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'GoogleGenerativeAI Embeddings' + this.name = 'googleGenerativeAiEmbeddings' + this.version = 1.0 + this.type = 'GoogleGenerativeAiEmbeddings' + this.icon = 'gemini.png' + this.category = 'Embeddings' + this.description = 'Google Generative API to generate embeddings for a given text' + this.baseClasses = [this.type, ...getBaseClasses(GoogleVertexAIEmbeddings)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['googleGenerativeAI'], + optional: true, + description: 'Google Generative AI credential.' + } + this.inputs = [ + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'embedding-001', + name: 'embedding-001' + } + ], + default: 'embedding-001' + }, + { + label: 'Task Type', + name: 'tasktype', + type: 'options', + description: 'Type of task for which the embedding will be used', + options: [ + { label: 'TASK_TYPE_UNSPECIFIED', name: 'TASK_TYPE_UNSPECIFIED' }, + { label: 'RETRIEVAL_QUERY', name: 'RETRIEVAL_QUERY' }, + { label: 'RETRIEVAL_DOCUMENT', name: 'RETRIEVAL_DOCUMENT' }, + { label: 'SEMANTIC_SIMILARITY', name: 'SEMANTIC_SIMILARITY' }, + { label: 'CLASSIFICATION', name: 'CLASSIFICATION' }, + { label: 'CLUSTERING', name: 'CLUSTERING' } + ], + default: 'TASK_TYPE_UNSPECIFIED' + } + ] + } + + // eslint-disable-next-line unused-imports/no-unused-vars + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const modelName = nodeData.inputs?.modelName as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('googleGenerativeAPIKey', credentialData, nodeData) + + let taskType: TaskType + switch (nodeData.inputs?.tasktype as string) { + case 'RETRIEVAL_QUERY': + taskType = TaskType.RETRIEVAL_QUERY + break + case 'RETRIEVAL_DOCUMENT': + taskType = TaskType.RETRIEVAL_DOCUMENT + break + case 'SEMANTIC_SIMILARITY': + taskType = TaskType.SEMANTIC_SIMILARITY + break + case 'CLASSIFICATION': + taskType = TaskType.CLASSIFICATION + break + case 'CLUSTERING': + taskType = TaskType.CLUSTERING + break + default: + taskType = TaskType.TASK_TYPE_UNSPECIFIED + break + } + const obj: GoogleGenerativeAIEmbeddingsParams = { + apiKey: apiKey, + modelName: modelName, + taskType: taskType + } + + const model = new GoogleGenerativeAIEmbeddings(obj) + return model + } +} + +module.exports = { nodeClass: GoogleGenerativeAIEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/gemini.png b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0d60f44817cdbf1bd5483d88c1929831a84eed GIT binary patch literal 57143 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

1U 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 literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 1874ca10..6bdc908a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -26,6 +26,7 @@ "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", + "@langchain/google-genai": "^0.0.3", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", "@pinecone-database/pinecone": "^1.1.1", From f475732e6ba1aa5e78d6b352519e86cca5f13a08 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 15 Dec 2023 22:20:51 +0530 Subject: [PATCH 065/502] Support for Google Gemini Models: Minor fixes --- .../ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts | 6 +++--- .../GoogleGenerativeAIEmbedding.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index 26913424..95ee0575 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -18,7 +18,7 @@ class GoogleGenerativeAI_ChatModels implements INode { constructor() { this.label = 'ChatGoogleGenerativeAI' this.name = 'chatGoogleGenerativeAI' - this.version = 2.0 + this.version = 1.0 this.type = 'ChatGoogleGenerativeAI' this.icon = 'gemini.png' this.category = 'Chat Models' @@ -29,7 +29,7 @@ class GoogleGenerativeAI_ChatModels implements INode { name: 'credential', type: 'credential', credentialNames: ['googleGenerativeAI'], - optional: true, + optional: false, description: 'Google Generative AI credential.' } this.inputs = [ @@ -100,7 +100,7 @@ class GoogleGenerativeAI_ChatModels implements INode { const model = new ChatGoogleGenerativeAI(obj) if (topP) model.topP = parseFloat(topP) if (cache) model.cache = cache - if (temperature) model.temperature = parseInt(temperature) + if (temperature) model.temperature = parseFloat(temperature) return model } } diff --git a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts index 7682b280..86921b42 100644 --- a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts +++ b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts @@ -24,13 +24,13 @@ class GoogleGenerativeAIEmbedding_Embeddings implements INode { this.icon = 'gemini.png' this.category = 'Embeddings' this.description = 'Google Generative API to generate embeddings for a given text' - this.baseClasses = [this.type, ...getBaseClasses(GoogleVertexAIEmbeddings)] + this.baseClasses = [this.type, ...getBaseClasses(GoogleGenerativeAIEmbeddings)] this.credential = { label: 'Connect Credential', name: 'credential', type: 'credential', credentialNames: ['googleGenerativeAI'], - optional: true, + optional: false, description: 'Google Generative AI credential.' } this.inputs = [ From 87233a0e365e7949b8d58aff270768f37ebf1f2d Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 15 Dec 2023 16:59:08 +0000 Subject: [PATCH 066/502] Update GoogleGenerativeAIEmbedding.ts --- .../GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts index 86921b42..fa5cff45 100644 --- a/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts +++ b/packages/components/nodes/embeddings/GoogleGenerativeAIEmbedding/GoogleGenerativeAIEmbedding.ts @@ -1,4 +1,3 @@ -import { GoogleVertexAIEmbeddings } from 'langchain/embeddings/googlevertexai' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { GoogleGenerativeAIEmbeddings, GoogleGenerativeAIEmbeddingsParams } from '@langchain/google-genai' From 911b4fe7fb77c9cfbf34d4929358c34a847e435c Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 15 Dec 2023 17:15:00 +0000 Subject: [PATCH 067/502] add new utilties - customfunction, setter/getter, replace code editor --- .../components/nodes/tools/CustomTool/core.ts | 32 +- .../CustomFunction/CustomFunction.ts | 124 ++++++++ .../CustomFunction/customfunction.svg | 1 + .../utilities/GetVariable/GetVariable.ts | 52 ++++ .../nodes/utilities/GetVariable/getvar.svg | 1 + .../utilities/SetVariable/SetVariable.ts | 56 ++++ .../nodes/utilities/SetVariable/setvar.svg | 1 + packages/components/src/utils.ts | 57 ++++ packages/server/src/index.ts | 22 ++ packages/server/src/utils/index.ts | 23 +- packages/ui/package.json | 11 +- packages/ui/src/api/nodes.js | 5 +- .../ui-component/dialog/ExpandTextDialog.js | 134 ++++++--- .../ui/src/ui-component/editor/CodeEditor.js | 48 +++ .../src/ui-component/editor/DarkCodeEditor.js | 43 --- .../ui-component/editor/LightCodeEditor.js | 43 --- .../ui/src/ui-component/editor/prism-dark.css | 275 ------------------ .../src/ui-component/editor/prism-light.css | 207 ------------- packages/ui/src/ui-component/input/Input.js | 32 +- .../src/ui-component/json/SelectVariable.js | 7 +- .../ui/src/views/canvas/NodeInputHandler.js | 40 ++- packages/ui/src/views/tools/ToolDialog.js | 39 +-- 22 files changed, 543 insertions(+), 710 deletions(-) create mode 100644 packages/components/nodes/utilities/CustomFunction/CustomFunction.ts create mode 100644 packages/components/nodes/utilities/CustomFunction/customfunction.svg create mode 100644 packages/components/nodes/utilities/GetVariable/GetVariable.ts create mode 100644 packages/components/nodes/utilities/GetVariable/getvar.svg create mode 100644 packages/components/nodes/utilities/SetVariable/SetVariable.ts create mode 100644 packages/components/nodes/utilities/SetVariable/setvar.svg create mode 100644 packages/ui/src/ui-component/editor/CodeEditor.js delete mode 100644 packages/ui/src/ui-component/editor/DarkCodeEditor.js delete mode 100644 packages/ui/src/ui-component/editor/LightCodeEditor.js delete mode 100644 packages/ui/src/ui-component/editor/prism-dark.css delete mode 100644 packages/ui/src/ui-component/editor/prism-light.css diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 12dd72f1..2aa06b54 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -2,37 +2,7 @@ import { z } from 'zod' import { CallbackManagerForToolRun } from 'langchain/callbacks' import { StructuredTool, ToolParams } from 'langchain/tools' import { NodeVM } from 'vm2' - -/* - * List of dependencies allowed to be import in vm2 - */ -const availableDependencies = [ - '@dqbd/tiktoken', - '@getzep/zep-js', - '@huggingface/inference', - '@pinecone-database/pinecone', - '@supabase/supabase-js', - 'axios', - 'cheerio', - 'chromadb', - 'cohere-ai', - 'd3-dsv', - 'form-data', - 'graphql', - 'html-to-text', - 'langchain', - 'linkifyjs', - 'mammoth', - 'moment', - 'node-fetch', - 'pdf-parse', - 'pdfjs-dist', - 'playwright', - 'puppeteer', - 'srt-parser-2', - 'typeorm', - 'weaviate-ts-client' -] +import { availableDependencies } from '../../../src/utils' export interface BaseDynamicToolInput extends ToolParams { name: string diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts new file mode 100644 index 00000000..b358b24b --- /dev/null +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -0,0 +1,124 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { NodeVM } from 'vm2' +import { availableDependencies, handleEscapeCharacters } from '../../../src/utils' + +class CustomFunction_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Custom JS Function' + this.name = 'customFunction' + this.version = 1.0 + this.type = 'CustomFunction' + this.icon = 'customfunction.svg' + this.category = 'Utilities' + this.description = `Execute custom javascript function` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input Variables', + name: 'functionInputVariables', + description: 'Input variables can be used in the function with prefix $. For example: $var', + type: 'json', + optional: true, + acceptVariable: true, + list: true + }, + { + label: 'Function Name', + name: 'functionName', + type: 'string', + optional: true, + placeholder: 'My Function' + }, + { + label: 'Javascript Function', + name: 'javascriptFunction', + type: 'code' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, input: string): Promise { + const javascriptFunction = nodeData.inputs?.javascriptFunction as string + const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables + + let inputVars: ICommonObject = {} + if (functionInputVariablesRaw) { + try { + inputVars = + typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw) + } catch (exception) { + throw new Error("Invalid JSON in the PromptTemplate's promptValues: " + exception) + } + } + + let sandbox: any = { $input: input } + + if (Object.keys(inputVars).length) { + for (const item in inputVars) { + sandbox[`$${item}`] = inputVars[item] + } + } + + const defaultAllowBuiltInDep = [ + 'assert', + 'buffer', + 'crypto', + 'events', + 'http', + 'https', + 'net', + 'path', + 'querystring', + 'timers', + 'tls', + 'url', + 'zlib' + ] + + const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP + ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) + : defaultAllowBuiltInDep + const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] + const deps = availableDependencies.concat(externalDeps) + + const nodeVMOptions = { + console: 'inherit', + sandbox, + require: { + external: { modules: deps }, + builtin: builtinDeps + } + } as any + + const vm = new NodeVM(nodeVMOptions) + try { + const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + if (typeof response === 'string') { + return handleEscapeCharacters(response, false) + } + return response + } catch (e) { + throw new Error(e) + } + } +} + +module.exports = { nodeClass: CustomFunction_Utilities } diff --git a/packages/components/nodes/utilities/CustomFunction/customfunction.svg b/packages/components/nodes/utilities/CustomFunction/customfunction.svg new file mode 100644 index 00000000..bf60fcae --- /dev/null +++ b/packages/components/nodes/utilities/CustomFunction/customfunction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/utilities/GetVariable/GetVariable.ts b/packages/components/nodes/utilities/GetVariable/GetVariable.ts new file mode 100644 index 00000000..dde5a2d9 --- /dev/null +++ b/packages/components/nodes/utilities/GetVariable/GetVariable.ts @@ -0,0 +1,52 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class GetVariable_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Get Variable' + this.name = 'getVariable' + this.version = 1.0 + this.type = 'GetVariable' + this.icon = 'getvar.svg' + this.category = 'Utilities' + this.description = `Get variable that was saved using Set Variable node` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Variable Name', + name: 'variableName', + type: 'string', + placeholder: 'var1' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const variableName = nodeData.inputs?.variableName as string + const dynamicVars = options.dynamicVariables as Record + + if (Object.prototype.hasOwnProperty.call(dynamicVars, variableName)) { + return dynamicVars[variableName] + } + return undefined + } +} + +module.exports = { nodeClass: GetVariable_Utilities } diff --git a/packages/components/nodes/utilities/GetVariable/getvar.svg b/packages/components/nodes/utilities/GetVariable/getvar.svg new file mode 100644 index 00000000..49e27ab1 --- /dev/null +++ b/packages/components/nodes/utilities/GetVariable/getvar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/utilities/SetVariable/SetVariable.ts b/packages/components/nodes/utilities/SetVariable/SetVariable.ts new file mode 100644 index 00000000..8542668c --- /dev/null +++ b/packages/components/nodes/utilities/SetVariable/SetVariable.ts @@ -0,0 +1,56 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class SetVariable_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Set Variable' + this.name = 'setVariable' + this.version = 1.0 + this.type = 'SetVariable' + this.icon = 'setvar.svg' + this.category = 'Utilities' + this.description = `Set variable which can be retrieved at a later stage. Variable is only available during runtime.` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input', + name: 'input', + type: 'string | number | boolean | json | array', + optional: true, + list: true + }, + { + label: 'Variable Name', + name: 'variableName', + type: 'string', + placeholder: 'var1' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData): Promise { + const inputRaw = nodeData.inputs?.input + const variableName = nodeData.inputs?.variableName as string + + return { output: inputRaw, dynamicVariables: { [variableName]: inputRaw } } + } +} + +module.exports = { nodeClass: SetVariable_Utilities } diff --git a/packages/components/nodes/utilities/SetVariable/setvar.svg b/packages/components/nodes/utilities/SetVariable/setvar.svg new file mode 100644 index 00000000..c8d643c9 --- /dev/null +++ b/packages/components/nodes/utilities/SetVariable/setvar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 404f7c75..239b13ca 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -12,6 +12,63 @@ import { AIMessage, HumanMessage } 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 +/* + * List of dependencies allowed to be import in vm2 + */ +export const availableDependencies = [ + '@aws-sdk/client-bedrock-runtime', + '@aws-sdk/client-dynamodb', + '@aws-sdk/client-s3', + '@elastic/elasticsearch', + '@dqbd/tiktoken', + '@getzep/zep-js', + '@gomomento/sdk', + '@gomomento/sdk-core', + '@google-ai/generativelanguage', + '@huggingface/inference', + '@notionhq/client', + '@opensearch-project/opensearch', + '@pinecone-database/pinecone', + '@qdrant/js-client-rest', + '@supabase/supabase-js', + '@upstash/redis', + '@zilliz/milvus2-sdk-node', + 'apify-client', + 'axios', + 'cheerio', + 'chromadb', + 'cohere-ai', + 'd3-dsv', + 'faiss-node', + 'form-data', + 'google-auth-library', + 'graphql', + 'html-to-text', + 'ioredis', + 'langchain', + 'langfuse', + 'langsmith', + 'linkifyjs', + 'llmonitor', + 'mammoth', + 'moment', + 'mongodb', + 'mysql2', + 'node-fetch', + 'node-html-markdown', + 'notion-to-md', + 'openai', + 'pdf-parse', + 'pdfjs-dist', + 'pg', + 'playwright', + 'puppeteer', + 'redis', + 'replicate', + 'srt-parser-2', + 'typeorm', + 'weaviate-ts-client' +] /** * Get base classes of components diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index fb4a5f5a..85e9eac4 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -281,6 +281,28 @@ export class App { } }) + // execute custom function node + this.app.post('/api/v1/node-custom-function', async (req: Request, res: Response) => { + const body = req.body + const nodeData = { inputs: body } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, 'customFunction')) { + try { + const nodeInstanceFilePath = this.nodesPool.componentNodes['customFunction'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + const returnOptions: INodeOptionsValue[] = await newNodeInstance.init(nodeData) + + return res.json(returnOptions) + } catch (error) { + return res.status(500).send(`Error running custom function: ${error}`) + } + } else { + res.status(404).send(`Node customFunction not found`) + return + } + }) + // ---------------------------------------- // Chatflows // ---------------------------------------- diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 2bf1c04a..d411050c 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -231,6 +231,7 @@ export const buildLangchain = async ( // Create a Queue and add our initial node in it const nodeQueue = [] as INodeQueue[] const exploredNode = {} as IExploredNode + const dynamicVariables = {} as Record // In the case of infinite loop, only max 3 loops will be executed const maxLoop = 3 @@ -267,20 +268,36 @@ export const buildLangchain = async ( appDataSource, databaseEntities, logger, - cachePool + cachePool, + dynamicVariables }) logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) break } else { logger.debug(`[server]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) - flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question, { + let outputResult = await newNodeInstance.init(reactFlowNodeData, question, { chatId, chatflowid, appDataSource, databaseEntities, logger, - cachePool + cachePool, + dynamicVariables }) + + // Save dynamic variables + if (reactFlowNode.data.name === 'setVariable') { + const dynamicVars = outputResult?.dynamicVariables ?? {} + + for (const variableKey in dynamicVars) { + dynamicVariables[variableKey] = dynamicVars[variableKey] + } + + outputResult = outputResult?.output + } + + flowNodes[nodeIndex].data.instance = outputResult + logger.debug(`[server]: Finished initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) } } catch (e: any) { diff --git a/packages/ui/package.json b/packages/ui/package.json index 7a739978..2aed7d97 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,13 +8,20 @@ "email": "henryheng@flowiseai.com" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/view": "^6.22.3", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.0.3", - "@mui/material": "^5.11.12", + "@mui/lab": "^5.0.0-alpha.156", + "@mui/material": "^5.15.0", "@mui/x-data-grid": "^6.8.0", "@tabler/icons": "^1.39.1", + "@uiw/codemirror-theme-sublime": "^4.21.21", + "@uiw/codemirror-theme-vscode": "^4.21.21", + "@uiw/react-codemirror": "^4.21.21", "clsx": "^1.1.1", "flowise-embed": "*", "flowise-embed-react": "*", @@ -26,7 +33,6 @@ "lodash": "^4.17.21", "moment": "^2.29.3", "notistack": "^2.0.4", - "prismjs": "^1.28.0", "prop-types": "^15.7.2", "react": "^18.2.0", "react-code-blocks": "^0.0.9-0", @@ -39,7 +45,6 @@ "react-redux": "^8.0.5", "react-router": "~6.3.0", "react-router-dom": "~6.3.0", - "react-simple-code-editor": "^0.11.2", "react-syntax-highlighter": "^15.5.0", "reactflow": "^11.5.6", "redux": "^4.0.5", diff --git a/packages/ui/src/api/nodes.js b/packages/ui/src/api/nodes.js index 7eb4c351..3b7eacc5 100644 --- a/packages/ui/src/api/nodes.js +++ b/packages/ui/src/api/nodes.js @@ -4,7 +4,10 @@ const getAllNodes = () => client.get('/nodes') const getSpecificNode = (name) => client.get(`/nodes/${name}`) +const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body) + export default { getAllNodes, - getSpecificNode + getSpecificNode, + executeCustomFunctionNode } diff --git a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js index 2a4ec4f5..0ef70e29 100644 --- a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js +++ b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js @@ -2,14 +2,24 @@ import { createPortal } from 'react-dom' import { useState, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import PropTypes from 'prop-types' +import PerfectScrollbar from 'react-perfect-scrollbar' + +// MUI import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import PerfectScrollbar from 'react-perfect-scrollbar' +import { LoadingButton } from '@mui/lab' + +// Project Import import { StyledButton } from 'ui-component/button/StyledButton' -import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' -import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + +// Store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +// API +import nodesApi from 'api/nodes' +import useApi from 'hooks/useApi' + import './ExpandTextDialog.css' const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { @@ -18,18 +28,30 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const theme = useTheme() const dispatch = useDispatch() const customization = useSelector((state) => state.customization) - const languageType = 'json' const [inputValue, setInputValue] = useState('') const [inputParam, setInputParam] = useState(null) + const [languageType, setLanguageType] = useState('json') + const [loading, setLoading] = useState(false) + const [codeExecutedResult, setCodeExecutedResult] = useState('') + + const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode) useEffect(() => { if (dialogProps.value) setInputValue(dialogProps.value) - if (dialogProps.inputParam) setInputParam(dialogProps.inputParam) + if (dialogProps.inputParam) { + setInputParam(dialogProps.inputParam) + if (dialogProps.inputParam.type === 'code') { + setLanguageType('js') + } + } return () => { setInputValue('') + setLoading(false) setInputParam(null) + setLanguageType('json') + setCodeExecutedResult('') } }, [dialogProps]) @@ -39,11 +61,31 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { return () => dispatch({ type: HIDE_CANVAS_DIALOG }) }, [show, dispatch]) + useEffect(() => { + setLoading(executeCustomFunctionNodeApi.loading) + }, [executeCustomFunctionNodeApi.loading]) + + useEffect(() => { + if (executeCustomFunctionNodeApi.data) { + setCodeExecutedResult(executeCustomFunctionNodeApi.data) + } + }, [executeCustomFunctionNodeApi.data]) + + useEffect(() => { + if (executeCustomFunctionNodeApi.error) { + if (typeof executeCustomFunctionNodeApi.error === 'object' && executeCustomFunctionNodeApi.error?.response?.data) { + setCodeExecutedResult(executeCustomFunctionNodeApi.error?.response?.data) + } else if (typeof executeCustomFunctionNodeApi.error === 'string') { + setCodeExecutedResult(executeCustomFunctionNodeApi.error) + } + } + }, [executeCustomFunctionNodeApi.error]) + const component = show ? (

- {inputParam && inputParam.type === 'string' && ( + {inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
{inputParam.label} @@ -54,42 +96,66 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { borderColor: theme.palette.grey['500'], borderRadius: '12px', height: '100%', - maxHeight: 'calc(100vh - 220px)', + maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)', overflowX: 'hidden', backgroundColor: 'white' }} > - {customization.isDarkMode ? ( - setInputValue(code)} - placeholder={inputParam.placeholder} - type={languageType} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%' - }} - /> - ) : ( - setInputValue(code)} - placeholder={inputParam.placeholder} - type={languageType} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%' - }} - /> - )} + setInputValue(code)} + />
)}
+ {languageType === 'js' && ( + { + setLoading(true) + executeCustomFunctionNodeApi.request({ javascriptFunction: inputValue }) + }} + > + Execute + + )} + {codeExecutedResult && ( +
+ +
+ )}
diff --git a/packages/ui/src/ui-component/editor/CodeEditor.js b/packages/ui/src/ui-component/editor/CodeEditor.js new file mode 100644 index 00000000..120e19a0 --- /dev/null +++ b/packages/ui/src/ui-component/editor/CodeEditor.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import CodeMirror from '@uiw/react-codemirror' +import { javascript } from '@codemirror/lang-javascript' +import { json } from '@codemirror/lang-json' +import { vscodeDark } from '@uiw/codemirror-theme-vscode' +import { sublime } from '@uiw/codemirror-theme-sublime' +import { EditorView } from '@codemirror/view' + +export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled = false, basicSetup = {}, onValueChange }) => { + const customStyle = EditorView.baseTheme({ + '&': { + color: '#191b1f', + padding: '10px' + }, + '.cm-placeholder': { + color: 'rgba(120, 120, 120, 0.5)' + } + }) + + return ( + + ) +} + +CodeEditor.propTypes = { + value: PropTypes.string, + height: PropTypes.string, + theme: PropTypes.string, + lang: PropTypes.string, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + basicSetup: PropTypes.object, + onValueChange: PropTypes.func +} diff --git a/packages/ui/src/ui-component/editor/DarkCodeEditor.js b/packages/ui/src/ui-component/editor/DarkCodeEditor.js deleted file mode 100644 index bf0719dd..00000000 --- a/packages/ui/src/ui-component/editor/DarkCodeEditor.js +++ /dev/null @@ -1,43 +0,0 @@ -import Editor from 'react-simple-code-editor' -import { highlight, languages } from 'prismjs/components/prism-core' -import 'prismjs/components/prism-clike' -import 'prismjs/components/prism-javascript' -import 'prismjs/components/prism-json' -import 'prismjs/components/prism-markup' -import './prism-dark.css' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' - -export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => { - const theme = useTheme() - - return ( - highlight(code, type === 'json' ? languages.json : languages.js)} - padding={10} - onValueChange={onValueChange} - onMouseUp={onMouseUp} - onBlur={onBlur} - tabSize={4} - style={{ - ...style, - background: theme.palette.codeEditor.main - }} - textareaClassName='editor__textarea' - /> - ) -} - -DarkCodeEditor.propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - disabled: PropTypes.bool, - type: PropTypes.string, - style: PropTypes.object, - onValueChange: PropTypes.func, - onMouseUp: PropTypes.func, - onBlur: PropTypes.func -} diff --git a/packages/ui/src/ui-component/editor/LightCodeEditor.js b/packages/ui/src/ui-component/editor/LightCodeEditor.js deleted file mode 100644 index 14dcbf29..00000000 --- a/packages/ui/src/ui-component/editor/LightCodeEditor.js +++ /dev/null @@ -1,43 +0,0 @@ -import Editor from 'react-simple-code-editor' -import { highlight, languages } from 'prismjs/components/prism-core' -import 'prismjs/components/prism-clike' -import 'prismjs/components/prism-javascript' -import 'prismjs/components/prism-json' -import 'prismjs/components/prism-markup' -import './prism-light.css' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' - -export const LightCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => { - const theme = useTheme() - - return ( - highlight(code, type === 'json' ? languages.json : languages.js)} - padding={10} - onValueChange={onValueChange} - onMouseUp={onMouseUp} - onBlur={onBlur} - tabSize={4} - style={{ - ...style, - background: theme.palette.card.main - }} - textareaClassName='editor__textarea' - /> - ) -} - -LightCodeEditor.propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - disabled: PropTypes.bool, - type: PropTypes.string, - style: PropTypes.object, - onValueChange: PropTypes.func, - onMouseUp: PropTypes.func, - onBlur: PropTypes.func -} diff --git a/packages/ui/src/ui-component/editor/prism-dark.css b/packages/ui/src/ui-component/editor/prism-dark.css deleted file mode 100644 index c4bfb413..00000000 --- a/packages/ui/src/ui-component/editor/prism-dark.css +++ /dev/null @@ -1,275 +0,0 @@ -pre[class*='language-'], -code[class*='language-'] { - color: #d4d4d4; - font-size: 13px; - text-shadow: none; - font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace; - direction: ltr; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -pre[class*='language-']::selection, -code[class*='language-']::selection, -pre[class*='language-'] *::selection, -code[class*='language-'] *::selection { - text-shadow: none; - background: #264f78; -} - -@media print { - pre[class*='language-'], - code[class*='language-'] { - text-shadow: none; - } -} - -pre[class*='language-'] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - background: #1e1e1e; -} - -:not(pre) > code[class*='language-'] { - padding: 0.1em 0.3em; - border-radius: 0.3em; - color: #db4c69; - background: #1e1e1e; -} -/********************************************************* -* Tokens -*/ -.namespace { - opacity: 0.7; -} - -.token.doctype .token.doctype-tag { - color: #569cd6; -} - -.token.doctype .token.name { - color: #9cdcfe; -} - -.token.comment, -.token.prolog { - color: #6a9955; -} - -.token.punctuation, -.language-html .language-css .token.punctuation, -.language-html .language-javascript .token.punctuation { - color: #d4d4d4; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.constant, -.token.symbol, -.token.inserted, -.token.unit { - color: #b5cea8; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.deleted { - color: #ce9178; -} - -.language-css .token.string.url { - text-decoration: underline; -} - -.token.operator, -.token.entity { - color: #d4d4d4; -} - -.token.operator.arrow { - color: #569cd6; -} - -.token.atrule { - color: #ce9178; -} - -.token.atrule .token.rule { - color: #c586c0; -} - -.token.atrule .token.url { - color: #9cdcfe; -} - -.token.atrule .token.url .token.function { - color: #dcdcaa; -} - -.token.atrule .token.url .token.punctuation { - color: #d4d4d4; -} - -.token.keyword { - color: #569cd6; -} - -.token.keyword.module, -.token.keyword.control-flow { - color: #c586c0; -} - -.token.function, -.token.function .token.maybe-class-name { - color: #dcdcaa; -} - -.token.regex { - color: #d16969; -} - -.token.important { - color: #569cd6; -} - -.token.italic { - font-style: italic; -} - -.token.constant { - color: #9cdcfe; -} - -.token.class-name, -.token.maybe-class-name { - color: #4ec9b0; -} - -.token.console { - color: #9cdcfe; -} - -.token.parameter { - color: #9cdcfe; -} - -.token.interpolation { - color: #9cdcfe; -} - -.token.punctuation.interpolation-punctuation { - color: #569cd6; -} - -.token.boolean { - color: #569cd6; -} - -.token.property, -.token.variable, -.token.imports .token.maybe-class-name, -.token.exports .token.maybe-class-name { - color: #9cdcfe; -} - -.token.selector { - color: #d7ba7d; -} - -.token.escape { - color: #d7ba7d; -} - -.token.tag { - color: #569cd6; -} - -.token.tag .token.punctuation { - color: #808080; -} - -.token.cdata { - color: #808080; -} - -.token.attr-name { - color: #9cdcfe; -} - -.token.attr-value, -.token.attr-value .token.punctuation { - color: #ce9178; -} - -.token.attr-value .token.punctuation.attr-equals { - color: #d4d4d4; -} - -.token.entity { - color: #569cd6; -} - -.token.namespace { - color: #4ec9b0; -} -/********************************************************* -* Language Specific -*/ - -pre[class*='language-javascript'], -code[class*='language-javascript'], -pre[class*='language-jsx'], -code[class*='language-jsx'], -pre[class*='language-typescript'], -code[class*='language-typescript'], -pre[class*='language-tsx'], -code[class*='language-tsx'] { - color: #9cdcfe; -} - -pre[class*='language-css'], -code[class*='language-css'] { - color: #ce9178; -} - -pre[class*='language-html'], -code[class*='language-html'] { - color: #d4d4d4; -} - -.language-regex .token.anchor { - color: #dcdcaa; -} - -.language-html .token.punctuation { - color: #808080; -} -/********************************************************* -* Line highlighting -*/ -pre[class*='language-'] > code[class*='language-'] { - position: relative; - z-index: 1; -} - -.line-highlight.line-highlight { - background: #f7ebc6; - box-shadow: inset 5px 0 0 #f7d87c; - z-index: 0; -} diff --git a/packages/ui/src/ui-component/editor/prism-light.css b/packages/ui/src/ui-component/editor/prism-light.css deleted file mode 100644 index 95d6d6eb..00000000 --- a/packages/ui/src/ui-component/editor/prism-light.css +++ /dev/null @@ -1,207 +0,0 @@ -code[class*='language-'], -pre[class*='language-'] { - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - color: #90a4ae; - background: #fafafa; - font-family: Roboto Mono, monospace; - font-size: 1em; - line-height: 1.5em; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -code[class*='language-']::-moz-selection, -pre[class*='language-']::-moz-selection, -code[class*='language-'] ::-moz-selection, -pre[class*='language-'] ::-moz-selection { - background: #cceae7; - color: #263238; -} - -code[class*='language-']::selection, -pre[class*='language-']::selection, -code[class*='language-'] ::selection, -pre[class*='language-'] ::selection { - background: #cceae7; - color: #263238; -} - -:not(pre) > code[class*='language-'] { - white-space: normal; - border-radius: 0.2em; - padding: 0.1em; -} - -pre[class*='language-'] { - overflow: auto; - position: relative; - margin: 0.5em 0; - padding: 1.25em 1em; -} - -.language-css > code, -.language-sass > code, -.language-scss > code { - color: #f76d47; -} - -[class*='language-'] .namespace { - opacity: 0.7; -} - -.token.atrule { - color: #7c4dff; -} - -.token.attr-name { - color: #39adb5; -} - -.token.attr-value { - color: #f6a434; -} - -.token.attribute { - color: #f6a434; -} - -.token.boolean { - color: #7c4dff; -} - -.token.builtin { - color: #39adb5; -} - -.token.cdata { - color: #39adb5; -} - -.token.char { - color: #39adb5; -} - -.token.class { - color: #39adb5; -} - -.token.class-name { - color: #6182b8; -} - -.token.comment { - color: #aabfc9; -} - -.token.constant { - color: #7c4dff; -} - -.token.deleted { - color: #e53935; -} - -.token.doctype { - color: #aabfc9; -} - -.token.entity { - color: #e53935; -} - -.token.function { - color: #7c4dff; -} - -.token.hexcode { - color: #f76d47; -} - -.token.id { - color: #7c4dff; - font-weight: bold; -} - -.token.important { - color: #7c4dff; - font-weight: bold; -} - -.token.inserted { - color: #39adb5; -} - -.token.keyword { - color: #7c4dff; -} - -.token.number { - color: #f76d47; -} - -.token.operator { - color: #39adb5; -} - -.token.prolog { - color: #aabfc9; -} - -.token.property { - color: #39adb5; -} - -.token.pseudo-class { - color: #f6a434; -} - -.token.pseudo-element { - color: #f6a434; -} - -.token.punctuation { - color: #39adb5; -} - -.token.regex { - color: #6182b8; -} - -.token.selector { - color: #e53935; -} - -.token.string { - color: #f6a434; -} - -.token.symbol { - color: #7c4dff; -} - -.token.tag { - color: #e53935; -} - -.token.unit { - color: #f76d47; -} - -.token.url { - color: #e53935; -} - -.token.variable { - color: #e53935; -} diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index 6993847b..3e575938 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -1,23 +1,10 @@ import { useState, useEffect, useRef } from 'react' import PropTypes from 'prop-types' import { FormControl, OutlinedInput, Popover } from '@mui/material' -import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog' import SelectVariable from 'ui-component/json/SelectVariable' import { getAvailableNodesForVariable } from 'utils/genericHelper' -export const Input = ({ - inputParam, - value, - nodes, - edges, - nodeId, - onChange, - disabled = false, - showDialog, - dialogProps, - onDialogCancel, - onDialogConfirm -}) => { +export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => { const [myValue, setMyValue] = useState(value ?? '') const [anchorEl, setAnchorEl] = useState(null) const [availableNodesForVariable, setAvailableNodesForVariable] = useState([]) @@ -86,17 +73,6 @@ export const Input = ({ }} /> - {showDialog && ( - { - setMyValue(newValue) - onDialogConfirm(newValue, inputParamName) - }} - > - )}
{inputParam?.acceptVariable && ( diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 892a6273..18e82338 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -22,8 +22,11 @@ import { flowContext } from 'store/context/ReactFlowContext' import { isValidConnection } from 'utils/genericHelper' import { JsonEditorInput } from 'ui-component/json/JsonEditor' import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + import ToolDialog from 'views/tools/ToolDialog' import AssistantDialog from 'views/assistants/AssistantDialog' +import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog' import FormatPromptValuesDialog from 'ui-component/dialog/FormatPromptValuesDialog' import CredentialInputHandler from './CredentialInputHandler' @@ -83,7 +86,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA } } } - const onFormatPromptValuesClicked = (value, inputParam) => { + const onEditJSONClicked = (value, inputParam) => { // Preset values if the field is format prompt values let inputValue = value if (inputParam.name === 'promptValues' && !value) { @@ -255,7 +258,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam.description && }
- {inputParam.type === 'string' && inputParam.rows && ( + {((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && ( (data.inputs[inputParam.name] = newValue)} /> )} + {inputParam.type === 'code' && ( + <> +
+
+ (data.inputs[inputParam.name] = code)} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +
+ + )} {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( setShowExpandDialog(false)} - onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)} /> )} {inputParam.type === 'json' && ( @@ -353,11 +369,11 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam?.acceptVariable && ( <> setAsyncOptionEditDialog('')} onConfirm={onConfirmAsyncOption} > + setShowExpandDialog(false)} + onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)} + >
) } diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js index 398e9eb8..6272e05f 100644 --- a/packages/ui/src/views/tools/ToolDialog.js +++ b/packages/ui/src/views/tools/ToolDialog.js @@ -12,9 +12,7 @@ import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' import { GridActionsCellItem } from '@mui/x-data-grid' import DeleteIcon from '@mui/icons-material/Delete' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' -import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' -import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' -import { useTheme } from '@mui/material/styles' +import { CodeEditor } from 'ui-component/editor/CodeEditor' // Icons import { IconX, IconFileExport } from '@tabler/icons' @@ -56,7 +54,6 @@ try { const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => { const portalElement = document.getElementById('portal') - const theme = useTheme() const customization = useSelector((state) => state.customization) const dispatch = useDispatch() @@ -490,32 +487,14 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = See Example )} - {customization.isDarkMode ? ( - setToolFunc(code)} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%', - borderRadius: 5 - }} - /> - ) : ( - setToolFunc(code)} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%', - border: `1px solid ${theme.palette.grey[300]}`, - borderRadius: 5 - }} - /> - )} + setToolFunc(code)} + /> From 6e4822c3bb9d4062528817dc157f69e86bebeda2 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 15 Dec 2023 17:49:29 +0000 Subject: [PATCH 068/502] update bedrock --- .../chatmodels/AWSBedrock/AWSChatBedrock.ts | 24 +++++++++++++------ .../AWSBedrockEmbedding.ts | 12 ++++++++-- .../nodes/llms/AWSBedrock/AWSBedrock.ts | 14 +++++++++-- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts index 956fcdb3..29faf524 100644 --- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts +++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts @@ -1,9 +1,9 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { ChatBedrock } from 'langchain/chat_models/bedrock' +import { BedrockChat } from 'langchain/chat_models/bedrock' import { BaseBedrockInput } from 'langchain/dist/util/bedrock' import { BaseCache } from 'langchain/schema' -import { BaseLLMParams } from 'langchain/llms/base' +import { BaseChatModelParams } from 'langchain/chat_models/base' /** * I had to run the following to build the component @@ -25,14 +25,14 @@ class AWSChatBedrock_ChatModels implements INode { inputs: INodeParams[] constructor() { - this.label = 'AWS Bedrock' + this.label = 'AWS ChatBedrock' this.name = 'awsChatBedrock' this.version = 3.0 this.type = 'AWSChatBedrock' this.icon = 'awsBedrock.png' this.category = 'Chat Models' this.description = 'Wrapper around AWS Bedrock large language models that use the Chat endpoint' - this.baseClasses = [this.type, ...getBaseClasses(ChatBedrock)] + this.baseClasses = [this.type, ...getBaseClasses(BedrockChat)] this.credential = { label: 'AWS Credential', name: 'credential', @@ -102,6 +102,13 @@ class AWSChatBedrock_ChatModels implements INode { ], default: 'anthropic.claude-v2' }, + { + label: 'Custom Model Name', + name: 'customModel', + description: 'If provided, will override model selected from Model Name option', + type: 'string', + optional: true + }, { label: 'Temperature', name: 'temperature', @@ -109,6 +116,7 @@ class AWSChatBedrock_ChatModels implements INode { step: 0.1, description: 'Temperature parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 0.7 }, { @@ -118,6 +126,7 @@ class AWSChatBedrock_ChatModels implements INode { step: 10, description: 'Max Tokens parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 200 } ] @@ -126,14 +135,15 @@ class AWSChatBedrock_ChatModels implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const iRegion = nodeData.inputs?.region as string const iModel = nodeData.inputs?.model as string + const customModel = nodeData.inputs?.customModel as string const iTemperature = nodeData.inputs?.temperature as string const iMax_tokens_to_sample = nodeData.inputs?.max_tokens_to_sample as string const cache = nodeData.inputs?.cache as BaseCache const streaming = nodeData.inputs?.streaming as boolean - const obj: BaseBedrockInput & BaseLLMParams = { + const obj: BaseBedrockInput & BaseChatModelParams = { region: iRegion, - model: iModel, + model: customModel ?? iModel, maxTokens: parseInt(iMax_tokens_to_sample, 10), temperature: parseFloat(iTemperature), streaming: streaming ?? true @@ -160,7 +170,7 @@ class AWSChatBedrock_ChatModels implements INode { } if (cache) obj.cache = cache - const amazonBedrock = new ChatBedrock(obj) + const amazonBedrock = new BedrockChat(obj) return amazonBedrock } } diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts index 8249d512..5f7ce17c 100644 --- a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts @@ -18,7 +18,7 @@ class AWSBedrockEmbedding_Embeddings implements INode { constructor() { this.label = 'AWS Bedrock Embeddings' this.name = 'AWSBedrockEmbeddings' - this.version = 2.0 + this.version = 3.0 this.type = 'AWSBedrockEmbeddings' this.icon = 'awsBedrock.png' this.category = 'Embeddings' @@ -86,6 +86,13 @@ class AWSBedrockEmbedding_Embeddings implements INode { { label: 'cohere.embed-multilingual-v3', name: 'cohere.embed-multilingual-v3' } ], default: 'amazon.titan-embed-text-v1' + }, + { + label: 'Custom Model Name', + name: 'customModel', + description: 'If provided, will override model selected from Model Name option', + type: 'string', + optional: true } ] } @@ -93,9 +100,10 @@ class AWSBedrockEmbedding_Embeddings implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const iRegion = nodeData.inputs?.region as string const iModel = nodeData.inputs?.model as string + const customModel = nodeData.inputs?.customModel as string const obj: BedrockEmbeddingsParams = { - model: iModel, + model: customModel ?? iModel, region: iRegion } diff --git a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts index 177a32ef..459c4296 100644 --- a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts +++ b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts @@ -27,7 +27,7 @@ class AWSBedrock_LLMs implements INode { constructor() { this.label = 'AWS Bedrock' this.name = 'awsBedrock' - this.version = 2.0 + this.version = 3.0 this.type = 'AWSBedrock' this.icon = 'awsBedrock.png' this.category = 'LLMs' @@ -105,6 +105,13 @@ class AWSBedrock_LLMs implements INode { { label: 'ai21.j2-ultra', name: 'ai21.j2-ultra' } ] }, + { + label: 'Custom Model Name', + name: 'customModel', + description: 'If provided, will override model selected from Model Name option', + type: 'string', + optional: true + }, { label: 'Temperature', name: 'temperature', @@ -112,6 +119,7 @@ class AWSBedrock_LLMs implements INode { step: 0.1, description: 'Temperature parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 0.7 }, { @@ -121,6 +129,7 @@ class AWSBedrock_LLMs implements INode { step: 10, description: 'Max Tokens parameter may not apply to certain model. Please check available model parameters', optional: true, + additionalParams: true, default: 200 } ] @@ -129,11 +138,12 @@ class AWSBedrock_LLMs implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const iRegion = nodeData.inputs?.region as string const iModel = nodeData.inputs?.model as string + const customModel = nodeData.inputs?.customModel as string const iTemperature = nodeData.inputs?.temperature as string const iMax_tokens_to_sample = nodeData.inputs?.max_tokens_to_sample as string const cache = nodeData.inputs?.cache as BaseCache const obj: Partial & BaseLLMParams = { - model: iModel, + model: customModel ?? iModel, region: iRegion, temperature: parseFloat(iTemperature), maxTokens: parseInt(iMax_tokens_to_sample, 10) From 05db5333968012abb18ea9e41c825c1e23b3f93f Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 15 Dec 2023 18:59:11 +0000 Subject: [PATCH 069/502] add mistral --- .../GoogleGenerativeAI.credential.ts | 3 +- .../credentials/MistralApi.credential.ts | 25 +++ .../ChatGoogleGenerativeAI.ts | 3 +- .../chatmodels/ChatMistral/ChatMistral.ts | 151 ++++++++++++++++++ .../chatmodels/ChatMistral/mistralai.png | Bin 0 -> 4542 bytes .../MistralEmbedding/MistralEmbedding.ts | 95 +++++++++++ .../embeddings/MistralEmbedding/mistralai.png | Bin 0 -> 4542 bytes packages/components/package.json | 1 + packages/server/src/utils/index.ts | 1 + 9 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 packages/components/credentials/MistralApi.credential.ts create mode 100644 packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts create mode 100644 packages/components/nodes/chatmodels/ChatMistral/mistralai.png create mode 100644 packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts create mode 100644 packages/components/nodes/embeddings/MistralEmbedding/mistralai.png diff --git a/packages/components/credentials/GoogleGenerativeAI.credential.ts b/packages/components/credentials/GoogleGenerativeAI.credential.ts index 9a1f3f28..e5ad45bf 100644 --- a/packages/components/credentials/GoogleGenerativeAI.credential.ts +++ b/packages/components/credentials/GoogleGenerativeAI.credential.ts @@ -11,7 +11,8 @@ class GoogleGenerativeAICredential implements INodeCredential { this.label = 'Google Generative AI' this.name = 'googleGenerativeAI' this.version = 1.0 - this.description = 'Get your API Key here.' + this.description = + 'You can get your API key from official page here.' this.inputs = [ { label: 'Google AI API Key', diff --git a/packages/components/credentials/MistralApi.credential.ts b/packages/components/credentials/MistralApi.credential.ts new file mode 100644 index 00000000..a254f665 --- /dev/null +++ b/packages/components/credentials/MistralApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MistralAICredential implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'MistralAI API' + this.name = 'mistralAIApi' + this.version = 1.0 + this.description = 'You can get your API key from official console here.' + this.inputs = [ + { + label: 'MistralAI API Key', + name: 'mistralAIAPIKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: MistralAICredential } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index 95ee0575..7044645f 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -49,8 +49,7 @@ class GoogleGenerativeAI_ChatModels implements INode { name: 'gemini-pro' } ], - default: 'gemini-pro', - optional: true + default: 'gemini-pro' }, { label: 'Temperature', diff --git a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts new file mode 100644 index 00000000..d9db85cd --- /dev/null +++ b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts @@ -0,0 +1,151 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { BaseCache } from 'langchain/schema' +import { ChatMistralAI, ChatMistralAIInput } from '@langchain/mistralai' + +class ChatMistral_ChatModels implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'ChatMistralAI' + this.name = 'chatMistralAI' + this.version = 1.0 + this.type = 'ChatMistralAI' + this.icon = 'mistralai.png' + this.category = 'Chat Models' + this.description = 'Wrapper around Mistral large language models that use the Chat endpoint' + this.baseClasses = [this.type, ...getBaseClasses(ChatMistralAI)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mistralAIApi'] + } + this.inputs = [ + { + label: 'Cache', + name: 'cache', + type: 'BaseCache', + optional: true + }, + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'mistral-tiny', + name: 'mistral-tiny' + }, + { + label: 'mistral-small', + name: 'mistral-small' + }, + { + label: 'mistral-medium', + name: 'mistral-medium' + } + ], + default: 'mistral-tiny' + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + description: + 'What sampling temperature to use, between 0.0 and 1.0. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.', + step: 0.1, + default: 0.9, + optional: true + }, + { + label: 'Max Output Tokens', + name: 'maxOutputTokens', + type: 'number', + description: 'The maximum number of tokens to generate in the completion.', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Top Probability', + name: 'topP', + type: 'number', + description: + 'Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Random Seed', + name: 'randomSeed', + type: 'number', + description: 'The seed to use for random sampling. If set, different calls will generate deterministic results.', + step: 1, + optional: true, + additionalParams: true + }, + { + label: 'Safe Mode', + name: 'safeMode', + type: 'boolean', + description: 'Whether to inject a safety prompt before all conversations.', + optional: true, + additionalParams: true + }, + { + label: 'Override Endpoint', + name: 'overrideEndpoint', + type: 'string', + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('mistralAIAPIKey', credentialData, nodeData) + + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const maxOutputTokens = nodeData.inputs?.maxOutputTokens as string + const topP = nodeData.inputs?.topP as string + const safeMode = nodeData.inputs?.safeMode as boolean + const randomSeed = nodeData.inputs?.safeMode as string + const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string + // Waiting fix from langchain to enable streaming + const streaming = nodeData.inputs?.streaming as boolean + + const cache = nodeData.inputs?.cache as BaseCache + + const obj: ChatMistralAIInput = { + apiKey: apiKey, + modelName: modelName + } + + if (maxOutputTokens) obj.maxTokens = parseInt(maxOutputTokens, 10) + if (topP) obj.topP = parseFloat(topP) + if (cache) obj.cache = cache + if (temperature) obj.temperature = parseFloat(temperature) + if (randomSeed) obj.randomSeed = parseFloat(randomSeed) + if (safeMode) obj.safeMode = safeMode + if (overrideEndpoint) obj.endpoint = overrideEndpoint + + const model = new ChatMistralAI(obj) + + return model + } +} + +module.exports = { nodeClass: ChatMistral_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatMistral/mistralai.png b/packages/components/nodes/chatmodels/ChatMistral/mistralai.png new file mode 100644 index 0000000000000000000000000000000000000000..1019f495d4d690dd639aa9f4e5751c403b2eff27 GIT binary patch literal 4542 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

GOhkykkrx z5<$gl4%{{1&%57c5Qv$XoY%TcILb6UdoL+VK0U}ckiTqHx51ZU zmwjXW&11_L&eEgCFq@TP2dU4yzzlh^iUi9p_NpSM8p1Mj^7zmjK?2}Op8*u3nr2;h Kf#<=y@BRlu0mfYb literal 0 HcmV?d00001 diff --git a/packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts b/packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts new file mode 100644 index 00000000..d0a0198c --- /dev/null +++ b/packages/components/nodes/embeddings/MistralEmbedding/MistralEmbedding.ts @@ -0,0 +1,95 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { MistralAIEmbeddings, MistralAIEmbeddingsParams } from '@langchain/mistralai' + +class MistralEmbedding_Embeddings implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'MistralAI Embeddings' + this.name = 'mistralAI Embeddings' + this.version = 1.0 + this.type = 'MistralAIEmbeddings' + this.icon = 'mistralai.png' + this.category = 'Embeddings' + this.description = 'MistralAI API to generate embeddings for a given text' + this.baseClasses = [this.type, ...getBaseClasses(MistralAIEmbeddings)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mistralAIApi'] + } + this.inputs = [ + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'mistral-embed', + name: 'mistral-embed' + } + ], + default: 'mistral-embed' + }, + { + label: 'Batch Size', + name: 'batchSize', + type: 'number', + step: 1, + default: 512, + optional: true, + additionalParams: true + }, + { + label: 'Strip New Lines', + name: 'stripNewLines', + type: 'boolean', + default: true, + optional: true, + additionalParams: true + }, + { + label: 'Override Endpoint', + name: 'overrideEndpoint', + type: 'string', + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const modelName = nodeData.inputs?.modelName as string + const batchSize = nodeData.inputs?.batchSize as string + const stripNewLines = nodeData.inputs?.stripNewLines as boolean + const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('mistralAIAPIKey', credentialData, nodeData) + + const obj: MistralAIEmbeddingsParams = { + apiKey: apiKey, + modelName: modelName + } + + if (batchSize) obj.batchSize = parseInt(batchSize, 10) + if (stripNewLines) obj.stripNewLines = stripNewLines + if (overrideEndpoint) obj.endpoint = overrideEndpoint + + const model = new MistralAIEmbeddings(obj) + return model + } +} + +module.exports = { nodeClass: MistralEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/MistralEmbedding/mistralai.png b/packages/components/nodes/embeddings/MistralEmbedding/mistralai.png new file mode 100644 index 0000000000000000000000000000000000000000..1019f495d4d690dd639aa9f4e5751c403b2eff27 GIT binary patch literal 4542 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

GOhkykkrx z5<$gl4%{{1&%57c5Qv$XoY%TcILb6UdoL+VK0U}ckiTqHx51ZU zmwjXW&11_L&eEgCFq@TP2dU4yzzlh^iUi9p_NpSM8p1Mj^7zmjK?2}Op8*u3nr2;h Kf#<=y@BRlu0mfYb literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 6bdc908a..cbc347ff 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -27,6 +27,7 @@ "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", "@langchain/google-genai": "^0.0.3", + "@langchain/mistralai": "^0.0.2", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", "@pinecone-database/pinecone": "^1.1.1", diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 2bf1c04a..5f12c66e 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -711,6 +711,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component /** * Check to see if flow valid for stream + * TODO: perform check from component level. i.e: set streaming on component, and check here * @param {IReactFlowNode[]} reactFlowNodes * @param {INodeData} endingNodeData * @returns {boolean} From 5a66d238699212e1a759aa7d86261fbb8f065aeb Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 15 Dec 2023 23:53:47 +0000 Subject: [PATCH 070/502] Update package.json --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index cbc347ff..52e59b41 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -27,7 +27,7 @@ "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", "@langchain/google-genai": "^0.0.3", - "@langchain/mistralai": "^0.0.2", + "@langchain/mistralai": "^0.0.3", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", "@pinecone-database/pinecone": "^1.1.1", From 0707072a3e389dbeddf42a2a339ed40eb58fe6ca Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 15 Dec 2023 23:54:39 +0000 Subject: [PATCH 071/502] Update ChatMistral.ts --- packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts index d9db85cd..9e12251f 100644 --- a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts +++ b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts @@ -124,7 +124,7 @@ class ChatMistral_ChatModels implements INode { const safeMode = nodeData.inputs?.safeMode as boolean const randomSeed = nodeData.inputs?.safeMode as string const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string - // Waiting fix from langchain to enable streaming + // Waiting fix from langchain + mistral to enable streaming - https://github.com/mistralai/client-js/issues/18 const streaming = nodeData.inputs?.streaming as boolean const cache = nodeData.inputs?.cache as BaseCache From e8af8b007a8f8b06d5014f59f2bacd0bf6376414 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 17 Dec 2023 18:58:37 +0000 Subject: [PATCH 072/502] add marketplace template --- .../marketplaces/chatflows/SQL Prompt.json | 1237 +++++++++++++++++ packages/server/src/index.ts | 7 +- .../ui/src/views/canvas/NodeInputHandler.js | 1 + 3 files changed, 1242 insertions(+), 3 deletions(-) create mode 100644 packages/server/marketplaces/chatflows/SQL Prompt.json diff --git a/packages/server/marketplaces/chatflows/SQL Prompt.json b/packages/server/marketplaces/chatflows/SQL Prompt.json new file mode 100644 index 00000000..9244e8de --- /dev/null +++ b/packages/server/marketplaces/chatflows/SQL Prompt.json @@ -0,0 +1,1237 @@ +{ + "description": "Manually construct prompts to query a SQL database", + "badge": "new", + "nodes": [ + { + "width": 300, + "height": 511, + "id": "promptTemplate_0", + "position": { + "x": 638.5481508577102, + "y": 84.0454315632386 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_0", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_0-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Based on the provided SQL table schema and question below, return a SQL SELECT ALL query that would answer the user's question. For example: SELECT * FROM table WHERE id = '1'.\n------------\nSCHEMA: {schema}\n------------\nQUESTION: {question}\n------------\nSQL QUERY:", + "promptValues": "{\"schema\":\"{{setVariable_0.data.instance}}\",\"question\":\"{{question}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 638.5481508577102, + "y": 84.0454315632386 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_0", + "position": { + "x": 1095.1973126620626, + "y": -83.98379829183628 + }, + "type": "customNode", + "data": { + "id": "llmChain_0", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_0-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_0-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_0-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_0-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_0-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_0.data.instance}}", + "prompt": "{{promptTemplate_0.data.instance}}", + "outputParser": "", + "inputModeration": "", + "chainName": "SQL Query Chain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_0-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_0-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "outputPrediction" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1095.1973126620626, + "y": -83.98379829183628 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 636.5762708317321, + "y": -543.3151550847003 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0", + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 636.5762708317321, + "y": -543.3151550847003 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_1", + "position": { + "x": 2636.1598769864936, + "y": -653.0025971757484 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_1", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_1-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_1-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_1-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_1-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_1-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0", + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 2636.1598769864936, + "y": -653.0025971757484 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_1", + "position": { + "x": 3089.9937691022837, + "y": -109.24001734925716 + }, + "type": "customNode", + "data": { + "id": "llmChain_1", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_1-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_1-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_1-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_1-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_1-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_1.data.instance}}", + "prompt": "{{promptTemplate_1.data.instance}}", + "outputParser": "", + "inputModeration": "", + "chainName": "Final Chain" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_1-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_1-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 3089.9937691022837, + "y": -109.24001734925716 + }, + "dragging": false + }, + { + "width": 300, + "height": 669, + "id": "customFunction_2", + "position": { + "x": -152.63957160907668, + "y": -212.74538890862547 + }, + "type": "customNode", + "data": { + "id": "customFunction_2", + "label": "Custom JS Function", + "version": 1, + "name": "customFunction", + "type": "CustomFunction", + "baseClasses": ["CustomFunction", "Utilities"], + "category": "Utilities", + "description": "Execute custom javascript function", + "inputParams": [ + { + "label": "Input Variables", + "name": "functionInputVariables", + "description": "Input variables can be used in the function with prefix $. For example: $var", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "customFunction_2-input-functionInputVariables-json" + }, + { + "label": "Function Name", + "name": "functionName", + "type": "string", + "placeholder": "My Function", + "id": "customFunction_2-input-functionName-string" + }, + { + "label": "Javascript Function", + "name": "javascriptFunction", + "type": "code", + "id": "customFunction_2-input-javascriptFunction-code" + } + ], + "inputAnchors": [], + "inputs": { + "functionInputVariables": "", + "functionName": "Get SQL Schema Prompt", + "javascriptFunction": "const HOST = 'singlestore-host';\nconst USER = 'admin';\nconst PASSWORD = 'mypassword';\nconst DATABASE = 'mydb';\nconst TABLE = 'samples';\nconst mysql = require('mysql2/promise');\n\nlet sqlSchemaPrompt;\n\n/**\n * Ideal prompt contains schema info and examples\n * Follows best practices as specified form https://arxiv.org/abs/2204.00498\n * =========================================\n * CREATE TABLE samples (firstName varchar NOT NULL, lastName varchar)\n * SELECT * FROM samples LIMIT 3\n * firstName lastName\n * Stephen Tyler\n * Jack McGinnis\n * Steven Repici\n * =========================================\n*/\nfunction getSQLPrompt() {\n return new Promise(async (resolve, reject) => {\n \n const singleStoreConnection = mysql.createPool({\n host: HOST,\n user: USER,\n password: PASSWORD,\n database: DATABASE,\n });\n\n // Get schema info\n const [schemaInfo] = await singleStoreConnection.execute(\n `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = \"${TABLE}\"`\n );\n\n const createColumns = [];\n const columnNames = [];\n\n for (const schemaData of schemaInfo) {\n columnNames.push(`${schemaData['COLUMN_NAME']}`);\n createColumns.push(`${schemaData['COLUMN_NAME']} ${schemaData['COLUMN_TYPE']} ${schemaData['IS_NULLABLE'] === 'NO' ? 'NOT NULL' : ''}`);\n }\n\n const sqlCreateTableQuery = `CREATE TABLE samples (${createColumns.join(', ')})`;\n const sqlSelectTableQuery = `SELECT * FROM samples LIMIT 3`;\n\n // Get first 3 rows\n const [rows] = await singleStoreConnection.execute(\n sqlSelectTableQuery,\n );\n \n const allValues = [];\n for (const row of rows) {\n const rowValues = [];\n for (const colName in row) {\n rowValues.push(row[colName]);\n }\n allValues.push(rowValues.join(' '));\n }\n\n sqlSchemaPrompt = sqlCreateTableQuery + '\\n' + sqlSelectTableQuery + '\\n' + columnNames.join(' ') + '\\n' + allValues.join('\\n');\n \n resolve();\n });\n}\n\nasync function main() {\n await getSQLPrompt();\n}\n\nawait main();\n\nreturn sqlSchemaPrompt;" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "customFunction_2-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -152.63957160907668, + "y": -212.74538890862547 + }, + "dragging": false + }, + { + "width": 300, + "height": 669, + "id": "customFunction_1", + "position": { + "x": 1887.4670208331604, + "y": -275.95340782935716 + }, + "type": "customNode", + "data": { + "id": "customFunction_1", + "label": "Custom JS Function", + "version": 1, + "name": "customFunction", + "type": "CustomFunction", + "baseClasses": ["CustomFunction", "Utilities"], + "category": "Utilities", + "description": "Execute custom javascript function", + "inputParams": [ + { + "label": "Input Variables", + "name": "functionInputVariables", + "description": "Input variables can be used in the function with prefix $. For example: $var", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "customFunction_1-input-functionInputVariables-json" + }, + { + "label": "Function Name", + "name": "functionName", + "type": "string", + "placeholder": "My Function", + "id": "customFunction_1-input-functionName-string" + }, + { + "label": "Javascript Function", + "name": "javascriptFunction", + "type": "code", + "id": "customFunction_1-input-javascriptFunction-code" + } + ], + "inputAnchors": [], + "inputs": { + "functionInputVariables": "{\"sqlQuery\":\"{{setVariable_1.data.instance}}\"}", + "functionName": "Run SQL Query", + "javascriptFunction": "const HOST = 'singlestore-host';\nconst USER = 'admin';\nconst PASSWORD = 'mypassword';\nconst DATABASE = 'mydb';\nconst TABLE = 'samples';\nconst mysql = require('mysql2/promise');\n\nlet result;\n\nfunction getSQLResult() {\n return new Promise(async (resolve, reject) => {\n \n const singleStoreConnection = mysql.createPool({\n host: HOST,\n user: USER,\n password: PASSWORD,\n database: DATABASE,\n });\n \n const [rows] = await singleStoreConnection.execute(\n $sqlQuery\n );\n\n result = JSON.stringify(rows)\n \n resolve();\n });\n}\n\nasync function main() {\n await getSQLResult();\n}\n\nawait main();\n\nreturn result;" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "customFunction_1-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1887.4670208331604, + "y": -275.95340782935716 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "promptTemplate_1", + "position": { + "x": 2638.3935631956588, + "y": -18.55855423639423 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_1", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_1-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_1-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Based on the table schema below, question, SQL query, and SQL response, write a natural language response, be details as possible:\n------------\nSCHEMA: {schema}\n------------\nQUESTION: {question}\n------------\nSQL QUERY: {sqlQuery}\n------------\nSQL RESPONSE: {sqlResponse}\n------------\nNATURAL LANGUAGE RESPONSE:", + "promptValues": "{\"schema\":\"{{getVariable_0.data.instance}}\",\"question\":\"{{question}}\",\"sqlResponse\":\"{{customFunction_1.data.instance}}\",\"sqlQuery\":\"{{getVariable_1.data.instance}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 2638.3935631956588, + "y": -18.55855423639423 + } + }, + { + "width": 300, + "height": 355, + "id": "setVariable_0", + "position": { + "x": 247.02296459986826, + "y": -60.27462140472403 + }, + "type": "customNode", + "data": { + "id": "setVariable_0", + "label": "Set Variable", + "version": 1, + "name": "setVariable", + "type": "SetVariable", + "baseClasses": ["SetVariable", "Utilities"], + "category": "Utilities", + "description": "Set variable which can be retrieved at a later stage. Variable is only available during runtime.", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "setVariable_0-input-variableName-string" + } + ], + "inputAnchors": [ + { + "label": "Input", + "name": "input", + "type": "string | number | boolean | json | array", + "optional": true, + "list": true, + "id": "setVariable_0-input-input-string | number | boolean | json | array" + } + ], + "inputs": { + "input": ["{{customFunction_2.data.instance}}"], + "variableName": "schemaPrompt" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "setVariable_0-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 247.02296459986826, + "y": -60.27462140472403 + }, + "dragging": false + }, + { + "width": 300, + "height": 304, + "id": "getVariable_0", + "position": { + "x": 2248.4540716891547, + "y": -47.21232652005119 + }, + "type": "customNode", + "data": { + "id": "getVariable_0", + "label": "Get Variable", + "version": 1, + "name": "getVariable", + "type": "GetVariable", + "baseClasses": ["GetVariable", "Utilities"], + "category": "Utilities", + "description": "Get variable that was saved using Set Variable node", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "getVariable_0-input-variableName-string" + } + ], + "inputAnchors": [], + "inputs": { + "variableName": "schemaPrompt" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "getVariable_0-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "positionAbsolute": { + "x": 2248.4540716891547, + "y": -47.21232652005119 + }, + "selected": false, + "dragging": false + }, + { + "width": 300, + "height": 304, + "id": "getVariable_1", + "position": { + "x": 2256.0258940322105, + "y": 437.4363694364632 + }, + "type": "customNode", + "data": { + "id": "getVariable_1", + "label": "Get Variable", + "version": 1, + "name": "getVariable", + "type": "GetVariable", + "baseClasses": ["GetVariable", "Utilities"], + "category": "Utilities", + "description": "Get variable that was saved using Set Variable node", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "getVariable_1-input-variableName-string" + } + ], + "inputAnchors": [], + "inputs": { + "variableName": "sqlQuery" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "getVariable_1-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "positionAbsolute": { + "x": 2256.0258940322105, + "y": 437.4363694364632 + }, + "selected": false, + "dragging": false + }, + { + "width": 300, + "height": 355, + "id": "setVariable_1", + "position": { + "x": 1482.8091395089693, + "y": -33.943355212355016 + }, + "type": "customNode", + "data": { + "id": "setVariable_1", + "label": "Set Variable", + "version": 1, + "name": "setVariable", + "type": "SetVariable", + "baseClasses": ["SetVariable", "Utilities"], + "category": "Utilities", + "description": "Set variable which can be retrieved at a later stage. Variable is only available during runtime.", + "inputParams": [ + { + "label": "Variable Name", + "name": "variableName", + "type": "string", + "placeholder": "var1", + "id": "setVariable_1-input-variableName-string" + } + ], + "inputAnchors": [ + { + "label": "Input", + "name": "input", + "type": "string | number | boolean | json | array", + "optional": true, + "list": true, + "id": "setVariable_1-input-input-string | number | boolean | json | array" + } + ], + "inputs": { + "input": ["{{llmChain_0.data.instance}}"], + "variableName": "sqlQuery" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "setVariable_1-output-output-string|number|boolean|json|array", + "name": "output", + "label": "Output", + "type": "string | number | boolean | json | array" + } + ], + "default": "output" + } + ], + "outputs": { + "output": "output" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1482.8091395089693, + "y": -33.943355212355016 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "promptTemplate_0", + "sourceHandle": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_0-promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_0-llmChain_0-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_0-llmChain_0-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "chatOpenAI_1", + "sourceHandle": "chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_1", + "targetHandle": "llmChain_1-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_1-chatOpenAI_1-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_1-llmChain_1-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "customFunction_1", + "sourceHandle": "customFunction_1-output-output-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "customFunction_1-customFunction_1-output-output-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_1", + "sourceHandle": "promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_1", + "targetHandle": "llmChain_1-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_1-promptTemplate_1-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_1-llmChain_1-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "customFunction_2", + "sourceHandle": "customFunction_2-output-output-string|number|boolean|json|array", + "target": "setVariable_0", + "targetHandle": "setVariable_0-input-input-string | number | boolean | json | array", + "type": "buttonedge", + "id": "customFunction_2-customFunction_2-output-output-string|number|boolean|json|array-setVariable_0-setVariable_0-input-input-string | number | boolean | json | array", + "data": { + "label": "" + } + }, + { + "source": "setVariable_0", + "sourceHandle": "setVariable_0-output-output-string|number|boolean|json|array", + "target": "promptTemplate_0", + "targetHandle": "promptTemplate_0-input-promptValues-json", + "type": "buttonedge", + "id": "setVariable_0-setVariable_0-output-output-string|number|boolean|json|array-promptTemplate_0-promptTemplate_0-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "getVariable_0", + "sourceHandle": "getVariable_0-output-output-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "getVariable_0-getVariable_0-output-output-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "getVariable_1", + "sourceHandle": "getVariable_1-output-output-string|number|boolean|json|array", + "target": "promptTemplate_1", + "targetHandle": "promptTemplate_1-input-promptValues-json", + "type": "buttonedge", + "id": "getVariable_1-getVariable_1-output-output-string|number|boolean|json|array-promptTemplate_1-promptTemplate_1-input-promptValues-json", + "data": { + "label": "" + } + }, + { + "source": "llmChain_0", + "sourceHandle": "llmChain_0-output-outputPrediction-string|json", + "target": "setVariable_1", + "targetHandle": "setVariable_1-input-input-string | number | boolean | json | array", + "type": "buttonedge", + "id": "llmChain_0-llmChain_0-output-outputPrediction-string|json-setVariable_1-setVariable_1-input-input-string | number | boolean | json | array", + "data": { + "label": "" + } + }, + { + "source": "setVariable_1", + "sourceHandle": "setVariable_1-output-output-string|number|boolean|json|array", + "target": "customFunction_1", + "targetHandle": "customFunction_1-input-functionInputVariables-json", + "type": "buttonedge", + "id": "setVariable_1-setVariable_1-output-output-string|number|boolean|json|array-customFunction_1-customFunction_1-input-functionInputVariables-json", + "data": { + "label": "" + } + } + ] +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 85e9eac4..9c31a333 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -55,7 +55,7 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' +import { ICommonObject, IMessage, INodeOptionsValue, handleEscapeCharacters } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' import { sanitizeMiddleware } from './utils/XSS' @@ -291,9 +291,10 @@ export class App { const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() - const returnOptions: INodeOptionsValue[] = await newNodeInstance.init(nodeData) + const returnData = await newNodeInstance.init(nodeData) + const result = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData - return res.json(returnOptions) + return res.json(result) } catch (error) { return res.status(500).send(`Error running custom function: ${error}`) } diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 18e82338..92a43cf8 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -371,6 +371,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {dialogProps.type !== 'TEMPLATE' && (

{ value={variableName ?? ''} />
- -
- - Value - - -
-
- setVariableValue(e.target.value)} - value={variableValue ?? ''} - /> - Leave the value empty for runtime variables. Will be populated at runtime. -
@@ -245,14 +231,30 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { onSelect={(newValue) => setVariableType(newValue)} value={variableType} /> - - Runtime: Value would be populated from env. Static: Value would be used as is. - + {variableType === 'static' && ( + +
+ + Value * + +
+
+ setVariableValue(e.target.value)} + value={variableValue ?? ''} + /> +
+ )} (dialogType === 'ADD' ? addNewVariable() : saveVariable())} > diff --git a/packages/ui/src/views/variables/HowToUseVariablesDialog.js b/packages/ui/src/views/variables/HowToUseVariablesDialog.js new file mode 100644 index 00000000..f328f226 --- /dev/null +++ b/packages/ui/src/views/variables/HowToUseVariablesDialog.js @@ -0,0 +1,72 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + +const overrideConfig = `{ + overrideConfig: { + vars: { + var1: 'abc' + } + } +}` + +const HowToUseVariablesDialog = ({ show, onCancel }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + How To Use Variables + + +

Variables can be used in Custom Tool Function with the $ prefix.

+ `} + height={'50px'} + theme={'dark'} + lang={'js'} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +

+ If variable type is Static, the value will be retrieved as it is. If variable type is Runtime, the value will be + retrieved from .env file. +

+

+ You can also override variable values in API overrideConfig using vars: +

+ +

+ Read more from{' '} + + docs + +

+ + + ) : null + + return createPortal(component, portalElement) +} + +HowToUseVariablesDialog.propTypes = { + show: PropTypes.bool, + onCancel: PropTypes.func +} + +export default HowToUseVariablesDialog diff --git a/packages/ui/src/views/variables/index.js b/packages/ui/src/views/variables/index.js index 9399eb20..ca8a3dee 100644 --- a/packages/ui/src/views/variables/index.js +++ b/packages/ui/src/views/variables/index.js @@ -19,7 +19,8 @@ import { Toolbar, TextField, InputAdornment, - ButtonGroup + ButtonGroup, + Chip } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -40,10 +41,11 @@ import useNotifier from 'utils/useNotifier' // Icons import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons' -import CredentialEmptySVG from 'assets/images/credential_empty.svg' +import VariablesEmptySVG from 'assets/images/variables_empty.svg' // const import AddEditVariableDialog from './AddEditVariableDialog' +import HowToUseVariablesDialog from './HowToUseVariablesDialog' // ==============================|| Credentials ||============================== // @@ -60,6 +62,7 @@ const Variables = () => { const [showVariableDialog, setShowVariableDialog] = useState(false) const [variableDialogProps, setVariableDialogProps] = useState({}) const [variables, setVariables] = useState([]) + const [showHowToDialog, setShowHowToDialog] = useState(false) const { confirm } = useConfirm() @@ -173,7 +176,7 @@ const Variables = () => { width: '100%' }} > -

Environment Variables 

+

Variables 

{ }} /> + { 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 44294d00677e5b3df97370164af64caadcadbccc Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 21 Dec 2023 20:37:04 +0000 Subject: [PATCH 120/502] update default location for encryption key --- packages/components/src/utils.ts | 14 ++++++++++++-- packages/server/src/utils/index.ts | 14 +++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 98936817..757ecc65 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -428,7 +428,17 @@ export const getEnvironmentVariable = (name: string): string | undefined => { * @returns {string} */ const getEncryptionKeyFilePath = (): string => { - const checkPaths = [path.join(getUserHome(), '.flowise', 'encryption.key')] + const checkPaths = [ + path.join(__dirname, '..', '..', 'encryption.key'), + path.join(__dirname, '..', '..', 'server', 'encryption.key'), + path.join(__dirname, '..', '..', '..', 'encryption.key'), + path.join(__dirname, '..', '..', '..', 'server', 'encryption.key'), + path.join(__dirname, '..', '..', '..', '..', 'encryption.key'), + path.join(__dirname, '..', '..', '..', '..', 'server', 'encryption.key'), + path.join(__dirname, '..', '..', '..', '..', '..', 'encryption.key'), + path.join(__dirname, '..', '..', '..', '..', '..', 'server', 'encryption.key'), + path.join(getUserHome(), '.flowise', 'encryption.key') + ] for (const checkPath of checkPaths) { if (fs.existsSync(checkPath)) { return checkPath @@ -437,7 +447,7 @@ const getEncryptionKeyFilePath = (): string => { return '' } -const getEncryptionKeyPath = (): string => { +export const getEncryptionKeyPath = (): string => { return process.env.SECRETKEY_PATH ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') : getEncryptionKeyFilePath() } diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index e0c91723..39bd0854 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -23,6 +23,7 @@ import { convertChatHistoryToText, getInputVariables, handleEscapeCharacters, + getEncryptionKeyPath, ICommonObject, IDatabaseEntity, IMessage @@ -852,16 +853,6 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod return isChatOrLLMsExist && isValidChainOrAgent && !isOutputParserExist } -/** - * Returns the path of encryption key - * @returns {string} - */ -export const getEncryptionKeyPath = (): string => { - return process.env.SECRETKEY_PATH - ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') - : path.join(getUserHome(), '.flowise', 'encryption.key') -} - /** * Generate an encryption key * @returns {string} @@ -882,7 +873,8 @@ export const getEncryptionKey = async (): Promise => { return await fs.promises.readFile(getEncryptionKeyPath(), 'utf8') } catch (error) { const encryptKey = generateEncryptKey() - await fs.promises.writeFile(getEncryptionKeyPath(), encryptKey) + const defaultLocation = path.join(getUserHome(), '.flowise', 'encryption.key') + await fs.promises.writeFile(defaultLocation, encryptKey) return encryptKey } } From 1a4ead3544fa0be086cb8ec174b2da4d2c0cd647 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 22 Dec 2023 01:58:40 +0000 Subject: [PATCH 121/502] update S3 loader --- .../nodes/documentloaders/S3File/S3File.ts | 77 ++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/packages/components/nodes/documentloaders/S3File/S3File.ts b/packages/components/nodes/documentloaders/S3File/S3File.ts index 58ffd8af..eadb4d99 100644 --- a/packages/components/nodes/documentloaders/S3File/S3File.ts +++ b/packages/components/nodes/documentloaders/S3File/S3File.ts @@ -30,7 +30,7 @@ class S3_DocumentLoaders implements INode { constructor() { this.label = 'S3' this.name = 'S3' - this.version = 1.0 + this.version = 2.0 this.type = 'Document' this.icon = 's3.svg' this.category = 'Document Loaders' @@ -113,12 +113,62 @@ class S3_DocumentLoaders implements INode { optional: true }, { - label: 'NarrativeText Only', - name: 'narrativeTextOnly', + label: 'Element Type', + name: 'elementType', description: - 'Only load documents with NarrativeText metadata from Unstructured. See how Unstructured partition data here', - default: true, - type: 'boolean', + 'Unstructured partition document into different types, select the types to return. If not selected, all types will be returned', + type: 'multiOptions', + options: [ + { + label: 'FigureCaption', + name: 'FigureCaption' + }, + { + label: 'NarrativeText', + name: 'NarrativeText' + }, + { + label: 'ListItem', + name: 'ListItem' + }, + { + label: 'Title', + name: 'Title' + }, + { + label: 'Address', + name: 'Address' + }, + { + label: 'Table', + name: 'Table' + }, + { + label: 'PageBreak', + name: 'PageBreak' + }, + { + label: 'Header', + name: 'Header' + }, + { + label: 'Footer', + name: 'Footer' + }, + { + label: 'UncategorizedText', + name: 'UncategorizedText' + }, + { + label: 'Image', + name: 'Image' + }, + { + label: 'Formula', + name: 'Formula' + } + ], + default: [], optional: true, additionalParams: true }, @@ -138,7 +188,7 @@ class S3_DocumentLoaders implements INode { const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string const unstructuredAPIKey = nodeData.inputs?.unstructuredAPIKey as string const metadata = nodeData.inputs?.metadata - const narrativeTextOnly = nodeData.inputs?.narrativeTextOnly as boolean + const elementType = nodeData.inputs?.elementType as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessKeyId = getCredentialParam('awsKey', credentialData, nodeData) @@ -169,6 +219,15 @@ class S3_DocumentLoaders implements INode { } } + let elementTypes: string[] = [] + if (elementType) { + try { + elementTypes = JSON.parse(elementType) + } catch (e) { + elementTypes = [] + } + } + loader.load = async () => { const tempDir = fsDefault.mkdtempSync(path.join(os.tmpdir(), 's3fileloader-')) @@ -235,10 +294,10 @@ class S3_DocumentLoaders implements INode { } } }) - return narrativeTextOnly ? finaldocs.filter((doc) => doc.metadata.category === 'NarrativeText') : finaldocs + return elementTypes.length ? finaldocs.filter((doc) => elementTypes.includes(doc.metadata.category)) : finaldocs } - return narrativeTextOnly ? docs.filter((doc) => doc.metadata.category === 'NarrativeText') : docs + return elementTypes.length ? docs.filter((doc) => elementTypes.includes(doc.metadata.category)) : docs } } module.exports = { nodeClass: S3_DocumentLoaders } From 1ffa56897446cb36bd1e0a5699b6bc9c89e844a4 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 09:08:53 +0530 Subject: [PATCH 122/502] Addition of Pinecone MMR search --- .../nodes/vectorstores/Pinecone/Pinecone.ts | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index 4e6967bc..3f2e3ef1 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -23,11 +23,11 @@ class Pinecone_VectorStores implements INode { constructor() { this.label = 'Pinecone' this.name = 'pinecone' - this.version = 1.0 + this.version = 2.0 this.type = 'Pinecone' this.icon = 'pinecone.svg' this.category = 'Vector Stores' - this.description = `Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database` + this.description = `Upsert embedded data and perform search upon query using Pinecone, a leading fully managed hosted vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -77,6 +77,43 @@ class Pinecone_VectorStores implements INode { type: 'number', additionalParams: true, optional: true + }, + { + label: 'Search Type', + name: 'searchType', + type: 'options', + default: 'similarity', + options: [ + { + label: 'Similarity', + name: 'similarity' + }, + { + label: 'Max Marginal Relevance', + name: 'mmr' + } + ], + additionalParams: true, + optional: true + }, + { + label: 'Fetch K (for MMR Search)', + name: 'fetchK', + description: 'Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR', + placeholder: '20', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'Lambda (for MMR Search)', + name: 'lambda', + description: + 'Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR', + placeholder: '0.5', + type: 'number', + additionalParams: true, + optional: true } ] this.outputs = [ @@ -141,6 +178,7 @@ class Pinecone_VectorStores implements INode { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const output = nodeData.outputs?.output as string + const searchType = nodeData.outputs?.searchType as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 @@ -176,8 +214,25 @@ class Pinecone_VectorStores implements INode { const vectorStore = await PineconeStore.fromExistingIndex(embeddings, obj) if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever + if ('mmr' === searchType) { + const fetchK = nodeData.inputs?.fetchK as string + const lambda = nodeData.inputs?.lambda as string + const f = fetchK ? parseInt(fetchK) : 20 + const l = lambda ? parseFloat(lambda) : 0.5 + const retriever = vectorStore.asRetriever({ + searchType: 'mmr', + k: 5, + searchKwargs: { + fetchK: f, + lambda: l + } + }) + return retriever + } else { + // "searchType" is "similarity" + const retriever = vectorStore.asRetriever(k) + return retriever + } } else if (output === 'vectorStore') { ;(vectorStore as any).k = k return vectorStore From 579f66e57e3da71e0a53db10cf1baeb986d4bb31 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 09:20:55 +0530 Subject: [PATCH 123/502] Updating langchain to 0.0.198 --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 9cb0bf1e..018d7a77 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -52,7 +52,7 @@ "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "langchain": "^0.0.196", + "langchain": "^0.0.198", "langfuse": "^1.2.0", "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.49", From 56b043264a5370ac041f8fde1adf928b5068eace Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 12:08:48 +0530 Subject: [PATCH 124/502] MMR Search for Pinecone, Weaviate, Zep and Supabase --- .../nodes/vectorstores/Pinecone/Pinecone.ts | 73 ++---------------- .../nodes/vectorstores/Supabase/Supabase.ts | 18 ++--- .../nodes/vectorstores/VectorStoreUtils.ts | 76 +++++++++++++++++++ .../nodes/vectorstores/Weaviate/Weaviate.ts | 18 ++--- .../components/nodes/vectorstores/Zep/Zep.ts | 22 ++---- 5 files changed, 98 insertions(+), 109 deletions(-) create mode 100644 packages/components/nodes/vectorstores/VectorStoreUtils.ts diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index 3f2e3ef1..4b91a9b5 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Pinecone_VectorStores implements INode { label: string @@ -23,11 +24,11 @@ class Pinecone_VectorStores implements INode { constructor() { this.label = 'Pinecone' this.name = 'pinecone' - this.version = 2.0 + this.version = 3.0 this.type = 'Pinecone' this.icon = 'pinecone.svg' this.category = 'Vector Stores' - this.description = `Upsert embedded data and perform search upon query using Pinecone, a leading fully managed hosted vector database` + this.description = `Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -77,45 +78,9 @@ class Pinecone_VectorStores implements INode { type: 'number', additionalParams: true, optional: true - }, - { - label: 'Search Type', - name: 'searchType', - type: 'options', - default: 'similarity', - options: [ - { - label: 'Similarity', - name: 'similarity' - }, - { - label: 'Max Marginal Relevance', - name: 'mmr' - } - ], - additionalParams: true, - optional: true - }, - { - label: 'Fetch K (for MMR Search)', - name: 'fetchK', - description: 'Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR', - placeholder: '20', - type: 'number', - additionalParams: true, - optional: true - }, - { - label: 'Lambda (for MMR Search)', - name: 'lambda', - description: - 'Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR', - placeholder: '0.5', - type: 'number', - additionalParams: true, - optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Pinecone Retriever', @@ -177,10 +142,6 @@ class Pinecone_VectorStores implements INode { const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings - const output = nodeData.outputs?.output as string - const searchType = nodeData.outputs?.searchType as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) @@ -213,31 +174,7 @@ class Pinecone_VectorStores implements INode { const vectorStore = await PineconeStore.fromExistingIndex(embeddings, obj) - if (output === 'retriever') { - if ('mmr' === searchType) { - const fetchK = nodeData.inputs?.fetchK as string - const lambda = nodeData.inputs?.lambda as string - const f = fetchK ? parseInt(fetchK) : 20 - const l = lambda ? parseFloat(lambda) : 0.5 - const retriever = vectorStore.asRetriever({ - searchType: 'mmr', - k: 5, - searchKwargs: { - fetchK: f, - lambda: l - } - }) - return retriever - } else { - // "searchType" is "similarity" - const retriever = vectorStore.asRetriever(k) - return retriever - } - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/Supabase/Supabase.ts b/packages/components/nodes/vectorstores/Supabase/Supabase.ts index 13840ab7..a5477914 100644 --- a/packages/components/nodes/vectorstores/Supabase/Supabase.ts +++ b/packages/components/nodes/vectorstores/Supabase/Supabase.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { SupabaseLibArgs, SupabaseVectorStore } from 'langchain/vectorstores/supabase' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Supabase_VectorStores implements INode { label: string @@ -23,11 +24,11 @@ class Supabase_VectorStores implements INode { constructor() { this.label = 'Supabase' this.name = 'supabase' - this.version = 1.0 + this.version = 2.0 this.type = 'Supabase' this.icon = 'supabase.svg' this.category = 'Vector Stores' - this.description = 'Upsert embedded data and perform similarity search upon query using Supabase via pgvector extension' + this.description = 'Upsert embedded data and perform similarity or mmr search upon query using Supabase via pgvector extension' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -81,6 +82,7 @@ class Supabase_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Supabase Retriever', @@ -135,9 +137,6 @@ class Supabase_VectorStores implements INode { const queryName = nodeData.inputs?.queryName as string const embeddings = nodeData.inputs?.embeddings as Embeddings const supabaseMetadataFilter = nodeData.inputs?.supabaseMetadataFilter - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const supabaseApiKey = getCredentialParam('supabaseApiKey', credentialData, nodeData) @@ -157,14 +156,7 @@ class Supabase_VectorStores implements INode { const vectorStore = await SupabaseVectorStore.fromExistingIndex(embeddings, obj) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/VectorStoreUtils.ts b/packages/components/nodes/vectorstores/VectorStoreUtils.ts new file mode 100644 index 00000000..b63a4121 --- /dev/null +++ b/packages/components/nodes/vectorstores/VectorStoreUtils.ts @@ -0,0 +1,76 @@ +import { INodeData } from '../../src' + +export const resolveVectorStoreOrRetriever = (nodeData: INodeData, vectorStore: any) => { + const output = nodeData.outputs?.output as string + const searchType = nodeData.outputs?.searchType as string + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + + if (output === 'retriever') { + if ('mmr' === searchType) { + const fetchK = nodeData.inputs?.fetchK as string + const lambda = nodeData.inputs?.lambda as string + const f = fetchK ? parseInt(fetchK) : 20 + const l = lambda ? parseFloat(lambda) : 0.5 + const retriever = vectorStore.asRetriever({ + searchType: 'mmr', + k: k, + searchKwargs: { + fetchK: f, + lambda: l + } + }) + return retriever + } else { + // "searchType" is "similarity" + return vectorStore.asRetriever(k) + } + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } +} + +export const addMMRInputParams = (inputs: any[]) => { + const mmrInputParams = [ + { + label: 'Search Type', + name: 'searchType', + type: 'options', + default: 'similarity', + options: [ + { + label: 'Similarity', + name: 'similarity' + }, + { + label: 'Max Marginal Relevance', + name: 'mmr' + } + ], + additionalParams: true, + optional: true + }, + { + label: 'Fetch K (for MMR Search)', + name: 'fetchK', + description: 'Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR', + placeholder: '20', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'Lambda (for MMR Search)', + name: 'lambda', + description: + 'Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR', + placeholder: '0.5', + type: 'number', + additionalParams: true, + optional: true + } + ] + + inputs.push(...mmrInputParams) +} diff --git a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts index 5c31c737..0eface58 100644 --- a/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts +++ b/packages/components/nodes/vectorstores/Weaviate/Weaviate.ts @@ -5,6 +5,7 @@ import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Weaviate_VectorStores implements INode { label: string @@ -23,12 +24,12 @@ class Weaviate_VectorStores implements INode { constructor() { this.label = 'Weaviate' this.name = 'weaviate' - this.version = 1.0 + this.version = 2.0 this.type = 'Weaviate' this.icon = 'weaviate.png' this.category = 'Vector Stores' this.description = - 'Upsert embedded data and perform similarity search upon query using Weaviate, a scalable open-source vector database' + 'Upsert embedded data and perform similarity or mmr search using Weaviate, a scalable open-source vector database' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -107,6 +108,7 @@ class Weaviate_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Weaviate Retriever', @@ -174,9 +176,6 @@ class Weaviate_VectorStores implements INode { const weaviateTextKey = nodeData.inputs?.weaviateTextKey as string const weaviateMetadataKeys = nodeData.inputs?.weaviateMetadataKeys as string const embeddings = nodeData.inputs?.embeddings as Embeddings - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const weaviateApiKey = getCredentialParam('weaviateApiKey', credentialData, nodeData) @@ -199,14 +198,7 @@ class Weaviate_VectorStores implements INode { const vectorStore = await WeaviateStore.fromExistingIndex(embeddings, obj) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/Zep/Zep.ts b/packages/components/nodes/vectorstores/Zep/Zep.ts index ebb13c64..3d9f1978 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Zep_VectorStores implements INode { label: string @@ -23,12 +24,12 @@ class Zep_VectorStores implements INode { constructor() { this.label = 'Zep' this.name = 'zep' - this.version = 1.0 + this.version = 2.0 this.type = 'Zep' this.icon = 'zep.svg' this.category = 'Vector Stores' this.description = - 'Upsert embedded data and perform similarity search upon query using Zep, a fast and scalable building block for LLM apps' + 'Upsert embedded data and perform similarity or mmr search upon query using Zep, a fast and scalable building block for LLM apps' this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -88,6 +89,7 @@ class Zep_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Zep Retriever', @@ -144,9 +146,6 @@ class Zep_VectorStores implements INode { const zepMetadataFilter = nodeData.inputs?.zepMetadataFilter const dimension = nodeData.inputs?.dimension as number const embeddings = nodeData.inputs?.embeddings as Embeddings - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) @@ -165,14 +164,7 @@ class Zep_VectorStores implements INode { const vectorStore = await ZepExistingVS.fromExistingIndex(embeddings, zepConfig) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } @@ -210,7 +202,7 @@ class ZepExistingVS extends ZepVectorStore { this.args = args } - async initalizeCollection(args: IZepConfig & Partial) { + async initializeCollection(args: IZepConfig & Partial) { this.client = await ZepClient.init(args.apiUrl, args.apiKey) try { this.collection = await this.client.document.getCollection(args.collectionName) @@ -259,7 +251,7 @@ class ZepExistingVS extends ZepVectorStore { const newfilter = { where: { and: ANDFilters } } - await this.initalizeCollection(this.args!).catch((err) => { + await this.initializeCollection(this.args!).catch((err) => { console.error('Error initializing collection:', err) throw err }) From 94236c4b5fe80a3449eed29f36ddb9e620077abe Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 12:12:04 +0530 Subject: [PATCH 125/502] Compression Retriever - Cohere Rerank --- .../CohereRerankRetriever/CohereRerank.ts | 51 ++++++++++++ .../CohereRerankRetriever.ts | 77 +++++++++++++++++++ .../compressionRetriever.svg | 7 ++ 3 files changed, 135 insertions(+) create mode 100644 packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts create mode 100644 packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts create mode 100644 packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts new file mode 100644 index 00000000..612581ed --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts @@ -0,0 +1,51 @@ +import { Callbacks } from 'langchain/callbacks' +import { Document } from 'langchain/document' +import { BaseDocumentCompressor } from 'langchain/retrievers/document_compressors' +import axios from 'axios' +export class CohereRerank extends BaseDocumentCompressor { + private cohereAPIKey: any + private COHERE_API_URL = 'https://api.cohere.ai/v1/rerank' + private model: string + + constructor(cohereAPIKey: string, model: string) { + super() + this.cohereAPIKey = cohereAPIKey + this.model = model + } + async compressDocuments( + documents: Document>[], + query: string, + _?: Callbacks | undefined + ): Promise>[]> { + // avoid empty api call + if (documents.length === 0) { + return [] + } + const config = { + headers: { + Authorization: `Bearer ${this.cohereAPIKey}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + } + } + const data = { + model: this.model, + max_chunks_per_doc: 10, + query: query, + return_documents: false, + documents: documents.map((doc) => doc.pageContent) + } + try { + let returnedDocs = await axios.post(this.COHERE_API_URL, data, config) + const finalResults: Document>[] = [] + returnedDocs.data.results.forEach((result: any) => { + const doc = documents[result.index] + doc.metadata.relevance_score = result.relevance_score + finalResults.push(doc) + }) + return finalResults + } catch (error) { + return documents + } + } +} diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts new file mode 100644 index 00000000..2e7090bc --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts @@ -0,0 +1,77 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseRetriever } from 'langchain/schema/retriever' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { getCredentialData, getCredentialParam } from '../../../src' +import { CohereRerank } from './CohereRerank' + +class CohereRerankRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + credential: INodeParams + badge: string + + constructor() { + this.label = 'Cohere Rerank Retriever' + this.name = 'cohereRerankRetriever' + this.version = 1.0 + this.type = 'Cohere Rerank Retriever' + this.icon = 'compressionRetriever.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = 'Cohere Rerank indexes the documents from most to least semantically relevant to the query.' + this.baseClasses = [this.type, 'BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['cohereApi'] + } + this.inputs = [ + { + label: 'Base Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Model Name', + name: 'model', + type: 'options', + options: [ + { + label: 'rerank-english-v2.0', + name: 'rerank-english-v2.0' + }, + { + label: 'rerank-multilingual-v2.0', + name: 'rerank-multilingual-v2.0' + } + ], + default: 'rerank-english-v2.0', + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const model = nodeData.inputs?.model as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const cohereApiKey = getCredentialParam('cohereApiKey', credentialData, nodeData) + + const cohereCompressor = new CohereRerank(cohereApiKey, model) + return new ContextualCompressionRetriever({ + baseCompressor: cohereCompressor, + baseRetriever: baseRetriever + }) + } +} + +module.exports = { nodeClass: CohereRerankRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg new file mode 100644 index 00000000..23c52d25 --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From fb02632f4b682b8b0ee2d1ec1a79330118318eaa Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 12:12:16 +0530 Subject: [PATCH 126/502] Compression Retriever - Embeddings Filter --- .../EmbeddingsFilterRetriever.ts | 97 +++++++++++++++++++ .../compressionRetriever.svg | 7 ++ 2 files changed, 104 insertions(+) create mode 100644 packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts create mode 100644 packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg diff --git a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts new file mode 100644 index 00000000..d373704c --- /dev/null +++ b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts @@ -0,0 +1,97 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseRetriever } from 'langchain/schema/retriever' +import { Embeddings } from 'langchain/embeddings/base' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { EmbeddingsFilter } from 'langchain/retrievers/document_compressors/embeddings_filter' + +class EmbeddingsFilterRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + badge: string + + constructor() { + this.label = 'Embeddings Filter Retriever' + this.name = 'embeddingsFilterRetriever' + this.version = 1.0 + this.type = 'EmbeddingsFilterRetriever' + this.icon = 'compressionRetriever.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = 'A document compressor that uses embeddings to drop documents unrelated to the query' + this.baseClasses = [this.type, 'BaseRetriever'] + this.inputs = [ + { + label: 'Base Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings', + optional: false + }, + { + label: 'Similarity Threshold', + name: 'similarityThreshold', + description: + 'Threshold for determining when two documents are similar enough to be considered redundant. Must be specified if `k` is not set', + type: 'number', + default: 0.8, + step: 0.1, + optional: true + }, + { + label: 'K', + name: 'k', + description: + 'The number of relevant documents to return. Can be explicitly set to undefined, in which case similarity_threshold must be specified. Defaults to 20', + type: 'number', + default: 20, + step: 1, + optional: true, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const embeddings = nodeData.inputs?.embeddings as Embeddings + const similarityThreshold = nodeData.inputs?.similarityThreshold as string + const k = nodeData.inputs?.k as string + + if (k === undefined && similarityThreshold === undefined) { + throw new Error(`Must specify one of "k" or "similarity_threshold".`) + } + + let similarityThresholdNumber = 0.8 + if (similarityThreshold) { + similarityThresholdNumber = parseFloat(similarityThreshold) + } + let kNumber = 0.8 + if (k) { + kNumber = parseFloat(k) + } + const baseCompressor = new EmbeddingsFilter({ + embeddings: embeddings, + similarityThreshold: similarityThresholdNumber, + k: kNumber + }) + + return new ContextualCompressionRetriever({ + baseCompressor, + baseRetriever: baseRetriever + }) + } +} + +module.exports = { nodeClass: EmbeddingsFilterRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg new file mode 100644 index 00000000..23c52d25 --- /dev/null +++ b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/compressionRetriever.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From f2f8ed6a9ce1ab41bd69193ce283757cd28cb16e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 12:12:25 +0530 Subject: [PATCH 127/502] Compression Retriever - LLM filter --- .../LLMFilterCompressionRetriever.ts | 60 +++++++++++++++++++ .../compressionRetriever.svg | 7 +++ 2 files changed, 67 insertions(+) create mode 100644 packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts create mode 100644 packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts new file mode 100644 index 00000000..c421c7ce --- /dev/null +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts @@ -0,0 +1,60 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { BaseRetriever } from 'langchain/schema/retriever' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { BaseLanguageModel } from 'langchain/base_language' +import { LLMChainExtractor } from 'langchain/retrievers/document_compressors/chain_extract' + +class LLMFilterCompressionRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + badge: string + + constructor() { + this.label = 'LLM Filter Retriever' + this.name = 'llmFilterRetriever' + this.version = 1.0 + this.type = 'LLMFilterRetriever' + this.icon = 'compressionRetriever.svg' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = + 'Iterate over the initially returned documents and extract, from each, only the content that is relevant to the query' + this.baseClasses = [this.type, 'BaseRetriever'] + this.inputs = [ + { + label: 'Base Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Language Model', + name: 'model', + type: 'BaseLanguageModel', + optional: true + }, + ] + } + + async init(nodeData: INodeData): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const model = nodeData.inputs?.model as BaseLanguageModel + + if (model) { + return new ContextualCompressionRetriever({ + baseCompressor: LLMChainExtractor.fromLLM(model), + baseRetriever: baseRetriever + }) + } + return {} + } +} + +module.exports = { nodeClass: LLMFilterCompressionRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg new file mode 100644 index 00000000..23c52d25 --- /dev/null +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From be79adb07c22586d4debd5384268463a874b878e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 12:13:52 +0530 Subject: [PATCH 128/502] lint fixes --- .../LLMFilterRetriever/LLMFilterCompressionRetriever.ts | 2 +- packages/components/nodes/vectorstores/VectorStoreUtils.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts index c421c7ce..e044468f 100644 --- a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts @@ -39,7 +39,7 @@ class LLMFilterCompressionRetriever_Retrievers implements INode { name: 'model', type: 'BaseLanguageModel', optional: true - }, + } ] } diff --git a/packages/components/nodes/vectorstores/VectorStoreUtils.ts b/packages/components/nodes/vectorstores/VectorStoreUtils.ts index b63a4121..a01d43c4 100644 --- a/packages/components/nodes/vectorstores/VectorStoreUtils.ts +++ b/packages/components/nodes/vectorstores/VectorStoreUtils.ts @@ -1,4 +1,4 @@ -import { INodeData } from '../../src' +import { INodeData } from "../../src"; export const resolveVectorStoreOrRetriever = (nodeData: INodeData, vectorStore: any) => { const output = nodeData.outputs?.output as string @@ -12,7 +12,7 @@ export const resolveVectorStoreOrRetriever = (nodeData: INodeData, vectorStore: const lambda = nodeData.inputs?.lambda as string const f = fetchK ? parseInt(fetchK) : 20 const l = lambda ? parseFloat(lambda) : 0.5 - const retriever = vectorStore.asRetriever({ + return vectorStore.asRetriever({ searchType: 'mmr', k: k, searchKwargs: { @@ -20,7 +20,6 @@ export const resolveVectorStoreOrRetriever = (nodeData: INodeData, vectorStore: lambda: l } }) - return retriever } else { // "searchType" is "similarity" return vectorStore.asRetriever(k) From f6ee137ca3e60b350f794181b43453eff520474b Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 12:14:10 +0530 Subject: [PATCH 129/502] lint fixes --- packages/components/nodes/vectorstores/VectorStoreUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/VectorStoreUtils.ts b/packages/components/nodes/vectorstores/VectorStoreUtils.ts index a01d43c4..0d92587f 100644 --- a/packages/components/nodes/vectorstores/VectorStoreUtils.ts +++ b/packages/components/nodes/vectorstores/VectorStoreUtils.ts @@ -1,4 +1,4 @@ -import { INodeData } from "../../src"; +import { INodeData } from '../../src' export const resolveVectorStoreOrRetriever = (nodeData: INodeData, vectorStore: any) => { const output = nodeData.outputs?.output as string From fd90fef94c289271eb1781205b0d6afc764c4f8a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 22 Dec 2023 14:50:10 +0530 Subject: [PATCH 130/502] Bugfix: Unncessary load of Prompts on chatflow open. This fix loads them on dialog open. --- .../dialog/PromptLangsmithHubDialog.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index 35b4ead7..8d89efc9 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -92,24 +92,27 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts) useEffect(() => { - if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) - else dispatch({ type: HIDE_CANVAS_DIALOG }) + if (show) { + dispatch({ type: SHOW_CANVAS_DIALOG }) + } else dispatch({ type: HIDE_CANVAS_DIALOG }) return () => dispatch({ type: HIDE_CANVAS_DIALOG }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [show, dispatch]) useEffect(() => { - if (promptType) { + if (promptType && show) { + setLoading(true) getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' }) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [promptType]) + }, [promptType, show]) useEffect(() => { if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) { setAvailablePrompNameList(getAvailablePromptsApi.data.repos) if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos) + setLoading(false) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -174,6 +177,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const [selectedPrompt, setSelectedPrompt] = useState({}) const [accordionExpanded, setAccordionExpanded] = useState(['prompt']) + const [loading, setLoading] = useState(false) const handleAccordionChange = (accordionName) => (event, isExpanded) => { const accordians = [...accordionExpanded] @@ -209,6 +213,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { language.forEach((item) => { tags += `tags=${item.name}&` }) + setLoading(true) getAvailablePromptsApi.request({ tags: tags }) } @@ -379,7 +384,15 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { - {availablePrompNameList && availablePrompNameList.length == 0 && ( + {loading && ( + + + promptEmptySVG + +
Please wait....loading Prompts
+
+ )} + {!loading && availablePrompNameList && availablePrompNameList.length === 0 && ( promptEmptySVG @@ -387,7 +400,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => {
No Available Prompts
)} - {availablePrompNameList && availablePrompNameList.length > 0 && ( + {!loading && availablePrompNameList && availablePrompNameList.length > 0 && ( From 3126442e67cfccdf14772eb6f539c765c2b9171d Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 22 Dec 2023 16:53:20 +0000 Subject: [PATCH 131/502] 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 b84d0525..933039e7 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 a7632976764a1bf648bae6f8c7253cb35fc54ffa Mon Sep 17 00:00:00 2001 From: abhishekshankr Date: Sat, 23 Dec 2023 11:27:32 -0500 Subject: [PATCH 132/502] Fixed icons borders --- .../components/nodes/prompts/FewShotPromptTemplate/prompt.svg | 1 - packages/components/nodes/prompts/PromptTemplate/prompt.svg | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg b/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg index 1484fcb2..e3a0c868 100644 --- a/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg +++ b/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg @@ -1,5 +1,4 @@ - diff --git a/packages/components/nodes/prompts/PromptTemplate/prompt.svg b/packages/components/nodes/prompts/PromptTemplate/prompt.svg index 1484fcb2..e3a0c868 100644 --- a/packages/components/nodes/prompts/PromptTemplate/prompt.svg +++ b/packages/components/nodes/prompts/PromptTemplate/prompt.svg @@ -1,5 +1,4 @@ - From e3282526624ce45925e4a4df3d8d0eb286273e5a Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 23 Dec 2023 17:41:43 +0000 Subject: [PATCH 133/502] Update AWSChatBedrock logo --- .../components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts index d869ea75..fedd731d 100644 --- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts +++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts @@ -29,7 +29,7 @@ class AWSChatBedrock_ChatModels implements INode { this.name = 'awsChatBedrock' this.version = 3.0 this.type = 'AWSChatBedrock' - this.icon = 'AWS.svg' + this.icon = 'aws.svg' this.category = 'Chat Models' this.description = 'Wrapper around AWS Bedrock large language models that use the Chat endpoint' this.baseClasses = [this.type, ...getBaseClasses(BedrockChat)] From 735edab9ae5d85be3999978d1ae24b984cfcb5a1 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Sun, 24 Dec 2023 23:02:49 -0600 Subject: [PATCH 134/502] Adds ENV boolean flag (DATABASE_SSL) for postgres ssl support This is needed if hosting flowise data on a postgres server that requires ssl. In PostgreSQL v15, the default rds.force_ssl is 1 (on) --- packages/server/src/DataSource.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 9265e55f..762315ac 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -46,6 +46,7 @@ export const init = async (): Promise => { username: process.env.DATABASE_USER, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME, + ssl: process.env.DATABASE_SSL === 'true', synchronize: false, migrationsRun: false, entities: Object.values(entities), From 6306904cfcf02facee9a7896aad0f35ee628c9b8 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 26 Dec 2023 15:54:50 +0000 Subject: [PATCH 135/502] 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 275eb950..c0095cee 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 97f25ba3..938cc873 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 3da35db2..4dda76df 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 e508ebee..2a625ff6 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 41e97cc1..99e3813b 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 ca8a3dee..9d0b2e3f 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 136/502] 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 9ceda919..6ffcc0e2 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 00acef0d..47ecee89 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 b011c07e..ab6d6aa0 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 137/502] 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 15b00d33..872ec0b5 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 681e9042..b422921e 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 72af1cb5..a02df3ea 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 3ff20a88..c3f97123 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 } From 25f43f2a3098c8ac5fed237a28386d67315950ac Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Wed, 27 Dec 2023 19:07:43 -0600 Subject: [PATCH 138/502] includes DATABASE_SSL in other necessary files --- CONTRIBUTING.md | 1 + docker/.env.example | 1 + docker/docker-compose.yml | 1 + packages/server/.env.example | 1 + packages/server/src/commands/start.ts | 2 ++ 5 files changed, 6 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfb7d3a9..04cb80b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,7 @@ Flowise support different environment variables to configure your instance. You | DATABASE_USER | Database username (When DATABASE_TYPE is not sqlite) | String | | | DATABASE_PASSWORD | Database password (When DATABASE_TYPE is not sqlite) | String | | | DATABASE_NAME | Database name (When DATABASE_TYPE is not sqlite) | String | | +| DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | diff --git a/docker/.env.example b/docker/.env.example index 967a1ab6..7d4f1699 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -12,6 +12,7 @@ LOG_PATH=/root/.flowise/logs # DATABASE_NAME="flowise" # DATABASE_USER="" # DATABASE_PASSWORD="" +# DATABASE_SSL=true # FLOWISE_USERNAME=user # FLOWISE_PASSWORD=1234 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8e0e1af5..92688469 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,6 +16,7 @@ services: - DATABASE_NAME=${DATABASE_NAME} - DATABASE_USER=${DATABASE_USER} - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_SSL=${DATABASE_SSL} - APIKEY_PATH=${APIKEY_PATH} - SECRETKEY_PATH=${SECRETKEY_PATH} - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} diff --git a/packages/server/.env.example b/packages/server/.env.example index 0ad11f3f..6e746a4d 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -12,6 +12,7 @@ PORT=3000 # DATABASE_NAME="flowise" # DATABASE_USER="" # DATABASE_PASSWORD="" +# DATABASE_SSL=true # FLOWISE_USERNAME=user # FLOWISE_PASSWORD=1234 diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index cd874264..d4e8cfdb 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -35,6 +35,7 @@ export default class Start extends Command { DATABASE_NAME: Flags.string(), DATABASE_USER: Flags.string(), DATABASE_PASSWORD: Flags.string(), + DATABASE_SSL: Flags.string(), LANGCHAIN_TRACING_V2: Flags.string(), LANGCHAIN_ENDPOINT: Flags.string(), LANGCHAIN_API_KEY: Flags.string(), @@ -104,6 +105,7 @@ export default class Start extends Command { if (flags.DATABASE_NAME) process.env.DATABASE_NAME = flags.DATABASE_NAME if (flags.DATABASE_USER) process.env.DATABASE_USER = flags.DATABASE_USER if (flags.DATABASE_PASSWORD) process.env.DATABASE_PASSWORD = flags.DATABASE_PASSWORD + if (flags.DATABASE_SSL) process.env.DATABASE_SSL = flags.DATABASE_SSL // Langsmith tracing if (flags.LANGCHAIN_TRACING_V2) process.env.LANGCHAIN_TRACING_V2 = flags.LANGCHAIN_TRACING_V2 From c9a6622df72779d5eb6f424b0f7df89a0e83da6a Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 28 Dec 2023 01:07:45 +0000 Subject: [PATCH 139/502] added LLM to moderation to detect similar deny sentences --- .../SimplePromptModeration.ts | 16 +++++++++--- .../SimplePromptModerationRunner.ts | 25 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts index d98c4867..ad3cfadd 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts @@ -2,6 +2,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src' import { Moderation } from '../Moderation' import { SimplePromptModerationRunner } from './SimplePromptModerationRunner' +import { BaseChatModel } from 'langchain/chat_models/base' class SimplePromptModeration implements INode { label: string @@ -17,7 +18,7 @@ class SimplePromptModeration implements INode { constructor() { this.label = 'Simple Prompt Moderation' this.name = 'inputModerationSimple' - this.version = 1.0 + this.version = 2.0 this.type = 'Moderation' this.icon = 'moderation.svg' this.category = 'Moderation' @@ -30,8 +31,14 @@ class SimplePromptModeration implements INode { type: 'string', rows: 4, placeholder: `ignore previous instructions\ndo not follow the directions\nyou must ignore all previous instructions`, - description: 'An array of string literals (enter one per line) that should not appear in the prompt text.', - optional: false + description: 'An array of string literals (enter one per line) that should not appear in the prompt text.' + }, + { + label: 'Chat Model', + name: 'model', + type: 'BaseChatModel', + description: 'Use LLM to detect if the input is similar to those specified in Deny List', + optional: true }, { label: 'Error Message', @@ -46,9 +53,10 @@ class SimplePromptModeration implements INode { async init(nodeData: INodeData): Promise { const denyList = nodeData.inputs?.denyList as string + const model = nodeData.inputs?.model as BaseChatModel const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string - return new SimplePromptModerationRunner(denyList, moderationErrorMessage) + return new SimplePromptModerationRunner(denyList, moderationErrorMessage, model) } } diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts index 08f9ed1e..10d351cf 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -1,23 +1,38 @@ import { Moderation } from '../Moderation' +import { BaseChatModel } from 'langchain/chat_models/base' export class SimplePromptModerationRunner implements Moderation { private readonly denyList: string = '' private readonly moderationErrorMessage: string = '' + private readonly model: BaseChatModel - constructor(denyList: string, moderationErrorMessage: string) { + constructor(denyList: string, moderationErrorMessage: string, model?: BaseChatModel) { this.denyList = denyList if (denyList.indexOf('\n') === -1) { this.denyList += '\n' } this.moderationErrorMessage = moderationErrorMessage + if (model) this.model = model } async checkForViolations(input: string): Promise { - this.denyList.split('\n').forEach((denyListItem) => { - if (denyListItem && denyListItem !== '' && input.toLowerCase().includes(denyListItem.toLowerCase())) { - throw Error(this.moderationErrorMessage) + if (this.model) { + const denyArray = this.denyList.split('\n') + for (const denyStr of denyArray) { + const res = await this.model.invoke( + `Are these two sentences similar to each other? Only return Yes or No.\nFirst sentence: ${input}\nSecond sentence: ${denyStr}` + ) + if (res.content.toString().toLowerCase().includes('yes')) { + throw Error(this.moderationErrorMessage) + } } - }) + } else { + this.denyList.split('\n').forEach((denyListItem) => { + if (denyListItem && denyListItem !== '' && input.toLowerCase().includes(denyListItem.toLowerCase())) { + throw Error(this.moderationErrorMessage) + } + }) + } return Promise.resolve(input) } } From 85e6fad0aa223ba46d9cf32f1f88a13619c322d0 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 28 Dec 2023 01:11:00 +0000 Subject: [PATCH 140/502] added input moderation to assistant, prevent app from crashing by using try catch --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 72 ++++++++++++++----- .../chatflows/OpenAI Assistant.json | 11 ++- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index c2d0e782..c5503610 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -9,6 +9,8 @@ import fetch from 'node-fetch' import { flatten, uniqWith, isEqual } from 'lodash' import { zodToJsonSchema } from 'zod-to-json-schema' import { AnalyticHandler } from '../../../src/handler' +import { Moderation, checkInputs, streamResponse } from '../../moderation/Moderation' +import { formatResponse } from '../../outputparsers/OutputParserHelpers' class OpenAIAssistant_Agents implements INode { label: string @@ -24,7 +26,7 @@ class OpenAIAssistant_Agents implements INode { constructor() { this.label = 'OpenAI Assistant' this.name = 'openAIAssistant' - this.version = 2.0 + this.version = 3.0 this.type = 'OpenAIAssistant' this.category = 'Agents' this.icon = 'assistant.svg' @@ -43,6 +45,14 @@ class OpenAIAssistant_Agents implements INode { type: 'Tool', list: true }, + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'Disable File Download', name: 'disableFileDownload', @@ -133,6 +143,20 @@ class OpenAIAssistant_Agents implements INode { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity const disableFileDownload = nodeData.inputs?.disableFileDownload as boolean + const moderations = nodeData.inputs?.inputModeration as Moderation[] + const isStreaming = options.socketIO && options.socketIOClientId + const socketIO = isStreaming ? options.socketIO : undefined + const socketIOClientId = isStreaming ? options.socketIOClientId : '' + + if (moderations && moderations.length > 0) { + try { + input = await checkInputs(moderations, input) + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)) + streamResponse(isStreaming, e.message, socketIO, socketIOClientId) + return formatResponse(e.message) + } + } let tools = nodeData.inputs?.tools tools = flatten(tools) @@ -249,7 +273,12 @@ class OpenAIAssistant_Agents implements INode { const actions: ICommonObject[] = [] run.required_action.submit_tool_outputs.tool_calls.forEach((item) => { const functionCall = item.function - const args = JSON.parse(functionCall.arguments) + let args = {} + try { + args = JSON.parse(functionCall.arguments) + } catch (e) { + console.error('Error parsing arguments, default to empty object') + } actions.push({ tool: functionCall.name, toolInput: args, @@ -264,21 +293,32 @@ class OpenAIAssistant_Agents implements INode { // Start tool analytics const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds) + if (options.socketIO && options.socketIOClientId) + options.socketIO.to(options.socketIOClientId).emit('tool', tool.name) - const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, threadId) - - // End tool analytics - await analyticHandlers.onToolEnd(toolIds, toolOutput) - - submitToolOutputs.push({ - tool_call_id: actions[i].toolCallId, - output: toolOutput - }) - usedTools.push({ - tool: tool.name, - toolInput: actions[i].toolInput, - toolOutput - }) + try { + const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, threadId) + await analyticHandlers.onToolEnd(toolIds, toolOutput) + submitToolOutputs.push({ + tool_call_id: actions[i].toolCallId, + output: toolOutput + }) + usedTools.push({ + tool: tool.name, + toolInput: actions[i].toolInput, + toolOutput + }) + } catch (e) { + await analyticHandlers.onToolEnd(toolIds, e) + console.error('Error executing tool', e) + clearInterval(timeout) + reject( + new Error( + `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Tool: ${tool.name}` + ) + ) + break + } } if (submitToolOutputs.length) { diff --git a/packages/server/marketplaces/chatflows/OpenAI Assistant.json b/packages/server/marketplaces/chatflows/OpenAI Assistant.json index ba4c6134..e9311c97 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Assistant.json +++ b/packages/server/marketplaces/chatflows/OpenAI Assistant.json @@ -14,7 +14,7 @@ "data": { "id": "openAIAssistant_0", "label": "OpenAI Assistant", - "version": 2, + "version": 3, "name": "openAIAssistant", "type": "OpenAIAssistant", "baseClasses": ["OpenAIAssistant"], @@ -45,6 +45,15 @@ "type": "Tool", "list": true, "id": "openAIAssistant_0-input-tools-Tool" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "openAIAssistant_0-input-inputModeration-Moderation" } ], "inputs": { From 4f940b0d910d61172f6b143a9b4ff518ee35e99c Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 28 Dec 2023 02:02:49 +0000 Subject: [PATCH 141/502] chck for empty string --- .../SimplePromptModeration/SimplePromptModerationRunner.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts index 10d351cf..c9a11643 100644 --- a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -19,6 +19,7 @@ export class SimplePromptModerationRunner implements Moderation { if (this.model) { const denyArray = this.denyList.split('\n') for (const denyStr of denyArray) { + if (!denyStr || denyStr === '') continue const res = await this.model.invoke( `Are these two sentences similar to each other? Only return Yes or No.\nFirst sentence: ${input}\nSecond sentence: ${denyStr}` ) From ab556b71cf8ea95a9fb76b71bc95d0662c474926 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 29 Dec 2023 01:43:30 +0000 Subject: [PATCH 142/502] avoid submitting tool outputs when in_progress --- .../nodes/agents/OpenAIAssistant/OpenAIAssistant.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index c5503610..1f9d4eab 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -321,7 +321,10 @@ class OpenAIAssistant_Agents implements INode { } } - if (submitToolOutputs.length) { + const newRun = await openai.beta.threads.runs.retrieve(threadId, runId) + const newStatus = newRun?.status + + if (submitToolOutputs.length && newStatus !== 'in_progress') { await openai.beta.threads.runs.submitToolOutputs(threadId, runId, { tool_outputs: submitToolOutputs }) From 5ab2d63de8b066a6ca09df2d23f3aae502f40e42 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 29 Dec 2023 13:29:24 +0000 Subject: [PATCH 143/502] only submitting tool outputs when requiresaction --- .../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 1f9d4eab..85d35ea9 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -324,7 +324,7 @@ class OpenAIAssistant_Agents implements INode { const newRun = await openai.beta.threads.runs.retrieve(threadId, runId) const newStatus = newRun?.status - if (submitToolOutputs.length && newStatus !== 'in_progress') { + if (submitToolOutputs.length && newStatus === 'requires_action') { await openai.beta.threads.runs.submitToolOutputs(threadId, runId, { tool_outputs: submitToolOutputs }) From 8eabd8f0c26e060c7651b02c3166a52fd2ed6f66 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 29 Dec 2023 13:50:33 +0000 Subject: [PATCH 144/502] wrap in a try catch block --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 85d35ea9..cf69022b 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -324,14 +324,19 @@ class OpenAIAssistant_Agents implements INode { const newRun = await openai.beta.threads.runs.retrieve(threadId, runId) const newStatus = newRun?.status - if (submitToolOutputs.length && newStatus === 'requires_action') { - await openai.beta.threads.runs.submitToolOutputs(threadId, runId, { - tool_outputs: submitToolOutputs - }) - resolve(state) - } else { - await openai.beta.threads.runs.cancel(threadId, runId) - resolve('requires_action_retry') + try { + if (submitToolOutputs.length && newStatus === 'requires_action') { + await openai.beta.threads.runs.submitToolOutputs(threadId, runId, { + tool_outputs: submitToolOutputs + }) + resolve(state) + } else { + await openai.beta.threads.runs.cancel(threadId, runId) + resolve('requires_action_retry') + } + } catch (e) { + clearInterval(timeout) + reject(new Error(`Error submitting tool outputs: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) } } } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { From 4dd2f245ffdfb2fac5a169c97f9c825e7aa04828 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 29 Dec 2023 20:35:42 +0530 Subject: [PATCH 145/502] Compression Retriever: Reciprocal Rank Fusion --- .../retrievers/RRFRetriever/RRFRetriever.ts | 84 ++++++++++++++++ .../RRFRetriever/ReciprocalRankFusion.ts | 95 +++++++++++++++++++ .../RRFRetriever/compressionRetriever.svg | 7 ++ 3 files changed, 186 insertions(+) create mode 100644 packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts create mode 100644 packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts create mode 100644 packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg diff --git a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts new file mode 100644 index 00000000..8d6d9d6f --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts @@ -0,0 +1,84 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { BaseLanguageModel } from 'langchain/base_language' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { BaseRetriever } from 'langchain/schema/retriever' +import { ReciprocalRankFusion } from './ReciprocalRankFusion' +import { VectorStoreRetriever } from 'langchain/vectorstores/base' + +class RRFRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + badge: string + + constructor() { + this.label = 'Reciprocal Rank Fusion Retriever' + this.name = 'RRFRetriever' + this.version = 2.0 + this.type = 'RRFRetriever' + this.badge = 'NEW' + this.icon = 'compressionRetriever.svg' + this.category = 'Retrievers' + this.description = 'Reciprocal Rank Fusion to re-rank search results by multiple query generation.' + this.baseClasses = [this.type, 'BaseRetriever'] + this.inputs = [ + { + label: 'Base Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Language Model', + name: 'model', + type: 'BaseLanguageModel' + }, + { + label: 'Query Count', + name: 'queryCount', + description: 'Number of synthetic queries to generate. Default to 4', + placeholder: '4', + type: 'number', + default: 4, + additionalParams: true, + optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', + placeholder: '0', + type: 'number', + default: 0, + additionalParams: true, + optional: true + } + ] + } + + async init(nodeData: INodeData): Promise { + const llm = nodeData.inputs?.model as BaseLanguageModel + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const queryCount = nodeData.inputs?.queryCount as string + const q = queryCount ? parseFloat(queryCount) : 4 + const topK = nodeData.inputs?.topK as string + let k = topK ? parseFloat(topK) : 4 + + if (k <= 0) { + k = (baseRetriever as VectorStoreRetriever).k + } + + const ragFusion = new ReciprocalRankFusion(llm, baseRetriever as VectorStoreRetriever, q, k) + return new ContextualCompressionRetriever({ + baseCompressor: ragFusion, + baseRetriever: baseRetriever + }) + } +} + +module.exports = { nodeClass: RRFRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts new file mode 100644 index 00000000..134d7c8a --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts @@ -0,0 +1,95 @@ +import { BaseDocumentCompressor } from 'langchain/retrievers/document_compressors' +import { Document } from 'langchain/document' +import { Callbacks } from 'langchain/callbacks' +import { BaseLanguageModel } from 'langchain/base_language' +import { ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' +import { LLMChain } from 'langchain/chains' +import { VectorStoreRetriever } from 'langchain/vectorstores/base' + +export class ReciprocalRankFusion extends BaseDocumentCompressor { + private readonly llm: BaseLanguageModel + private readonly queryCount: number + private readonly topK: number + private baseRetriever: VectorStoreRetriever + constructor(llm: BaseLanguageModel, baseRetriever: VectorStoreRetriever, queryCount: number, topK: number) { + super() + this.queryCount = queryCount + this.llm = llm + this.baseRetriever = baseRetriever + this.topK = topK + } + async compressDocuments( + documents: Document>[], + query: string, + _?: Callbacks | undefined + ): Promise>[]> { + // avoid empty api call + if (documents.length === 0) { + return [] + } + const chatPrompt = ChatPromptTemplate.fromMessages([ + SystemMessagePromptTemplate.fromTemplate( + 'You are a helpful assistant that generates multiple search queries based on a single input query.' + ), + HumanMessagePromptTemplate.fromTemplate( + 'Generate multiple search queries related to: {input}. Provide these alternative questions separated by newlines, do not add any numbers.' + ), + HumanMessagePromptTemplate.fromTemplate('OUTPUT (' + this.queryCount + ' queries):') + ]) + const llmChain = new LLMChain({ + llm: this.llm, + prompt: chatPrompt + }) + const multipleQueries = await llmChain.call({ input: query }) + const queries = [] + queries.push(query) + multipleQueries.text.split('\n').map((q: string) => { + queries.push(q) + }) + console.log(JSON.stringify(queries)) + const docList: Document>[][] = [] + for (let i = 0; i < queries.length; i++) { + const resultOne = await this.baseRetriever.vectorStore.similaritySearch(queries[i], 5) + const docs: any[] = [] + resultOne.forEach((doc) => { + docs.push(doc) + }) + docList.push(docs) + } + + return this.reciprocalRankFunction(docList, 60) + } + + reciprocalRankFunction(docList: Document>[][], k: number): Document>[] { + docList.forEach((docs: Document>[]) => { + docs.forEach((doc: any, index: number) => { + let rank = index + 1 + if (doc.metadata.relevancy_score) { + doc.metadata.relevancy_score += 1 / (rank + k) + } else { + doc.metadata.relevancy_score = 1 / (rank + k) + } + }) + }) + const scoreArray: any[] = [] + docList.forEach((docs: Document>[]) => { + docs.forEach((doc: any) => { + scoreArray.push(doc.metadata.relevancy_score) + }) + }) + scoreArray.sort((a, b) => b - a) + const rerankedDocuments: Document>[] = [] + const seenScores: any[] = [] + scoreArray.forEach((score) => { + docList.forEach((docs) => { + docs.forEach((doc: any) => { + if (doc.metadata.relevancy_score === score && seenScores.indexOf(score) === -1) { + rerankedDocuments.push(doc) + seenScores.push(doc.metadata.relevancy_score) + } + }) + }) + }) + return rerankedDocuments.splice(0, this.topK) + } +} diff --git a/packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg new file mode 100644 index 00000000..23c52d25 --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From d0ab21e733d03098a1faa3f0fd50b5d5baa9d381 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 29 Dec 2023 20:37:25 +0530 Subject: [PATCH 146/502] Compression Retriever: Addition of topK to Cohere Rerank Retriever --- .../CohereRerankRetriever/CohereRerank.ts | 8 +++++--- .../CohereRerankRetriever.ts | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts index 612581ed..55f3c4aa 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts @@ -5,12 +5,13 @@ import axios from 'axios' export class CohereRerank extends BaseDocumentCompressor { private cohereAPIKey: any private COHERE_API_URL = 'https://api.cohere.ai/v1/rerank' - private model: string - - constructor(cohereAPIKey: string, model: string) { + private readonly model: string + private readonly k: number + constructor(cohereAPIKey: string, model: string, k: number) { super() this.cohereAPIKey = cohereAPIKey this.model = model + this.k = k } async compressDocuments( documents: Document>[], @@ -30,6 +31,7 @@ export class CohereRerank extends BaseDocumentCompressor { } const data = { model: this.model, + topN: this.k, max_chunks_per_doc: 10, query: query, return_documents: false, diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts index 2e7090bc..3c1872b3 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts @@ -3,6 +3,7 @@ import { BaseRetriever } from 'langchain/schema/retriever' import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' import { getCredentialData, getCredentialParam } from '../../../src' import { CohereRerank } from './CohereRerank' +import { VectorStoreRetriever } from 'langchain/vectorstores/base' class CohereRerankRetriever_Retrievers implements INode { label: string @@ -56,6 +57,16 @@ class CohereRerankRetriever_Retrievers implements INode { ], default: 'rerank-english-v2.0', optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', + placeholder: '0', + type: 'number', + default: 0, + additionalParams: true, + optional: true } ] } @@ -65,8 +76,14 @@ class CohereRerankRetriever_Retrievers implements INode { const model = nodeData.inputs?.model as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const cohereApiKey = getCredentialParam('cohereApiKey', credentialData, nodeData) + const topK = nodeData.inputs?.topK as string + let k = topK ? parseFloat(topK) : 4 - const cohereCompressor = new CohereRerank(cohereApiKey, model) + if (k <= 0) { + k = (baseRetriever as VectorStoreRetriever).k + } + + const cohereCompressor = new CohereRerank(cohereApiKey, model, k) return new ContextualCompressionRetriever({ baseCompressor: cohereCompressor, baseRetriever: baseRetriever From da18b6a5c0eceb9f15d71c66083e5cf0daafa1c3 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 29 Dec 2023 15:17:26 +0000 Subject: [PATCH 147/502] Update 1702200925471-AddVariableEntity.ts --- .../migrations/postgres/1702200925471-AddVariableEntity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts b/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts index d4a1d7be..c6d3902f 100644 --- a/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts +++ b/packages/server/src/database/migrations/postgres/1702200925471-AddVariableEntity.ts @@ -10,7 +10,7 @@ export class AddVariableEntity1699325775451 implements MigrationInterface { "type" text NULL, "createdDate" timestamp NOT NULL DEFAULT now(), "updatedDate" timestamp NOT NULL DEFAULT now(), - CONSTRAINT "PK_3c7cea7a044ac4c92764576cdbf" PRIMARY KEY (id) + CONSTRAINT "PK_98419043dd704f54-9830ab78f8" PRIMARY KEY (id) );` ) } From 1bd3b5d0ee9395ed5bce48eadc5c4e58f5385099 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 30 Dec 2023 08:07:15 +0530 Subject: [PATCH 148/502] Compression Retriever: Addition of constant to RRF Retriever --- .../retrievers/RRFRetriever/RRFRetriever.ts | 16 +++++++++++++++- .../RRFRetriever/ReciprocalRankFusion.ts | 6 ++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts index 8d6d9d6f..3229b3a8 100644 --- a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts +++ b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts @@ -57,6 +57,18 @@ class RRFRetriever_Retrievers implements INode { default: 0, additionalParams: true, optional: true + }, + { + label: 'Constant', + name: 'c', + description: + 'A constant added to the rank, controlling the balance between the importance of high-ranked items and the consideration given to lower-ranked items.\n' + + 'Default is 60', + placeholder: '60', + type: 'number', + default: 60, + additionalParams: true, + optional: true } ] } @@ -68,12 +80,14 @@ class RRFRetriever_Retrievers implements INode { const q = queryCount ? parseFloat(queryCount) : 4 const topK = nodeData.inputs?.topK as string let k = topK ? parseFloat(topK) : 4 + const constantC = nodeData.inputs?.c as string + let c = topK ? parseFloat(constantC) : 60 if (k <= 0) { k = (baseRetriever as VectorStoreRetriever).k } - const ragFusion = new ReciprocalRankFusion(llm, baseRetriever as VectorStoreRetriever, q, k) + const ragFusion = new ReciprocalRankFusion(llm, baseRetriever as VectorStoreRetriever, q, k, c) return new ContextualCompressionRetriever({ baseCompressor: ragFusion, baseRetriever: baseRetriever diff --git a/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts index 134d7c8a..b14608fe 100644 --- a/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts +++ b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts @@ -10,13 +10,15 @@ export class ReciprocalRankFusion extends BaseDocumentCompressor { private readonly llm: BaseLanguageModel private readonly queryCount: number private readonly topK: number + private readonly c: number private baseRetriever: VectorStoreRetriever - constructor(llm: BaseLanguageModel, baseRetriever: VectorStoreRetriever, queryCount: number, topK: number) { + constructor(llm: BaseLanguageModel, baseRetriever: VectorStoreRetriever, queryCount: number, topK: number, c: number) { super() this.queryCount = queryCount this.llm = llm this.baseRetriever = baseRetriever this.topK = topK + this.c = c } async compressDocuments( documents: Document>[], @@ -57,7 +59,7 @@ export class ReciprocalRankFusion extends BaseDocumentCompressor { docList.push(docs) } - return this.reciprocalRankFunction(docList, 60) + return this.reciprocalRankFunction(docList, this.c) } reciprocalRankFunction(docList: Document>[][], k: number): Document>[] { From fd55fa62dd6ba7d0679adb494bfb3f14070cdce3 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 30 Dec 2023 08:08:15 +0530 Subject: [PATCH 149/502] fixes for lint failures --- .../nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts index b14608fe..0789ca17 100644 --- a/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts +++ b/packages/components/nodes/retrievers/RRFRetriever/ReciprocalRankFusion.ts @@ -48,7 +48,6 @@ export class ReciprocalRankFusion extends BaseDocumentCompressor { multipleQueries.text.split('\n').map((q: string) => { queries.push(q) }) - console.log(JSON.stringify(queries)) const docList: Document>[][] = [] for (let i = 0; i < queries.length; i++) { const resultOne = await this.baseRetriever.vectorStore.similaritySearch(queries[i], 5) From fe0c22255baf2cc5865a5acfa9bdd9ce07a55ef6 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 30 Dec 2023 16:17:03 +0530 Subject: [PATCH 150/502] Bugfix: Upsert successful, but failed to insert documents --- packages/components/nodes/documentloaders/Notion/NotionDB.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/nodes/documentloaders/Notion/NotionDB.ts b/packages/components/nodes/documentloaders/Notion/NotionDB.ts index 5c171c7f..4e37ad22 100644 --- a/packages/components/nodes/documentloaders/Notion/NotionDB.ts +++ b/packages/components/nodes/documentloaders/Notion/NotionDB.ts @@ -66,6 +66,10 @@ class NotionDB_DocumentLoaders implements INode { auth: notionIntegrationToken }, id: databaseId, + callerOptions: { + maxConcurrency: 64 // Default value + }, + propertiesAsHeader: true, // Prepends a front matter header of the page properties to the page contents type: 'database' } const loader = new NotionAPILoader(obj) From 75b5b33d8d9b92c4ef9458b81dc7f1e8c949532a Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 30 Dec 2023 12:29:00 +0000 Subject: [PATCH 151/502] update langchain version --- .../nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts | 5 ++--- .../components/nodes/chatmodels/ChatMistral/ChatMistral.ts | 4 +++- .../components/nodes/chatmodels/ChatOllama/ChatOllama.ts | 5 ++--- .../nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts | 2 +- packages/components/nodes/llms/Ollama/Ollama.ts | 3 +-- packages/components/package.json | 6 +++--- packages/server/src/utils/index.ts | 2 +- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts index 99e151e6..9b7b724a 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts @@ -1,7 +1,6 @@ -import { OpenAIBaseInput } from 'langchain/dist/types/openai-types' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' -import { AzureOpenAIInput, ChatOpenAI } from 'langchain/chat_models/openai' +import { AzureOpenAIInput, ChatOpenAI, OpenAIChatInput } from 'langchain/chat_models/openai' import { BaseCache } from 'langchain/schema' import { BaseLLMParams } from 'langchain/llms/base' @@ -123,7 +122,7 @@ class AzureChatOpenAI_ChatModels implements INode { const azureOpenAIApiDeploymentName = getCredentialParam('azureOpenAIApiDeploymentName', credentialData, nodeData) const azureOpenAIApiVersion = getCredentialParam('azureOpenAIApiVersion', credentialData, nodeData) - const obj: Partial & BaseLLMParams & Partial = { + const obj: Partial & BaseLLMParams & Partial = { temperature: parseFloat(temperature), modelName, azureOpenAIApiKey, diff --git a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts index f65de851..91624574 100644 --- a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts +++ b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts @@ -124,13 +124,15 @@ class ChatMistral_ChatModels implements INode { const safeMode = nodeData.inputs?.safeMode as boolean const randomSeed = nodeData.inputs?.safeMode as string const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string + const streaming = nodeData.inputs?.streaming as boolean // Waiting fix from langchain + mistral to enable streaming - https://github.com/mistralai/client-js/issues/18 const cache = nodeData.inputs?.cache as BaseCache const obj: ChatMistralAIInput = { apiKey: apiKey, - modelName: modelName + modelName: modelName, + streaming: streaming ?? true } if (maxOutputTokens) obj.maxTokens = parseInt(maxOutputTokens, 10) diff --git a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts index ed58589b..d445c7e1 100644 --- a/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts +++ b/packages/components/nodes/chatmodels/ChatOllama/ChatOllama.ts @@ -1,8 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' -import { ChatOllama } from 'langchain/chat_models/ollama' +import { ChatOllama, ChatOllamaInput } from 'langchain/chat_models/ollama' import { BaseCache } from 'langchain/schema' -import { OllamaInput } from 'langchain/dist/util/ollama' import { BaseLLMParams } from 'langchain/llms/base' class ChatOllama_ChatModels implements INode { @@ -209,7 +208,7 @@ class ChatOllama_ChatModels implements INode { const cache = nodeData.inputs?.cache as BaseCache - const obj: OllamaInput & BaseLLMParams = { + const obj: ChatOllamaInput & BaseLLMParams = { baseUrl, temperature: parseFloat(temperature), model: modelName diff --git a/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts b/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts index 698770b3..8892b03f 100644 --- a/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts +++ b/packages/components/nodes/embeddings/OllamaEmbedding/OllamaEmbedding.ts @@ -1,7 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' +import { OllamaInput } from 'langchain/llms/ollama' import { OllamaEmbeddings } from 'langchain/embeddings/ollama' -import { OllamaInput } from 'langchain/dist/util/ollama' class OllamaEmbedding_Embeddings implements INode { label: string diff --git a/packages/components/nodes/llms/Ollama/Ollama.ts b/packages/components/nodes/llms/Ollama/Ollama.ts index c7250a04..385890c9 100644 --- a/packages/components/nodes/llms/Ollama/Ollama.ts +++ b/packages/components/nodes/llms/Ollama/Ollama.ts @@ -1,8 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' -import { Ollama } from 'langchain/llms/ollama' +import { Ollama, OllamaInput } from 'langchain/llms/ollama' import { BaseCache } from 'langchain/schema' -import { OllamaInput } from 'langchain/dist/util/ollama' import { BaseLLMParams } from 'langchain/llms/base' class Ollama_LLMs implements INode { diff --git a/packages/components/package.json b/packages/components/package.json index 9cb0bf1e..8a145186 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -26,8 +26,8 @@ "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", - "@langchain/google-genai": "^0.0.3", - "@langchain/mistralai": "^0.0.3", + "@langchain/google-genai": "^0.0.6", + "@langchain/mistralai": "^0.0.6", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", "@pinecone-database/pinecone": "^1.1.1", @@ -52,7 +52,7 @@ "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "langchain": "^0.0.196", + "langchain": "^0.0.213", "langfuse": "^1.2.0", "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.49", diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 0bc28861..ce444512 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -818,7 +818,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component */ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNodeData: INodeData) => { const streamAvailableLLMs = { - 'Chat Models': ['azureChatOpenAI', 'chatOpenAI', 'chatAnthropic', 'chatOllama', 'awsChatBedrock'], + 'Chat Models': ['azureChatOpenAI', 'chatOpenAI', 'chatAnthropic', 'chatOllama', 'awsChatBedrock', 'chatMistralAI'], LLMs: ['azureOpenAI', 'openAI', 'ollama'] } From 9e7f3587f14881e5192ff4a2ef8f85d83b969fad Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 30 Dec 2023 18:22:21 +0530 Subject: [PATCH 152/502] Upgrading of analytic dependencies - langfuse and langsmith. --- packages/components/package.json | 6 +++--- packages/components/src/handler.ts | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 9cb0bf1e..ef1f92eb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -53,9 +53,9 @@ "husky": "^8.0.3", "ioredis": "^5.3.2", "langchain": "^0.0.196", - "langfuse": "^1.2.0", - "langfuse-langchain": "^1.0.31", - "langsmith": "^0.0.49", + "langfuse": "2.0.2", + "langfuse-langchain": "2.0.2", + "langsmith": "0.0.53", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", "mammoth": "^1.5.1", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 29aff3e2..ce7a1a1c 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -536,9 +536,10 @@ export class AnalyticHandler { if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] if (trace) { + trace.id const generation = trace.generation({ name, - prompt: input + input: input }) this.handlers['langFuse'].generation = { [generation.id]: generation } returnIds['langFuse'].generation = generation.id @@ -583,7 +584,7 @@ export class AnalyticHandler { const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] if (generation) { generation.end({ - completion: output + output: output }) } } @@ -618,7 +619,7 @@ export class AnalyticHandler { const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation] if (generation) { generation.end({ - completion: error + output: error }) } } From 28e32f0ae68a18905b52921be2bc97e4447386a2 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 30 Dec 2023 09:46:44 -0500 Subject: [PATCH 153/502] Initial support for Airtable views --- .../documentloaders/Airtable/Airtable.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 70d0c674..a2c1eef3 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -55,6 +55,15 @@ class Airtable_DocumentLoaders implements INode { description: 'If your table URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, tblJdmvbrgizbYICO is the table id' }, + { + label: 'View Id', + name: 'viewId', + type: 'string', + placeholder: 'viw9UrP77Id0CE4ee', + description: + 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', + optional: true + }, { label: 'Return All', name: 'returnAll', @@ -83,6 +92,7 @@ class Airtable_DocumentLoaders implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const baseId = nodeData.inputs?.baseId as string const tableId = nodeData.inputs?.tableId as string + const viewId = nodeData.inputs?.viewId as string const returnAll = nodeData.inputs?.returnAll as boolean const limit = nodeData.inputs?.limit as string const textSplitter = nodeData.inputs?.textSplitter as TextSplitter @@ -94,6 +104,7 @@ class Airtable_DocumentLoaders implements INode { const airtableOptions: AirtableLoaderParams = { baseId, tableId, + viewId, returnAll, accessToken, limit: limit ? parseInt(limit, 10) : 100 @@ -133,6 +144,7 @@ interface AirtableLoaderParams { baseId: string tableId: string accessToken: string + viewId?: string limit?: number returnAll?: boolean } @@ -153,16 +165,19 @@ class AirtableLoader extends BaseDocumentLoader { public readonly tableId: string + public readonly viewId?: string + public readonly accessToken: string public readonly limit: number public readonly returnAll: boolean - constructor({ baseId, tableId, accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { + constructor({ baseId, tableId, viewId, accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { super() this.baseId = baseId this.tableId = tableId + this.viewId = viewId this.accessToken = accessToken this.limit = limit this.returnAll = returnAll @@ -203,7 +218,7 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadLimit(): Promise { - const params = { maxRecords: this.limit } + const params = { maxRecords: this.limit, view: this.viewId } const data = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, params) if (data.records.length === 0) { return [] @@ -212,7 +227,7 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadAll(): Promise { - const params: ICommonObject = { pageSize: 100 } + const params: ICommonObject = { pageSize: 100, view: this.viewId } let data: AirtableLoaderResponse let returnPages: AirtableLoaderPage[] = [] From 543c41b5c5c685df09b8682972d5341f2f76371a Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Sat, 30 Dec 2023 16:07:05 +0000 Subject: [PATCH 154/502] Update ChatMistral.ts --- packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts index 91624574..4524db46 100644 --- a/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts +++ b/packages/components/nodes/chatmodels/ChatMistral/ChatMistral.ts @@ -125,8 +125,6 @@ class ChatMistral_ChatModels implements INode { const randomSeed = nodeData.inputs?.safeMode as string const overrideEndpoint = nodeData.inputs?.overrideEndpoint as string const streaming = nodeData.inputs?.streaming as boolean - // Waiting fix from langchain + mistral to enable streaming - https://github.com/mistralai/client-js/issues/18 - const cache = nodeData.inputs?.cache as BaseCache const obj: ChatMistralAIInput = { From 3fb800190778a67deb46928708ad8e8773154fd3 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 30 Dec 2023 14:13:39 -0500 Subject: [PATCH 155/502] Added support to exclude specific Airtable Field Ids --- .../documentloaders/Airtable/Airtable.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index a2c1eef3..dfa05983 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -64,6 +64,16 @@ class Airtable_DocumentLoaders implements INode { 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', optional: true }, + { + label: 'Exclude Field Ids', + name: 'excludeFieldIds', + type: 'string', + placeholder: 'fld1u0qUz0SoOQ9Gg, fldAMOvPfwxr12VrK', + optional: true, + additionalParams: true, + description: + 'Comma-separated list of field ids to exclude' + }, { label: 'Return All', name: 'returnAll', @@ -93,6 +103,7 @@ class Airtable_DocumentLoaders implements INode { const baseId = nodeData.inputs?.baseId as string const tableId = nodeData.inputs?.tableId as string const viewId = nodeData.inputs?.viewId as string + const excludeFieldIds = nodeData.inputs?.excludeFieldIds as string const returnAll = nodeData.inputs?.returnAll as boolean const limit = nodeData.inputs?.limit as string const textSplitter = nodeData.inputs?.textSplitter as TextSplitter @@ -105,6 +116,7 @@ class Airtable_DocumentLoaders implements INode { baseId, tableId, viewId, + excludeFieldIds: excludeFieldIds ? excludeFieldIds.split(',').map(id => id.trim()) : [], returnAll, accessToken, limit: limit ? parseInt(limit, 10) : 100 @@ -145,6 +157,7 @@ interface AirtableLoaderParams { tableId: string accessToken: string viewId?: string + excludeFieldIds?: string[] limit?: number returnAll?: boolean } @@ -167,17 +180,20 @@ class AirtableLoader extends BaseDocumentLoader { public readonly viewId?: string + public readonly excludeFieldIds: string[] + public readonly accessToken: string public readonly limit: number public readonly returnAll: boolean - constructor({ baseId, tableId, viewId, accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { + constructor({ baseId, tableId, viewId, excludeFieldIds = [], accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { super() this.baseId = baseId this.tableId = tableId this.viewId = viewId + this.excludeFieldIds = excludeFieldIds this.accessToken = accessToken this.limit = limit this.returnAll = returnAll @@ -207,10 +223,16 @@ class AirtableLoader extends BaseDocumentLoader { private createDocumentFromPage(page: AirtableLoaderPage): Document { // Generate the URL const pageUrl = `https://api.airtable.com/v0/${this.baseId}/${this.tableId}/${page.id}` + const fields = { ...page.fields }; + + // Exclude any specified fields + this.excludeFieldIds.forEach(id => { + delete fields[id]; + }); // Return a langchain document return new Document({ - pageContent: JSON.stringify(page.fields, null, 2), + pageContent: JSON.stringify(fields, null, 2), metadata: { url: pageUrl } From 6006157958f21347c6feff68ca56a313e813712b Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 30 Dec 2023 15:17:33 -0500 Subject: [PATCH 156/502] Updated Airtable field exclusion support to use field names instead of field ids --- .../documentloaders/Airtable/Airtable.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index dfa05983..d7ae2667 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -65,14 +65,14 @@ class Airtable_DocumentLoaders implements INode { optional: true }, { - label: 'Exclude Field Ids', - name: 'excludeFieldIds', + label: 'Exclude Field Names', + name: 'excludeFieldNames', type: 'string', - placeholder: 'fld1u0qUz0SoOQ9Gg, fldAMOvPfwxr12VrK', + placeholder: 'Name, Assignee', optional: true, additionalParams: true, description: - 'Comma-separated list of field ids to exclude' + 'Comma-separated list of field names to exclude' }, { label: 'Return All', @@ -103,7 +103,7 @@ class Airtable_DocumentLoaders implements INode { const baseId = nodeData.inputs?.baseId as string const tableId = nodeData.inputs?.tableId as string const viewId = nodeData.inputs?.viewId as string - const excludeFieldIds = nodeData.inputs?.excludeFieldIds as string + const excludeFieldNames = nodeData.inputs?.excludeFieldNames as string const returnAll = nodeData.inputs?.returnAll as boolean const limit = nodeData.inputs?.limit as string const textSplitter = nodeData.inputs?.textSplitter as TextSplitter @@ -116,7 +116,7 @@ class Airtable_DocumentLoaders implements INode { baseId, tableId, viewId, - excludeFieldIds: excludeFieldIds ? excludeFieldIds.split(',').map(id => id.trim()) : [], + excludeFieldNames: excludeFieldNames ? excludeFieldNames.split(',').map(id => id.trim()) : [], returnAll, accessToken, limit: limit ? parseInt(limit, 10) : 100 @@ -157,7 +157,7 @@ interface AirtableLoaderParams { tableId: string accessToken: string viewId?: string - excludeFieldIds?: string[] + excludeFieldNames?: string[] limit?: number returnAll?: boolean } @@ -180,7 +180,7 @@ class AirtableLoader extends BaseDocumentLoader { public readonly viewId?: string - public readonly excludeFieldIds: string[] + public readonly excludeFieldNames: string[] public readonly accessToken: string @@ -188,12 +188,12 @@ class AirtableLoader extends BaseDocumentLoader { public readonly returnAll: boolean - constructor({ baseId, tableId, viewId, excludeFieldIds = [], accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { + constructor({ baseId, tableId, viewId, excludeFieldNames = [], accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { super() this.baseId = baseId this.tableId = tableId this.viewId = viewId - this.excludeFieldIds = excludeFieldIds + this.excludeFieldNames = excludeFieldNames this.accessToken = accessToken this.limit = limit this.returnAll = returnAll @@ -226,7 +226,7 @@ class AirtableLoader extends BaseDocumentLoader { const fields = { ...page.fields }; // Exclude any specified fields - this.excludeFieldIds.forEach(id => { + this.excludeFieldNames.forEach(id => { delete fields[id]; }); From 573863ae6c071e7e2f02d8cd632524919772bab7 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 30 Dec 2023 23:39:05 +0000 Subject: [PATCH 157/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.8=20minor=20?= =?UTF-8?q?release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 81fb3637..5ecbb59b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.7", + "version": "1.4.8", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 581dfafe..54409e29 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.7", + "version": "1.4.8", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 8b4793b317bae87e9304979871bbd8bf5061217c Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 30 Dec 2023 23:39:34 +0000 Subject: [PATCH 158/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.5.0?= =?UTF-8?q?=20minor=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 9cb0bf1e..cb3448eb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.9", + "version": "1.5.0", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 45a42c4404c82872861110606d69cb8ac3f27f14 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 30 Dec 2023 23:39:53 +0000 Subject: [PATCH 159/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.6=20minor?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ab0befb..c5549b23 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.5", + "version": "1.4.6", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From da76a151ff08d346c1091895876f109326b8fc89 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sun, 31 Dec 2023 12:56:35 +0530 Subject: [PATCH 160/502] minor typo fixes... --- packages/components/src/handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index ce7a1a1c..1eb05a51 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -536,7 +536,6 @@ export class AnalyticHandler { if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) { const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace] if (trace) { - trace.id const generation = trace.generation({ name, input: input From 9ba38dcd73fff805a13da61669f5163d28e563ee Mon Sep 17 00:00:00 2001 From: cosark <121065588+cosark@users.noreply.github.com> Date: Sun, 31 Dec 2023 17:16:53 -0700 Subject: [PATCH 161/502] Added deploy template for RepoCloud.io Integration with RepoCloud Deploy Template to the Flowise GitHub page, enabling one-click deployment. This addition simplifies the process for users to quickly deploy and scale Flowise using RepoCloud's efficient cloud hosting services. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 25026237..a80c53f7 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,10 @@ Flowise support different environment variables to configure your instance. You [![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) +### [RepoCloud](https://repocloud.io/details/?app_id=29) + +[![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) + ### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) HuggingFace Spaces From 467e71ba1f01dcd3ab2b86c79d981987f9667ba2 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 18:36:23 -0800 Subject: [PATCH 162/502] added support for MMR --- .../nodes/chains/VectaraChain/VectaraChain.ts | 38 ++++++++++++++----- .../nodes/vectorstores/Vectara/Vectara.ts | 31 +++++++++++++-- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 3799d062..c80b354f 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -69,22 +69,23 @@ class VectaraChain_Chains implements INode { options: [ { label: 'vectara-summary-ext-v1.2.0 (gpt-3.5-turbo)', - name: 'vectara-summary-ext-v1.2.0' + name: 'vectara-summary-ext-v1.2.0', + description: 'base summarizer, available to all Vectara users' }, { label: 'vectara-experimental-summary-ext-2023-10-23-small (gpt-3.5-turbo)', name: 'vectara-experimental-summary-ext-2023-10-23-small', - description: 'In beta, available to both Growth and Scale Vectara users' + description: `In beta, available to both Growth and Scale Vectara users` }, { label: 'vectara-summary-ext-v1.3.0 (gpt-4.0)', name: 'vectara-summary-ext-v1.3.0', - description: 'Only available to paying Scale Vectara users' + description: 'Only available to Scale Vectara users' }, { label: 'vectara-experimental-summary-ext-2023-10-23-med (gpt-4.0)', name: 'vectara-experimental-summary-ext-2023-10-23-med', - description: 'In beta, only available to paying Scale Vectara users' + description: `In beta, only available to Scale Vectara users` } ], default: 'vectara-summary-ext-v1.2.0' @@ -228,7 +229,7 @@ class VectaraChain_Chains implements INode { async run(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore - const responseLang = (nodeData.inputs?.responseLang as string) ?? 'auto' + const responseLang = (nodeData.inputs?.responseLang as string) ?? 'eng' const summarizerPromptName = nodeData.inputs?.summarizerPromptName as string const maxSummarizedResultsStr = nodeData.inputs?.maxSummarizedResults as string const maxSummarizedResults = maxSummarizedResultsStr ? parseInt(maxSummarizedResultsStr, 10) : 7 @@ -247,17 +248,28 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) + const mmrRerankerId = 272725718 // Vectara reranker ID for MMR const data = { query: [ { query: input, start: 0, - numResults: topK, + numResults: vectaraFilter?.mmrConfig?.mmrK > 0 ? vectaraFilter?.mmrK : topK, + corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 }, - corpusKey: corpusKeys, + ...(vectaraFilter?.mmrConfig?.mmrK > 0 + ? { + rerankingConfig: { + rerankerId: mmrRerankerId, + mmrConfig: { + diversityBias: vectaraFilter?.mmrConfig.diversityBias + } + } + } + : {}), summary: [ { summarizerPromptName, @@ -285,6 +297,14 @@ class VectaraChain_Chains implements INode { const documents = result.responseSet[0].document let rawSummarizedText = '' + // remove responses that are not in the topK (in case of MMR) + // Note that this does not really matter functionally due to the reorder citations, but it is more efficient + const maxResponses = vectaraFilter?.mmrConfig?.mmrK > 0 ? Math.min(responses.length, topK) : responses.length + if (responses.length > maxResponses) { + responses.splice(0, maxResponses) + } + + // Add metadata to each text response given its corresponding document metadata for (let i = 0; i < responses.length; i += 1) { const responseMetadata = responses[i].metadata const documentMetadata = documents[responses[i].documentIndex].metadata @@ -301,13 +321,13 @@ class VectaraChain_Chains implements INode { responses[i].metadata = combinedMetadata } + // Create the summarization response const summaryStatus = result.responseSet[0].summary[0].status if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { throw new Error( `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` ) } - if ( summaryStatus.length > 0 && summaryStatus[0].code === 'NOT_FOUND' && @@ -316,8 +336,8 @@ class VectaraChain_Chains implements INode { throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) } + // Reorder citations in summary and create the list of returned source documents rawSummarizedText = result.responseSet[0].summary[0]?.text - let summarizedText = reorderCitations(rawSummarizedText) let summaryResponses = applyCitationOrder(responses, rawSummarizedText) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 7460c586..98acf00c 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -82,7 +82,9 @@ class Vectara_VectorStores implements INode { label: 'Lambda', name: 'lambda', description: - 'Improves retrieval accuracy by adjusting the balance (from 0 to 1) between neural search and keyword-based search factors.', + 'Enable hybrid search to improve retrieval accuracy by adjusting the balance (from 0 to 1) between neural search and keyword-based search factors.' + + 'A value of 0.0 means that only neural search is used, while a value of 1.0 means that only keyword-based search is used. Defaults to 0.0 (neural only).', + default: 0.0, type: 'number', additionalParams: true, optional: true @@ -90,8 +92,26 @@ class Vectara_VectorStores implements INode { { label: 'Top K', name: 'topK', - description: 'Number of top results to fetch. Defaults to 4', - placeholder: '4', + description: 'Number of top results to fetch. Defaults to 5', + placeholder: '5', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'MMR K', + name: 'mmrK', + description: 'Number of top results to fetch for MMR. Defaults to 50', + placeholder: '50', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'MMR diversity bias', + name: 'mmrDiversityBias', + description: 'The diversity bias to use for MMR. Defaults to 0.3', + placeholder: '0.3', type: 'number', additionalParams: true, optional: true @@ -191,7 +211,9 @@ class Vectara_VectorStores implements INode { const lambda = nodeData.inputs?.lambda as number const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 + const k = topK ? parseFloat(topK) : 5 + const mmrK = nodeData.inputs?.mmrK as number + const mmrDiversityBias = nodeData.inputs?.mmrDiversityBias as number const vectaraArgs: VectaraLibArgs = { apiKey: apiKey, @@ -208,6 +230,7 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig + if (mmrK) vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } const vectorStore = new VectaraStore(vectaraArgs) From e5f0ca0c0ac869d1ab49e835e77d8e810199bd3f Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 18:59:49 -0800 Subject: [PATCH 163/502] bug fix --- .../components/nodes/chains/VectaraChain/VectaraChain.ts | 8 +++++--- packages/components/nodes/vectorstores/Vectara/Vectara.ts | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index c80b354f..16257b69 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -249,18 +249,20 @@ class VectaraChain_Chains implements INode { })) const mmrRerankerId = 272725718 // Vectara reranker ID for MMR + const mmrEnabled = vectaraFilter?.mmrConfig?.mmrDiversityBias > 0 + const data = { query: [ { query: input, start: 0, - numResults: vectaraFilter?.mmrConfig?.mmrK > 0 ? vectaraFilter?.mmrK : topK, + numResults: mmrEnabled ? vectaraFilter?.mmrK : topK, corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 }, - ...(vectaraFilter?.mmrConfig?.mmrK > 0 + ...(mmrEnabled ? { rerankingConfig: { rerankerId: mmrRerankerId, @@ -299,7 +301,7 @@ class VectaraChain_Chains implements INode { // remove responses that are not in the topK (in case of MMR) // Note that this does not really matter functionally due to the reorder citations, but it is more efficient - const maxResponses = vectaraFilter?.mmrConfig?.mmrK > 0 ? Math.min(responses.length, topK) : responses.length + const maxResponses = mmrEnabled ? Math.min(responses.length, topK) : responses.length if (responses.length > maxResponses) { responses.splice(0, maxResponses) } diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 98acf00c..488a8803 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -110,8 +110,8 @@ class Vectara_VectorStores implements INode { { label: 'MMR diversity bias', name: 'mmrDiversityBias', - description: 'The diversity bias to use for MMR. Defaults to 0.3', - placeholder: '0.3', + description: 'The diversity bias to use for MMR. Defaults to 0 (MMR disabled)', + placeholder: '0.0', type: 'number', additionalParams: true, optional: true @@ -230,7 +230,7 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig - if (mmrK) vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } + vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } const vectorStore = new VectaraStore(vectaraArgs) From dc3e4fd059b64d153de0e5c11ebb0836b59ca56f Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 19:18:33 -0800 Subject: [PATCH 164/502] bug fix 2 --- .../nodes/vectorstores/Vectara/Vectara.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 488a8803..d83f6cb9 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -1,5 +1,12 @@ import { flatten } from 'lodash' -import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile } from 'langchain/vectorstores/vectara' +import { + VectaraStore, + VectaraLibArgs, + VectaraFilter, + VectaraContextConfig, + VectaraFile, + VectaraMMRConfig +} from 'langchain/vectorstores/vectara' import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' @@ -230,7 +237,10 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig - vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } + const mmrConfig: VectaraMMRConfig = {} + mmrConfig.mmrK = mmrK + mmrConfig.diversityBias = mmrDiversityBias + vectaraFilter.mmrConfig = mmrConfig const vectorStore = new VectaraStore(vectaraArgs) From b44a0f1d39f5c7f3c1c00a16900229fb6a3b7e08 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 21:53:48 -0800 Subject: [PATCH 165/502] bugfix --- .../nodes/chains/VectaraChain/VectaraChain.ts | 4 ++-- .../nodes/vectorstores/Vectara/Vectara.ts | 14 ++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 16257b69..986d587a 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -249,14 +249,14 @@ class VectaraChain_Chains implements INode { })) const mmrRerankerId = 272725718 // Vectara reranker ID for MMR - const mmrEnabled = vectaraFilter?.mmrConfig?.mmrDiversityBias > 0 + const mmrEnabled = vectaraFilter?.mmrConfig?.enabled const data = { query: [ { query: input, start: 0, - numResults: mmrEnabled ? vectaraFilter?.mmrK : topK, + numResults: mmrEnabled ? vectaraFilter?.mmrTopK : topK, corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index d83f6cb9..be63d582 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -1,12 +1,5 @@ import { flatten } from 'lodash' -import { - VectaraStore, - VectaraLibArgs, - VectaraFilter, - VectaraContextConfig, - VectaraFile, - VectaraMMRConfig -} from 'langchain/vectorstores/vectara' +import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile, MMRConfig } from 'langchain/vectorstores/vectara' import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' @@ -237,8 +230,9 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig - const mmrConfig: VectaraMMRConfig = {} - mmrConfig.mmrK = mmrK + const mmrConfig: MMRConfig = {} + mmrConfig.enabled = mmrDiversityBias > 0 + mmrConfig.mmrTopK = mmrK mmrConfig.diversityBias = mmrDiversityBias vectaraFilter.mmrConfig = mmrConfig From e4ab1df4286bef22db119a6cc57f7c771d42b773 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 23:40:04 -0800 Subject: [PATCH 166/502] na --- package.json | 2 +- packages/components/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5ecbb59b..cac38984 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,5 @@ }, "engines": { "node": ">=18.15.0" - } + }, } diff --git a/packages/components/package.json b/packages/components/package.json index cb3448eb..72c8f815 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -47,12 +47,12 @@ "express": "^4.17.3", "faiss-node": "^0.2.2", "form-data": "^4.0.0", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.4.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "langchain": "^0.0.196", + "langchain": "^0.0.213", "langfuse": "^1.2.0", "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.49", From 3f835fb50731f8eafede6bfeb59e01f8500b8b91 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 23:41:19 -0800 Subject: [PATCH 167/502] extra comma --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cac38984..5ecbb59b 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,5 @@ }, "engines": { "node": ">=18.15.0" - }, + } } From e88859f5d4326a44743e0a87e871e31ca9e00c7d Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Mon, 1 Jan 2024 13:33:09 -0500 Subject: [PATCH 168/502] Added support for gpt-4 and gpt-4-32k models --- .../components/nodes/llms/Azure OpenAI/AzureOpenAI.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts b/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts index f50e3d95..a8ac8830 100644 --- a/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts +++ b/packages/components/nodes/llms/Azure OpenAI/AzureOpenAI.ts @@ -18,7 +18,7 @@ class AzureOpenAI_LLMs implements INode { constructor() { this.label = 'Azure OpenAI' this.name = 'azureOpenAI' - this.version = 2.0 + this.version = 2.1 this.type = 'AzureOpenAI' this.icon = 'Azure.svg' this.category = 'LLMs' @@ -89,6 +89,14 @@ class AzureOpenAI_LLMs implements INode { { label: 'gpt-35-turbo', name: 'gpt-35-turbo' + }, + { + label: 'gpt-4', + name: 'gpt-4' + }, + { + label: 'gpt-4-32k', + name: 'gpt-4-32k' } ], default: 'text-davinci-003', From 383f612e114050eb71977bb0498ac356c3afbdf3 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 3 Jan 2024 00:35:55 +0000 Subject: [PATCH 169/502] add check for secretkey_path --- packages/server/src/utils/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 39bd0854..67187325 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -873,7 +873,9 @@ export const getEncryptionKey = async (): Promise => { return await fs.promises.readFile(getEncryptionKeyPath(), 'utf8') } catch (error) { const encryptKey = generateEncryptKey() - const defaultLocation = path.join(getUserHome(), '.flowise', 'encryption.key') + const defaultLocation = process.env.SECRETKEY_PATH + ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') + : path.join(getUserHome(), '.flowise', 'encryption.key') await fs.promises.writeFile(defaultLocation, encryptKey) return encryptKey } From e513b69e3eb50dd0b453338df30937a892d28496 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 3 Jan 2024 00:54:31 +0000 Subject: [PATCH 170/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.9=20bugfix?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5ecbb59b..5a9bfcbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.8", + "version": "1.4.9", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 54409e29..f1c0b7f7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.8", + "version": "1.4.9", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 14aa19987880e62f0f623088c3a9fcd800b798e6 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 3 Jan 2024 17:40:39 +0000 Subject: [PATCH 171/502] no message --- packages/components/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 50377e1a..f3371d26 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -52,10 +52,10 @@ "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "langchain": "^0.0.213", + "langchain": "^0.0.214", "langfuse": "^1.2.0", "langfuse-langchain": "^1.0.31", - "langsmith": "^0.0.49", + "langsmith": "^0.0.53", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", "mammoth": "^1.5.1", From c035363d6f5558c55e0ccf4308e3c87d80715e6e Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Wed, 3 Jan 2024 13:20:39 -0500 Subject: [PATCH 172/502] Fixing linting issues using 'yarn lint-fix' --- .../nodes/documentloaders/Airtable/Airtable.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index a2c1eef3..a7cd5021 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -55,14 +55,14 @@ class Airtable_DocumentLoaders implements INode { description: 'If your table URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, tblJdmvbrgizbYICO is the table id' }, - { - label: 'View Id', - name: 'viewId', - type: 'string', - placeholder: 'viw9UrP77Id0CE4ee', - description: - 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', - optional: true + { + label: 'View Id', + name: 'viewId', + type: 'string', + placeholder: 'viw9UrP77Id0CE4ee', + description: + 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', + optional: true }, { label: 'Return All', From 66701cec8a8def398813f584663ab83b3cb13a26 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Wed, 3 Jan 2024 13:35:25 -0500 Subject: [PATCH 173/502] Fixing linting issues using 'yarn lint-fix' --- .../documentloaders/Airtable/Airtable.ts | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index d7ae2667..04d7d735 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -55,24 +55,23 @@ class Airtable_DocumentLoaders implements INode { description: 'If your table URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, tblJdmvbrgizbYICO is the table id' }, - { - label: 'View Id', - name: 'viewId', - type: 'string', - placeholder: 'viw9UrP77Id0CE4ee', - description: - 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', - optional: true + { + label: 'View Id', + name: 'viewId', + type: 'string', + placeholder: 'viw9UrP77Id0CE4ee', + description: + 'If your view URL looks like: https://airtable.com/app11RobdGoX0YNsC/tblJdmvbrgizbYICO/viw9UrP77Id0CE4ee, viw9UrP77Id0CE4ee is the view id', + optional: true }, { - label: 'Exclude Field Names', - name: 'excludeFieldNames', - type: 'string', - placeholder: 'Name, Assignee', - optional: true, + label: 'Exclude Field Names', + name: 'excludeFieldNames', + type: 'string', + placeholder: 'Name, Assignee', + optional: true, additionalParams: true, - description: - 'Comma-separated list of field names to exclude' + description: 'Comma-separated list of field names to exclude' }, { label: 'Return All', @@ -116,7 +115,7 @@ class Airtable_DocumentLoaders implements INode { baseId, tableId, viewId, - excludeFieldNames: excludeFieldNames ? excludeFieldNames.split(',').map(id => id.trim()) : [], + excludeFieldNames: excludeFieldNames ? excludeFieldNames.split(',').map((id) => id.trim()) : [], returnAll, accessToken, limit: limit ? parseInt(limit, 10) : 100 @@ -223,12 +222,12 @@ class AirtableLoader extends BaseDocumentLoader { private createDocumentFromPage(page: AirtableLoaderPage): Document { // Generate the URL const pageUrl = `https://api.airtable.com/v0/${this.baseId}/${this.tableId}/${page.id}` - const fields = { ...page.fields }; + const fields = { ...page.fields } // Exclude any specified fields - this.excludeFieldNames.forEach(id => { - delete fields[id]; - }); + this.excludeFieldNames.forEach((id) => { + delete fields[id] + }) // Return a langchain document return new Document({ From e8c85035f238cdacd3f23e148914d49502aff753 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Wed, 3 Jan 2024 15:25:00 -0500 Subject: [PATCH 174/502] Made streaming support a configurable option within the AzureChatOpenAI node --- .../chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts index 9b7b724a..33f6b76e 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts @@ -19,7 +19,7 @@ class AzureChatOpenAI_ChatModels implements INode { constructor() { this.label = 'Azure ChatOpenAI' this.name = 'azureChatOpenAI' - this.version = 2.0 + this.version = 2.1 this.type = 'AzureChatOpenAI' this.icon = 'Azure.svg' this.category = 'Chat Models' @@ -102,6 +102,14 @@ class AzureChatOpenAI_ChatModels implements INode { step: 1, optional: true, additionalParams: true + }, + { + label: 'Streaming', + name: 'streaming', + type: 'boolean', + default: true, + optional: true, + additionalParams: true } ] } From 53bfd07694b6bd6e1783dae08f7ebaa20ffa4f05 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Wed, 3 Jan 2024 21:23:43 -0500 Subject: [PATCH 175/502] Bumping version to reflect new feature --- packages/components/nodes/documentloaders/Airtable/Airtable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index a7cd5021..9a824ac9 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -20,7 +20,7 @@ class Airtable_DocumentLoaders implements INode { constructor() { this.label = 'Airtable' this.name = 'airtable' - this.version = 1.0 + this.version = 2.0 this.type = 'Document' this.icon = 'airtable.svg' this.category = 'Document Loaders' From efe602970ce2df8ef1b4f186f3ea3f42ff217d6c Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 4 Jan 2024 14:19:46 +0100 Subject: [PATCH 176/502] Added ssl flag for postgres vectorstores --- .../nodes/vectorstores/Postgres/Postgres_Exisiting.ts | 11 ++++++++++- .../nodes/vectorstores/Postgres/Postgres_Upsert.ts | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts b/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts index 99794a0d..da3b1d18 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts @@ -52,6 +52,13 @@ class Postgres_Existing_VectorStores implements INode { name: 'database', type: 'string' }, + { + label: 'SSL Connection', + name: 'sslConnection', + type: 'boolean', + default: false, + optional: false + }, { label: 'Port', name: 'port', @@ -109,6 +116,7 @@ class Postgres_Existing_VectorStores implements INode { const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 + const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -126,7 +134,8 @@ class Postgres_Existing_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string + database: nodeData.inputs?.database as string, + ssl: sslConnection } const args = { diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts b/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts index f706cbe8..25551517 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts @@ -59,6 +59,13 @@ class PostgresUpsert_VectorStores implements INode { name: 'database', type: 'string' }, + { + label: 'SSL Connection', + name: 'sslConnection', + type: 'boolean', + default: false, + optional: false + }, { label: 'Port', name: 'port', @@ -117,6 +124,7 @@ class PostgresUpsert_VectorStores implements INode { const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 + const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -134,7 +142,8 @@ class PostgresUpsert_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string + database: nodeData.inputs?.database as string, + ssl: sslConnection } const args = { From b833cb80d4f5adeaed612db5159dd8f727935153 Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 4 Jan 2024 14:51:41 +0100 Subject: [PATCH 177/502] Added ssl flag --- .../nodes/vectorstores/Postgres/Postgres.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index ac4b80c3..75f9669a 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -60,6 +60,13 @@ class Postgres_VectorStores implements INode { name: 'database', type: 'string' }, + { + label: 'SSL Connection', + name: 'sslConnection', + type: 'boolean', + default: false, + optional: false + }, { label: 'Port', name: 'port', @@ -117,6 +124,7 @@ class Postgres_VectorStores implements INode { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const additionalConfig = nodeData.inputs?.additionalConfig as string + const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -134,7 +142,8 @@ class Postgres_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string + database: nodeData.inputs?.database as string, + ssl: sslConnection } const args = { From 609ae8703de416c1a62dab565f7e7aa13177b333 Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 4 Jan 2024 15:58:18 +0100 Subject: [PATCH 178/502] Added a function which cheks which port to use when port is 443 or 80 --- .../nodes/vectorstores/Qdrant/Qdrant.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 6413f8bf..390e7fc9 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -149,9 +149,12 @@ class Qdrant_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) + const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl); + const client = new QdrantClient({ url: qdrantServerUrl, - apiKey: qdrantApiKey + apiKey: qdrantApiKey, + port: port }) const flattenDocs = docs && docs.length ? flatten(docs) : [] @@ -198,9 +201,12 @@ class Qdrant_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) + const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl); + const client = new QdrantClient({ url: qdrantServerUrl, - apiKey: qdrantApiKey + apiKey: qdrantApiKey, + port: port }) const dbConfig: QdrantLibArgs = { @@ -242,6 +248,25 @@ class Qdrant_VectorStores implements INode { } return vectorStore } + + /** + * Determine the port number from the given URL. + * + * The problem is when not doing this the qdrant-client.js will fall back on 6663 when you enter a port 443 and 80. + * See: https://stackoverflow.com/questions/59104197/nodejs-new-url-urlhttps-myurl-com80-lists-the-port-as-empty + * @param qdrantServerUrl the url to get the port from + */ + static determinePortByUrl(qdrantServerUrl: string) :number { + let port = 6333; + const parsedUrl = new URL(qdrantServerUrl); + if (parsedUrl.protocol === 'https:' && parsedUrl.port === '') { + port = 443; + } + if (parsedUrl.protocol === 'http:' && parsedUrl.port === '') { + port = 80; + } + return port; + } } module.exports = { nodeClass: Qdrant_VectorStores } From 2355cb2ec5c83fe7302c919d89361cd33db35fff Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 4 Jan 2024 16:07:54 +0100 Subject: [PATCH 179/502] Fixed port handling so it returns the correct port and not only 6663 --- packages/components/nodes/vectorstores/Qdrant/Qdrant.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 390e7fc9..54b55d34 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -257,14 +257,18 @@ class Qdrant_VectorStores implements INode { * @param qdrantServerUrl the url to get the port from */ static determinePortByUrl(qdrantServerUrl: string) :number { - let port = 6333; const parsedUrl = new URL(qdrantServerUrl); + + let port = parsedUrl.port ? parseInt(parsedUrl.port) : 6663 + if (parsedUrl.protocol === 'https:' && parsedUrl.port === '') { port = 443; } if (parsedUrl.protocol === 'http:' && parsedUrl.port === '') { port = 80; } + + return port; } } From a046d5296176b0649dcc6abd9d29f8ef8e003a4e Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 4 Jan 2024 16:23:08 +0100 Subject: [PATCH 180/502] Make the linter happy --- .../components/nodes/vectorstores/Qdrant/Qdrant.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 54b55d34..5e01b030 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -149,7 +149,7 @@ class Qdrant_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) - const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl); + const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl) const client = new QdrantClient({ url: qdrantServerUrl, @@ -201,7 +201,7 @@ class Qdrant_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) - const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl); + const port = Qdrant_VectorStores.determinePortByUrl(qdrantServerUrl) const client = new QdrantClient({ url: qdrantServerUrl, @@ -256,20 +256,20 @@ class Qdrant_VectorStores implements INode { * See: https://stackoverflow.com/questions/59104197/nodejs-new-url-urlhttps-myurl-com80-lists-the-port-as-empty * @param qdrantServerUrl the url to get the port from */ - static determinePortByUrl(qdrantServerUrl: string) :number { - const parsedUrl = new URL(qdrantServerUrl); + static determinePortByUrl(qdrantServerUrl: string): number { + const parsedUrl = new URL(qdrantServerUrl) let port = parsedUrl.port ? parseInt(parsedUrl.port) : 6663 if (parsedUrl.protocol === 'https:' && parsedUrl.port === '') { - port = 443; + port = 443 } if (parsedUrl.protocol === 'http:' && parsedUrl.port === '') { - port = 80; + port = 80 } - return port; + return port } } From e35faa57afb5243fe4b011d408b1d96eee263622 Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Thu, 4 Jan 2024 16:27:53 +0100 Subject: [PATCH 181/502] One last linting --- packages/components/nodes/vectorstores/Qdrant/Qdrant.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 5e01b030..e07b728a 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -268,7 +268,6 @@ class Qdrant_VectorStores implements INode { port = 80 } - return port } } From e3982476b0064bbc4bfe42af0cd06500a1095751 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 4 Jan 2024 12:07:25 -0500 Subject: [PATCH 182/502] Bumping version --- packages/components/nodes/documentloaders/Airtable/Airtable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 04d7d735..0f212a0a 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -20,7 +20,7 @@ class Airtable_DocumentLoaders implements INode { constructor() { this.label = 'Airtable' this.name = 'airtable' - this.version = 1.0 + this.version = 2.0 this.type = 'Document' this.icon = 'airtable.svg' this.category = 'Document Loaders' From 3d2b4077cfd5231e908bc279e0bba5fd9e995056 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 4 Jan 2024 12:09:08 -0500 Subject: [PATCH 183/502] Removed streaming feature since it broke chatflows --- .../nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts index 33f6b76e..a459a8ec 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts @@ -102,14 +102,6 @@ class AzureChatOpenAI_ChatModels implements INode { step: 1, optional: true, additionalParams: true - }, - { - label: 'Streaming', - name: 'streaming', - type: 'boolean', - default: true, - optional: true, - additionalParams: true } ] } From d882ebfcb6a12f6fb51c5cda276e70b6fafa2f1b Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 4 Jan 2024 23:57:36 +0000 Subject: [PATCH 184/502] remove restrictions --- LICENSE.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 0f4afcd1..80800001 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,22 +2,6 @@ Version 2.0, January 2004 http://www.apache.org/licenses/ -Flowise is governed by the Apache License 2.0, with additional terms and conditions outlined below: - -Flowise can be used for commercial purposes for "backend-as-a-service" for your applications or as a development platform for enterprises. However, under specific conditions, you must reach out to the project's administrators to secure a commercial license: - -a. Multi-tenant SaaS service: Unless you have explicit written authorization from Flowise, you may not utilize the Flowise source code to operate a multi-tenant SaaS service that closely resembles the Flowise cloud-based services. -b. Logo and copyright information: While using Flowise in commercial application, you are prohibited from removing or altering the LOGO or copyright information displayed in the Flowise console and UI. - -For inquiries regarding licensing matters, please contact hello@flowiseai.com via email. - -Contributors are required to consent to the following terms related to their contributed code: - -a. The project maintainers have the authority to modify the open-source agreement to be more stringent or lenient. -b. Contributed code can be used for commercial purposes, including Flowise's cloud-based services. - -All other rights and restrictions are in accordance with the Apache License 2.0. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. From 8cb939386210a62484cdf4bf5b11a7c455eafaa1 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 5 Jan 2024 02:46:36 +0000 Subject: [PATCH 185/502] add fix for passing json variable --- .../nodes/utilities/CustomFunction/CustomFunction.ts | 2 +- packages/server/src/utils/index.ts | 6 +++++- packages/ui/src/ui-component/dialog/ExpandTextDialog.js | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts index b358b24b..37511e47 100644 --- a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -65,7 +65,7 @@ class CustomFunction_Utilities implements INode { inputVars = typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw) } catch (exception) { - throw new Error("Invalid JSON in the PromptTemplate's promptValues: " + exception) + throw new Error('Invalid JSON in the Custom Function Input Variables: ' + exception) } } diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index e7a35c82..9c2d1d79 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -561,7 +561,11 @@ export const getVariableValue = ( variablePaths.forEach((path) => { const variableValue = variableDict[path] // Replace all occurrence - returnVal = returnVal.split(path).join(variableValue) + if (typeof variableValue === 'object') { + returnVal = returnVal.split(path).join(JSON.stringify(variableValue).replace(/"/g, '\\"')) + } else { + returnVal = returnVal.split(path).join(variableValue) + } }) return returnVal } diff --git a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js index 0ef70e29..f4fdb9f9 100644 --- a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js +++ b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js @@ -67,7 +67,11 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { useEffect(() => { if (executeCustomFunctionNodeApi.data) { - setCodeExecutedResult(executeCustomFunctionNodeApi.data) + if (typeof executeCustomFunctionNodeApi.data === 'object') { + setCodeExecutedResult(JSON.stringify(executeCustomFunctionNodeApi.data, null, 2)) + } else { + setCodeExecutedResult(executeCustomFunctionNodeApi.data) + } } }, [executeCustomFunctionNodeApi.data]) From 36ce6b7a853f8b8f507cf773373134722f4fe063 Mon Sep 17 00:00:00 2001 From: fanux Date: Fri, 5 Jan 2024 18:17:41 +0800 Subject: [PATCH 186/502] add one-click deploy on sealos --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 25026237..6e2ade7d 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,10 @@ Flowise support different environment variables to configure your instance. You HuggingFace Spaces +### Sealos + +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + ### [AWS](https://docs.flowiseai.com/deployment/aws) ### [Azure](https://docs.flowiseai.com/deployment/azure) From d8a778e4d989c87b18b24d5d35c52211c53e532a Mon Sep 17 00:00:00 2001 From: tuxBurner Date: Fri, 5 Jan 2024 12:46:47 +0100 Subject: [PATCH 187/502] Bumped version from 1.0 to 2.0 --- packages/components/nodes/vectorstores/Postgres/Postgres.ts | 2 +- .../nodes/vectorstores/Postgres/Postgres_Exisiting.ts | 2 +- .../components/nodes/vectorstores/Postgres/Postgres_Upsert.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 75f9669a..4e8bae32 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -24,7 +24,7 @@ class Postgres_VectorStores implements INode { constructor() { this.label = 'Postgres' this.name = 'postgres' - this.version = 1.0 + this.version = 2.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts b/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts index da3b1d18..3fa8a107 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres_Exisiting.ts @@ -23,7 +23,7 @@ class Postgres_Existing_VectorStores implements INode { constructor() { this.label = 'Postgres Load Existing Index' this.name = 'postgresExistingIndex' - this.version = 1.0 + this.version = 2.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts b/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts index 25551517..d26a642d 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres_Upsert.ts @@ -24,7 +24,7 @@ class PostgresUpsert_VectorStores implements INode { constructor() { this.label = 'Postgres Upsert Document' this.name = 'postgresUpsert' - this.version = 1.0 + this.version = 2.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' From 595f1ed7f2ec634d25386b29d37febc1816abfb1 Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Sat, 6 Jan 2024 17:16:06 -0700 Subject: [PATCH 188/502] Introduce new credential for LocalAI, Pass optional auth to LocalAI, New env var --- .../credentials/LcoalAIApi.credential.ts | 23 +++++++++++++++ .../chatmodels/ChatLocalAI/ChatLocalAI.ts | 29 +++++++++++++++---- packages/server/.env.example | 2 ++ 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 packages/components/credentials/LcoalAIApi.credential.ts diff --git a/packages/components/credentials/LcoalAIApi.credential.ts b/packages/components/credentials/LcoalAIApi.credential.ts new file mode 100644 index 00000000..624e07fa --- /dev/null +++ b/packages/components/credentials/LcoalAIApi.credential.ts @@ -0,0 +1,23 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class LocalAIApi implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + + constructor() { + this.label = 'LocalAI API' + this.name = 'LocalAIApi' + this.version = 1.0 + this.inputs = [ + { + label: 'LocalAI Api Key', + name: 'LocalAIApiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: LocalAIApi } diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index 18ed409b..258db1f8 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -1,5 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { OpenAIChat } from 'langchain/llms/openai' import { OpenAIChatInput } from 'langchain/chat_models/openai' import { BaseCache } from 'langchain/schema' @@ -14,6 +14,7 @@ class ChatLocalAI_ChatModels implements INode { category: string description: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -25,6 +26,16 @@ class ChatLocalAI_ChatModels implements INode { this.category = 'Chat Models' this.description = 'Use local LLMs like llama.cpp, gpt4all using LocalAI' this.baseClasses = [this.type, 'BaseChatModel', ...getBaseClasses(OpenAIChat)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['LocalAIApi'], + optional: true + } + + const modelOptions = JSON.parse(process.env.LOCALAI_CHAT_MODELS || '[]'); + this.inputs = [ { label: 'Cache', @@ -41,8 +52,10 @@ class ChatLocalAI_ChatModels implements INode { { label: 'Model Name', name: 'modelName', - type: 'string', - placeholder: 'gpt4all-lora-quantized.bin' + type: 'options', + options: modelOptions, + default: modelOptions.length > 0 ? modelOptions[0].name : '', + optional: true }, { label: 'Temperature', @@ -79,19 +92,23 @@ class ChatLocalAI_ChatModels implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const temperature = nodeData.inputs?.temperature as string const modelName = nodeData.inputs?.modelName as string const maxTokens = nodeData.inputs?.maxTokens as string const topP = nodeData.inputs?.topP as string const timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const openAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) + const cache = nodeData.inputs?.cache as BaseCache const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { temperature: parseFloat(temperature), modelName, - openAIApiKey: 'sk-' + openAIApiKey } if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) diff --git a/packages/server/.env.example b/packages/server/.env.example index 6e746a4d..9b7be0ff 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -26,3 +26,5 @@ PORT=3000 # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key # LANGCHAIN_PROJECT=your_project + +# LOCALAI_CHAT_MODELS='[{"label": "model1", "name": "model1"}, {"label": "model2", "name": "model2"}]' From accea214d22356f780428f0d40a00b9d3353904f Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Sat, 6 Jan 2024 17:33:41 -0700 Subject: [PATCH 189/502] Updating docs --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04cb80b4..2c91906c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,6 +141,7 @@ Flowise support different environment variables to configure your instance. You | DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | +| LOCALAI_CHAT_MODELS | JSON-encoded string representing an array of chat models for LocalAI. Each object in the array should have a 'label' and 'name' property. | String | '[]' (Empty Array) | You can also specify the env variables when using `npx`. For example: From 02482f1b3862779ea14e8a335f4299c76030a21e Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Jan 2024 13:02:56 +0000 Subject: [PATCH 190/502] change agent/chain with memory to use runnable --- .../ConversationalAgent.ts | 163 +++-- .../ConversationalRetrievalAgent.ts | 127 ++-- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 75 ++- .../OpenAIFunctionAgent.ts | 273 +------- .../ConversationChain/ConversationChain.ts | 146 +++-- .../ConversationalRetrievalQAChain.ts | 380 +++++++---- .../ConversationalRetrievalQAChain/prompts.ts | 79 +-- .../nodes/memory/BufferMemory/BufferMemory.ts | 33 +- .../BufferWindowMemory/BufferWindowMemory.ts | 34 +- .../ConversationSummaryMemory.ts | 42 +- .../nodes/memory/DynamoDb/DynamoDb.ts | 49 +- .../memory/MongoDBMemory/MongoDBMemory.ts | 49 +- .../memory/MotorheadMemory/MotorheadMemory.ts | 92 ++- .../RedisBackedChatMemory.ts | 70 +- .../UpstashRedisBackedChatMemory.ts | 49 +- .../nodes/memory/ZepMemory/ZepMemory.ts | 46 +- .../nodes/tools/CustomTool/CustomTool.ts | 8 +- .../components/nodes/tools/CustomTool/core.ts | 17 +- packages/components/package.json | 1 + packages/components/src/Interface.ts | 28 +- packages/components/src/agents.ts | 615 ++++++++++++++++++ .../marketplaces/chatflows/API Agent.json | 2 +- .../chatflows/Chat with a Podcast.json | 56 +- .../marketplaces/chatflows/Claude LLM.json | 2 +- .../chatflows/Conversational Agent.json | 2 +- .../Conversational Retrieval QA Chain.json | 62 +- .../chatflows/Flowise Docs QnA.json | 61 +- .../marketplaces/chatflows/Local QnA.json | 61 +- .../chatflows/Long Term Memory.json | 63 +- .../chatflows/Metadata Filter.json | 61 +- .../chatflows/Multiple VectorDB.json | 2 +- .../chatflows/Simple Conversation Chain.json | 2 +- .../chatflows/Vectara LLM Chain Upload.json | 55 +- .../marketplaces/chatflows/WebBrowser.json | 2 +- .../marketplaces/chatflows/WebPage QnA.json | 63 +- packages/server/src/index.ts | 98 ++- packages/server/src/utils/index.ts | 175 ++--- .../ui/src/views/canvas/NodeInputHandler.js | 3 +- 38 files changed, 1752 insertions(+), 1394 deletions(-) create mode 100644 packages/components/src/agents.ts diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index 8a2329b5..7f857b1c 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -1,11 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor, InitializeAgentExecutorOptions } from 'langchain/agents' import { Tool } from 'langchain/tools' -import { BaseChatMemory } from 'langchain/memory' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' import { BaseChatModel } from 'langchain/chat_models/base' import { flatten } from 'lodash' -import { additionalCallbacks } from '../../../src/handler' +import { AgentStep, BaseMessage, ChainValues, AIMessage, HumanMessage } from 'langchain/schema' +import { RunnableSequence } from 'langchain/schema/runnable' +import { getBaseClasses } from '../../../src/utils' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { AgentExecutor } from '../../../src/agents' +import { ChatConversationalAgent } from 'langchain/agents' +import { renderTemplate } from '@langchain/core/prompts' const DEFAULT_PREFIX = `Assistant is a large language model trained by OpenAI. @@ -15,6 +18,15 @@ Assistant is constantly learning and improving, and its capabilities are constan Overall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.` +const TEMPLATE_TOOL_RESPONSE = `TOOL RESPONSE: +--------------------- +{observation} + +USER'S INPUT +-------------------- + +Okay, so what is the response to my last comment? If using information obtained from the tools you must mention it explicitly without mentioning the tool names - I have forgotten all TOOL RESPONSES! Remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else.` + class ConversationalAgent_Agents implements INode { label: string name: string @@ -25,8 +37,9 @@ class ConversationalAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Agent' this.name = 'conversationalAgent' this.version = 2.0 @@ -43,7 +56,7 @@ class ConversationalAgent_Agents implements INode { list: true }, { - label: 'Language Model', + label: 'Chat Model', name: 'model', type: 'BaseChatModel' }, @@ -62,52 +75,114 @@ class ConversationalAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseChatModel - let tools = nodeData.inputs?.tools as Tool[] - tools = flatten(tools) - const memory = nodeData.inputs?.memory as BaseChatMemory - const systemMessage = nodeData.inputs?.systemMessage as string - - const obj: InitializeAgentExecutorOptions = { - agentType: 'chat-conversational-react-description', - verbose: process.env.DEBUG === 'true' ? true : false - } - - const agentArgs: any = {} - if (systemMessage) { - agentArgs.systemMessage = systemMessage - } - - if (Object.keys(agentArgs).length) obj.agentArgs = agentArgs - - const executor = await initializeAgentExecutorWithOptions(tools, model, obj) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - const memory = nodeData.inputs?.memory as BaseChatMemory - - 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 memory = nodeData.inputs?.memory as FlowiseMemory + const executor = await prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) - const result = await executor.call({ input }, [...callbacks]) - return result?.output + let res: ChainValues = {} + + if (options.socketIO && options.socketIOClientId) { + const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) + } else { + 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 prepareAgent = async ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as BaseChatModel + let tools = nodeData.inputs?.tools as Tool[] + tools = flatten(tools) + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + /** Bind a stop token to the model */ + const modelWithStop = model.bind({ + stop: ['\nObservation'] + }) + + const outputParser = ChatConversationalAgent.getDefaultOutputParser({ + llm: model, + toolNames: tools.map((tool) => tool.name) + }) + + const prompt = ChatConversationalAgent.createPrompt(tools, { + systemMessage: systemMessage ? systemMessage : DEFAULT_PREFIX, + outputParser + }) + + const runnableAgent = RunnableSequence.from([ + { + [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, + agent_scratchpad: async (i: { input: string; steps: AgentStep[] }) => await constructScratchPad(i.steps), + [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithStop, + outputParser + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + +const constructScratchPad = async (steps: AgentStep[]): Promise => { + const thoughts: BaseMessage[] = [] + for (const step of steps) { + thoughts.push(new AIMessage(step.action.log)) + thoughts.push( + new HumanMessage( + renderTemplate(TEMPLATE_TOOL_RESPONSE, 'f-string', { + observation: step.observation + }) + ) + ) + } + return thoughts +} + module.exports = { nodeClass: ConversationalAgent_Agents } diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index 643c6a65..406a156f 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts @@ -1,9 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' +import { ChainValues, AgentStep, BaseMessage } from 'langchain/schema' import { flatten } from 'lodash' -import { BaseChatMemory } from 'langchain/memory' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { formatToOpenAIFunction } from 'langchain/tools' +import { RunnableSequence } from 'langchain/schema/runnable' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { OpenAIFunctionsAgentOutputParser } from 'langchain/agents/openai/output_parser' +import { AgentExecutor, formatAgentSteps } from '../../../src/agents' const defaultMessage = `Do your best to answer the questions. Feel free to use any tools available to look up relevant information, only if necessary.` @@ -17,8 +22,9 @@ class ConversationalRetrievalAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Retrieval Agent' this.name = 'conversationalRetrievalAgent' this.version = 3.0 @@ -54,55 +60,96 @@ class ConversationalRetrievalAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model - 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 ?? defaultMessage - }, - returnIntermediateSteps: true - }) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - - if (executor.memory) { - ;(executor.memory as any).memoryKey = 'chat_history' - ;(executor.memory as any).outputKey = 'output' - ;(executor.memory as any).returnMessages = true - - const chatHistoryClassName = (executor.memory as any).chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - ;(executor.memory as any).chatHistory = mapChatHistory(options) - } - } + const memory = nodeData.inputs?.memory as FlowiseMemory + const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) 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.call({ input }, [loggerHandler, handler, ...callbacks]) - return result?.output + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const result = await executor.call({ input }, [loggerHandler, ...callbacks]) - return result?.output + 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 prepareAgent = ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as ChatOpenAI + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + let tools = nodeData.inputs?.tools + tools = flatten(tools) + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + const prompt = ChatPromptTemplate.fromMessages([ + ['ai', systemMessage ? systemMessage : defaultMessage], + new MessagesPlaceholder(memoryKey), + ['human', `{${inputKey}}`], + new MessagesPlaceholder('agent_scratchpad') + ]) + + const modelWithFunctions = model.bind({ + functions: [...tools.map((tool: any) => formatToOpenAIFunction(tool))] + }) + + const runnableAgent = RunnableSequence.from([ + { + [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 = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithFunctions, + new OpenAIFunctionsAgentOutputParser() + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + returnIntermediateSteps: true, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + module.exports = { nodeClass: ConversationalRetrievalAgent_Agents } diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index cf69022b..62ecec5b 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -96,45 +96,51 @@ class OpenAIAssistant_Agents implements INode { return null } - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const selectedAssistantId = nodeData.inputs?.selectedAssistant as string - const appDataSource = options.appDataSource as DataSource - const databaseEntities = options.databaseEntities as IDatabaseEntity - let sessionId = nodeData.inputs?.sessionId as string + async clearChatMessages(nodeData: INodeData, options: ICommonObject, sessionIdObj: { type: string; id: string }): Promise { + const selectedAssistantId = nodeData.inputs?.selectedAssistant as string + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity - const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ - id: selectedAssistantId + const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ + id: selectedAssistantId + }) + + if (!assistant) { + options.logger.error(`Assistant ${selectedAssistantId} not found`) + return + } + + if (!sessionIdObj) return + + let sessionId = '' + if (sessionIdObj.type === 'chatId') { + const chatId = sessionIdObj.id + const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ + chatId }) - - if (!assistant) { - options.logger.error(`Assistant ${selectedAssistantId} not found`) + if (!chatmsg) { + options.logger.error(`Chat Message with Chat Id: ${chatId} not found`) return } + sessionId = chatmsg.sessionId + } else if (sessionIdObj.type === 'threadId') { + sessionId = sessionIdObj.id + } - if (!sessionId && options.chatId) { - const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: options.chatId - }) - if (!chatmsg) { - options.logger.error(`Chat Message with Chat Id: ${options.chatId} not found`) - return - } - sessionId = chatmsg.sessionId - } + const credentialData = await getCredentialData(assistant.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + if (!openAIApiKey) { + options.logger.error(`OpenAI ApiKey not found`) + return + } - const credentialData = await getCredentialData(assistant.credential ?? '', options) - const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) - if (!openAIApiKey) { - options.logger.error(`OpenAI ApiKey not found`) - return - } - - const openai = new OpenAI({ apiKey: openAIApiKey }) - options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + const openai = new OpenAI({ apiKey: openAIApiKey }) + options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + try { if (sessionId) await openai.beta.threads.del(sessionId) options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) + } catch (e) { + throw new Error(e) } } @@ -297,7 +303,11 @@ class OpenAIAssistant_Agents implements INode { options.socketIO.to(options.socketIOClientId).emit('tool', tool.name) try { - const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, threadId) + const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, { + sessionId: threadId, + chatId: options.chatId, + input + }) await analyticHandlers.onToolEnd(toolIds, toolOutput) submitToolOutputs.push({ tool_call_id: actions[i].toolCallId, @@ -462,6 +472,7 @@ class OpenAIAssistant_Agents implements INode { const imageRegex = /]*\/>/g let llmOutput = returnVal.replace(imageRegex, '') llmOutput = llmOutput.replace('
    ', '') + await analyticHandlers.onLLMEnd(llmIds, llmOutput) await analyticHandlers.onChainEnd(parentIds, messageData, true) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index c0095cee..135121d2 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,17 +1,14 @@ -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' -import { CallbackManagerForChainRun } from 'langchain/callbacks' -import { formatToOpenAIFunction } from 'langchain/tools' -import { ToolInputParsingException, Tool } from '@langchain/core/tools' +import { ChainValues, AgentStep, BaseMessage } from 'langchain/schema' import { getBaseClasses } from '../../../src/utils' import { flatten } from 'lodash' import { RunnableSequence } from 'langchain/schema/runnable' +import { formatToOpenAIFunction } from 'langchain/tools' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' 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' +import { AgentExecutor, formatAgentSteps } from '../../../src/agents' class OpenAIFunctionAgent_Agents implements INode { label: string @@ -25,7 +22,7 @@ class OpenAIFunctionAgent_Agents implements INode { inputs: INodeParams[] sessionId?: string - constructor(fields: { sessionId?: string }) { + constructor(fields?: { sessionId?: string }) { this.label = 'OpenAI Function Agent' this.name = 'openAIFunctionAgent' this.version = 3.0 @@ -33,7 +30,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(LCAgentExecutor)] + this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] this.inputs = [ { label: 'Allowed Tools', @@ -63,19 +60,13 @@ class OpenAIFunctionAgent_Agents implements INode { this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const memory = nodeData.inputs?.memory as FlowiseMemory - - const executor = prepareAgent(nodeData, this.sessionId) - if (memory) executor.memory = memory - - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory as FlowiseMemory - - const executor = prepareAgent(nodeData, this.sessionId) + const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) @@ -107,17 +98,11 @@ class OpenAIFunctionAgent_Agents implements INode { } } -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 prepareAgent = ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { const model = nodeData.inputs?.model as ChatOpenAI const memory = nodeData.inputs?.memory as FlowiseMemory const systemMessage = nodeData.inputs?.systemMessage as string @@ -142,7 +127,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 = (await memory.getChatMessages(sessionId, true)) as BaseMessage[] + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] return messages ?? [] } }, @@ -154,231 +139,13 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { const executor = AgentExecutor.fromAgentAndTools({ agent: runnableAgent, tools, - sessionId + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + verbose: process.env.DEBUG === 'true' ? true : false }) return executor } -type AgentExecutorOutput = ChainValues - -class AgentExecutor extends LCAgentExecutor { - sessionId?: string - - static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string }): AgentExecutor { - const newInstance = new AgentExecutor(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 - } -} - module.exports = { nodeClass: OpenAIFunctionAgent_Agents } diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 54d4252a..fcd9921e 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -1,14 +1,16 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' import { ConversationChain } from 'langchain/chains' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' +import { getBaseClasses } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' -import { BufferMemory } from 'langchain/memory' import { BaseChatModel } from 'langchain/chat_models/base' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { flatten } from 'lodash' import { Document } from 'langchain/document' +import { RunnableSequence } from 'langchain/schema/runnable' +import { StringOutputParser } from 'langchain/schema/output_parser' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` +const inputKey = 'input' class ConversationChain_Chains implements INode { label: string @@ -20,8 +22,9 @@ class ConversationChain_Chains implements INode { baseClasses: string[] description: string inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversation Chain' this.name = 'conversationChain' this.version = 1.0 @@ -32,7 +35,7 @@ class ConversationChain_Chains implements INode { this.baseClasses = [this.type, ...getBaseClasses(ConversationChain)] this.inputs = [ { - label: 'Language Model', + label: 'Chat Model', name: 'model', type: 'BaseChatModel' }, @@ -60,76 +63,99 @@ class ConversationChain_Chains implements INode { placeholder: 'You are a helpful assistant that write codes' } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseChatModel - const memory = nodeData.inputs?.memory as BufferMemory - const prompt = nodeData.inputs?.systemMessagePrompt as string - const docs = nodeData.inputs?.document as Document[] - - const flattenDocs = docs && docs.length ? flatten(docs) : [] - const finalDocs = [] - for (let i = 0; i < flattenDocs.length; i += 1) { - if (flattenDocs[i] && flattenDocs[i].pageContent) { - finalDocs.push(new Document(flattenDocs[i])) - } - } - - let finalText = '' - for (let i = 0; i < finalDocs.length; i += 1) { - finalText += finalDocs[i].pageContent - } - - const replaceChar: string[] = ['{', '}'] - for (const char of replaceChar) finalText = finalText.replaceAll(char, '') - - if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` - - const obj: any = { - llm: model, - memory, - verbose: process.env.DEBUG === 'true' ? true : false - } - - const chatPrompt = ChatPromptTemplate.fromMessages([ - SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), - new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), - HumanMessagePromptTemplate.fromTemplate('{input}') - ]) - obj.prompt = chatPrompt - - const chain = new ConversationChain(obj) + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) return chain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chain = nodeData.instance as ConversationChain - const memory = nodeData.inputs?.memory as BufferMemory - memory.returnMessages = true // Return true for BaseChatModel - - if (options && options.chatHistory) { - const chatHistoryClassName = memory.chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - memory.chatHistory = mapChatHistory(options) - } - } - - chain.memory = memory + const memory = nodeData.inputs?.memory + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) + let res = '' + if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.call({ input }, [loggerHandler, handler, ...callbacks]) - return res?.response + res = await chain.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const res = await chain.call({ input }, [loggerHandler, ...callbacks]) - return res?.response + res = await chain.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res } } +const prepareChatPrompt = (nodeData: INodeData) => { + const memory = nodeData.inputs?.memory as FlowiseMemory + const prompt = nodeData.inputs?.systemMessagePrompt as string + const docs = nodeData.inputs?.document as Document[] + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + let finalText = '' + for (let i = 0; i < finalDocs.length; i += 1) { + finalText += finalDocs[i].pageContent + } + + const replaceChar: string[] = ['{', '}'] + for (const char of replaceChar) finalText = finalText.replaceAll(char, '') + + if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` + + const chatPrompt = ChatPromptTemplate.fromMessages([ + SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) + ]) + + return chatPrompt +} + +const prepareChain = (nodeData: INodeData, sessionId?: string, chatHistory: IMessage[] = []) => { + const model = nodeData.inputs?.model as BaseChatModel + const memory = nodeData.inputs?.memory as FlowiseMemory + const memoryKey = memory.memoryKey ?? 'chat_history' + + const conversationChain = RunnableSequence.from([ + { + [inputKey]: (input: { input: string }) => input.input, + [memoryKey]: async () => { + const history = await memory.getChatMessages(sessionId, true, chatHistory) + return history + } + }, + prepareChatPrompt(nodeData), + model, + new StringOutputParser() + ]) + + return conversationChain +} + module.exports = { nodeClass: ConversationChain_Chains } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts index 36376e13..5f98cba1 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -1,20 +1,25 @@ import { BaseLanguageModel } from 'langchain/base_language' -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' -import { ConversationalRetrievalQAChain, QAChainParams } from 'langchain/chains' +import { ConversationalRetrievalQAChain } from 'langchain/chains' import { BaseRetriever } from 'langchain/schema/retriever' -import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BufferMemoryInput } from 'langchain/memory' import { PromptTemplate } from 'langchain/prompts' -import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' -import { - default_map_reduce_template, - default_qa_template, - qa_template, - map_reduce_template, - CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT, - refine_question_template, - refine_template -} from './prompts' +import { QA_TEMPLATE, REPHRASE_TEMPLATE, RESPONSE_TEMPLATE } from './prompts' +import { Runnable, RunnableSequence, RunnableMap, RunnableBranch, RunnableLambda } from 'langchain/schema/runnable' +import { BaseMessage, HumanMessage, AIMessage } from 'langchain/schema' +import { StringOutputParser } from 'langchain/schema/output_parser' +import type { Document } from 'langchain/document' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { applyPatch } from 'fast-json-patch' +import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' +import { ConsoleCallbackHandler, additionalCallbacks } from '../../../src/handler' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' + +type RetrievalChainInput = { + chat_history: string + question: string +} + +const sourceRunnableName = 'FindDocs' class ConversationalRetrievalQAChain_Chains implements INode { label: string @@ -26,11 +31,12 @@ class ConversationalRetrievalQAChain_Chains implements INode { baseClasses: string[] description: string inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Retrieval QA Chain' this.name = 'conversationalRetrievalQAChain' - this.version = 1.0 + this.version = 2.0 this.type = 'ConversationalRetrievalQAChain' this.icon = 'qa.svg' this.category = 'Chains' @@ -38,9 +44,9 @@ class ConversationalRetrievalQAChain_Chains implements INode { this.baseClasses = [this.type, ...getBaseClasses(ConversationalRetrievalQAChain)] this.inputs = [ { - label: 'Language Model', + label: 'Chat Model', name: 'model', - type: 'BaseLanguageModel' + type: 'BaseChatModel' }, { label: 'Vector Store Retriever', @@ -60,6 +66,29 @@ class ConversationalRetrievalQAChain_Chains implements INode { type: 'boolean', optional: true }, + { + label: 'Rephrase Prompt', + name: 'rephrasePrompt', + type: 'string', + description: 'Using previous chat history, rephrase question into a standalone question', + warning: 'Prompt must include input variables: {chat_history} and {question}', + rows: 4, + additionalParams: true, + optional: true, + default: REPHRASE_TEMPLATE + }, + { + label: 'Response Prompt', + name: 'responsePrompt', + type: 'string', + description: 'Taking the rephrased question, search for answer from the provided context', + warning: 'Prompt must include input variable: {context}', + rows: 4, + additionalParams: true, + optional: true, + default: RESPONSE_TEMPLATE + } + /** Deprecated { label: 'System Message', name: 'systemMessagePrompt', @@ -70,6 +99,7 @@ class ConversationalRetrievalQAChain_Chains implements INode { placeholder: 'I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". You will provide me with answers from the given info. If the answer is not included, say exactly "Hmm, I am not sure." and stop after that. Refuse to answer any question not about the info. Never break character.' }, + // TODO: create standalone chains for these 3 modes as they are not compatible with memory { label: 'Chain Option', name: 'chainOption', @@ -95,124 +125,246 @@ class ConversationalRetrievalQAChain_Chains implements INode { additionalParams: true, optional: true } + */ ] + this.sessionId = fields?.sessionId } async init(nodeData: INodeData): Promise { const model = nodeData.inputs?.model as BaseLanguageModel const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string - const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const chainOption = nodeData.inputs?.chainOption as string - const externalMemory = nodeData.inputs?.memory + const rephrasePrompt = nodeData.inputs?.rephrasePrompt as string + const responsePrompt = nodeData.inputs?.responsePrompt as string - const obj: any = { - verbose: process.env.DEBUG === 'true' ? true : false, - questionGeneratorChainOptions: { - template: CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT - } + let customResponsePrompt = responsePrompt + // If the deprecated systemMessagePrompt is still exists + if (systemMessagePrompt) { + customResponsePrompt = `${systemMessagePrompt}\n${QA_TEMPLATE}` } - if (returnSourceDocuments) obj.returnSourceDocuments = returnSourceDocuments - - if (chainOption === 'map_reduce') { - obj.qaChainOptions = { - type: 'map_reduce', - combinePrompt: PromptTemplate.fromTemplate( - systemMessagePrompt ? `${systemMessagePrompt}\n${map_reduce_template}` : default_map_reduce_template - ) - } as QAChainParams - } else if (chainOption === 'refine') { - const qprompt = new PromptTemplate({ - inputVariables: ['context', 'question'], - template: refine_question_template(systemMessagePrompt) - }) - const rprompt = new PromptTemplate({ - inputVariables: ['context', 'question', 'existing_answer'], - template: refine_template - }) - obj.qaChainOptions = { - type: 'refine', - questionPrompt: qprompt, - refinePrompt: rprompt - } as QAChainParams - } else { - obj.qaChainOptions = { - type: 'stuff', - prompt: PromptTemplate.fromTemplate(systemMessagePrompt ? `${systemMessagePrompt}\n${qa_template}` : default_qa_template) - } as QAChainParams - } - - if (externalMemory) { - externalMemory.memoryKey = 'chat_history' - externalMemory.inputKey = 'question' - externalMemory.outputKey = 'text' - externalMemory.returnMessages = true - if (chainOption === 'refine') externalMemory.outputKey = 'output_text' - obj.memory = externalMemory - } else { - const fields: BufferMemoryInput = { - memoryKey: 'chat_history', - inputKey: 'question', - outputKey: 'text', - returnMessages: true - } - if (chainOption === 'refine') fields.outputKey = 'output_text' - obj.memory = new BufferMemory(fields) - } - - const chain = ConversationalRetrievalQAChain.fromLLM(model, vectorStoreRetriever, obj) - return chain + const answerChain = createChain(model, vectorStoreRetriever, rephrasePrompt, customResponsePrompt) + return answerChain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chain = nodeData.instance as ConversationalRetrievalQAChain + const model = nodeData.inputs?.model as BaseLanguageModel + const externalMemory = nodeData.inputs?.memory + const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever + const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string + const rephrasePrompt = nodeData.inputs?.rephrasePrompt as string + const responsePrompt = nodeData.inputs?.responsePrompt as string const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const chainOption = nodeData.inputs?.chainOption as string - let model = nodeData.inputs?.model - - // Temporary fix: https://github.com/hwchase17/langchainjs/issues/754 - model.streaming = false - chain.questionGeneratorChain.llm = model - - const obj = { question: input } - - if (options && options.chatHistory && chain.memory) { - const chatHistoryClassName = (chain.memory as any).chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - ;(chain.memory as any).chatHistory = mapChatHistory(options) - } + let customResponsePrompt = responsePrompt + // If the deprecated systemMessagePrompt is still exists + if (systemMessagePrompt) { + customResponsePrompt = `${systemMessagePrompt}\n${QA_TEMPLATE}` } + let memory: FlowiseMemory | undefined = externalMemory + if (!memory) { + memory = new BufferMemory({ + returnMessages: true, + memoryKey: 'chat_history', + inputKey: 'input' + }) + } + + const answerChain = createChain(model, vectorStoreRetriever, rephrasePrompt, customResponsePrompt) + + const history = ((await memory.getChatMessages(this.sessionId, false, options.chatHistory)) as IMessage[]) ?? [] + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) - if (options.socketIO && options.socketIOClientId) { - const handler = new CustomChainHandler( - options.socketIO, - options.socketIOClientId, - chainOption === 'refine' ? 4 : undefined, - returnSourceDocuments - ) - const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) - if (chainOption === 'refine') { - if (res.output_text && res.sourceDocuments) { - return { - text: res.output_text, - sourceDocuments: res.sourceDocuments - } - } - return res?.output_text + const stream = answerChain.streamLog( + { question: input, chat_history: history }, + { callbacks: [loggerHandler, ...callbacks] }, + { + includeNames: [sourceRunnableName] + } + ) + + let streamedResponse: Record = {} + let sourceDocuments: ICommonObject[] = [] + let text = '' + let isStreamingStarted = false + const isStreamingEnabled = options.socketIO && options.socketIOClientId + + for await (const chunk of stream) { + streamedResponse = applyPatch(streamedResponse, chunk.ops).newDocument + + if (streamedResponse.final_output) { + text = streamedResponse.final_output?.output + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('end') + if (Array.isArray(streamedResponse?.logs?.[sourceRunnableName]?.final_output?.output)) { + sourceDocuments = streamedResponse?.logs?.[sourceRunnableName]?.final_output?.output + if (isStreamingEnabled && returnSourceDocuments) + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', sourceDocuments) + } + } + + if ( + Array.isArray(streamedResponse?.streamed_output) && + streamedResponse?.streamed_output.length && + !streamedResponse.final_output + ) { + const token = streamedResponse.streamed_output[streamedResponse.streamed_output.length - 1] + + if (!isStreamingStarted) { + isStreamingStarted = true + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('start', token) + } + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('token', token) } - if (res.text && res.sourceDocuments) return res - return res?.text - } else { - const res = await chain.call(obj, [loggerHandler, ...callbacks]) - if (res.text && res.sourceDocuments) return res - return res?.text } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: text, + type: 'apiMessage' + } + ], + this.sessionId + ) + + if (returnSourceDocuments) return { text, sourceDocuments } + else return { text } + } +} + +const createRetrieverChain = (llm: BaseLanguageModel, retriever: Runnable, rephrasePrompt: string) => { + // Small speed/accuracy optimization: no need to rephrase the first question + // since there shouldn't be any meta-references to prior chat history + const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(rephrasePrompt) + const condenseQuestionChain = RunnableSequence.from([CONDENSE_QUESTION_PROMPT, llm, new StringOutputParser()]).withConfig({ + runName: 'CondenseQuestion' + }) + + const hasHistoryCheckFn = RunnableLambda.from((input: RetrievalChainInput) => input.chat_history.length > 0).withConfig({ + runName: 'HasChatHistoryCheck' + }) + + const conversationChain = condenseQuestionChain.pipe(retriever).withConfig({ + runName: 'RetrievalChainWithHistory' + }) + + const basicRetrievalChain = RunnableLambda.from((input: RetrievalChainInput) => input.question) + .withConfig({ + runName: 'Itemgetter:question' + }) + .pipe(retriever) + .withConfig({ runName: 'RetrievalChainWithNoHistory' }) + + return RunnableBranch.from([[hasHistoryCheckFn, conversationChain], basicRetrievalChain]).withConfig({ runName: sourceRunnableName }) +} + +const formatDocs = (docs: Document[]) => { + return docs.map((doc, i) => `${doc.pageContent}`).join('\n') +} + +const formatChatHistoryAsString = (history: BaseMessage[]) => { + return history.map((message) => `${message._getType()}: ${message.content}`).join('\n') +} + +const serializeHistory = (input: any) => { + const chatHistory: IMessage[] = input.chat_history || [] + const convertedChatHistory = [] + for (const message of chatHistory) { + if (message.type === 'userMessage') { + convertedChatHistory.push(new HumanMessage({ content: message.message })) + } + if (message.type === 'apiMessage') { + convertedChatHistory.push(new AIMessage({ content: message.message })) + } + } + return convertedChatHistory +} + +const createChain = ( + llm: BaseLanguageModel, + retriever: Runnable, + rephrasePrompt = REPHRASE_TEMPLATE, + responsePrompt = RESPONSE_TEMPLATE +) => { + const retrieverChain = createRetrieverChain(llm, retriever, rephrasePrompt) + + const context = RunnableMap.from({ + context: RunnableSequence.from([ + ({ question, chat_history }) => ({ + question, + chat_history: formatChatHistoryAsString(chat_history) + }), + retrieverChain, + RunnableLambda.from(formatDocs).withConfig({ + runName: 'FormatDocumentChunks' + }) + ]), + question: RunnableLambda.from((input: RetrievalChainInput) => input.question).withConfig({ + runName: 'Itemgetter:question' + }), + chat_history: RunnableLambda.from((input: RetrievalChainInput) => input.chat_history).withConfig({ + runName: 'Itemgetter:chat_history' + }) + }).withConfig({ tags: ['RetrieveDocs'] }) + + const prompt = ChatPromptTemplate.fromMessages([ + ['system', responsePrompt], + new MessagesPlaceholder('chat_history'), + ['human', `{question}`] + ]) + + const responseSynthesizerChain = RunnableSequence.from([prompt, llm, new StringOutputParser()]).withConfig({ + tags: ['GenerateResponse'] + }) + + const conversationalQAChain = RunnableSequence.from([ + { + question: RunnableLambda.from((input: RetrievalChainInput) => input.question).withConfig({ + runName: 'Itemgetter:question' + }), + chat_history: RunnableLambda.from(serializeHistory).withConfig({ + runName: 'SerializeHistory' + }) + }, + context, + responseSynthesizerChain + ]) + + return conversationalQAChain +} + +class BufferMemory extends FlowiseMemory implements MemoryMethods { + constructor(fields: BufferMemoryInput) { + super(fields) + } + + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + const memoryResult = await this.loadMemoryVariables({}) + const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return + } + + async clearChatMessages(): Promise { + await this.clear() } } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts index 132e3a97..dccc7358 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts @@ -1,64 +1,27 @@ -export const default_qa_template = `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. - -{context} - -Question: {question} -Helpful Answer:` - -export const qa_template = `Use the following pieces of context to answer the question at the end. - -{context} - -Question: {question} -Helpful Answer:` - -export const default_map_reduce_template = `Given the following extracted parts of a long document and a question, create a final answer. -If you don't know the answer, just say that you don't know. Don't try to make up an answer. - -{summaries} - -Question: {question} -Helpful Answer:` - -export const map_reduce_template = `Given the following extracted parts of a long document and a question, create a final answer. - -{summaries} - -Question: {question} -Helpful Answer:` - -export const refine_question_template = (sysPrompt?: string) => { - let returnPrompt = '' - if (sysPrompt) - returnPrompt = `Context information is below. ---------------------- -{context} ---------------------- -Given the context information and not prior knowledge, ${sysPrompt} -Answer the question: {question}. -Answer:` - if (!sysPrompt) - returnPrompt = `Context information is below. ---------------------- -{context} ---------------------- -Given the context information and not prior knowledge, answer the question: {question}. -Answer:` - return returnPrompt -} - -export const refine_template = `The original question is as follows: {question} -We have provided an existing answer: {existing_answer} -We have the opportunity to refine the existing answer (only if needed) with some more context below. ------------- -{context} ------------- -Given the new context, refine the original answer to better answer the question. -If you can't find answer from the context, return the original answer.` - export const CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, answer in the same language as the follow up question. include it in the standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question:` + +export const RESPONSE_TEMPLATE = `I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". Using the provided context, answer the user's question to the best of your ability using the resources provided. +If there is nothing in the context relevant to the question at hand, just say "Hmm, I'm not sure" and stop after that. Refuse to answer any question not about the info. Never break character. +------------ +{context} +------------ +REMEMBER: If there is no relevant information within the context, just say "Hmm, I'm not sure". Don't try to make up an answer. Never break character.` + +export const QA_TEMPLATE = `Use the following pieces of context to answer the question at the end. + +{context} + +Question: {question} +Helpful Answer:` + +export const REPHRASE_TEMPLATE = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. + +Chat History: +{chat_history} +Follow Up Input: {question} +Standalone Question:` diff --git a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts index 0ad8adec..4a6252b5 100644 --- a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts +++ b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts @@ -1,4 +1,4 @@ -import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { BaseMessage } from 'langchain/schema' @@ -55,36 +55,27 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { super(fields) } - async getChatMessages(_?: string, returnBaseMessages = false): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + 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 addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } 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 ca8d0ddf..c21405a4 100644 --- a/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts +++ b/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts @@ -1,4 +1,4 @@ -import { FlowiseWindowMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { FlowiseWindowMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferWindowMemory, BufferWindowMemoryInput } from 'langchain/memory' import { BaseMessage } from 'langchain/schema' @@ -67,36 +67,28 @@ class BufferWindowMemoryExtended extends FlowiseWindowMemory implements MemoryMe super(fields) } - async getChatMessages(_?: string, returnBaseMessages = false): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + // Insert into chatHistory + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + 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 addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } 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: BufferWindowMemory_Memory } diff --git a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts index 107ab7db..45d39326 100644 --- a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts +++ b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts @@ -1,4 +1,4 @@ -import { FlowiseSummaryMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { FlowiseSummaryMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { ConversationSummaryMemory, ConversationSummaryMemoryInput } from 'langchain/memory' import { BaseLanguageModel } from 'langchain/base_language' @@ -66,40 +66,32 @@ class ConversationSummaryMemoryExtended extends FlowiseSummaryMemory implements super(fields) } - async getChatMessages(_?: string, returnBaseMessages = false): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + this.buffer = '' + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + // Get summary + const chatMessages = await this.chatHistory.getMessages() + this.buffer = chatMessages.length ? await this.predictNewSummary(chatMessages.slice(-2), this.buffer) : '' + 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 addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } 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) - } } module.exports = { nodeClass: ConversationSummaryMemory_Memory } diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 872ec0b5..91c1d369 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -12,13 +12,7 @@ import { 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/utils' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class DynamoDb_Memory implements INode { @@ -70,7 +64,8 @@ class DynamoDb_Memory implements INode { label: 'Session ID', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -88,25 +83,6 @@ class DynamoDb_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initalizeDynamoDB(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const dynamodbMemory = await initalizeDynamoDB(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing DynamoDb memory session ${sessionId ? sessionId : chatId}`) - await dynamodbMemory.clear() - options.logger.info(`Successfully cleared DynamoDb memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const dynamodbMemory = await initalizeDynamoDB(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await dynamodbMemory.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { @@ -114,17 +90,7 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P const partitionKey = nodeData.inputs?.partitionKey as string const region = nodeData.inputs?.region as string const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options.chatId - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessKeyId = getCredentialParam('accessKey', credentialData, nodeData) @@ -150,7 +116,6 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: dynamoDb, - isSessionIdUsingChatMessageId, sessionId, dynamodbClient: client }) @@ -158,7 +123,6 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean dynamodbClient: DynamoDBClient sessionId: string } @@ -178,7 +142,6 @@ interface DynamoDBSerializedChatMessage { } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - isSessionIdUsingChatMessageId = false sessionId = '' dynamodbClient: DynamoDBClient @@ -306,10 +269,6 @@ 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 b422921e..c593c20d 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -2,13 +2,7 @@ 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, - serializeChatHistory -} from '../../../src/utils' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class MongoDB_Memory implements INode { @@ -55,7 +49,8 @@ class MongoDB_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -73,42 +68,13 @@ class MongoDB_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initializeMongoDB(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const mongodbMemory = await initializeMongoDB(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing MongoDB memory session ${sessionId ? sessionId : chatId}`) - await mongodbMemory.clear() - options.logger.info(`Successfully cleared MongoDB memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const mongodbMemory = await initializeMongoDB(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await mongodbMemory.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { const databaseName = nodeData.inputs?.databaseName as string const collectionName = nodeData.inputs?.collectionName as string const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -149,14 +115,12 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, collection }) } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean collection: Collection sessionId: string } @@ -164,7 +128,6 @@ interface BufferMemoryExtendedInput { class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { sessionId = '' collection: Collection - isSessionIdUsingChatMessageId? = false constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) @@ -221,10 +184,6 @@ 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/MotorheadMemory/MotorheadMemory.ts b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts index 938cc873..19506fc1 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -1,9 +1,14 @@ 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' +import { MotorheadMemory, MotorheadMemoryInput, InputValues, OutputValues } from 'langchain/memory' import fetch from 'node-fetch' -import { BaseMessage } from 'langchain/schema' +import { AIMessage, BaseMessage, ChatMessage, HumanMessage } from 'langchain/schema' + +type MotorheadMessage = { + content: string + role: 'Human' | 'AI' +} class MotorMemory_Memory implements INode { label: string @@ -46,7 +51,8 @@ class MotorMemory_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -64,49 +70,19 @@ class MotorMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initalizeMotorhead(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const motorhead = await initalizeMotorhead(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Motorhead memory session ${sessionId ? sessionId : chatId}`) - await motorhead.clear() - options.logger.info(`Successfully cleared Motorhead memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const motorhead = await initalizeMotorhead(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await motorhead.loadMemoryVariables({}) - return getBufferString(memoryResult[key]) - } - } } const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): Promise => { const memoryKey = nodeData.inputs?.memoryKey as string const baseURL = nodeData.inputs?.baseURL as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const clientId = getCredentialParam('clientId', credentialData, nodeData) - let obj: MotorheadMemoryInput & MotorheadMemoryExtendedInput = { + let obj: MotorheadMemoryInput = { returnMessages: true, - isSessionIdUsingChatMessageId, sessionId, memoryKey } @@ -132,23 +108,9 @@ const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): return motorheadMemory } -interface MotorheadMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean -} - class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false - - constructor(fields: MotorheadMemoryInput & MotorheadMemoryExtendedInput) { + constructor(fields: MotorheadMemoryInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId - } - - async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { - if (overrideSessionId) { - this.sessionId = overrideSessionId - } - return super.loadMemoryVariables({ values }) } async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { @@ -180,9 +142,33 @@ class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { 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) + try { + const resp = await this.caller.call(fetch, `${this.url}/sessions/${id}/memory`, { + //@ts-ignore + signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined, + headers: this._getHeaders() as ICommonObject, + method: 'GET' + }) + const data = await resp.json() + const rawStoredMessages: MotorheadMessage[] = data?.data?.messages ?? [] + + const baseMessages = rawStoredMessages.reverse().map((message) => { + const { content, role } = message + if (role === 'Human') { + return new HumanMessage(content) + } else if (role === 'AI') { + return new AIMessage(content) + } else { + // default to generic ChatMessage + return new ChatMessage(content, role) + } + }) + + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } catch (error) { + console.error('Error getting session: ', error) + return [] + } } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index a02df3ea..baf4ea6b 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -1,15 +1,9 @@ -import { INode, INodeData, INodeParams, ICommonObject, IMessage, MessageType, FlowiseMemory, MemoryMethods } from '../../../src/Interface' -import { - convertBaseMessagetoIMessage, - getBaseClasses, - getCredentialData, - getCredentialParam, - serializeChatHistory -} from '../../../src/utils' +import { Redis } from 'ioredis' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis' import { mapStoredMessageToChatMessage, BaseMessage, AIMessage, HumanMessage } from 'langchain/schema' -import { Redis } from 'ioredis' +import { INode, INodeData, INodeParams, ICommonObject, MessageType, IMessage, MemoryMethods, FlowiseMemory } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' class RedisBackedChatMemory_Memory implements INode { label: string @@ -44,7 +38,8 @@ class RedisBackedChatMemory_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -78,47 +73,19 @@ class RedisBackedChatMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return await initalizeRedis(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeRedis(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Redis memory session ${sessionId ? sessionId : chatId}`) - await redis.clear() - options.logger.info(`Successfully cleared Redis memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const redis = await initalizeRedis(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await redis.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const sessionTTL = nodeData.inputs?.sessionTTL as number const memoryKey = nodeData.inputs?.memoryKey as string + const sessionId = nodeData.inputs?.sessionId as string const windowSize = nodeData.inputs?.windowSize as number - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - 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) let client: Redis + if (!redisUrl || redisUrl === '') { const username = getCredentialParam('redisCacheUser', credentialData, nodeData) const password = getCredentialParam('redisCachePwd', credentialData, nodeData) @@ -153,7 +120,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const redisChatMessageHistory = new RedisChatMessageHistory(obj) - redisChatMessageHistory.getMessages = async (): Promise => { + /*redisChatMessageHistory.getMessages = async (): Promise => { const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, windowSize ? -windowSize : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) return orderedMessages.map(mapStoredMessageToChatMessage) @@ -169,44 +136,45 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom redisChatMessageHistory.clear = async (): Promise => { await client.del((redisChatMessageHistory as any).sessionId) - } + }*/ const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, + windowSize, redisClient: client }) + return memory } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean redisClient: Redis sessionId: string + windowSize?: number } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false sessionId = '' redisClient: Redis + windowSize?: number constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.redisClient = fields.redisClient + this.windowSize = fields.windowSize } - async getChatMessages(overrideSessionId = '', returnBaseMessage = false): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.redisClient) return [] const id = overrideSessionId ?? this.sessionId - const rawStoredMessages = await this.redisClient.lrange(id, 0, -1) + const rawStoredMessages = await this.redisClient.lrange(id, this.windowSize ? this.windowSize * -1 : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) const baseMessages = orderedMessages.map(mapStoredMessageToChatMessage) - return returnBaseMessage ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { @@ -236,10 +204,6 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { 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 c3f97123..3d7f6dbf 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -3,13 +3,7 @@ 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 { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' class UpstashRedisBackedChatMemory_Memory implements INode { @@ -51,7 +45,8 @@ class UpstashRedisBackedChatMemory_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -70,40 +65,12 @@ class UpstashRedisBackedChatMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initalizeUpstashRedis(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeUpstashRedis(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Upstash Redis memory session ${sessionId ? sessionId : chatId}`) - await redis.clear() - options.logger.info(`Successfully cleared Upstash Redis memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeUpstashRedis(nodeData, options) - const key = 'chat_history' - const memoryResult = await redis.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const baseURL = nodeData.inputs?.baseURL as string const sessionTTL = nodeData.inputs?.sessionTTL as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const upstashRestToken = getCredentialParam('upstashRestToken', credentialData, nodeData) @@ -122,7 +89,6 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject const memory = new BufferMemoryExtended({ memoryKey: 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, redisClient: client }) @@ -131,19 +97,16 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean redisClient: Redis sessionId: string } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false sessionId = '' redisClient: Redis constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.redisClient = fields.redisClient } @@ -186,10 +149,6 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { await this.redisClient.del(id) await this.clear() } - - async resumeMessages(): Promise { - return - } } module.exports = { nodeClass: UpstashRedisBackedChatMemory_Memory } diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index 4dda76df..597eee8a 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -2,7 +2,7 @@ import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } f import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' -import { InputValues, MemoryVariables, OutputValues, getBufferString } from 'langchain/memory' +import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory' import { BaseMessage } from 'langchain/schema' class ZepMemory_Memory implements INode { @@ -55,10 +55,9 @@ class ZepMemory_Memory implements INode { label: 'Size', name: 'k', type: 'number', - placeholder: '10', + default: '10', description: 'Window of size k to surface the last k back-and-forth to use as memory.', - additionalParams: true, - optional: true + additionalParams: true }, { label: 'AI Prefix', @@ -101,27 +100,6 @@ class ZepMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return await initalizeZep(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const zep = await initalizeZep(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Zep memory session ${sessionId ? sessionId : chatId}`) - await zep.clear() - options.logger.info(`Successfully cleared Zep memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const aiPrefix = nodeData.inputs?.aiPrefix as string - const humanPrefix = nodeData.inputs?.humanPrefix as string - const zep = await initalizeZep(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await zep.loadMemoryVariables({}) - return getBufferString(memoryResult[key], humanPrefix, aiPrefix) - } - } } const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promise => { @@ -131,30 +109,19 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis const memoryKey = nodeData.inputs?.memoryKey as string const inputKey = nodeData.inputs?.inputKey as string const k = nodeData.inputs?.k as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const obj: ZepMemoryInput & ZepMemoryExtendedInput = { baseURL, - sessionId, aiPrefix, humanPrefix, returnMessages: true, memoryKey, inputKey, - isSessionIdUsingChatMessageId, + sessionId, k: k ? parseInt(k, 10) : undefined } if (apiKey) obj.apiKey = apiKey @@ -163,17 +130,14 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis } interface ZepMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean k?: number } class ZepMemoryExtended extends ZepMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false lastN?: number constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.lastN = fields.k } diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index 6ffcc0e2..a983d0d9 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, input: string, options: ICommonObject): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const selectedToolId = nodeData.inputs?.selectedTool as string const customToolFunc = nodeData.inputs?.customToolFunc as string @@ -99,11 +99,7 @@ class CustomTool_Tools implements INode { } } - const flow = { - chatId: options.chatId, // id is uppercase (I) - chatflowId: options.chatflowid, // id is lowercase (i) - input - } + const flow = { chatflowId: options.chatflowid } let dynamicStructuredTool = new DynamicStructuredTool(obj) dynamicStructuredTool.setVariables(variables) diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 338b0ae9..b543aefa 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -55,7 +55,12 @@ export class DynamicStructuredTool< this.schema = fields.schema } - async call(arg: z.output, configArg?: RunnableConfig | Callbacks, tags?: string[], overrideSessionId?: string): Promise { + async call( + arg: z.output, + configArg?: RunnableConfig | Callbacks, + tags?: string[], + flowConfig?: { sessionId?: string; chatId?: string; input?: string } + ): Promise { const config = parseCallbackConfigArg(configArg) if (config.runName === undefined) { config.runName = this.name @@ -86,7 +91,7 @@ export class DynamicStructuredTool< ) let result try { - result = await this._call(parsed, runManager, overrideSessionId) + result = await this._call(parsed, runManager, flowConfig) } catch (e) { await runManager?.handleToolError(e) throw e @@ -95,7 +100,11 @@ export class DynamicStructuredTool< return result } - protected async _call(arg: z.output, _?: CallbackManagerForToolRun, overrideSessionId?: string): Promise { + protected async _call( + arg: z.output, + _?: CallbackManagerForToolRun, + flowConfig?: { sessionId?: string; chatId?: string; input?: string } + ): Promise { let sandbox: any = {} if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { @@ -126,7 +135,7 @@ export class DynamicStructuredTool< // inject flow properties if (this.flowObj) { - sandbox['$flow'] = { ...this.flowObj, sessionId: overrideSessionId } + sandbox['$flow'] = { ...this.flowObj, ...flowConfig } } const defaultAllowBuiltInDep = [ diff --git a/packages/components/package.json b/packages/components/package.json index a2565430..a77d91e4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -46,6 +46,7 @@ "dotenv": "^16.0.0", "express": "^4.17.3", "faiss-node": "^0.2.2", + "fast-json-patch": "^3.1.1", "form-data": "^4.0.0", "google-auth-library": "^9.0.0", "graphql": "^16.6.0", diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 2a625ff6..676618e5 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -108,10 +108,6 @@ export interface INode extends INodeProperties { search: (nodeData: INodeData, options?: ICommonObject) => Promise delete: (nodeData: INodeData, options?: ICommonObject) => Promise } - memoryMethods?: { - clearSessionMemory: (nodeData: INodeData, options?: ICommonObject) => Promise - getChatMessages: (nodeData: INodeData, options?: ICommonObject) => Promise - } init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise } @@ -204,29 +200,37 @@ import { BaseMessage } from 'langchain/schema' import { BufferMemory, BufferWindowMemory, ConversationSummaryMemory } from 'langchain/memory' export interface MemoryMethods { - getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise + getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean, prevHistory?: IMessage[]): 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 getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): 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 getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): 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 getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): 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/components/src/agents.ts b/packages/components/src/agents.ts new file mode 100644 index 00000000..e30a0c43 --- /dev/null +++ b/packages/components/src/agents.ts @@ -0,0 +1,615 @@ +import { AgentExecutorInput, BaseSingleActionAgent, BaseMultiActionAgent, RunnableAgent, StoppingMethod } from 'langchain/agents' +import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' +import { OutputParserException } from 'langchain/schema/output_parser' +import { CallbackManager, CallbackManagerForChainRun, Callbacks } from 'langchain/callbacks' +import { ToolInputParsingException, Tool } from '@langchain/core/tools' +import { Runnable } from 'langchain/schema/runnable' +import { BaseChain, SerializedLLMChain } from 'langchain/chains' +import { Serializable } from '@langchain/core/load/serializable' + +type AgentExecutorOutput = ChainValues + +interface AgentExecutorIteratorInput { + agentExecutor: AgentExecutor + inputs: Record + callbacks?: Callbacks + tags?: string[] + metadata?: Record + runName?: string + runManager?: CallbackManagerForChainRun +} + +//TODO: stream tools back +export class AgentExecutorIterator extends Serializable implements AgentExecutorIteratorInput { + lc_namespace = ['langchain', 'agents', 'executor_iterator'] + + agentExecutor: AgentExecutor + + inputs: Record + + callbacks: Callbacks + + tags: string[] | undefined + + metadata: Record | undefined + + runName: string | undefined + + private _finalOutputs: Record | undefined + + get finalOutputs(): Record | undefined { + return this._finalOutputs + } + + /** Intended to be used as a setter method, needs to be async. */ + async setFinalOutputs(value: Record | undefined) { + this._finalOutputs = undefined + if (value) { + const preparedOutputs: Record = await this.agentExecutor.prepOutputs(this.inputs, value, true) + this._finalOutputs = preparedOutputs + } + } + + runManager: CallbackManagerForChainRun | undefined + + intermediateSteps: AgentStep[] = [] + + iterations = 0 + + get nameToToolMap(): Record { + const toolMap = this.agentExecutor.tools.map((tool) => ({ + [tool.name]: tool + })) + return Object.assign({}, ...toolMap) + } + + constructor(fields: AgentExecutorIteratorInput) { + super(fields) + this.agentExecutor = fields.agentExecutor + this.inputs = fields.inputs + this.tags = fields.tags + this.metadata = fields.metadata + this.runName = fields.runName + this.runManager = fields.runManager + } + + /** + * Reset the iterator to its initial state, clearing intermediate steps, + * iterations, and the final output. + */ + reset(): void { + this.intermediateSteps = [] + this.iterations = 0 + this._finalOutputs = undefined + } + + updateIterations(): void { + this.iterations += 1 + } + + async *streamIterator() { + this.reset() + + // Loop to handle iteration + while (true) { + try { + if (this.iterations === 0) { + await this.onFirstStep() + } + + const result = await this._callNext() + yield result + } catch (e: any) { + if ('message' in e && e.message.startsWith('Final outputs already reached: ')) { + if (!this.finalOutputs) { + throw e + } + return this.finalOutputs + } + if (this.runManager) { + await this.runManager.handleChainError(e) + } + throw e + } + } + } + + /** + * Perform any necessary setup for the first step + * of the asynchronous iterator. + */ + async onFirstStep(): Promise { + if (this.iterations === 0) { + const callbackManager = await CallbackManager.configure( + this.callbacks, + this.agentExecutor.callbacks, + this.tags, + this.agentExecutor.tags, + this.metadata, + this.agentExecutor.metadata, + { + verbose: this.agentExecutor.verbose + } + ) + this.runManager = await callbackManager?.handleChainStart( + this.agentExecutor.toJSON(), + this.inputs, + undefined, + undefined, + this.tags, + this.metadata, + this.runName + ) + } + } + + /** + * Execute the next step in the chain using the + * AgentExecutor's _takeNextStep method. + */ + async _executeNextStep(runManager?: CallbackManagerForChainRun): Promise { + return this.agentExecutor._takeNextStep(this.nameToToolMap, this.inputs, this.intermediateSteps, runManager) + } + + /** + * Process the output of the next step, + * handling AgentFinish and tool return cases. + */ + async _processNextStepOutput( + nextStepOutput: AgentFinish | AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise> { + if ('returnValues' in nextStepOutput) { + const output = await this.agentExecutor._return(nextStepOutput as AgentFinish, this.intermediateSteps, runManager) + if (this.runManager) { + await this.runManager.handleChainEnd(output) + } + await this.setFinalOutputs(output) + return output + } + + this.intermediateSteps = this.intermediateSteps.concat(nextStepOutput as AgentStep[]) + + let output: Record = {} + if (Array.isArray(nextStepOutput) && nextStepOutput.length === 1) { + const nextStep = nextStepOutput[0] + const toolReturn = await this.agentExecutor._getToolReturn(nextStep) + if (toolReturn) { + output = await this.agentExecutor._return(toolReturn, this.intermediateSteps, runManager) + if (this.runManager) { + await this.runManager.handleChainEnd(output) + } + await this.setFinalOutputs(output) + } + } + output = { intermediateSteps: nextStepOutput as AgentStep[] } + return output + } + + async _stop(): Promise> { + const output = await this.agentExecutor.agent.returnStoppedResponse( + this.agentExecutor.earlyStoppingMethod, + this.intermediateSteps, + this.inputs + ) + const returnedOutput = await this.agentExecutor._return(output, this.intermediateSteps, this.runManager) + await this.setFinalOutputs(returnedOutput) + return returnedOutput + } + + async _callNext(): Promise> { + // final output already reached: stopiteration (final output) + if (this.finalOutputs) { + throw new Error(`Final outputs already reached: ${JSON.stringify(this.finalOutputs, null, 2)}`) + } + // timeout/max iterations: stopiteration (stopped response) + if (!this.agentExecutor.shouldContinueGetter(this.iterations)) { + return this._stop() + } + const nextStepOutput = await this._executeNextStep(this.runManager) + const output = await this._processNextStepOutput(nextStepOutput, this.runManager) + this.updateIterations() + return output + } +} + +export class AgentExecutor extends BaseChain { + static lc_name() { + return 'AgentExecutor' + } + + get lc_namespace() { + return ['langchain', 'agents', 'executor'] + } + + agent: BaseSingleActionAgent | BaseMultiActionAgent + + tools: this['agent']['ToolType'][] + + returnIntermediateSteps = false + + maxIterations?: number = 15 + + earlyStoppingMethod: StoppingMethod = 'force' + + sessionId?: string + + chatId?: string + + input?: string + + /** + * How to handle errors raised by the agent's output parser. + Defaults to `False`, which raises the error. + + If `true`, the error will be sent back to the LLM as an observation. + If a string, the string itself will be sent to the LLM as an observation. + If a callable function, the function will be called with the exception + as an argument, and the result of that function will be passed to the agent + as an observation. + */ + handleParsingErrors: boolean | string | ((e: OutputParserException | ToolInputParsingException) => string) = false + + get inputKeys() { + return this.agent.inputKeys + } + + get outputKeys() { + return this.agent.returnValues + } + + constructor(input: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }) { + let agent: BaseSingleActionAgent | BaseMultiActionAgent + if (Runnable.isRunnable(input.agent)) { + agent = new RunnableAgent({ runnable: input.agent }) + } else { + agent = input.agent + } + + super(input) + this.agent = agent + this.tools = input.tools + this.handleParsingErrors = input.handleParsingErrors ?? this.handleParsingErrors + /* Getting rid of this because RunnableAgent doesnt allow return direct + if (this.agent._agentActionType() === "multi") { + for (const tool of this.tools) { + if (tool.returnDirect) { + throw new Error( + `Tool with return direct ${tool.name} not supported for multi-action agent.` + ); + } + } + }*/ + this.returnIntermediateSteps = input.returnIntermediateSteps ?? this.returnIntermediateSteps + this.maxIterations = input.maxIterations ?? this.maxIterations + this.earlyStoppingMethod = input.earlyStoppingMethod ?? this.earlyStoppingMethod + this.sessionId = input.sessionId + this.chatId = input.chatId + this.input = input.input + } + + static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }): AgentExecutor { + const newInstance = new AgentExecutor(fields) + if (fields.sessionId) newInstance.sessionId = fields.sessionId + if (fields.chatId) newInstance.chatId = fields.chatId + if (fields.input) newInstance.input = fields.input + return newInstance + } + + get shouldContinueGetter() { + return this.shouldContinue.bind(this) + } + + /** + * Method that checks if the agent execution should continue based on the + * number of iterations. + * @param iterations The current number of iterations. + * @returns A boolean indicating whether the agent execution should continue. + */ + private shouldContinue(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.shouldContinue(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, chatId, input as parameter + * Tool Call Parameters: + * - arg: z.output + * - configArg?: RunnableConfig | Callbacks + * - tags?: string[] + * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } + */ + observation = tool + ? // @ts-ignore + await tool.call(action.toolInput, runManager?.getChild(), undefined, { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + }) + : `${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, chatId, input as parameter + * Tool Call Parameters: + * - arg: z.output + * - configArg?: RunnableConfig | Callbacks + * - tags?: string[] + * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } + */ + // @ts-ignore + observation = await tool.call(agentAction.toolInput, runManager?.getChild(), undefined, { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + }) + } 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 + } + + async _return( + output: AgentFinish, + intermediateSteps: AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise { + if (runManager) { + await runManager.handleAgentEnd(output) + } + const finalOutput: Record = output.returnValues + if (this.returnIntermediateSteps) { + finalOutput.intermediateSteps = intermediateSteps + } + return finalOutput + } + + async _getToolReturn(nextStepOutput: AgentStep): Promise { + const { action, observation } = nextStepOutput + const nameToolMap = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + const [returnValueKey = 'output'] = this.agent.returnValues + // Invalid tools won't be in the map, so we return False. + if (action.tool in nameToolMap) { + if (nameToolMap[action.tool].returnDirect) { + return { + returnValues: { [returnValueKey]: observation }, + log: '' + } + } + } + return null + } + + _returnStoppedResponse(earlyStoppingMethod: StoppingMethod) { + if (earlyStoppingMethod === 'force') { + return { + returnValues: { + output: 'Agent stopped due to iteration limit or time limit.' + }, + log: '' + } as AgentFinish + } + throw new Error(`Got unsupported early_stopping_method: ${earlyStoppingMethod}`) + } + + async *_streamIterator(inputs: Record): AsyncGenerator { + const agentExecutorIterator = new AgentExecutorIterator({ + inputs, + agentExecutor: this, + metadata: this.metadata, + tags: this.tags, + callbacks: this.callbacks + }) + const iterator = agentExecutorIterator.streamIterator() + for await (const step of iterator) { + if (!step) { + continue + } + yield step + } + } + + _chainType() { + return 'agent_executor' as const + } + + serialize(): SerializedLLMChain { + throw new Error('Cannot serialize an AgentExecutor') + } +} + +class ExceptionTool extends Tool { + name = '_Exception' + + description = 'Exception tool' + + async _call(query: string) { + return query + } +} + +export 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)] + } + }) diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index 93270848..eabc8f2e 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -936,7 +936,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Chat with a Podcast.json b/packages/server/marketplaces/chatflows/Chat with a Podcast.json index 2a9e05b5..0a5d4ac6 100644 --- a/packages/server/marketplaces/chatflows/Chat with a Podcast.json +++ b/packages/server/marketplaces/chatflows/Chat with a Podcast.json @@ -13,7 +13,7 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], @@ -28,47 +28,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -89,9 +78,8 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{memoryVectorStore_0.data.instance}}", "memory": "", - "returnSourceDocuments": "", - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { @@ -625,9 +613,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 0ead3dd8..39d4d400 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -90,7 +90,7 @@ ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationChain_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 8994594a..b27d3886 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -354,7 +354,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index 5c55d833..e2fd6421 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -249,10 +249,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -264,47 +264,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -325,16 +314,15 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{pinecone_0.data.instance}}", "memory": "", - "returnSourceDocuments": "", - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -704,9 +692,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json index ac84cf56..16f70801 100644 --- a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json +++ b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json @@ -156,9 +156,9 @@ "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", "name": "conversationalRetrievalQAChain", - "version": 1, + "version": 2, "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -170,47 +170,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -232,15 +221,15 @@ "vectorStoreRetriever": "{{memoryVectorStore_0.data.instance}}", "memory": "", "returnSourceDocuments": true, - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -668,9 +657,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index e24ad7ca..6f78cb05 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -83,10 +83,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -98,47 +98,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -158,14 +147,16 @@ "inputs": { "model": "{{chatOllama_0.data.instance}}", "vectorStoreRetriever": "{{faiss_0.data.instance}}", - "memory": "" + "memory": "", + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -649,9 +640,9 @@ "source": "chatOllama_0", "sourceHandle": "chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOllama_0-chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOllama_0-chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index c39f746a..cf0fa4d4 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -13,10 +13,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -28,47 +28,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -89,14 +78,16 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{qdrant_0.data.instance}}", "memory": "{{ZepMemory_0.data.instance}}", - "returnSourceDocuments": true + "returnSourceDocuments": true, + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -232,7 +223,7 @@ "label": "Session Id", "name": "sessionId", "type": "string", - "description": "if empty, chatId will be used automatically", + "description": "If not specified, a random id will be used. Learn more", "default": "", "additionalParams": true, "optional": true, @@ -709,9 +700,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index 9865ae70..abd85d36 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -249,10 +249,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -264,47 +264,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -323,14 +312,16 @@ ], "inputs": { "model": "{{chatOpenAI_0.data.instance}}", - "vectorStoreRetriever": "{{pinecone_0.data.instance}}" + "vectorStoreRetriever": "{{pinecone_0.data.instance}}", + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -763,9 +754,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index a2a807cd..d53cb55e 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -1567,7 +1567,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index 2dac3823..2322136c 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -262,7 +262,7 @@ ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationChain_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index d9f9fb49..6f0edeea 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -190,7 +190,7 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], @@ -205,47 +205,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -267,8 +256,8 @@ "vectorStoreRetriever": "{{vectara_0.data.instance}}", "memory": "", "returnSourceDocuments": true, - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { @@ -427,9 +416,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 0547366a..d905b54b 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -578,7 +578,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index 9b1119b9..1b1d8de6 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -162,10 +162,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -177,47 +177,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -239,15 +228,15 @@ "vectorStoreRetriever": "{{pinecone_0.data.instance}}", "memory": "{{RedisBackedChatMemory_0.data.instance}}", "returnSourceDocuments": true, - "systemMessagePrompt": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given context. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Do not make up any information that is not in the context. Refuse to answer any question not about the info. Never break character.", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -589,7 +578,7 @@ "label": "Session Id", "name": "sessionId", "type": "string", - "description": "If not specified, the first CHAT_MESSAGE_ID will be used as sessionId", + "description": "If not specified, a random id will be used. Learn more", "default": "", "additionalParams": true, "optional": true, @@ -772,9 +761,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1ad4c795..8f5ab5db 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -20,7 +20,6 @@ import { ICredentialReturnResponse, chatType, IChatMessage, - IReactFlowEdge, IDepthQueue, INodeDirectedGraph } from './Interface' @@ -39,14 +38,14 @@ import { databaseEntities, transformToCredentialEntity, decryptCredentialData, - clearAllSessionMemory, replaceInputsWithConfig, getEncryptionKey, - checkMemorySessionId, - clearSessionMemoryFromViewMessageDialog, + getMemorySessionId, getUserHome, - replaceChatHistory, - getAllConnectedNodes + getSessionChatHistory, + getAllConnectedNodes, + clearSessionMemory, + findMemoryNode } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -532,17 +531,18 @@ export class App { const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes - if (isClearFromViewMessageDialog) { - await clearSessionMemoryFromViewMessageDialog( + try { + await clearSessionMemory( nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId, - memoryType + memoryType, + isClearFromViewMessageDialog ) - } else { - await clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId) + } catch (e) { + return res.status(500).send('Error clearing chat messages') } const deleteOptions: FindOptionsWhere = { chatflowid, chatId } @@ -1397,26 +1397,6 @@ export class App { return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) } - /** - * Method that find memory label that is connected within chatflow - * In a chatflow, there should only be 1 memory node - * @param {IReactFlowNode[]} nodes - * @param {IReactFlowEdge[]} edges - * @returns {string | undefined} - */ - findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined { - const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') - const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) - - for (const edge of edges) { - if (memoryNodeIds.includes(edge.source)) { - const memoryNode = nodes.find((node) => node.data.id === edge.source) - return memoryNode - } - } - return undefined - } - async upsertVector(req: Request, res: Response, isInternal: boolean = false) { try { const chatflowid = req.params.id @@ -1585,7 +1565,6 @@ export class App { * - Still in sync (i.e the flow has not been modified since) * - Existing overrideConfig and new overrideConfig are the same * - Flow doesn't start with/contain nodes that depend on incomingInput.question - * - Its not an Upsert request * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath ***/ const isFlowReusable = () => { @@ -1639,22 +1618,28 @@ export class App { isStreamValid = isFlowValidForStream(nodes, endingNodeData) } - let chatHistory: IMessage[] | string = incomingInput.history + let chatHistory: IMessage[] = incomingInput.history ?? [] - // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory + // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node for (const endingNode of endingNodes) { const endingNodeData = endingNode.data + if (!endingNodeData.inputs?.memory) continue - if ( - endingNodeData.inputs?.memory && - !incomingInput.history && - (incomingInput.chatId || incomingInput.overrideConfig?.sessionId) - ) { - const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') - const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) - if (memoryNode) { - chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) - } + + const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') + const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) + + if (!memoryNode) continue + + if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { + chatHistory = await getSessionChatHistory( + memoryNode, + this.nodesPool.componentNodes, + incomingInput, + this.AppDataSource, + databaseEntities, + logger + ) } } @@ -1713,16 +1698,11 @@ export class App { logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - let sessionId = undefined - if (nodeToExecuteData.instance) sessionId = checkMemorySessionId(nodeToExecuteData.instance, chatId) - - const memoryNode = this.findMemoryLabel(nodes, edges) + const memoryNode = findMemoryNode(nodes, edges) const memoryType = memoryNode?.data.label - let chatHistory: IMessage[] | string = incomingInput.history - if (memoryNode && !incomingInput.history && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { - chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) - } + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) @@ -1730,24 +1710,24 @@ export class App { let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, chatflowid, - chatHistory, - socketIO, - socketIOClientId: incomingInput.socketIOClientId, + chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, databaseEntities, analytic: chatflow.analytic, - chatId + socketIO, + socketIOClientId: incomingInput.socketIOClientId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, chatflowid, - chatHistory, + chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, databaseEntities, - analytic: chatflow.analytic, - chatId + analytic: chatflow.analytic }) result = typeof result === 'string' ? { text: result } : result diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index e7a35c82..7569d541 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -26,7 +26,8 @@ import { getEncryptionKeyPath, ICommonObject, IDatabaseEntity, - IMessage + IMessage, + FlowiseMemory } from 'flowise-components' import { randomBytes } from 'crypto' import { AES, enc } from 'crypto-js' @@ -270,7 +271,7 @@ export const buildLangchain = async ( depthQueue: IDepthQueue, componentNodes: IComponentNodes, question: string, - chatHistory: IMessage[] | string, + chatHistory: IMessage[], chatId: string, chatflowid: string, appDataSource: DataSource, @@ -317,9 +318,10 @@ export const buildLangchain = async ( await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, { chatId, chatflowid, + chatHistory, + logger, appDataSource, databaseEntities, - logger, cachePool, dynamicVariables }) @@ -330,9 +332,10 @@ export const buildLangchain = async ( let outputResult = await newNodeInstance.init(reactFlowNodeData, question, { chatId, chatflowid, + chatHistory, + logger, appDataSource, databaseEntities, - logger, cachePool, dynamicVariables }) @@ -424,66 +427,52 @@ export const buildLangchain = async ( } /** - * Clear all session memories on the canvas - * @param {IReactFlowNode[]} reactFlowNodes - * @param {IComponentNodes} componentNodes - * @param {string} chatId - * @param {DataSource} appDataSource - * @param {string} sessionId - */ -export const clearAllSessionMemory = async ( - reactFlowNodes: IReactFlowNode[], - componentNodes: IComponentNodes, - chatId: string, - appDataSource: DataSource, - sessionId?: string -) => { - for (const node of reactFlowNodes) { - if (node.data.category !== 'Memory' && node.data.type !== 'OpenAIAssistant') continue - const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const newNodeInstance = new nodeModule.nodeClass() - - if (sessionId && node.data.inputs) { - node.data.inputs.sessionId = sessionId - } - - if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.clearSessionMemory) { - await newNodeInstance.memoryMethods.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) - } - } -} - -/** - * Clear specific session memory from View Message Dialog UI + * Clear session memories * @param {IReactFlowNode[]} reactFlowNodes * @param {IComponentNodes} componentNodes * @param {string} chatId * @param {DataSource} appDataSource * @param {string} sessionId * @param {string} memoryType + * @param {string} isClearFromViewMessageDialog */ -export const clearSessionMemoryFromViewMessageDialog = async ( +export const clearSessionMemory = async ( reactFlowNodes: IReactFlowNode[], componentNodes: IComponentNodes, chatId: string, appDataSource: DataSource, sessionId?: string, - memoryType?: string + memoryType?: string, + isClearFromViewMessageDialog?: string ) => { - if (!sessionId) return for (const node of reactFlowNodes) { if (node.data.category !== 'Memory' && node.data.type !== 'OpenAIAssistant') continue - if (memoryType && node.data.label !== memoryType) continue + + // Only clear specific session memory from View Message Dialog UI + if (isClearFromViewMessageDialog && memoryType && node.data.label !== memoryType) continue + const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + const options: ICommonObject = { chatId, appDataSource, databaseEntities, logger } - if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId - - if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.clearSessionMemory) { - await newNodeInstance.memoryMethods.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) - return + // SessionId always take priority first because it is the sessionId used for 3rd party memory node + if (sessionId && node.data.inputs) { + if (node.data.type === 'OpenAIAssistant') { + await newNodeInstance.clearChatMessages(node.data, options, { type: 'threadId', id: sessionId }) + } else { + node.data.inputs.sessionId = sessionId + const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options) + await initializedInstance.clearChatMessages(sessionId) + } + } else if (chatId && node.data.inputs) { + if (node.data.type === 'OpenAIAssistant') { + await newNodeInstance.clearChatMessages(node.data, options, { type: 'chatId', id: chatId }) + } else { + node.data.inputs.sessionId = chatId + const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options) + await initializedInstance.clearChatMessages(chatId) + } } } } @@ -500,7 +489,7 @@ export const getVariableValue = ( paramValue: string, reactFlowNodes: IReactFlowNode[], question: string, - chatHistory: IMessage[] | string, + chatHistory: IMessage[], isAcceptVariable = false ) => { let returnVal = paramValue @@ -533,10 +522,7 @@ export const getVariableValue = ( } if (isAcceptVariable && variableFullPath === CHAT_HISTORY_VAR_PREFIX) { - variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters( - typeof chatHistory === 'string' ? chatHistory : convertChatHistoryToText(chatHistory), - false - ) + variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(convertChatHistoryToText(chatHistory), false) } // Split by first occurrence of '.' to get just nodeId @@ -579,7 +565,7 @@ export const resolveVariables = ( reactFlowNodeData: INodeData, reactFlowNodes: IReactFlowNode[], question: string, - chatHistory: IMessage[] | string + chatHistory: IMessage[] ): INodeData => { let flowNodeData = cloneDeep(reactFlowNodeData) const types = 'inputs' @@ -966,21 +952,43 @@ export const redactCredentialWithPasswordType = ( } /** - * Replace sessionId with new chatId - * Ex: after clear chat history, use the new chatId as sessionId + * Get sessionId + * Hierarchy of sessionId (top down) + * API/Embed: + * (1) Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' } + * (2) Provided in API body - incomingInput.chatId + * + * API/Embed + UI: + * (3) Hard-coded sessionId in UI + * (4) Not specified on UI nor API, default to chatId * @param {any} instance + * @param {IncomingInput} incomingInput * @param {string} chatId */ -export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => { - if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { - instance.memory.sessionId = chatId - instance.memory.chatHistory.sessionId = chatId +export const getMemorySessionId = ( + memoryNode: IReactFlowNode, + incomingInput: IncomingInput, + chatId: string, + isInternal: boolean +): string | undefined => { + if (!isInternal) { + // Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' } + if (incomingInput.overrideConfig?.sessionId) { + return incomingInput.overrideConfig?.sessionId + } + // Provided in API body - incomingInput.chatId + if (incomingInput.chatId) { + return incomingInput.chatId + } } - if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId - else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId) - return instance.memory.chatHistory.sessionId - return undefined + // Hard-coded sessionId in UI + if (memoryNode.data.inputs?.sessionId) { + return memoryNode.data.inputs.sessionId + } + + // Default chatId + return chatId } /** @@ -992,31 +1000,52 @@ export const checkMemorySessionId = (instance: any, chatId: string): string | un * @param {any} logger * @returns {string} */ -export const replaceChatHistory = async ( +export const getSessionChatHistory = async ( memoryNode: IReactFlowNode, + componentNodes: IComponentNodes, incomingInput: IncomingInput, appDataSource: DataSource, databaseEntities: IDatabaseEntity, logger: any -): Promise => { - const nodeInstanceFilePath = memoryNode.data.filePath as string +): Promise => { + const nodeInstanceFilePath = componentNodes[memoryNode.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + // Replace memory's sessionId/chatId if (incomingInput.overrideConfig?.sessionId && memoryNode.data.inputs) { memoryNode.data.inputs.sessionId = incomingInput.overrideConfig.sessionId + } else if (incomingInput.chatId && memoryNode.data.inputs) { + memoryNode.data.inputs.sessionId = incomingInput.chatId } - if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.getChatMessages) { - return await newNodeInstance.memoryMethods.getChatMessages(memoryNode.data, { - chatId: incomingInput.chatId, - appDataSource, - databaseEntities, - logger - }) - } + const initializedInstance: FlowiseMemory = await newNodeInstance.init(memoryNode.data, '', { + appDataSource, + databaseEntities, + logger + }) - return '' + return (await initializedInstance.getChatMessages()) as IMessage[] +} + +/** + * Method that find memory that is connected within chatflow + * In a chatflow, there should only be 1 memory node + * @param {IReactFlowNode[]} nodes + * @param {IReactFlowEdge[]} edges + * @returns {string | undefined} + */ +export const findMemoryNode = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined => { + const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') + const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) + + for (const edge of edges) { + if (memoryNodeIds.includes(edge.source)) { + const memoryNode = nodes.find((node) => node.data.id === edge.source) + return memoryNode + } + } + return undefined } /** diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 617d1066..a673d6b7 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -280,6 +280,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA style={{ display: 'flex', flexDirection: 'row', + alignItems: 'center', borderRadius: 10, background: 'rgb(254,252,191)', padding: 10, @@ -287,7 +288,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA marginBottom: 10 }} > - + {inputParam.warning} )} From 244093923d77f0b2a09d3ffbecb30d74777b5102 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 07:25:54 -0800 Subject: [PATCH 191/502] updates per PR comments --- .../components/nodes/chains/VectaraChain/VectaraChain.ts | 3 ++- packages/components/nodes/vectorstores/Vectara/Vectara.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 986d587a..7d65c9cd 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -248,7 +248,8 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) - const mmrRerankerId = 272725718 // Vectara reranker ID for MMR + // Vectara reranker ID for MMR (https://docs.vectara.com/docs/api-reference/search-apis/reranking#maximal-marginal-relevance-mmr-reranker) + const mmrRerankerId = 272725718 const mmrEnabled = vectaraFilter?.mmrConfig?.enabled const data = { diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index be63d582..df709e0b 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -110,7 +110,10 @@ class Vectara_VectorStores implements INode { { label: 'MMR diversity bias', name: 'mmrDiversityBias', - description: 'The diversity bias to use for MMR. Defaults to 0 (MMR disabled)', + description: + 'The diversity bias to use for MMR. This is a value between 0.0 and 1.0' + + 'Values closer to 1.0 optimize for the most diverse results.' + + 'Defaults to 0 (MMR disabled)', placeholder: '0.0', type: 'number', additionalParams: true, From bb77e3f591b6bb03b00ca7fc3901984ed9d8241f Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Jan 2024 17:13:07 +0000 Subject: [PATCH 192/502] fix chatbot config --- packages/server/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1ad4c795..b6f59191 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -362,7 +362,8 @@ export class App { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) - if (chatflow && chatflow.chatbotConfig) { + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + if (chatflow.chatbotConfig) { try { const parsedConfig = JSON.parse(chatflow.chatbotConfig) return res.json(parsedConfig) @@ -370,7 +371,7 @@ export class App { return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) } } - return res.status(404).send(`Chatbot Config for Chatflow ${req.params.id} not found`) + return res.status(200).send('OK') }) // Save chatflow From 78a6926ca3f049007bd9777316f6083aee733220 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 10:26:17 -0800 Subject: [PATCH 193/502] added step to diversityBias --- packages/components/nodes/vectorstores/Vectara/Vectara.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index df709e0b..45825b4f 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -110,6 +110,7 @@ class Vectara_VectorStores implements INode { { label: 'MMR diversity bias', name: 'mmrDiversityBias', + step: 0.1, description: 'The diversity bias to use for MMR. This is a value between 0.0 and 1.0' + 'Values closer to 1.0 optimize for the most diverse results.' + From 4622fd8a02a39517f264af63acc411f0b282d89c Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Jan 2024 18:39:36 +0000 Subject: [PATCH 194/502] update self-host readme --- README.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 82e92832..3e6b7e56 100644 --- a/README.md +++ b/README.md @@ -145,37 +145,40 @@ Flowise support different environment variables to configure your instance. You ## 🌐 Self Host -### [Railway](https://docs.flowiseai.com/deployment/railway) +Deploy Flowise self-hosted in your existing infrastructure, we support various [deployments](https://docs.flowiseai.com/configuration/deployment) -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) +- [AWS](https://docs.flowiseai.com/deployment/aws) +- [Azure](https://docs.flowiseai.com/deployment/azure) +- [Digital Ocean](https://docs.flowiseai.com/deployment/digital-ocean) +- [GCP](https://docs.flowiseai.com/deployment/gcp) +-
    + Others -### [Render](https://docs.flowiseai.com/deployment/render) + - [Railway](https://docs.flowiseai.com/deployment/railway) -[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) + [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) -### [Elestio](https://elest.io/open-source/flowiseai) + - [Render](https://docs.flowiseai.com/deployment/render) -[![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) + [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) -### [RepoCloud](https://repocloud.io/details/?app_id=29) + - [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) -[![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) + HuggingFace Spaces -### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) + - [Elestio](https://elest.io/open-source/flowiseai) -HuggingFace Spaces + [![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) -### Sealos + - [Sealos](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) -[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) -### [AWS](https://docs.flowiseai.com/deployment/aws) + - [RepoCloud](https://repocloud.io/details/?app_id=29) -### [Azure](https://docs.flowiseai.com/deployment/azure) + [![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) -### [DigitalOcean](https://docs.flowiseai.com/deployment/digital-ocean) - -### [GCP](https://docs.flowiseai.com/deployment/gcp) +
    ## 💻 Cloud Hosted From 07411a78a3378e1eac112c337c131fb11a44b9d6 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Jan 2024 23:34:53 +0000 Subject: [PATCH 195/502] removed zapier nla credential --- .../credentials/ZapierNLAApi.credential.ts | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 packages/components/credentials/ZapierNLAApi.credential.ts diff --git a/packages/components/credentials/ZapierNLAApi.credential.ts b/packages/components/credentials/ZapierNLAApi.credential.ts deleted file mode 100644 index 72035660..00000000 --- a/packages/components/credentials/ZapierNLAApi.credential.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { INodeParams, INodeCredential } from '../src/Interface' - -class ZapierNLAApi implements INodeCredential { - label: string - name: string - version: number - description: string - inputs: INodeParams[] - - constructor() { - this.label = 'Zapier NLA API' - this.name = 'zapierNLAApi' - this.version = 1.0 - this.inputs = [ - { - label: 'Zapier NLA Api Key', - name: 'zapierNLAApiKey', - type: 'password' - } - ] - } -} - -module.exports = { credClass: ZapierNLAApi } From a26167ac5afff1df527f19d60e3b94c4ca539ac7 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 16:09:50 -0800 Subject: [PATCH 196/502] updated component to V2.0 Updated marketplace "Chain Upload" JSON file --- .../nodes/vectorstores/Vectara/Vectara.ts | 2 +- .../chatflows/Vectara LLM Chain Upload.json | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 45825b4f..939a4ac3 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -22,7 +22,7 @@ class Vectara_VectorStores implements INode { constructor() { this.label = 'Vectara' this.name = 'vectara' - this.version = 1.0 + this.version = 2.0 this.type = 'Vectara' this.icon = 'vectara.png' this.category = 'Vector Stores' diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index d9f9fb49..3f6fcda5 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -361,13 +361,36 @@ { "label": "Top K", "name": "topK", - "description": "Number of top results to fetch. Defaults to 4", - "placeholder": "4", + "description": "Number of top results to fetch. Defaults to 5", + "placeholder": "5", "type": "number", "additionalParams": true, "optional": true, "id": "vectara_0-input-topK-number" + }, + { + "label": "MMR K", + "name": "mmrK", + "description": "The number of results to rerank if MMR is enabled.", + "placeholder": "50", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "vectara_0-input-mmrK-number" + }, + { + "label": "MMR Diversity Bias", + "name": "mmrDiversityBias", + "step": 0.1, + "description": "Diversity Bias parameter for MMR, if enabled. 0.0 means no diversiry bias, 1.0 means maximum diversity bias. Defaults to 0.0 (MMR disabled).", + "placeholder": "0.0", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "vectara_0-input-mmrDiversityBias-number" } + + ], "inputAnchors": [ { @@ -385,7 +408,9 @@ "sentencesBefore": "", "sentencesAfter": "", "lambda": "", - "topK": "" + "topK": "", + "mmrK": "", + "mmrDiversityBias": "" }, "outputAnchors": [ { From b5bcfc0d5c32f0ed91b950479555fb93a0c5d087 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 16:46:36 -0800 Subject: [PATCH 197/502] after yarn lint-fix --- .../marketplaces/chatflows/Vectara LLM Chain Upload.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index 3f6fcda5..33b93578 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -377,7 +377,7 @@ "additionalParams": true, "optional": true, "id": "vectara_0-input-mmrK-number" - }, + }, { "label": "MMR Diversity Bias", "name": "mmrDiversityBias", @@ -389,8 +389,6 @@ "optional": true, "id": "vectara_0-input-mmrDiversityBias-number" } - - ], "inputAnchors": [ { From 6ec1c9249b58e1a05c9bcb70c67aa8417bd6155a Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Mon, 8 Jan 2024 17:53:18 -0700 Subject: [PATCH 198/502] Revert model var to string, refactor for case without a key and just override if so --- CONTRIBUTING.md | 1 - .../nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts | 17 ++++++----------- packages/server/.env.example | 2 -- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c91906c..04cb80b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,6 @@ Flowise support different environment variables to configure your instance. You | DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | -| LOCALAI_CHAT_MODELS | JSON-encoded string representing an array of chat models for LocalAI. Each object in the array should have a 'label' and 'name' property. | String | '[]' (Empty Array) | You can also specify the env variables when using `npx`. For example: diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index 258db1f8..c44f03ce 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -33,9 +33,6 @@ class ChatLocalAI_ChatModels implements INode { credentialNames: ['LocalAIApi'], optional: true } - - const modelOptions = JSON.parse(process.env.LOCALAI_CHAT_MODELS || '[]'); - this.inputs = [ { label: 'Cache', @@ -52,10 +49,8 @@ class ChatLocalAI_ChatModels implements INode { { label: 'Model Name', name: 'modelName', - type: 'options', - options: modelOptions, - default: modelOptions.length > 0 ? modelOptions[0].name : '', - optional: true + type: 'string', + placeholder: 'gpt4all-lora-quantized.bin' }, { label: 'Temperature', @@ -99,22 +94,22 @@ class ChatLocalAI_ChatModels implements INode { const topP = nodeData.inputs?.topP as string const timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const openAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) + const localAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) const cache = nodeData.inputs?.cache as BaseCache - const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { + const obj: Partial & BaseLLMParams & { localAIApiKey?: string } = { temperature: parseFloat(temperature), modelName, - openAIApiKey + openAIApiKey: 'sk-' } if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) if (topP) obj.topP = parseFloat(topP) if (timeout) obj.timeout = parseInt(timeout, 10) if (cache) obj.cache = cache + if (localAIApiKey) obj.openAIApiKey = localAIApiKey const model = new OpenAIChat(obj, { basePath }) diff --git a/packages/server/.env.example b/packages/server/.env.example index 9b7be0ff..6e746a4d 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -26,5 +26,3 @@ PORT=3000 # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key # LANGCHAIN_PROJECT=your_project - -# LOCALAI_CHAT_MODELS='[{"label": "model1", "name": "model1"}, {"label": "model2", "name": "model2"}]' From 6a114b3717206e742187593a0ddb0ba5f964ae58 Mon Sep 17 00:00:00 2001 From: Carson Yang Date: Tue, 9 Jan 2024 13:58:40 +0800 Subject: [PATCH 199/502] Update README-ZH.md --- README-ZH.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README-ZH.md b/README-ZH.md index 2805ef9b..208eee92 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -153,6 +153,10 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package [![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) +### [Sealos](https://docs.flowiseai.com/configuration/deployment/sealos) + +[![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://template.cloud.sealos.io/deploy?templateName=flowise) + ### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) HuggingFace Spaces From 1dc96906966394cd999a8b69676076274c9a38d3 Mon Sep 17 00:00:00 2001 From: YISH Date: Wed, 10 Jan 2024 17:32:46 +0800 Subject: [PATCH 200/502] Add milvusTextField configuration for Milvus langchain python use `text` https://github.com/langchain-ai/langchain/blob/master/libs/community/langchain_community/vectorstores/milvus.py#L119 while langchian js use `langchain` https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-community/src/vectorstores/milvus.ts#L61 so it is necessary to add milvusTextField configuration for Milvus. --- .../components/nodes/vectorstores/Milvus/Milvus.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index 090f35f7..7566f8a8 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -65,6 +65,14 @@ class Milvus_VectorStores implements INode { name: 'milvusCollection', type: 'string' }, + { + label: 'Milvus Text Field', + name: 'milvusTextField', + type: 'string', + placeholder: 'langchain_text', + optional: true, + additionalParams: true + }, { label: 'Milvus Filter', name: 'milvusFilter', @@ -150,6 +158,7 @@ class Milvus_VectorStores implements INode { const address = nodeData.inputs?.milvusServerUrl as string const collectionName = nodeData.inputs?.milvusCollection as string const milvusFilter = nodeData.inputs?.milvusFilter as string + const textField = nodeData.inputs?.milvusTextField as string // embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings @@ -169,7 +178,8 @@ class Milvus_VectorStores implements INode { // init MilvusLibArgs const milVusArgs: MilvusLibArgs = { url: address, - collectionName: collectionName + collectionName: collectionName, + textField: textField } if (milvusUser) milVusArgs.username = milvusUser From be0fff4d9d9cc2f910fad491b7af07549f52e82a Mon Sep 17 00:00:00 2001 From: YISH Date: Wed, 10 Jan 2024 17:41:53 +0800 Subject: [PATCH 201/502] Fix OpenAIFunctionAgent that function not return string result refer to https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/agents/format_scratchpad/openai_functions.py#L29 and fix the role of systemMessage from `ai` to `system`. --- .../OpenAIFunctionAgent/OpenAIFunctionAgent.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index c0095cee..ac000c1e 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -109,9 +109,18 @@ class OpenAIFunctionAgent_Agents implements INode { const formatAgentSteps = (steps: AgentStep[]): BaseMessage[] => steps.flatMap(({ action, observation }) => { + const create_function_message = (observation: string, action: AgentAction) => { + let content: string + if (typeof observation !== 'string') { + content = JSON.stringify(observation) + } else { + content = observation + } + return new FunctionMessage(content, action.tool) + } if ('messageLog' in action && action.messageLog !== undefined) { const log = action.messageLog as BaseMessage[] - return log.concat(new FunctionMessage(observation, action.tool)) + return log.concat(create_function_message(observation, action)) } else { return [new AIMessage(action.log)] } @@ -127,7 +136,7 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { const inputKey = memory.inputKey ? memory.inputKey : 'input' const prompt = ChatPromptTemplate.fromMessages([ - ['ai', systemMessage ? systemMessage : `You are a helpful AI assistant.`], + ['system', systemMessage ? systemMessage : `You are a helpful AI assistant.`], new MessagesPlaceholder(memoryKey), ['human', `{${inputKey}}`], new MessagesPlaceholder('agent_scratchpad') From 3bc52426bace3a79613e4b4587ad7961ac551cd4 Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Jan 2024 12:30:01 -0800 Subject: [PATCH 202/502] Correct DockerHub link in docs from private to public repo page --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index d3ad1c19..11b29cf3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,6 @@ # Flowise Docker Hub Image -Starts Flowise from [DockerHub Image](https://hub.docker.com/repository/docker/flowiseai/flowise/general) +Starts Flowise from [DockerHub Image](https://hub.docker.com/r/flowiseai/flowise) ## Usage From d70c097341fcee2f8d9210c70d74402b84f4e829 Mon Sep 17 00:00:00 2001 From: Ilango Date: Thu, 11 Jan 2024 14:37:10 +0530 Subject: [PATCH 203/502] Add a sticky note node under tools --- .../nodes/tools/StickyNotes/StickyNote.ts | 42 ++++++ .../nodes/tools/StickyNotes/stickyNote.svg | 5 + packages/ui/src/views/canvas/CanvasNode.js | 120 +++++++++--------- 3 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 packages/components/nodes/tools/StickyNotes/StickyNote.ts create mode 100644 packages/components/nodes/tools/StickyNotes/stickyNote.svg diff --git a/packages/components/nodes/tools/StickyNotes/StickyNote.ts b/packages/components/nodes/tools/StickyNotes/StickyNote.ts new file mode 100644 index 00000000..0ae980a0 --- /dev/null +++ b/packages/components/nodes/tools/StickyNotes/StickyNote.ts @@ -0,0 +1,42 @@ +import { INode, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class StickyNote implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + disableOutput: boolean + + constructor() { + this.label = 'Sticky Note' + this.name = 'stickyNote' + this.version = 1.0 + this.type = 'StickyNote' + this.icon = 'stickyNote.svg' + this.category = 'Tools' + this.description = 'Add a note about a node' + this.inputs = [ + { + label: 'Note', + name: 'note', + type: 'string', + rows: 4, + placeholder: 'Input your notes', + optional: true + } + ] + this.disableOutput = true + this.baseClasses = [this.type] + } + + async init(): Promise { + return new StickyNote() + } +} + +module.exports = { nodeClass: StickyNote } diff --git a/packages/components/nodes/tools/StickyNotes/stickyNote.svg b/packages/components/nodes/tools/StickyNotes/stickyNote.svg new file mode 100644 index 00000000..81c45058 --- /dev/null +++ b/packages/components/nodes/tools/StickyNotes/stickyNote.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index e52de640..a039f6d1 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -54,6 +54,7 @@ const CanvasNode = ({ data }) => { const [infoDialogProps, setInfoDialogProps] = useState({}) const [warningMessage, setWarningMessage] = useState('') const [open, setOpen] = useState(false) + const isNote = data.type === 'StickyNote' const handleClose = () => { setOpen(false) @@ -150,47 +151,49 @@ const CanvasNode = ({ data }) => { placement='right-start' > -
    - -
    - Notification -
    -
    - - - {data.label} - - - {warningMessage && ( - <> -
    - {warningMessage}} placement='top'> - - - - - - )} -
    - {(data.inputAnchors.length > 0 || data.inputParams.length > 0) && ( + {!isNote && ( +
    + +
    + Notification +
    +
    + + + {data.label} + + + {warningMessage && ( + <> +
    + {warningMessage}} placement='top'> + + + + + + )} +
    + )} + {(data.inputAnchors.length > 0 || data.inputParams.length > 0) && !isNote && ( <> @@ -230,22 +233,25 @@ const CanvasNode = ({ data }) => { )} - - - - Output - - - - - {data.outputAnchors.map((outputAnchor, index) => ( - - ))} + {!isNote && ( + <> + + + + Output + + + + {data.outputAnchors.map((outputAnchor, index) => ( + + ))} + + )} From 22ebf95a598323e70b9ec6ddf6b7e2f25c7016df Mon Sep 17 00:00:00 2001 From: Ilango Date: Thu, 11 Jan 2024 21:15:29 +0530 Subject: [PATCH 204/502] Rename StickyNote node and move to utilities category --- .../{tools/StickyNotes => utilities/StickyNote}/StickyNote.ts | 4 ++-- .../StickyNotes => utilities/StickyNote}/stickyNote.svg | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/components/nodes/{tools/StickyNotes => utilities/StickyNote}/StickyNote.ts (89%) rename packages/components/nodes/{tools/StickyNotes => utilities/StickyNote}/stickyNote.svg (100%) diff --git a/packages/components/nodes/tools/StickyNotes/StickyNote.ts b/packages/components/nodes/utilities/StickyNote/StickyNote.ts similarity index 89% rename from packages/components/nodes/tools/StickyNotes/StickyNote.ts rename to packages/components/nodes/utilities/StickyNote/StickyNote.ts index 0ae980a0..270e2524 100644 --- a/packages/components/nodes/tools/StickyNotes/StickyNote.ts +++ b/packages/components/nodes/utilities/StickyNote/StickyNote.ts @@ -1,4 +1,4 @@ -import { INode, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { INode, INodeParams } from '../../../src/Interface' class StickyNote implements INode { label: string @@ -18,7 +18,7 @@ class StickyNote implements INode { this.version = 1.0 this.type = 'StickyNote' this.icon = 'stickyNote.svg' - this.category = 'Tools' + this.category = 'Utilities' this.description = 'Add a note about a node' this.inputs = [ { diff --git a/packages/components/nodes/tools/StickyNotes/stickyNote.svg b/packages/components/nodes/utilities/StickyNote/stickyNote.svg similarity index 100% rename from packages/components/nodes/tools/StickyNotes/stickyNote.svg rename to packages/components/nodes/utilities/StickyNote/stickyNote.svg From 6462ba0faeca7411de33ac3c384f2fe80f0a498e Mon Sep 17 00:00:00 2001 From: Ilango Date: Thu, 11 Jan 2024 21:20:47 +0530 Subject: [PATCH 205/502] Add todo for category type in INode --- packages/components/src/Interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 2a625ff6..9da26f82 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -90,7 +90,7 @@ export interface INodeProperties { type: string icon: string version: number - category: string + category: string // TODO: use enum instead of string baseClasses: string[] description?: string filePath?: string From e634a6b584b9d01381b5d31eeab68445a78763dd Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 00:31:21 +0500 Subject: [PATCH 206/502] feature: Integrate Astra Vectorstore --- .../credentials/AstraApi.credential.ts | 34 ++++ .../nodes/vectorstores/Astra/Astra.ts | 190 ++++++++++++++++++ .../nodes/vectorstores/Astra/astra.svg | 1 + packages/components/package.json | 2 + 4 files changed, 227 insertions(+) create mode 100644 packages/components/credentials/AstraApi.credential.ts create mode 100644 packages/components/nodes/vectorstores/Astra/Astra.ts create mode 100644 packages/components/nodes/vectorstores/Astra/astra.svg diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts new file mode 100644 index 00000000..ad4c65a8 --- /dev/null +++ b/packages/components/credentials/AstraApi.credential.ts @@ -0,0 +1,34 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AstraApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Astra API' + this.name = 'AstraApi' + this.version = 1.0 + this.inputs = [ + { + label: 'Colection Name', + name: 'collectionName', + type: 'string' + }, + { + label: 'Astra DB Application Token', + name: 'applicationToken', + type: 'password' + }, + { + label: 'Astra DB Api Endpoint', + name: 'dbEndPoint', + type: 'string' + } + ] + } +} + +module.exports = { credClass: AstraApi } diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts new file mode 100644 index 00000000..648a8b49 --- /dev/null +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -0,0 +1,190 @@ +import { flatten } from 'lodash' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData } from '../../../src/utils' +import { AstraDBVectorStore, AstraLibArgs } from '@langchain/community/vectorstores/astradb' + +class Astra_VectorStores implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Astra' + this.name = 'Astra' + this.version = 1.0 + this.type = 'Astra' + this.icon = 'astra.svg' + this.category = 'Vector Stores' + this.description = `Upsert embedded data and perform similarity search upon query using DataStax Astra DB, a serverless vector database that’s perfect for managing mission-critical AI workloads` + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.badge = 'NEW' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['AstraApi'] + } + this.inputs = [ + { + label: 'Document', + name: 'document', + type: 'Document', + list: true, + optional: true + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Vector Dimension', + name: 'vectorDimension', + type: 'number', + placeholder: '1536', + optional: true, + description: 'Dimension used for storing vector embedding' + }, + { + label: 'Similarity Metric', + name: 'similarityMetric', + type: 'string', + placeholder: 'cosine', + optional: true, + description: 'cosine | euclidean | dot_product' + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'Astra Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Astra Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(AstraDBVectorStore)] + } + ] + } + + //@ts-ignore + vectorStoreMethods = { + async upsert(nodeData: INodeData, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + const embeddings = nodeData.inputs?.embeddings as Embeddings + const vectorDimension = nodeData.inputs?.vectorDimension as number + const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + + const expectedSimilarityMetric = ['cosine', 'euclidean', 'dot_product'] + if (similarityMetric && !expectedSimilarityMetric.includes(similarityMetric)) { + throw new Error(`Invalid Similarity Metric should be one of 'cosine' | 'euclidean' | 'dot_product'`) + } + + const clientConfig = { + token: credentialData?.applicationToken ?? 'dummy', + endpoint: credentialData?.dbEndPoint ?? 'dummy' + } + + const astraConfig: AstraLibArgs = { + ...clientConfig, + collection: credentialData.collectionName ?? 'flowise_test', + collectionOptions: { + vector: { + dimension: vectorDimension ?? 1536, + metric: similarityMetric ?? 'cosine' + } + } + } + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + try { + await AstraDBVectorStore.fromDocuments(finalDocs, embeddings, astraConfig) + } catch (e) { + throw new Error(e) + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + const embeddings = nodeData.inputs?.embeddings as Embeddings + const vectorDimension = nodeData.inputs?.vectorDimension as number + const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined + const output = nodeData.outputs?.output as string + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + + const expectedSimilarityMetric = ['cosine', 'euclidean', 'dot_product'] + if (similarityMetric && !expectedSimilarityMetric.includes(similarityMetric)) { + throw new Error(`Invalid Similarity Metric should be one of 'cosine' | 'euclidean' | 'dot_product'`) + } + + const clientConfig = { + token: credentialData?.applicationToken ?? 'dummy', + endpoint: credentialData?.dbEndPoint ?? 'dummy' + } + + const astraConfig: AstraLibArgs = { + ...clientConfig, + collection: credentialData.collectionName ?? 'flowise_test', + collectionOptions: { + vector: { + dimension: vectorDimension ?? 1536, + metric: similarityMetric ?? 'cosine' + } + } + } + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + const vectorStore = await AstraDBVectorStore.fromExistingIndex(embeddings, astraConfig) + + if (output === 'retriever') { + const retriever = vectorStore.asRetriever(k) + return retriever + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } + return vectorStore + } +} + +module.exports = { nodeClass: Astra_VectorStores } diff --git a/packages/components/nodes/vectorstores/Astra/astra.svg b/packages/components/nodes/vectorstores/Astra/astra.svg new file mode 100644 index 00000000..59c2fc3f --- /dev/null +++ b/packages/components/nodes/vectorstores/Astra/astra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/package.json b/packages/components/package.json index a2565430..07b2c3df 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -19,6 +19,7 @@ "@aws-sdk/client-bedrock-runtime": "3.422.0", "@aws-sdk/client-dynamodb": "^3.360.0", "@aws-sdk/client-s3": "^3.427.0", + "@datastax/astra-db-ts": "^0.1.2", "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", "@getzep/zep-js": "^0.9.0", @@ -26,6 +27,7 @@ "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", + "@langchain/community": "^0.0.16", "@langchain/google-genai": "^0.0.6", "@langchain/mistralai": "^0.0.6", "@notionhq/client": "^2.2.8", From 8a470a85b77499c2ad4ff5773b508b7d9e99aefe Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 01:36:57 +0500 Subject: [PATCH 207/502] chore: refactoring (naming convention) --- packages/components/credentials/AstraApi.credential.ts | 10 +++++----- packages/components/nodes/vectorstores/Astra/Astra.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts index ad4c65a8..3bec1a49 100644 --- a/packages/components/credentials/AstraApi.credential.ts +++ b/packages/components/credentials/AstraApi.credential.ts @@ -1,6 +1,6 @@ import { INodeParams, INodeCredential } from '../src/Interface' -class AstraApi implements INodeCredential { +class AstraDBApi implements INodeCredential { label: string name: string version: number @@ -8,12 +8,12 @@ class AstraApi implements INodeCredential { inputs: INodeParams[] constructor() { - this.label = 'Astra API' - this.name = 'AstraApi' + this.label = 'Astra DB API' + this.name = 'AstraDBApi' this.version = 1.0 this.inputs = [ { - label: 'Colection Name', + label: 'Collection Name', name: 'collectionName', type: 'string' }, @@ -31,4 +31,4 @@ class AstraApi implements INodeCredential { } } -module.exports = { credClass: AstraApi } +module.exports = { credClass: AstraDBApi } diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts index 648a8b49..e3377cb5 100644 --- a/packages/components/nodes/vectorstores/Astra/Astra.ts +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -33,7 +33,7 @@ class Astra_VectorStores implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['AstraApi'] + credentialNames: ['AstraDBApi'] } this.inputs = [ { From e2365ff22000b1942bf1523ac58136afc46873af Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 01:43:22 +0500 Subject: [PATCH 208/502] Update AstraApi.credential.ts --- packages/components/credentials/AstraApi.credential.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts index 3bec1a49..a89a259f 100644 --- a/packages/components/credentials/AstraApi.credential.ts +++ b/packages/components/credentials/AstraApi.credential.ts @@ -13,7 +13,7 @@ class AstraDBApi implements INodeCredential { this.version = 1.0 this.inputs = [ { - label: 'Collection Name', + label: 'Astra DB Collection Name', name: 'collectionName', type: 'string' }, From 9aaa4313cbddac3bf66a977ba4e7ee11d1b22dbd Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 02:43:43 +0500 Subject: [PATCH 209/502] svg added and refactored again --- .../components/nodes/vectorstores/Astra/Astra.ts | 8 ++++---- .../components/nodes/vectorstores/Astra/astra.svg | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts index e3377cb5..865f1044 100644 --- a/packages/components/nodes/vectorstores/Astra/Astra.ts +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -103,8 +103,8 @@ class Astra_VectorStores implements INode { } const clientConfig = { - token: credentialData?.applicationToken ?? 'dummy', - endpoint: credentialData?.dbEndPoint ?? 'dummy' + token: credentialData?.applicationToken, + endpoint: credentialData?.dbEndPoint } const astraConfig: AstraLibArgs = { @@ -151,8 +151,8 @@ class Astra_VectorStores implements INode { } const clientConfig = { - token: credentialData?.applicationToken ?? 'dummy', - endpoint: credentialData?.dbEndPoint ?? 'dummy' + token: credentialData?.applicationToken, + endpoint: credentialData?.dbEndPoint } const astraConfig: AstraLibArgs = { diff --git a/packages/components/nodes/vectorstores/Astra/astra.svg b/packages/components/nodes/vectorstores/Astra/astra.svg index 59c2fc3f..de58397d 100644 --- a/packages/components/nodes/vectorstores/Astra/astra.svg +++ b/packages/components/nodes/vectorstores/Astra/astra.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + + + + + From 42f80fade23c0799e311c13f9ff482306d13a7c2 Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 12 Jan 2024 13:34:52 +0530 Subject: [PATCH 210/502] Add new node type for canvas - sticky note --- packages/ui/src/views/canvas/CardWrapper.js | 21 ++++ packages/ui/src/views/canvas/LightTooltip.js | 12 +++ packages/ui/src/views/canvas/StickyNote.js | 105 +++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 packages/ui/src/views/canvas/CardWrapper.js create mode 100644 packages/ui/src/views/canvas/LightTooltip.js create mode 100644 packages/ui/src/views/canvas/StickyNote.js diff --git a/packages/ui/src/views/canvas/CardWrapper.js b/packages/ui/src/views/canvas/CardWrapper.js new file mode 100644 index 00000000..3e010899 --- /dev/null +++ b/packages/ui/src/views/canvas/CardWrapper.js @@ -0,0 +1,21 @@ +// material-ui +import { styled } from '@mui/material/styles' + +// project imports +import MainCard from '../../ui-component/cards/MainCard' + +const CardWrapper = styled(MainCard)(({ theme }) => ({ + background: theme.palette.card.main, + color: theme.darkTextPrimary, + border: 'solid 1px', + borderColor: theme.palette.primary[200] + 75, + width: '300px', + height: 'auto', + padding: '10px', + boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)', + '&:hover': { + borderColor: theme.palette.primary.main + } +})) + +export default CardWrapper diff --git a/packages/ui/src/views/canvas/LightTooltip.js b/packages/ui/src/views/canvas/LightTooltip.js new file mode 100644 index 00000000..32e34aae --- /dev/null +++ b/packages/ui/src/views/canvas/LightTooltip.js @@ -0,0 +1,12 @@ +import { styled } from '@mui/material/styles' +import Tooltip, { tooltipClasses } from '@mui/material/Tooltip' + +const LightTooltip = styled(({ className, ...props }) => )(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: theme.palette.nodeToolTip.background, + color: theme.palette.nodeToolTip.color, + boxShadow: theme.shadows[1] + } +})) + +export default LightTooltip diff --git a/packages/ui/src/views/canvas/StickyNote.js b/packages/ui/src/views/canvas/StickyNote.js new file mode 100644 index 00000000..53406513 --- /dev/null +++ b/packages/ui/src/views/canvas/StickyNote.js @@ -0,0 +1,105 @@ +import PropTypes from 'prop-types' +import { useContext, useState } from 'react' +import { useSelector } from 'react-redux' + +// material-ui +import { useTheme } from '@mui/material/styles' + +// project imports +import CardWrapper from './CardWrapper' +import LightTooltip from './LightTooltip' +import { IconButton, Box } from '@mui/material' +import { IconCopy, IconTrash } from '@tabler/icons' +import { Input } from 'ui-component/input/Input' + +// const +import { flowContext } from '../../store/context/ReactFlowContext' + +const StickyNote = ({ data }) => { + const theme = useTheme() + const canvas = useSelector((state) => state.canvas) + const { deleteNode, duplicateNode } = useContext(flowContext) + const [inputParam] = data.inputParams + + const [open, setOpen] = useState(false) + + const handleClose = () => { + setOpen(false) + } + + const handleOpen = () => { + setOpen(true) + } + + console.log(data.id) + + return ( + <> + + + { + duplicateNode(data.id) + }} + sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.primary.main } }} + color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'} + > + + + { + deleteNode(data.id) + }} + sx={{ height: '35px', width: '35px', '&:hover': { color: 'red' } }} + color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'} + > + + + + } + placement='right-start' + > + + (data.inputs[inputParam.name] = newValue)} + value={data.inputs[inputParam.name] ?? inputParam.default ?? ''} + nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []} + edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []} + nodeId={data.id} + /> + + + + + ) +} + +StickyNote.propTypes = { + data: PropTypes.object +} + +export default StickyNote From 1544f6140784a4f6e96cbb106da4e765733bdb6f Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 12 Jan 2024 13:41:58 +0530 Subject: [PATCH 211/502] Refactor CanvasNode to use the new sticky node --- packages/ui/src/views/canvas/CanvasNode.js | 148 +++++++++------------ packages/ui/src/views/canvas/index.js | 5 +- 2 files changed, 63 insertions(+), 90 deletions(-) diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index a039f6d1..043baaea 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -3,12 +3,13 @@ import { useContext, useState, useEffect } from 'react' import { useSelector } from 'react-redux' // material-ui -import { styled, useTheme } from '@mui/material/styles' +import { useTheme } from '@mui/material/styles' import { IconButton, Box, Typography, Divider, Button } from '@mui/material' -import Tooltip, { tooltipClasses } from '@mui/material/Tooltip' +import Tooltip from '@mui/material/Tooltip' // project imports -import MainCard from 'ui-component/cards/MainCard' +import CardWrapper from './CardWrapper' +import LightTooltip from './LightTooltip' import NodeInputHandler from './NodeInputHandler' import NodeOutputHandler from './NodeOutputHandler' import AdditionalParamsDialog from 'ui-component/dialog/AdditionalParamsDialog' @@ -19,28 +20,6 @@ import { baseURL } from 'store/constant' import { IconTrash, IconCopy, IconInfoCircle, IconAlertTriangle } from '@tabler/icons' import { flowContext } from 'store/context/ReactFlowContext' -const CardWrapper = styled(MainCard)(({ theme }) => ({ - background: theme.palette.card.main, - color: theme.darkTextPrimary, - border: 'solid 1px', - borderColor: theme.palette.primary[200] + 75, - width: '300px', - height: 'auto', - padding: '10px', - boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)', - '&:hover': { - borderColor: theme.palette.primary.main - } -})) - -const LightTooltip = styled(({ className, ...props }) => )(({ theme }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: theme.palette.nodeToolTip.background, - color: theme.palette.nodeToolTip.color, - boxShadow: theme.shadows[1] - } -})) - // ===========================|| CANVAS NODE ||=========================== // const CanvasNode = ({ data }) => { @@ -54,7 +33,6 @@ const CanvasNode = ({ data }) => { const [infoDialogProps, setInfoDialogProps] = useState({}) const [warningMessage, setWarningMessage] = useState('') const [open, setOpen] = useState(false) - const isNote = data.type === 'StickyNote' const handleClose = () => { setOpen(false) @@ -151,49 +129,47 @@ const CanvasNode = ({ data }) => { placement='right-start' > - {!isNote && ( -
    - -
    - Notification -
    -
    - - - {data.label} - - - {warningMessage && ( - <> -
    - {warningMessage}} placement='top'> - - - - - - )} -
    - )} - {(data.inputAnchors.length > 0 || data.inputParams.length > 0) && !isNote && ( +
    + +
    + Notification +
    +
    + + + {data.label} + + + {warningMessage && ( + <> +
    + {warningMessage}} placement='top'> + + + + + + )} +
    + {(data.inputAnchors.length > 0 || data.inputParams.length > 0) && ( <> @@ -233,25 +209,21 @@ const CanvasNode = ({ data }) => { )} - {!isNote && ( - <> - - - - Output - - - - {data.outputAnchors.map((outputAnchor, index) => ( - - ))} - - )} + + + + Output + + + + {data.outputAnchors.map((outputAnchor, index) => ( + + ))} diff --git a/packages/ui/src/views/canvas/index.js b/packages/ui/src/views/canvas/index.js index 9aa53cc6..29f83ea4 100644 --- a/packages/ui/src/views/canvas/index.js +++ b/packages/ui/src/views/canvas/index.js @@ -21,6 +21,7 @@ import { useTheme } from '@mui/material/styles' // project imports import CanvasNode from './CanvasNode' import ButtonEdge from './ButtonEdge' +import StickyNote from './StickyNote' import CanvasHeader from './CanvasHeader' import AddNodes from './AddNodes' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' @@ -46,7 +47,7 @@ import useNotifier from 'utils/useNotifier' // const import { FLOWISE_CREDENTIAL_ID } from 'store/constant' -const nodeTypes = { customNode: CanvasNode } +const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: ButtonEdge } // ==============================|| CANVAS ||============================== // @@ -276,7 +277,7 @@ const Canvas = () => { const newNode = { id: newNodeId, position, - type: 'customNode', + type: nodeData.type !== 'StickyNote' ? 'customNode' : 'stickyNote', data: initNode(nodeData, newNodeId) } From 951fda75c8255e7f306925d1572f5771dc132c4d Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 12 Jan 2024 13:44:24 +0530 Subject: [PATCH 212/502] Update node input component --- packages/ui/src/ui-component/input/Input.js | 80 ++++++++++++++------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index 3e575938..b8434ff2 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react' import PropTypes from 'prop-types' -import { FormControl, OutlinedInput, Popover } from '@mui/material' +import { FormControl, OutlinedInput, InputBase, Popover } from '@mui/material' import SelectVariable from 'ui-component/json/SelectVariable' import { getAvailableNodesForVariable } from 'utils/genericHelper' @@ -50,29 +50,61 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab return ( <> - - { - setMyValue(e.target.value) - onChange(e.target.value) - }} - inputProps={{ - step: inputParam.step ?? 1, - style: { - height: inputParam.rows ? '90px' : 'inherit' - } - }} - /> - + {inputParam.name === 'note' ? ( + + { + setMyValue(e.target.value) + onChange(e.target.value) + }} + inputProps={{ + step: inputParam.step ?? 1, + style: { + border: 'none', + background: 'none' + } + }} + sx={{ + border: 'none', + background: 'none', + padding: '10px 14px' + }} + /> + + ) : ( + + { + setMyValue(e.target.value) + onChange(e.target.value) + }} + inputProps={{ + step: inputParam.step ?? 1, + style: { + height: inputParam.rows ? '90px' : 'inherit' + } + }} + /> + + )}
    {inputParam?.acceptVariable && ( Date: Fri, 12 Jan 2024 13:45:06 +0530 Subject: [PATCH 213/502] Update sticky note node configuration --- .../nodes/utilities/StickyNote/StickyNote.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/components/nodes/utilities/StickyNote/StickyNote.ts b/packages/components/nodes/utilities/StickyNote/StickyNote.ts index 270e2524..8b0ec208 100644 --- a/packages/components/nodes/utilities/StickyNote/StickyNote.ts +++ b/packages/components/nodes/utilities/StickyNote/StickyNote.ts @@ -10,7 +10,6 @@ class StickyNote implements INode { category: string baseClasses: string[] inputs: INodeParams[] - disableOutput: boolean constructor() { this.label = 'Sticky Note' @@ -19,18 +18,17 @@ class StickyNote implements INode { this.type = 'StickyNote' this.icon = 'stickyNote.svg' this.category = 'Utilities' - this.description = 'Add a note about a node' + this.description = 'Add a sticky note' this.inputs = [ { - label: 'Note', + label: '', name: 'note', type: 'string', - rows: 4, - placeholder: 'Input your notes', + rows: 1, + placeholder: 'Type something here', optional: true } ] - this.disableOutput = true this.baseClasses = [this.type] } From 2742a26c6ab6eac830868065c3b81b0dfb91fc26 Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 12 Jan 2024 14:08:55 +0530 Subject: [PATCH 214/502] Fix console error and remove console.log statements --- packages/ui/src/ui-component/input/Input.js | 2 +- packages/ui/src/views/canvas/StickyNote.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index b8434ff2..b7a1912d 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -53,7 +53,7 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab {inputParam.name === 'note' ? ( { setOpen(true) } - console.log(data.id) - return ( <> Date: Fri, 12 Jan 2024 16:50:47 +0530 Subject: [PATCH 215/502] Fix sticky note text color in dark mode --- packages/ui/src/ui-component/input/Input.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index b7a1912d..806ab53d 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -70,7 +70,8 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab step: inputParam.step ?? 1, style: { border: 'none', - background: 'none' + background: 'none', + color: '#212121' } }} sx={{ From e104af43463f66c7266d2eec4d9b1a1b336cc19f Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 12 Jan 2024 17:09:36 +0530 Subject: [PATCH 216/502] Fix placeholder color for sticky note node --- packages/ui/src/ui-component/input/Input.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index 806ab53d..e59f012c 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -77,7 +77,12 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab sx={{ border: 'none', background: 'none', - padding: '10px 14px' + padding: '10px 14px', + textarea: { + '&::placeholder': { + color: '#616161' + } + } }} /> From c15250f28be3995ac3147c1a57e5b2811aae4d2e Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 13 Jan 2024 16:18:35 +0800 Subject: [PATCH 217/502] add TopK and safetySettings --- .../ChatGoogleGenerativeAI.ts | 87 ++++++++++++++++++- packages/components/src/utils.ts | 17 ++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index 546fa224..311b4e8d 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -1,7 +1,8 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { convertStringToArrayString, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { BaseCache } from 'langchain/schema' import { ChatGoogleGenerativeAI } from '@langchain/google-genai' +import { HarmBlockThreshold, HarmCategory } from '@google/generative-ai' class GoogleGenerativeAI_ChatModels implements INode { label: string @@ -74,6 +75,72 @@ class GoogleGenerativeAI_ChatModels implements INode { step: 0.1, optional: true, additionalParams: true + }, + { + label: 'topK', + name: 'topK', + type: 'number', + step: 0.1, + optional: true, + additionalParams: true + }, + { + label: 'Harm Category', + name: 'harmCategory', + type: 'multiOptions', + description: + 'Refer to official guide on how to use Harm Category', + options: [ + { + label: 'Dangerous', + name: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT + }, + { + label: 'Harassment', + name: HarmCategory.HARM_CATEGORY_HARASSMENT + }, + { + label: 'Hate Speech', + name: HarmCategory.HARM_CATEGORY_HATE_SPEECH + }, + { + label: 'Sexually Explicit', + name: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT + } + ], + optional: true, + additionalParams: true + }, + { + label: 'Harm Block Threshold', + name: 'harmBlockThreshold', + type: 'multiOptions', + description: + 'Refer to official guide on how to use Harm Block Threshold', + options: [ + { + label: 'Low and Above', + name: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + }, + { + label: 'Medium and Above', + name: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE + }, + { + label: 'None', + name: HarmBlockThreshold.BLOCK_NONE + }, + { + label: 'Only High', + name: HarmBlockThreshold.BLOCK_ONLY_HIGH + }, + { + label: 'Threshold Unspecified', + name: HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED + } + ], + optional: true, + additionalParams: true } ] } @@ -86,6 +153,9 @@ class GoogleGenerativeAI_ChatModels implements INode { const modelName = nodeData.inputs?.modelName as string const maxOutputTokens = nodeData.inputs?.maxOutputTokens as string const topP = nodeData.inputs?.topP as string + const topK = nodeData.inputs?.topK as string + const harmCategory = nodeData.inputs?.harmCategory as string + const harmBlockThreshold = nodeData.inputs?.harmBlockThreshold as string const cache = nodeData.inputs?.cache as BaseCache const obj = { @@ -98,8 +168,23 @@ class GoogleGenerativeAI_ChatModels implements INode { const model = new ChatGoogleGenerativeAI(obj) if (topP) model.topP = parseFloat(topP) + if (topK) model.topP = parseFloat(topK) if (cache) model.cache = cache if (temperature) model.temperature = parseFloat(temperature) + + // safetySettings + let harmCategories: string[] = convertStringToArrayString(harmCategory) + let harmBlockThresholds: string[] = convertStringToArrayString(harmBlockThreshold) + if (harmCategories.length != harmBlockThresholds.length) + throw new Error(`Harm Category & Harm Block Threshold are not the same length`) + const safetySettings = harmCategories.map((value, index) => { + return { + category: value, + threshold: harmBlockThresholds[index] + } + }) + if (safetySettings) model.safetySettings = safetySettings + return model } } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 22fa6f4a..88c553cf 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -673,3 +673,20 @@ export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[ } return formatmessages } + +/** + * Convert String to Array String + * @param {string} inputString + * @returns {string[]} + */ +export const convertStringToArrayString = (inputString: string): string[] => { + let ArrayString: string[] = [] + if (inputString) { + try { + ArrayString = JSON.parse(inputString) + } catch (e) { + ArrayString = [] + } + } + return ArrayString +} From bdc892df2b36917df5586742c6eaa2fb1528e9fd Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sun, 14 Jan 2024 00:51:28 +0800 Subject: [PATCH 218/502] fix error TS2322: Type '{ category: string; threshold: string; }[]' is not assignable to type 'SafetySetting[] --- .../ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index 311b4e8d..ddae6780 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -177,13 +177,13 @@ class GoogleGenerativeAI_ChatModels implements INode { let harmBlockThresholds: string[] = convertStringToArrayString(harmBlockThreshold) if (harmCategories.length != harmBlockThresholds.length) throw new Error(`Harm Category & Harm Block Threshold are not the same length`) - const safetySettings = harmCategories.map((value, index) => { + const safetySettings: typeof model.safetySettings = harmCategories.map((value, index) => { return { category: value, threshold: harmBlockThresholds[index] } }) - if (safetySettings) model.safetySettings = safetySettings + if (safetySettings.length > 0) model.safetySettings = safetySettings return model } From 203b182cd54154d451369edc26291cc1f2f0b1cf Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sun, 14 Jan 2024 01:35:39 +0800 Subject: [PATCH 219/502] fix error TS2322 part 2 --- .../ChatGoogleGenerativeAI.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index ddae6780..737b0bf2 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -1,7 +1,7 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { convertStringToArrayString, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { BaseCache } from 'langchain/schema' -import { ChatGoogleGenerativeAI } from '@langchain/google-genai' +import { ChatGoogleGenerativeAI, GoogleGenerativeAIChatInput } from '@langchain/google-genai' import { HarmBlockThreshold, HarmCategory } from '@google/generative-ai' class GoogleGenerativeAI_ChatModels implements INode { @@ -158,10 +158,23 @@ class GoogleGenerativeAI_ChatModels implements INode { const harmBlockThreshold = nodeData.inputs?.harmBlockThreshold as string const cache = nodeData.inputs?.cache as BaseCache - const obj = { + // safetySettings + let harmCategories: string[] = convertStringToArrayString(harmCategory) + let harmBlockThresholds: string[] = convertStringToArrayString(harmBlockThreshold) + if (harmCategories.length != harmBlockThresholds.length) + throw new Error(`Harm Category & Harm Block Threshold are not the same length`) + const safetySettings = harmCategories.map((value, index) => { + return { + category: value, + threshold: harmBlockThresholds[index] + } + }) + + const obj: Partial = { apiKey: apiKey, modelName: modelName, - maxOutputTokens: 2048 + maxOutputTokens: 2048, + safetySettings: safetySettings.length > 0 ? safetySettings : undefined } if (maxOutputTokens) obj.maxOutputTokens = parseInt(maxOutputTokens, 10) @@ -172,19 +185,6 @@ class GoogleGenerativeAI_ChatModels implements INode { if (cache) model.cache = cache if (temperature) model.temperature = parseFloat(temperature) - // safetySettings - let harmCategories: string[] = convertStringToArrayString(harmCategory) - let harmBlockThresholds: string[] = convertStringToArrayString(harmBlockThreshold) - if (harmCategories.length != harmBlockThresholds.length) - throw new Error(`Harm Category & Harm Block Threshold are not the same length`) - const safetySettings: typeof model.safetySettings = harmCategories.map((value, index) => { - return { - category: value, - threshold: harmBlockThresholds[index] - } - }) - if (safetySettings.length > 0) model.safetySettings = safetySettings - return model } } From 4d15e886fe156e3d89c728f406aee0fb1cd91253 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sun, 14 Jan 2024 03:38:14 +0800 Subject: [PATCH 220/502] fix error TS2322 part 3 --- .../ChatGoogleGenerativeAI.ts | 73 +++++++++++++++---- packages/components/package.json | 1 + packages/components/src/utils.ts | 4 +- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index 737b0bf2..bd660b47 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -1,8 +1,9 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { convertStringToArrayString, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { convertMultiOptionsToStringArray, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { BaseCache } from 'langchain/schema' import { ChatGoogleGenerativeAI, GoogleGenerativeAIChatInput } from '@langchain/google-genai' import { HarmBlockThreshold, HarmCategory } from '@google/generative-ai' +import type { SafetySetting } from '@google/generative-ai' class GoogleGenerativeAI_ChatModels implements INode { label: string @@ -158,23 +159,10 @@ class GoogleGenerativeAI_ChatModels implements INode { const harmBlockThreshold = nodeData.inputs?.harmBlockThreshold as string const cache = nodeData.inputs?.cache as BaseCache - // safetySettings - let harmCategories: string[] = convertStringToArrayString(harmCategory) - let harmBlockThresholds: string[] = convertStringToArrayString(harmBlockThreshold) - if (harmCategories.length != harmBlockThresholds.length) - throw new Error(`Harm Category & Harm Block Threshold are not the same length`) - const safetySettings = harmCategories.map((value, index) => { - return { - category: value, - threshold: harmBlockThresholds[index] - } - }) - const obj: Partial = { apiKey: apiKey, modelName: modelName, - maxOutputTokens: 2048, - safetySettings: safetySettings.length > 0 ? safetySettings : undefined + maxOutputTokens: 2048 } if (maxOutputTokens) obj.maxOutputTokens = parseInt(maxOutputTokens, 10) @@ -185,8 +173,63 @@ class GoogleGenerativeAI_ChatModels implements INode { if (cache) model.cache = cache if (temperature) model.temperature = parseFloat(temperature) + // Safety Settings + let harmCategories: string[] = convertMultiOptionsToStringArray(harmCategory) + let harmBlockThresholds: string[] = convertMultiOptionsToStringArray(harmBlockThreshold) + if (harmCategories.length != harmBlockThresholds.length) + throw new Error(`Harm Category & Harm Block Threshold are not the same length`) + const safetySettings: SafetySetting[] = harmCategories.map((value, index) => { + return { + category: categoryInput(value), + threshold: thresholdInput(harmBlockThresholds[index]) + } + }) + if (safetySettings.length > 0) model.safetySettings = safetySettings + return model } } +const categoryInput = (categoryInput: string): HarmCategory => { + let categoryOutput: HarmCategory + switch (categoryInput) { + case HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: + categoryOutput = HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT + break + case HarmCategory.HARM_CATEGORY_HATE_SPEECH: + categoryOutput = HarmCategory.HARM_CATEGORY_HATE_SPEECH + break + case HarmCategory.HARM_CATEGORY_HARASSMENT: + categoryOutput = HarmCategory.HARM_CATEGORY_HARASSMENT + break + case HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: + categoryOutput = HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT + break + default: + categoryOutput = HarmCategory.HARM_CATEGORY_UNSPECIFIED + } + return categoryOutput +} + +const thresholdInput = (thresholdInput: string): HarmBlockThreshold => { + let thresholdOutput: HarmBlockThreshold + switch (thresholdInput) { + case HarmBlockThreshold.BLOCK_LOW_AND_ABOVE: + thresholdOutput = HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + break + case HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: + thresholdOutput = HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE + break + case HarmBlockThreshold.BLOCK_NONE: + thresholdOutput = HarmBlockThreshold.BLOCK_NONE + break + case HarmBlockThreshold.BLOCK_ONLY_HIGH: + thresholdOutput = HarmBlockThreshold.BLOCK_ONLY_HIGH + break + default: + thresholdOutput = HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED + } + return thresholdOutput +} + module.exports = { nodeClass: GoogleGenerativeAI_ChatModels } diff --git a/packages/components/package.json b/packages/components/package.json index a2565430..55e84074 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -25,6 +25,7 @@ "@gomomento/sdk": "^1.51.1", "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", + "@google/generative-ai": "^0.1.3", "@huggingface/inference": "^2.6.1", "@langchain/google-genai": "^0.0.6", "@langchain/mistralai": "^0.0.6", diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 88c553cf..2d983562 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -675,11 +675,11 @@ export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[ } /** - * Convert String to Array String + * Convert MultiOptions String to String Array * @param {string} inputString * @returns {string[]} */ -export const convertStringToArrayString = (inputString: string): string[] => { +export const convertMultiOptionsToStringArray = (inputString: string): string[] => { let ArrayString: string[] = [] if (inputString) { try { From 8a4b6a72247d6dfb36a088b861cbc0efd3cde5e4 Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Sat, 13 Jan 2024 19:14:45 -0700 Subject: [PATCH 221/502] Fixing naming, handling embeddings for LocalAI also --- ....credential.ts => LocalAIApi.credential.ts} | 4 ++-- .../chatmodels/ChatLocalAI/ChatLocalAI.ts | 6 +++--- .../LocalAIEmbedding/LocalAIEmbedding.ts | 18 ++++++++++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) rename packages/components/credentials/{LcoalAIApi.credential.ts => LocalAIApi.credential.ts} (86%) diff --git a/packages/components/credentials/LcoalAIApi.credential.ts b/packages/components/credentials/LocalAIApi.credential.ts similarity index 86% rename from packages/components/credentials/LcoalAIApi.credential.ts rename to packages/components/credentials/LocalAIApi.credential.ts index 624e07fa..4aafe040 100644 --- a/packages/components/credentials/LcoalAIApi.credential.ts +++ b/packages/components/credentials/LocalAIApi.credential.ts @@ -8,12 +8,12 @@ class LocalAIApi implements INodeCredential { constructor() { this.label = 'LocalAI API' - this.name = 'LocalAIApi' + this.name = 'localAIApi' this.version = 1.0 this.inputs = [ { label: 'LocalAI Api Key', - name: 'LocalAIApiKey', + name: 'localAIApiKey', type: 'password' } ] diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index c44f03ce..f2825d0d 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -30,7 +30,7 @@ class ChatLocalAI_ChatModels implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['LocalAIApi'], + credentialNames: ['localAIApi'], optional: true } this.inputs = [ @@ -95,11 +95,11 @@ class ChatLocalAI_ChatModels implements INode { const timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const localAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) + const localAIApiKey = getCredentialParam('localAIApiKey', credentialData, nodeData) const cache = nodeData.inputs?.cache as BaseCache - const obj: Partial & BaseLLMParams & { localAIApiKey?: string } = { + const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { temperature: parseFloat(temperature), modelName, openAIApiKey: 'sk-' diff --git a/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts b/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts index 557e35d6..24efaf8c 100644 --- a/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts +++ b/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts @@ -1,4 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getCredentialData, getCredentialParam } from '../../../src/utils' import { OpenAIEmbeddings, OpenAIEmbeddingsParams } from 'langchain/embeddings/openai' class LocalAIEmbedding_Embeddings implements INode { @@ -10,6 +11,7 @@ class LocalAIEmbedding_Embeddings implements INode { category: string description: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -21,6 +23,13 @@ class LocalAIEmbedding_Embeddings implements INode { this.category = 'Embeddings' this.description = 'Use local embeddings models like llama.cpp' this.baseClasses = [this.type, 'Embeddings'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['localAIApi'], + optional: true + } this.inputs = [ { label: 'Base Path', @@ -37,15 +46,20 @@ class LocalAIEmbedding_Embeddings implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const modelName = nodeData.inputs?.modelName as string const basePath = nodeData.inputs?.basePath as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const localAIApiKey = getCredentialParam('localAIApiKey', credentialData, nodeData) + const obj: Partial & { openAIApiKey?: string } = { modelName, openAIApiKey: 'sk-' } + if (localAIApiKey) obj.openAIApiKey = localAIApiKey + const model = new OpenAIEmbeddings(obj, { basePath }) return model From ec50493851332e49b9e896713cdcc33a24b23241 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 14 Jan 2024 11:57:53 +0000 Subject: [PATCH 222/502] delete message API --- packages/server/src/index.ts | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b6f59191..cdb3dc3e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -523,7 +523,7 @@ export class App { res.status(404).send(`Chatflow ${chatflowid} not found`) return } - const chatId = (req.query?.chatId as string) ?? (await getChatId(chatflowid)) + const chatId = req.query?.chatId as string const memoryType = req.query?.memoryType as string | undefined const sessionId = req.query?.sessionId as string | undefined const chatType = req.query?.chatType as string | undefined @@ -546,7 +546,8 @@ export class App { await clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId) } - const deleteOptions: FindOptionsWhere = { chatflowid, chatId } + const deleteOptions: FindOptionsWhere = { chatflowid } + if (chatId) deleteOptions.chatId = chatId if (memoryType) deleteOptions.memoryType = memoryType if (sessionId) deleteOptions.sessionId = sessionId if (chatType) deleteOptions.chatType = chatType @@ -634,7 +635,7 @@ export class App { return res.json(result) }) - // Delete all chatmessages from chatflowid + // Delete all credentials from chatflowid this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) @@ -1811,23 +1812,6 @@ export class App { } } -/** - * Get first chat message id - * @param {string} chatflowid - * @returns {string} - */ -export async function getChatId(chatflowid: string): Promise { - // first chatmessage id as the unique chat id - const firstChatMessage = await getDataSource() - .getRepository(ChatMessage) - .createQueryBuilder('cm') - .select('cm.id') - .where('chatflowid = :chatflowid', { chatflowid }) - .orderBy('cm.createdDate', 'ASC') - .getOne() - return firstChatMessage ? firstChatMessage.id : '' -} - let serverApp: App | undefined export async function getAllChatFlow(): Promise { From 04ef695b3f9b2ab0ff7d42f65a5d4f19178d8664 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 15 Jan 2024 02:46:21 +0000 Subject: [PATCH 223/502] update README-ZH md --- README-ZH.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/README-ZH.md b/README-ZH.md index 208eee92..8750ebc7 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -145,29 +145,40 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package ## 🌐 自托管 -### [Railway](https://docs.flowiseai.com/deployment/railway) +在您现有的基础设施中部署自托管的 Flowise,我们支持各种[部署](https://docs.flowiseai.com/configuration/deployment) -[![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) +- [AWS](https://docs.flowiseai.com/deployment/aws) +- [Azure](https://docs.flowiseai.com/deployment/azure) +- [Digital Ocean](https://docs.flowiseai.com/deployment/digital-ocean) +- [GCP](https://docs.flowiseai.com/deployment/gcp) +-
    + 其他 -### [Render](https://docs.flowiseai.com/deployment/render) + - [Railway](https://docs.flowiseai.com/deployment/railway) -[![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) + [![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) -### [Sealos](https://docs.flowiseai.com/configuration/deployment/sealos) + - [Render](https://docs.flowiseai.com/deployment/render) -[![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://template.cloud.sealos.io/deploy?templateName=flowise) + [![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) -### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) + - [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) -HuggingFace Spaces + HuggingFace Spaces -### [AWS](https://docs.flowiseai.com/deployment/aws) + - [Elestio](https://elest.io/open-source/flowiseai) -### [Azure](https://docs.flowiseai.com/deployment/azure) + [![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) -### [DigitalOcean](https://docs.flowiseai.com/deployment/digital-ocean) + - [Sealos](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) -### [GCP](https://docs.flowiseai.com/deployment/gcp) + [![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + + - [RepoCloud](https://repocloud.io/details/?app_id=29) + + [![部署到 RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) + +
    ## 💻 云托管 From 4ef9c1f5118bbf4f910215e1cd56e1f6e0762891 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 15 Jan 2024 18:28:46 +0000 Subject: [PATCH 224/502] add sessionId tracking --- packages/components/package.json | 2 +- packages/components/src/handler.ts | 42 +++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index a2565430..86405c2b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -54,7 +54,7 @@ "ioredis": "^5.3.2", "langchain": "^0.0.214", "langfuse": "2.0.2", - "langfuse-langchain": "2.0.2", + "langfuse-langchain": "2.3.3", "langsmith": "0.0.53", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 1eb05a51..df72a685 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -1,13 +1,13 @@ -import { BaseTracer, Run, BaseCallbackHandler } from 'langchain/callbacks' +import { BaseTracer, Run, BaseCallbackHandler, LangChainTracer } from 'langchain/callbacks' import { AgentAction, ChainValues } from 'langchain/schema' import { Logger } from 'winston' import { Server } from 'socket.io' import { Client } from 'langsmith' -import { LangChainTracer } from 'langchain/callbacks' -import { LLMonitorHandler } from 'langchain/callbacks/handlers/llmonitor' +import { LLMonitorHandler, LLMonitorHandlerFields } from 'langchain/callbacks/handlers/llmonitor' import { getCredentialData, getCredentialParam } from './utils' import { ICommonObject, INodeData } from './Interface' import CallbackHandler from 'langfuse-langchain' +import { LangChainTracerFields } from '@langchain/core/tracers/tracer_langchain' import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith' import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' import monitor from 'llmonitor' @@ -235,11 +235,16 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO apiKey: langSmithApiKey }) - const tracer = new LangChainTracer({ + let langSmithField: LangChainTracerFields = { projectName: langSmithProject ?? 'default', - //@ts-ignore client - }) + } + + if (nodeData?.inputs?.analytics?.langSmith) { + langSmithField = { ...langSmithField, ...nodeData?.inputs?.analytics?.langSmith } + } + + const tracer = new LangChainTracer(langSmithField) callbacks.push(tracer) } else if (provider === 'langFuse') { const release = analytic[provider].release as string @@ -248,13 +253,17 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, nodeData) const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, nodeData) - const langFuseOptions: any = { + let langFuseOptions: any = { secretKey: langFuseSecretKey, publicKey: langFusePublicKey, baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' } if (release) langFuseOptions.release = release - if (options.chatId) langFuseOptions.userId = options.chatId + if (options.chatId) langFuseOptions.sessionId = options.chatId + + if (nodeData?.inputs?.analytics?.langFuse) { + langFuseOptions = { ...langFuseOptions, ...nodeData?.inputs?.analytics?.langFuse } + } const handler = new CallbackHandler(langFuseOptions) callbacks.push(handler) @@ -262,11 +271,15 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, nodeData) const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, nodeData) - const llmonitorFields: ICommonObject = { + let llmonitorFields: LLMonitorHandlerFields = { appId: llmonitorAppId, apiUrl: llmonitorEndpoint ?? 'https://app.llmonitor.com' } + if (nodeData?.inputs?.analytics?.llmonitor) { + llmonitorFields = { ...llmonitorFields, ...nodeData?.inputs?.analytics?.llmonitor } + } + const handler = new LLMonitorHandler(llmonitorFields) callbacks.push(handler) } @@ -360,7 +373,8 @@ export class AnalyticHandler { }, serialized: {}, project_name: this.handlers['langSmith'].langSmithProject, - client: this.handlers['langSmith'].client + client: this.handlers['langSmith'].client, + ...this.nodeData?.inputs?.analytics?.langSmith } const parentRun = new RunTree(parentRunConfig) await parentRun.postRun() @@ -390,8 +404,9 @@ export class AnalyticHandler { const langfuse: Langfuse = this.handlers['langFuse'].client langfuseTraceClient = langfuse.trace({ name, - userId: this.options.chatId, - metadata: { tags: ['openai-assistant'] } + sessionId: this.options.chatId, + metadata: { tags: ['openai-assistant'] }, + ...this.nodeData?.inputs?.analytics?.langFuse }) } else { langfuseTraceClient = this.handlers['langFuse'].trace[parentIds['langFuse']] @@ -420,7 +435,8 @@ export class AnalyticHandler { runId, name, userId: this.options.chatId, - input + input, + ...this.nodeData?.inputs?.analytics?.llmonitor }) this.handlers['llmonitor'].chainEvent = { [runId]: runId } returnIds['llmonitor'].chainEvent = runId From b3ab8527f5c03d4dda4ab22ded7a648cb6cc44d0 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 15 Jan 2024 18:36:31 +0000 Subject: [PATCH 225/502] add ts-ignore --- packages/components/src/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index df72a685..5d2b53f6 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -237,6 +237,7 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO let langSmithField: LangChainTracerFields = { projectName: langSmithProject ?? 'default', + //@ts-ignore client } From d7a998f26649c9878bfbc5140f1c65323e6f480a Mon Sep 17 00:00:00 2001 From: automaton82 Date: Mon, 15 Jan 2024 23:27:33 -0500 Subject: [PATCH 226/502] Update package.json Updating turbo to the latest compatible version to fix a bug with github actions not able to build arm64 docker images. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a9bfcbf..12fbe20f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "pretty-quick": "^3.1.3", "rimraf": "^3.0.2", "run-script-os": "^1.1.6", - "turbo": "1.7.4", + "turbo": "^1.7.4", "typescript": "^4.8.4" }, "engines": { From f66c03ab0ae3f83f7f4b03f6e2a9cf9a8b9212f6 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 8 Jan 2024 13:02:56 +0000 Subject: [PATCH 227/502] change agent/chain with memory to use runnable --- .../ConversationalAgent.ts | 163 +++-- .../ConversationalRetrievalAgent.ts | 127 ++-- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 75 ++- .../OpenAIFunctionAgent.ts | 273 +------- .../ConversationChain/ConversationChain.ts | 146 +++-- .../ConversationalRetrievalQAChain.ts | 380 +++++++---- .../ConversationalRetrievalQAChain/prompts.ts | 79 +-- .../nodes/memory/BufferMemory/BufferMemory.ts | 33 +- .../BufferWindowMemory/BufferWindowMemory.ts | 34 +- .../ConversationSummaryMemory.ts | 42 +- .../nodes/memory/DynamoDb/DynamoDb.ts | 49 +- .../memory/MongoDBMemory/MongoDBMemory.ts | 49 +- .../memory/MotorheadMemory/MotorheadMemory.ts | 92 ++- .../RedisBackedChatMemory.ts | 70 +- .../UpstashRedisBackedChatMemory.ts | 49 +- .../nodes/memory/ZepMemory/ZepMemory.ts | 46 +- .../nodes/tools/CustomTool/CustomTool.ts | 8 +- .../components/nodes/tools/CustomTool/core.ts | 17 +- packages/components/package.json | 1 + packages/components/src/Interface.ts | 28 +- packages/components/src/agents.ts | 615 ++++++++++++++++++ .../marketplaces/chatflows/API Agent.json | 2 +- .../chatflows/Chat with a Podcast.json | 56 +- .../marketplaces/chatflows/Claude LLM.json | 2 +- .../chatflows/Conversational Agent.json | 2 +- .../Conversational Retrieval QA Chain.json | 62 +- .../chatflows/Flowise Docs QnA.json | 61 +- .../marketplaces/chatflows/Local QnA.json | 61 +- .../chatflows/Long Term Memory.json | 63 +- .../chatflows/Metadata Filter.json | 61 +- .../chatflows/Multiple VectorDB.json | 2 +- .../chatflows/Simple Conversation Chain.json | 2 +- .../chatflows/Vectara LLM Chain Upload.json | 55 +- .../marketplaces/chatflows/WebBrowser.json | 2 +- .../marketplaces/chatflows/WebPage QnA.json | 63 +- packages/server/src/index.ts | 98 ++- packages/server/src/utils/index.ts | 175 ++--- .../ui/src/views/canvas/NodeInputHandler.js | 3 +- 38 files changed, 1752 insertions(+), 1394 deletions(-) create mode 100644 packages/components/src/agents.ts diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index 8a2329b5..7f857b1c 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -1,11 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor, InitializeAgentExecutorOptions } from 'langchain/agents' import { Tool } from 'langchain/tools' -import { BaseChatMemory } from 'langchain/memory' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' import { BaseChatModel } from 'langchain/chat_models/base' import { flatten } from 'lodash' -import { additionalCallbacks } from '../../../src/handler' +import { AgentStep, BaseMessage, ChainValues, AIMessage, HumanMessage } from 'langchain/schema' +import { RunnableSequence } from 'langchain/schema/runnable' +import { getBaseClasses } from '../../../src/utils' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { AgentExecutor } from '../../../src/agents' +import { ChatConversationalAgent } from 'langchain/agents' +import { renderTemplate } from '@langchain/core/prompts' const DEFAULT_PREFIX = `Assistant is a large language model trained by OpenAI. @@ -15,6 +18,15 @@ Assistant is constantly learning and improving, and its capabilities are constan Overall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.` +const TEMPLATE_TOOL_RESPONSE = `TOOL RESPONSE: +--------------------- +{observation} + +USER'S INPUT +-------------------- + +Okay, so what is the response to my last comment? If using information obtained from the tools you must mention it explicitly without mentioning the tool names - I have forgotten all TOOL RESPONSES! Remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else.` + class ConversationalAgent_Agents implements INode { label: string name: string @@ -25,8 +37,9 @@ class ConversationalAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Agent' this.name = 'conversationalAgent' this.version = 2.0 @@ -43,7 +56,7 @@ class ConversationalAgent_Agents implements INode { list: true }, { - label: 'Language Model', + label: 'Chat Model', name: 'model', type: 'BaseChatModel' }, @@ -62,52 +75,114 @@ class ConversationalAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseChatModel - let tools = nodeData.inputs?.tools as Tool[] - tools = flatten(tools) - const memory = nodeData.inputs?.memory as BaseChatMemory - const systemMessage = nodeData.inputs?.systemMessage as string - - const obj: InitializeAgentExecutorOptions = { - agentType: 'chat-conversational-react-description', - verbose: process.env.DEBUG === 'true' ? true : false - } - - const agentArgs: any = {} - if (systemMessage) { - agentArgs.systemMessage = systemMessage - } - - if (Object.keys(agentArgs).length) obj.agentArgs = agentArgs - - const executor = await initializeAgentExecutorWithOptions(tools, model, obj) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - const memory = nodeData.inputs?.memory as BaseChatMemory - - 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 memory = nodeData.inputs?.memory as FlowiseMemory + const executor = await prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) - const result = await executor.call({ input }, [...callbacks]) - return result?.output + let res: ChainValues = {} + + if (options.socketIO && options.socketIOClientId) { + const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) + } else { + 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 prepareAgent = async ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as BaseChatModel + let tools = nodeData.inputs?.tools as Tool[] + tools = flatten(tools) + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + /** Bind a stop token to the model */ + const modelWithStop = model.bind({ + stop: ['\nObservation'] + }) + + const outputParser = ChatConversationalAgent.getDefaultOutputParser({ + llm: model, + toolNames: tools.map((tool) => tool.name) + }) + + const prompt = ChatConversationalAgent.createPrompt(tools, { + systemMessage: systemMessage ? systemMessage : DEFAULT_PREFIX, + outputParser + }) + + const runnableAgent = RunnableSequence.from([ + { + [inputKey]: (i: { input: string; steps: AgentStep[] }) => i.input, + agent_scratchpad: async (i: { input: string; steps: AgentStep[] }) => await constructScratchPad(i.steps), + [memoryKey]: async (_: { input: string; steps: AgentStep[] }) => { + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithStop, + outputParser + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + +const constructScratchPad = async (steps: AgentStep[]): Promise => { + const thoughts: BaseMessage[] = [] + for (const step of steps) { + thoughts.push(new AIMessage(step.action.log)) + thoughts.push( + new HumanMessage( + renderTemplate(TEMPLATE_TOOL_RESPONSE, 'f-string', { + observation: step.observation + }) + ) + ) + } + return thoughts +} + module.exports = { nodeClass: ConversationalAgent_Agents } diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index 643c6a65..406a156f 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts @@ -1,9 +1,14 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' +import { ChainValues, AgentStep, BaseMessage } from 'langchain/schema' import { flatten } from 'lodash' -import { BaseChatMemory } from 'langchain/memory' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { formatToOpenAIFunction } from 'langchain/tools' +import { RunnableSequence } from 'langchain/schema/runnable' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { OpenAIFunctionsAgentOutputParser } from 'langchain/agents/openai/output_parser' +import { AgentExecutor, formatAgentSteps } from '../../../src/agents' const defaultMessage = `Do your best to answer the questions. Feel free to use any tools available to look up relevant information, only if necessary.` @@ -17,8 +22,9 @@ class ConversationalRetrievalAgent_Agents implements INode { category: string baseClasses: string[] inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Retrieval Agent' this.name = 'conversationalRetrievalAgent' this.version = 3.0 @@ -54,55 +60,96 @@ class ConversationalRetrievalAgent_Agents implements INode { additionalParams: true } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model - 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 ?? defaultMessage - }, - returnIntermediateSteps: true - }) - executor.memory = memory - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const executor = nodeData.instance as AgentExecutor - - if (executor.memory) { - ;(executor.memory as any).memoryKey = 'chat_history' - ;(executor.memory as any).outputKey = 'output' - ;(executor.memory as any).returnMessages = true - - const chatHistoryClassName = (executor.memory as any).chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - ;(executor.memory as any).chatHistory = mapChatHistory(options) - } - } + const memory = nodeData.inputs?.memory as FlowiseMemory + const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) 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.call({ input }, [loggerHandler, handler, ...callbacks]) - return result?.output + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const result = await executor.call({ input }, [loggerHandler, ...callbacks]) - return result?.output + 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 prepareAgent = ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as ChatOpenAI + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + let tools = nodeData.inputs?.tools + tools = flatten(tools) + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + const inputKey = memory.inputKey ? memory.inputKey : 'input' + + const prompt = ChatPromptTemplate.fromMessages([ + ['ai', systemMessage ? systemMessage : defaultMessage], + new MessagesPlaceholder(memoryKey), + ['human', `{${inputKey}}`], + new MessagesPlaceholder('agent_scratchpad') + ]) + + const modelWithFunctions = model.bind({ + functions: [...tools.map((tool: any) => formatToOpenAIFunction(tool))] + }) + + const runnableAgent = RunnableSequence.from([ + { + [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 = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] + return messages ?? [] + } + }, + prompt, + modelWithFunctions, + new OpenAIFunctionsAgentOutputParser() + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + returnIntermediateSteps: true, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + module.exports = { nodeClass: ConversationalRetrievalAgent_Agents } diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index cf69022b..62ecec5b 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -96,45 +96,51 @@ class OpenAIAssistant_Agents implements INode { return null } - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const selectedAssistantId = nodeData.inputs?.selectedAssistant as string - const appDataSource = options.appDataSource as DataSource - const databaseEntities = options.databaseEntities as IDatabaseEntity - let sessionId = nodeData.inputs?.sessionId as string + async clearChatMessages(nodeData: INodeData, options: ICommonObject, sessionIdObj: { type: string; id: string }): Promise { + const selectedAssistantId = nodeData.inputs?.selectedAssistant as string + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity - const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ - id: selectedAssistantId + const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({ + id: selectedAssistantId + }) + + if (!assistant) { + options.logger.error(`Assistant ${selectedAssistantId} not found`) + return + } + + if (!sessionIdObj) return + + let sessionId = '' + if (sessionIdObj.type === 'chatId') { + const chatId = sessionIdObj.id + const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ + chatId }) - - if (!assistant) { - options.logger.error(`Assistant ${selectedAssistantId} not found`) + if (!chatmsg) { + options.logger.error(`Chat Message with Chat Id: ${chatId} not found`) return } + sessionId = chatmsg.sessionId + } else if (sessionIdObj.type === 'threadId') { + sessionId = sessionIdObj.id + } - if (!sessionId && options.chatId) { - const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ - chatId: options.chatId - }) - if (!chatmsg) { - options.logger.error(`Chat Message with Chat Id: ${options.chatId} not found`) - return - } - sessionId = chatmsg.sessionId - } + const credentialData = await getCredentialData(assistant.credential ?? '', options) + const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) + if (!openAIApiKey) { + options.logger.error(`OpenAI ApiKey not found`) + return + } - const credentialData = await getCredentialData(assistant.credential ?? '', options) - const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) - if (!openAIApiKey) { - options.logger.error(`OpenAI ApiKey not found`) - return - } - - const openai = new OpenAI({ apiKey: openAIApiKey }) - options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + const openai = new OpenAI({ apiKey: openAIApiKey }) + options.logger.info(`Clearing OpenAI Thread ${sessionId}`) + try { if (sessionId) await openai.beta.threads.del(sessionId) options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) + } catch (e) { + throw new Error(e) } } @@ -297,7 +303,11 @@ class OpenAIAssistant_Agents implements INode { options.socketIO.to(options.socketIOClientId).emit('tool', tool.name) try { - const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, threadId) + const toolOutput = await tool.call(actions[i].toolInput, undefined, undefined, { + sessionId: threadId, + chatId: options.chatId, + input + }) await analyticHandlers.onToolEnd(toolIds, toolOutput) submitToolOutputs.push({ tool_call_id: actions[i].toolCallId, @@ -462,6 +472,7 @@ class OpenAIAssistant_Agents implements INode { const imageRegex = /]*\/>/g let llmOutput = returnVal.replace(imageRegex, '') llmOutput = llmOutput.replace('
    ', '') + await analyticHandlers.onLLMEnd(llmIds, llmOutput) await analyticHandlers.onChainEnd(parentIds, messageData, true) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index c0095cee..135121d2 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -1,17 +1,14 @@ -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' -import { CallbackManagerForChainRun } from 'langchain/callbacks' -import { formatToOpenAIFunction } from 'langchain/tools' -import { ToolInputParsingException, Tool } from '@langchain/core/tools' +import { ChainValues, AgentStep, BaseMessage } from 'langchain/schema' import { getBaseClasses } from '../../../src/utils' import { flatten } from 'lodash' import { RunnableSequence } from 'langchain/schema/runnable' +import { formatToOpenAIFunction } from 'langchain/tools' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' 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' +import { AgentExecutor, formatAgentSteps } from '../../../src/agents' class OpenAIFunctionAgent_Agents implements INode { label: string @@ -25,7 +22,7 @@ class OpenAIFunctionAgent_Agents implements INode { inputs: INodeParams[] sessionId?: string - constructor(fields: { sessionId?: string }) { + constructor(fields?: { sessionId?: string }) { this.label = 'OpenAI Function Agent' this.name = 'openAIFunctionAgent' this.version = 3.0 @@ -33,7 +30,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(LCAgentExecutor)] + this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] this.inputs = [ { label: 'Allowed Tools', @@ -63,19 +60,13 @@ class OpenAIFunctionAgent_Agents implements INode { this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const memory = nodeData.inputs?.memory as FlowiseMemory - - const executor = prepareAgent(nodeData, this.sessionId) - if (memory) executor.memory = memory - - return executor + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory as FlowiseMemory - - const executor = prepareAgent(nodeData, this.sessionId) + const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) @@ -107,17 +98,11 @@ class OpenAIFunctionAgent_Agents implements INode { } } -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 prepareAgent = ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { const model = nodeData.inputs?.model as ChatOpenAI const memory = nodeData.inputs?.memory as FlowiseMemory const systemMessage = nodeData.inputs?.systemMessage as string @@ -142,7 +127,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 = (await memory.getChatMessages(sessionId, true)) as BaseMessage[] + const messages = (await memory.getChatMessages(flowObj?.sessionId, true, chatHistory)) as BaseMessage[] return messages ?? [] } }, @@ -154,231 +139,13 @@ const prepareAgent = (nodeData: INodeData, sessionId?: string) => { const executor = AgentExecutor.fromAgentAndTools({ agent: runnableAgent, tools, - sessionId + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + verbose: process.env.DEBUG === 'true' ? true : false }) return executor } -type AgentExecutorOutput = ChainValues - -class AgentExecutor extends LCAgentExecutor { - sessionId?: string - - static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string }): AgentExecutor { - const newInstance = new AgentExecutor(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 - } -} - module.exports = { nodeClass: OpenAIFunctionAgent_Agents } diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 54d4252a..fcd9921e 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -1,14 +1,16 @@ -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' import { ConversationChain } from 'langchain/chains' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' +import { getBaseClasses } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' -import { BufferMemory } from 'langchain/memory' import { BaseChatModel } from 'langchain/chat_models/base' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { flatten } from 'lodash' import { Document } from 'langchain/document' +import { RunnableSequence } from 'langchain/schema/runnable' +import { StringOutputParser } from 'langchain/schema/output_parser' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` +const inputKey = 'input' class ConversationChain_Chains implements INode { label: string @@ -20,8 +22,9 @@ class ConversationChain_Chains implements INode { baseClasses: string[] description: string inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversation Chain' this.name = 'conversationChain' this.version = 1.0 @@ -32,7 +35,7 @@ class ConversationChain_Chains implements INode { this.baseClasses = [this.type, ...getBaseClasses(ConversationChain)] this.inputs = [ { - label: 'Language Model', + label: 'Chat Model', name: 'model', type: 'BaseChatModel' }, @@ -60,76 +63,99 @@ class ConversationChain_Chains implements INode { placeholder: 'You are a helpful assistant that write codes' } ] + this.sessionId = fields?.sessionId } - async init(nodeData: INodeData): Promise { - const model = nodeData.inputs?.model as BaseChatModel - const memory = nodeData.inputs?.memory as BufferMemory - const prompt = nodeData.inputs?.systemMessagePrompt as string - const docs = nodeData.inputs?.document as Document[] - - const flattenDocs = docs && docs.length ? flatten(docs) : [] - const finalDocs = [] - for (let i = 0; i < flattenDocs.length; i += 1) { - if (flattenDocs[i] && flattenDocs[i].pageContent) { - finalDocs.push(new Document(flattenDocs[i])) - } - } - - let finalText = '' - for (let i = 0; i < finalDocs.length; i += 1) { - finalText += finalDocs[i].pageContent - } - - const replaceChar: string[] = ['{', '}'] - for (const char of replaceChar) finalText = finalText.replaceAll(char, '') - - if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` - - const obj: any = { - llm: model, - memory, - verbose: process.env.DEBUG === 'true' ? true : false - } - - const chatPrompt = ChatPromptTemplate.fromMessages([ - SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), - new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), - HumanMessagePromptTemplate.fromTemplate('{input}') - ]) - obj.prompt = chatPrompt - - const chain = new ConversationChain(obj) + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) return chain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chain = nodeData.instance as ConversationChain - const memory = nodeData.inputs?.memory as BufferMemory - memory.returnMessages = true // Return true for BaseChatModel - - if (options && options.chatHistory) { - const chatHistoryClassName = memory.chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - memory.chatHistory = mapChatHistory(options) - } - } - - chain.memory = memory + const memory = nodeData.inputs?.memory + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) + let res = '' + if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.call({ input }, [loggerHandler, handler, ...callbacks]) - return res?.response + res = await chain.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) } else { - const res = await chain.call({ input }, [loggerHandler, ...callbacks]) - return res?.response + res = await chain.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return res } } +const prepareChatPrompt = (nodeData: INodeData) => { + const memory = nodeData.inputs?.memory as FlowiseMemory + const prompt = nodeData.inputs?.systemMessagePrompt as string + const docs = nodeData.inputs?.document as Document[] + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + let finalText = '' + for (let i = 0; i < finalDocs.length; i += 1) { + finalText += finalDocs[i].pageContent + } + + const replaceChar: string[] = ['{', '}'] + for (const char of replaceChar) finalText = finalText.replaceAll(char, '') + + if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` + + const chatPrompt = ChatPromptTemplate.fromMessages([ + SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) + ]) + + return chatPrompt +} + +const prepareChain = (nodeData: INodeData, sessionId?: string, chatHistory: IMessage[] = []) => { + const model = nodeData.inputs?.model as BaseChatModel + const memory = nodeData.inputs?.memory as FlowiseMemory + const memoryKey = memory.memoryKey ?? 'chat_history' + + const conversationChain = RunnableSequence.from([ + { + [inputKey]: (input: { input: string }) => input.input, + [memoryKey]: async () => { + const history = await memory.getChatMessages(sessionId, true, chatHistory) + return history + } + }, + prepareChatPrompt(nodeData), + model, + new StringOutputParser() + ]) + + return conversationChain +} + module.exports = { nodeClass: ConversationChain_Chains } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts index 36376e13..5f98cba1 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -1,20 +1,25 @@ import { BaseLanguageModel } from 'langchain/base_language' -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, mapChatHistory } from '../../../src/utils' -import { ConversationalRetrievalQAChain, QAChainParams } from 'langchain/chains' +import { ConversationalRetrievalQAChain } from 'langchain/chains' import { BaseRetriever } from 'langchain/schema/retriever' -import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BufferMemoryInput } from 'langchain/memory' import { PromptTemplate } from 'langchain/prompts' -import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' -import { - default_map_reduce_template, - default_qa_template, - qa_template, - map_reduce_template, - CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT, - refine_question_template, - refine_template -} from './prompts' +import { QA_TEMPLATE, REPHRASE_TEMPLATE, RESPONSE_TEMPLATE } from './prompts' +import { Runnable, RunnableSequence, RunnableMap, RunnableBranch, RunnableLambda } from 'langchain/schema/runnable' +import { BaseMessage, HumanMessage, AIMessage } from 'langchain/schema' +import { StringOutputParser } from 'langchain/schema/output_parser' +import type { Document } from 'langchain/document' +import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts' +import { applyPatch } from 'fast-json-patch' +import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' +import { ConsoleCallbackHandler, additionalCallbacks } from '../../../src/handler' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' + +type RetrievalChainInput = { + chat_history: string + question: string +} + +const sourceRunnableName = 'FindDocs' class ConversationalRetrievalQAChain_Chains implements INode { label: string @@ -26,11 +31,12 @@ class ConversationalRetrievalQAChain_Chains implements INode { baseClasses: string[] description: string inputs: INodeParams[] + sessionId?: string - constructor() { + constructor(fields?: { sessionId?: string }) { this.label = 'Conversational Retrieval QA Chain' this.name = 'conversationalRetrievalQAChain' - this.version = 1.0 + this.version = 2.0 this.type = 'ConversationalRetrievalQAChain' this.icon = 'qa.svg' this.category = 'Chains' @@ -38,9 +44,9 @@ class ConversationalRetrievalQAChain_Chains implements INode { this.baseClasses = [this.type, ...getBaseClasses(ConversationalRetrievalQAChain)] this.inputs = [ { - label: 'Language Model', + label: 'Chat Model', name: 'model', - type: 'BaseLanguageModel' + type: 'BaseChatModel' }, { label: 'Vector Store Retriever', @@ -60,6 +66,29 @@ class ConversationalRetrievalQAChain_Chains implements INode { type: 'boolean', optional: true }, + { + label: 'Rephrase Prompt', + name: 'rephrasePrompt', + type: 'string', + description: 'Using previous chat history, rephrase question into a standalone question', + warning: 'Prompt must include input variables: {chat_history} and {question}', + rows: 4, + additionalParams: true, + optional: true, + default: REPHRASE_TEMPLATE + }, + { + label: 'Response Prompt', + name: 'responsePrompt', + type: 'string', + description: 'Taking the rephrased question, search for answer from the provided context', + warning: 'Prompt must include input variable: {context}', + rows: 4, + additionalParams: true, + optional: true, + default: RESPONSE_TEMPLATE + } + /** Deprecated { label: 'System Message', name: 'systemMessagePrompt', @@ -70,6 +99,7 @@ class ConversationalRetrievalQAChain_Chains implements INode { placeholder: 'I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". You will provide me with answers from the given info. If the answer is not included, say exactly "Hmm, I am not sure." and stop after that. Refuse to answer any question not about the info. Never break character.' }, + // TODO: create standalone chains for these 3 modes as they are not compatible with memory { label: 'Chain Option', name: 'chainOption', @@ -95,124 +125,246 @@ class ConversationalRetrievalQAChain_Chains implements INode { additionalParams: true, optional: true } + */ ] + this.sessionId = fields?.sessionId } async init(nodeData: INodeData): Promise { const model = nodeData.inputs?.model as BaseLanguageModel const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string - const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const chainOption = nodeData.inputs?.chainOption as string - const externalMemory = nodeData.inputs?.memory + const rephrasePrompt = nodeData.inputs?.rephrasePrompt as string + const responsePrompt = nodeData.inputs?.responsePrompt as string - const obj: any = { - verbose: process.env.DEBUG === 'true' ? true : false, - questionGeneratorChainOptions: { - template: CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT - } + let customResponsePrompt = responsePrompt + // If the deprecated systemMessagePrompt is still exists + if (systemMessagePrompt) { + customResponsePrompt = `${systemMessagePrompt}\n${QA_TEMPLATE}` } - if (returnSourceDocuments) obj.returnSourceDocuments = returnSourceDocuments - - if (chainOption === 'map_reduce') { - obj.qaChainOptions = { - type: 'map_reduce', - combinePrompt: PromptTemplate.fromTemplate( - systemMessagePrompt ? `${systemMessagePrompt}\n${map_reduce_template}` : default_map_reduce_template - ) - } as QAChainParams - } else if (chainOption === 'refine') { - const qprompt = new PromptTemplate({ - inputVariables: ['context', 'question'], - template: refine_question_template(systemMessagePrompt) - }) - const rprompt = new PromptTemplate({ - inputVariables: ['context', 'question', 'existing_answer'], - template: refine_template - }) - obj.qaChainOptions = { - type: 'refine', - questionPrompt: qprompt, - refinePrompt: rprompt - } as QAChainParams - } else { - obj.qaChainOptions = { - type: 'stuff', - prompt: PromptTemplate.fromTemplate(systemMessagePrompt ? `${systemMessagePrompt}\n${qa_template}` : default_qa_template) - } as QAChainParams - } - - if (externalMemory) { - externalMemory.memoryKey = 'chat_history' - externalMemory.inputKey = 'question' - externalMemory.outputKey = 'text' - externalMemory.returnMessages = true - if (chainOption === 'refine') externalMemory.outputKey = 'output_text' - obj.memory = externalMemory - } else { - const fields: BufferMemoryInput = { - memoryKey: 'chat_history', - inputKey: 'question', - outputKey: 'text', - returnMessages: true - } - if (chainOption === 'refine') fields.outputKey = 'output_text' - obj.memory = new BufferMemory(fields) - } - - const chain = ConversationalRetrievalQAChain.fromLLM(model, vectorStoreRetriever, obj) - return chain + const answerChain = createChain(model, vectorStoreRetriever, rephrasePrompt, customResponsePrompt) + return answerChain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { - const chain = nodeData.instance as ConversationalRetrievalQAChain + const model = nodeData.inputs?.model as BaseLanguageModel + const externalMemory = nodeData.inputs?.memory + const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as BaseRetriever + const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string + const rephrasePrompt = nodeData.inputs?.rephrasePrompt as string + const responsePrompt = nodeData.inputs?.responsePrompt as string const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const chainOption = nodeData.inputs?.chainOption as string - let model = nodeData.inputs?.model - - // Temporary fix: https://github.com/hwchase17/langchainjs/issues/754 - model.streaming = false - chain.questionGeneratorChain.llm = model - - const obj = { question: input } - - if (options && options.chatHistory && chain.memory) { - const chatHistoryClassName = (chain.memory as any).chatHistory.constructor.name - // Only replace when its In-Memory - if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') { - ;(chain.memory as any).chatHistory = mapChatHistory(options) - } + let customResponsePrompt = responsePrompt + // If the deprecated systemMessagePrompt is still exists + if (systemMessagePrompt) { + customResponsePrompt = `${systemMessagePrompt}\n${QA_TEMPLATE}` } + let memory: FlowiseMemory | undefined = externalMemory + if (!memory) { + memory = new BufferMemory({ + returnMessages: true, + memoryKey: 'chat_history', + inputKey: 'input' + }) + } + + const answerChain = createChain(model, vectorStoreRetriever, rephrasePrompt, customResponsePrompt) + + const history = ((await memory.getChatMessages(this.sessionId, false, options.chatHistory)) as IMessage[]) ?? [] + const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) - if (options.socketIO && options.socketIOClientId) { - const handler = new CustomChainHandler( - options.socketIO, - options.socketIOClientId, - chainOption === 'refine' ? 4 : undefined, - returnSourceDocuments - ) - const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) - if (chainOption === 'refine') { - if (res.output_text && res.sourceDocuments) { - return { - text: res.output_text, - sourceDocuments: res.sourceDocuments - } - } - return res?.output_text + const stream = answerChain.streamLog( + { question: input, chat_history: history }, + { callbacks: [loggerHandler, ...callbacks] }, + { + includeNames: [sourceRunnableName] + } + ) + + let streamedResponse: Record = {} + let sourceDocuments: ICommonObject[] = [] + let text = '' + let isStreamingStarted = false + const isStreamingEnabled = options.socketIO && options.socketIOClientId + + for await (const chunk of stream) { + streamedResponse = applyPatch(streamedResponse, chunk.ops).newDocument + + if (streamedResponse.final_output) { + text = streamedResponse.final_output?.output + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('end') + if (Array.isArray(streamedResponse?.logs?.[sourceRunnableName]?.final_output?.output)) { + sourceDocuments = streamedResponse?.logs?.[sourceRunnableName]?.final_output?.output + if (isStreamingEnabled && returnSourceDocuments) + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', sourceDocuments) + } + } + + if ( + Array.isArray(streamedResponse?.streamed_output) && + streamedResponse?.streamed_output.length && + !streamedResponse.final_output + ) { + const token = streamedResponse.streamed_output[streamedResponse.streamed_output.length - 1] + + if (!isStreamingStarted) { + isStreamingStarted = true + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('start', token) + } + if (isStreamingEnabled) options.socketIO.to(options.socketIOClientId).emit('token', token) } - if (res.text && res.sourceDocuments) return res - return res?.text - } else { - const res = await chain.call(obj, [loggerHandler, ...callbacks]) - if (res.text && res.sourceDocuments) return res - return res?.text } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: text, + type: 'apiMessage' + } + ], + this.sessionId + ) + + if (returnSourceDocuments) return { text, sourceDocuments } + else return { text } + } +} + +const createRetrieverChain = (llm: BaseLanguageModel, retriever: Runnable, rephrasePrompt: string) => { + // Small speed/accuracy optimization: no need to rephrase the first question + // since there shouldn't be any meta-references to prior chat history + const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(rephrasePrompt) + const condenseQuestionChain = RunnableSequence.from([CONDENSE_QUESTION_PROMPT, llm, new StringOutputParser()]).withConfig({ + runName: 'CondenseQuestion' + }) + + const hasHistoryCheckFn = RunnableLambda.from((input: RetrievalChainInput) => input.chat_history.length > 0).withConfig({ + runName: 'HasChatHistoryCheck' + }) + + const conversationChain = condenseQuestionChain.pipe(retriever).withConfig({ + runName: 'RetrievalChainWithHistory' + }) + + const basicRetrievalChain = RunnableLambda.from((input: RetrievalChainInput) => input.question) + .withConfig({ + runName: 'Itemgetter:question' + }) + .pipe(retriever) + .withConfig({ runName: 'RetrievalChainWithNoHistory' }) + + return RunnableBranch.from([[hasHistoryCheckFn, conversationChain], basicRetrievalChain]).withConfig({ runName: sourceRunnableName }) +} + +const formatDocs = (docs: Document[]) => { + return docs.map((doc, i) => `${doc.pageContent}`).join('\n') +} + +const formatChatHistoryAsString = (history: BaseMessage[]) => { + return history.map((message) => `${message._getType()}: ${message.content}`).join('\n') +} + +const serializeHistory = (input: any) => { + const chatHistory: IMessage[] = input.chat_history || [] + const convertedChatHistory = [] + for (const message of chatHistory) { + if (message.type === 'userMessage') { + convertedChatHistory.push(new HumanMessage({ content: message.message })) + } + if (message.type === 'apiMessage') { + convertedChatHistory.push(new AIMessage({ content: message.message })) + } + } + return convertedChatHistory +} + +const createChain = ( + llm: BaseLanguageModel, + retriever: Runnable, + rephrasePrompt = REPHRASE_TEMPLATE, + responsePrompt = RESPONSE_TEMPLATE +) => { + const retrieverChain = createRetrieverChain(llm, retriever, rephrasePrompt) + + const context = RunnableMap.from({ + context: RunnableSequence.from([ + ({ question, chat_history }) => ({ + question, + chat_history: formatChatHistoryAsString(chat_history) + }), + retrieverChain, + RunnableLambda.from(formatDocs).withConfig({ + runName: 'FormatDocumentChunks' + }) + ]), + question: RunnableLambda.from((input: RetrievalChainInput) => input.question).withConfig({ + runName: 'Itemgetter:question' + }), + chat_history: RunnableLambda.from((input: RetrievalChainInput) => input.chat_history).withConfig({ + runName: 'Itemgetter:chat_history' + }) + }).withConfig({ tags: ['RetrieveDocs'] }) + + const prompt = ChatPromptTemplate.fromMessages([ + ['system', responsePrompt], + new MessagesPlaceholder('chat_history'), + ['human', `{question}`] + ]) + + const responseSynthesizerChain = RunnableSequence.from([prompt, llm, new StringOutputParser()]).withConfig({ + tags: ['GenerateResponse'] + }) + + const conversationalQAChain = RunnableSequence.from([ + { + question: RunnableLambda.from((input: RetrievalChainInput) => input.question).withConfig({ + runName: 'Itemgetter:question' + }), + chat_history: RunnableLambda.from(serializeHistory).withConfig({ + runName: 'SerializeHistory' + }) + }, + context, + responseSynthesizerChain + ]) + + return conversationalQAChain +} + +class BufferMemory extends FlowiseMemory implements MemoryMethods { + constructor(fields: BufferMemoryInput) { + super(fields) + } + + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + const memoryResult = await this.loadMemoryVariables({}) + const baseMessages = memoryResult[this.memoryKey ?? 'chat_history'] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return + } + + async clearChatMessages(): Promise { + await this.clear() } } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts index 132e3a97..dccc7358 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/prompts.ts @@ -1,64 +1,27 @@ -export const default_qa_template = `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. - -{context} - -Question: {question} -Helpful Answer:` - -export const qa_template = `Use the following pieces of context to answer the question at the end. - -{context} - -Question: {question} -Helpful Answer:` - -export const default_map_reduce_template = `Given the following extracted parts of a long document and a question, create a final answer. -If you don't know the answer, just say that you don't know. Don't try to make up an answer. - -{summaries} - -Question: {question} -Helpful Answer:` - -export const map_reduce_template = `Given the following extracted parts of a long document and a question, create a final answer. - -{summaries} - -Question: {question} -Helpful Answer:` - -export const refine_question_template = (sysPrompt?: string) => { - let returnPrompt = '' - if (sysPrompt) - returnPrompt = `Context information is below. ---------------------- -{context} ---------------------- -Given the context information and not prior knowledge, ${sysPrompt} -Answer the question: {question}. -Answer:` - if (!sysPrompt) - returnPrompt = `Context information is below. ---------------------- -{context} ---------------------- -Given the context information and not prior knowledge, answer the question: {question}. -Answer:` - return returnPrompt -} - -export const refine_template = `The original question is as follows: {question} -We have provided an existing answer: {existing_answer} -We have the opportunity to refine the existing answer (only if needed) with some more context below. ------------- -{context} ------------- -Given the new context, refine the original answer to better answer the question. -If you can't find answer from the context, return the original answer.` - export const CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, answer in the same language as the follow up question. include it in the standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question:` + +export const RESPONSE_TEMPLATE = `I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". Using the provided context, answer the user's question to the best of your ability using the resources provided. +If there is nothing in the context relevant to the question at hand, just say "Hmm, I'm not sure" and stop after that. Refuse to answer any question not about the info. Never break character. +------------ +{context} +------------ +REMEMBER: If there is no relevant information within the context, just say "Hmm, I'm not sure". Don't try to make up an answer. Never break character.` + +export const QA_TEMPLATE = `Use the following pieces of context to answer the question at the end. + +{context} + +Question: {question} +Helpful Answer:` + +export const REPHRASE_TEMPLATE = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. + +Chat History: +{chat_history} +Follow Up Input: {question} +Standalone Question:` diff --git a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts index 0ad8adec..4a6252b5 100644 --- a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts +++ b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts @@ -1,4 +1,4 @@ -import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { BaseMessage } from 'langchain/schema' @@ -55,36 +55,27 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { super(fields) } - async getChatMessages(_?: string, returnBaseMessages = false): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + 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 addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } 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 ca8d0ddf..c21405a4 100644 --- a/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts +++ b/packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts @@ -1,4 +1,4 @@ -import { FlowiseWindowMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { FlowiseWindowMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { BufferWindowMemory, BufferWindowMemoryInput } from 'langchain/memory' import { BaseMessage } from 'langchain/schema' @@ -67,36 +67,28 @@ class BufferWindowMemoryExtended extends FlowiseWindowMemory implements MemoryMe super(fields) } - async getChatMessages(_?: string, returnBaseMessages = false): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + + // Insert into chatHistory + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + 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 addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } 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: BufferWindowMemory_Memory } diff --git a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts index 107ab7db..45d39326 100644 --- a/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts +++ b/packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts @@ -1,4 +1,4 @@ -import { FlowiseSummaryMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { FlowiseSummaryMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { ConversationSummaryMemory, ConversationSummaryMemoryInput } from 'langchain/memory' import { BaseLanguageModel } from 'langchain/base_language' @@ -66,40 +66,32 @@ class ConversationSummaryMemoryExtended extends FlowiseSummaryMemory implements super(fields) } - async getChatMessages(_?: string, returnBaseMessages = false): Promise { + async getChatMessages(_?: string, returnBaseMessages = false, prevHistory: IMessage[] = []): Promise { + await this.chatHistory.clear() + this.buffer = '' + + for (const msg of prevHistory) { + if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message) + else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message) + } + + // Get summary + const chatMessages = await this.chatHistory.getMessages() + this.buffer = chatMessages.length ? await this.predictNewSummary(chatMessages.slice(-2), this.buffer) : '' + 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 addChatMessages(): Promise { + // adding chat messages will be done on the fly in getChatMessages() + return } 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) - } } module.exports = { nodeClass: ConversationSummaryMemory_Memory } diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 872ec0b5..91c1d369 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -12,13 +12,7 @@ import { 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/utils' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class DynamoDb_Memory implements INode { @@ -70,7 +64,8 @@ class DynamoDb_Memory implements INode { label: 'Session ID', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -88,25 +83,6 @@ class DynamoDb_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initalizeDynamoDB(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const dynamodbMemory = await initalizeDynamoDB(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing DynamoDb memory session ${sessionId ? sessionId : chatId}`) - await dynamodbMemory.clear() - options.logger.info(`Successfully cleared DynamoDb memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const dynamodbMemory = await initalizeDynamoDB(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await dynamodbMemory.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { @@ -114,17 +90,7 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P const partitionKey = nodeData.inputs?.partitionKey as string const region = nodeData.inputs?.region as string const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options.chatId - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessKeyId = getCredentialParam('accessKey', credentialData, nodeData) @@ -150,7 +116,6 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: dynamoDb, - isSessionIdUsingChatMessageId, sessionId, dynamodbClient: client }) @@ -158,7 +123,6 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean dynamodbClient: DynamoDBClient sessionId: string } @@ -178,7 +142,6 @@ interface DynamoDBSerializedChatMessage { } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - isSessionIdUsingChatMessageId = false sessionId = '' dynamodbClient: DynamoDBClient @@ -306,10 +269,6 @@ 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 b422921e..c593c20d 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -2,13 +2,7 @@ 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, - serializeChatHistory -} from '../../../src/utils' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' class MongoDB_Memory implements INode { @@ -55,7 +49,8 @@ class MongoDB_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -73,42 +68,13 @@ class MongoDB_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initializeMongoDB(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const mongodbMemory = await initializeMongoDB(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing MongoDB memory session ${sessionId ? sessionId : chatId}`) - await mongodbMemory.clear() - options.logger.info(`Successfully cleared MongoDB memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const mongodbMemory = await initializeMongoDB(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await mongodbMemory.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { const databaseName = nodeData.inputs?.databaseName as string const collectionName = nodeData.inputs?.collectionName as string const memoryKey = nodeData.inputs?.memoryKey as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -149,14 +115,12 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, collection }) } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean collection: Collection sessionId: string } @@ -164,7 +128,6 @@ interface BufferMemoryExtendedInput { class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { sessionId = '' collection: Collection - isSessionIdUsingChatMessageId? = false constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) @@ -221,10 +184,6 @@ 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/MotorheadMemory/MotorheadMemory.ts b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts index 938cc873..19506fc1 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -1,9 +1,14 @@ 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' +import { MotorheadMemory, MotorheadMemoryInput, InputValues, OutputValues } from 'langchain/memory' import fetch from 'node-fetch' -import { BaseMessage } from 'langchain/schema' +import { AIMessage, BaseMessage, ChatMessage, HumanMessage } from 'langchain/schema' + +type MotorheadMessage = { + content: string + role: 'Human' | 'AI' +} class MotorMemory_Memory implements INode { label: string @@ -46,7 +51,8 @@ class MotorMemory_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -64,49 +70,19 @@ class MotorMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initalizeMotorhead(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const motorhead = await initalizeMotorhead(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Motorhead memory session ${sessionId ? sessionId : chatId}`) - await motorhead.clear() - options.logger.info(`Successfully cleared Motorhead memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const motorhead = await initalizeMotorhead(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await motorhead.loadMemoryVariables({}) - return getBufferString(memoryResult[key]) - } - } } const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): Promise => { const memoryKey = nodeData.inputs?.memoryKey as string const baseURL = nodeData.inputs?.baseURL as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const clientId = getCredentialParam('clientId', credentialData, nodeData) - let obj: MotorheadMemoryInput & MotorheadMemoryExtendedInput = { + let obj: MotorheadMemoryInput = { returnMessages: true, - isSessionIdUsingChatMessageId, sessionId, memoryKey } @@ -132,23 +108,9 @@ const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): return motorheadMemory } -interface MotorheadMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean -} - class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false - - constructor(fields: MotorheadMemoryInput & MotorheadMemoryExtendedInput) { + constructor(fields: MotorheadMemoryInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId - } - - async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { - if (overrideSessionId) { - this.sessionId = overrideSessionId - } - return super.loadMemoryVariables({ values }) } async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { @@ -180,9 +142,33 @@ class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { 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) + try { + const resp = await this.caller.call(fetch, `${this.url}/sessions/${id}/memory`, { + //@ts-ignore + signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined, + headers: this._getHeaders() as ICommonObject, + method: 'GET' + }) + const data = await resp.json() + const rawStoredMessages: MotorheadMessage[] = data?.data?.messages ?? [] + + const baseMessages = rawStoredMessages.reverse().map((message) => { + const { content, role } = message + if (role === 'Human') { + return new HumanMessage(content) + } else if (role === 'AI') { + return new AIMessage(content) + } else { + // default to generic ChatMessage + return new ChatMessage(content, role) + } + }) + + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } catch (error) { + console.error('Error getting session: ', error) + return [] + } } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index a02df3ea..baf4ea6b 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -1,15 +1,9 @@ -import { INode, INodeData, INodeParams, ICommonObject, IMessage, MessageType, FlowiseMemory, MemoryMethods } from '../../../src/Interface' -import { - convertBaseMessagetoIMessage, - getBaseClasses, - getCredentialData, - getCredentialParam, - serializeChatHistory -} from '../../../src/utils' +import { Redis } from 'ioredis' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis' import { mapStoredMessageToChatMessage, BaseMessage, AIMessage, HumanMessage } from 'langchain/schema' -import { Redis } from 'ioredis' +import { INode, INodeData, INodeParams, ICommonObject, MessageType, IMessage, MemoryMethods, FlowiseMemory } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' class RedisBackedChatMemory_Memory implements INode { label: string @@ -44,7 +38,8 @@ class RedisBackedChatMemory_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -78,47 +73,19 @@ class RedisBackedChatMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return await initalizeRedis(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeRedis(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Redis memory session ${sessionId ? sessionId : chatId}`) - await redis.clear() - options.logger.info(`Successfully cleared Redis memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const redis = await initalizeRedis(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await redis.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const sessionTTL = nodeData.inputs?.sessionTTL as number const memoryKey = nodeData.inputs?.memoryKey as string + const sessionId = nodeData.inputs?.sessionId as string const windowSize = nodeData.inputs?.windowSize as number - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - 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) let client: Redis + if (!redisUrl || redisUrl === '') { const username = getCredentialParam('redisCacheUser', credentialData, nodeData) const password = getCredentialParam('redisCachePwd', credentialData, nodeData) @@ -153,7 +120,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const redisChatMessageHistory = new RedisChatMessageHistory(obj) - redisChatMessageHistory.getMessages = async (): Promise => { + /*redisChatMessageHistory.getMessages = async (): Promise => { const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, windowSize ? -windowSize : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) return orderedMessages.map(mapStoredMessageToChatMessage) @@ -169,44 +136,45 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom redisChatMessageHistory.clear = async (): Promise => { await client.del((redisChatMessageHistory as any).sessionId) - } + }*/ const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, + windowSize, redisClient: client }) + return memory } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean redisClient: Redis sessionId: string + windowSize?: number } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false sessionId = '' redisClient: Redis + windowSize?: number constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.redisClient = fields.redisClient + this.windowSize = fields.windowSize } - async getChatMessages(overrideSessionId = '', returnBaseMessage = false): Promise { + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.redisClient) return [] const id = overrideSessionId ?? this.sessionId - const rawStoredMessages = await this.redisClient.lrange(id, 0, -1) + const rawStoredMessages = await this.redisClient.lrange(id, this.windowSize ? this.windowSize * -1 : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) const baseMessages = orderedMessages.map(mapStoredMessageToChatMessage) - return returnBaseMessage ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { @@ -236,10 +204,6 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { 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 c3f97123..3d7f6dbf 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -3,13 +3,7 @@ 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 { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' class UpstashRedisBackedChatMemory_Memory implements INode { @@ -51,7 +45,8 @@ class UpstashRedisBackedChatMemory_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -70,40 +65,12 @@ class UpstashRedisBackedChatMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return initalizeUpstashRedis(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeUpstashRedis(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Upstash Redis memory session ${sessionId ? sessionId : chatId}`) - await redis.clear() - options.logger.info(`Successfully cleared Upstash Redis memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const redis = await initalizeUpstashRedis(nodeData, options) - const key = 'chat_history' - const memoryResult = await redis.loadMemoryVariables({}) - return serializeChatHistory(memoryResult[key]) - } - } } const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject): Promise => { const baseURL = nodeData.inputs?.baseURL as string const sessionTTL = nodeData.inputs?.sessionTTL as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const upstashRestToken = getCredentialParam('upstashRestToken', credentialData, nodeData) @@ -122,7 +89,6 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject const memory = new BufferMemoryExtended({ memoryKey: 'chat_history', chatHistory: redisChatMessageHistory, - isSessionIdUsingChatMessageId, sessionId, redisClient: client }) @@ -131,19 +97,16 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject } interface BufferMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean redisClient: Redis sessionId: string } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false sessionId = '' redisClient: Redis constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.sessionId = fields.sessionId this.redisClient = fields.redisClient } @@ -186,10 +149,6 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { await this.redisClient.del(id) await this.clear() } - - async resumeMessages(): Promise { - return - } } module.exports = { nodeClass: UpstashRedisBackedChatMemory_Memory } diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index 4dda76df..597eee8a 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -2,7 +2,7 @@ import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } f import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep' import { ICommonObject } from '../../../src' -import { InputValues, MemoryVariables, OutputValues, getBufferString } from 'langchain/memory' +import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory' import { BaseMessage } from 'langchain/schema' class ZepMemory_Memory implements INode { @@ -55,10 +55,9 @@ class ZepMemory_Memory implements INode { label: 'Size', name: 'k', type: 'number', - placeholder: '10', + default: '10', description: 'Window of size k to surface the last k back-and-forth to use as memory.', - additionalParams: true, - optional: true + additionalParams: true }, { label: 'AI Prefix', @@ -101,27 +100,6 @@ class ZepMemory_Memory implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { return await initalizeZep(nodeData, options) } - - //@ts-ignore - memoryMethods = { - async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { - const zep = await initalizeZep(nodeData, options) - const sessionId = nodeData.inputs?.sessionId as string - const chatId = options?.chatId as string - options.logger.info(`Clearing Zep memory session ${sessionId ? sessionId : chatId}`) - await zep.clear() - options.logger.info(`Successfully cleared Zep memory session ${sessionId ? sessionId : chatId}`) - }, - async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise { - const memoryKey = nodeData.inputs?.memoryKey as string - const aiPrefix = nodeData.inputs?.aiPrefix as string - const humanPrefix = nodeData.inputs?.humanPrefix as string - const zep = await initalizeZep(nodeData, options) - const key = memoryKey ?? 'chat_history' - const memoryResult = await zep.loadMemoryVariables({}) - return getBufferString(memoryResult[key], humanPrefix, aiPrefix) - } - } } const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promise => { @@ -131,30 +109,19 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis const memoryKey = nodeData.inputs?.memoryKey as string const inputKey = nodeData.inputs?.inputKey as string const k = nodeData.inputs?.k as string - const chatId = options?.chatId as string - - let isSessionIdUsingChatMessageId = false - let sessionId = '' - - if (!nodeData.inputs?.sessionId && chatId) { - isSessionIdUsingChatMessageId = true - sessionId = chatId - } else { - sessionId = nodeData.inputs?.sessionId - } + const sessionId = nodeData.inputs?.sessionId as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const apiKey = getCredentialParam('apiKey', credentialData, nodeData) const obj: ZepMemoryInput & ZepMemoryExtendedInput = { baseURL, - sessionId, aiPrefix, humanPrefix, returnMessages: true, memoryKey, inputKey, - isSessionIdUsingChatMessageId, + sessionId, k: k ? parseInt(k, 10) : undefined } if (apiKey) obj.apiKey = apiKey @@ -163,17 +130,14 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis } interface ZepMemoryExtendedInput { - isSessionIdUsingChatMessageId: boolean k?: number } class ZepMemoryExtended extends ZepMemory implements MemoryMethods { - isSessionIdUsingChatMessageId? = false lastN?: number constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) { super(fields) - this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId this.lastN = fields.k } diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index 6ffcc0e2..a983d0d9 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, input: string, options: ICommonObject): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const selectedToolId = nodeData.inputs?.selectedTool as string const customToolFunc = nodeData.inputs?.customToolFunc as string @@ -99,11 +99,7 @@ class CustomTool_Tools implements INode { } } - const flow = { - chatId: options.chatId, // id is uppercase (I) - chatflowId: options.chatflowid, // id is lowercase (i) - input - } + const flow = { chatflowId: options.chatflowid } let dynamicStructuredTool = new DynamicStructuredTool(obj) dynamicStructuredTool.setVariables(variables) diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 338b0ae9..b543aefa 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -55,7 +55,12 @@ export class DynamicStructuredTool< this.schema = fields.schema } - async call(arg: z.output, configArg?: RunnableConfig | Callbacks, tags?: string[], overrideSessionId?: string): Promise { + async call( + arg: z.output, + configArg?: RunnableConfig | Callbacks, + tags?: string[], + flowConfig?: { sessionId?: string; chatId?: string; input?: string } + ): Promise { const config = parseCallbackConfigArg(configArg) if (config.runName === undefined) { config.runName = this.name @@ -86,7 +91,7 @@ export class DynamicStructuredTool< ) let result try { - result = await this._call(parsed, runManager, overrideSessionId) + result = await this._call(parsed, runManager, flowConfig) } catch (e) { await runManager?.handleToolError(e) throw e @@ -95,7 +100,11 @@ export class DynamicStructuredTool< return result } - protected async _call(arg: z.output, _?: CallbackManagerForToolRun, overrideSessionId?: string): Promise { + protected async _call( + arg: z.output, + _?: CallbackManagerForToolRun, + flowConfig?: { sessionId?: string; chatId?: string; input?: string } + ): Promise { let sandbox: any = {} if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { @@ -126,7 +135,7 @@ export class DynamicStructuredTool< // inject flow properties if (this.flowObj) { - sandbox['$flow'] = { ...this.flowObj, sessionId: overrideSessionId } + sandbox['$flow'] = { ...this.flowObj, ...flowConfig } } const defaultAllowBuiltInDep = [ diff --git a/packages/components/package.json b/packages/components/package.json index a2565430..a77d91e4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -46,6 +46,7 @@ "dotenv": "^16.0.0", "express": "^4.17.3", "faiss-node": "^0.2.2", + "fast-json-patch": "^3.1.1", "form-data": "^4.0.0", "google-auth-library": "^9.0.0", "graphql": "^16.6.0", diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 9da26f82..d74ba1b4 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -108,10 +108,6 @@ export interface INode extends INodeProperties { search: (nodeData: INodeData, options?: ICommonObject) => Promise delete: (nodeData: INodeData, options?: ICommonObject) => Promise } - memoryMethods?: { - clearSessionMemory: (nodeData: INodeData, options?: ICommonObject) => Promise - getChatMessages: (nodeData: INodeData, options?: ICommonObject) => Promise - } init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise } @@ -204,29 +200,37 @@ import { BaseMessage } from 'langchain/schema' import { BufferMemory, BufferWindowMemory, ConversationSummaryMemory } from 'langchain/memory' export interface MemoryMethods { - getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean): Promise + getChatMessages(overrideSessionId?: string, returnBaseMessages?: boolean, prevHistory?: IMessage[]): 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 getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): 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 getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): 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 getChatMessages( + overrideSessionId?: string, + returnBaseMessages?: boolean, + prevHistory?: IMessage[] + ): 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/components/src/agents.ts b/packages/components/src/agents.ts new file mode 100644 index 00000000..e30a0c43 --- /dev/null +++ b/packages/components/src/agents.ts @@ -0,0 +1,615 @@ +import { AgentExecutorInput, BaseSingleActionAgent, BaseMultiActionAgent, RunnableAgent, StoppingMethod } from 'langchain/agents' +import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' +import { OutputParserException } from 'langchain/schema/output_parser' +import { CallbackManager, CallbackManagerForChainRun, Callbacks } from 'langchain/callbacks' +import { ToolInputParsingException, Tool } from '@langchain/core/tools' +import { Runnable } from 'langchain/schema/runnable' +import { BaseChain, SerializedLLMChain } from 'langchain/chains' +import { Serializable } from '@langchain/core/load/serializable' + +type AgentExecutorOutput = ChainValues + +interface AgentExecutorIteratorInput { + agentExecutor: AgentExecutor + inputs: Record + callbacks?: Callbacks + tags?: string[] + metadata?: Record + runName?: string + runManager?: CallbackManagerForChainRun +} + +//TODO: stream tools back +export class AgentExecutorIterator extends Serializable implements AgentExecutorIteratorInput { + lc_namespace = ['langchain', 'agents', 'executor_iterator'] + + agentExecutor: AgentExecutor + + inputs: Record + + callbacks: Callbacks + + tags: string[] | undefined + + metadata: Record | undefined + + runName: string | undefined + + private _finalOutputs: Record | undefined + + get finalOutputs(): Record | undefined { + return this._finalOutputs + } + + /** Intended to be used as a setter method, needs to be async. */ + async setFinalOutputs(value: Record | undefined) { + this._finalOutputs = undefined + if (value) { + const preparedOutputs: Record = await this.agentExecutor.prepOutputs(this.inputs, value, true) + this._finalOutputs = preparedOutputs + } + } + + runManager: CallbackManagerForChainRun | undefined + + intermediateSteps: AgentStep[] = [] + + iterations = 0 + + get nameToToolMap(): Record { + const toolMap = this.agentExecutor.tools.map((tool) => ({ + [tool.name]: tool + })) + return Object.assign({}, ...toolMap) + } + + constructor(fields: AgentExecutorIteratorInput) { + super(fields) + this.agentExecutor = fields.agentExecutor + this.inputs = fields.inputs + this.tags = fields.tags + this.metadata = fields.metadata + this.runName = fields.runName + this.runManager = fields.runManager + } + + /** + * Reset the iterator to its initial state, clearing intermediate steps, + * iterations, and the final output. + */ + reset(): void { + this.intermediateSteps = [] + this.iterations = 0 + this._finalOutputs = undefined + } + + updateIterations(): void { + this.iterations += 1 + } + + async *streamIterator() { + this.reset() + + // Loop to handle iteration + while (true) { + try { + if (this.iterations === 0) { + await this.onFirstStep() + } + + const result = await this._callNext() + yield result + } catch (e: any) { + if ('message' in e && e.message.startsWith('Final outputs already reached: ')) { + if (!this.finalOutputs) { + throw e + } + return this.finalOutputs + } + if (this.runManager) { + await this.runManager.handleChainError(e) + } + throw e + } + } + } + + /** + * Perform any necessary setup for the first step + * of the asynchronous iterator. + */ + async onFirstStep(): Promise { + if (this.iterations === 0) { + const callbackManager = await CallbackManager.configure( + this.callbacks, + this.agentExecutor.callbacks, + this.tags, + this.agentExecutor.tags, + this.metadata, + this.agentExecutor.metadata, + { + verbose: this.agentExecutor.verbose + } + ) + this.runManager = await callbackManager?.handleChainStart( + this.agentExecutor.toJSON(), + this.inputs, + undefined, + undefined, + this.tags, + this.metadata, + this.runName + ) + } + } + + /** + * Execute the next step in the chain using the + * AgentExecutor's _takeNextStep method. + */ + async _executeNextStep(runManager?: CallbackManagerForChainRun): Promise { + return this.agentExecutor._takeNextStep(this.nameToToolMap, this.inputs, this.intermediateSteps, runManager) + } + + /** + * Process the output of the next step, + * handling AgentFinish and tool return cases. + */ + async _processNextStepOutput( + nextStepOutput: AgentFinish | AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise> { + if ('returnValues' in nextStepOutput) { + const output = await this.agentExecutor._return(nextStepOutput as AgentFinish, this.intermediateSteps, runManager) + if (this.runManager) { + await this.runManager.handleChainEnd(output) + } + await this.setFinalOutputs(output) + return output + } + + this.intermediateSteps = this.intermediateSteps.concat(nextStepOutput as AgentStep[]) + + let output: Record = {} + if (Array.isArray(nextStepOutput) && nextStepOutput.length === 1) { + const nextStep = nextStepOutput[0] + const toolReturn = await this.agentExecutor._getToolReturn(nextStep) + if (toolReturn) { + output = await this.agentExecutor._return(toolReturn, this.intermediateSteps, runManager) + if (this.runManager) { + await this.runManager.handleChainEnd(output) + } + await this.setFinalOutputs(output) + } + } + output = { intermediateSteps: nextStepOutput as AgentStep[] } + return output + } + + async _stop(): Promise> { + const output = await this.agentExecutor.agent.returnStoppedResponse( + this.agentExecutor.earlyStoppingMethod, + this.intermediateSteps, + this.inputs + ) + const returnedOutput = await this.agentExecutor._return(output, this.intermediateSteps, this.runManager) + await this.setFinalOutputs(returnedOutput) + return returnedOutput + } + + async _callNext(): Promise> { + // final output already reached: stopiteration (final output) + if (this.finalOutputs) { + throw new Error(`Final outputs already reached: ${JSON.stringify(this.finalOutputs, null, 2)}`) + } + // timeout/max iterations: stopiteration (stopped response) + if (!this.agentExecutor.shouldContinueGetter(this.iterations)) { + return this._stop() + } + const nextStepOutput = await this._executeNextStep(this.runManager) + const output = await this._processNextStepOutput(nextStepOutput, this.runManager) + this.updateIterations() + return output + } +} + +export class AgentExecutor extends BaseChain { + static lc_name() { + return 'AgentExecutor' + } + + get lc_namespace() { + return ['langchain', 'agents', 'executor'] + } + + agent: BaseSingleActionAgent | BaseMultiActionAgent + + tools: this['agent']['ToolType'][] + + returnIntermediateSteps = false + + maxIterations?: number = 15 + + earlyStoppingMethod: StoppingMethod = 'force' + + sessionId?: string + + chatId?: string + + input?: string + + /** + * How to handle errors raised by the agent's output parser. + Defaults to `False`, which raises the error. + + If `true`, the error will be sent back to the LLM as an observation. + If a string, the string itself will be sent to the LLM as an observation. + If a callable function, the function will be called with the exception + as an argument, and the result of that function will be passed to the agent + as an observation. + */ + handleParsingErrors: boolean | string | ((e: OutputParserException | ToolInputParsingException) => string) = false + + get inputKeys() { + return this.agent.inputKeys + } + + get outputKeys() { + return this.agent.returnValues + } + + constructor(input: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }) { + let agent: BaseSingleActionAgent | BaseMultiActionAgent + if (Runnable.isRunnable(input.agent)) { + agent = new RunnableAgent({ runnable: input.agent }) + } else { + agent = input.agent + } + + super(input) + this.agent = agent + this.tools = input.tools + this.handleParsingErrors = input.handleParsingErrors ?? this.handleParsingErrors + /* Getting rid of this because RunnableAgent doesnt allow return direct + if (this.agent._agentActionType() === "multi") { + for (const tool of this.tools) { + if (tool.returnDirect) { + throw new Error( + `Tool with return direct ${tool.name} not supported for multi-action agent.` + ); + } + } + }*/ + this.returnIntermediateSteps = input.returnIntermediateSteps ?? this.returnIntermediateSteps + this.maxIterations = input.maxIterations ?? this.maxIterations + this.earlyStoppingMethod = input.earlyStoppingMethod ?? this.earlyStoppingMethod + this.sessionId = input.sessionId + this.chatId = input.chatId + this.input = input.input + } + + static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }): AgentExecutor { + const newInstance = new AgentExecutor(fields) + if (fields.sessionId) newInstance.sessionId = fields.sessionId + if (fields.chatId) newInstance.chatId = fields.chatId + if (fields.input) newInstance.input = fields.input + return newInstance + } + + get shouldContinueGetter() { + return this.shouldContinue.bind(this) + } + + /** + * Method that checks if the agent execution should continue based on the + * number of iterations. + * @param iterations The current number of iterations. + * @returns A boolean indicating whether the agent execution should continue. + */ + private shouldContinue(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.shouldContinue(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, chatId, input as parameter + * Tool Call Parameters: + * - arg: z.output + * - configArg?: RunnableConfig | Callbacks + * - tags?: string[] + * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } + */ + observation = tool + ? // @ts-ignore + await tool.call(action.toolInput, runManager?.getChild(), undefined, { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + }) + : `${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, chatId, input as parameter + * Tool Call Parameters: + * - arg: z.output + * - configArg?: RunnableConfig | Callbacks + * - tags?: string[] + * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } + */ + // @ts-ignore + observation = await tool.call(agentAction.toolInput, runManager?.getChild(), undefined, { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + }) + } 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 + } + + async _return( + output: AgentFinish, + intermediateSteps: AgentStep[], + runManager?: CallbackManagerForChainRun + ): Promise { + if (runManager) { + await runManager.handleAgentEnd(output) + } + const finalOutput: Record = output.returnValues + if (this.returnIntermediateSteps) { + finalOutput.intermediateSteps = intermediateSteps + } + return finalOutput + } + + async _getToolReturn(nextStepOutput: AgentStep): Promise { + const { action, observation } = nextStepOutput + const nameToolMap = Object.fromEntries(this.tools.map((t) => [t.name.toLowerCase(), t])) + const [returnValueKey = 'output'] = this.agent.returnValues + // Invalid tools won't be in the map, so we return False. + if (action.tool in nameToolMap) { + if (nameToolMap[action.tool].returnDirect) { + return { + returnValues: { [returnValueKey]: observation }, + log: '' + } + } + } + return null + } + + _returnStoppedResponse(earlyStoppingMethod: StoppingMethod) { + if (earlyStoppingMethod === 'force') { + return { + returnValues: { + output: 'Agent stopped due to iteration limit or time limit.' + }, + log: '' + } as AgentFinish + } + throw new Error(`Got unsupported early_stopping_method: ${earlyStoppingMethod}`) + } + + async *_streamIterator(inputs: Record): AsyncGenerator { + const agentExecutorIterator = new AgentExecutorIterator({ + inputs, + agentExecutor: this, + metadata: this.metadata, + tags: this.tags, + callbacks: this.callbacks + }) + const iterator = agentExecutorIterator.streamIterator() + for await (const step of iterator) { + if (!step) { + continue + } + yield step + } + } + + _chainType() { + return 'agent_executor' as const + } + + serialize(): SerializedLLMChain { + throw new Error('Cannot serialize an AgentExecutor') + } +} + +class ExceptionTool extends Tool { + name = '_Exception' + + description = 'Exception tool' + + async _call(query: string) { + return query + } +} + +export 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)] + } + }) diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index 93270848..eabc8f2e 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -936,7 +936,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Chat with a Podcast.json b/packages/server/marketplaces/chatflows/Chat with a Podcast.json index 2a9e05b5..0a5d4ac6 100644 --- a/packages/server/marketplaces/chatflows/Chat with a Podcast.json +++ b/packages/server/marketplaces/chatflows/Chat with a Podcast.json @@ -13,7 +13,7 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], @@ -28,47 +28,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -89,9 +78,8 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{memoryVectorStore_0.data.instance}}", "memory": "", - "returnSourceDocuments": "", - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { @@ -625,9 +613,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 0ead3dd8..39d4d400 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -90,7 +90,7 @@ ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationChain_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 8994594a..b27d3886 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -354,7 +354,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index 5c55d833..e2fd6421 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -249,10 +249,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -264,47 +264,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -325,16 +314,15 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{pinecone_0.data.instance}}", "memory": "", - "returnSourceDocuments": "", - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -704,9 +692,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json index ac84cf56..16f70801 100644 --- a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json +++ b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json @@ -156,9 +156,9 @@ "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", "name": "conversationalRetrievalQAChain", - "version": 1, + "version": 2, "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -170,47 +170,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -232,15 +221,15 @@ "vectorStoreRetriever": "{{memoryVectorStore_0.data.instance}}", "memory": "", "returnSourceDocuments": true, - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -668,9 +657,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index e24ad7ca..6f78cb05 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -83,10 +83,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -98,47 +98,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -158,14 +147,16 @@ "inputs": { "model": "{{chatOllama_0.data.instance}}", "vectorStoreRetriever": "{{faiss_0.data.instance}}", - "memory": "" + "memory": "", + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -649,9 +640,9 @@ "source": "chatOllama_0", "sourceHandle": "chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOllama_0-chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOllama_0-chatOllama_0-output-chatOllama-ChatOllama|SimpleChatModel|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index c39f746a..cf0fa4d4 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -13,10 +13,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -28,47 +28,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -89,14 +78,16 @@ "model": "{{chatOpenAI_0.data.instance}}", "vectorStoreRetriever": "{{qdrant_0.data.instance}}", "memory": "{{ZepMemory_0.data.instance}}", - "returnSourceDocuments": true + "returnSourceDocuments": true, + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -232,7 +223,7 @@ "label": "Session Id", "name": "sessionId", "type": "string", - "description": "if empty, chatId will be used automatically", + "description": "If not specified, a random id will be used. Learn more", "default": "", "additionalParams": true, "optional": true, @@ -709,9 +700,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index 9865ae70..abd85d36 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -249,10 +249,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "BaseLangChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -264,47 +264,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -323,14 +312,16 @@ ], "inputs": { "model": "{{chatOpenAI_0.data.instance}}", - "vectorStoreRetriever": "{{pinecone_0.data.instance}}" + "vectorStoreRetriever": "{{pinecone_0.data.instance}}", + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|BaseLangChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain | BaseLangChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -763,9 +754,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index a2a807cd..d53cb55e 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -1567,7 +1567,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index 2dac3823..2322136c 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -262,7 +262,7 @@ ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationChain_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index d9f9fb49..6f0edeea 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -190,7 +190,7 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], @@ -205,47 +205,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -267,8 +256,8 @@ "vectorStoreRetriever": "{{vectara_0.data.instance}}", "memory": "", "returnSourceDocuments": true, - "systemMessagePrompt": "", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { @@ -427,9 +416,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 0547366a..d905b54b 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -578,7 +578,7 @@ "id": "conversationalAgent_0-input-tools-Tool" }, { - "label": "Language Model", + "label": "Chat Model", "name": "model", "type": "BaseChatModel", "id": "conversationalAgent_0-input-model-BaseChatModel" diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index 9b1119b9..1b1d8de6 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -162,10 +162,10 @@ "data": { "id": "conversationalRetrievalQAChain_0", "label": "Conversational Retrieval QA Chain", - "version": 1, + "version": 2, "name": "conversationalRetrievalQAChain", "type": "ConversationalRetrievalQAChain", - "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain"], + "baseClasses": ["ConversationalRetrievalQAChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Document QA - built on RetrievalQAChain to provide a chat history component", "inputParams": [ @@ -177,47 +177,36 @@ "id": "conversationalRetrievalQAChain_0-input-returnSourceDocuments-boolean" }, { - "label": "System Message", - "name": "systemMessagePrompt", + "label": "Rephrase Prompt", + "name": "rephrasePrompt", "type": "string", + "description": "Using previous chat history, rephrase question into a standalone question", + "warning": "Prompt must include input variables: {chat_history} and {question}", "rows": 4, "additionalParams": true, "optional": true, - "placeholder": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given info. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Refuse to answer any question not about the info. Never break character.", - "id": "conversationalRetrievalQAChain_0-input-systemMessagePrompt-string" + "default": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "id": "conversationalRetrievalQAChain_0-input-rephrasePrompt-string" }, { - "label": "Chain Option", - "name": "chainOption", - "type": "options", - "options": [ - { - "label": "MapReduceDocumentsChain", - "name": "map_reduce", - "description": "Suitable for QA tasks over larger documents and can run the preprocessing step in parallel, reducing the running time" - }, - { - "label": "RefineDocumentsChain", - "name": "refine", - "description": "Suitable for QA tasks over a large number of documents." - }, - { - "label": "StuffDocumentsChain", - "name": "stuff", - "description": "Suitable for QA tasks over a small number of documents." - } - ], + "label": "Response Prompt", + "name": "responsePrompt", + "type": "string", + "description": "Taking the rephrased question, search for answer from the provided context", + "warning": "Prompt must include input variable: {context}", + "rows": 4, "additionalParams": true, "optional": true, - "id": "conversationalRetrievalQAChain_0-input-chainOption-options" + "default": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.", + "id": "conversationalRetrievalQAChain_0-input-responsePrompt-string" } ], "inputAnchors": [ { - "label": "Language Model", + "label": "Chat Model", "name": "model", - "type": "BaseLanguageModel", - "id": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel" + "type": "BaseChatModel", + "id": "conversationalRetrievalQAChain_0-input-model-BaseChatModel" }, { "label": "Vector Store Retriever", @@ -239,15 +228,15 @@ "vectorStoreRetriever": "{{pinecone_0.data.instance}}", "memory": "{{RedisBackedChatMemory_0.data.instance}}", "returnSourceDocuments": true, - "systemMessagePrompt": "I want you to act as a document that I am having a conversation with. Your name is \"AI Assistant\". You will provide me with answers from the given context. If the answer is not included, say exactly \"Hmm, I am not sure.\" and stop after that. Do not make up any information that is not in the context. Refuse to answer any question not about the info. Never break character.", - "chainOption": "" + "rephrasePrompt": "Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.\n\nChat History:\n{chat_history}\nFollow Up Input: {question}\nStandalone Question:", + "responsePrompt": "You are a helpful assistant. Using the provided context, answer the user's question to the best of your ability using the resources provided.\nIf there is nothing in the context relevant to the question at hand, just say \"Hmm, I'm not sure.\" Don't try to make up an answer.\n------------\n{context}\n------------\nREMEMBER: If there is no relevant information within the context, just say \"Hmm, I'm not sure.\" Don't try to make up an answer." }, "outputAnchors": [ { - "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain", + "id": "conversationalRetrievalQAChain_0-output-conversationalRetrievalQAChain-ConversationalRetrievalQAChain|BaseChain|Runnable", "name": "conversationalRetrievalQAChain", "label": "ConversationalRetrievalQAChain", - "type": "ConversationalRetrievalQAChain | BaseChain" + "type": "ConversationalRetrievalQAChain | BaseChain | Runnable" } ], "outputs": {}, @@ -589,7 +578,7 @@ "label": "Session Id", "name": "sessionId", "type": "string", - "description": "If not specified, the first CHAT_MESSAGE_ID will be used as sessionId", + "description": "If not specified, a random id will be used. Learn more", "default": "", "additionalParams": true, "optional": true, @@ -772,9 +761,9 @@ "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationalRetrievalQAChain_0", - "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "targetHandle": "conversationalRetrievalQAChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseLanguageModel", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationalRetrievalQAChain_0-conversationalRetrievalQAChain_0-input-model-BaseChatModel", "data": { "label": "" } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b6f59191..b446445e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -20,7 +20,6 @@ import { ICredentialReturnResponse, chatType, IChatMessage, - IReactFlowEdge, IDepthQueue, INodeDirectedGraph } from './Interface' @@ -39,14 +38,14 @@ import { databaseEntities, transformToCredentialEntity, decryptCredentialData, - clearAllSessionMemory, replaceInputsWithConfig, getEncryptionKey, - checkMemorySessionId, - clearSessionMemoryFromViewMessageDialog, + getMemorySessionId, getUserHome, - replaceChatHistory, - getAllConnectedNodes + getSessionChatHistory, + getAllConnectedNodes, + clearSessionMemory, + findMemoryNode } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -533,17 +532,18 @@ export class App { const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes - if (isClearFromViewMessageDialog) { - await clearSessionMemoryFromViewMessageDialog( + try { + await clearSessionMemory( nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId, - memoryType + memoryType, + isClearFromViewMessageDialog ) - } else { - await clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId) + } catch (e) { + return res.status(500).send('Error clearing chat messages') } const deleteOptions: FindOptionsWhere = { chatflowid, chatId } @@ -1398,26 +1398,6 @@ export class App { return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) } - /** - * Method that find memory label that is connected within chatflow - * In a chatflow, there should only be 1 memory node - * @param {IReactFlowNode[]} nodes - * @param {IReactFlowEdge[]} edges - * @returns {string | undefined} - */ - findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined { - const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') - const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) - - for (const edge of edges) { - if (memoryNodeIds.includes(edge.source)) { - const memoryNode = nodes.find((node) => node.data.id === edge.source) - return memoryNode - } - } - return undefined - } - async upsertVector(req: Request, res: Response, isInternal: boolean = false) { try { const chatflowid = req.params.id @@ -1586,7 +1566,6 @@ export class App { * - Still in sync (i.e the flow has not been modified since) * - Existing overrideConfig and new overrideConfig are the same * - Flow doesn't start with/contain nodes that depend on incomingInput.question - * - Its not an Upsert request * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath ***/ const isFlowReusable = () => { @@ -1640,22 +1619,28 @@ export class App { isStreamValid = isFlowValidForStream(nodes, endingNodeData) } - let chatHistory: IMessage[] | string = incomingInput.history + let chatHistory: IMessage[] = incomingInput.history ?? [] - // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory + // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node for (const endingNode of endingNodes) { const endingNodeData = endingNode.data + if (!endingNodeData.inputs?.memory) continue - if ( - endingNodeData.inputs?.memory && - !incomingInput.history && - (incomingInput.chatId || incomingInput.overrideConfig?.sessionId) - ) { - const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') - const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) - if (memoryNode) { - chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) - } + + const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') + const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) + + if (!memoryNode) continue + + if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { + chatHistory = await getSessionChatHistory( + memoryNode, + this.nodesPool.componentNodes, + incomingInput, + this.AppDataSource, + databaseEntities, + logger + ) } } @@ -1714,16 +1699,11 @@ export class App { logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - let sessionId = undefined - if (nodeToExecuteData.instance) sessionId = checkMemorySessionId(nodeToExecuteData.instance, chatId) - - const memoryNode = this.findMemoryLabel(nodes, edges) + const memoryNode = findMemoryNode(nodes, edges) const memoryType = memoryNode?.data.label - let chatHistory: IMessage[] | string = incomingInput.history - if (memoryNode && !incomingInput.history && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { - chatHistory = await replaceChatHistory(memoryNode, incomingInput, this.AppDataSource, databaseEntities, logger) - } + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) @@ -1731,24 +1711,24 @@ export class App { let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, chatflowid, - chatHistory, - socketIO, - socketIOClientId: incomingInput.socketIOClientId, + chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, databaseEntities, analytic: chatflow.analytic, - chatId + socketIO, + socketIOClientId: incomingInput.socketIOClientId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, chatflowid, - chatHistory, + chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, databaseEntities, - analytic: chatflow.analytic, - chatId + analytic: chatflow.analytic }) result = typeof result === 'string' ? { text: result } : result diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 9c2d1d79..dafe612c 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -26,7 +26,8 @@ import { getEncryptionKeyPath, ICommonObject, IDatabaseEntity, - IMessage + IMessage, + FlowiseMemory } from 'flowise-components' import { randomBytes } from 'crypto' import { AES, enc } from 'crypto-js' @@ -270,7 +271,7 @@ export const buildLangchain = async ( depthQueue: IDepthQueue, componentNodes: IComponentNodes, question: string, - chatHistory: IMessage[] | string, + chatHistory: IMessage[], chatId: string, chatflowid: string, appDataSource: DataSource, @@ -317,9 +318,10 @@ export const buildLangchain = async ( await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, { chatId, chatflowid, + chatHistory, + logger, appDataSource, databaseEntities, - logger, cachePool, dynamicVariables }) @@ -330,9 +332,10 @@ export const buildLangchain = async ( let outputResult = await newNodeInstance.init(reactFlowNodeData, question, { chatId, chatflowid, + chatHistory, + logger, appDataSource, databaseEntities, - logger, cachePool, dynamicVariables }) @@ -424,66 +427,52 @@ export const buildLangchain = async ( } /** - * Clear all session memories on the canvas - * @param {IReactFlowNode[]} reactFlowNodes - * @param {IComponentNodes} componentNodes - * @param {string} chatId - * @param {DataSource} appDataSource - * @param {string} sessionId - */ -export const clearAllSessionMemory = async ( - reactFlowNodes: IReactFlowNode[], - componentNodes: IComponentNodes, - chatId: string, - appDataSource: DataSource, - sessionId?: string -) => { - for (const node of reactFlowNodes) { - if (node.data.category !== 'Memory' && node.data.type !== 'OpenAIAssistant') continue - const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const newNodeInstance = new nodeModule.nodeClass() - - if (sessionId && node.data.inputs) { - node.data.inputs.sessionId = sessionId - } - - if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.clearSessionMemory) { - await newNodeInstance.memoryMethods.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) - } - } -} - -/** - * Clear specific session memory from View Message Dialog UI + * Clear session memories * @param {IReactFlowNode[]} reactFlowNodes * @param {IComponentNodes} componentNodes * @param {string} chatId * @param {DataSource} appDataSource * @param {string} sessionId * @param {string} memoryType + * @param {string} isClearFromViewMessageDialog */ -export const clearSessionMemoryFromViewMessageDialog = async ( +export const clearSessionMemory = async ( reactFlowNodes: IReactFlowNode[], componentNodes: IComponentNodes, chatId: string, appDataSource: DataSource, sessionId?: string, - memoryType?: string + memoryType?: string, + isClearFromViewMessageDialog?: string ) => { - if (!sessionId) return for (const node of reactFlowNodes) { if (node.data.category !== 'Memory' && node.data.type !== 'OpenAIAssistant') continue - if (memoryType && node.data.label !== memoryType) continue + + // Only clear specific session memory from View Message Dialog UI + if (isClearFromViewMessageDialog && memoryType && node.data.label !== memoryType) continue + const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + const options: ICommonObject = { chatId, appDataSource, databaseEntities, logger } - if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId - - if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.clearSessionMemory) { - await newNodeInstance.memoryMethods.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) - return + // SessionId always take priority first because it is the sessionId used for 3rd party memory node + if (sessionId && node.data.inputs) { + if (node.data.type === 'OpenAIAssistant') { + await newNodeInstance.clearChatMessages(node.data, options, { type: 'threadId', id: sessionId }) + } else { + node.data.inputs.sessionId = sessionId + const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options) + await initializedInstance.clearChatMessages(sessionId) + } + } else if (chatId && node.data.inputs) { + if (node.data.type === 'OpenAIAssistant') { + await newNodeInstance.clearChatMessages(node.data, options, { type: 'chatId', id: chatId }) + } else { + node.data.inputs.sessionId = chatId + const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options) + await initializedInstance.clearChatMessages(chatId) + } } } } @@ -500,7 +489,7 @@ export const getVariableValue = ( paramValue: string, reactFlowNodes: IReactFlowNode[], question: string, - chatHistory: IMessage[] | string, + chatHistory: IMessage[], isAcceptVariable = false ) => { let returnVal = paramValue @@ -533,10 +522,7 @@ export const getVariableValue = ( } if (isAcceptVariable && variableFullPath === CHAT_HISTORY_VAR_PREFIX) { - variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters( - typeof chatHistory === 'string' ? chatHistory : convertChatHistoryToText(chatHistory), - false - ) + variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(convertChatHistoryToText(chatHistory), false) } // Split by first occurrence of '.' to get just nodeId @@ -583,7 +569,7 @@ export const resolveVariables = ( reactFlowNodeData: INodeData, reactFlowNodes: IReactFlowNode[], question: string, - chatHistory: IMessage[] | string + chatHistory: IMessage[] ): INodeData => { let flowNodeData = cloneDeep(reactFlowNodeData) const types = 'inputs' @@ -970,21 +956,43 @@ export const redactCredentialWithPasswordType = ( } /** - * Replace sessionId with new chatId - * Ex: after clear chat history, use the new chatId as sessionId + * Get sessionId + * Hierarchy of sessionId (top down) + * API/Embed: + * (1) Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' } + * (2) Provided in API body - incomingInput.chatId + * + * API/Embed + UI: + * (3) Hard-coded sessionId in UI + * (4) Not specified on UI nor API, default to chatId * @param {any} instance + * @param {IncomingInput} incomingInput * @param {string} chatId */ -export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => { - if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { - instance.memory.sessionId = chatId - instance.memory.chatHistory.sessionId = chatId +export const getMemorySessionId = ( + memoryNode: IReactFlowNode, + incomingInput: IncomingInput, + chatId: string, + isInternal: boolean +): string | undefined => { + if (!isInternal) { + // Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' } + if (incomingInput.overrideConfig?.sessionId) { + return incomingInput.overrideConfig?.sessionId + } + // Provided in API body - incomingInput.chatId + if (incomingInput.chatId) { + return incomingInput.chatId + } } - if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId - else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId) - return instance.memory.chatHistory.sessionId - return undefined + // Hard-coded sessionId in UI + if (memoryNode.data.inputs?.sessionId) { + return memoryNode.data.inputs.sessionId + } + + // Default chatId + return chatId } /** @@ -996,31 +1004,52 @@ export const checkMemorySessionId = (instance: any, chatId: string): string | un * @param {any} logger * @returns {string} */ -export const replaceChatHistory = async ( +export const getSessionChatHistory = async ( memoryNode: IReactFlowNode, + componentNodes: IComponentNodes, incomingInput: IncomingInput, appDataSource: DataSource, databaseEntities: IDatabaseEntity, logger: any -): Promise => { - const nodeInstanceFilePath = memoryNode.data.filePath as string +): Promise => { + const nodeInstanceFilePath = componentNodes[memoryNode.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + // Replace memory's sessionId/chatId if (incomingInput.overrideConfig?.sessionId && memoryNode.data.inputs) { memoryNode.data.inputs.sessionId = incomingInput.overrideConfig.sessionId + } else if (incomingInput.chatId && memoryNode.data.inputs) { + memoryNode.data.inputs.sessionId = incomingInput.chatId } - if (newNodeInstance.memoryMethods && newNodeInstance.memoryMethods.getChatMessages) { - return await newNodeInstance.memoryMethods.getChatMessages(memoryNode.data, { - chatId: incomingInput.chatId, - appDataSource, - databaseEntities, - logger - }) - } + const initializedInstance: FlowiseMemory = await newNodeInstance.init(memoryNode.data, '', { + appDataSource, + databaseEntities, + logger + }) - return '' + return (await initializedInstance.getChatMessages()) as IMessage[] +} + +/** + * Method that find memory that is connected within chatflow + * In a chatflow, there should only be 1 memory node + * @param {IReactFlowNode[]} nodes + * @param {IReactFlowEdge[]} edges + * @returns {string | undefined} + */ +export const findMemoryNode = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined => { + const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') + const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) + + for (const edge of edges) { + if (memoryNodeIds.includes(edge.source)) { + const memoryNode = nodes.find((node) => node.data.id === edge.source) + return memoryNode + } + } + return undefined } /** diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 617d1066..a673d6b7 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -280,6 +280,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA style={{ display: 'flex', flexDirection: 'row', + alignItems: 'center', borderRadius: 10, background: 'rgb(254,252,191)', padding: 10, @@ -287,7 +288,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA marginBottom: 10 }} > - + {inputParam.warning} )} From 4364537595a9b1e914eabe834337451bc0053382 Mon Sep 17 00:00:00 2001 From: YISH Date: Wed, 10 Jan 2024 17:32:46 +0800 Subject: [PATCH 228/502] Add milvusTextField configuration for Milvus langchain python use `text` https://github.com/langchain-ai/langchain/blob/master/libs/community/langchain_community/vectorstores/milvus.py#L119 while langchian js use `langchain` https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-community/src/vectorstores/milvus.ts#L61 so it is necessary to add milvusTextField configuration for Milvus. --- .../components/nodes/vectorstores/Milvus/Milvus.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index 090f35f7..7566f8a8 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -65,6 +65,14 @@ class Milvus_VectorStores implements INode { name: 'milvusCollection', type: 'string' }, + { + label: 'Milvus Text Field', + name: 'milvusTextField', + type: 'string', + placeholder: 'langchain_text', + optional: true, + additionalParams: true + }, { label: 'Milvus Filter', name: 'milvusFilter', @@ -150,6 +158,7 @@ class Milvus_VectorStores implements INode { const address = nodeData.inputs?.milvusServerUrl as string const collectionName = nodeData.inputs?.milvusCollection as string const milvusFilter = nodeData.inputs?.milvusFilter as string + const textField = nodeData.inputs?.milvusTextField as string // embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings @@ -169,7 +178,8 @@ class Milvus_VectorStores implements INode { // init MilvusLibArgs const milVusArgs: MilvusLibArgs = { url: address, - collectionName: collectionName + collectionName: collectionName, + textField: textField } if (milvusUser) milVusArgs.username = milvusUser From 4b9b30bf7a0eaf3840211995e99bea79a0c38fbb Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 00:31:21 +0500 Subject: [PATCH 229/502] feature: Integrate Astra Vectorstore --- .../credentials/AstraApi.credential.ts | 34 ++++ .../nodes/vectorstores/Astra/Astra.ts | 190 ++++++++++++++++++ .../nodes/vectorstores/Astra/astra.svg | 1 + packages/components/package.json | 2 + 4 files changed, 227 insertions(+) create mode 100644 packages/components/credentials/AstraApi.credential.ts create mode 100644 packages/components/nodes/vectorstores/Astra/Astra.ts create mode 100644 packages/components/nodes/vectorstores/Astra/astra.svg diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts new file mode 100644 index 00000000..ad4c65a8 --- /dev/null +++ b/packages/components/credentials/AstraApi.credential.ts @@ -0,0 +1,34 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AstraApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Astra API' + this.name = 'AstraApi' + this.version = 1.0 + this.inputs = [ + { + label: 'Colection Name', + name: 'collectionName', + type: 'string' + }, + { + label: 'Astra DB Application Token', + name: 'applicationToken', + type: 'password' + }, + { + label: 'Astra DB Api Endpoint', + name: 'dbEndPoint', + type: 'string' + } + ] + } +} + +module.exports = { credClass: AstraApi } diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts new file mode 100644 index 00000000..648a8b49 --- /dev/null +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -0,0 +1,190 @@ +import { flatten } from 'lodash' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData } from '../../../src/utils' +import { AstraDBVectorStore, AstraLibArgs } from '@langchain/community/vectorstores/astradb' + +class Astra_VectorStores implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Astra' + this.name = 'Astra' + this.version = 1.0 + this.type = 'Astra' + this.icon = 'astra.svg' + this.category = 'Vector Stores' + this.description = `Upsert embedded data and perform similarity search upon query using DataStax Astra DB, a serverless vector database that’s perfect for managing mission-critical AI workloads` + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.badge = 'NEW' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['AstraApi'] + } + this.inputs = [ + { + label: 'Document', + name: 'document', + type: 'Document', + list: true, + optional: true + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Vector Dimension', + name: 'vectorDimension', + type: 'number', + placeholder: '1536', + optional: true, + description: 'Dimension used for storing vector embedding' + }, + { + label: 'Similarity Metric', + name: 'similarityMetric', + type: 'string', + placeholder: 'cosine', + optional: true, + description: 'cosine | euclidean | dot_product' + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'Astra Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Astra Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(AstraDBVectorStore)] + } + ] + } + + //@ts-ignore + vectorStoreMethods = { + async upsert(nodeData: INodeData, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + const embeddings = nodeData.inputs?.embeddings as Embeddings + const vectorDimension = nodeData.inputs?.vectorDimension as number + const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + + const expectedSimilarityMetric = ['cosine', 'euclidean', 'dot_product'] + if (similarityMetric && !expectedSimilarityMetric.includes(similarityMetric)) { + throw new Error(`Invalid Similarity Metric should be one of 'cosine' | 'euclidean' | 'dot_product'`) + } + + const clientConfig = { + token: credentialData?.applicationToken ?? 'dummy', + endpoint: credentialData?.dbEndPoint ?? 'dummy' + } + + const astraConfig: AstraLibArgs = { + ...clientConfig, + collection: credentialData.collectionName ?? 'flowise_test', + collectionOptions: { + vector: { + dimension: vectorDimension ?? 1536, + metric: similarityMetric ?? 'cosine' + } + } + } + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + try { + await AstraDBVectorStore.fromDocuments(finalDocs, embeddings, astraConfig) + } catch (e) { + throw new Error(e) + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + const embeddings = nodeData.inputs?.embeddings as Embeddings + const vectorDimension = nodeData.inputs?.vectorDimension as number + const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined + const output = nodeData.outputs?.output as string + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + + const expectedSimilarityMetric = ['cosine', 'euclidean', 'dot_product'] + if (similarityMetric && !expectedSimilarityMetric.includes(similarityMetric)) { + throw new Error(`Invalid Similarity Metric should be one of 'cosine' | 'euclidean' | 'dot_product'`) + } + + const clientConfig = { + token: credentialData?.applicationToken ?? 'dummy', + endpoint: credentialData?.dbEndPoint ?? 'dummy' + } + + const astraConfig: AstraLibArgs = { + ...clientConfig, + collection: credentialData.collectionName ?? 'flowise_test', + collectionOptions: { + vector: { + dimension: vectorDimension ?? 1536, + metric: similarityMetric ?? 'cosine' + } + } + } + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + + const vectorStore = await AstraDBVectorStore.fromExistingIndex(embeddings, astraConfig) + + if (output === 'retriever') { + const retriever = vectorStore.asRetriever(k) + return retriever + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } + return vectorStore + } +} + +module.exports = { nodeClass: Astra_VectorStores } diff --git a/packages/components/nodes/vectorstores/Astra/astra.svg b/packages/components/nodes/vectorstores/Astra/astra.svg new file mode 100644 index 00000000..59c2fc3f --- /dev/null +++ b/packages/components/nodes/vectorstores/Astra/astra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/package.json b/packages/components/package.json index a77d91e4..ff0b687b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -19,6 +19,7 @@ "@aws-sdk/client-bedrock-runtime": "3.422.0", "@aws-sdk/client-dynamodb": "^3.360.0", "@aws-sdk/client-s3": "^3.427.0", + "@datastax/astra-db-ts": "^0.1.2", "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", "@getzep/zep-js": "^0.9.0", @@ -26,6 +27,7 @@ "@gomomento/sdk-core": "^1.51.1", "@google-ai/generativelanguage": "^0.2.1", "@huggingface/inference": "^2.6.1", + "@langchain/community": "^0.0.16", "@langchain/google-genai": "^0.0.6", "@langchain/mistralai": "^0.0.6", "@notionhq/client": "^2.2.8", From 4b39b4115b01b4d294af8d3375c9ff45a9df957a Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 01:36:57 +0500 Subject: [PATCH 230/502] chore: refactoring (naming convention) --- packages/components/credentials/AstraApi.credential.ts | 10 +++++----- packages/components/nodes/vectorstores/Astra/Astra.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts index ad4c65a8..3bec1a49 100644 --- a/packages/components/credentials/AstraApi.credential.ts +++ b/packages/components/credentials/AstraApi.credential.ts @@ -1,6 +1,6 @@ import { INodeParams, INodeCredential } from '../src/Interface' -class AstraApi implements INodeCredential { +class AstraDBApi implements INodeCredential { label: string name: string version: number @@ -8,12 +8,12 @@ class AstraApi implements INodeCredential { inputs: INodeParams[] constructor() { - this.label = 'Astra API' - this.name = 'AstraApi' + this.label = 'Astra DB API' + this.name = 'AstraDBApi' this.version = 1.0 this.inputs = [ { - label: 'Colection Name', + label: 'Collection Name', name: 'collectionName', type: 'string' }, @@ -31,4 +31,4 @@ class AstraApi implements INodeCredential { } } -module.exports = { credClass: AstraApi } +module.exports = { credClass: AstraDBApi } diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts index 648a8b49..e3377cb5 100644 --- a/packages/components/nodes/vectorstores/Astra/Astra.ts +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -33,7 +33,7 @@ class Astra_VectorStores implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['AstraApi'] + credentialNames: ['AstraDBApi'] } this.inputs = [ { From b3c9c32a4869ec51219ca30eac8ec105b56fcb64 Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 01:43:22 +0500 Subject: [PATCH 231/502] Update AstraApi.credential.ts --- packages/components/credentials/AstraApi.credential.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/credentials/AstraApi.credential.ts b/packages/components/credentials/AstraApi.credential.ts index 3bec1a49..a89a259f 100644 --- a/packages/components/credentials/AstraApi.credential.ts +++ b/packages/components/credentials/AstraApi.credential.ts @@ -13,7 +13,7 @@ class AstraDBApi implements INodeCredential { this.version = 1.0 this.inputs = [ { - label: 'Collection Name', + label: 'Astra DB Collection Name', name: 'collectionName', type: 'string' }, From adfeb37e8bcf2a4db044460a80a7b2c927e9f3d9 Mon Sep 17 00:00:00 2001 From: hakeemsyd Date: Fri, 12 Jan 2024 02:43:43 +0500 Subject: [PATCH 232/502] svg added and refactored again --- .../components/nodes/vectorstores/Astra/Astra.ts | 8 ++++---- .../components/nodes/vectorstores/Astra/astra.svg | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts index e3377cb5..865f1044 100644 --- a/packages/components/nodes/vectorstores/Astra/Astra.ts +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -103,8 +103,8 @@ class Astra_VectorStores implements INode { } const clientConfig = { - token: credentialData?.applicationToken ?? 'dummy', - endpoint: credentialData?.dbEndPoint ?? 'dummy' + token: credentialData?.applicationToken, + endpoint: credentialData?.dbEndPoint } const astraConfig: AstraLibArgs = { @@ -151,8 +151,8 @@ class Astra_VectorStores implements INode { } const clientConfig = { - token: credentialData?.applicationToken ?? 'dummy', - endpoint: credentialData?.dbEndPoint ?? 'dummy' + token: credentialData?.applicationToken, + endpoint: credentialData?.dbEndPoint } const astraConfig: AstraLibArgs = { diff --git a/packages/components/nodes/vectorstores/Astra/astra.svg b/packages/components/nodes/vectorstores/Astra/astra.svg index 59c2fc3f..de58397d 100644 --- a/packages/components/nodes/vectorstores/Astra/astra.svg +++ b/packages/components/nodes/vectorstores/Astra/astra.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + + + + + From b1b9b9fcffe3d45abdd022a5ac2496c9044185fe Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 18:36:23 -0800 Subject: [PATCH 233/502] added support for MMR --- .../nodes/chains/VectaraChain/VectaraChain.ts | 38 ++++++++++++++----- .../nodes/vectorstores/Vectara/Vectara.ts | 31 +++++++++++++-- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 3799d062..c80b354f 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -69,22 +69,23 @@ class VectaraChain_Chains implements INode { options: [ { label: 'vectara-summary-ext-v1.2.0 (gpt-3.5-turbo)', - name: 'vectara-summary-ext-v1.2.0' + name: 'vectara-summary-ext-v1.2.0', + description: 'base summarizer, available to all Vectara users' }, { label: 'vectara-experimental-summary-ext-2023-10-23-small (gpt-3.5-turbo)', name: 'vectara-experimental-summary-ext-2023-10-23-small', - description: 'In beta, available to both Growth and Scale Vectara users' + description: `In beta, available to both Growth and Scale Vectara users` }, { label: 'vectara-summary-ext-v1.3.0 (gpt-4.0)', name: 'vectara-summary-ext-v1.3.0', - description: 'Only available to paying Scale Vectara users' + description: 'Only available to Scale Vectara users' }, { label: 'vectara-experimental-summary-ext-2023-10-23-med (gpt-4.0)', name: 'vectara-experimental-summary-ext-2023-10-23-med', - description: 'In beta, only available to paying Scale Vectara users' + description: `In beta, only available to Scale Vectara users` } ], default: 'vectara-summary-ext-v1.2.0' @@ -228,7 +229,7 @@ class VectaraChain_Chains implements INode { async run(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore - const responseLang = (nodeData.inputs?.responseLang as string) ?? 'auto' + const responseLang = (nodeData.inputs?.responseLang as string) ?? 'eng' const summarizerPromptName = nodeData.inputs?.summarizerPromptName as string const maxSummarizedResultsStr = nodeData.inputs?.maxSummarizedResults as string const maxSummarizedResults = maxSummarizedResultsStr ? parseInt(maxSummarizedResultsStr, 10) : 7 @@ -247,17 +248,28 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) + const mmrRerankerId = 272725718 // Vectara reranker ID for MMR const data = { query: [ { query: input, start: 0, - numResults: topK, + numResults: vectaraFilter?.mmrConfig?.mmrK > 0 ? vectaraFilter?.mmrK : topK, + corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 }, - corpusKey: corpusKeys, + ...(vectaraFilter?.mmrConfig?.mmrK > 0 + ? { + rerankingConfig: { + rerankerId: mmrRerankerId, + mmrConfig: { + diversityBias: vectaraFilter?.mmrConfig.diversityBias + } + } + } + : {}), summary: [ { summarizerPromptName, @@ -285,6 +297,14 @@ class VectaraChain_Chains implements INode { const documents = result.responseSet[0].document let rawSummarizedText = '' + // remove responses that are not in the topK (in case of MMR) + // Note that this does not really matter functionally due to the reorder citations, but it is more efficient + const maxResponses = vectaraFilter?.mmrConfig?.mmrK > 0 ? Math.min(responses.length, topK) : responses.length + if (responses.length > maxResponses) { + responses.splice(0, maxResponses) + } + + // Add metadata to each text response given its corresponding document metadata for (let i = 0; i < responses.length; i += 1) { const responseMetadata = responses[i].metadata const documentMetadata = documents[responses[i].documentIndex].metadata @@ -301,13 +321,13 @@ class VectaraChain_Chains implements INode { responses[i].metadata = combinedMetadata } + // Create the summarization response const summaryStatus = result.responseSet[0].summary[0].status if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { throw new Error( `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` ) } - if ( summaryStatus.length > 0 && summaryStatus[0].code === 'NOT_FOUND' && @@ -316,8 +336,8 @@ class VectaraChain_Chains implements INode { throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) } + // Reorder citations in summary and create the list of returned source documents rawSummarizedText = result.responseSet[0].summary[0]?.text - let summarizedText = reorderCitations(rawSummarizedText) let summaryResponses = applyCitationOrder(responses, rawSummarizedText) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 7460c586..98acf00c 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -82,7 +82,9 @@ class Vectara_VectorStores implements INode { label: 'Lambda', name: 'lambda', description: - 'Improves retrieval accuracy by adjusting the balance (from 0 to 1) between neural search and keyword-based search factors.', + 'Enable hybrid search to improve retrieval accuracy by adjusting the balance (from 0 to 1) between neural search and keyword-based search factors.' + + 'A value of 0.0 means that only neural search is used, while a value of 1.0 means that only keyword-based search is used. Defaults to 0.0 (neural only).', + default: 0.0, type: 'number', additionalParams: true, optional: true @@ -90,8 +92,26 @@ class Vectara_VectorStores implements INode { { label: 'Top K', name: 'topK', - description: 'Number of top results to fetch. Defaults to 4', - placeholder: '4', + description: 'Number of top results to fetch. Defaults to 5', + placeholder: '5', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'MMR K', + name: 'mmrK', + description: 'Number of top results to fetch for MMR. Defaults to 50', + placeholder: '50', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'MMR diversity bias', + name: 'mmrDiversityBias', + description: 'The diversity bias to use for MMR. Defaults to 0.3', + placeholder: '0.3', type: 'number', additionalParams: true, optional: true @@ -191,7 +211,9 @@ class Vectara_VectorStores implements INode { const lambda = nodeData.inputs?.lambda as number const output = nodeData.outputs?.output as string const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 + const k = topK ? parseFloat(topK) : 5 + const mmrK = nodeData.inputs?.mmrK as number + const mmrDiversityBias = nodeData.inputs?.mmrDiversityBias as number const vectaraArgs: VectaraLibArgs = { apiKey: apiKey, @@ -208,6 +230,7 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig + if (mmrK) vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } const vectorStore = new VectaraStore(vectaraArgs) From 78bc93cc9e68392b3a9e10fcb196c01fe9da42c4 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 18:59:49 -0800 Subject: [PATCH 234/502] bug fix --- .../components/nodes/chains/VectaraChain/VectaraChain.ts | 8 +++++--- packages/components/nodes/vectorstores/Vectara/Vectara.ts | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index c80b354f..16257b69 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -249,18 +249,20 @@ class VectaraChain_Chains implements INode { })) const mmrRerankerId = 272725718 // Vectara reranker ID for MMR + const mmrEnabled = vectaraFilter?.mmrConfig?.mmrDiversityBias > 0 + const data = { query: [ { query: input, start: 0, - numResults: vectaraFilter?.mmrConfig?.mmrK > 0 ? vectaraFilter?.mmrK : topK, + numResults: mmrEnabled ? vectaraFilter?.mmrK : topK, corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 }, - ...(vectaraFilter?.mmrConfig?.mmrK > 0 + ...(mmrEnabled ? { rerankingConfig: { rerankerId: mmrRerankerId, @@ -299,7 +301,7 @@ class VectaraChain_Chains implements INode { // remove responses that are not in the topK (in case of MMR) // Note that this does not really matter functionally due to the reorder citations, but it is more efficient - const maxResponses = vectaraFilter?.mmrConfig?.mmrK > 0 ? Math.min(responses.length, topK) : responses.length + const maxResponses = mmrEnabled ? Math.min(responses.length, topK) : responses.length if (responses.length > maxResponses) { responses.splice(0, maxResponses) } diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 98acf00c..488a8803 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -110,8 +110,8 @@ class Vectara_VectorStores implements INode { { label: 'MMR diversity bias', name: 'mmrDiversityBias', - description: 'The diversity bias to use for MMR. Defaults to 0.3', - placeholder: '0.3', + description: 'The diversity bias to use for MMR. Defaults to 0 (MMR disabled)', + placeholder: '0.0', type: 'number', additionalParams: true, optional: true @@ -230,7 +230,7 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig - if (mmrK) vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } + vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } const vectorStore = new VectaraStore(vectaraArgs) From 356137b88bb2a95dc328354fec06b4a91edb6c97 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 19:18:33 -0800 Subject: [PATCH 235/502] bug fix 2 --- .../nodes/vectorstores/Vectara/Vectara.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 488a8803..d83f6cb9 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -1,5 +1,12 @@ import { flatten } from 'lodash' -import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile } from 'langchain/vectorstores/vectara' +import { + VectaraStore, + VectaraLibArgs, + VectaraFilter, + VectaraContextConfig, + VectaraFile, + VectaraMMRConfig +} from 'langchain/vectorstores/vectara' import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' @@ -230,7 +237,10 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig - vectaraFilter.mmrConfig = { mmrK: mmrK, diversityBias: mmrDiversityBias } + const mmrConfig: VectaraMMRConfig = {} + mmrConfig.mmrK = mmrK + mmrConfig.diversityBias = mmrDiversityBias + vectaraFilter.mmrConfig = mmrConfig const vectorStore = new VectaraStore(vectaraArgs) From 0c2252c642eb5cb9b7d373ecfd10d714f058a93d Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 21:53:48 -0800 Subject: [PATCH 236/502] bugfix --- .../nodes/chains/VectaraChain/VectaraChain.ts | 4 ++-- .../nodes/vectorstores/Vectara/Vectara.ts | 14 ++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 16257b69..986d587a 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -249,14 +249,14 @@ class VectaraChain_Chains implements INode { })) const mmrRerankerId = 272725718 // Vectara reranker ID for MMR - const mmrEnabled = vectaraFilter?.mmrConfig?.mmrDiversityBias > 0 + const mmrEnabled = vectaraFilter?.mmrConfig?.enabled const data = { query: [ { query: input, start: 0, - numResults: mmrEnabled ? vectaraFilter?.mmrK : topK, + numResults: mmrEnabled ? vectaraFilter?.mmrTopK : topK, corpusKey: corpusKeys, contextConfig: { sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index d83f6cb9..be63d582 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -1,12 +1,5 @@ import { flatten } from 'lodash' -import { - VectaraStore, - VectaraLibArgs, - VectaraFilter, - VectaraContextConfig, - VectaraFile, - VectaraMMRConfig -} from 'langchain/vectorstores/vectara' +import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile, MMRConfig } from 'langchain/vectorstores/vectara' import { Document } from 'langchain/document' import { Embeddings } from 'langchain/embeddings/base' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' @@ -237,8 +230,9 @@ class Vectara_VectorStores implements INode { if (sentencesBefore) vectaraContextConfig.sentencesBefore = sentencesBefore if (sentencesAfter) vectaraContextConfig.sentencesAfter = sentencesAfter vectaraFilter.contextConfig = vectaraContextConfig - const mmrConfig: VectaraMMRConfig = {} - mmrConfig.mmrK = mmrK + const mmrConfig: MMRConfig = {} + mmrConfig.enabled = mmrDiversityBias > 0 + mmrConfig.mmrTopK = mmrK mmrConfig.diversityBias = mmrDiversityBias vectaraFilter.mmrConfig = mmrConfig From 2ae8a60ec103029756c3d7a73bc32939b32cd4bb Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 23:40:04 -0800 Subject: [PATCH 237/502] na --- package.json | 2 +- packages/components/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5a9bfcbf..19a79da7 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,5 @@ }, "engines": { "node": ">=18.15.0" - } + }, } diff --git a/packages/components/package.json b/packages/components/package.json index ff0b687b..57ab7b3d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -50,7 +50,7 @@ "faiss-node": "^0.2.2", "fast-json-patch": "^3.1.1", "form-data": "^4.0.0", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.4.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", "husky": "^8.0.3", From 9a63d30333d6262b12bed0a30e8a55a81289d06b Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 23:41:19 -0800 Subject: [PATCH 238/502] extra comma --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19a79da7..5a9bfcbf 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,5 @@ }, "engines": { "node": ">=18.15.0" - }, + } } From c93b01e8212bdb5b40189b18c17785f69501e5aa Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 07:25:54 -0800 Subject: [PATCH 239/502] updates per PR comments --- .../components/nodes/chains/VectaraChain/VectaraChain.ts | 3 ++- packages/components/nodes/vectorstores/Vectara/Vectara.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 986d587a..7d65c9cd 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -248,7 +248,8 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) - const mmrRerankerId = 272725718 // Vectara reranker ID for MMR + // Vectara reranker ID for MMR (https://docs.vectara.com/docs/api-reference/search-apis/reranking#maximal-marginal-relevance-mmr-reranker) + const mmrRerankerId = 272725718 const mmrEnabled = vectaraFilter?.mmrConfig?.enabled const data = { diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index be63d582..df709e0b 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -110,7 +110,10 @@ class Vectara_VectorStores implements INode { { label: 'MMR diversity bias', name: 'mmrDiversityBias', - description: 'The diversity bias to use for MMR. Defaults to 0 (MMR disabled)', + description: + 'The diversity bias to use for MMR. This is a value between 0.0 and 1.0' + + 'Values closer to 1.0 optimize for the most diverse results.' + + 'Defaults to 0 (MMR disabled)', placeholder: '0.0', type: 'number', additionalParams: true, From 2484d2a6766f8a076e6632f9c47f6c156bc968fd Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 10:26:17 -0800 Subject: [PATCH 240/502] added step to diversityBias --- packages/components/nodes/vectorstores/Vectara/Vectara.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index df709e0b..45825b4f 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -110,6 +110,7 @@ class Vectara_VectorStores implements INode { { label: 'MMR diversity bias', name: 'mmrDiversityBias', + step: 0.1, description: 'The diversity bias to use for MMR. This is a value between 0.0 and 1.0' + 'Values closer to 1.0 optimize for the most diverse results.' + From 7acbc0b0684ec447343fada8bd38269bb1e3b713 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 16:09:50 -0800 Subject: [PATCH 241/502] updated component to V2.0 Updated marketplace "Chain Upload" JSON file --- .../nodes/vectorstores/Vectara/Vectara.ts | 2 +- .../chatflows/Vectara LLM Chain Upload.json | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/vectorstores/Vectara/Vectara.ts b/packages/components/nodes/vectorstores/Vectara/Vectara.ts index 45825b4f..939a4ac3 100644 --- a/packages/components/nodes/vectorstores/Vectara/Vectara.ts +++ b/packages/components/nodes/vectorstores/Vectara/Vectara.ts @@ -22,7 +22,7 @@ class Vectara_VectorStores implements INode { constructor() { this.label = 'Vectara' this.name = 'vectara' - this.version = 1.0 + this.version = 2.0 this.type = 'Vectara' this.icon = 'vectara.png' this.category = 'Vector Stores' diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index 6f0edeea..e544486e 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -350,13 +350,36 @@ { "label": "Top K", "name": "topK", - "description": "Number of top results to fetch. Defaults to 4", - "placeholder": "4", + "description": "Number of top results to fetch. Defaults to 5", + "placeholder": "5", "type": "number", "additionalParams": true, "optional": true, "id": "vectara_0-input-topK-number" + }, + { + "label": "MMR K", + "name": "mmrK", + "description": "The number of results to rerank if MMR is enabled.", + "placeholder": "50", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "vectara_0-input-mmrK-number" + }, + { + "label": "MMR Diversity Bias", + "name": "mmrDiversityBias", + "step": 0.1, + "description": "Diversity Bias parameter for MMR, if enabled. 0.0 means no diversiry bias, 1.0 means maximum diversity bias. Defaults to 0.0 (MMR disabled).", + "placeholder": "0.0", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "vectara_0-input-mmrDiversityBias-number" } + + ], "inputAnchors": [ { @@ -374,7 +397,9 @@ "sentencesBefore": "", "sentencesAfter": "", "lambda": "", - "topK": "" + "topK": "", + "mmrK": "", + "mmrDiversityBias": "" }, "outputAnchors": [ { From a3ea487ecb0d108fec12b3fca31d7d8950841e6f Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Mon, 8 Jan 2024 16:46:36 -0800 Subject: [PATCH 242/502] after yarn lint-fix --- .../marketplaces/chatflows/Vectara LLM Chain Upload.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json index e544486e..1a440be7 100644 --- a/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json +++ b/packages/server/marketplaces/chatflows/Vectara LLM Chain Upload.json @@ -366,7 +366,7 @@ "additionalParams": true, "optional": true, "id": "vectara_0-input-mmrK-number" - }, + }, { "label": "MMR Diversity Bias", "name": "mmrDiversityBias", @@ -378,8 +378,6 @@ "optional": true, "id": "vectara_0-input-mmrDiversityBias-number" } - - ], "inputAnchors": [ { From c8f624de9c108ef3856bcb8f9c7336dd4e1e20f2 Mon Sep 17 00:00:00 2001 From: YISH Date: Wed, 10 Jan 2024 17:41:53 +0800 Subject: [PATCH 243/502] Fix OpenAIFunctionAgent that function not return string result refer to https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/agents/format_scratchpad/openai_functions.py#L29 and fix the role of systemMessage from `ai` to `system`. --- .../nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 135121d2..c21c887a 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -112,7 +112,7 @@ const prepareAgent = ( const inputKey = memory.inputKey ? memory.inputKey : 'input' const prompt = ChatPromptTemplate.fromMessages([ - ['ai', systemMessage ? systemMessage : `You are a helpful AI assistant.`], + ['system', systemMessage ? systemMessage : `You are a helpful AI assistant.`], new MessagesPlaceholder(memoryKey), ['human', `{${inputKey}}`], new MessagesPlaceholder('agent_scratchpad') From 79e988be09dea9560aada68f1fd601af19c12666 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 14 Jan 2024 11:57:53 +0000 Subject: [PATCH 244/502] delete message API --- packages/server/src/index.ts | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b446445e..94a3b538 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -522,7 +522,7 @@ export class App { res.status(404).send(`Chatflow ${chatflowid} not found`) return } - const chatId = (req.query?.chatId as string) ?? (await getChatId(chatflowid)) + const chatId = req.query?.chatId as string const memoryType = req.query?.memoryType as string | undefined const sessionId = req.query?.sessionId as string | undefined const chatType = req.query?.chatType as string | undefined @@ -546,7 +546,8 @@ export class App { return res.status(500).send('Error clearing chat messages') } - const deleteOptions: FindOptionsWhere = { chatflowid, chatId } + const deleteOptions: FindOptionsWhere = { chatflowid } + if (chatId) deleteOptions.chatId = chatId if (memoryType) deleteOptions.memoryType = memoryType if (sessionId) deleteOptions.sessionId = sessionId if (chatType) deleteOptions.chatType = chatType @@ -634,7 +635,7 @@ export class App { return res.json(result) }) - // Delete all chatmessages from chatflowid + // Delete all credentials from chatflowid this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) @@ -1791,23 +1792,6 @@ export class App { } } -/** - * Get first chat message id - * @param {string} chatflowid - * @returns {string} - */ -export async function getChatId(chatflowid: string): Promise { - // first chatmessage id as the unique chat id - const firstChatMessage = await getDataSource() - .getRepository(ChatMessage) - .createQueryBuilder('cm') - .select('cm.id') - .where('chatflowid = :chatflowid', { chatflowid }) - .orderBy('cm.createdDate', 'ASC') - .getOne() - return firstChatMessage ? firstChatMessage.id : '' -} - let serverApp: App | undefined export async function getAllChatFlow(): Promise { From 58f9e88b1f2c6a08b8be96964afe07ebd25cc890 Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Sat, 6 Jan 2024 17:16:06 -0700 Subject: [PATCH 245/502] Introduce new credential for LocalAI, Pass optional auth to LocalAI, New env var --- .../credentials/LcoalAIApi.credential.ts | 23 +++++++++++++++ .../chatmodels/ChatLocalAI/ChatLocalAI.ts | 29 +++++++++++++++---- packages/server/.env.example | 2 ++ 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 packages/components/credentials/LcoalAIApi.credential.ts diff --git a/packages/components/credentials/LcoalAIApi.credential.ts b/packages/components/credentials/LcoalAIApi.credential.ts new file mode 100644 index 00000000..624e07fa --- /dev/null +++ b/packages/components/credentials/LcoalAIApi.credential.ts @@ -0,0 +1,23 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class LocalAIApi implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + + constructor() { + this.label = 'LocalAI API' + this.name = 'LocalAIApi' + this.version = 1.0 + this.inputs = [ + { + label: 'LocalAI Api Key', + name: 'LocalAIApiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: LocalAIApi } diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index 18ed409b..258db1f8 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -1,5 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses } from '../../../src/utils' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { OpenAIChat } from 'langchain/llms/openai' import { OpenAIChatInput } from 'langchain/chat_models/openai' import { BaseCache } from 'langchain/schema' @@ -14,6 +14,7 @@ class ChatLocalAI_ChatModels implements INode { category: string description: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -25,6 +26,16 @@ class ChatLocalAI_ChatModels implements INode { this.category = 'Chat Models' this.description = 'Use local LLMs like llama.cpp, gpt4all using LocalAI' this.baseClasses = [this.type, 'BaseChatModel', ...getBaseClasses(OpenAIChat)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['LocalAIApi'], + optional: true + } + + const modelOptions = JSON.parse(process.env.LOCALAI_CHAT_MODELS || '[]'); + this.inputs = [ { label: 'Cache', @@ -41,8 +52,10 @@ class ChatLocalAI_ChatModels implements INode { { label: 'Model Name', name: 'modelName', - type: 'string', - placeholder: 'gpt4all-lora-quantized.bin' + type: 'options', + options: modelOptions, + default: modelOptions.length > 0 ? modelOptions[0].name : '', + optional: true }, { label: 'Temperature', @@ -79,19 +92,23 @@ class ChatLocalAI_ChatModels implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const temperature = nodeData.inputs?.temperature as string const modelName = nodeData.inputs?.modelName as string const maxTokens = nodeData.inputs?.maxTokens as string const topP = nodeData.inputs?.topP as string const timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const openAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) + const cache = nodeData.inputs?.cache as BaseCache const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { temperature: parseFloat(temperature), modelName, - openAIApiKey: 'sk-' + openAIApiKey } if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) diff --git a/packages/server/.env.example b/packages/server/.env.example index 6e746a4d..9b7be0ff 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -26,3 +26,5 @@ PORT=3000 # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key # LANGCHAIN_PROJECT=your_project + +# LOCALAI_CHAT_MODELS='[{"label": "model1", "name": "model1"}, {"label": "model2", "name": "model2"}]' From d9b75cdf8e81721cd2c713084cfb930230ca8010 Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Sat, 6 Jan 2024 17:33:41 -0700 Subject: [PATCH 246/502] Updating docs --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04cb80b4..2c91906c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,6 +141,7 @@ Flowise support different environment variables to configure your instance. You | DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | +| LOCALAI_CHAT_MODELS | JSON-encoded string representing an array of chat models for LocalAI. Each object in the array should have a 'label' and 'name' property. | String | '[]' (Empty Array) | You can also specify the env variables when using `npx`. For example: From 06201e7cf09dc5e206f7902611cc9716c021e0c8 Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Mon, 8 Jan 2024 17:53:18 -0700 Subject: [PATCH 247/502] Revert model var to string, refactor for case without a key and just override if so --- CONTRIBUTING.md | 1 - .../nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts | 17 ++++++----------- packages/server/.env.example | 2 -- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c91906c..04cb80b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,6 @@ Flowise support different environment variables to configure your instance. You | DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | -| LOCALAI_CHAT_MODELS | JSON-encoded string representing an array of chat models for LocalAI. Each object in the array should have a 'label' and 'name' property. | String | '[]' (Empty Array) | You can also specify the env variables when using `npx`. For example: diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index 258db1f8..c44f03ce 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -33,9 +33,6 @@ class ChatLocalAI_ChatModels implements INode { credentialNames: ['LocalAIApi'], optional: true } - - const modelOptions = JSON.parse(process.env.LOCALAI_CHAT_MODELS || '[]'); - this.inputs = [ { label: 'Cache', @@ -52,10 +49,8 @@ class ChatLocalAI_ChatModels implements INode { { label: 'Model Name', name: 'modelName', - type: 'options', - options: modelOptions, - default: modelOptions.length > 0 ? modelOptions[0].name : '', - optional: true + type: 'string', + placeholder: 'gpt4all-lora-quantized.bin' }, { label: 'Temperature', @@ -99,22 +94,22 @@ class ChatLocalAI_ChatModels implements INode { const topP = nodeData.inputs?.topP as string const timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const openAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) + const localAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) const cache = nodeData.inputs?.cache as BaseCache - const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { + const obj: Partial & BaseLLMParams & { localAIApiKey?: string } = { temperature: parseFloat(temperature), modelName, - openAIApiKey + openAIApiKey: 'sk-' } if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10) if (topP) obj.topP = parseFloat(topP) if (timeout) obj.timeout = parseInt(timeout, 10) if (cache) obj.cache = cache + if (localAIApiKey) obj.openAIApiKey = localAIApiKey const model = new OpenAIChat(obj, { basePath }) diff --git a/packages/server/.env.example b/packages/server/.env.example index 9b7be0ff..6e746a4d 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -26,5 +26,3 @@ PORT=3000 # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key # LANGCHAIN_PROJECT=your_project - -# LOCALAI_CHAT_MODELS='[{"label": "model1", "name": "model1"}, {"label": "model2", "name": "model2"}]' From b9d1d75d6a043770cf3ca2f3db7781746423ea8f Mon Sep 17 00:00:00 2001 From: Keith Kacsh Date: Sat, 13 Jan 2024 19:14:45 -0700 Subject: [PATCH 248/502] Fixing naming, handling embeddings for LocalAI also --- ....credential.ts => LocalAIApi.credential.ts} | 4 ++-- .../chatmodels/ChatLocalAI/ChatLocalAI.ts | 6 +++--- .../LocalAIEmbedding/LocalAIEmbedding.ts | 18 ++++++++++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) rename packages/components/credentials/{LcoalAIApi.credential.ts => LocalAIApi.credential.ts} (86%) diff --git a/packages/components/credentials/LcoalAIApi.credential.ts b/packages/components/credentials/LocalAIApi.credential.ts similarity index 86% rename from packages/components/credentials/LcoalAIApi.credential.ts rename to packages/components/credentials/LocalAIApi.credential.ts index 624e07fa..4aafe040 100644 --- a/packages/components/credentials/LcoalAIApi.credential.ts +++ b/packages/components/credentials/LocalAIApi.credential.ts @@ -8,12 +8,12 @@ class LocalAIApi implements INodeCredential { constructor() { this.label = 'LocalAI API' - this.name = 'LocalAIApi' + this.name = 'localAIApi' this.version = 1.0 this.inputs = [ { label: 'LocalAI Api Key', - name: 'LocalAIApiKey', + name: 'localAIApiKey', type: 'password' } ] diff --git a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts index c44f03ce..f2825d0d 100644 --- a/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts +++ b/packages/components/nodes/chatmodels/ChatLocalAI/ChatLocalAI.ts @@ -30,7 +30,7 @@ class ChatLocalAI_ChatModels implements INode { label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['LocalAIApi'], + credentialNames: ['localAIApi'], optional: true } this.inputs = [ @@ -95,11 +95,11 @@ class ChatLocalAI_ChatModels implements INode { const timeout = nodeData.inputs?.timeout as string const basePath = nodeData.inputs?.basePath as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const localAIApiKey = getCredentialParam('LocalAIApiKey', credentialData, nodeData) + const localAIApiKey = getCredentialParam('localAIApiKey', credentialData, nodeData) const cache = nodeData.inputs?.cache as BaseCache - const obj: Partial & BaseLLMParams & { localAIApiKey?: string } = { + const obj: Partial & BaseLLMParams & { openAIApiKey?: string } = { temperature: parseFloat(temperature), modelName, openAIApiKey: 'sk-' diff --git a/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts b/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts index 557e35d6..24efaf8c 100644 --- a/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts +++ b/packages/components/nodes/embeddings/LocalAIEmbedding/LocalAIEmbedding.ts @@ -1,4 +1,5 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getCredentialData, getCredentialParam } from '../../../src/utils' import { OpenAIEmbeddings, OpenAIEmbeddingsParams } from 'langchain/embeddings/openai' class LocalAIEmbedding_Embeddings implements INode { @@ -10,6 +11,7 @@ class LocalAIEmbedding_Embeddings implements INode { category: string description: string baseClasses: string[] + credential: INodeParams inputs: INodeParams[] constructor() { @@ -21,6 +23,13 @@ class LocalAIEmbedding_Embeddings implements INode { this.category = 'Embeddings' this.description = 'Use local embeddings models like llama.cpp' this.baseClasses = [this.type, 'Embeddings'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['localAIApi'], + optional: true + } this.inputs = [ { label: 'Base Path', @@ -37,15 +46,20 @@ class LocalAIEmbedding_Embeddings implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const modelName = nodeData.inputs?.modelName as string const basePath = nodeData.inputs?.basePath as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const localAIApiKey = getCredentialParam('localAIApiKey', credentialData, nodeData) + const obj: Partial & { openAIApiKey?: string } = { modelName, openAIApiKey: 'sk-' } + if (localAIApiKey) obj.openAIApiKey = localAIApiKey + const model = new OpenAIEmbeddings(obj, { basePath }) return model From 8f92bba145261c391574e900aafb1b63fef97b9d Mon Sep 17 00:00:00 2001 From: Carson Yang Date: Tue, 9 Jan 2024 13:58:40 +0800 Subject: [PATCH 249/502] Update README-ZH.md --- README-ZH.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README-ZH.md b/README-ZH.md index 2805ef9b..208eee92 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -153,6 +153,10 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package [![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) +### [Sealos](https://docs.flowiseai.com/configuration/deployment/sealos) + +[![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://template.cloud.sealos.io/deploy?templateName=flowise) + ### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) HuggingFace Spaces From 3d96169b7584a35764d3d724a5eecd674a0c9d1f Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 15 Jan 2024 02:46:21 +0000 Subject: [PATCH 250/502] update README-ZH md --- README-ZH.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/README-ZH.md b/README-ZH.md index 208eee92..8750ebc7 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -145,29 +145,40 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package ## 🌐 自托管 -### [Railway](https://docs.flowiseai.com/deployment/railway) +在您现有的基础设施中部署自托管的 Flowise,我们支持各种[部署](https://docs.flowiseai.com/configuration/deployment) -[![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) +- [AWS](https://docs.flowiseai.com/deployment/aws) +- [Azure](https://docs.flowiseai.com/deployment/azure) +- [Digital Ocean](https://docs.flowiseai.com/deployment/digital-ocean) +- [GCP](https://docs.flowiseai.com/deployment/gcp) +-
    + 其他 -### [Render](https://docs.flowiseai.com/deployment/render) + - [Railway](https://docs.flowiseai.com/deployment/railway) -[![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) + [![在 Railway 上部署](https://railway.app/button.svg)](https://railway.app/template/pn4G8S?referralCode=WVNPD9) -### [Sealos](https://docs.flowiseai.com/configuration/deployment/sealos) + - [Render](https://docs.flowiseai.com/deployment/render) -[![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://template.cloud.sealos.io/deploy?templateName=flowise) + [![部署到 Render](https://render.com/images/deploy-to-render-button.svg)](https://docs.flowiseai.com/deployment/render) -### [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) + - [HuggingFace Spaces](https://docs.flowiseai.com/deployment/hugging-face) -HuggingFace Spaces + HuggingFace Spaces -### [AWS](https://docs.flowiseai.com/deployment/aws) + - [Elestio](https://elest.io/open-source/flowiseai) -### [Azure](https://docs.flowiseai.com/deployment/azure) + [![Deploy](https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png)](https://elest.io/open-source/flowiseai) -### [DigitalOcean](https://docs.flowiseai.com/deployment/digital-ocean) + - [Sealos](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) -### [GCP](https://docs.flowiseai.com/deployment/gcp) + [![部署到 Sealos](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dflowise) + + - [RepoCloud](https://repocloud.io/details/?app_id=29) + + [![部署到 RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=29) + +
    ## 💻 云托管 From 853992949dc3810081d3a9d11bd95c08b605c055 Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Jan 2024 12:30:01 -0800 Subject: [PATCH 251/502] Correct DockerHub link in docs from private to public repo page --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index d3ad1c19..11b29cf3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,6 @@ # Flowise Docker Hub Image -Starts Flowise from [DockerHub Image](https://hub.docker.com/repository/docker/flowiseai/flowise/general) +Starts Flowise from [DockerHub Image](https://hub.docker.com/r/flowiseai/flowise) ## Usage From 8bf72493b434f482b0a0e2d443b6201609ef5354 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 15 Jan 2024 18:28:46 +0000 Subject: [PATCH 252/502] add sessionId tracking --- packages/components/package.json | 2 +- packages/components/src/handler.ts | 42 +++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 57ab7b3d..c90ea5cc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -57,7 +57,7 @@ "ioredis": "^5.3.2", "langchain": "^0.0.214", "langfuse": "2.0.2", - "langfuse-langchain": "2.0.2", + "langfuse-langchain": "2.3.3", "langsmith": "0.0.53", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 1eb05a51..df72a685 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -1,13 +1,13 @@ -import { BaseTracer, Run, BaseCallbackHandler } from 'langchain/callbacks' +import { BaseTracer, Run, BaseCallbackHandler, LangChainTracer } from 'langchain/callbacks' import { AgentAction, ChainValues } from 'langchain/schema' import { Logger } from 'winston' import { Server } from 'socket.io' import { Client } from 'langsmith' -import { LangChainTracer } from 'langchain/callbacks' -import { LLMonitorHandler } from 'langchain/callbacks/handlers/llmonitor' +import { LLMonitorHandler, LLMonitorHandlerFields } from 'langchain/callbacks/handlers/llmonitor' import { getCredentialData, getCredentialParam } from './utils' import { ICommonObject, INodeData } from './Interface' import CallbackHandler from 'langfuse-langchain' +import { LangChainTracerFields } from '@langchain/core/tracers/tracer_langchain' import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith' import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' import monitor from 'llmonitor' @@ -235,11 +235,16 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO apiKey: langSmithApiKey }) - const tracer = new LangChainTracer({ + let langSmithField: LangChainTracerFields = { projectName: langSmithProject ?? 'default', - //@ts-ignore client - }) + } + + if (nodeData?.inputs?.analytics?.langSmith) { + langSmithField = { ...langSmithField, ...nodeData?.inputs?.analytics?.langSmith } + } + + const tracer = new LangChainTracer(langSmithField) callbacks.push(tracer) } else if (provider === 'langFuse') { const release = analytic[provider].release as string @@ -248,13 +253,17 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, nodeData) const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, nodeData) - const langFuseOptions: any = { + let langFuseOptions: any = { secretKey: langFuseSecretKey, publicKey: langFusePublicKey, baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' } if (release) langFuseOptions.release = release - if (options.chatId) langFuseOptions.userId = options.chatId + if (options.chatId) langFuseOptions.sessionId = options.chatId + + if (nodeData?.inputs?.analytics?.langFuse) { + langFuseOptions = { ...langFuseOptions, ...nodeData?.inputs?.analytics?.langFuse } + } const handler = new CallbackHandler(langFuseOptions) callbacks.push(handler) @@ -262,11 +271,15 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, nodeData) const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, nodeData) - const llmonitorFields: ICommonObject = { + let llmonitorFields: LLMonitorHandlerFields = { appId: llmonitorAppId, apiUrl: llmonitorEndpoint ?? 'https://app.llmonitor.com' } + if (nodeData?.inputs?.analytics?.llmonitor) { + llmonitorFields = { ...llmonitorFields, ...nodeData?.inputs?.analytics?.llmonitor } + } + const handler = new LLMonitorHandler(llmonitorFields) callbacks.push(handler) } @@ -360,7 +373,8 @@ export class AnalyticHandler { }, serialized: {}, project_name: this.handlers['langSmith'].langSmithProject, - client: this.handlers['langSmith'].client + client: this.handlers['langSmith'].client, + ...this.nodeData?.inputs?.analytics?.langSmith } const parentRun = new RunTree(parentRunConfig) await parentRun.postRun() @@ -390,8 +404,9 @@ export class AnalyticHandler { const langfuse: Langfuse = this.handlers['langFuse'].client langfuseTraceClient = langfuse.trace({ name, - userId: this.options.chatId, - metadata: { tags: ['openai-assistant'] } + sessionId: this.options.chatId, + metadata: { tags: ['openai-assistant'] }, + ...this.nodeData?.inputs?.analytics?.langFuse }) } else { langfuseTraceClient = this.handlers['langFuse'].trace[parentIds['langFuse']] @@ -420,7 +435,8 @@ export class AnalyticHandler { runId, name, userId: this.options.chatId, - input + input, + ...this.nodeData?.inputs?.analytics?.llmonitor }) this.handlers['llmonitor'].chainEvent = { [runId]: runId } returnIds['llmonitor'].chainEvent = runId From 779e036c2331a1e336664b2ec8f36150402e7b58 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 15 Jan 2024 18:36:31 +0000 Subject: [PATCH 253/502] add ts-ignore --- packages/components/src/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index df72a685..5d2b53f6 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -237,6 +237,7 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO let langSmithField: LangChainTracerFields = { projectName: langSmithProject ?? 'default', + //@ts-ignore client } From 7fd339982e550deb62aba25c8c4542640ccd7a03 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Sun, 31 Dec 2023 23:40:04 -0800 Subject: [PATCH 254/502] na --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a9bfcbf..19a79da7 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,5 @@ }, "engines": { "node": ">=18.15.0" - } + }, } From f9d6089245e6702b1dd4b4170cb9ef2d2dd7f966 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 16 Jan 2024 11:20:51 +0530 Subject: [PATCH 255/502] Fix package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19a79da7..5a9bfcbf 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,5 @@ }, "engines": { "node": ">=18.15.0" - }, + } } From c77e23b0d15989f78f310145ae8614e53049d887 Mon Sep 17 00:00:00 2001 From: YISH Date: Tue, 16 Jan 2024 15:32:51 +0800 Subject: [PATCH 256/502] Fix CustomFunction receiving excaped inputs --- .../nodes/utilities/CustomFunction/CustomFunction.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts index 37511e47..749c3a86 100644 --- a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -73,7 +73,11 @@ class CustomFunction_Utilities implements INode { if (Object.keys(inputVars).length) { for (const item in inputVars) { - sandbox[`$${item}`] = inputVars[item] + let value = inputVars[item] + if (typeof value === 'string') { + value = handleEscapeCharacters(value, true) + } + sandbox[`$${item}`] = value } } From f3a244a93c497ab5c84baa475a673ea48896a0de Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Tue, 16 Jan 2024 16:19:21 +0800 Subject: [PATCH 257/502] modify google gemini based on request changes --- .../ChatGoogleGenerativeAI.ts | 59 +++---------------- packages/components/src/utils.ts | 9 +-- 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index bd660b47..9a4b8891 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -78,10 +78,11 @@ class GoogleGenerativeAI_ChatModels implements INode { additionalParams: true }, { - label: 'topK', + label: 'Top Next Highest Probability Tokens', name: 'topK', type: 'number', - step: 0.1, + description: `Decode using top-k sampling: consider the set of top_k most probable tokens. Must be positive`, + step: 1, optional: true, additionalParams: true }, @@ -90,7 +91,7 @@ class GoogleGenerativeAI_ChatModels implements INode { name: 'harmCategory', type: 'multiOptions', description: - 'Refer to official guide on how to use Harm Category', + 'Refer to official guide on how to use Harm Category', options: [ { label: 'Dangerous', @@ -117,7 +118,7 @@ class GoogleGenerativeAI_ChatModels implements INode { name: 'harmBlockThreshold', type: 'multiOptions', description: - 'Refer to official guide on how to use Harm Block Threshold', + 'Refer to official guide on how to use Harm Block Threshold', options: [ { label: 'Low and Above', @@ -169,7 +170,7 @@ class GoogleGenerativeAI_ChatModels implements INode { const model = new ChatGoogleGenerativeAI(obj) if (topP) model.topP = parseFloat(topP) - if (topK) model.topP = parseFloat(topK) + if (topK) model.topK = parseFloat(topK) if (cache) model.cache = cache if (temperature) model.temperature = parseFloat(temperature) @@ -178,10 +179,10 @@ class GoogleGenerativeAI_ChatModels implements INode { let harmBlockThresholds: string[] = convertMultiOptionsToStringArray(harmBlockThreshold) if (harmCategories.length != harmBlockThresholds.length) throw new Error(`Harm Category & Harm Block Threshold are not the same length`) - const safetySettings: SafetySetting[] = harmCategories.map((value, index) => { + const safetySettings: SafetySetting[] = harmCategories.map((harmCategory, index) => { return { - category: categoryInput(value), - threshold: thresholdInput(harmBlockThresholds[index]) + category: harmCategory as HarmCategory, + threshold: harmBlockThresholds[index] as HarmBlockThreshold } }) if (safetySettings.length > 0) model.safetySettings = safetySettings @@ -190,46 +191,4 @@ class GoogleGenerativeAI_ChatModels implements INode { } } -const categoryInput = (categoryInput: string): HarmCategory => { - let categoryOutput: HarmCategory - switch (categoryInput) { - case HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: - categoryOutput = HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT - break - case HarmCategory.HARM_CATEGORY_HATE_SPEECH: - categoryOutput = HarmCategory.HARM_CATEGORY_HATE_SPEECH - break - case HarmCategory.HARM_CATEGORY_HARASSMENT: - categoryOutput = HarmCategory.HARM_CATEGORY_HARASSMENT - break - case HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: - categoryOutput = HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT - break - default: - categoryOutput = HarmCategory.HARM_CATEGORY_UNSPECIFIED - } - return categoryOutput -} - -const thresholdInput = (thresholdInput: string): HarmBlockThreshold => { - let thresholdOutput: HarmBlockThreshold - switch (thresholdInput) { - case HarmBlockThreshold.BLOCK_LOW_AND_ABOVE: - thresholdOutput = HarmBlockThreshold.BLOCK_LOW_AND_ABOVE - break - case HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: - thresholdOutput = HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE - break - case HarmBlockThreshold.BLOCK_NONE: - thresholdOutput = HarmBlockThreshold.BLOCK_NONE - break - case HarmBlockThreshold.BLOCK_ONLY_HIGH: - thresholdOutput = HarmBlockThreshold.BLOCK_ONLY_HIGH - break - default: - thresholdOutput = HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED - } - return thresholdOutput -} - module.exports = { nodeClass: GoogleGenerativeAI_ChatModels } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 2d983562..eacfa4a0 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -681,12 +681,7 @@ export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[ */ export const convertMultiOptionsToStringArray = (inputString: string): string[] => { let ArrayString: string[] = [] - if (inputString) { - try { - ArrayString = JSON.parse(inputString) - } catch (e) { - ArrayString = [] - } - } + if (inputString) ArrayString = JSON.parse(inputString) + return ArrayString } From 74602484b29b93fb2b63089be874d6390a3fb3cd Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 16 Jan 2024 14:25:18 +0530 Subject: [PATCH 258/502] Fix sticky note in marketplace chatflows --- .../cards/NodeCardWrapper.js} | 6 +++--- .../tooltip/NodeTooltip.js} | 4 ++-- packages/ui/src/views/canvas/CanvasNode.js | 12 ++++++------ packages/ui/src/views/canvas/StickyNote.js | 12 ++++++------ .../ui/src/views/marketplaces/MarketplaceCanvas.js | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) rename packages/ui/src/{views/canvas/CardWrapper.js => ui-component/cards/NodeCardWrapper.js} (75%) rename packages/ui/src/{views/canvas/LightTooltip.js => ui-component/tooltip/NodeTooltip.js} (66%) diff --git a/packages/ui/src/views/canvas/CardWrapper.js b/packages/ui/src/ui-component/cards/NodeCardWrapper.js similarity index 75% rename from packages/ui/src/views/canvas/CardWrapper.js rename to packages/ui/src/ui-component/cards/NodeCardWrapper.js index 3e010899..7d7cafe5 100644 --- a/packages/ui/src/views/canvas/CardWrapper.js +++ b/packages/ui/src/ui-component/cards/NodeCardWrapper.js @@ -2,9 +2,9 @@ import { styled } from '@mui/material/styles' // project imports -import MainCard from '../../ui-component/cards/MainCard' +import MainCard from './MainCard' -const CardWrapper = styled(MainCard)(({ theme }) => ({ +const NodeCardWrapper = styled(MainCard)(({ theme }) => ({ background: theme.palette.card.main, color: theme.darkTextPrimary, border: 'solid 1px', @@ -18,4 +18,4 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({ } })) -export default CardWrapper +export default NodeCardWrapper diff --git a/packages/ui/src/views/canvas/LightTooltip.js b/packages/ui/src/ui-component/tooltip/NodeTooltip.js similarity index 66% rename from packages/ui/src/views/canvas/LightTooltip.js rename to packages/ui/src/ui-component/tooltip/NodeTooltip.js index 32e34aae..30bd7cf8 100644 --- a/packages/ui/src/views/canvas/LightTooltip.js +++ b/packages/ui/src/ui-component/tooltip/NodeTooltip.js @@ -1,7 +1,7 @@ import { styled } from '@mui/material/styles' import Tooltip, { tooltipClasses } from '@mui/material/Tooltip' -const LightTooltip = styled(({ className, ...props }) => )(({ theme }) => ({ +const NodeTooltip = styled(({ className, ...props }) => )(({ theme }) => ({ [`& .${tooltipClasses.tooltip}`]: { backgroundColor: theme.palette.nodeToolTip.background, color: theme.palette.nodeToolTip.color, @@ -9,4 +9,4 @@ const LightTooltip = styled(({ className, ...props }) => { return ( <> - { }} border={false} > - { ))} - - + + { return ( <> - { }} border={false} > - { nodeId={data.id} /> - - + + ) } diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.js b/packages/ui/src/views/marketplaces/MarketplaceCanvas.js index 7ce29451..613f3cdb 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.js +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.js @@ -11,10 +11,10 @@ import { useTheme } from '@mui/material/styles' // project imports import MarketplaceCanvasNode from './MarketplaceCanvasNode' - import MarketplaceCanvasHeader from './MarketplaceCanvasHeader' +import StickyNote from '../canvas/StickyNote' -const nodeTypes = { customNode: MarketplaceCanvasNode } +const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: '' } // ==============================|| CANVAS ||============================== // From 9b114212c0cc1f0a719b20ad56d6be0685ac8eb4 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 16 Jan 2024 14:25:36 +0530 Subject: [PATCH 259/502] Apply prettier --- packages/server/src/database/entities/Variable.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/server/src/database/entities/Variable.ts b/packages/server/src/database/entities/Variable.ts index 88e0587d..6af7a237 100644 --- a/packages/server/src/database/entities/Variable.ts +++ b/packages/server/src/database/entities/Variable.ts @@ -1,9 +1,9 @@ /* eslint-disable */ import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm' -import { IVariable } from "../../Interface"; +import { IVariable } from '../../Interface' @Entity() -export class Variable implements IVariable{ +export class Variable implements IVariable { @PrimaryGeneratedColumn('uuid') id: string @@ -13,10 +13,9 @@ export class Variable implements IVariable{ @Column({ nullable: true, type: 'text' }) value: string - @Column({default: 'string', type: 'text'}) + @Column({ default: 'string', type: 'text' }) type: string - @CreateDateColumn() createdDate: Date From 2661a42a136b5b546e26f39c53e6a381a5c1bf85 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Tue, 16 Jan 2024 22:13:43 +0800 Subject: [PATCH 260/502] modify google gemini based on requested changes --- packages/components/src/utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index eacfa4a0..2215eb41 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -681,7 +681,10 @@ export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[ */ export const convertMultiOptionsToStringArray = (inputString: string): string[] => { let ArrayString: string[] = [] - if (inputString) ArrayString = JSON.parse(inputString) - + try { + ArrayString = JSON.parse(inputString) + } catch (e) { + ArrayString = [] + } return ArrayString } From 0e7df3a5b51d2cc6f30c3faadcb0b711ee863f62 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 16 Jan 2024 20:52:25 +0530 Subject: [PATCH 261/502] Lock upstash redis version which will fix the credentials and deployment issues --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index c90ea5cc..c35419bc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -37,7 +37,7 @@ "@supabase/supabase-js": "^2.29.0", "@types/js-yaml": "^4.0.5", "@types/jsdom": "^21.1.1", - "@upstash/redis": "^1.22.1", + "@upstash/redis": "1.22.1", "@zilliz/milvus2-sdk-node": "^2.2.24", "apify-client": "^2.7.1", "axios": "1.6.2", From 019e7caac36dbc85fcdbf17db5b8f5a3664d53bc Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 16 Jan 2024 16:07:28 +0000 Subject: [PATCH 262/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.5.1?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index c35419bc..ddf09399 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.5.0", + "version": "1.5.1", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From f16b29503d58568f00301abdecdf8d046f70f4f7 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 16 Jan 2024 16:08:25 +0000 Subject: [PATCH 263/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.7=20relea?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index c5549b23..fef08851 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.6", + "version": "1.4.7", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From 8c1e62be425810b0d7cc7725e707a8415a670a4d Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 16 Jan 2024 16:09:13 +0000 Subject: [PATCH 264/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.10=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 12fbe20f..561694d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.9", + "version": "1.4.10", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index f1c0b7f7..79ff4961 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.9", + "version": "1.4.10", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 398a31f4265e4cc9f450ff2caccf9ed097478673 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 17 Jan 2024 00:39:14 +0000 Subject: [PATCH 265/502] UI touchup --- packages/server/src/Interface.ts | 5 + packages/server/src/index.ts | 87 +++--- packages/ui/src/api/chatflows.js | 1 + .../ui/src/ui-component/button/ImageButton.js | 57 ++++ .../ui-component/cards/StarterPromptsCard.css | 1 - .../ui-component/cards/StarterPromptsCard.js | 7 +- .../ui/src/views/chatmessage/ChatMessage.css | 32 +-- .../ui/src/views/chatmessage/ChatMessage.js | 268 ++++++++++-------- .../src/views/chatmessage/audio-recording.js | 11 + 9 files changed, 297 insertions(+), 172 deletions(-) create mode 100644 packages/ui/src/ui-component/button/ImageButton.js diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 942fe490..a944e064 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -214,3 +214,8 @@ export interface ICredentialReqBody { export interface ICredentialReturnResponse extends ICredential { plainDataObj: ICredentialDataDecrypted } + +export interface IUploadFileSizeAndTypes { + fileTypes: string[] + maxUploadSize: number +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f62e2c56..4451b838 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -21,7 +21,8 @@ import { chatType, IChatMessage, IDepthQueue, - INodeDirectedGraph + INodeDirectedGraph, + IUploadFileSizeAndTypes } from './Interface' import { getNodeModulesPackagePath, @@ -57,7 +58,7 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, IMessage, INodeOptionsValue, handleEscapeCharacters } from 'flowise-components' +import { ICommonObject, IMessage, INodeOptionsValue, INodeParams, handleEscapeCharacters } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' import { sanitizeMiddleware } from './utils/XSS' @@ -147,7 +148,9 @@ export class App { '/api/v1/node-icon/', '/api/v1/components-credentials-icon/', '/api/v1/chatflows-streaming', + '/api/v1/chatflows-uploads', '/api/v1/openai-assistants-file', + '/api/v1/get-upload-file', '/api/v1/ip' ] this.app.use((req, res, next) => { @@ -464,8 +467,45 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const obj = this.shouldAllowUploads(chatflow) - return res.json(obj) + const uploadAllowedNodes = ['OpenAIMultiModalChain', 'OpenAIWhisper'] + + try { + const flowObj = JSON.parse(chatflow.flowData) + let isUploadAllowed = false + const allowances: IUploadFileSizeAndTypes[] = [] + + flowObj.nodes.forEach((node: IReactFlowNode) => { + if (uploadAllowedNodes.indexOf(node.data.type) > -1) { + logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) + isUploadAllowed = true + + const allowance: IUploadFileSizeAndTypes = { + fileTypes: [], + maxUploadSize: 0 + } + + node.data.inputParams.map((param: INodeParams) => { + if (param.name === 'allowedUploadTypes') { + allowance.fileTypes = (param.default as string).split(';') + } + if (param.name === 'maxUploadSize') { + allowance.maxUploadSize = parseInt(param.default ? (param.default as string) : '0') + } + }) + + if (allowance.fileTypes && allowance.maxUploadSize) { + allowances.push(allowance) + } + } + }) + + return res.json({ + isUploadAllowed, + uploadFileSizeAndTypes: allowances + }) + } catch (e) { + return res.status(500).send(e) + } }) // ---------------------------------------- @@ -1058,10 +1098,14 @@ export class App { return res.status(500).send(`Invalid file path`) } const filePath = path.join(getUserHome(), '.flowise', 'gptvision', req.query.chatId as string, req.params.id) - console.log(filePath) - if (!path.isAbsolute(filePath) || !fs.existsSync(filePath)) { + //raise error if file path is not absolute + if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) + //raise error if file path contains '..' + if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) + //only return from the .flowise gptvision folder + if (!(filePath.includes('.flowise') && filePath.includes('gptvision') && filePath.includes(req.query.chatId as string))) return res.status(500).send(`Invalid file path`) - } + res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) streamFileToUser(res, filePath) }) @@ -1350,35 +1394,6 @@ export class App { }) } - private uploadAllowedNodes = ['OpenAIMultiModalChain', 'OpenAIWhisper'] - private shouldAllowUploads(result: ChatFlow): any { - const flowObj = JSON.parse(result.flowData) - let allowUploads = false - const allowances: any = [] - flowObj.nodes.forEach((node: IReactFlowNode) => { - if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) { - logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) - allowUploads = true - const allowance: any = {} - node.data.inputParams.map((param: any) => { - if (param.name === 'allowedUploadTypes') { - allowance.allowedTypes = param.default.split(';') - } - if (param.name === 'maxUploadSize') { - allowance.maxUploadSize = parseInt(param.default ? param.default : '0') - } - }) - if (allowance.allowedTypes && allowance.maxUploadSize) { - allowances.push(allowance) - } - } - }) - return { - allowUploads, - allowed: allowances - } - } - /** * Validate API Key * @param {Request} req diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index c02ca5cd..586fe183 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -13,6 +13,7 @@ const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body) const deleteChatflow = (id) => client.delete(`/chatflows/${id}`) const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`) + const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`) export default { diff --git a/packages/ui/src/ui-component/button/ImageButton.js b/packages/ui/src/ui-component/button/ImageButton.js new file mode 100644 index 00000000..7a10b966 --- /dev/null +++ b/packages/ui/src/ui-component/button/ImageButton.js @@ -0,0 +1,57 @@ +import { styled } from '@mui/material/styles' +import ButtonBase from '@mui/material/ButtonBase' + +export const ImageButton = styled(ButtonBase)(({ theme }) => ({ + position: 'relative', + height: 200, + borderRadius: '10px', + [theme.breakpoints.down('sm')]: { + width: '100% !important', // Overrides inline-style + height: 100 + }, + '&:hover, &.Mui-focusVisible': { + zIndex: 1, + '& .MuiImageBackdrop-root': { + opacity: 0.4 + }, + '& .MuiImageMarked-root': { + opacity: 1 + }, + '& .MuiTypography-root': { + border: '4px solid currentColor' + } + } +})) + +export const ImageSrc = styled('span')({ + position: 'absolute', + borderRadius: '10px', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundSize: 'cover', + backgroundPosition: 'center 40%' +}) + +export const ImageBackdrop = styled('span')(({ theme }) => ({ + position: 'absolute', + borderRadius: '10px', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundColor: theme.palette.common.black, + opacity: 0.1, + transition: theme.transitions.create('opacity') +})) + +export const ImageMarked = styled('span')(() => ({ + height: 25, + width: 25, + backgroundColor: 'transparent', + position: 'absolute', + top: 'auto', + left: 'auto', + opacity: 0 +})) diff --git a/packages/ui/src/ui-component/cards/StarterPromptsCard.css b/packages/ui/src/ui-component/cards/StarterPromptsCard.css index 85c2d415..028b8b34 100644 --- a/packages/ui/src/ui-component/cards/StarterPromptsCard.css +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.css @@ -1,6 +1,5 @@ .button-container { position: absolute; - bottom: 0; z-index: 1000; display: flex; overflow-x: auto; diff --git a/packages/ui/src/ui-component/cards/StarterPromptsCard.js b/packages/ui/src/ui-component/cards/StarterPromptsCard.js index 3abd8378..cfec4ba4 100644 --- a/packages/ui/src/ui-component/cards/StarterPromptsCard.js +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.js @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import { Chip } from '@mui/material' import './StarterPromptsCard.css' -const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => { +const StarterPromptsCard = ({ isGrid, starterPrompts, sx, onPromptClick }) => { return ( - + {starterPrompts.map((sp, index) => ( onPromptClick(sp.prompt, e)} /> ))} @@ -15,7 +15,8 @@ const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => { StarterPromptsCard.propTypes = { isGrid: PropTypes.bool, - starterPrompts: PropTypes.arrayOf(PropTypes.string), + starterPrompts: PropTypes.array, + sx: PropTypes.object, onPromptClick: PropTypes.func } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 3b0bb9e3..9e7a1857 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -146,6 +146,16 @@ align-items: center; } +.preview { + position: absolute; + bottom: 0; + z-index: 1000; + display: flex; + overflow-x: auto; + -webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */ + scrollbar-width: none; /* For Firefox */ +} + .file-drop-field { position: relative; /* Needed to position the icon correctly */ /* Other styling for the field */ @@ -162,26 +172,6 @@ flex-direction: column; justify-content: center; align-items: center; - z-index: 10; /* Ensure it's above other content */ + z-index: 2000; /* Ensure it's above other content */ border: 2px dashed #0094ff; /* Example style */ } - -.preview-container { - -} - -.preview-card { - border: 2px solid #E7EDF3; - border-radius: 16%; - transition: 0.4s; -} - -.preview-card&:hover { - border-color: #5B9FED; -} - - -.button { - flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ - margin: 5px; /* Adjust as needed for spacing between buttons */ -} \ No newline at end of file diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index d9003f72..0d969c5e 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' import { useSelector } from 'react-redux' import PropTypes from 'prop-types' import socketIOClient from 'socket.io-client' @@ -8,30 +8,33 @@ import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import axios from 'axios' -import audioUploadSVG from 'assets/images/wave-sound.jpg' import { Box, Button, Card, - CardActions, CardMedia, Chip, CircularProgress, Divider, - Grid, IconButton, InputAdornment, OutlinedInput, Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconCircleDot } from '@tabler/icons' +import { IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconCircleDot, IconTrash } from '@tabler/icons' +import robotPNG from 'assets/images/robot.png' +import userPNG from 'assets/images/account.png' +import audioUploadSVG from 'assets/images/wave-sound.jpg' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' import SourceDocDialog from 'ui-component/dialog/SourceDocDialog' +import StarterPromptsCard from 'ui-component/cards/StarterPromptsCard' +import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording' +import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from 'ui-component/button/ImageButton' import './ChatMessage.css' import './audio-recording.css' @@ -46,12 +49,14 @@ import useApi from 'hooks/useApi' // Const import { baseURL, maxScroll } from 'store/constant' -import robotPNG from 'assets/images/robot.png' -import userPNG from 'assets/images/account.png' -import StarterPromptsCard from '../../ui-component/cards/StarterPromptsCard' +// Utils import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' -import DeleteIcon from '@mui/icons-material/Delete' -import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording' + +const messageImageStyle = { + width: '128px', + height: '128px', + objectFit: 'cover' +} export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -76,13 +81,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const inputRef = useRef(null) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) const [starterPrompts, setStarterPrompts] = useState([]) // drag & drop and file input const fileUploadRef = useRef(null) - const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [previews, setPreviews] = useState([]) const [isDragOver, setIsDragOver] = useState(false) @@ -91,20 +96,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const [isRecording, setIsRecording] = useState(false) const [recordingNotSupported, setRecordingNotSupported] = useState(false) - const handleDragOver = (e) => { - if (!isChatFlowAvailableForUploads) { - return - } - e.preventDefault() - } const isFileAllowedForUpload = (file) => { const constraints = getAllowChatFlowUploads.data + /** + * {isUploadAllowed: boolean, uploadFileSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} + */ let acceptFile = false - if (constraints.allowUploads) { + if (constraints.isUploadAllowed) { const fileType = file.type const sizeInMB = file.size / 1024 / 1024 - constraints.allowed.map((allowed) => { - if (allowed.allowedTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { + constraints.uploadFileSizeAndTypes.map((allowed) => { + if (allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { acceptFile = true } }) @@ -114,11 +116,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } return acceptFile } + const handleDrop = async (e) => { if (!isChatFlowAvailableForUploads) { return } e.preventDefault() + e.stopPropagation() setIsDragOver(false) let files = [] if (e.dataTransfer.files.length > 0) { @@ -156,10 +160,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const newFiles = await Promise.all(files) setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) - // if (newFiles.length > 0) { - // document.getElementById('messagelist').style.height = '80%' - // } } + if (e.dataTransfer.items) { for (const item of e.dataTransfer.items) { if (item.kind === 'string' && item.type.match('^text/uri-list')) { @@ -191,6 +193,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } } + const handleFileChange = async (event) => { const fileObj = event.target.files && event.target.files[0] if (!fileObj) { @@ -247,9 +250,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } + const handleDragOver = (e) => { + e.preventDefault() + e.stopPropagation() + } + const handleDragEnter = (e) => { if (isChatFlowAvailableForUploads) { e.preventDefault() + e.stopPropagation() setIsDragOver(true) } } @@ -257,34 +266,27 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const handleDragLeave = (e) => { if (isChatFlowAvailableForUploads) { e.preventDefault() + e.stopPropagation() if (e.originalEvent?.pageX !== 0 || e.originalEvent?.pageY !== 0) { + setIsDragOver(false) return false } - setIsDragOver(false) // Set the drag over state to false when the drag leaves + setIsDragOver(false) } } + const handleDeletePreview = (itemToDelete) => { if (itemToDelete.type === 'file') { URL.revokeObjectURL(itemToDelete.preview) // Clean up for file } setPreviews(previews.filter((item) => item !== itemToDelete)) } + const handleUploadClick = () => { // 👇️ open file input box on click of another element fileUploadRef.current.click() } - const previewStyle = { - width: '128px', - height: '64px', - objectFit: 'fit' // This makes the image cover the area, cropping it if necessary - } - const messageImageStyle = { - width: '128px', - height: '128px', - objectFit: 'cover' // This makes the image cover the area, cropping it if necessary - } - const clearPreviews = () => { // Revoke the data uris to avoid memory leaks previews.forEach((file) => URL.revokeObjectURL(file.preview)) @@ -295,11 +297,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setIsRecording(true) startAudioRecording(setIsRecording, setRecordingNotSupported) } + const onRecordingCancelled = () => { cancelAudioRecording() setIsRecording(false) setRecordingNotSupported(false) } + const onRecordingStopped = () => { stopAudioRecording(addRecordingToPreviews) setIsRecording(false) @@ -505,7 +509,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // Get chatflow uploads capability useEffect(() => { if (getAllowChatFlowUploads.data) { - setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.allowUploads ?? false) + setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.isUploadAllowed ?? false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [getAllowChatFlowUploads.data]) @@ -544,12 +548,18 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { useEffect(() => { let socket if (open && chatflowid) { + // API request getChatmessageApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid) getAllowChatFlowUploads.request(chatflowid) getChatflowConfig.request(chatflowid) + + // Scroll to bottom scrollToBottom() + setIsRecording(false) + + // SocketIO socket = socketIOClient(baseURL) socket.on('connect', () => { @@ -584,20 +594,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, [open, chatflowid]) return ( -
    - {isDragOver && getAllowChatFlowUploads.data?.allowUploads && ( + <> + {isDragOver && getAllowChatFlowUploads.data?.isUploadAllowed && ( Drop here to upload - {getAllowChatFlowUploads.data.allowed.map((allowed) => { + {getAllowChatFlowUploads.data.uploadFileSizeAndTypes.map((allowed) => { return ( <> - {allowed.allowedTypes?.join(', ')} + {allowed.fileTypes?.join(', ')} Max Allowed Size: {allowed.maxUploadSize} MB ) @@ -639,7 +643,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { )} )} -
    +
    {messages && messages.map((message, index) => { @@ -687,6 +697,42 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })}
    )} + {message.fileUploads && message.fileUploads.length > 0 && ( +
    + {message.fileUploads.map((item, index) => { + return ( + <> + {item.mime.startsWith('image/') ? ( + + + + ) : ( + // eslint-disable-next-line jsx-a11y/media-has-caption + + )} + + ) + })} +
    + )}
    {/* Messages are being rendered in Markdown format */} { })}
    )} - {message.fileUploads && - message.fileUploads.map((item, index) => { - return ( - <> - {item.mime.startsWith('image/') ? ( - - - - ) : ( - // eslint-disable-next-line jsx-a11y/media-has-caption - - )} - - ) - })} {message.sourceDocuments && (
    {removeDuplicateURL(message).map((source, index) => { @@ -796,55 +818,79 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    {messages && messages.length === 1 && ( - + 0 ? 70 : 0 }} + starterPrompts={starterPrompts || []} + onPromptClick={handlePromptClick} + isGrid={isDialog} + /> )}
    - -
    + +
    {previews && previews.length > 0 && ( -
    -
    - - {previews.map((item, index) => ( - <> - {item.mime.startsWith('image/') ? ( - - - - - handleDeletePreview(item)} size='small'> - - - - - - ) : ( - - - - - handleDeletePreview(item)} size='small'> - - - - - - )} - - ))} - -
    -
    + + {previews.map((item, index) => ( + <> + {item.mime.startsWith('image/') ? ( + handleDeletePreview(item)} + > + + + + + + + ) : ( + + + handleDeletePreview(item)} size='small'> + + + + )} + + ))} + )} +
    +
    { maxRows={isDialog ? 7 : 2} startAdornment={ isChatFlowAvailableForUploads && ( - + { } /> {isChatFlowAvailableForUploads && ( - + )}
    setSourceDialogOpen(false)} /> -
    + ) } diff --git a/packages/ui/src/views/chatmessage/audio-recording.js b/packages/ui/src/views/chatmessage/audio-recording.js index f5cba001..1fbaddc1 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.js +++ b/packages/ui/src/views/chatmessage/audio-recording.js @@ -68,30 +68,39 @@ export function startAudioRecording(onRecordingStart, onUnsupportedBrowser) { //Error handling structure switch (error.name) { case 'AbortError': //error from navigator.mediaDevices.getUserMedia + // eslint-disable-next-line no-console console.log('An AbortError has occurred.') break case 'NotAllowedError': //error from navigator.mediaDevices.getUserMedia + // eslint-disable-next-line no-console console.log('A NotAllowedError has occurred. User might have denied permission.') break case 'NotFoundError': //error from navigator.mediaDevices.getUserMedia + // eslint-disable-next-line no-console console.log('A NotFoundError has occurred.') break case 'NotReadableError': //error from navigator.mediaDevices.getUserMedia + // eslint-disable-next-line no-console console.log('A NotReadableError has occurred.') break case 'SecurityError': //error from navigator.mediaDevices.getUserMedia or from the MediaRecorder.start + // eslint-disable-next-line no-console console.log('A SecurityError has occurred.') break case 'TypeError': //error from navigator.mediaDevices.getUserMedia + // eslint-disable-next-line no-console console.log('A TypeError has occurred.') break case 'InvalidStateError': //error from the MediaRecorder.start + // eslint-disable-next-line no-console console.log('An InvalidStateError has occurred.') break case 'UnknownError': //error from the MediaRecorder.start + // eslint-disable-next-line no-console console.log('An UnknownError has occurred.') break default: + // eslint-disable-next-line no-console console.log('An error occurred with the error name ' + error.name) } }) @@ -113,9 +122,11 @@ export function stopAudioRecording(addRecordingToPreviews) { //Error handling structure switch (error.name) { case 'InvalidStateError': //error from the MediaRecorder.stop + // eslint-disable-next-line no-console console.log('An InvalidStateError has occurred.') break default: + // eslint-disable-next-line no-console console.log('An error occurred with the error name ' + error.name) } }) From 3407fa92f4e39f8d4804257224639063deced46a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 17 Jan 2024 18:29:37 +0530 Subject: [PATCH 266/502] Compression Retriever - Cohere Rerank - Add max chunks per document as optional parameter --- .../CohereRerankRetriever/CohereRerank.ts | 6 ++++-- .../CohereRerankRetriever/CohereRerankRetriever.ts | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts index 55f3c4aa..f74b8365 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts @@ -7,11 +7,13 @@ export class CohereRerank extends BaseDocumentCompressor { private COHERE_API_URL = 'https://api.cohere.ai/v1/rerank' private readonly model: string private readonly k: number - constructor(cohereAPIKey: string, model: string, k: number) { + private readonly maxChunksPerDoc: number + constructor(cohereAPIKey: string, model: string, k: number, maxChunksPerDoc: number) { super() this.cohereAPIKey = cohereAPIKey this.model = model this.k = k + this.maxChunksPerDoc = maxChunksPerDoc } async compressDocuments( documents: Document>[], @@ -32,7 +34,7 @@ export class CohereRerank extends BaseDocumentCompressor { const data = { model: this.model, topN: this.k, - max_chunks_per_doc: 10, + max_chunks_per_doc: this.maxChunksPerDoc, query: query, return_documents: false, documents: documents.map((doc) => doc.pageContent) diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts index 3c1872b3..ca89ca77 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts @@ -67,6 +67,15 @@ class CohereRerankRetriever_Retrievers implements INode { default: 0, additionalParams: true, optional: true + }, + { + label: 'Max Chunks Per Document', + name: 'maxChunksPerDoc', + placeholder: '10', + type: 'number', + default: 10, + additionalParams: true, + optional: true } ] } @@ -78,12 +87,14 @@ class CohereRerankRetriever_Retrievers implements INode { const cohereApiKey = getCredentialParam('cohereApiKey', credentialData, nodeData) const topK = nodeData.inputs?.topK as string let k = topK ? parseFloat(topK) : 4 + const maxChunks = nodeData.inputs?.maxChunksPerDoc as string + let max = maxChunks ? parseInt(maxChunks) : 10 if (k <= 0) { k = (baseRetriever as VectorStoreRetriever).k } - const cohereCompressor = new CohereRerank(cohereApiKey, model, k) + const cohereCompressor = new CohereRerank(cohereApiKey, model, k, max) return new ContextualCompressionRetriever({ baseCompressor: cohereCompressor, baseRetriever: baseRetriever From 1bf7944776c83ceb3903fe9d33dba82f2acb1fdf Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 17 Jan 2024 15:55:56 +0000 Subject: [PATCH 267/502] update retrievers and add mmr to other vector stores --- .../CohereRerankRetriever/Cohere.svg | 1 + .../CohereRerankRetriever/CohereRerank.ts | 7 +- .../CohereRerankRetriever.ts | 74 ++++++++++++--- .../compressionRetriever.svg | 7 -- .../EmbeddingsFilterRetriever.ts | 62 ++++++++++--- .../retrievers/HydeRetriever/HydeRetriever.ts | 50 ++++++++++- .../LLMFilterCompressionRetriever.ts | 62 ++++++++++--- .../compressionRetriever.svg | 7 -- .../LLMFilterRetriever/llmFilterRetriever.svg | 1 + .../retrievers/RRFRetriever/RRFRetriever.ts | 64 ++++++++++--- .../RRFRetriever/compressionRetriever.svg | 7 -- .../retrievers/RRFRetriever/rrfRetriever.svg | 1 + .../SimilarityThresholdRetriever.ts | 21 +++-- .../nodes/vectorstores/Astra/Astra.ts | 16 +--- .../vectorstores/MongoDBAtlas/MongoDBAtlas.ts | 15 +--- .../nodes/vectorstores/Pinecone/Pinecone.ts | 2 +- .../marketplaces/chatflows/AutoGPT.json | 46 +++++++++- .../marketplaces/chatflows/BabyAGI.json | 46 +++++++++- .../Conversational Retrieval Agent.json | 46 +++++++++- .../Conversational Retrieval QA Chain.json | 46 +++++++++- .../chatflows/Metadata Filter.json | 46 +++++++++- .../chatflows/Multi Retrieval QA Chain.json | 90 ++++++++++++++++++- .../marketplaces/chatflows/WebPage QnA.json | 46 +++++++++- 23 files changed, 642 insertions(+), 121 deletions(-) create mode 100644 packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg delete mode 100644 packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg delete mode 100644 packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg create mode 100644 packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg delete mode 100644 packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg create mode 100644 packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg b/packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg new file mode 100644 index 00000000..88bcabe3 --- /dev/null +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/Cohere.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts index 55f3c4aa..e70c044f 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts @@ -7,11 +7,14 @@ export class CohereRerank extends BaseDocumentCompressor { private COHERE_API_URL = 'https://api.cohere.ai/v1/rerank' private readonly model: string private readonly k: number - constructor(cohereAPIKey: string, model: string, k: number) { + private readonly max_chunks_per_doc: number + + constructor(cohereAPIKey: string, model: string, k: number, max_chunks_per_doc: number) { super() this.cohereAPIKey = cohereAPIKey this.model = model this.k = k + this.max_chunks_per_doc = max_chunks_per_doc } async compressDocuments( documents: Document>[], @@ -32,8 +35,8 @@ export class CohereRerank extends BaseDocumentCompressor { const data = { model: this.model, topN: this.k, - max_chunks_per_doc: 10, query: query, + max_chunks_per_doc: this.max_chunks_per_doc, return_documents: false, documents: documents.map((doc) => doc.pageContent) } diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts index 3c1872b3..442fdc7a 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts @@ -1,7 +1,7 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { BaseRetriever } from 'langchain/schema/retriever' import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' -import { getCredentialData, getCredentialParam } from '../../../src' +import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src' import { CohereRerank } from './CohereRerank' import { VectorStoreRetriever } from 'langchain/vectorstores/base' @@ -15,16 +15,16 @@ class CohereRerankRetriever_Retrievers implements INode { category: string baseClasses: string[] inputs: INodeParams[] - outputs: INodeOutputsValue[] credential: INodeParams badge: string + outputs: INodeOutputsValue[] constructor() { this.label = 'Cohere Rerank Retriever' this.name = 'cohereRerankRetriever' this.version = 1.0 this.type = 'Cohere Rerank Retriever' - this.icon = 'compressionRetriever.svg' + this.icon = 'Cohere.svg' this.category = 'Retrievers' this.badge = 'NEW' this.description = 'Cohere Rerank indexes the documents from most to least semantically relevant to the query.' @@ -37,7 +37,7 @@ class CohereRerankRetriever_Retrievers implements INode { } this.inputs = [ { - label: 'Base Retriever', + label: 'Vector Store Retriever', name: 'baseRetriever', type: 'VectorStoreRetriever' }, @@ -58,36 +58,84 @@ class CohereRerankRetriever_Retrievers implements INode { default: 'rerank-english-v2.0', optional: true }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, { label: 'Top K', name: 'topK', description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', - placeholder: '0', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + }, + { + label: 'Max Chunks Per Doc', + name: 'maxChunksPerDoc', + description: 'The maximum number of chunks to produce internally from a document. Default to 10', + placeholder: '10', type: 'number', - default: 0, additionalParams: true, optional: true } ] + this.outputs = [ + { + label: 'Cohere Rerank Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] } - async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever const model = nodeData.inputs?.model as string + const query = nodeData.inputs?.query as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const cohereApiKey = getCredentialParam('cohereApiKey', credentialData, nodeData) const topK = nodeData.inputs?.topK as string - let k = topK ? parseFloat(topK) : 4 + const k = topK ? parseFloat(topK) : (baseRetriever as VectorStoreRetriever).k ?? 4 + const maxChunksPerDoc = nodeData.inputs?.maxChunksPerDoc as string + const max_chunks_per_doc = maxChunksPerDoc ? parseFloat(maxChunksPerDoc) : 10 + const output = nodeData.outputs?.output as string - if (k <= 0) { - k = (baseRetriever as VectorStoreRetriever).k - } + const cohereCompressor = new CohereRerank(cohereApiKey, model, k, max_chunks_per_doc) - const cohereCompressor = new CohereRerank(cohereApiKey, model, k) - return new ContextualCompressionRetriever({ + const retriever = new ContextualCompressionRetriever({ baseCompressor: cohereCompressor, baseRetriever: baseRetriever }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever } } diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg deleted file mode 100644 index 23c52d25..00000000 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/compressionRetriever.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts index d373704c..d1049fa4 100644 --- a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts +++ b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts @@ -3,6 +3,7 @@ import { BaseRetriever } from 'langchain/schema/retriever' import { Embeddings } from 'langchain/embeddings/base' import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' import { EmbeddingsFilter } from 'langchain/retrievers/document_compressors/embeddings_filter' +import { handleEscapeCharacters } from '../../../src/utils' class EmbeddingsFilterRetriever_Retrievers implements INode { label: string @@ -29,15 +30,22 @@ class EmbeddingsFilterRetriever_Retrievers implements INode { this.baseClasses = [this.type, 'BaseRetriever'] this.inputs = [ { - label: 'Base Retriever', + label: 'Vector Store Retriever', name: 'baseRetriever', type: 'VectorStoreRetriever' }, { label: 'Embeddings', name: 'embeddings', - type: 'Embeddings', - optional: false + type: 'Embeddings' + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true }, { label: 'Similarity Threshold', @@ -61,36 +69,64 @@ class EmbeddingsFilterRetriever_Retrievers implements INode { additionalParams: true } ] + this.outputs = [ + { + label: 'Embeddings Filter Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, input: string): Promise { const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever const embeddings = nodeData.inputs?.embeddings as Embeddings + const query = nodeData.inputs?.query as string const similarityThreshold = nodeData.inputs?.similarityThreshold as string const k = nodeData.inputs?.k as string + const output = nodeData.outputs?.output as string if (k === undefined && similarityThreshold === undefined) { throw new Error(`Must specify one of "k" or "similarity_threshold".`) } - let similarityThresholdNumber = 0.8 - if (similarityThreshold) { - similarityThresholdNumber = parseFloat(similarityThreshold) - } - let kNumber = 0.8 - if (k) { - kNumber = parseFloat(k) - } + const similarityThresholdNumber = similarityThreshold ? parseFloat(similarityThreshold) : 0.8 + const kNumber = k ? parseFloat(k) : undefined + const baseCompressor = new EmbeddingsFilter({ embeddings: embeddings, similarityThreshold: similarityThresholdNumber, k: kNumber }) - return new ContextualCompressionRetriever({ + const retriever = new ContextualCompressionRetriever({ baseCompressor, baseRetriever: baseRetriever }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever } } diff --git a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts index 10d9a6e7..10fff764 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts +++ b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts @@ -1,8 +1,9 @@ import { VectorStore } from 'langchain/vectorstores/base' -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { HydeRetriever, HydeRetrieverOptions, PromptKey } from 'langchain/retrievers/hyde' import { BaseLanguageModel } from 'langchain/base_language' import { PromptTemplate } from 'langchain/prompts' +import { handleEscapeCharacters } from '../../../src/utils' class HydeRetriever_Retrievers implements INode { label: string @@ -14,11 +15,12 @@ class HydeRetriever_Retrievers implements INode { category: string baseClasses: string[] inputs: INodeParams[] + outputs: INodeOutputsValue[] constructor() { - this.label = 'Hyde Retriever' + this.label = 'HyDE Retriever' this.name = 'HydeRetriever' - this.version = 2.0 + this.version = 3.0 this.type = 'HydeRetriever' this.icon = 'hyderetriever.svg' this.category = 'Retrievers' @@ -35,6 +37,14 @@ class HydeRetriever_Retrievers implements INode { name: 'vectorStore', type: 'VectorStore' }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, { label: 'Select Defined Prompt', name: 'promptKey', @@ -121,15 +131,34 @@ Passage:` optional: true } ] + this.outputs = [ + { + label: 'HyDE Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, input: string): Promise { const llm = nodeData.inputs?.model as BaseLanguageModel const vectorStore = nodeData.inputs?.vectorStore as VectorStore const promptKey = nodeData.inputs?.promptKey as PromptKey const customPrompt = nodeData.inputs?.customPrompt as string + const query = nodeData.inputs?.query as string const topK = nodeData.inputs?.topK as string const k = topK ? parseFloat(topK) : 4 + const output = nodeData.outputs?.output as string const obj: HydeRetrieverOptions = { llm, @@ -141,6 +170,19 @@ Passage:` else if (promptKey) obj.promptTemplate = promptKey const retriever = new HydeRetriever(obj) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + return retriever } } diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts index e044468f..6b710cf3 100644 --- a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts @@ -3,6 +3,7 @@ import { BaseRetriever } from 'langchain/schema/retriever' import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' import { BaseLanguageModel } from 'langchain/base_language' import { LLMChainExtractor } from 'langchain/retrievers/document_compressors/chain_extract' +import { handleEscapeCharacters } from '../../../src/utils' class LLMFilterCompressionRetriever_Retrievers implements INode { label: string @@ -22,7 +23,7 @@ class LLMFilterCompressionRetriever_Retrievers implements INode { this.name = 'llmFilterRetriever' this.version = 1.0 this.type = 'LLMFilterRetriever' - this.icon = 'compressionRetriever.svg' + this.icon = 'llmFilterRetriever.svg' this.category = 'Retrievers' this.badge = 'NEW' this.description = @@ -30,30 +31,69 @@ class LLMFilterCompressionRetriever_Retrievers implements INode { this.baseClasses = [this.type, 'BaseRetriever'] this.inputs = [ { - label: 'Base Retriever', + label: 'Vector Store Retriever', name: 'baseRetriever', type: 'VectorStoreRetriever' }, { label: 'Language Model', name: 'model', - type: 'BaseLanguageModel', - optional: true + type: 'BaseLanguageModel' + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + } + ] + this.outputs = [ + { + label: 'LLM Filter Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] } ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, input: string): Promise { const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever const model = nodeData.inputs?.model as BaseLanguageModel + const query = nodeData.inputs?.query as string + const output = nodeData.outputs?.output as string - if (model) { - return new ContextualCompressionRetriever({ - baseCompressor: LLMChainExtractor.fromLLM(model), - baseRetriever: baseRetriever - }) + if (!model) throw new Error('There must be a LLM model connected to LLM Filter Retriever') + + const retriever = new ContextualCompressionRetriever({ + baseCompressor: LLMChainExtractor.fromLLM(model), + baseRetriever: baseRetriever + }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) } - return {} + + return retriever } } diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg deleted file mode 100644 index 23c52d25..00000000 --- a/packages/components/nodes/retrievers/LLMFilterRetriever/compressionRetriever.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg b/packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg new file mode 100644 index 00000000..d3f4d15f --- /dev/null +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/llmFilterRetriever.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts index 3229b3a8..ed15ed24 100644 --- a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts +++ b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts @@ -1,9 +1,10 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { BaseLanguageModel } from 'langchain/base_language' import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' import { BaseRetriever } from 'langchain/schema/retriever' import { ReciprocalRankFusion } from './ReciprocalRankFusion' import { VectorStoreRetriever } from 'langchain/vectorstores/base' +import { handleEscapeCharacters } from '../../../src/utils' class RRFRetriever_Retrievers implements INode { label: string @@ -16,20 +17,21 @@ class RRFRetriever_Retrievers implements INode { baseClasses: string[] inputs: INodeParams[] badge: string + outputs: INodeOutputsValue[] constructor() { this.label = 'Reciprocal Rank Fusion Retriever' this.name = 'RRFRetriever' - this.version = 2.0 + this.version = 1.0 this.type = 'RRFRetriever' this.badge = 'NEW' - this.icon = 'compressionRetriever.svg' + this.icon = 'rrfRetriever.svg' this.category = 'Retrievers' this.description = 'Reciprocal Rank Fusion to re-rank search results by multiple query generation.' this.baseClasses = [this.type, 'BaseRetriever'] this.inputs = [ { - label: 'Base Retriever', + label: 'Vector Store Retriever', name: 'baseRetriever', type: 'VectorStoreRetriever' }, @@ -38,6 +40,14 @@ class RRFRetriever_Retrievers implements INode { name: 'model', type: 'BaseLanguageModel' }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, { label: 'Query Count', name: 'queryCount', @@ -54,7 +64,6 @@ class RRFRetriever_Retrievers implements INode { description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', placeholder: '0', type: 'number', - default: 0, additionalParams: true, optional: true }, @@ -71,27 +80,56 @@ class RRFRetriever_Retrievers implements INode { optional: true } ] + this.outputs = [ + { + label: 'Reciprocal Rank Fusion Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + baseClasses: ['Document'] + }, + { + label: 'Text', + name: 'text', + baseClasses: ['string', 'json'] + } + ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, input: string): Promise { const llm = nodeData.inputs?.model as BaseLanguageModel const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const query = nodeData.inputs?.query as string const queryCount = nodeData.inputs?.queryCount as string const q = queryCount ? parseFloat(queryCount) : 4 const topK = nodeData.inputs?.topK as string - let k = topK ? parseFloat(topK) : 4 + const k = topK ? parseFloat(topK) : (baseRetriever as VectorStoreRetriever).k ?? 4 const constantC = nodeData.inputs?.c as string - let c = topK ? parseFloat(constantC) : 60 - - if (k <= 0) { - k = (baseRetriever as VectorStoreRetriever).k - } + const c = topK ? parseFloat(constantC) : 60 + const output = nodeData.outputs?.output as string const ragFusion = new ReciprocalRankFusion(llm, baseRetriever as VectorStoreRetriever, q, k, c) - return new ContextualCompressionRetriever({ + const retriever = new ContextualCompressionRetriever({ baseCompressor: ragFusion, baseRetriever: baseRetriever }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever } } diff --git a/packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg b/packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg deleted file mode 100644 index 23c52d25..00000000 --- a/packages/components/nodes/retrievers/RRFRetriever/compressionRetriever.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg b/packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg new file mode 100644 index 00000000..56fbcc5a --- /dev/null +++ b/packages/components/nodes/retrievers/RRFRetriever/rrfRetriever.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts index a9f4b3d8..5f5a9ed0 100644 --- a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts +++ b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts @@ -18,7 +18,7 @@ class SimilarityThresholdRetriever_Retrievers implements INode { constructor() { this.label = 'Similarity Score Threshold Retriever' this.name = 'similarityThresholdRetriever' - this.version = 1.0 + this.version = 2.0 this.type = 'SimilarityThresholdRetriever' this.icon = 'similaritythreshold.svg' this.category = 'Retrievers' @@ -30,6 +30,14 @@ class SimilarityThresholdRetriever_Retrievers implements INode { name: 'vectorStore', type: 'VectorStore' }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, { label: 'Minimum Similarity Score (%)', name: 'minSimilarityScore', @@ -44,7 +52,8 @@ class SimilarityThresholdRetriever_Retrievers implements INode { description: `The maximum number of results to fetch`, type: 'number', default: 20, - step: 1 + step: 1, + additionalParams: true }, { label: 'K Increment', @@ -52,7 +61,8 @@ class SimilarityThresholdRetriever_Retrievers implements INode { description: `How much to increase K by each time. It'll fetch N results, then N + kIncrement, then N + kIncrement * 2, etc.`, type: 'number', default: 2, - step: 1 + step: 1, + additionalParams: true } ] this.outputs = [ @@ -77,6 +87,7 @@ class SimilarityThresholdRetriever_Retrievers implements INode { async init(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectorStore as VectorStore const minSimilarityScore = nodeData.inputs?.minSimilarityScore as number + const query = nodeData.inputs?.query as string const maxK = nodeData.inputs?.maxK as string const kIncrement = nodeData.inputs?.kIncrement as string @@ -89,11 +100,11 @@ class SimilarityThresholdRetriever_Retrievers implements INode { }) if (output === 'retriever') return retriever - else if (output === 'document') return await retriever.getRelevantDocuments(input) + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) else if (output === 'text') { let finaltext = '' - const docs = await retriever.getRelevantDocuments(input) + const docs = await retriever.getRelevantDocuments(query ? query : input) for (const doc of docs) finaltext += `${doc.pageContent}\n` diff --git a/packages/components/nodes/vectorstores/Astra/Astra.ts b/packages/components/nodes/vectorstores/Astra/Astra.ts index 865f1044..edaadc9c 100644 --- a/packages/components/nodes/vectorstores/Astra/Astra.ts +++ b/packages/components/nodes/vectorstores/Astra/Astra.ts @@ -4,6 +4,7 @@ import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData } from '../../../src/utils' import { AstraDBVectorStore, AstraLibArgs } from '@langchain/community/vectorstores/astradb' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class Astra_VectorStores implements INode { label: string @@ -26,7 +27,7 @@ class Astra_VectorStores implements INode { this.type = 'Astra' this.icon = 'astra.svg' this.category = 'Vector Stores' - this.description = `Upsert embedded data and perform similarity search upon query using DataStax Astra DB, a serverless vector database that’s perfect for managing mission-critical AI workloads` + this.description = `Upsert embedded data and perform similarity or mmr search upon query using DataStax Astra DB, a serverless vector database that’s perfect for managing mission-critical AI workloads` this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] this.badge = 'NEW' this.credential = { @@ -74,6 +75,7 @@ class Astra_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'Astra Retriever', @@ -139,9 +141,6 @@ class Astra_VectorStores implements INode { const embeddings = nodeData.inputs?.embeddings as Embeddings const vectorDimension = nodeData.inputs?.vectorDimension as number const similarityMetric = nodeData.inputs?.similarityMetric as 'cosine' | 'euclidean' | 'dot_product' | undefined - const output = nodeData.outputs?.output as string - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 const credentialData = await getCredentialData(nodeData.credential ?? '', options) @@ -176,14 +175,7 @@ class Astra_VectorStores implements INode { const vectorStore = await AstraDBVectorStore.fromExistingIndex(embeddings, astraConfig) - if (output === 'retriever') { - const retriever = vectorStore.asRetriever(k) - return retriever - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts index 9bc23f10..6ba7199f 100644 --- a/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts +++ b/packages/components/nodes/vectorstores/MongoDBAtlas/MongoDBAtlas.ts @@ -5,6 +5,7 @@ import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' class MongoDBAtlas_VectorStores implements INode { label: string @@ -24,7 +25,7 @@ class MongoDBAtlas_VectorStores implements INode { this.label = 'MongoDB Atlas' this.name = 'mongoDBAtlas' this.version = 1.0 - this.description = `Upsert embedded data and perform similarity search upon query using MongoDB Atlas, a managed cloud mongodb database` + this.description = `Upsert embedded data and perform similarity or mmr search upon query using MongoDB Atlas, a managed cloud mongodb database` this.type = 'MongoDB Atlas' this.icon = 'mongodb.svg' this.category = 'Vector Stores' @@ -95,6 +96,7 @@ class MongoDBAtlas_VectorStores implements INode { optional: true } ] + addMMRInputParams(this.inputs) this.outputs = [ { label: 'MongoDB Retriever', @@ -162,9 +164,6 @@ class MongoDBAtlas_VectorStores implements INode { let textKey = nodeData.inputs?.textKey as string let embeddingKey = nodeData.inputs?.embeddingKey as string const embeddings = nodeData.inputs?.embeddings as Embeddings - const topK = nodeData.inputs?.topK as string - const k = topK ? parseFloat(topK) : 4 - const output = nodeData.outputs?.output as string let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -181,13 +180,7 @@ class MongoDBAtlas_VectorStores implements INode { embeddingKey }) - if (output === 'retriever') { - return vectorStore.asRetriever(k) - } else if (output === 'vectorStore') { - ;(vectorStore as any).k = k - return vectorStore - } - return vectorStore + return resolveVectorStoreOrRetriever(nodeData, vectorStore) } } diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index 4b91a9b5..6623b1a2 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -24,7 +24,7 @@ class Pinecone_VectorStores implements INode { constructor() { this.label = 'Pinecone' this.name = 'pinecone' - this.version = 3.0 + this.version = 2.0 this.type = 'Pinecone' this.icon = 'pinecone.svg' this.category = 'Vector Stores' diff --git a/packages/server/marketplaces/chatflows/AutoGPT.json b/packages/server/marketplaces/chatflows/AutoGPT.json index 150fe17e..0062cd43 100644 --- a/packages/server/marketplaces/chatflows/AutoGPT.json +++ b/packages/server/marketplaces/chatflows/AutoGPT.json @@ -511,7 +511,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -552,6 +552,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -576,7 +615,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/BabyAGI.json b/packages/server/marketplaces/chatflows/BabyAGI.json index ab387205..81e3f230 100644 --- a/packages/server/marketplaces/chatflows/BabyAGI.json +++ b/packages/server/marketplaces/chatflows/BabyAGI.json @@ -166,7 +166,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -207,6 +207,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -231,7 +270,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 0e9e41bd..4378a47d 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -301,7 +301,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -342,6 +342,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -366,7 +405,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index e2fd6421..253a1dfc 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -541,7 +541,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -582,6 +582,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -606,7 +645,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index abd85d36..f7b2fbfb 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -625,7 +625,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -666,6 +666,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -690,7 +729,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "{\"id\":{\"$in\":[\"doc1\",\"doc2\"]}}", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json index 5388d965..e86b28c9 100644 --- a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json @@ -560,7 +560,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -601,6 +601,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -625,7 +664,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { @@ -840,6 +882,45 @@ "additionalParams": true, "optional": true, "id": "supabase_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -865,7 +946,10 @@ "tableName": "", "queryName": "", "supabaseMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index 1b1d8de6..df05feef 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -643,7 +643,7 @@ "type": "Pinecone", "baseClasses": ["Pinecone", "VectorStoreRetriever", "BaseRetriever"], "category": "Vector Stores", - "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "description": "Upsert embedded data and perform similarity or mmr search using Pinecone, a leading fully managed hosted vector database", "inputParams": [ { "label": "Connect Credential", @@ -684,6 +684,45 @@ "additionalParams": true, "optional": true, "id": "pinecone_0-input-topK-number" + }, + { + "label": "Search Type", + "name": "searchType", + "type": "options", + "default": "similarity", + "options": [ + { + "label": "Similarity", + "name": "similarity" + }, + { + "label": "Max Marginal Relevance", + "name": "mmr" + } + ], + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-searchType-options" + }, + { + "label": "Fetch K (for MMR Search)", + "name": "fetchK", + "description": "Number of initial documents to fetch for MMR reranking. Default to 20. Used only when the search type is MMR", + "placeholder": "20", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-fetchK-number" + }, + { + "label": "Lambda (for MMR Search)", + "name": "lambda", + "description": "Number between 0 and 1 that determines the degree of diversity among the results, where 0 corresponds to maximum diversity and 1 to minimum diversity. Used only when the search type is MMR", + "placeholder": "0.5", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pinecone_0-input-lambda-number" } ], "inputAnchors": [ @@ -708,7 +747,10 @@ "pineconeIndex": "", "pineconeNamespace": "", "pineconeMetadataFilter": "", - "topK": "" + "topK": "", + "searchType": "similarity", + "fetchK": "", + "lambda": "" }, "outputAnchors": [ { From f26a99ade246a11d8cead064807cdf6752e83fbc Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 17 Jan 2024 22:08:04 +0000 Subject: [PATCH 268/502] update figma loader --- .../nodes/documentloaders/Figma/Figma.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Figma/Figma.ts b/packages/components/nodes/documentloaders/Figma/Figma.ts index 3d313044..6d7aa530 100644 --- a/packages/components/nodes/documentloaders/Figma/Figma.ts +++ b/packages/components/nodes/documentloaders/Figma/Figma.ts @@ -1,6 +1,7 @@ import { getCredentialData, getCredentialParam } from '../../../src' import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { FigmaFileLoader, FigmaLoaderParams } from 'langchain/document_loaders/web/figma' +import { TextSplitter } from 'langchain/text_splitter' class Figma_DocumentLoaders implements INode { label: string @@ -71,6 +72,8 @@ class Figma_DocumentLoaders implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const nodeIds = (nodeData.inputs?.nodeIds as string)?.trim().split(',') || [] const fileKey = nodeData.inputs?.fileKey as string + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const metadata = nodeData.inputs?.metadata const credentialData = await getCredentialData(nodeData.credential ?? '', options) const accessToken = getCredentialParam('accessToken', credentialData, nodeData) @@ -82,7 +85,21 @@ class Figma_DocumentLoaders implements INode { } const loader = new FigmaFileLoader(figmaOptions) - const docs = await loader.load() + + const docs = textSplitter ? await loader.loadAndSplit() : await loader.load() + + if (metadata) { + const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata) + return docs.map((doc) => { + return { + ...doc, + metadata: { + ...doc.metadata, + ...parsedMetadata + } + } + }) + } return docs } From 4256655c7bace928ff4bc8e12ebe81696c9304f1 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 17 Jan 2024 23:47:11 +0000 Subject: [PATCH 269/502] add $vars and $flow to custom function --- .../nodes/tools/CustomTool/CustomTool.ts | 20 +----- .../components/nodes/tools/CustomTool/core.ts | 39 +--------- .../CustomFunction/CustomFunction.ts | 35 +++++---- .../IfElseFunction/IfElseFunction.ts | 35 +++++---- packages/components/src/Interface.ts | 6 ++ packages/components/src/utils.ts | 72 ++++++++++++++++++- packages/server/src/index.ts | 27 +++++-- packages/server/src/utils/index.ts | 3 + 8 files changed, 136 insertions(+), 101 deletions(-) diff --git a/packages/components/nodes/tools/CustomTool/CustomTool.ts b/packages/components/nodes/tools/CustomTool/CustomTool.ts index a983d0d9..6ba5bc26 100644 --- a/packages/components/nodes/tools/CustomTool/CustomTool.ts +++ b/packages/components/nodes/tools/CustomTool/CustomTool.ts @@ -1,5 +1,5 @@ import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { convertSchemaToZod, getBaseClasses } from '../../../src/utils' +import { convertSchemaToZod, getBaseClasses, getVars } from '../../../src/utils' import { DynamicStructuredTool } from './core' import { z } from 'zod' import { DataSource } from 'typeorm' @@ -81,23 +81,7 @@ class CustomTool_Tools implements INode { } if (customToolFunc) obj.code = customToolFunc - 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?.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.vars[propertyName] - } else { - // add it the variables, if not found locally in the db - variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.vars[propertyName] }) - } - } - } + const variables = await getVars(appDataSource, databaseEntities, nodeData) const flow = { chatflowId: options.chatflowid } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index b543aefa..19be88f1 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { NodeVM } from 'vm2' -import { availableDependencies } from '../../../src/utils' +import { availableDependencies, defaultAllowBuiltInDep, prepareSandboxVars } 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' @@ -112,48 +112,13 @@ export class DynamicStructuredTool< } } - // 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(vars, item.name, { - enumerable: true, - configurable: true, - writable: true, - value: value - }) - } - } - sandbox['$vars'] = vars + sandbox['$vars'] = prepareSandboxVars(this.variables) // inject flow properties if (this.flowObj) { sandbox['$flow'] = { ...this.flowObj, ...flowConfig } } - const defaultAllowBuiltInDep = [ - 'assert', - 'buffer', - 'crypto', - 'events', - 'http', - 'https', - 'net', - 'path', - 'querystring', - 'timers', - 'tls', - 'url', - 'zlib' - ] - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) : defaultAllowBuiltInDep diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts index 749c3a86..ff29d589 100644 --- a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -1,6 +1,7 @@ -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { NodeVM } from 'vm2' -import { availableDependencies, handleEscapeCharacters } from '../../../src/utils' +import { DataSource } from 'typeorm' +import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' class CustomFunction_Utilities implements INode { label: string @@ -55,9 +56,19 @@ class CustomFunction_Utilities implements INode { ] } - async init(nodeData: INodeData, input: string): Promise { + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { const javascriptFunction = nodeData.inputs?.javascriptFunction as string const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const variables = await getVars(appDataSource, databaseEntities, nodeData) + const flow = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId, + input + } let inputVars: ICommonObject = {} if (functionInputVariablesRaw) { @@ -70,6 +81,8 @@ class CustomFunction_Utilities implements INode { } let sandbox: any = { $input: input } + sandbox['$vars'] = prepareSandboxVars(variables) + sandbox['$flow'] = flow if (Object.keys(inputVars).length) { for (const item in inputVars) { @@ -81,22 +94,6 @@ class CustomFunction_Utilities implements INode { } } - const defaultAllowBuiltInDep = [ - 'assert', - 'buffer', - 'crypto', - 'events', - 'http', - 'https', - 'net', - 'path', - 'querystring', - 'timers', - 'tls', - 'url', - 'zlib' - ] - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) : defaultAllowBuiltInDep diff --git a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts index 862521eb..55339e1a 100644 --- a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts +++ b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts @@ -1,6 +1,7 @@ -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { NodeVM } from 'vm2' -import { availableDependencies } from '../../../src/utils' +import { DataSource } from 'typeorm' +import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils' class IfElseFunction_Utilities implements INode { label: string @@ -73,10 +74,20 @@ class IfElseFunction_Utilities implements INode { ] } - async init(nodeData: INodeData, input: string): Promise { + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { const ifFunction = nodeData.inputs?.ifFunction as string const elseFunction = nodeData.inputs?.elseFunction as string const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const variables = await getVars(appDataSource, databaseEntities, nodeData) + const flow = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId, + input + } let inputVars: ICommonObject = {} if (functionInputVariablesRaw) { @@ -89,6 +100,8 @@ class IfElseFunction_Utilities implements INode { } let sandbox: any = { $input: input } + sandbox['$vars'] = prepareSandboxVars(variables) + sandbox['$flow'] = flow if (Object.keys(inputVars).length) { for (const item in inputVars) { @@ -96,22 +109,6 @@ class IfElseFunction_Utilities implements INode { } } - const defaultAllowBuiltInDep = [ - 'assert', - 'buffer', - 'crypto', - 'events', - 'http', - 'https', - 'net', - 'path', - 'querystring', - 'timers', - 'tls', - 'url', - 'zlib' - ] - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) : defaultAllowBuiltInDep diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index d74ba1b4..fe08f070 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -29,6 +29,12 @@ export interface ICommonObject { [key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[] } +export interface IVariable { + name: string + value: string + type: string +} + export type IDatabaseEntity = { [key: string]: any } diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 2215eb41..7e9a68eb 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -5,7 +5,7 @@ import * as path from 'path' import { JSDOM } from 'jsdom' import { z } from 'zod' import { DataSource } from 'typeorm' -import { ICommonObject, IDatabaseEntity, IMessage, INodeData } from './Interface' +import { ICommonObject, IDatabaseEntity, IMessage, INodeData, IVariable } from './Interface' import { AES, enc } from 'crypto-js' import { ChatMessageHistory } from 'langchain/memory' import { AIMessage, HumanMessage, BaseMessage } from 'langchain/schema' @@ -70,6 +70,22 @@ export const availableDependencies = [ 'weaviate-ts-client' ] +export const defaultAllowBuiltInDep = [ + 'assert', + 'buffer', + 'crypto', + 'events', + 'http', + 'https', + 'net', + 'path', + 'querystring', + 'timers', + 'tls', + 'url', + 'zlib' +] + /** * Get base classes of components * @@ -688,3 +704,57 @@ export const convertMultiOptionsToStringArray = (inputString: string): string[] } return ArrayString } + +/** + * Get variables + * @param {DataSource} appDataSource + * @param {IDatabaseEntity} databaseEntities + * @param {INodeData} nodeData + */ +export const getVars = async (appDataSource: DataSource, databaseEntities: IDatabaseEntity, nodeData: INodeData) => { + const variables = ((await appDataSource.getRepository(databaseEntities['Variable']).find()) as IVariable[]) ?? [] + + // override variables defined in overrideConfig + // nodeData.inputs.variables is an Object, check each property and override the variable + 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.vars[propertyName] + } else { + // add it the variables, if not found locally in the db + variables.push({ name: propertyName, type: 'static', value: nodeData.inputs.vars[propertyName] }) + } + } + } + + return variables +} + +/** + * Prepare sandbox variables + * @param {IVariable[]} variables + */ +export const prepareSandboxVars = (variables: IVariable[]) => { + let vars = {} + if (variables) { + for (const item of variables) { + let value = item.value + + // read from .env file + if (item.type === 'runtime') { + value = process.env[item.name] ?? '' + } + + Object.defineProperty(vars, item.name, { + enumerable: true, + configurable: true, + writable: true, + value: value + }) + } + } + return vars +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 94a3b538..1986d207 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -294,7 +294,13 @@ export class App { const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() - const returnData = await newNodeInstance.init(nodeData) + const options: ICommonObject = { + appDataSource: this.AppDataSource, + databaseEntities, + logger + } + + const returnData = await newNodeInstance.init(nodeData, '', options) const result = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData return res.json(result) @@ -1448,6 +1454,11 @@ export class App { let chatId = incomingInput.chatId ?? '' let isUpsert = true + // Get session ID + const memoryNode = findMemoryNode(nodes, edges) + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) + const vsNodes = nodes.filter( (node) => node.data.category === 'Vector Stores' && @@ -1485,6 +1496,7 @@ export class App { incomingInput.question, chatHistory, chatId, + sessionId ?? '', chatflowid, this.AppDataSource, incomingInput?.overrideConfig, @@ -1562,6 +1574,12 @@ export class App { const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges + // Get session ID + const memoryNode = findMemoryNode(nodes, edges) + const memoryType = memoryNode?.data.label + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) + /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met: * - Node Data already exists in pool * - Still in sync (i.e the flow has not been modified since) @@ -1671,6 +1689,7 @@ export class App { incomingInput.question, chatHistory, chatId, + sessionId ?? '', chatflowid, this.AppDataSource, incomingInput?.overrideConfig, @@ -1700,12 +1719,6 @@ export class App { logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - const memoryNode = findMemoryNode(nodes, edges) - const memoryType = memoryNode?.data.label - - let sessionId = undefined - if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) - const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const nodeInstance = new nodeModule.nodeClass({ sessionId }) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index dafe612c..2d6acf58 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -273,6 +273,7 @@ export const buildLangchain = async ( question: string, chatHistory: IMessage[], chatId: string, + sessionId: string, chatflowid: string, appDataSource: DataSource, overrideConfig?: ICommonObject, @@ -317,6 +318,7 @@ export const buildLangchain = async ( logger.debug(`[server]: Upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, { chatId, + sessionId, chatflowid, chatHistory, logger, @@ -331,6 +333,7 @@ export const buildLangchain = async ( logger.debug(`[server]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) let outputResult = await newNodeInstance.init(reactFlowNodeData, question, { chatId, + sessionId, chatflowid, chatHistory, logger, From d02b5a89885495cbf28997da3e0f4ed3335e7372 Mon Sep 17 00:00:00 2001 From: Octavian Cioaca <132788754+domselardi@users.noreply.github.com> Date: Thu, 18 Jan 2024 01:47:33 +0100 Subject: [PATCH 270/502] Add autoSync github workflows --- .../workflows/autoSyncMergedPullRequest.yml | 24 ++++++++++++++ .github/workflows/autoSyncSingleCommit.yml | 31 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/autoSyncMergedPullRequest.yml create mode 100644 .github/workflows/autoSyncSingleCommit.yml diff --git a/.github/workflows/autoSyncMergedPullRequest.yml b/.github/workflows/autoSyncMergedPullRequest.yml new file mode 100644 index 00000000..5b1991d7 --- /dev/null +++ b/.github/workflows/autoSyncMergedPullRequest.yml @@ -0,0 +1,24 @@ +name: autoSyncMergedPullRequest +on: + pull_request: + types: + - closed + branches: [ "main" ] +jobs: + build: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Show PR info + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: | + echo The PR #${{ github.event.pull_request.number }} was merged on main branch! + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.AUTOSYNC_TOKEN }} + repository: ${{ secrets.AUTOSYNC_CH_URL }} + event-type: ${{ secrets.AUTOSYNC_PR_EVENT_TYPE }} + client-payload: '{"ref": "${{ github.ref }}", "prNumber": "${{ github.event.pull_request.number }}", "sha": "${{ github.sha }}"}' diff --git a/.github/workflows/autoSyncSingleCommit.yml b/.github/workflows/autoSyncSingleCommit.yml new file mode 100644 index 00000000..ce429a60 --- /dev/null +++ b/.github/workflows/autoSyncSingleCommit.yml @@ -0,0 +1,31 @@ +name: autoSyncSingleCommit +on: + push: + branches: + - main +jobs: + doNotAutoSyncSingleCommit: + if: github.event.commits[1] != null + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: IGNORE autoSyncSingleCommit + run: | + echo This single commit has came from a merged commit. We will ignore it. This case is handled in autoSyncMergedPullRequest workflow for merge commits comming from merged pull requests only! Beware, the regular merge commits are not handled by any workflow for the moment. + autoSyncSingleCommit: + if: github.event.commits[1] == null + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: autoSyncSingleCommit + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: | + echo Autosync a single commit with id: ${{ github.sha }} from openSource main branch towards cloud hosted version. + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.AUTOSYNC_TOKEN }} + repository: ${{ secrets.AUTOSYNC_CH_URL }} + event-type: ${{ secrets.AUTOSYNC_SC_EVENT_TYPE }} + client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' From 8a14a52d9072746d40b377866120a1d68802772a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 18 Jan 2024 13:03:27 +0530 Subject: [PATCH 271/502] GPT Vision: Renaming to OpenAIMultiModalChain and merging the functionality of Wisper. --- .../OpenAIMultiModalChain.ts} | 101 +++++++++++++----- .../OpenAIMultiModalChain}/VLLMChain.ts | 71 ++++++------ .../OpenAIMultiModalChain}/chain.svg | 0 .../nodes/multimodal/OpenAI/AudioWhisper.ts | 66 ------------ .../nodes/multimodal/OpenAI/audio.svg | 1 - .../nodes/multimodal/OpenAI/list.png | Bin 5002 -> 0 bytes packages/components/src/Interface.ts | 7 ++ packages/server/src/index.ts | 4 +- 8 files changed, 118 insertions(+), 132 deletions(-) rename packages/components/nodes/{multimodal/OpenAI/OpenAIVisionChain.ts => chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts} (77%) rename packages/components/nodes/{multimodal/OpenAI => chains/OpenAIMultiModalChain}/VLLMChain.ts (71%) rename packages/components/nodes/{multimodal/OpenAI => chains/OpenAIMultiModalChain}/chain.svg (100%) delete mode 100644 packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts delete mode 100644 packages/components/nodes/multimodal/OpenAI/audio.svg delete mode 100644 packages/components/nodes/multimodal/OpenAI/list.png diff --git a/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts b/packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts similarity index 77% rename from packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts rename to packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts index 1ff4f4c9..f62d58bc 100644 --- a/packages/components/nodes/multimodal/OpenAI/OpenAIVisionChain.ts +++ b/packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts @@ -1,10 +1,17 @@ -import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { + ICommonObject, + INode, + INodeData, + INodeOutputsValue, + INodeParams +} from "../../../src/Interface"; import { getBaseClasses, getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils' -import { OpenAIVisionChainInput, VLLMChain } from './VLLMChain' +import { OpenAIMultiModalChainInput, VLLMChain } from "./VLLMChain"; import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { formatResponse } from '../../outputparsers/OutputParserHelpers' +import { checkInputs, Moderation, streamResponse } from "../../moderation/Moderation"; -class OpenAIVisionChain_Chains implements INode { +class OpenAIMultiModalChain_Chains implements INode { label: string name: string version: number @@ -24,7 +31,7 @@ class OpenAIVisionChain_Chains implements INode { this.version = 1.0 this.type = 'OpenAIMultiModalChain' this.icon = 'chain.svg' - this.category = 'MultiModal' + this.category = 'Chains' this.badge = 'BETA' this.description = 'Chain to query against Image and Audio Input.' this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] @@ -35,18 +42,20 @@ class OpenAIVisionChain_Chains implements INode { credentialNames: ['openAIApi'] } this.inputs = [ - { - label: 'Audio Input', - name: 'audioInput', - type: 'OpenAIWhisper', - optional: true - }, { label: 'Prompt', name: 'prompt', type: 'BasePromptTemplate', optional: true }, + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'Model Name', name: 'modelName', @@ -55,14 +64,38 @@ class OpenAIVisionChain_Chains implements INode { { label: 'gpt-4-vision-preview', name: 'gpt-4-vision-preview' - }, - { - label: 'whisper-1', - name: 'whisper-1' } ], default: 'gpt-4-vision-preview' }, + { + label: 'Speech to Text', + name: 'speechToText', + type: 'boolean', + optional: true, + }, + // TODO: only show when speechToText is true + { + label: 'Speech to Text Method', + description: 'How to turn audio into text', + name: 'speechToTextMode', + type: 'options', + options: [ + { + label: 'Transcriptions', + name: 'transcriptions', + description: 'Transcribe audio into whatever language the audio is in. Default method when Speech to Text is turned on.' + }, + { + label: 'Translations', + name: 'translations', + description: 'Translate and transcribe the audio into english.' + } + ], + optional: false, + default: 'transcriptions', + additionalParams: true + }, { label: 'Image Resolution', description: 'This parameter controls the resolution in which the model views the image.', @@ -76,6 +109,10 @@ class OpenAIVisionChain_Chains implements INode { { label: 'High', name: 'high' + }, + { + label: 'Auto', + name: 'auto' } ], default: 'low', @@ -107,18 +144,11 @@ class OpenAIVisionChain_Chains implements INode { optional: true, additionalParams: true }, - { - label: 'Chain Name', - name: 'chainName', - type: 'string', - placeholder: 'Name Your Chain', - optional: true - }, { label: 'Accepted Upload Types', name: 'allowedUploadTypes', type: 'string', - default: 'image/gif;image/jpeg;image/png;image/webp', + default: 'image/gif;image/jpeg;image/png;image/webp;audio/mpeg;audio/x-wav;audio/mp4', hidden: true }, { @@ -154,19 +184,23 @@ class OpenAIVisionChain_Chains implements INode { const modelName = nodeData.inputs?.modelName as string const maxTokens = nodeData.inputs?.maxTokens as string const topP = nodeData.inputs?.topP as string - const whisperConfig = nodeData.inputs?.audioInput + const speechToText = nodeData.inputs?.speechToText as boolean - const fields: OpenAIVisionChainInput = { + + const fields: OpenAIMultiModalChainInput = { openAIApiKey: openAIApiKey, imageResolution: imageResolution, verbose: process.env.DEBUG === 'true', - imageUrls: options.uploads, + uploads: options.uploads, modelName: modelName } if (temperature) fields.temperature = parseFloat(temperature) if (maxTokens) fields.maxTokens = parseInt(maxTokens, 10) if (topP) fields.topP = parseFloat(topP) - if (whisperConfig) fields.whisperConfig = whisperConfig + if (speechToText) { + const speechToTextMode = nodeData.inputs?.speechToTextMode ?? 'transcriptions' + if (speechToTextMode) fields.speechToTextMode = speechToTextMode + } if (output === this.name) { const chain = new VLLMChain({ @@ -221,6 +255,17 @@ const runPrediction = async ( const isStreaming = options.socketIO && options.socketIOClientId const socketIO = isStreaming ? options.socketIO : undefined const socketIOClientId = isStreaming ? options.socketIOClientId : '' + const moderations = nodeData.inputs?.inputModeration as Moderation[] + if (moderations && moderations.length > 0) { + try { + // Use the output of the moderation chain as input for the LLM chain + input = await checkInputs(moderations, input) + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)) + streamResponse(isStreaming, e.message, socketIO, socketIOClientId) + return formatResponse(e.message) + } + } /** * Apply string transformation to reverse converted special chars: @@ -229,7 +274,7 @@ const runPrediction = async ( */ const promptValues = handleEscapeCharacters(promptValuesRaw, true) if (options?.uploads) { - chain.imageUrls = options.uploads + chain.uploads = options.uploads } if (promptValues && inputVariables.length > 0) { let seen: string[] = [] @@ -285,4 +330,4 @@ const runPrediction = async ( } } -module.exports = { nodeClass: OpenAIVisionChain_Chains } +module.exports = { nodeClass: OpenAIMultiModalChain_Chains } diff --git a/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts b/packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts similarity index 71% rename from packages/components/nodes/multimodal/OpenAI/VLLMChain.ts rename to packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts index dd44ebb5..2cf2ce95 100644 --- a/packages/components/nodes/multimodal/OpenAI/VLLMChain.ts +++ b/packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts @@ -1,27 +1,30 @@ -import { OpenAI as OpenAIClient, ClientOptions } from 'openai' +import { OpenAI as OpenAIClient, ClientOptions, OpenAI } from 'openai' import { BaseChain, ChainInputs } from 'langchain/chains' import { ChainValues } from 'langchain/schema' -import { BasePromptTemplate, ChatPromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' +import { BasePromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate } from 'langchain/prompts' import path from 'path' import { getUserHome } from '../../../src/utils' import fs from 'fs' +import { ChatCompletionContentPart, ChatCompletionMessageParam } from 'openai/src/resources/chat/completions' +import ChatCompletionCreateParamsNonStreaming = OpenAI.ChatCompletionCreateParamsNonStreaming +import { IFileUpload } from '../../../src' /** * Interface for the input parameters of the OpenAIVisionChain class. */ -export interface OpenAIVisionChainInput extends ChainInputs { +export interface OpenAIMultiModalChainInput extends ChainInputs { openAIApiKey?: string openAIOrganization?: string throwError?: boolean prompt?: BasePromptTemplate configuration?: ClientOptions - imageUrls?: [] - imageResolution?: string + uploads?: IFileUpload[] + imageResolution?: 'auto' | 'low' | 'high' temperature?: number modelName?: string maxTokens?: number topP?: number - whisperConfig?: any + speechToTextMode?: string } /** @@ -29,7 +32,7 @@ export interface OpenAIVisionChainInput extends ChainInputs { * Vision API. It extends the BaseChain class and implements the * OpenAIVisionChainInput interface. */ -export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { +export class VLLMChain extends BaseChain implements OpenAIMultiModalChainInput { static lc_name() { return 'VLLMChain' } @@ -37,8 +40,8 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { inputKey = 'input' outputKey = 'text' - imageUrls?: [] - imageResolution: string = 'low' + uploads?: IFileUpload[] + imageResolution: 'auto' | 'low' | 'high' openAIApiKey?: string openAIOrganization?: string clientConfig: ClientOptions @@ -49,9 +52,9 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { maxTokens?: number topP?: number - whisperConfig?: any + speechToTextMode?: any - constructor(fields: OpenAIVisionChainInput) { + constructor(fields: OpenAIMultiModalChainInput) { super(fields) this.throwError = fields?.throwError ?? false this.imageResolution = fields?.imageResolution ?? 'low' @@ -61,8 +64,8 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { this.modelName = fields?.modelName this.maxTokens = fields?.maxTokens this.topP = fields?.topP - this.imageUrls = fields?.imageUrls ?? [] - this.whisperConfig = fields?.whisperConfig ?? {} + this.uploads = fields?.uploads ?? [] + this.speechToTextMode = fields?.speechToTextMode ?? {} if (!this.openAIApiKey) { throw new Error('OpenAI API key not found') } @@ -81,8 +84,8 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { async _call(values: ChainValues): Promise { const userInput = values[this.inputKey] - const vRequest: any = { - model: this.modelName, + const vRequest: ChatCompletionCreateParamsNonStreaming = { + model: 'gpt-4-vision-preview', temperature: this.temperature, top_p: this.topP, messages: [] @@ -90,42 +93,42 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { if (this.maxTokens) vRequest.max_tokens = this.maxTokens else vRequest.max_tokens = 1024 - const userRole: any = { role: 'user' } - userRole.content = [] - userRole.content.push({ + const chatMessages: ChatCompletionContentPart[] = [] + const userRole: ChatCompletionMessageParam = { role: 'user', content: [] } + chatMessages.push({ type: 'text', text: userInput }) - if (this.whisperConfig && this.imageUrls && this.imageUrls.length > 0) { - const audioUploads = this.getAudioUploads(this.imageUrls) + if (this.speechToTextMode && this.uploads && this.uploads.length > 0) { + const audioUploads = this.getAudioUploads(this.uploads) for (const url of audioUploads) { const filePath = path.join(getUserHome(), '.flowise', 'gptvision', url.data, url.name) // as the image is stored in the server, read the file and convert it to base64 const audio_file = fs.createReadStream(filePath) - if (this.whisperConfig.purpose === 'transcription') { + if (this.speechToTextMode.purpose === 'transcriptions') { const transcription = await this.client.audio.transcriptions.create({ file: audio_file, model: 'whisper-1' }) - userRole.content.push({ + chatMessages.push({ type: 'text', text: transcription.text }) - } else if (this.whisperConfig.purpose === 'translation') { + } else if (this.speechToTextMode.purpose === 'translations') { const translation = await this.client.audio.translations.create({ file: audio_file, model: 'whisper-1' }) - userRole.content.push({ + chatMessages.push({ type: 'text', text: translation.text }) } } } - if (this.imageUrls && this.imageUrls.length > 0) { - const imageUploads = this.getImageUploads(this.imageUrls) + if (this.uploads && this.uploads.length > 0) { + const imageUploads = this.getImageUploads(this.uploads) for (const url of imageUploads) { let bf = url.data if (url.type == 'stored-file') { @@ -135,7 +138,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { const contents = fs.readFileSync(filePath) bf = 'data:' + url.mime + ';base64,' + contents.toString('base64') } - userRole.content.push({ + chatMessages.push({ type: 'image_url', image_url: { url: bf, @@ -144,6 +147,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { }) } } + userRole.content = chatMessages vRequest.messages.push(userRole) if (this.prompt && this.prompt instanceof ChatPromptTemplate) { let chatPrompt = this.prompt as ChatPromptTemplate @@ -151,12 +155,12 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { if (message instanceof SystemMessagePromptTemplate) { vRequest.messages.push({ role: 'system', - content: [ - { - type: 'text', - text: (message.prompt as any).template - } - ] + content: (message.prompt as any).template + }) + } else if (message instanceof HumanMessagePromptTemplate) { + vRequest.messages.push({ + role: 'user', + content: (message.prompt as any).template }) } }) @@ -164,7 +168,6 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { let response try { - // @ts-ignore response = await this.client.chat.completions.create(vRequest) } catch (error) { if (error instanceof Error) { diff --git a/packages/components/nodes/multimodal/OpenAI/chain.svg b/packages/components/nodes/chains/OpenAIMultiModalChain/chain.svg similarity index 100% rename from packages/components/nodes/multimodal/OpenAI/chain.svg rename to packages/components/nodes/chains/OpenAIMultiModalChain/chain.svg diff --git a/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts b/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts deleted file mode 100644 index aa2c71e1..00000000 --- a/packages/components/nodes/multimodal/OpenAI/AudioWhisper.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { INode, INodeData, INodeParams } from '../../../src' - -class OpenAIAudioWhisper implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - badge: string - category: string - baseClasses: string[] - inputs: INodeParams[] - - constructor() { - this.label = 'Open AI Whisper' - this.name = 'openAIAudioWhisper' - this.version = 1.0 - this.type = 'OpenAIWhisper' - this.description = 'Speech to text using OpenAI Whisper API' - this.icon = 'audio.svg' - this.badge = 'BETA' - this.category = 'MultiModal' - this.baseClasses = [this.type] - this.inputs = [ - { - label: 'Purpose', - name: 'purpose', - type: 'options', - options: [ - { - label: 'Transcription', - name: 'transcription' - }, - { - label: 'Translation', - name: 'translation' - } - ], - default: 'transcription' - }, - { - label: 'Accepted Upload Types', - name: 'allowedUploadTypes', - type: 'string', - default: 'audio/mpeg;audio/x-wav;audio/mp4', - hidden: true - }, - { - label: 'Maximum Upload Size (MB)', - name: 'maxUploadSize', - type: 'number', - default: '5', - hidden: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const purpose = nodeData.inputs?.purpose as string - - return { purpose } - } -} - -module.exports = { nodeClass: OpenAIAudioWhisper } diff --git a/packages/components/nodes/multimodal/OpenAI/audio.svg b/packages/components/nodes/multimodal/OpenAI/audio.svg deleted file mode 100644 index 3bcbbdcd..00000000 --- a/packages/components/nodes/multimodal/OpenAI/audio.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/components/nodes/multimodal/OpenAI/list.png b/packages/components/nodes/multimodal/OpenAI/list.png deleted file mode 100644 index acb4e5d68f200207a97e10ee63125eb4e040fcec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5002 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 abstract clearChatMessages(overrideSessionId?: string): Promise } + +export interface IFileUpload { + data: string + type: string + name: string + mime: string +} \ No newline at end of file diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4451b838..61aff470 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1695,9 +1695,7 @@ export class App { if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) if (endingNodeData && endingNodeData.category !== 'Chains' && endingNodeData.category !== 'Agents') { - if (endingNodeData.type !== 'OpenAIMultiModalChain') { - return res.status(500).send(`Ending node must be either a Chain or Agent`) - } + return res.status(500).send(`Ending node must be either a Chain or Agent`) } if ( From 188311187a13429a4c92d5059edd957854d02587 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 18 Jan 2024 13:04:25 +0530 Subject: [PATCH 272/502] GPT Vision: Fix for error when only speech input is sent. --- packages/server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 61aff470..7f1b9414 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1606,7 +1606,7 @@ export class App { if (incomingInput.uploads) { // @ts-ignore ;(incomingInput.uploads as any[]).forEach((upload: any) => { - if (upload.type === 'file') { + if (upload.type === 'file' || upload.type === 'audio') { const filename = upload.name const dir = path.join(getUserHome(), '.flowise', 'gptvision', chatId) if (!fs.existsSync(dir)) { @@ -1615,7 +1615,7 @@ export class App { const filePath = path.join(dir, filename) const splitDataURI = upload.data.split(',') const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - //TODO: check if file exists, what should we do if it exists? + //writes data to a file, replacing the file if it already exists. fs.writeFileSync(filePath, bf) // don't need to store the file contents in chatmessage, just the filename and chatId upload.data = chatId From 9222aafc6f23f80ec40323c11ed070706a7daa87 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 18 Jan 2024 17:04:49 +0530 Subject: [PATCH 273/502] GPT Vision: Updated behaviour to submit voice recording directly without the need to do another submit. --- .../OpenAIMultiModalChain.ts | 37 ++++++---- .../chains/OpenAIMultiModalChain/VLLMChain.ts | 70 +++++++++++-------- .../ui/src/views/chatmessage/ChatMessage.js | 9 ++- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts b/packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts index f62d58bc..a3f7e815 100644 --- a/packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts +++ b/packages/components/nodes/chains/OpenAIMultiModalChain/OpenAIMultiModalChain.ts @@ -1,15 +1,9 @@ -import { - ICommonObject, - INode, - INodeData, - INodeOutputsValue, - INodeParams -} from "../../../src/Interface"; +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils' -import { OpenAIMultiModalChainInput, VLLMChain } from "./VLLMChain"; +import { OpenAIMultiModalChainInput, VLLMChain } from './VLLMChain' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { formatResponse } from '../../outputparsers/OutputParserHelpers' -import { checkInputs, Moderation, streamResponse } from "../../moderation/Moderation"; +import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' class OpenAIMultiModalChain_Chains implements INode { label: string @@ -72,7 +66,7 @@ class OpenAIMultiModalChain_Chains implements INode { label: 'Speech to Text', name: 'speechToText', type: 'boolean', - optional: true, + optional: true }, // TODO: only show when speechToText is true { @@ -84,7 +78,8 @@ class OpenAIMultiModalChain_Chains implements INode { { label: 'Transcriptions', name: 'transcriptions', - description: 'Transcribe audio into whatever language the audio is in. Default method when Speech to Text is turned on.' + description: + 'Transcribe audio into whatever language the audio is in. Default method when Speech to Text is turned on.' }, { label: 'Translations', @@ -186,7 +181,6 @@ class OpenAIMultiModalChain_Chains implements INode { const topP = nodeData.inputs?.topP as string const speechToText = nodeData.inputs?.speechToText as boolean - const fields: OpenAIMultiModalChainInput = { openAIApiKey: openAIApiKey, imageResolution: imageResolution, @@ -256,6 +250,22 @@ const runPrediction = async ( const socketIO = isStreaming ? options.socketIO : undefined const socketIOClientId = isStreaming ? options.socketIOClientId : '' const moderations = nodeData.inputs?.inputModeration as Moderation[] + const speechToText = nodeData.inputs?.speechToText as boolean + + if (options?.uploads) { + if (options.uploads.length === 1 && input.length === 0) { + if (speechToText) { + //special case, text input is empty, but we have an upload (recorded audio) + const convertedText = await chain.processAudioWithWisper(options.uploads[0], undefined) + //so we use the upload as input + input = convertedText + } + // do not send the audio file to the model + } else { + chain.uploads = options.uploads + } + } + if (moderations && moderations.length > 0) { try { // Use the output of the moderation chain as input for the LLM chain @@ -273,9 +283,6 @@ const runPrediction = async ( * TO: { "value": "hello i am ben\n\n\thow are you?" } */ const promptValues = handleEscapeCharacters(promptValuesRaw, true) - if (options?.uploads) { - chain.uploads = options.uploads - } if (promptValues && inputVariables.length > 0) { let seen: string[] = [] diff --git a/packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts b/packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts index 2cf2ce95..5fcb6252 100644 --- a/packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts +++ b/packages/components/nodes/chains/OpenAIMultiModalChain/VLLMChain.ts @@ -101,42 +101,20 @@ export class VLLMChain extends BaseChain implements OpenAIMultiModalChainInput { }) if (this.speechToTextMode && this.uploads && this.uploads.length > 0) { const audioUploads = this.getAudioUploads(this.uploads) - for (const url of audioUploads) { - const filePath = path.join(getUserHome(), '.flowise', 'gptvision', url.data, url.name) - - // as the image is stored in the server, read the file and convert it to base64 - const audio_file = fs.createReadStream(filePath) - if (this.speechToTextMode.purpose === 'transcriptions') { - const transcription = await this.client.audio.transcriptions.create({ - file: audio_file, - model: 'whisper-1' - }) - chatMessages.push({ - type: 'text', - text: transcription.text - }) - } else if (this.speechToTextMode.purpose === 'translations') { - const translation = await this.client.audio.translations.create({ - file: audio_file, - model: 'whisper-1' - }) - chatMessages.push({ - type: 'text', - text: translation.text - }) - } + for (const upload of audioUploads) { + await this.processAudioWithWisper(upload, chatMessages) } } if (this.uploads && this.uploads.length > 0) { const imageUploads = this.getImageUploads(this.uploads) - for (const url of imageUploads) { - let bf = url.data - if (url.type == 'stored-file') { - const filePath = path.join(getUserHome(), '.flowise', 'gptvision', url.data, url.name) + for (const upload of imageUploads) { + let bf = upload.data + if (upload.type == 'stored-file') { + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) // as the image is stored in the server, read the file and convert it to base64 const contents = fs.readFileSync(filePath) - bf = 'data:' + url.mime + ';base64,' + contents.toString('base64') + bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64') } chatMessages.push({ type: 'image_url', @@ -182,6 +160,40 @@ export class VLLMChain extends BaseChain implements OpenAIMultiModalChainInput { } } + public async processAudioWithWisper(upload: IFileUpload, chatMessages: ChatCompletionContentPart[] | undefined): Promise { + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) + + // as the image is stored in the server, read the file and convert it to base64 + const audio_file = fs.createReadStream(filePath) + if (this.speechToTextMode === 'transcriptions') { + const transcription = await this.client.audio.transcriptions.create({ + file: audio_file, + model: 'whisper-1' + }) + if (chatMessages) { + chatMessages.push({ + type: 'text', + text: transcription.text + }) + } + return transcription.text + } else if (this.speechToTextMode === 'translations') { + const translation = await this.client.audio.translations.create({ + file: audio_file, + model: 'whisper-1' + }) + if (chatMessages) { + chatMessages.push({ + type: 'text', + text: translation.text + }) + } + return translation.text + } + //should never get here + return '' + } + getAudioUploads = (urls: any[]) => { return urls.filter((url: any) => url.mime.startsWith('audio/')) } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0d969c5e..82b17ded 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -304,10 +304,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setRecordingNotSupported(false) } - const onRecordingStopped = () => { + const onRecordingStopped = async () => { stopAudioRecording(addRecordingToPreviews) setIsRecording(false) setRecordingNotSupported(false) + handlePromptClick('') } const onSourceDialogClick = (data, title) => { @@ -366,7 +367,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (e) e.preventDefault() if (!promptStarterInput && userInput.trim() === '') { - return + if (!(previews.length === 1 && previews[0].type === 'audio')) { + return + } } let input = userInput @@ -626,7 +629,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    From f87d84997c0c948bc4572a225f248101e43c9b63 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 18 Jan 2024 17:11:43 +0530 Subject: [PATCH 274/502] GPT Vision: lint fixes --- packages/components/src/Interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index e7f6fe86..c098fdd8 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -240,4 +240,4 @@ export interface IFileUpload { type: string name: string mime: string -} \ No newline at end of file +} From db7915f426c1016f352de11d00bfdfe1ec14a31d Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 18 Jan 2024 15:21:01 +0000 Subject: [PATCH 275/502] Update artillery-load-test.yml --- artillery-load-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artillery-load-test.yml b/artillery-load-test.yml index 6b1c8140..809a2a8e 100644 --- a/artillery-load-test.yml +++ b/artillery-load-test.yml @@ -33,4 +33,4 @@ scenarios: # Seconds # Total Users = 2 + 3 + 3 = 8 # Each making 1 HTTP call -# Over a duration of 3 seconds +# Over a durations of 3 seconds From 0214ed1a7333d5ecc108a442393bf41f70026377 Mon Sep 17 00:00:00 2001 From: niztal Date: Thu, 18 Jan 2024 19:28:38 +0200 Subject: [PATCH 276/502] support pinecone serverless indexes --- packages/components/credentials/PineconeApi.credential.ts | 5 ----- packages/components/nodes/vectorstores/Pinecone/Pinecone.ts | 6 ++---- .../nodes/vectorstores/Pinecone/Pinecone_Existing.ts | 4 +--- .../nodes/vectorstores/Pinecone/Pinecone_Upsert.ts | 4 +--- packages/components/package.json | 2 +- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/components/credentials/PineconeApi.credential.ts b/packages/components/credentials/PineconeApi.credential.ts index 4c5f62fe..486c9834 100644 --- a/packages/components/credentials/PineconeApi.credential.ts +++ b/packages/components/credentials/PineconeApi.credential.ts @@ -16,11 +16,6 @@ class PineconeApi implements INodeCredential { label: 'Pinecone Api Key', name: 'pineconeApiKey', type: 'password' - }, - { - label: 'Pinecone Environment', - name: 'pineconeEnv', - type: 'string' } ] } diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index 6623b1a2..dd0c76f7 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -108,8 +108,7 @@ class Pinecone_VectorStores implements INode { const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) @@ -148,8 +147,7 @@ class Pinecone_VectorStores implements INode { const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts index 9eea70de..f805fc33 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Existing.ts @@ -95,11 +95,9 @@ class Pinecone_Existing_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts index cb54e6e9..14dbf663 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_Upsert.ts @@ -96,11 +96,9 @@ class PineconeUpsert_VectorStores implements INode { const credentialData = await getCredentialData(nodeData.credential ?? '', options) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) - const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData) const client = new Pinecone({ - apiKey: pineconeApiKey, - environment: pineconeEnv + apiKey: pineconeApiKey }) const pineconeIndex = client.Index(index) diff --git a/packages/components/package.json b/packages/components/package.json index 894014d4..0d4cd274 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -33,7 +33,7 @@ "@langchain/mistralai": "^0.0.6", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", - "@pinecone-database/pinecone": "^1.1.1", + "@pinecone-database/pinecone": "^2.0.1", "@qdrant/js-client-rest": "^1.2.2", "@supabase/supabase-js": "^2.29.0", "@types/js-yaml": "^4.0.5", From 953e1468bbfb2e51d4e958c4a4e8389fc6174a09 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 19 Jan 2024 00:02:31 +0000 Subject: [PATCH 277/502] add telemetry --- packages/server/package.json | 1 + packages/server/src/index.ts | 44 +++++++++++++++++++- packages/server/src/utils/index.ts | 56 ++++++++++++++++++++++++++ packages/server/src/utils/telemetry.ts | 50 +++++++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/utils/telemetry.ts diff --git a/packages/server/package.json b/packages/server/package.json index 79ff4961..fe27f8d6 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -60,6 +60,7 @@ "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "pg": "^8.11.1", + "posthog-node": "^3.5.0", "reflect-metadata": "^0.1.13", "sanitize-html": "^2.11.0", "socket.io": "^4.6.1", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1986d207..c64333dd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -45,7 +45,9 @@ import { getSessionChatHistory, getAllConnectedNodes, clearSessionMemory, - findMemoryNode + findMemoryNode, + getTelemetryFlowObj, + getAppVersion } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -64,6 +66,7 @@ import { sanitizeMiddleware } from './utils/XSS' import axios from 'axios' import { Client } from 'langchainhub' import { parsePrompt } from './utils/hub' +import { Telemetry } from './utils/telemetry' import { Variable } from './database/entities/Variable' export class App { @@ -71,6 +74,7 @@ export class App { nodesPool: NodesPool chatflowPool: ChatflowPool cachePool: CachePool + telemetry: Telemetry AppDataSource = getDataSource() constructor() { @@ -105,6 +109,9 @@ export class App { // Initialize cache pool this.cachePool = new CachePool() + + // Initialize telemetry + this.telemetry = new Telemetry() }) .catch((err) => { logger.error('❌ [server]: Error during Data Source initialization:', err) @@ -388,6 +395,12 @@ export class App { const chatflow = this.AppDataSource.getRepository(ChatFlow).create(newChatFlow) const results = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + await this.telemetry.sendTelemetry('chatflow_created', { + version: await getAppVersion(), + chatlowId: results.id, + flowGraph: getTelemetryFlowObj(JSON.parse(results.flowData)?.nodes, JSON.parse(results.flowData)?.edges) + }) + return res.json(results) }) @@ -674,6 +687,12 @@ export class App { const tool = this.AppDataSource.getRepository(Tool).create(newTool) const results = await this.AppDataSource.getRepository(Tool).save(tool) + await this.telemetry.sendTelemetry('tool_created', { + version: await getAppVersion(), + toolId: results.id, + toolName: results.name + }) + return res.json(results) }) @@ -880,6 +899,11 @@ export class App { const assistant = this.AppDataSource.getRepository(Assistant).create(newAssistant) const results = await this.AppDataSource.getRepository(Assistant).save(assistant) + await this.telemetry.sendTelemetry('assistant_created', { + version: await getAppVersion(), + assistantId: results.id + }) + return res.json(results) }) @@ -1508,6 +1532,15 @@ export class App { const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id)) this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) + + await this.telemetry.sendTelemetry('vector_upserted', { + version: await getAppVersion(), + chatlowId: chatflowid, + type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + flowGraph: getTelemetryFlowObj(nodes, edges), + stopNodeId + }) + return res.status(201).send('Successfully Upserted') } catch (e: any) { logger.error('[server]: Error:', e) @@ -1784,6 +1817,14 @@ export class App { await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) + await this.telemetry.sendTelemetry('prediction_sent', { + version: await getAppVersion(), + chatlowId: chatflowid, + chatId, + sessionId, + type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + flowGraph: getTelemetryFlowObj(nodes, edges) + }) // Only return ChatId when its Internal OR incoming input has ChatId, to avoid confusion when calling API if (incomingInput.chatId || isInternal) result.chatId = chatId @@ -1798,6 +1839,7 @@ export class App { async stopApp() { try { const removePromises: any[] = [] + removePromises.push(this.telemetry.flush()) await Promise.all(removePromises) } catch (e) { logger.error(`❌[server]: Flowise Server shut down error: ${e}`) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 2d6acf58..a232094e 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1082,3 +1082,59 @@ export const getAllValuesFromJson = (obj: any): any[] => { extractValues(obj) return values } + +/** + * Get only essential flow data items for telemetry + * @param {IReactFlowNode[]} nodes + * @param {IReactFlowEdge[]} edges + */ +export const getTelemetryFlowObj = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]) => { + const nodeData = nodes.map((node) => node.id) + const edgeData = edges.map((edge) => ({ source: edge.source, target: edge.target })) + return { nodes: nodeData, edges: edgeData } +} + +/** + * Get user settings file + * TODO: move env variables to settings json file, easier configuration + */ +export const getUserSettingsFilePath = () => { + const checkPaths = [path.join(getUserHome(), '.flowise', 'settings.json')] + for (const checkPath of checkPaths) { + if (fs.existsSync(checkPath)) { + return checkPath + } + } + return '' +} + +/** + * Get app current version + */ +export const getAppVersion = async () => { + const getPackageJsonPath = (): string => { + const checkPaths = [ + path.join(__dirname, '..', 'package.json'), + path.join(__dirname, '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', '..', '..', 'package.json') + ] + for (const checkPath of checkPaths) { + if (fs.existsSync(checkPath)) { + return checkPath + } + } + return '' + } + + const packagejsonPath = getPackageJsonPath() + if (!packagejsonPath) return '' + try { + const content = await fs.promises.readFile(packagejsonPath, 'utf8') + const parsedContent = JSON.parse(content) + return parsedContent.version + } catch (error) { + return '' + } +} diff --git a/packages/server/src/utils/telemetry.ts b/packages/server/src/utils/telemetry.ts new file mode 100644 index 00000000..4254ea76 --- /dev/null +++ b/packages/server/src/utils/telemetry.ts @@ -0,0 +1,50 @@ +import { v4 as uuidv4 } from 'uuid' +import { PostHog } from 'posthog-node' +import path from 'path' +import fs from 'fs' +import { getUserHome, getUserSettingsFilePath } from '.' + +export class Telemetry { + postHog?: PostHog + + constructor() { + if (process.env.DISABLE_FLOWISE_TELEMETRY !== 'true') { + this.postHog = new PostHog('phc_jEDuFYnOnuXsws986TLWzuisbRjwFqTl9JL8tDMgqme') + } else { + this.postHog = undefined + } + } + + async id(): Promise { + try { + const settingsContent = await fs.promises.readFile(getUserSettingsFilePath(), 'utf8') + const settings = JSON.parse(settingsContent) + return settings.instanceId + } catch (error) { + const instanceId = uuidv4() + const settings = { + instanceId + } + const defaultLocation = path.join(getUserHome(), '.flowise', 'settings.json') + await fs.promises.writeFile(defaultLocation, JSON.stringify(settings, null, 2)) + return instanceId + } + } + + async sendTelemetry(event: string, properties = {}): Promise { + if (this.postHog) { + const distinctId = await this.id() + this.postHog.capture({ + event, + distinctId, + properties + }) + } + } + + async flush(): Promise { + if (this.postHog) { + await this.postHog.shutdownAsync() + } + } +} From b5de88784bd718989b0a8102ef59c36a020b4c5b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 19 Jan 2024 00:38:08 +0000 Subject: [PATCH 278/502] add flag --- CONTRIBUTING-ZH.md | 1 + CONTRIBUTING.md | 1 + docker/.env.example | 4 +++- docker/docker-compose.yml | 1 + packages/server/.env.example | 2 ++ packages/server/src/commands/start.ts | 6 +++++- 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING-ZH.md b/CONTRIBUTING-ZH.md index 5a752280..7e35d194 100644 --- a/CONTRIBUTING-ZH.md +++ b/CONTRIBUTING-ZH.md @@ -138,6 +138,7 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | DATABASE_NAME | 数据库名称(当 DATABASE_TYPE 不是 sqlite 时) | 字符串 | | | SECRETKEY_PATH | 保存加密密钥(用于加密/解密凭据)的位置 | 字符串 | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | 加密密钥用于替代存储在 SECRETKEY_PATH 中的密钥 | 字符串 | +| DISABLE_FLOWISE_TELEMETRY | 关闭遥测 | 字符串 | 您也可以在使用 `npx` 时指定环境变量。例如: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04cb80b4..88d1aaac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,6 +141,7 @@ Flowise support different environment variables to configure your instance. You | DATABASE_SSL | Database connection overssl (When DATABASE_TYPE is postgre) | Boolean | false | | SECRETKEY_PATH | Location where encryption key (used to encrypt/decrypt credentials) is saved | String | `your-path/Flowise/packages/server` | | FLOWISE_SECRETKEY_OVERWRITE | Encryption key to be used instead of the key stored in SECRETKEY_PATH | String | +| DISABLE_FLOWISE_TELEMETRY | Turn off telemetry | Boolean | You can also specify the env variables when using `npx`. For example: diff --git a/docker/.env.example b/docker/.env.example index 7d4f1699..0fe69dd1 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -25,4 +25,6 @@ LOG_PATH=/root/.flowise/logs # LANGCHAIN_TRACING_V2=true # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key -# LANGCHAIN_PROJECT=your_project \ No newline at end of file +# LANGCHAIN_PROJECT=your_project + +# DISABLE_FLOWISE_TELEMETRY=true \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 92688469..c8c88bf3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -22,6 +22,7 @@ services: - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} - LOG_LEVEL=${LOG_LEVEL} - LOG_PATH=${LOG_PATH} + - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY} ports: - '${PORT}:${PORT}' volumes: diff --git a/packages/server/.env.example b/packages/server/.env.example index 6e746a4d..ed54ac66 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -26,3 +26,5 @@ PORT=3000 # LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # LANGCHAIN_API_KEY=your_api_key # LANGCHAIN_PROJECT=your_project + +# DISABLE_FLOWISE_TELEMETRY=true \ No newline at end of file diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index d4e8cfdb..08cd8298 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -39,7 +39,8 @@ export default class Start extends Command { LANGCHAIN_TRACING_V2: Flags.string(), LANGCHAIN_ENDPOINT: Flags.string(), LANGCHAIN_API_KEY: Flags.string(), - LANGCHAIN_PROJECT: Flags.string() + LANGCHAIN_PROJECT: Flags.string(), + DISABLE_FLOWISE_TELEMETRY: Flags.string() } async stopProcess() { @@ -113,6 +114,9 @@ export default class Start extends Command { if (flags.LANGCHAIN_API_KEY) process.env.LANGCHAIN_API_KEY = flags.LANGCHAIN_API_KEY if (flags.LANGCHAIN_PROJECT) process.env.LANGCHAIN_PROJECT = flags.LANGCHAIN_PROJECT + // Telemetry + if (flags.DISABLE_FLOWISE_TELEMETRY) process.env.DISABLE_FLOWISE_TELEMETRY = flags.DISABLE_FLOWISE_TELEMETRY + await (async () => { try { logger.info('Starting Flowise...') From 2640f6b1dc1d86a29f7d70a436a87f26ae55b239 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 19 Jan 2024 00:49:52 +0000 Subject: [PATCH 279/502] update prediction_sent metadata --- packages/server/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index c64333dd..3c5766fd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1821,7 +1821,6 @@ export class App { version: await getAppVersion(), chatlowId: chatflowid, chatId, - sessionId, type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, flowGraph: getTelemetryFlowObj(nodes, edges) }) From e7edbc695cd41bf59cfb206d1bb0b15a77c0bcb8 Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 19 Jan 2024 12:29:41 +0530 Subject: [PATCH 280/502] Add api endpoint for fetching links from a url --- packages/server/src/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 94a3b538..c7079ed9 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -57,7 +57,7 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, IMessage, INodeOptionsValue, handleEscapeCharacters } from 'flowise-components' +import { ICommonObject, IMessage, INodeOptionsValue, handleEscapeCharacters, webCrawl, xmlScrape } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' import { sanitizeMiddleware } from './utils/XSS' @@ -1087,6 +1087,19 @@ export class App { } }) + // ---------------------------------------- + // Scraper + // ---------------------------------------- + + this.app.get('/api/v1/fetch-links', async (req: Request, res: Response) => { + const url = decodeURIComponent(req.query.url as string) + const relativeLinksMethod = req.query.relativeLinksMethod as string + if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) + const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, 0) : await xmlScrape(url, 0) + + res.json({ status: 'OK', links }) + }) + // ---------------------------------------- // Upsert // ---------------------------------------- From 1b8813a8b92df702fc26922dd361f018b486b4ee Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 19 Jan 2024 12:32:58 +0530 Subject: [PATCH 281/502] Show a manage links button for web scraper nodes - cheerio, puppeteer, playwright --- .../ui/src/views/canvas/NodeInputHandler.js | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index a673d6b7..6c7a277e 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -28,6 +28,8 @@ import ToolDialog from 'views/tools/ToolDialog' import AssistantDialog from 'views/assistants/AssistantDialog' import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog' import FormatPromptValuesDialog from 'ui-component/dialog/FormatPromptValuesDialog' +import PromptLangsmithHubDialog from 'ui-component/dialog/PromptLangsmithHubDialog' +import ManageScrapedLinksDialog from 'ui-component/dialog/ManageScrapedLinksDialog' import CredentialInputHandler from './CredentialInputHandler' // utils @@ -35,7 +37,6 @@ import { getInputVariables } from 'utils/genericHelper' // const import { FLOWISE_CREDENTIAL_ID } from 'store/constant' -import PromptLangsmithHubDialog from '../../ui-component/dialog/PromptLangsmithHubDialog' const EDITABLE_OPTIONS = ['selectedTool', 'selectedAssistant'] @@ -62,22 +63,25 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false) const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({}) const [showPromptHubDialog, setShowPromptHubDialog] = useState(false) + const [showManageScrapedLinksDialog, setShowManageScrapedLinksDialog] = useState(false) + const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({}) const onExpandDialogClicked = (value, inputParam) => { - const dialogProp = { + const dialogProps = { value, inputParam, disabled, confirmButtonName: 'Save', cancelButtonName: 'Cancel' } - setExpandDialogProps(dialogProp) + setExpandDialogProps(dialogProps) setShowExpandDialog(true) } const onShowPromptHubButtonClicked = () => { setShowPromptHubDialog(true) } + const onShowPromptHubButtonSubmit = (templates) => { setShowPromptHubDialog(false) for (const t of templates) { @@ -86,6 +90,23 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA } } } + + const onManageLinksDialogClicked = (url, selectedLinks) => { + const dialogProps = { + url, + selectedLinks, + confirmButtonName: 'Save', + cancelButtonName: 'Cancel' + } + setManageScrapedLinksDialogProps(dialogProps) + setShowManageScrapedLinksDialog(true) + } + + const onManageLinksDialogSave = (links) => { + setShowManageScrapedLinksDialog(false) + data.inputs.selectedLinks = links + } + const onEditJSONClicked = (value, inputParam) => { // Preset values if the field is format prompt values let inputValue = value @@ -436,6 +457,37 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
    )} + {(data.name === 'cheerioWebScraper' || + data.name === 'puppeteerWebScraper' || + data.name === 'playwrightWebScraper') && + inputParam.name === 'url' && ( + <> + + setShowManageScrapedLinksDialog(false)} + onSave={onManageLinksDialogSave} + /> + + )} )} From 9637c122974e8236a8fcc8d9487cfd5cdbd64e72 Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 19 Jan 2024 12:33:54 +0530 Subject: [PATCH 282/502] Show a dialog to fetch and manage links in web scraper nodes --- .../dialog/ManageScrapedLinksDialog.js | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js diff --git a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js new file mode 100644 index 00000000..ecfcd403 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js @@ -0,0 +1,184 @@ +import PropTypes from 'prop-types' +import { createPortal } from 'react-dom' +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' + +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + IconButton, + OutlinedInput, + Stack, + Typography +} from '@mui/material' +import { IconTrash } from '@tabler/icons' +import PerfectScrollbar from 'react-perfect-scrollbar' + +import { BackdropLoader } from 'ui-component/loading/BackdropLoader' +import { StyledButton } from 'ui-component/button/StyledButton' + +import scraperApi from 'api/scraper' + +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' + +const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + const [loading, setLoading] = useState(false) + const [selectedLinks, setSelectedLinks] = useState([]) + const [url, setUrl] = useState('') + + useEffect(() => { + if (dialogProps.url) setUrl(dialogProps.url) + if (dialogProps.selectedLinks) setSelectedLinks(dialogProps.selectedLinks) + + return () => { + setLoading(false) + setSelectedLinks([]) + setUrl('') + } + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const handleFetchLinks = async () => { + setLoading(true) + const fetchLinksResp = await scraperApi.fetchAllLinks(url, 'webCrawl') + if (fetchLinksResp.data) { + setSelectedLinks(fetchLinksResp.data.links) + } + setLoading(false) + } + + const handleChangeLink = (index, event) => { + const { value } = event.target + const links = [...selectedLinks] + links[index] = value + setSelectedLinks(links) + } + + const handleRemoveLink = (index) => { + const links = [...selectedLinks] + links.splice(index, 1) + setSelectedLinks(links) + } + + const handleSaveLinks = () => { + onSave(selectedLinks) + } + + const component = show ? ( + + + {dialogProps.title || `Manage Scraped Links - ${url}`} + + + + + + { + setUrl(e.target.value) + }} + /> + + + + + Scraped Links + {selectedLinks.length > 0 ? ( + + {selectedLinks.map((link, index) => ( +
    + + handleChangeLink(index, e)} + size='small' + value={link} + name={`link_${index}`} + /> + + + handleRemoveLink(index)} + edge='end' + > + + + +
    + ))} +
    + ) : ( + <> + {loading && } +
    + Links scraped from the URL will appear here +
    + + )} +
    + + + + Save + + +
    + ) : null + + return createPortal(component, portalElement) +} + +ManageScrapedLinksDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onSave: PropTypes.func +} + +export default ManageScrapedLinksDialog From 43fa1166df61f1c5027429f99c8f41833227f4cb Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 19 Jan 2024 12:34:22 +0530 Subject: [PATCH 283/502] Add interface for fetching links from server --- packages/ui/src/api/scraper.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/ui/src/api/scraper.js diff --git a/packages/ui/src/api/scraper.js b/packages/ui/src/api/scraper.js new file mode 100644 index 00000000..382a9263 --- /dev/null +++ b/packages/ui/src/api/scraper.js @@ -0,0 +1,8 @@ +import client from './client' + +const fetchAllLinks = (url, relativeLinksMethod) => + client.get(`/fetch-links?url=${encodeURIComponent(url)}&relativeLinksMethod=${relativeLinksMethod}`) + +export default { + fetchAllLinks +} From bfa26a72c4fd6b373aea58a6f5e88a8842367685 Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 19 Jan 2024 14:25:04 +0530 Subject: [PATCH 284/502] Use selected links if available when scraping in cheerio, puppeteer, and playwright nodes --- .../nodes/documentloaders/Cheerio/Cheerio.ts | 12 +++++++++++- .../nodes/documentloaders/Playwright/Playwright.ts | 12 +++++++++++- .../nodes/documentloaders/Puppeteer/Puppeteer.ts | 12 +++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts index aa899bcb..e883c097 100644 --- a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts +++ b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts @@ -90,6 +90,7 @@ class Cheerio_DocumentLoaders implements INode { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string + const selectedLinks = nodeData.inputs?.selectedLinks as string[] let limit = nodeData.inputs?.limit as string let url = nodeData.inputs?.url as string @@ -127,13 +128,22 @@ class Cheerio_DocumentLoaders implements INode { if (!limit) limit = '10' else if (parseInt(limit) < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = - relativeLinksMethod === 'webCrawl' ? await webCrawl(url, parseInt(limit)) : await xmlScrape(url, parseInt(limit)) + selectedLinks && selectedLinks.length > 0 + ? selectedLinks.slice(0, parseInt(limit)) + : relativeLinksMethod === 'webCrawl' + ? await webCrawl(url, parseInt(limit)) + : await xmlScrape(url, parseInt(limit)) if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { docs.push(...(await cheerioLoader(page))) } if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + } else if (selectedLinks && selectedLinks.length > 0) { + if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) + for (const page of selectedLinks) { + docs.push(...(await cheerioLoader(page))) + } } else { docs = await cheerioLoader(url) } diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index eb246045..65be3ce7 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -118,6 +118,7 @@ class Playwright_DocumentLoaders implements INode { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string + const selectedLinks = nodeData.inputs?.selectedLinks as string[] let limit = nodeData.inputs?.limit as string let waitUntilGoToOption = nodeData.inputs?.waitUntilGoToOption as 'load' | 'domcontentloaded' | 'networkidle' | 'commit' | undefined let waitForSelector = nodeData.inputs?.waitForSelector as string @@ -168,13 +169,22 @@ class Playwright_DocumentLoaders implements INode { if (!limit) limit = '10' else if (parseInt(limit) < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = - relativeLinksMethod === 'webCrawl' ? await webCrawl(url, parseInt(limit)) : await xmlScrape(url, parseInt(limit)) + selectedLinks && selectedLinks.length > 0 + ? selectedLinks.slice(0, parseInt(limit)) + : relativeLinksMethod === 'webCrawl' + ? await webCrawl(url, parseInt(limit)) + : await xmlScrape(url, parseInt(limit)) if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { docs.push(...(await playwrightLoader(page))) } if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + } else if (selectedLinks && selectedLinks.length > 0) { + if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) + for (const page of selectedLinks) { + docs.push(...(await playwrightLoader(page))) + } } else { docs = await playwrightLoader(url) } diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 4691eb94..d5539659 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -119,6 +119,7 @@ class Puppeteer_DocumentLoaders implements INode { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string + const selectedLinks = nodeData.inputs?.selectedLinks as string[] let limit = nodeData.inputs?.limit as string let waitUntilGoToOption = nodeData.inputs?.waitUntilGoToOption as PuppeteerLifeCycleEvent let waitForSelector = nodeData.inputs?.waitForSelector as string @@ -169,13 +170,22 @@ class Puppeteer_DocumentLoaders implements INode { if (!limit) limit = '10' else if (parseInt(limit) < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = - relativeLinksMethod === 'webCrawl' ? await webCrawl(url, parseInt(limit)) : await xmlScrape(url, parseInt(limit)) + selectedLinks && selectedLinks.length > 0 + ? selectedLinks.slice(0, parseInt(limit)) + : relativeLinksMethod === 'webCrawl' + ? await webCrawl(url, parseInt(limit)) + : await xmlScrape(url, parseInt(limit)) if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { docs.push(...(await puppeteerLoader(page))) } if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + } else if (selectedLinks && selectedLinks.length > 0) { + if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) + for (const page of selectedLinks) { + docs.push(...(await puppeteerLoader(page))) + } } else { docs = await puppeteerLoader(url) } From e774bd3c12a98bff6066dcf155da629ccb50b0c4 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 19 Jan 2024 18:02:05 +0530 Subject: [PATCH 285/502] GPT Vision: Added multi model capabilities to ChatOpenAI and ConversationChain. --- .../ConversationChain/ConversationChain.ts | 35 ++++++-- .../nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 89 ++++++++++++++++++- packages/components/src/MultiModalUtils.ts | 87 ++++++++++++++++++ packages/server/src/index.ts | 43 +++++---- .../ui/src/views/chatmessage/ChatMessage.js | 4 +- 5 files changed, 229 insertions(+), 29 deletions(-) create mode 100644 packages/components/src/MultiModalUtils.ts diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index fcd9921e..0bba9b3c 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -1,4 +1,4 @@ -import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { FlowiseMemory, ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { ConversationChain } from 'langchain/chains' import { getBaseClasses } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' @@ -8,6 +8,8 @@ import { flatten } from 'lodash' import { Document } from 'langchain/document' import { RunnableSequence } from 'langchain/schema/runnable' import { StringOutputParser } from 'langchain/schema/output_parser' +import { addImagesToMessages, processSpeechToText } from '../../../src/MultiModalUtils' +import { HumanMessage } from 'langchain/schema' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` const inputKey = 'input' @@ -67,13 +69,15 @@ class ConversationChain_Chains implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) + const chain = prepareChain(nodeData, options, this.sessionId) return chain } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory - const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) + input = await processSpeechToText(nodeData, input, options) + + const chain = prepareChain(nodeData, options, this.sessionId) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) @@ -105,7 +109,7 @@ class ConversationChain_Chains implements INode { } } -const prepareChatPrompt = (nodeData: INodeData) => { +const prepareChatPrompt = (nodeData: INodeData, options: ICommonObject) => { const memory = nodeData.inputs?.memory as FlowiseMemory const prompt = nodeData.inputs?.systemMessagePrompt as string const docs = nodeData.inputs?.document as Document[] @@ -128,16 +132,31 @@ const prepareChatPrompt = (nodeData: INodeData) => { if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` - const chatPrompt = ChatPromptTemplate.fromMessages([ + // TODO: add audio uploads + // if (options.uploads.length > 0) { + // const audioUploads = getAudioUploads(options.uploads) + // for (const upload of audioUploads) { + // await this.processAudioWithWhisper(upload, chatMessages) + // } + // } + const imageContent = addImagesToMessages(nodeData, options) + + //TODO, this should not be any[], what interface should it be? + let promptMessages: any[] = [ SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) - ]) + ] + if (imageContent.length > 0) { + promptMessages.push(new HumanMessage({ content: imageContent })) + } + const chatPrompt = ChatPromptTemplate.fromMessages(promptMessages) return chatPrompt } -const prepareChain = (nodeData: INodeData, sessionId?: string, chatHistory: IMessage[] = []) => { +const prepareChain = (nodeData: INodeData, options: ICommonObject, sessionId?: string) => { + const chatHistory = options.chatHistory const model = nodeData.inputs?.model as BaseChatModel const memory = nodeData.inputs?.memory as FlowiseMemory const memoryKey = memory.memoryKey ?? 'chat_history' @@ -150,7 +169,7 @@ const prepareChain = (nodeData: INodeData, sessionId?: string, chatHistory: IMes return history } }, - prepareChatPrompt(nodeData), + prepareChatPrompt(nodeData, options), model, new StringOutputParser() ]) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 49326163..bc5814d0 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -19,7 +19,7 @@ class ChatOpenAI_ChatModels implements INode { constructor() { this.label = 'ChatOpenAI' this.name = 'chatOpenAI' - this.version = 2.0 + this.version = 3.0 this.type = 'ChatOpenAI' this.icon = 'openai.svg' this.category = 'Chat Models' @@ -152,6 +152,73 @@ class ChatOpenAI_ChatModels implements INode { type: 'json', optional: true, additionalParams: true + }, + { + label: 'Allow Image Uploads', + name: 'allowImageUploads', + type: 'boolean', + default: false, + optional: true + }, + { + label: 'Allow Audio Uploads', + name: 'allowAudioUploads', + type: 'boolean', + default: false, + optional: true + }, + { + label: 'Allow Speech to Text', + name: 'allowSpeechToText', + type: 'boolean', + default: false, + optional: true + }, + // TODO: only show when speechToText is true + { + label: 'Speech to Text Method', + description: 'How to turn audio into text', + name: 'speechToTextMode', + type: 'options', + options: [ + { + label: 'Transcriptions', + name: 'transcriptions', + description: + 'Transcribe audio into whatever language the audio is in. Default method when Speech to Text is turned on.' + }, + { + label: 'Translations', + name: 'translations', + description: 'Translate and transcribe the audio into english.' + } + ], + optional: false, + default: 'transcriptions', + additionalParams: true + }, + { + label: 'Image Resolution', + description: 'This parameter controls the resolution in which the model views the image.', + name: 'imageResolution', + type: 'options', + options: [ + { + label: 'Low', + name: 'low' + }, + { + label: 'High', + name: 'high' + }, + { + label: 'Auto', + name: 'auto' + } + ], + default: 'low', + optional: false, + additionalParams: true } ] } @@ -168,6 +235,12 @@ class ChatOpenAI_ChatModels implements INode { const basePath = nodeData.inputs?.basepath as string const baseOptions = nodeData.inputs?.baseOptions + const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean + const allowAudioUploads = nodeData.inputs?.allowAudioUploads as boolean + const allowSpeechToText = nodeData.inputs?.allowSpeechToText as boolean + const speechToTextMode = nodeData.inputs?.speechToTextMode as string + const imageResolution = nodeData.inputs?.imageResolution as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) @@ -200,6 +273,20 @@ class ChatOpenAI_ChatModels implements INode { basePath, baseOptions: parsedBaseOptions }) + + const multiModal = { + allowImageUploads: allowImageUploads ?? false, + allowAudioUploads: allowAudioUploads ?? false, + allowSpeechToText: allowSpeechToText ?? false, + imageResolution, + speechToTextMode + } + Object.defineProperty(model, 'multiModal', { + enumerable: true, + configurable: true, + writable: true, + value: multiModal + }) return model } } diff --git a/packages/components/src/MultiModalUtils.ts b/packages/components/src/MultiModalUtils.ts new file mode 100644 index 00000000..513915a5 --- /dev/null +++ b/packages/components/src/MultiModalUtils.ts @@ -0,0 +1,87 @@ +import { ICommonObject, INodeData } from './Interface' +import { BaseChatModel } from 'langchain/chat_models/base' +import { type ClientOptions, OpenAIClient } from '@langchain/openai' +import { ChatOpenAI } from 'langchain/chat_models/openai' +import path from 'path' +import { getUserHome } from './utils' +import fs from 'fs' +import { MessageContent } from '@langchain/core/dist/messages' + +export const processSpeechToText = async (nodeData: INodeData, input: string, options: ICommonObject) => { + const MODEL_NAME = 'whisper-1' + + let model = nodeData.inputs?.model as BaseChatModel + if (model instanceof ChatOpenAI && (model as any).multiModal) { + const multiModalConfig = (model as any).multiModal + if (options?.uploads) { + if (options.uploads.length === 1 && input.length === 0 && options.uploads[0].mime === 'audio/webm') { + const upload = options.uploads[0] + //special case, text input is empty, but we have an upload (recorded audio) + if (multiModalConfig.allowSpeechToText) { + const openAIClientOptions: ClientOptions = { + apiKey: model.openAIApiKey, + organization: model.organization + } + const openAIClient = new OpenAIClient(openAIClientOptions) + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) + + // as the image is stored in the server, read the file and convert it to base64 + const audio_file = fs.createReadStream(filePath) + + if (multiModalConfig.speechToTextMode === 'transcriptions') { + const transcription = await openAIClient.audio.transcriptions.create({ + file: audio_file, + model: MODEL_NAME + }) + return transcription.text + } else if (multiModalConfig.speechToTextMode === 'translations') { + const translation = await openAIClient.audio.translations.create({ + file: audio_file, + model: MODEL_NAME + }) + return translation.text + } + } else { + throw new Error('Speech to text is not selected, but found a recorded audio file. Please fix the chain.') + } + } + } + } + return input +} + +export const addImagesToMessages = (nodeData: INodeData, options: ICommonObject): MessageContent => { + const imageContent: MessageContent = [] + let model = nodeData.inputs?.model as BaseChatModel + if (model instanceof ChatOpenAI && (model as any).multiModal) { + if (options?.uploads && options?.uploads.length > 0) { + const imageUploads = getImageUploads(options.uploads) + for (const upload of imageUploads) { + let bf = upload.data + if (upload.type == 'stored-file') { + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) + + // as the image is stored in the server, read the file and convert it to base64 + const contents = fs.readFileSync(filePath) + bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64') + } + imageContent.push({ + type: 'image_url', + image_url: { + url: bf, + detail: 'low' + } + }) + } + } + } + return imageContent +} + +export const getAudioUploads = (uploads: any[]) => { + return uploads.filter((url: any) => url.mime.startsWith('audio/')) +} + +export const getImageUploads = (uploads: any[]) => { + return uploads.filter((url: any) => url.mime.startsWith('image/')) +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7f1b9414..da1057a9 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -467,40 +467,45 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const uploadAllowedNodes = ['OpenAIMultiModalChain', 'OpenAIWhisper'] + const uploadAllowedCategoryNodes = ['Chat Models'] try { const flowObj = JSON.parse(chatflow.flowData) - let isUploadAllowed = false const allowances: IUploadFileSizeAndTypes[] = [] - + let allowSpeechToText = false + let allowImageUploads = false + let allowAudioUploads = false flowObj.nodes.forEach((node: IReactFlowNode) => { - if (uploadAllowedNodes.indexOf(node.data.type) > -1) { + if (uploadAllowedCategoryNodes.indexOf(node.data.category) > -1) { logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) - isUploadAllowed = true - - const allowance: IUploadFileSizeAndTypes = { - fileTypes: [], - maxUploadSize: 0 - } + // there could be multiple components allowing uploads, so we check if it's already added + // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties node.data.inputParams.map((param: INodeParams) => { - if (param.name === 'allowedUploadTypes') { - allowance.fileTypes = (param.default as string).split(';') + if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads'] && !allowImageUploads) { + allowances.push({ + fileTypes: 'image/gif;image/jpeg;image/png;image/webp'.split(';'), + maxUploadSize: 5 + }) + allowImageUploads = true } - if (param.name === 'maxUploadSize') { - allowance.maxUploadSize = parseInt(param.default ? (param.default as string) : '0') + if (param.name === 'allowAudioUploads' && node.data.inputs?.['allowAudioUploads'] && !allowAudioUploads) { + allowances.push({ + fileTypes: 'audio/mpeg;audio/x-wav;audio/mp4'.split(';'), + maxUploadSize: 5 + }) + allowAudioUploads = true + } + if (param.name === 'allowSpeechToText' && node.data.inputs?.['allowSpeechToText']) { + allowSpeechToText = true } }) - - if (allowance.fileTypes && allowance.maxUploadSize) { - allowances.push(allowance) - } } }) return res.json({ - isUploadAllowed, + allowSpeechToText: allowSpeechToText, + isUploadAllowed: allowances.length > 0, uploadFileSizeAndTypes: allowances }) } catch (e) { diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 82b17ded..155b3e99 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -74,6 +74,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { ]) const [socketIOClientId, setSocketIOClientId] = useState('') const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false) + const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false) const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) const [chatId, setChatId] = useState(undefined) @@ -513,6 +514,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { useEffect(() => { if (getAllowChatFlowUploads.data) { setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.isUploadAllowed ?? false) + setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.allowSpeechToText ?? false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [getAllowChatFlowUploads.data]) @@ -922,7 +924,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } endAdornment={ <> - {isChatFlowAvailableForUploads && ( + {isChatFlowAvailableForSpeech && ( onMicrophonePressed()} From 31a4769079c73601596130cf2244899da79c7eee Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 19 Jan 2024 14:27:40 +0000 Subject: [PATCH 286/502] fix output prediction output parser --- .../nodes/chains/LLMChain/LLMChain.ts | 11 ++++++++-- .../CustomFunction/CustomFunction.ts | 11 ++++++++++ .../IfElseFunction/IfElseFunction.ts | 21 ++++++++++++++++--- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index b7c055e4..f83fc36a 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -82,7 +82,7 @@ class LLMChain_Chains implements INode { const model = nodeData.inputs?.model as BaseLanguageModel const prompt = nodeData.inputs?.prompt const output = nodeData.outputs?.output as string - const promptValues = prompt.promptValues as ICommonObject + let promptValues: ICommonObject | undefined = nodeData.inputs?.prompt.promptValues as ICommonObject const llmOutputParser = nodeData.inputs?.outputParser as BaseOutputParser this.outputParser = llmOutputParser if (llmOutputParser) { @@ -107,17 +107,24 @@ class LLMChain_Chains implements INode { verbose: process.env.DEBUG === 'true' }) const inputVariables = chain.prompt.inputVariables as string[] // ["product"] + promptValues = injectOutputParser(this.outputParser, chain, promptValues) const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) // eslint-disable-next-line no-console console.log('\x1b[92m\x1b[1m\n*****OUTPUT PREDICTION*****\n\x1b[0m\x1b[0m') // eslint-disable-next-line no-console console.log(res) + + let finalRes = res + if (this.outputParser && typeof res === 'object' && Object.prototype.hasOwnProperty.call(res, 'json')) { + finalRes = (res as ICommonObject).json + } + /** * Apply string transformation to convert special chars: * FROM: hello i am ben\n\n\thow are you? * TO: hello i am benFLOWISE_NEWLINEFLOWISE_NEWLINEFLOWISE_TABhow are you? */ - return handleEscapeCharacters(res, false) + return handleEscapeCharacters(finalRes, false) } } diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts index ff29d589..3dbb7c7d 100644 --- a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -80,6 +80,17 @@ class CustomFunction_Utilities implements INode { } } + // Some values might be a stringified JSON, parse it + for (const key in inputVars) { + if (typeof inputVars[key] === 'string' && inputVars[key].startsWith('{') && inputVars[key].endsWith('}')) { + try { + inputVars[key] = JSON.parse(inputVars[key]) + } catch (e) { + continue + } + } + } + let sandbox: any = { $input: input } sandbox['$vars'] = prepareSandboxVars(variables) sandbox['$flow'] = flow diff --git a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts index 55339e1a..1c5c0b7d 100644 --- a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts +++ b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts @@ -1,7 +1,7 @@ import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { NodeVM } from 'vm2' import { DataSource } from 'typeorm' -import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils' +import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' class IfElseFunction_Utilities implements INode { label: string @@ -95,7 +95,18 @@ class IfElseFunction_Utilities implements INode { inputVars = typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw) } catch (exception) { - throw new Error("Invalid JSON in the PromptTemplate's promptValues: " + exception) + throw new Error("Invalid JSON in the IfElse's Input Variables: " + exception) + } + } + + // Some values might be a stringified JSON, parse it + for (const key in inputVars) { + if (typeof inputVars[key] === 'string' && inputVars[key].startsWith('{') && inputVars[key].endsWith('}')) { + try { + inputVars[key] = JSON.parse(inputVars[key]) + } catch (e) { + continue + } } } @@ -105,7 +116,11 @@ class IfElseFunction_Utilities implements INode { if (Object.keys(inputVars).length) { for (const item in inputVars) { - sandbox[`$${item}`] = inputVars[item] + let value = inputVars[item] + if (typeof value === 'string') { + value = handleEscapeCharacters(value, true) + } + sandbox[`$${item}`] = value } } From 2b2a5b90289286464207bf1eed8f14a549ad02a1 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 19 Jan 2024 18:36:49 +0000 Subject: [PATCH 287/502] add console call back to chains --- .../ConversationChain/ConversationChain.ts | 86 ++-- .../ConversationalRetrievalQAChain.ts | 11 +- .../marketplaces/chatflows/Claude LLM.json | 261 +++++++---- .../chatflows/Simple Conversation Chain.json | 418 +++++++++--------- 4 files changed, 446 insertions(+), 330 deletions(-) diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index fcd9921e..764f4f0e 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -1,13 +1,12 @@ import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' import { ConversationChain } from 'langchain/chains' -import { getBaseClasses } from '../../../src/utils' +import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' import { BaseChatModel } from 'langchain/chat_models/base' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' -import { flatten } from 'lodash' -import { Document } from 'langchain/document' import { RunnableSequence } from 'langchain/schema/runnable' import { StringOutputParser } from 'langchain/schema/output_parser' +import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` const inputKey = 'input' @@ -27,7 +26,7 @@ class ConversationChain_Chains implements INode { constructor(fields?: { sessionId?: string }) { this.label = 'Conversation Chain' this.name = 'conversationChain' - this.version = 1.0 + this.version = 2.0 this.type = 'ConversationChain' this.icon = 'conv.svg' this.category = 'Chains' @@ -44,6 +43,14 @@ class ConversationChain_Chains implements INode { name: 'memory', type: 'BaseMemory' }, + { + label: 'Chat Prompt Template', + name: 'chatPromptTemplate', + type: 'ChatPromptTemplate', + description: 'Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable', + optional: true + }, + /* Deprecated { label: 'Document', name: 'document', @@ -52,15 +59,17 @@ class ConversationChain_Chains implements INode { 'Include whole document into the context window, if you get maximum context length error, please use model with higher context window like Claude 100k, or gpt4 32k', optional: true, list: true - }, + },*/ { label: 'System Message', name: 'systemMessagePrompt', type: 'string', rows: 4, + description: 'If Chat Prompt Template is provided, this will be ignored', additionalParams: true, optional: true, - placeholder: 'You are a helpful assistant that write codes' + default: systemMessage, + placeholder: systemMessage } ] this.sessionId = fields?.sessionId @@ -76,15 +85,21 @@ class ConversationChain_Chains implements INode { const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) - const callbacks = await additionalCallbacks(nodeData, options) + const additionalCallback = await additionalCallbacks(nodeData, options) let res = '' + let callbacks = [loggerHandler, ...additionalCallback] + + if (process.env.DEBUG === 'true') { + callbacks.push(new LCConsoleCallbackHandler()) + } if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - res = await chain.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) + callbacks.push(handler) + res = await chain.invoke({ input }, { callbacks }) } else { - res = await chain.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) + res = await chain.invoke({ input }, { callbacks }) } await memory.addChatMessages( @@ -108,28 +123,27 @@ class ConversationChain_Chains implements INode { const prepareChatPrompt = (nodeData: INodeData) => { const memory = nodeData.inputs?.memory as FlowiseMemory const prompt = nodeData.inputs?.systemMessagePrompt as string - const docs = nodeData.inputs?.document as Document[] + const chatPromptTemplate = nodeData.inputs?.chatPromptTemplate as ChatPromptTemplate - const flattenDocs = docs && docs.length ? flatten(docs) : [] - const finalDocs = [] - for (let i = 0; i < flattenDocs.length; i += 1) { - if (flattenDocs[i] && flattenDocs[i].pageContent) { - finalDocs.push(new Document(flattenDocs[i])) + if (chatPromptTemplate && chatPromptTemplate.promptMessages.length) { + const sysPrompt = chatPromptTemplate.promptMessages[0] + const humanPrompt = chatPromptTemplate.promptMessages[chatPromptTemplate.promptMessages.length - 1] + const chatPrompt = ChatPromptTemplate.fromMessages([ + sysPrompt, + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + humanPrompt + ]) + + if ((chatPromptTemplate as any).promptValues) { + // @ts-ignore + chatPrompt.promptValues = (chatPromptTemplate as any).promptValues } + + return chatPrompt } - let finalText = '' - for (let i = 0; i < finalDocs.length; i += 1) { - finalText += finalDocs[i].pageContent - } - - const replaceChar: string[] = ['{', '}'] - for (const char of replaceChar) finalText = finalText.replaceAll(char, '') - - if (finalText) systemMessage = `${systemMessage}\nThe AI has the following context:\n${finalText}` - const chatPrompt = ChatPromptTemplate.fromMessages([ - SystemMessagePromptTemplate.fromTemplate(prompt ? `${prompt}\n${systemMessage}` : systemMessage), + SystemMessagePromptTemplate.fromTemplate(prompt ? prompt : systemMessage), new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) ]) @@ -142,15 +156,31 @@ const prepareChain = (nodeData: INodeData, sessionId?: string, chatHistory: IMes const memory = nodeData.inputs?.memory as FlowiseMemory const memoryKey = memory.memoryKey ?? 'chat_history' + const chatPrompt = prepareChatPrompt(nodeData) + let promptVariables = {} + const promptValuesRaw = (chatPrompt as any).promptValues + if (promptValuesRaw) { + const promptValues = handleEscapeCharacters(promptValuesRaw, true) + for (const val in promptValues) { + promptVariables = { + ...promptVariables, + [val]: () => { + return promptValues[val] + } + } + } + } + const conversationChain = RunnableSequence.from([ { [inputKey]: (input: { input: string }) => input.input, [memoryKey]: async () => { const history = await memory.getChatMessages(sessionId, true, chatHistory) return history - } + }, + ...promptVariables }, - prepareChatPrompt(nodeData), + chatPrompt, model, new StringOutputParser() ]) diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts index 5f98cba1..964543de 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -13,6 +13,7 @@ import { applyPatch } from 'fast-json-patch' import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils' import { ConsoleCallbackHandler, additionalCallbacks } from '../../../src/handler' import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods } from '../../../src/Interface' +import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' type RetrievalChainInput = { chat_history: string @@ -176,11 +177,17 @@ class ConversationalRetrievalQAChain_Chains implements INode { const history = ((await memory.getChatMessages(this.sessionId, false, options.chatHistory)) as IMessage[]) ?? [] const loggerHandler = new ConsoleCallbackHandler(options.logger) - const callbacks = await additionalCallbacks(nodeData, options) + const additionalCallback = await additionalCallbacks(nodeData, options) + + let callbacks = [loggerHandler, ...additionalCallback] + + if (process.env.DEBUG === 'true') { + callbacks.push(new LCConsoleCallbackHandler()) + } const stream = answerChain.streamLog( { question: input, chat_history: history }, - { callbacks: [loggerHandler, ...callbacks] }, + { callbacks }, { includeNames: [sourceRunnableName] } diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 39d4d400..5d632ff1 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -6,15 +6,15 @@ "height": 376, "id": "bufferMemory_0", "position": { - "x": 451.4449437285705, - "y": 118.30026803362762 + "x": 240.5161028076149, + "y": 165.35849026339048 }, "type": "customNode", "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -53,8 +53,8 @@ }, "selected": false, "positionAbsolute": { - "x": 451.4449437285705, - "y": 118.30026803362762 + "x": 240.5161028076149, + "y": 165.35849026339048 }, "dragging": false }, @@ -63,17 +63,17 @@ "height": 383, "id": "conversationChain_0", "position": { - "x": 1176.1569322079652, - "y": 303.56879146735974 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "type": "customNode", "data": { "id": "conversationChain_0", "label": "Conversation Chain", + "version": 2, "name": "conversationChain", - "version": 1, "type": "ConversationChain", - "baseClasses": ["ConversationChain", "LLMChain", "BaseChain"], + "baseClasses": ["ConversationChain", "LLMChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Chat models specific conversational chain with memory", "inputParams": [ @@ -82,9 +82,11 @@ "name": "systemMessagePrompt", "type": "string", "rows": 4, + "description": "If Chat Prompt Template is provided, this will be ignored", "additionalParams": true, "optional": true, - "placeholder": "You are a helpful assistant that write codes", + "default": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", + "placeholder": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", "id": "conversationChain_0-input-systemMessagePrompt-string" } ], @@ -102,27 +104,26 @@ "id": "conversationChain_0-input-memory-BaseMemory" }, { - "label": "Document", - "name": "document", - "type": "Document", - "description": "Include whole document into the context window", + "label": "Chat Prompt Template", + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "description": "Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable", "optional": true, - "list": true, - "id": "conversationChain_0-input-document-Document" + "id": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" } ], "inputs": { "model": "{{chatAnthropic_0.data.instance}}", "memory": "{{bufferMemory_0.data.instance}}", - "document": ["{{pdfFile_0.data.instance}}"], - "systemMessagePrompt": "" + "chatPromptTemplate": "{{chatPromptTemplate_0.data.instance}}", + "systemMessagePrompt": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know." }, "outputAnchors": [ { - "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain", + "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain|Runnable", "name": "conversationChain", "label": "ConversationChain", - "type": "ConversationChain | LLMChain | BaseChain" + "type": "ConversationChain | LLMChain | BaseChain | Runnable" } ], "outputs": {}, @@ -130,27 +131,27 @@ }, "selected": false, "positionAbsolute": { - "x": 1176.1569322079652, - "y": 303.56879146735974 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "dragging": false }, { "width": 300, - "height": 523, + "height": 574, "id": "chatAnthropic_0", "position": { - "x": 800.5525382783799, - "y": -130.7988221837009 + "x": 585.3308245972187, + "y": -116.32789506560908 }, "type": "customNode", "data": { "id": "chatAnthropic_0", "label": "ChatAnthropic", - "name": "chatAnthropic", "version": 3, + "name": "chatAnthropic", "type": "ChatAnthropic", - "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel"], + "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel", "Runnable"], "category": "Chat Models", "description": "Wrapper around ChatAnthropic large language models that use the Chat endpoint", "inputParams": [ @@ -226,7 +227,7 @@ "name": "claude-instant-v1.1-100k" } ], - "default": "claude-v1", + "default": "claude-2", "optional": true, "id": "chatAnthropic_0-input-modelName-options" }, @@ -234,6 +235,7 @@ "label": "Temperature", "name": "temperature", "type": "number", + "step": 0.1, "default": 0.9, "optional": true, "id": "chatAnthropic_0-input-temperature-number" @@ -242,6 +244,7 @@ "label": "Max Tokens", "name": "maxTokensToSample", "type": "number", + "step": 1, "optional": true, "additionalParams": true, "id": "chatAnthropic_0-input-maxTokensToSample-number" @@ -250,6 +253,7 @@ "label": "Top P", "name": "topP", "type": "number", + "step": 0.1, "optional": true, "additionalParams": true, "id": "chatAnthropic_0-input-topP-number" @@ -258,6 +262,7 @@ "label": "Top K", "name": "topK", "type": "number", + "step": 0.1, "optional": true, "additionalParams": true, "id": "chatAnthropic_0-input-topK-number" @@ -273,6 +278,7 @@ } ], "inputs": { + "cache": "", "modelName": "claude-2.1", "temperature": 0.9, "maxTokensToSample": "", @@ -281,10 +287,10 @@ }, "outputAnchors": [ { - "id": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel", + "id": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel|Runnable", "name": "chatAnthropic", "label": "ChatAnthropic", - "type": "ChatAnthropic | BaseChatModel | BaseLanguageModel" + "type": "ChatAnthropic | BaseChatModel | BaseLanguageModel | Runnable" } ], "outputs": {}, @@ -292,61 +298,106 @@ }, "selected": false, "positionAbsolute": { - "x": 800.5525382783799, - "y": -130.7988221837009 + "x": 585.3308245972187, + "y": -116.32789506560908 }, "dragging": false }, { "width": 300, - "height": 507, - "id": "pdfFile_0", + "height": 688, + "id": "chatPromptTemplate_0", "position": { - "x": 94.16886576108482, - "y": 37.12056504707391 + "x": -106.44189698270114, + "y": 20.133956087516538 }, "type": "customNode", "data": { - "id": "pdfFile_0", - "label": "Pdf File", - "name": "pdfFile", + "id": "chatPromptTemplate_0", + "label": "Chat Prompt Template", "version": 1, + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "baseClasses": ["ChatPromptTemplate", "BaseChatPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a chat prompt", + "inputParams": [ + { + "label": "System Message", + "name": "systemMessagePrompt", + "type": "string", + "rows": 4, + "placeholder": "You are a helpful assistant that translates {input_language} to {output_language}.", + "id": "chatPromptTemplate_0-input-systemMessagePrompt-string" + }, + { + "label": "Human Message", + "name": "humanMessagePrompt", + "type": "string", + "rows": 4, + "placeholder": "{text}", + "id": "chatPromptTemplate_0-input-humanMessagePrompt-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "chatPromptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "systemMessagePrompt": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\nThe AI has the following context:\n{context}", + "humanMessagePrompt": "{input}", + "promptValues": "{\"context\":\"{{plainText_0.data.instance}}\",\"input\":\"{{question}}\"}" + }, + "outputAnchors": [ + { + "id": "chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable", + "name": "chatPromptTemplate", + "label": "ChatPromptTemplate", + "type": "ChatPromptTemplate | BaseChatPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -106.44189698270114, + "y": 20.133956087516538 + }, + "dragging": false + }, + { + "width": 300, + "height": 485, + "id": "plainText_0", + "position": { + "x": -487.7511991135089, + "y": 77.83838996645807 + }, + "type": "customNode", + "data": { + "id": "plainText_0", + "label": "Plain Text", + "version": 2, + "name": "plainText", "type": "Document", "baseClasses": ["Document"], "category": "Document Loaders", - "description": "Load data from PDF files", + "description": "Load data from plain text", "inputParams": [ { - "label": "Pdf File", - "name": "pdfFile", - "type": "file", - "fileType": ".pdf", - "id": "pdfFile_0-input-pdfFile-file" - }, - { - "label": "Usage", - "name": "usage", - "type": "options", - "options": [ - { - "label": "One document per page", - "name": "perPage" - }, - { - "label": "One document per file", - "name": "perFile" - } - ], - "default": "perPage", - "id": "pdfFile_0-input-usage-options" - }, - { - "label": "Use Legacy Build", - "name": "legacyBuild", - "type": "boolean", - "optional": true, - "additionalParams": true, - "id": "pdfFile_0-input-legacyBuild-boolean" + "label": "Text", + "name": "text", + "type": "string", + "rows": 4, + "placeholder": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua...", + "id": "plainText_0-input-text-string" }, { "label": "Metadata", @@ -354,7 +405,7 @@ "type": "json", "optional": true, "additionalParams": true, - "id": "pdfFile_0-input-metadata-json" + "id": "plainText_0-input-metadata-json" } ], "inputAnchors": [ @@ -363,30 +414,45 @@ "name": "textSplitter", "type": "TextSplitter", "optional": true, - "id": "pdfFile_0-input-textSplitter-TextSplitter" + "id": "plainText_0-input-textSplitter-TextSplitter" } ], "inputs": { + "text": "Welcome to Skyworld Hotel, where your dreams take flight and your stay soars to new heights. Nestled amidst breathtaking cityscape views, our upscale establishment offers an unparalleled blend of luxury and comfort. Our rooms are elegantly appointed, featuring modern amenities and plush furnishings to ensure your relaxation.\n\nIndulge in culinary delights at our rooftop restaurant, offering a gastronomic journey with panoramic vistas. Skyworld Hotel boasts state-of-the-art conference facilities, perfect for business travelers, and an inviting spa for relaxation seekers. Our attentive staff is dedicated to ensuring your every need is met, making your stay memorable.\n\nCentrally located, we offer easy access to local attractions, making us an ideal choice for both leisure and business travelers. Experience the world of hospitality like never before at Skyworld Hotel.", "textSplitter": "", - "usage": "perPage", - "legacyBuild": "", "metadata": "" }, "outputAnchors": [ { - "id": "pdfFile_0-output-pdfFile-Document", - "name": "pdfFile", - "label": "Document", - "type": "Document" + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "plainText_0-output-document-Document", + "name": "document", + "label": "Document", + "type": "Document" + }, + { + "id": "plainText_0-output-text-string|json", + "name": "text", + "label": "Text", + "type": "string | json" + } + ], + "default": "document" } ], - "outputs": {}, + "outputs": { + "output": "text" + }, "selected": false }, "selected": false, "positionAbsolute": { - "x": 94.16886576108482, - "y": 37.12056504707391 + "x": -487.7511991135089, + "y": 77.83838996645807 }, "dragging": false } @@ -398,32 +464,31 @@ "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-memory-BaseMemory", "type": "buttonedge", - "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory", - "data": { - "label": "" - } + "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory" }, { "source": "chatAnthropic_0", - "sourceHandle": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel", + "sourceHandle": "chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatAnthropic_0-chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel-conversationChain_0-conversationChain_0-input-model-BaseChatModel", - "data": { - "label": "" - } + "id": "chatAnthropic_0-chatAnthropic_0-output-chatAnthropic-ChatAnthropic|BaseChatModel|BaseLanguageModel|Runnable-conversationChain_0-conversationChain_0-input-model-BaseChatModel" }, { - "source": "pdfFile_0", - "sourceHandle": "pdfFile_0-output-pdfFile-Document", - "target": "conversationChain_0", - "targetHandle": "conversationChain_0-input-document-Document", + "source": "plainText_0", + "sourceHandle": "plainText_0-output-text-string|json", + "target": "chatPromptTemplate_0", + "targetHandle": "chatPromptTemplate_0-input-promptValues-json", "type": "buttonedge", - "id": "pdfFile_0-pdfFile_0-output-pdfFile-Document-conversationChain_0-conversationChain_0-input-document-Document", - "data": { - "label": "" - } + "id": "plainText_0-plainText_0-output-text-string|json-chatPromptTemplate_0-chatPromptTemplate_0-input-promptValues-json" + }, + { + "source": "chatPromptTemplate_0", + "sourceHandle": "chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable", + "target": "conversationChain_0", + "targetHandle": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate", + "type": "buttonedge", + "id": "chatPromptTemplate_0-chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable-conversationChain_0-conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" } ] } diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index 2322136c..9689bf8c 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -2,20 +2,210 @@ "description": "Basic example of Conversation Chain with built-in memory - works exactly like ChatGPT", "badge": "POPULAR", "nodes": [ + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 579.0877964395976, + "y": -138.68792413227874 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo-16k", + "temperature": 0.9, + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 579.0877964395976, + "y": -138.68792413227874 + }, + "dragging": false + }, { "width": 300, "height": 376, "id": "bufferMemory_0", "position": { - "x": 753.4300788823234, - "y": 479.5336426526603 + "x": 220.30240896145915, + "y": 351.61324070296877 }, "type": "customNode", "data": { "id": "bufferMemory_0", "label": "Buffer Memory", - "name": "bufferMemory", "version": 1, + "name": "bufferMemory", "type": "BufferMemory", "baseClasses": ["BufferMemory", "BaseChatMemory", "BaseMemory"], "category": "Memory", @@ -54,179 +244,8 @@ }, "selected": false, "positionAbsolute": { - "x": 753.4300788823234, - "y": 479.5336426526603 - }, - "dragging": false - }, - { - "width": 300, - "height": 523, - "id": "chatOpenAI_0", - "position": { - "x": 754.8942497823595, - "y": -140 - }, - "type": "customNode", - "data": { - "id": "chatOpenAI_0", - "label": "ChatOpenAI", - "name": "chatOpenAI", - "version": 2, - "type": "ChatOpenAI", - "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel"], - "category": "Chat Models", - "description": "Wrapper around OpenAI large language models that use the Chat endpoint", - "inputParams": [ - { - "label": "Connect Credential", - "name": "credential", - "type": "credential", - "credentialNames": ["openAIApi"], - "id": "chatOpenAI_0-input-credential-credential" - }, - { - "label": "Model Name", - "name": "modelName", - "type": "options", - "options": [ - { - "label": "gpt-4", - "name": "gpt-4" - }, - { - "label": "gpt-4-0613", - "name": "gpt-4-0613" - }, - { - "label": "gpt-4-32k", - "name": "gpt-4-32k" - }, - { - "label": "gpt-4-32k-0613", - "name": "gpt-4-32k-0613" - }, - { - "label": "gpt-3.5-turbo", - "name": "gpt-3.5-turbo" - }, - { - "label": "gpt-3.5-turbo-0613", - "name": "gpt-3.5-turbo-0613" - }, - { - "label": "gpt-3.5-turbo-16k", - "name": "gpt-3.5-turbo-16k" - }, - { - "label": "gpt-3.5-turbo-16k-0613", - "name": "gpt-3.5-turbo-16k-0613" - } - ], - "default": "gpt-3.5-turbo", - "optional": true, - "id": "chatOpenAI_0-input-modelName-options" - }, - { - "label": "Temperature", - "name": "temperature", - "type": "number", - "default": 0.9, - "optional": true, - "id": "chatOpenAI_0-input-temperature-number" - }, - { - "label": "Max Tokens", - "name": "maxTokens", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-maxTokens-number" - }, - { - "label": "Top Probability", - "name": "topP", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-topP-number" - }, - { - "label": "Frequency Penalty", - "name": "frequencyPenalty", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-frequencyPenalty-number" - }, - { - "label": "Presence Penalty", - "name": "presencePenalty", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-presencePenalty-number" - }, - { - "label": "Timeout", - "name": "timeout", - "type": "number", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-timeout-number" - }, - { - "label": "BasePath", - "name": "basepath", - "type": "string", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-basepath-string" - }, - { - "label": "BaseOptions", - "name": "baseOptions", - "type": "json", - "optional": true, - "additionalParams": true, - "id": "chatOpenAI_0-input-baseOptions-json" - } - ], - "inputAnchors": [ - { - "label": "Cache", - "name": "cache", - "type": "BaseCache", - "optional": true, - "id": "chatOpenAI_0-input-cache-BaseCache" - } - ], - "inputs": { - "modelName": "gpt-3.5-turbo", - "temperature": 0.9, - "maxTokens": "", - "topP": "", - "frequencyPenalty": "", - "presencePenalty": "", - "timeout": "", - "basepath": "", - "baseOptions": "" - }, - "outputAnchors": [ - { - "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", - "name": "chatOpenAI", - "label": "ChatOpenAI", - "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 754.8942497823595, - "y": -140 + "x": 220.30240896145915, + "y": 351.61324070296877 }, "dragging": false }, @@ -235,17 +254,17 @@ "height": 383, "id": "conversationChain_0", "position": { - "x": 1174.6496397666272, - "y": 311.1052536740497 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "type": "customNode", "data": { "id": "conversationChain_0", "label": "Conversation Chain", + "version": 2, "name": "conversationChain", - "version": 1, "type": "ConversationChain", - "baseClasses": ["ConversationChain", "LLMChain", "BaseChain"], + "baseClasses": ["ConversationChain", "LLMChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Chat models specific conversational chain with memory", "inputParams": [ @@ -254,9 +273,11 @@ "name": "systemMessagePrompt", "type": "string", "rows": 4, + "description": "If Chat Prompt Template is provided, this will be ignored", "additionalParams": true, "optional": true, - "placeholder": "You are a helpful assistant that write codes", + "default": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", + "placeholder": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.", "id": "conversationChain_0-input-systemMessagePrompt-string" } ], @@ -274,27 +295,26 @@ "id": "conversationChain_0-input-memory-BaseMemory" }, { - "label": "Document", - "name": "document", - "type": "Document", - "description": "Include whole document into the context window", + "label": "Chat Prompt Template", + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "description": "Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable", "optional": true, - "list": true, - "id": "conversationChain_0-input-document-Document" + "id": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" } ], "inputs": { "model": "{{chatOpenAI_0.data.instance}}", "memory": "{{bufferMemory_0.data.instance}}", - "document": "", - "systemMessagePrompt": "" + "chatPromptTemplate": "", + "systemMessagePrompt": "The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know." }, "outputAnchors": [ { - "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain", + "id": "conversationChain_0-output-conversationChain-ConversationChain|LLMChain|BaseChain|Runnable", "name": "conversationChain", "label": "ConversationChain", - "type": "ConversationChain | LLMChain | BaseChain" + "type": "ConversationChain | LLMChain | BaseChain | Runnable" } ], "outputs": {}, @@ -302,8 +322,8 @@ }, "selected": false, "positionAbsolute": { - "x": 1174.6496397666272, - "y": 311.1052536740497 + "x": 958.9887390513221, + "y": 318.8734467468765 }, "dragging": false } @@ -311,14 +331,11 @@ "edges": [ { "source": "chatOpenAI_0", - "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-model-BaseChatModel", "type": "buttonedge", - "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel-conversationChain_0-conversationChain_0-input-model-BaseChatModel", - "data": { - "label": "" - } + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-conversationChain_0-conversationChain_0-input-model-BaseChatModel" }, { "source": "bufferMemory_0", @@ -326,10 +343,7 @@ "target": "conversationChain_0", "targetHandle": "conversationChain_0-input-memory-BaseMemory", "type": "buttonedge", - "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory", - "data": { - "label": "" - } + "id": "bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-conversationChain_0-conversationChain_0-input-memory-BaseMemory" } ] } From d2e6b094f62b1781749a3635ec3a1a00f52bdf16 Mon Sep 17 00:00:00 2001 From: Octavian FlowiseAI <154992625+ocflowiseai@users.noreply.github.com> Date: Fri, 19 Jan 2024 23:20:50 +0100 Subject: [PATCH 288/502] Update autoSyncMergedPullRequest workflow flags --- .github/workflows/autoSyncMergedPullRequest.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/autoSyncMergedPullRequest.yml b/.github/workflows/autoSyncMergedPullRequest.yml index 5b1991d7..288d0971 100644 --- a/.github/workflows/autoSyncMergedPullRequest.yml +++ b/.github/workflows/autoSyncMergedPullRequest.yml @@ -5,14 +5,16 @@ on: - closed branches: [ "main" ] jobs: - build: + autoSyncMergedPullRequest: if: github.event.pull_request.merged == true runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v3 - name: Show PR info env: - GITHUB_CONTEXT: ${{ toJSON(github) }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo The PR #${{ github.event.pull_request.number }} was merged on main branch! - name: Repository Dispatch From 5d25c35a1ac4250bb79b21bc22243576a18b2e36 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 21 Jan 2024 18:24:54 +0000 Subject: [PATCH 289/502] add env path for settings json --- packages/server/src/utils/index.ts | 1 + packages/server/src/utils/telemetry.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index a232094e..aa3e9ef6 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1099,6 +1099,7 @@ export const getTelemetryFlowObj = (nodes: IReactFlowNode[], edges: IReactFlowEd * TODO: move env variables to settings json file, easier configuration */ export const getUserSettingsFilePath = () => { + if (process.env.SECRETKEY_PATH) return path.join(process.env.SECRETKEY_PATH, 'settings.json') const checkPaths = [path.join(getUserHome(), '.flowise', 'settings.json')] for (const checkPath of checkPaths) { if (fs.existsSync(checkPath)) { diff --git a/packages/server/src/utils/telemetry.ts b/packages/server/src/utils/telemetry.ts index 4254ea76..4b033f20 100644 --- a/packages/server/src/utils/telemetry.ts +++ b/packages/server/src/utils/telemetry.ts @@ -25,7 +25,9 @@ export class Telemetry { const settings = { instanceId } - const defaultLocation = path.join(getUserHome(), '.flowise', 'settings.json') + const defaultLocation = process.env.SECRETKEY_PATH + ? path.join(process.env.SECRETKEY_PATH, 'settings.json') + : path.join(getUserHome(), '.flowise', 'settings.json') await fs.promises.writeFile(defaultLocation, JSON.stringify(settings, null, 2)) return instanceId } From 2dd1c71cf26d2cb627da2a97be32aa5c312df01b Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 21 Jan 2024 22:26:58 +0000 Subject: [PATCH 290/502] return response --- packages/server/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 3c5766fd..b11653ee 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1825,8 +1825,10 @@ export class App { flowGraph: getTelemetryFlowObj(nodes, edges) }) - // Only return ChatId when its Internal OR incoming input has ChatId, to avoid confusion when calling API - if (incomingInput.chatId || isInternal) result.chatId = chatId + // Prepare response + result.chatId = chatId + if (sessionId) result.sessionId = sessionId + if (memoryType) result.memoryType = memoryType return res.json(result) } catch (e: any) { From 76cb8794bf0255cc7bb6f00bcdcd0ee2d13f0e99 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 08:19:20 +0530 Subject: [PATCH 291/502] Update where loader is rendered in manage links dialog --- .../dialog/ManageScrapedLinksDialog.js | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js index ecfcd403..443ef094 100644 --- a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js +++ b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js @@ -115,52 +115,52 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => { Scraped Links - {selectedLinks.length > 0 ? ( - - {selectedLinks.map((link, index) => ( -
    - - handleChangeLink(index, e)} - size='small' - value={link} - name={`link_${index}`} - /> - - - handleRemoveLink(index)} - edge='end' - > - - - -
    - ))} -
    - ) : ( - <> - {loading && } + <> + {loading && } + {selectedLinks.length > 0 ? ( + + {selectedLinks.map((link, index) => ( +
    + + handleChangeLink(index, e)} + size='small' + value={link} + name={`link_${index}`} + /> + + + handleRemoveLink(index)} + edge='end' + > + + + +
    + ))} +
    + ) : (
    Links scraped from the URL will appear here
    - - )} + )} + From 62ec17d6841db4f687a76bf9ede68622dd17c89b Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 08:19:41 +0530 Subject: [PATCH 292/502] Update manage links button variant --- packages/ui/src/views/canvas/NodeInputHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 6c7a277e..f7309bd3 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -470,7 +470,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA }} disabled={disabled} sx={{ borderRadius: '12px', width: '100%', mt: 1 }} - variant='contained' + variant='outlined' onClick={() => onManageLinksDialogClicked( data.inputs[inputParam.name] ?? inputParam.default ?? '', From bf60a1a2a929840d185f161d78f16567dcc8e8bb Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 08:30:36 +0530 Subject: [PATCH 293/502] Fix multiple calls to parseInt --- .../nodes/documentloaders/Cheerio/Cheerio.ts | 12 ++++++------ .../nodes/documentloaders/Playwright/Playwright.ts | 12 ++++++------ .../nodes/documentloaders/Puppeteer/Puppeteer.ts | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts index e883c097..28069c22 100644 --- a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts +++ b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts @@ -91,7 +91,7 @@ class Cheerio_DocumentLoaders implements INode { const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string const selectedLinks = nodeData.inputs?.selectedLinks as string[] - let limit = nodeData.inputs?.limit as string + let limit = parseInt(nodeData.inputs?.limit as string) let url = nodeData.inputs?.url as string url = url.trim() @@ -125,14 +125,14 @@ class Cheerio_DocumentLoaders implements INode { let docs = [] if (relativeLinksMethod) { if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) - if (!limit) limit = '10' - else if (parseInt(limit) < 0) throw new Error('Limit cannot be less than 0') + if (!limit) limit = 10 + else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = selectedLinks && selectedLinks.length > 0 - ? selectedLinks.slice(0, parseInt(limit)) + ? selectedLinks.slice(0, limit) : relativeLinksMethod === 'webCrawl' - ? await webCrawl(url, parseInt(limit)) - : await xmlScrape(url, parseInt(limit)) + ? await webCrawl(url, limit) + : await xmlScrape(url, limit) if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index 65be3ce7..fd4650c4 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -119,7 +119,7 @@ class Playwright_DocumentLoaders implements INode { const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string const selectedLinks = nodeData.inputs?.selectedLinks as string[] - let limit = nodeData.inputs?.limit as string + let limit = parseInt(nodeData.inputs?.limit as string) let waitUntilGoToOption = nodeData.inputs?.waitUntilGoToOption as 'load' | 'domcontentloaded' | 'networkidle' | 'commit' | undefined let waitForSelector = nodeData.inputs?.waitForSelector as string @@ -166,14 +166,14 @@ class Playwright_DocumentLoaders implements INode { let docs = [] if (relativeLinksMethod) { if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) - if (!limit) limit = '10' - else if (parseInt(limit) < 0) throw new Error('Limit cannot be less than 0') + if (!limit) limit = 10 + else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = selectedLinks && selectedLinks.length > 0 - ? selectedLinks.slice(0, parseInt(limit)) + ? selectedLinks.slice(0, limit) : relativeLinksMethod === 'webCrawl' - ? await webCrawl(url, parseInt(limit)) - : await xmlScrape(url, parseInt(limit)) + ? await webCrawl(url, limit) + : await xmlScrape(url, limit) if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index d5539659..ed004b6d 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -120,7 +120,7 @@ class Puppeteer_DocumentLoaders implements INode { const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string const selectedLinks = nodeData.inputs?.selectedLinks as string[] - let limit = nodeData.inputs?.limit as string + let limit = parseInt(nodeData.inputs?.limit as string) let waitUntilGoToOption = nodeData.inputs?.waitUntilGoToOption as PuppeteerLifeCycleEvent let waitForSelector = nodeData.inputs?.waitForSelector as string @@ -167,14 +167,14 @@ class Puppeteer_DocumentLoaders implements INode { let docs = [] if (relativeLinksMethod) { if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) - if (!limit) limit = '10' - else if (parseInt(limit) < 0) throw new Error('Limit cannot be less than 0') + if (!limit) limit = 10 + else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = selectedLinks && selectedLinks.length > 0 - ? selectedLinks.slice(0, parseInt(limit)) + ? selectedLinks.slice(0, limit) : relativeLinksMethod === 'webCrawl' - ? await webCrawl(url, parseInt(limit)) - : await xmlScrape(url, parseInt(limit)) + ? await webCrawl(url, limit) + : await xmlScrape(url, limit) if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { From c24708f53bcc830b96ba7634b0919f04fd846c80 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 08:42:44 +0530 Subject: [PATCH 294/502] Set default value for get links limit in web scraper nodes - cheerio, playwright, and puppeteer --- packages/components/nodes/documentloaders/Cheerio/Cheerio.ts | 1 + .../components/nodes/documentloaders/Playwright/Playwright.ts | 1 + packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts index 28069c22..2f0bd8b6 100644 --- a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts +++ b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts @@ -63,6 +63,7 @@ class Cheerio_DocumentLoaders implements INode { name: 'limit', type: 'number', optional: true, + default: '10', additionalParams: true, description: 'Only used when "Get Relative Links Method" is selected. Set 0 to retrieve all relative links, default limit is 10.', diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index fd4650c4..cb27f1c4 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -61,6 +61,7 @@ class Playwright_DocumentLoaders implements INode { name: 'limit', type: 'number', optional: true, + default: '10', additionalParams: true, description: 'Only used when "Get Relative Links Method" is selected. Set 0 to retrieve all relative links, default limit is 10.', diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index ed004b6d..fe7d4f8a 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -62,6 +62,7 @@ class Puppeteer_DocumentLoaders implements INode { name: 'limit', type: 'number', optional: true, + default: '10', additionalParams: true, description: 'Only used when "Get Relative Links Method" is selected. Set 0 to retrieve all relative links, default limit is 10.', From 193e5c4640d96b73499beb032e6e8e092b2dfa7d Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 08:49:04 +0530 Subject: [PATCH 295/502] Update console statements to use logger --- .../nodes/documentloaders/Cheerio/Cheerio.ts | 15 ++++++++------- .../documentloaders/Playwright/Playwright.ts | 15 ++++++++------- .../nodes/documentloaders/Puppeteer/Puppeteer.ts | 15 ++++++++------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts index 2f0bd8b6..3eba0ece 100644 --- a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts +++ b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts @@ -1,4 +1,4 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { CheerioWebBaseLoader, WebBaseLoaderParams } from 'langchain/document_loaders/web/cheerio' import { test } from 'linkifyjs' @@ -87,7 +87,7 @@ class Cheerio_DocumentLoaders implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string @@ -119,13 +119,13 @@ class Cheerio_DocumentLoaders implements INode { } return docs } catch (err) { - if (process.env.DEBUG === 'true') console.error(`error in CheerioWebBaseLoader: ${err.message}, on page: ${url}`) + if (process.env.DEBUG === 'true') options.logger.error(`error in CheerioWebBaseLoader: ${err.message}, on page: ${url}`) } } let docs = [] if (relativeLinksMethod) { - if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) + if (process.env.DEBUG === 'true') options.logger.info(`Start ${relativeLinksMethod}`) if (!limit) limit = 10 else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = @@ -134,14 +134,15 @@ class Cheerio_DocumentLoaders implements INode { : relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) - if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) + if (process.env.DEBUG === 'true') options.logger.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { docs.push(...(await cheerioLoader(page))) } - if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + if (process.env.DEBUG === 'true') options.logger.info(`Finish ${relativeLinksMethod}`) } else if (selectedLinks && selectedLinks.length > 0) { - if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) + if (process.env.DEBUG === 'true') + options.logger.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) for (const page of selectedLinks) { docs.push(...(await cheerioLoader(page))) } diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index cb27f1c4..2de166ce 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -1,4 +1,4 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { Browser, Page, PlaywrightWebBaseLoader, PlaywrightWebBaseLoaderOptions } from 'langchain/document_loaders/web/playwright' import { test } from 'linkifyjs' @@ -115,7 +115,7 @@ class Playwright_DocumentLoaders implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string @@ -160,13 +160,13 @@ class Playwright_DocumentLoaders implements INode { } return docs } catch (err) { - if (process.env.DEBUG === 'true') console.error(`error in PlaywrightWebBaseLoader: ${err.message}, on page: ${url}`) + if (process.env.DEBUG === 'true') options.logger.error(`error in PlaywrightWebBaseLoader: ${err.message}, on page: ${url}`) } } let docs = [] if (relativeLinksMethod) { - if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) + if (process.env.DEBUG === 'true') options.logger.info(`Start ${relativeLinksMethod}`) if (!limit) limit = 10 else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = @@ -175,14 +175,15 @@ class Playwright_DocumentLoaders implements INode { : relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) - if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) + if (process.env.DEBUG === 'true') options.logger.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { docs.push(...(await playwrightLoader(page))) } - if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + if (process.env.DEBUG === 'true') options.logger.info(`Finish ${relativeLinksMethod}`) } else if (selectedLinks && selectedLinks.length > 0) { - if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) + if (process.env.DEBUG === 'true') + options.logger.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) for (const page of selectedLinks) { docs.push(...(await playwrightLoader(page))) } diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index fe7d4f8a..3d28f310 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -1,4 +1,4 @@ -import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { Browser, Page, PuppeteerWebBaseLoader, PuppeteerWebBaseLoaderOptions } from 'langchain/document_loaders/web/puppeteer' import { test } from 'linkifyjs' @@ -116,7 +116,7 @@ class Puppeteer_DocumentLoaders implements INode { ] } - async init(nodeData: INodeData): Promise { + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const metadata = nodeData.inputs?.metadata const relativeLinksMethod = nodeData.inputs?.relativeLinksMethod as string @@ -161,13 +161,13 @@ class Puppeteer_DocumentLoaders implements INode { } return docs } catch (err) { - if (process.env.DEBUG === 'true') console.error(`error in PuppeteerWebBaseLoader: ${err.message}, on page: ${url}`) + if (process.env.DEBUG === 'true') options.logger.error(`error in PuppeteerWebBaseLoader: ${err.message}, on page: ${url}`) } } let docs = [] if (relativeLinksMethod) { - if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) + if (process.env.DEBUG === 'true') options.logger.info(`Start ${relativeLinksMethod}`) if (!limit) limit = 10 else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = @@ -176,14 +176,15 @@ class Puppeteer_DocumentLoaders implements INode { : relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) - if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) + if (process.env.DEBUG === 'true') options.logger.info(`pages: ${JSON.stringify(pages)}, length: ${pages.length}`) if (!pages || pages.length === 0) throw new Error('No relative links found') for (const page of pages) { docs.push(...(await puppeteerLoader(page))) } - if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + if (process.env.DEBUG === 'true') options.logger.info(`Finish ${relativeLinksMethod}`) } else if (selectedLinks && selectedLinks.length > 0) { - if (process.env.DEBUG === 'true') console.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) + if (process.env.DEBUG === 'true') + options.logger.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) for (const page of selectedLinks) { docs.push(...(await puppeteerLoader(page))) } From 7e5d8e7294cdf7affa9fb6aeee67148d7cde40b0 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 11:10:27 +0530 Subject: [PATCH 296/502] Fix image uploads appear on top of chat messages. Now image uploads will appear above the text input on its own row. --- .../src/views/chatmessage/ChatExpandDialog.js | 7 +- .../ui/src/views/chatmessage/ChatMessage.css | 28 ++-- .../ui/src/views/chatmessage/ChatMessage.js | 140 +++++++++--------- .../ui/src/views/chatmessage/ChatPopUp.js | 9 +- 4 files changed, 95 insertions(+), 89 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatExpandDialog.js b/packages/ui/src/views/chatmessage/ChatExpandDialog.js index 1b2037a8..e2044ea3 100644 --- a/packages/ui/src/views/chatmessage/ChatExpandDialog.js +++ b/packages/ui/src/views/chatmessage/ChatExpandDialog.js @@ -21,7 +21,7 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => { aria-describedby='alert-dialog-description' sx={{ overflow: 'visible' }} > - +
    {dialogProps.title}
    @@ -43,7 +43,10 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => { )}
    - + diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 9e7a1857..c00186bf 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -1,8 +1,6 @@ .messagelist { width: 100%; - height: 100%; - overflow-y: scroll; - overflow-x: hidden; + height: auto; border-radius: 0.5rem; } @@ -108,32 +106,38 @@ } .center { + width: 100%; display: flex; justify-content: center; align-items: center; position: relative; flex-direction: column; - padding: 10px; + padding: 12px; } -.cloud { +.cloud-wrapper, +.cloud-dialog-wrapper { width: 400px; height: calc(100vh - 260px); - overflow-y: scroll; - border-radius: 0.5rem; display: flex; - justify-content: center; - align-items: center; + align-items: start; + justify-content: start; + flex-direction: column; } +.cloud-dialog-wrapper { + width: 100%; +} + +.cloud, .cloud-dialog { width: 100%; - height: 100vh; + height: auto; + max-height: calc(100% - 72px); overflow-y: scroll; - border-radius: 0.5rem; display: flex; justify-content: center; - align-items: center; + align-items: start; } .cloud-message { diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 155b3e99..db2ed749 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -703,7 +703,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    )} {message.fileUploads && message.fileUploads.length > 0 && ( -
    +
    {message.fileUploads.map((item, index) => { return ( <> @@ -833,9 +840,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    -
    +
    {previews && previews.length > 0 && ( - + {previews.map((item, index) => ( <> {item.mime.startsWith('image/') ? ( @@ -886,85 +893,70 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { ))} )} - -
    - -
    -
    -
    - + + + + + + + ) + } + endAdornment={ + <> + {isChatFlowAvailableForUploads && ( + onMicrophonePressed()} type='button' disabled={loading || !chatflowid} - edge='start' + edge='end' > - - ) - } - endAdornment={ - <> - {isChatFlowAvailableForSpeech && ( - - onMicrophonePressed()} - type='button' - disabled={loading || !chatflowid} - edge='end' - > - - - - )} - - - {loading ? ( -
    - -
    - ) : ( - // Send icon SVG in input field - - )} -
    -
    - - } - /> - {isChatFlowAvailableForUploads && ( - - )} - -
    + )} + + + {loading ? ( +
    + +
    + ) : ( + // Send icon SVG in input field + + )} +
    +
    + + } + /> + {isChatFlowAvailableForUploads && ( + + )} +
    setSourceDialogOpen(false)} /> diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.js b/packages/ui/src/views/chatmessage/ChatPopUp.js index 670fb00f..91ba73e2 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.js +++ b/packages/ui/src/views/chatmessage/ChatPopUp.js @@ -191,7 +191,14 @@ export const ChatPopUp = ({ chatflowid }) => { - + From ddab853cfd2266d95f2f379917359a2e9ca210f1 Mon Sep 17 00:00:00 2001 From: Octavian FlowiseAI <154992625+ocflowiseai@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:57:01 +0100 Subject: [PATCH 297/502] Update autoSyncMergedPullRequest.yml - send pr title - send pr description --- .github/workflows/autoSyncMergedPullRequest.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/autoSyncMergedPullRequest.yml b/.github/workflows/autoSyncMergedPullRequest.yml index 288d0971..518db0bc 100644 --- a/.github/workflows/autoSyncMergedPullRequest.yml +++ b/.github/workflows/autoSyncMergedPullRequest.yml @@ -23,4 +23,11 @@ jobs: token: ${{ secrets.AUTOSYNC_TOKEN }} repository: ${{ secrets.AUTOSYNC_CH_URL }} event-type: ${{ secrets.AUTOSYNC_PR_EVENT_TYPE }} - client-payload: '{"ref": "${{ github.ref }}", "prNumber": "${{ github.event.pull_request.number }}", "sha": "${{ github.sha }}"}' + client-payload: >- + { + "ref": "${{ github.ref }}", + "prNumber": "${{ github.event.pull_request.number }}", + "prTitle": "${{ github.event.pull_request.title }}", + "prDescription": "${{ github.event.pull_request.description }}", + "sha": "${{ github.sha }}" + } From fb2e6a0e08cb62284ca11647e294d4dcf30136f7 Mon Sep 17 00:00:00 2001 From: Octavian FlowiseAI <154992625+ocflowiseai@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:08:29 +0100 Subject: [PATCH 298/502] Update autoSyncSingleCommit.yml - send single commit message --- .github/workflows/autoSyncSingleCommit.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/autoSyncSingleCommit.yml b/.github/workflows/autoSyncSingleCommit.yml index ce429a60..738e271a 100644 --- a/.github/workflows/autoSyncSingleCommit.yml +++ b/.github/workflows/autoSyncSingleCommit.yml @@ -28,4 +28,9 @@ jobs: token: ${{ secrets.AUTOSYNC_TOKEN }} repository: ${{ secrets.AUTOSYNC_CH_URL }} event-type: ${{ secrets.AUTOSYNC_SC_EVENT_TYPE }} - client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' + client-payload: >- + { + "ref": "${{ github.ref }}", + "sha": "${{ github.sha }}", + "commitMessage": "${{ github.event.commits[0].message }}", + } From 764efcccb738e1683b7e0cf623044ce3286d2f39 Mon Sep 17 00:00:00 2001 From: Octavian FlowiseAI <154992625+ocflowiseai@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:09:19 +0100 Subject: [PATCH 299/502] Update autoSyncSingleCommit.yml --- .github/workflows/autoSyncSingleCommit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/autoSyncSingleCommit.yml b/.github/workflows/autoSyncSingleCommit.yml index 738e271a..8700f232 100644 --- a/.github/workflows/autoSyncSingleCommit.yml +++ b/.github/workflows/autoSyncSingleCommit.yml @@ -32,5 +32,5 @@ jobs: { "ref": "${{ github.ref }}", "sha": "${{ github.sha }}", - "commitMessage": "${{ github.event.commits[0].message }}", + "commitMessage": "${{ github.event.commits[0].message }}" } From 59643b65d9cadb40efdf65c436cfa8e2420ae8e0 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 15:51:05 +0530 Subject: [PATCH 300/502] Fix the flickering issue when dragging files over the chat window --- .../ui/src/views/chatmessage/ChatMessage.css | 26 +++++++-- .../ui/src/views/chatmessage/ChatMessage.js | 53 ++++++++----------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index c00186bf..6acff1d5 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -119,16 +119,34 @@ .cloud-dialog-wrapper { width: 400px; height: calc(100vh - 260px); - display: flex; - align-items: start; - justify-content: start; - flex-direction: column; } .cloud-dialog-wrapper { width: 100%; } +.cloud-wrapper > div, +.cloud-dialog-wrapper > div { + width: 100%; + height: 100%; + display: flex; + align-items: start; + justify-content: start; + flex-direction: column; + position: relative; +} + +.image-dropzone { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 2001; /* Ensure it's above other content */ +} + .cloud, .cloud-dialog { width: 100%; diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index db2ed749..c9c69ae0 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -91,7 +91,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const fileUploadRef = useRef(null) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [previews, setPreviews] = useState([]) - const [isDragOver, setIsDragOver] = useState(false) + const [isDragActive, setIsDragActive] = useState(false) // recording const [isRecording, setIsRecording] = useState(false) @@ -123,8 +123,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { return } e.preventDefault() - e.stopPropagation() - setIsDragOver(false) + setIsDragActive(false) let files = [] if (e.dataTransfer.files.length > 0) { for (const file of e.dataTransfer.files) { @@ -251,28 +250,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } - const handleDragOver = (e) => { - e.preventDefault() - e.stopPropagation() - } - - const handleDragEnter = (e) => { + const handleDrag = (e) => { if (isChatFlowAvailableForUploads) { e.preventDefault() e.stopPropagation() - setIsDragOver(true) - } - } - - const handleDragLeave = (e) => { - if (isChatFlowAvailableForUploads) { - e.preventDefault() - e.stopPropagation() - if (e.originalEvent?.pageX !== 0 || e.originalEvent?.pageY !== 0) { - setIsDragOver(false) - return false + if (e.type === 'dragenter' || e.type === 'dragover') { + console.log('drag enter') + setIsDragActive(true) + } else if (e.type === 'dragleave') { + console.log('drag leave') + setIsDragActive(false) } - setIsDragOver(false) } } @@ -599,8 +587,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, [open, chatflowid]) return ( - <> - {isDragOver && getAllowChatFlowUploads.data?.isUploadAllowed && ( +
    + {isDragActive && ( +
    + )} + {isDragActive && getAllowChatFlowUploads.data?.isUploadAllowed && ( Drop here to upload {getAllowChatFlowUploads.data.uploadFileSizeAndTypes.map((allowed) => { @@ -648,13 +645,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { )} )} -
    +
    {messages && messages.map((message, index) => { @@ -959,7 +950,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    setSourceDialogOpen(false)} /> - +
    ) } From 7d0ae5286c421c13a5d8e05ae789affba4f14032 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 16:20:18 +0530 Subject: [PATCH 301/502] Fix chat popup styles and remove console statements --- packages/ui/src/views/chatmessage/ChatMessage.css | 1 + packages/ui/src/views/chatmessage/ChatMessage.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 6acff1d5..6742fbac 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -156,6 +156,7 @@ display: flex; justify-content: center; align-items: start; + flex-grow: 1; } .cloud-message { diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index c9c69ae0..51005a06 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -255,10 +255,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { e.preventDefault() e.stopPropagation() if (e.type === 'dragenter' || e.type === 'dragover') { - console.log('drag enter') setIsDragActive(true) } else if (e.type === 'dragleave') { - console.log('drag leave') setIsDragActive(false) } } From f384ad9086b3862b8ab7cf2c30f36f5f435a2bcf Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 22 Jan 2024 19:03:05 +0530 Subject: [PATCH 302/502] Update audio recording ui in internal chat --- .../ui/src/views/chatmessage/ChatMessage.js | 210 ++++++++++-------- .../src/views/chatmessage/audio-recording.css | 86 +++---- 2 files changed, 145 insertions(+), 151 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 51005a06..e52bcfd5 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -23,7 +23,7 @@ import { Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconCircleDot, IconTrash } from '@tabler/icons' +import { IconCircleDot, IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconSquare, IconTrash, IconX } from '@tabler/icons' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import audioUploadSVG from 'assets/images/wave-sound.jpg' @@ -608,41 +608,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })} )} - {isRecording && ( - -
    - Recording -
    - - - -
    - - - -

    00:00

    -
    - - - -
    -
    - {recordingNotSupported && ( -
    -
    -

    To record audio, use browsers like Chrome and Firefox that support audio recording.

    - -
    -
    - )} -
    - )}
    {messages && @@ -829,6 +794,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    + +
    {previews && previews.length > 0 && ( @@ -882,70 +849,129 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { ))} )} -
    - - - - - - ) - } - endAdornment={ - <> - {isChatFlowAvailableForUploads && ( - + {isRecording ? ( + <> + {recordingNotSupported && ( +
    +
    +

    To record audio, use modern browsers like Chrome or Firefox that support audio recording.

    + +
    +
    + )} + +
    + + + +

    00:00

    +
    +
    + + + + + + +
    +
    + + ) : ( + + onMicrophonePressed()} + onClick={handleUploadClick} type='button' disabled={loading || !chatflowid} - edge='end' + edge='start' > -
    - )} - - - {loading ? ( -
    - -
    - ) : ( - // Send icon SVG in input field - - )} -
    -
    - - } - /> - {isChatFlowAvailableForUploads && ( - - )} - + ) + } + endAdornment={ + <> + {isChatFlowAvailableForUploads && ( + + onMicrophonePressed()} + type='button' + disabled={loading || !chatflowid} + edge='end' + > + + + + )} + + + {loading ? ( +
    + +
    + ) : ( + // Send icon SVG in input field + + )} +
    +
    + + } + /> + {isChatFlowAvailableForUploads && ( + + )} + + )}
    setSourceDialogOpen(false)} />
    diff --git a/packages/ui/src/views/chatmessage/audio-recording.css b/packages/ui/src/views/chatmessage/audio-recording.css index fbca2f60..4b8e5566 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.css +++ b/packages/ui/src/views/chatmessage/audio-recording.css @@ -8,20 +8,6 @@ * { box-sizing: border-box; } - .audio-recording-container { - width: 100%; - /* view port height*/ - /*targeting Chrome & Safari*/ - display: -webkit-flex; - /*targeting IE10*/ - display: -ms-flex; - display: flex; - flex-direction: column; - justify-content: center; - /*horizontal centering*/ - align-items: center; - background-color: white; - } .start-recording-button { font-size: 70px; color: #435f7a; @@ -36,34 +22,13 @@ /*targeting IE10*/ display: -ms-flex; display: flex; - justify-content: space-evenly; + justify-content: center; /*horizontal centering*/ align-items: center; - width: 334px; - margin-bottom: 30px; - background-color: white; - } - .cancel-recording-button, - .stop-recording-button { - font-size: 70px; - cursor: pointer; - } - .cancel-recording-button { - color: red; - opacity: 0.7; - } - .cancel-recording-button:hover { - color: rgb(206, 4, 4); - } - .stop-recording-button { - color: #33cc33; - opacity: 0.7; - } - .stop-recording-button:hover { - color: #27a527; + gap: 12px; } .recording-elapsed-time { - font-size: 32px; + font-size: 16px; /*targeting Chrome & Safari*/ display: -webkit-flex; /*targeting IE10*/ @@ -73,6 +38,15 @@ /*horizontal centering*/ align-items: center; } + .recording-elapsed-time #elapsed-time { + margin: 0; + } + .recording-indicator-wrapper { + position: relative; + display: flex; + width: 16px; + height: 16px; + } .red-recording-dot { font-size: 25px; color: red; @@ -136,17 +110,11 @@ opacity: 1; } } - .elapsed-time { - font-size: 32px; - } .recording-control-buttons-container.hide { display: none; } .overlay { - position: absolute; - top: 0; width: 100%; - background-color: rgba(82, 76, 76, 0.35); /*targeting Chrome & Safari*/ display: -webkit-flex; /*targeting IE10*/ @@ -155,6 +123,7 @@ justify-content: center; /*horizontal centering*/ align-items: center; + margin-bottom: 12px; } .overlay.hide { display: none; @@ -165,16 +134,15 @@ /*targeting IE10*/ display: -ms-flex; display: flex; - flex-direction: column; justify-content: space-between; /*horizontal centering*/ align-items: center; - width: 317px; - height: 119px; - background-color: white; - border-radius: 10px; - padding: 15px; + width: 100%; font-size: 16px; + gap: 12px; + } + .browser-not-supporting-audio-recording-box > p { + margin: 0; } .close-browser-not-supported-box { cursor: pointer; @@ -219,16 +187,16 @@ -o-animation-iteration-count: infinite; } .text-indication-of-audio-playing span:nth-child(2) { - animation-delay: .4s; - -webkit-animation-delay: .4s; - -moz-animation-delay: .4s; - -o-animation-delay: .4s; + animation-delay: 0.4s; + -webkit-animation-delay: 0.4s; + -moz-animation-delay: 0.4s; + -o-animation-delay: 0.4s; } .text-indication-of-audio-playing span:nth-child(3) { - animation-delay: .8s; - -webkit-animation-delay: .8s; - -moz-animation-delay: .8s; - -o-animation-delay: .8s; + animation-delay: 0.8s; + -webkit-animation-delay: 0.8s; + -moz-animation-delay: 0.8s; + -o-animation-delay: 0.8s; } /* The animation code */ @keyframes blinking-dot { @@ -278,4 +246,4 @@ opacity: 0; } } -} \ No newline at end of file +} From 318686e622fc3633cc8714aa49804faa14f98ad7 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 23 Jan 2024 11:03:38 +0530 Subject: [PATCH 303/502] Fix issue where audio recording is not sent on stopping recording --- .../ui/src/views/chatmessage/ChatMessage.js | 359 +++++++++--------- 1 file changed, 183 insertions(+), 176 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index e52bcfd5..006e2425 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from 'react' +import { useState, useRef, useEffect, useCallback, Fragment } from 'react' import { useSelector } from 'react-redux' import PropTypes from 'prop-types' import socketIOClient from 'socket.io-client' @@ -96,6 +96,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // recording const [isRecording, setIsRecording] = useState(false) const [recordingNotSupported, setRecordingNotSupported] = useState(false) + const [isLoadingRecording, setIsLoadingRecording] = useState(false) const isFileAllowedForUpload = (file) => { const constraints = getAllowChatFlowUploads.data @@ -292,10 +293,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } const onRecordingStopped = async () => { + setIsLoadingRecording(true) stopAudioRecording(addRecordingToPreviews) - setIsRecording(false) - setRecordingNotSupported(false) - handlePromptClick('') } const onSourceDialogClick = (data, title) => { @@ -364,14 +363,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput setLoading(true) - const urls = [] - previews.map((item) => { - urls.push({ + const urls = previews.map((item) => { + return { data: item.data, type: item.type, name: item.name, mime: item.mime - }) + } }) clearPreviews() setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: urls }]) @@ -383,7 +381,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'), chatId } - if (urls) params.uploads = urls + if (urls && urls.length > 0) params.uploads = urls if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) @@ -584,6 +582,16 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, chatflowid]) + useEffect(() => { + // wait for audio recording to load and then send + if (previews.length === 1 && previews[0].type === 'audio') { + setIsRecording(false) + setRecordingNotSupported(false) + handlePromptClick('') + } + // eslint-disable-next-line + }, [previews]) + return (
    {isDragActive && ( @@ -614,169 +622,167 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { messages.map((message, index) => { return ( // The latest message sent by the user will be animated while waiting for a response - <> - - {/* Display the correct icon depending on the message type */} - {message.type === 'apiMessage' ? ( - AI - ) : ( - Me - )} -
    - {message.usedTools && ( -
    - {message.usedTools.map((tool, index) => { - return ( - onSourceDialogClick(tool, 'Used Tools')} - /> - ) - })} -
    - )} - {message.fileUploads && message.fileUploads.length > 0 && ( -
    - {message.fileUploads.map((item, index) => { - return ( - <> - {item.mime.startsWith('image/') ? ( - - - - ) : ( - // eslint-disable-next-line jsx-a11y/media-has-caption - - )} - - ) - })} -
    - )} -
    - {/* Messages are being rendered in Markdown format */} - - ) : ( - - {children} - - ) - } - }} - > - {message.message} - + + {/* Display the correct icon depending on the message type */} + {message.type === 'apiMessage' ? ( + AI + ) : ( + Me + )} +
    + {message.usedTools && ( +
    + {message.usedTools.map((tool, index) => { + return ( + onSourceDialogClick(tool, 'Used Tools')} + /> + ) + })}
    - {message.fileAnnotations && ( -
    - {message.fileAnnotations.map((fileAnnotation, index) => { - return ( - - ) - })} -
    - )} - {message.sourceDocuments && ( -
    - {removeDuplicateURL(message).map((source, index) => { - const URL = - source.metadata && source.metadata.source - ? isValidURL(source.metadata.source) - : undefined - return ( - - URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source) - } + )} + {message.fileUploads && message.fileUploads.length > 0 && ( +
    + {message.fileUploads.map((item, index) => { + return ( + <> + {item.mime.startsWith('image/') ? ( + + + + ) : ( + // eslint-disable-next-line jsx-a11y/media-has-caption + + )} + + ) + })} +
    + )} +
    + {/* Messages are being rendered in Markdown format */} + + ) : ( + + {children} + ) - })} -
    - )} + } + }} + > + {message.message} +
    - - + {message.fileAnnotations && ( +
    + {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
    + )} + {message.sourceDocuments && ( +
    + {removeDuplicateURL(message).map((source, index) => { + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined + return ( + + URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source) + } + /> + ) + })} +
    + )} +
    +
    ) })}
    @@ -800,11 +806,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {previews && previews.length > 0 && ( {previews.map((item, index) => ( - <> + {item.mime.startsWith('image/') ? ( { backgroundColor: theme.palette.grey[500], flex: '0 0 auto' }} - key={index} variant='outlined' > { )} - + ))} )} @@ -854,7 +858,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {recordingNotSupported && (
    -

    To record audio, use modern browsers like Chrome or Firefox that support audio recording.

    + + To record audio, use modern browsers like Chrome or Firefox that support audio recording. + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + onCancel() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Analytic Configuration: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const setValue = (value, providerName, inputParamName) => { + let newVal = {} + if (!Object.prototype.hasOwnProperty.call(speechToText, providerName)) { + newVal = { ...speechToText, [providerName]: {} } + } else { + newVal = { ...speechToText } + } + + newVal[providerName][inputParamName] = value + if (inputParamName === 'status' && value === true) { + //ensure that the others are turned off + speechToTextProviders.forEach((provider) => { + if (provider.name !== providerName) { + newVal[provider.name] = { ...speechToText[provider.name], status: false } + } + }) + } + setSpeechToText(newVal) + } + + const handleAccordionChange = (providerName) => (event, isExpanded) => { + const accordionProviders = { ...providerExpanded } + accordionProviders[providerName] = isExpanded + setProviderExpanded(accordionProviders) + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.speechToText) { + try { + setSpeechToText(JSON.parse(dialogProps.chatflow.speechToText)) + } catch (e) { + setSpeechToText({}) + console.error(e) + } + } + + return () => { + setSpeechToText({}) + setProviderExpanded({}) + } + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + Speech To Text Configuration + + + {speechToTextProviders.map((provider, index) => ( + + } aria-controls={provider.name} id={provider.name}> + + +
    + AI +
    +
    + + {provider.url} + + } + /> + {speechToText[provider.name] && speechToText[provider.name].status && ( +
    +
    + ON +
    + )} + + + + {provider.inputs.map((inputParam, index) => ( + +
    + + {inputParam.label} + {!inputParam.optional &&  *} + {inputParam.description && ( + + )} + +
    + {providerExpanded[provider.name] && inputParam.type === 'credential' && ( + setValue(newValue, provider.name, 'credentialId')} + /> + )} + {inputParam.type === 'boolean' && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + speechToText[provider.name] + ? speechToText[provider.name][inputParam.name] + : inputParam.default ?? false + } + /> + )} + {providerExpanded[provider.name] && + (inputParam.type === 'string' || + inputParam.type === 'password' || + inputParam.type === 'number') && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + speechToText[provider.name] + ? speechToText[provider.name][inputParam.name] + : inputParam.default ?? '' + } + /> + )} +
    + ))} +
    + + ))} + + + + Save + + +
    + ) : null + + return createPortal(component, portalElement) +} + +SpeechToTextDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default SpeechToTextDialog diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index 85408cd8..a8589f48 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -28,6 +28,7 @@ import useApi from 'hooks/useApi' import { generateExportFlowData } from 'utils/genericHelper' import { uiBaseURL } from 'store/constant' import { SET_CHATFLOW } from 'store/actions' +import SpeechToTextDialog from '../../ui-component/dialog/SpeechToTextDialog' // ==============================|| CANVAS HEADER ||============================== // @@ -46,6 +47,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [apiDialogProps, setAPIDialogProps] = useState({}) const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false) const [analyseDialogProps, setAnalyseDialogProps] = useState({}) + const [speechToAudioDialogOpen, setSpeechToAudioDialogOpen] = useState(false) + const [speechToAudioDialogProps, setSpeechToAudioialogProps] = useState({}) const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false) const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({}) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) @@ -71,6 +74,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl chatflow: chatflow }) setAnalyseDialogOpen(true) + } else if (setting === 'enableSpeechToText') { + setSpeechToAudioialogProps({ + title: 'Speech to Text', + chatflow: chatflow + }) + setSpeechToAudioDialogOpen(true) } else if (setting === 'viewMessages') { setViewMessagesDialogProps({ title: 'View Messages', @@ -385,6 +394,11 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl /> setAPIDialogOpen(false)} /> setAnalyseDialogOpen(false)} /> + setSpeechToAudioDialogOpen(false)} + /> Date: Sun, 28 Jan 2024 19:45:19 +0530 Subject: [PATCH 334/502] Update MongoDBMemory.ts --- .../memory/MongoDBMemory/MongoDBMemory.ts | 145 +++++++++--------- 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index c593c20d..b35de5ae 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,9 +1,19 @@ -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 } from '../../../src/utils' -import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +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 } from '../../../src/utils'; +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface'; + +let mongoClientSingleton = null; + +const getMongoClient = async (mongoDBConnectUrl) => { + if (!mongoClientSingleton) { + mongoClientSingleton = new MongoClient(mongoDBConnectUrl, { useNewUrlParser: true, useUnifiedTopology: true }); + await mongoClientSingleton.connect(); + } + return mongoClientSingleton; +}; class MongoDB_Memory implements INode { label: string @@ -18,20 +28,20 @@ class MongoDB_Memory implements INode { inputs: INodeParams[] constructor() { - this.label = 'MongoDB Atlas Chat Memory' - this.name = 'MongoDBAtlasChatMemory' - this.version = 1.0 - this.type = 'MongoDBAtlasChatMemory' - this.icon = 'mongodb.svg' - this.category = 'Memory' - this.description = 'Stores the conversation in MongoDB Atlas' - this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)] + this.label = 'MongoDB Atlas Chat Memory'; + this.name = 'MongoDBAtlasChatMemory'; + this.version = 1.0; + this.type = 'MongoDBAtlasChatMemory'; + this.icon = 'mongodb.svg'; + this.category = 'Memory'; + this.description = 'Stores the conversation in MongoDB Atlas'; + this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)]; this.credential = { label: 'Connect Credential', name: 'credential', type: 'credential', credentialNames: ['mongoDBUrlApi'] - } + }; this.inputs = [ { label: 'Database', @@ -49,8 +59,7 @@ class MongoDB_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: - 'If not specified, a random id will be used. Learn more', + description: 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -62,128 +71,126 @@ class MongoDB_Memory implements INode { default: 'chat_history', additionalParams: true } - ] + ]; } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - return initializeMongoDB(nodeData, options) + return initializeMongoDB(nodeData, options); } } const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { - const databaseName = nodeData.inputs?.databaseName as string - const collectionName = nodeData.inputs?.collectionName as string - const memoryKey = nodeData.inputs?.memoryKey as string - const sessionId = nodeData.inputs?.sessionId as string + const databaseName = nodeData.inputs?.databaseName as string; + const collectionName = nodeData.inputs?.collectionName as string; + const memoryKey = nodeData.inputs?.memoryKey as string; + const sessionId = nodeData.inputs?.sessionId as string; - const credentialData = await getCredentialData(nodeData.credential ?? '', options) - const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + const credentialData = await getCredentialData(nodeData.credential ?? '', options); + const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData); - const client = new MongoClient(mongoDBConnectUrl) - await client.connect() - - const collection = client.db(databaseName).collection(collectionName) + const client = await getMongoClient(mongoDBConnectUrl); + const collection = client.db(databaseName).collection(collectionName); const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, sessionId - }) + }); mongoDBChatMessageHistory.getMessages = async (): Promise => { const document = await collection.findOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId - }) - const messages = document?.messages || [] - return messages.map(mapStoredMessageToChatMessage) - } + }); + const messages = document?.messages || []; + return messages.map(mapStoredMessageToChatMessage); + }; mongoDBChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { - const messages = [message].map((msg) => msg.toDict()) + const messages = [message].map((msg) => msg.toDict()); await collection.updateOne( { sessionId: (mongoDBChatMessageHistory as any).sessionId }, { $push: { messages: { $each: messages } } }, { upsert: true } - ) - } + ); + }; mongoDBChatMessageHistory.clear = async (): Promise => { - await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }) - } + await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }); + }; return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, sessionId, collection - }) -} + }); +}; interface BufferMemoryExtendedInput { - collection: Collection - sessionId: string + collection: Collection; + sessionId: string; } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - sessionId = '' - collection: Collection + sessionId = ''; + collection: Collection; constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { - super(fields) - this.sessionId = fields.sessionId - this.collection = fields.collection + super(fields); + this.sessionId = fields.sessionId; + this.collection = fields.collection; } async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { - if (!this.collection) return [] + 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) + 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 + 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') + 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()) + 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()) + 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 + if (!this.collection) return; - const id = overrideSessionId ?? this.sessionId - await this.collection.deleteOne({ sessionId: id }) - await this.clear() + const id = overrideSessionId ?? this.sessionId; + await this.collection.deleteOne({ sessionId: id }); + await this.clear(); } } -module.exports = { nodeClass: MongoDB_Memory } +module.exports = { nodeClass: MongoDB_Memory }; From e154461f1dac59415f867ed5acee7e0f6d195cdb Mon Sep 17 00:00:00 2001 From: Anuj Verma <42962743+Ashes47@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:33:09 +0530 Subject: [PATCH 335/502] Update MongoDBMemory.ts --- .../memory/MongoDBMemory/MongoDBMemory.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index b35de5ae..c2c7ada6 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,19 +1,19 @@ -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 } from '../../../src/utils'; -import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface'; +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 } from '../../../src/utils' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' -let mongoClientSingleton = null; +let mongoClientSingleton = null const getMongoClient = async (mongoDBConnectUrl) => { if (!mongoClientSingleton) { - mongoClientSingleton = new MongoClient(mongoDBConnectUrl, { useNewUrlParser: true, useUnifiedTopology: true }); - await mongoClientSingleton.connect(); + mongoClientSingleton = new MongoClient(mongoDBConnectUrl, { useNewUrlParser: true, useUnifiedTopology: true }) + await mongoClientSingleton.connect() } - return mongoClientSingleton; -}; + return mongoClientSingleton +} class MongoDB_Memory implements INode { label: string From 36ab1681ac455a5cea1482e98d9765eaf3042071 Mon Sep 17 00:00:00 2001 From: Ashes47 Date: Sun, 28 Jan 2024 21:39:22 +0530 Subject: [PATCH 336/502] lint --- .../memory/MongoDBMemory/MongoDBMemory.ts | 121 +++++++++--------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index c2c7ada6..3c2903f3 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -28,20 +28,20 @@ class MongoDB_Memory implements INode { inputs: INodeParams[] constructor() { - this.label = 'MongoDB Atlas Chat Memory'; - this.name = 'MongoDBAtlasChatMemory'; - this.version = 1.0; - this.type = 'MongoDBAtlasChatMemory'; - this.icon = 'mongodb.svg'; - this.category = 'Memory'; - this.description = 'Stores the conversation in MongoDB Atlas'; - this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)]; + this.label = 'MongoDB Atlas Chat Memory' + this.name = 'MongoDBAtlasChatMemory' + this.version = 1.0 + this.type = 'MongoDBAtlasChatMemory' + this.icon = 'mongodb.svg' + this.category = 'Memory' + this.description = 'Stores the conversation in MongoDB Atlas' + this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)] this.credential = { label: 'Connect Credential', name: 'credential', type: 'credential', credentialNames: ['mongoDBUrlApi'] - }; + } this.inputs = [ { label: 'Database', @@ -59,7 +59,8 @@ class MongoDB_Memory implements INode { label: 'Session Id', name: 'sessionId', type: 'string', - description: 'If not specified, a random id will be used. Learn more', + description: + 'If not specified, a random id will be used. Learn more', default: '', additionalParams: true, optional: true @@ -71,126 +72,126 @@ class MongoDB_Memory implements INode { default: 'chat_history', additionalParams: true } - ]; + ] } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - return initializeMongoDB(nodeData, options); + return initializeMongoDB(nodeData, options) } } const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { - const databaseName = nodeData.inputs?.databaseName as string; - const collectionName = nodeData.inputs?.collectionName as string; - const memoryKey = nodeData.inputs?.memoryKey as string; - const sessionId = nodeData.inputs?.sessionId as string; + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const memoryKey = nodeData.inputs?.memoryKey as string + const sessionId = nodeData.inputs?.sessionId as string - const credentialData = await getCredentialData(nodeData.credential ?? '', options); - const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData); + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) - const client = await getMongoClient(mongoDBConnectUrl); - const collection = client.db(databaseName).collection(collectionName); + const client = await getMongoClient(mongoDBConnectUrl) + const collection = client.db(databaseName).collection(collectionName) const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, sessionId - }); + }) mongoDBChatMessageHistory.getMessages = async (): Promise => { const document = await collection.findOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId - }); - const messages = document?.messages || []; - return messages.map(mapStoredMessageToChatMessage); - }; + }) + const messages = document?.messages || [] + return messages.map(mapStoredMessageToChatMessage) + } mongoDBChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { - const messages = [message].map((msg) => msg.toDict()); + const messages = [message].map((msg) => msg.toDict()) await collection.updateOne( { sessionId: (mongoDBChatMessageHistory as any).sessionId }, { $push: { messages: { $each: messages } } }, { upsert: true } - ); - }; + ) + } mongoDBChatMessageHistory.clear = async (): Promise => { - await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }); - }; + await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }) + } return new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: mongoDBChatMessageHistory, sessionId, collection - }); -}; + }) +} interface BufferMemoryExtendedInput { - collection: Collection; - sessionId: string; + collection: Collection + sessionId: string } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - sessionId = ''; - collection: Collection; + sessionId = '' + collection: Collection constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { - super(fields); - this.sessionId = fields.sessionId; - this.collection = fields.collection; + super(fields) + this.sessionId = fields.sessionId + this.collection = fields.collection } async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { - if (!this.collection) return []; + 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); + 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; + 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'); + 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()); + 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()); + 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; + if (!this.collection) return - const id = overrideSessionId ?? this.sessionId; - await this.collection.deleteOne({ sessionId: id }); - await this.clear(); + const id = overrideSessionId ?? this.sessionId + await this.collection.deleteOne({ sessionId: id }) + await this.clear() } } -module.exports = { nodeClass: MongoDB_Memory }; +module.exports = { nodeClass: MongoDB_Memory } From 51388d50577f11b6d6757d1bbeb29e5d1f01db2f Mon Sep 17 00:00:00 2001 From: Ashes47 Date: Sun, 28 Jan 2024 21:48:15 +0530 Subject: [PATCH 337/502] update --- .../nodes/memory/MongoDBMemory/MongoDBMemory.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 3c2903f3..7399ad81 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -5,12 +5,15 @@ import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, BaseMessage } f import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' -let mongoClientSingleton = null +let mongoClientSingleton: MongoClient +let mongoUrl: string -const getMongoClient = async (mongoDBConnectUrl) => { - if (!mongoClientSingleton) { - mongoClientSingleton = new MongoClient(mongoDBConnectUrl, { useNewUrlParser: true, useUnifiedTopology: true }) +const getMongoClient = async (newMongoUrl: string) => { + if (!mongoClientSingleton || newMongoUrl !== mongoUrl) { + mongoClientSingleton = new MongoClient(newMongoUrl) + mongoUrl = newMongoUrl await mongoClientSingleton.connect() + return mongoClientSingleton } return mongoClientSingleton } From 1c108f35999a490c1d3727cdfb32736765d62587 Mon Sep 17 00:00:00 2001 From: Ashes47 Date: Sun, 28 Jan 2024 21:51:27 +0530 Subject: [PATCH 338/502] update --- .../nodes/memory/MongoDBMemory/MongoDBMemory.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 7399ad81..b7309dcd 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -9,15 +9,20 @@ let mongoClientSingleton: MongoClient let mongoUrl: string const getMongoClient = async (newMongoUrl: string) => { - if (!mongoClientSingleton || newMongoUrl !== mongoUrl) { + if (!mongoClientSingleton) { + // if client doesn't exists + mongoClientSingleton = new MongoClient(newMongoUrl) + mongoUrl = newMongoUrl + return mongoClientSingleton + } else if (mongoClientSingleton && newMongoUrl !== mongoUrl) { + // if client exists but url changed + mongoClientSingleton.close() mongoClientSingleton = new MongoClient(newMongoUrl) mongoUrl = newMongoUrl - await mongoClientSingleton.connect() return mongoClientSingleton } return mongoClientSingleton } - class MongoDB_Memory implements INode { label: string name: string From 393f9b57c69e1405731f7a9014a6b790713fd772 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 28 Jan 2024 17:18:18 +0000 Subject: [PATCH 339/502] use singleton redis connection --- .../nodes/cache/RedisCache/RedisCache.ts | 43 ++++++++++++- .../cache/RedisCache/RedisEmbeddingsCache.ts | 43 ++++++++++++- .../RedisBackedChatMemory.ts | 61 ++++++++++++------- .../nodes/vectorstores/Qdrant/Qdrant.ts | 7 --- .../nodes/vectorstores/Redis/Redis.ts | 31 ++++++++-- .../vectorstores/Redis/RedisSearchBase.ts | 28 +++++++-- .../vectorstores/Redis/Redis_Existing.ts | 1 - .../nodes/vectorstores/Redis/Redis_Upsert.ts | 1 - 8 files changed, 169 insertions(+), 46 deletions(-) diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index 4e61c239..c93adf58 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -1,9 +1,46 @@ import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' import { RedisCache as LangchainRedisCache } from 'langchain/cache/ioredis' -import { Redis } from 'ioredis' +import { Redis, RedisOptions } from 'ioredis' +import { isEqual } from 'lodash' import { Generation, ChatGeneration, StoredGeneration, mapStoredMessageToChatMessage } from 'langchain/schema' import hash from 'object-hash' +let redisClientSingleton: Redis +let redisClientOption: RedisOptions +let redisClientUrl: string + +const getRedisClientbyOption = (option: RedisOptions) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } else if (redisClientSingleton && !isEqual(option, redisClientOption)) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } + return redisClientSingleton +} + +const getRedisClientbyUrl = (url: string) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(url) + redisClientUrl = url + return redisClientSingleton + } else if (redisClientSingleton && url !== redisClientUrl) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = new Redis(url) + redisClientUrl = url + return redisClientSingleton + } + return redisClientSingleton +} + class RedisCache implements INode { label: string name: string @@ -60,7 +97,7 @@ class RedisCache implements INode { const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} - client = new Redis({ + client = getRedisClientbyOption({ port: portStr ? parseInt(portStr) : 6379, host, username, @@ -68,7 +105,7 @@ class RedisCache implements INode { ...tlsOptions }) } else { - client = new Redis(redisUrl) + client = getRedisClientbyUrl(redisUrl) } const redisClient = new LangchainRedisCache(client) diff --git a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts index fe1b4df8..b74413fe 100644 --- a/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts @@ -1,9 +1,46 @@ import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' -import { Redis } from 'ioredis' +import { Redis, RedisOptions } from 'ioredis' +import { isEqual } from 'lodash' import { CacheBackedEmbeddings } from 'langchain/embeddings/cache_backed' import { RedisByteStore } from 'langchain/storage/ioredis' import { Embeddings } from 'langchain/embeddings/base' +let redisClientSingleton: Redis +let redisClientOption: RedisOptions +let redisClientUrl: string + +const getRedisClientbyOption = (option: RedisOptions) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } else if (redisClientSingleton && !isEqual(option, redisClientOption)) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } + return redisClientSingleton +} + +const getRedisClientbyUrl = (url: string) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(url) + redisClientUrl = url + return redisClientSingleton + } else if (redisClientSingleton && url !== redisClientUrl) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = new Redis(url) + redisClientUrl = url + return redisClientSingleton + } + return redisClientSingleton +} + class RedisEmbeddingsCache implements INode { label: string name: string @@ -75,7 +112,7 @@ class RedisEmbeddingsCache implements INode { const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} - client = new Redis({ + client = getRedisClientbyOption({ port: portStr ? parseInt(portStr) : 6379, host, username, @@ -83,7 +120,7 @@ class RedisEmbeddingsCache implements INode { ...tlsOptions }) } else { - client = new Redis(redisUrl) + client = getRedisClientbyUrl(redisUrl) } ttl ??= '3600' diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index baf4ea6b..c54e07b5 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -1,10 +1,47 @@ -import { Redis } from 'ioredis' +import { Redis, RedisOptions } from 'ioredis' +import { isEqual } from 'lodash' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis' import { mapStoredMessageToChatMessage, BaseMessage, AIMessage, HumanMessage } from 'langchain/schema' import { INode, INodeData, INodeParams, ICommonObject, MessageType, IMessage, MemoryMethods, FlowiseMemory } from '../../../src/Interface' import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +let redisClientSingleton: Redis +let redisClientOption: RedisOptions +let redisClientUrl: string + +const getRedisClientbyOption = (option: RedisOptions) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } else if (redisClientSingleton && !isEqual(option, redisClientOption)) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } + return redisClientSingleton +} + +const getRedisClientbyUrl = (url: string) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(url) + redisClientUrl = url + return redisClientSingleton + } else if (redisClientSingleton && url !== redisClientUrl) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = new Redis(url) + redisClientUrl = url + return redisClientSingleton + } + return redisClientSingleton +} + class RedisBackedChatMemory_Memory implements INode { label: string name: string @@ -95,7 +132,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const tlsOptions = sslEnabled === true ? { tls: { rejectUnauthorized: false } } : {} - client = new Redis({ + client = getRedisClientbyOption({ port: portStr ? parseInt(portStr) : 6379, host, username, @@ -103,7 +140,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom ...tlsOptions }) } else { - client = new Redis(redisUrl) + client = getRedisClientbyUrl(redisUrl) } let obj: RedisChatMessageHistoryInput = { @@ -120,24 +157,6 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom const redisChatMessageHistory = new RedisChatMessageHistory(obj) - /*redisChatMessageHistory.getMessages = async (): Promise => { - const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, windowSize ? -windowSize : 0, -1) - const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) - return orderedMessages.map(mapStoredMessageToChatMessage) - } - - redisChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { - const messageToAdd = [message].map((msg) => msg.toDict()) - await client.lpush((redisChatMessageHistory as any).sessionId, JSON.stringify(messageToAdd[0])) - if (sessionTTL) { - await client.expire((redisChatMessageHistory as any).sessionId, sessionTTL) - } - } - - redisChatMessageHistory.clear = async (): Promise => { - await client.del((redisChatMessageHistory as any).sessionId) - }*/ - const memory = new BufferMemoryExtended({ memoryKey: memoryKey ?? 'chat_history', chatHistory: redisChatMessageHistory, diff --git a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts index 1d5f7788..80899942 100644 --- a/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts +++ b/packages/components/nodes/vectorstores/Qdrant/Qdrant.ts @@ -1,6 +1,5 @@ import { flatten } from 'lodash' import { QdrantClient } from '@qdrant/js-client-rest' -import type { Schemas as QdrantSchemas } from '@qdrant/js-client-rest' import { VectorStoreRetrieverInput } from 'langchain/vectorstores/base' import { Document } from 'langchain/document' import { QdrantVectorStore, QdrantLibArgs } from 'langchain/vectorstores/qdrant' @@ -9,12 +8,6 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' type RetrieverConfig = Partial> -type QdrantSearchResponse = QdrantSchemas['ScoredPoint'] & { - payload: { - metadata: object - content: string - } -} class Qdrant_VectorStores implements INode { label: string diff --git a/packages/components/nodes/vectorstores/Redis/Redis.ts b/packages/components/nodes/vectorstores/Redis/Redis.ts index 49f9e8ff..0dddf782 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis.ts @@ -1,5 +1,5 @@ -import { flatten } from 'lodash' -import { createClient, SearchOptions } from 'redis' +import { flatten, isEqual } from 'lodash' +import { createClient, SearchOptions, RedisClientOptions } from 'redis' import { Embeddings } from 'langchain/embeddings/base' import { RedisVectorStore, RedisVectorStoreConfig } from 'langchain/vectorstores/redis' import { Document } from 'langchain/document' @@ -7,6 +7,27 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { escapeAllStrings, escapeSpecialChars, unEscapeSpecialChars } from './utils' +let redisClientSingleton: ReturnType +let redisClientOption: RedisClientOptions + +const getRedisClient = async (option: RedisClientOptions) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = createClient(option) + await redisClientSingleton.connect() + redisClientOption = option + return redisClientSingleton + } else if (redisClientSingleton && !isEqual(option, redisClientOption)) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = createClient(option) + await redisClientSingleton.connect() + redisClientOption = option + return redisClientSingleton + } + return redisClientSingleton +} + class Redis_VectorStores implements INode { label: string name: string @@ -149,8 +170,7 @@ class Redis_VectorStores implements INode { } try { - const redisClient = createClient({ url: redisUrl }) - await redisClient.connect() + const redisClient = await getRedisClient({ url: redisUrl }) const storeConfig: RedisVectorStoreConfig = { redisClient: redisClient, @@ -210,8 +230,7 @@ class Redis_VectorStores implements INode { redisUrl = 'redis://' + username + ':' + password + '@' + host + ':' + portStr } - const redisClient = createClient({ url: redisUrl }) - await redisClient.connect() + const redisClient = await getRedisClient({ url: redisUrl }) const storeConfig: RedisVectorStoreConfig = { redisClient: redisClient, diff --git a/packages/components/nodes/vectorstores/Redis/RedisSearchBase.ts b/packages/components/nodes/vectorstores/Redis/RedisSearchBase.ts index b6aa6ebb..e87b49f9 100644 --- a/packages/components/nodes/vectorstores/Redis/RedisSearchBase.ts +++ b/packages/components/nodes/vectorstores/Redis/RedisSearchBase.ts @@ -7,13 +7,34 @@ import { INodeOutputsValue, INodeParams } from '../../../src' - import { Embeddings } from 'langchain/embeddings/base' import { VectorStore } from 'langchain/vectorstores/base' import { Document } from 'langchain/document' -import { createClient, SearchOptions } from 'redis' +import { createClient, SearchOptions, RedisClientOptions } from 'redis' import { RedisVectorStore } from 'langchain/vectorstores/redis' import { escapeSpecialChars, unEscapeSpecialChars } from './utils' +import { isEqual } from 'lodash' + +let redisClientSingleton: ReturnType +let redisClientOption: RedisClientOptions + +const getRedisClient = async (option: RedisClientOptions) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = createClient(option) + await redisClientSingleton.connect() + redisClientOption = option + return redisClientSingleton + } else if (redisClientSingleton && !isEqual(option, redisClientOption)) { + // if client exists but option changed + redisClientSingleton.quit() + redisClientSingleton = createClient(option) + await redisClientSingleton.connect() + redisClientOption = option + return redisClientSingleton + } + return redisClientSingleton +} export abstract class RedisSearchBase { label: string @@ -141,8 +162,7 @@ export abstract class RedisSearchBase { redisUrl = 'redis://' + username + ':' + password + '@' + host + ':' + portStr } - this.redisClient = createClient({ url: redisUrl }) - await this.redisClient.connect() + this.redisClient = await getRedisClient({ url: redisUrl }) const vectorStore = await this.constructVectorStore(embeddings, indexName, replaceIndex, docs) if (!contentKey || contentKey === '') contentKey = 'content' diff --git a/packages/components/nodes/vectorstores/Redis/Redis_Existing.ts b/packages/components/nodes/vectorstores/Redis/Redis_Existing.ts index e8848d33..6f2ec9b3 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis_Existing.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis_Existing.ts @@ -3,7 +3,6 @@ import { Embeddings } from 'langchain/embeddings/base' import { VectorStore } from 'langchain/vectorstores/base' import { RedisVectorStore, RedisVectorStoreConfig } from 'langchain/vectorstores/redis' import { Document } from 'langchain/document' - import { RedisSearchBase } from './RedisSearchBase' class RedisExisting_VectorStores extends RedisSearchBase implements INode { diff --git a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts index 4da58eaf..c4394824 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts @@ -1,7 +1,6 @@ import { ICommonObject, INode, INodeData } from '../../../src/Interface' import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' - import { flatten } from 'lodash' import { RedisSearchBase } from './RedisSearchBase' import { VectorStore } from 'langchain/vectorstores/base' From 74f7cd6e311ebd87699461537ce407a0a3760d20 Mon Sep 17 00:00:00 2001 From: niztal Date: Sun, 28 Jan 2024 23:34:21 +0200 Subject: [PATCH 340/502] fix bug settings.json not found on upsert --- packages/server/src/DataSource.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 762315ac..9ea391ef 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -1,5 +1,6 @@ import 'reflect-metadata' import path from 'path' +import * as fs from 'fs' import { DataSource } from 'typeorm' import { getUserHome } from './utils' import { entities } from './database/entities' @@ -10,10 +11,14 @@ import { postgresMigrations } from './database/migrations/postgres' let appDataSource: DataSource export const init = async (): Promise => { - let homePath + let homePath; + let flowisePath = path.join(getUserHome(), '.flowise'); + if (!fs.existsSync(flowisePath)) { + fs.mkdirSync(flowisePath); + } switch (process.env.DATABASE_TYPE) { case 'sqlite': - homePath = process.env.DATABASE_PATH ?? path.join(getUserHome(), '.flowise') + homePath = process.env.DATABASE_PATH ?? flowisePath; appDataSource = new DataSource({ type: 'sqlite', database: path.resolve(homePath, 'database.sqlite'), @@ -54,7 +59,7 @@ export const init = async (): Promise => { }) break default: - homePath = process.env.DATABASE_PATH ?? path.join(getUserHome(), '.flowise') + homePath = process.env.DATABASE_PATH ?? flowisePath; appDataSource = new DataSource({ type: 'sqlite', database: path.resolve(homePath, 'database.sqlite'), From 30ff29afc7113efdb49b1f8bf37e1a161c179467 Mon Sep 17 00:00:00 2001 From: niztal Date: Sun, 28 Jan 2024 23:49:42 +0200 Subject: [PATCH 341/502] fix lint errors --- packages/server/src/DataSource.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 9ea391ef..a23b4ce2 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -11,14 +11,14 @@ import { postgresMigrations } from './database/migrations/postgres' let appDataSource: DataSource export const init = async (): Promise => { - let homePath; - let flowisePath = path.join(getUserHome(), '.flowise'); + let homePath + let flowisePath = path.join(getUserHome(), '.flowise') if (!fs.existsSync(flowisePath)) { - fs.mkdirSync(flowisePath); + fs.mkdirSync(flowisePath) } switch (process.env.DATABASE_TYPE) { case 'sqlite': - homePath = process.env.DATABASE_PATH ?? flowisePath; + homePath = process.env.DATABASE_PATH ?? flowisePath appDataSource = new DataSource({ type: 'sqlite', database: path.resolve(homePath, 'database.sqlite'), @@ -59,7 +59,7 @@ export const init = async (): Promise => { }) break default: - homePath = process.env.DATABASE_PATH ?? flowisePath; + homePath = process.env.DATABASE_PATH ?? flowisePath appDataSource = new DataSource({ type: 'sqlite', database: path.resolve(homePath, 'database.sqlite'), From 21c47d8049c920631c1c53fb057b696fee319fca Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 28 Jan 2024 23:46:55 +0000 Subject: [PATCH 342/502] add subquery engine --- .../ChatOpenAI/ChatOpenAI_LlamaIndex.ts | 8 + .../OpenAIEmbedding_LlamaIndex.ts | 25 ++- .../SubQuestionQueryEngine.ts | 193 ++++++++++++++++++ .../SubQuestionQueryEngine/subQueryEngine.svg | 1 + .../tools/QueryEngineTool/QueryEngineTool.ts | 68 ++++++ .../tools/QueryEngineTool/queryEngineTool.svg | 1 + .../Pinecone/Pinecone_LlamaIndex.ts | 37 +++- .../vectorstores/SimpleStore/SimpleStore.ts | 27 ++- .../chatflows/Context Chat Engine.json | 64 +++++- .../marketplaces/chatflows/Query Engine.json | 48 ++++- packages/server/src/utils/index.ts | 2 +- 11 files changed, 453 insertions(+), 21 deletions(-) create mode 100644 packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts create mode 100644 packages/components/nodes/engine/SubQuestionQueryEngine/subQueryEngine.svg create mode 100644 packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts create mode 100644 packages/components/nodes/tools/QueryEngineTool/queryEngineTool.svg diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts index 58b40823..8b3567a6 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI_LlamaIndex.ts @@ -41,6 +41,14 @@ class ChatOpenAI_LlamaIndex_LLMs implements INode { label: 'gpt-4', name: 'gpt-4' }, + { + label: 'gpt-4-turbo-preview', + name: 'gpt-4-turbo-preview' + }, + { + label: 'gpt-4-0125-preview', + name: 'gpt-4-0125-preview' + }, { label: 'gpt-4-1106-preview', name: 'gpt-4-1106-preview' diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts index dfd6bbf5..960197fe 100644 --- a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts +++ b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding_LlamaIndex.ts @@ -32,6 +32,27 @@ class OpenAIEmbedding_LlamaIndex_Embeddings implements INode { credentialNames: ['openAIApi'] } this.inputs = [ + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'text-embedding-3-large', + name: 'text-embedding-3-large' + }, + { + label: 'text-embedding-3-small', + name: 'text-embedding-3-small' + }, + { + label: 'text-embedding-ada-002', + name: 'text-embedding-ada-002' + } + ], + default: 'text-embedding-ada-002', + optional: true + }, { label: 'Timeout', name: 'timeout', @@ -51,12 +72,14 @@ class OpenAIEmbedding_LlamaIndex_Embeddings implements INode { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const timeout = nodeData.inputs?.timeout as string + const modelName = nodeData.inputs?.modelName as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData) const obj: Partial = { - apiKey: openAIApiKey + apiKey: openAIApiKey, + model: modelName } if (timeout) obj.timeout = parseInt(timeout, 10) diff --git a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts new file mode 100644 index 00000000..a872c0a2 --- /dev/null +++ b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts @@ -0,0 +1,193 @@ +import { flatten } from 'lodash' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { + TreeSummarize, + SimpleResponseBuilder, + Refine, + BaseEmbedding, + ResponseSynthesizer, + CompactAndRefine, + QueryEngineTool, + LLMQuestionGenerator, + SubQuestionQueryEngine, + BaseNode, + Metadata, + serviceContextFromDefaults +} from 'llamaindex' +import { reformatSourceDocuments } from '../EngineUtils' + +class SubQuestionQueryEngine_LlamaIndex implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + tags: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + sessionId?: string + + constructor(fields?: { sessionId?: string }) { + this.label = 'Sub Question Query Engine' + this.name = 'subQuestionQueryEngine' + this.version = 1.0 + this.type = 'SubQuestionQueryEngine' + this.icon = 'subQueryEngine.svg' + this.category = 'Engine' + this.description = + 'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response' + this.baseClasses = [this.type] + this.tags = ['LlamaIndex'] + this.inputs = [ + { + label: 'QueryEngine Tools', + name: 'queryEngineTools', + type: 'QueryEngineTool', + list: true + }, + { + label: 'Chat Model', + name: 'model', + type: 'BaseChatModel_LlamaIndex' + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'BaseEmbedding_LlamaIndex' + }, + { + label: 'Response Synthesizer', + name: 'responseSynthesizer', + type: 'ResponseSynthesizer', + description: + 'ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See more', + optional: true + }, + { + label: 'Return Source Documents', + name: 'returnSourceDocuments', + type: 'boolean', + optional: true + } + ] + this.sessionId = fields?.sessionId + } + + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean + const embeddings = nodeData.inputs?.embeddings as BaseEmbedding + const model = nodeData.inputs?.model + + const serviceContext = serviceContextFromDefaults({ + llm: model, + embedModel: embeddings + }) + + let queryEngineTools = nodeData.inputs?.queryEngineTools as QueryEngineTool[] + queryEngineTools = flatten(queryEngineTools) + + let queryEngine = SubQuestionQueryEngine.fromDefaults({ + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + + const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer + if (responseSynthesizerObj) { + if (responseSynthesizerObj.type === 'TreeSummarize') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new TreeSummarize(serviceContext, responseSynthesizerObj.textQAPromptTemplate), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } else if (responseSynthesizerObj.type === 'CompactAndRefine') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new CompactAndRefine( + serviceContext, + responseSynthesizerObj.textQAPromptTemplate, + responseSynthesizerObj.refinePromptTemplate + ), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } else if (responseSynthesizerObj.type === 'Refine') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new Refine( + serviceContext, + responseSynthesizerObj.textQAPromptTemplate, + responseSynthesizerObj.refinePromptTemplate + ), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } else if (responseSynthesizerObj.type === 'SimpleResponseBuilder') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new SimpleResponseBuilder(serviceContext), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } + } + + let text = '' + let sourceDocuments: ICommonObject[] = [] + let sourceNodes: BaseNode[] = [] + let isStreamingStarted = false + const isStreamingEnabled = options.socketIO && options.socketIOClientId + + if (isStreamingEnabled) { + const stream = await queryEngine.query({ query: input, stream: true }) + for await (const chunk of stream) { + text += chunk.response + if (chunk.sourceNodes) sourceNodes = chunk.sourceNodes + if (!isStreamingStarted) { + isStreamingStarted = true + options.socketIO.to(options.socketIOClientId).emit('start', chunk.response) + } + + options.socketIO.to(options.socketIOClientId).emit('token', chunk.response) + } + + if (returnSourceDocuments) { + sourceDocuments = reformatSourceDocuments(sourceNodes) + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', sourceDocuments) + } + } else { + const response = await queryEngine.query({ query: input }) + text = response?.response + sourceDocuments = reformatSourceDocuments(response?.sourceNodes ?? []) + } + + if (returnSourceDocuments) return { text, sourceDocuments } + else return { text } + } +} + +module.exports = { nodeClass: SubQuestionQueryEngine_LlamaIndex } diff --git a/packages/components/nodes/engine/SubQuestionQueryEngine/subQueryEngine.svg b/packages/components/nodes/engine/SubQuestionQueryEngine/subQueryEngine.svg new file mode 100644 index 00000000..b94c20b5 --- /dev/null +++ b/packages/components/nodes/engine/SubQuestionQueryEngine/subQueryEngine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts b/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts new file mode 100644 index 00000000..163eff76 --- /dev/null +++ b/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts @@ -0,0 +1,68 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { VectorStoreIndex } from 'llamaindex' + +class QueryEngine_Tools implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + tags: string[] + baseClasses: string[] + inputs?: INodeParams[] + + constructor() { + this.label = 'QueryEngine Tool' + this.name = 'queryEngineToolLlamaIndex' + this.version = 1.0 + this.type = 'QueryEngineTool' + this.icon = 'queryEngineTool.svg' + this.category = 'Tools' + this.tags = ['LlamaIndex'] + this.description = 'Tool used to invoke query engine' + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'Vector Store Index', + name: 'vectorStoreIndex', + type: 'VectorStoreIndex' + }, + { + label: 'Tool Name', + name: 'toolName', + type: 'string', + description: 'Tool name must be small capital letter with underscore. Ex: my_tool' + }, + { + label: 'Tool Description', + name: 'toolDesc', + type: 'string', + rows: 4 + } + ] + } + + async init(nodeData: INodeData): Promise { + const vectorStoreIndex = nodeData.inputs?.vectorStoreIndex as VectorStoreIndex + const toolName = nodeData.inputs?.toolName as string + const toolDesc = nodeData.inputs?.toolDesc as string + const queryEngineTool = { + queryEngine: vectorStoreIndex.asQueryEngine({ + preFilters: { + ...(vectorStoreIndex as any).metadatafilter + } + }), + metadata: { + name: toolName, + description: toolDesc + }, + vectorStoreIndex + } + + return queryEngineTool + } +} + +module.exports = { nodeClass: QueryEngine_Tools } diff --git a/packages/components/nodes/tools/QueryEngineTool/queryEngineTool.svg b/packages/components/nodes/tools/QueryEngineTool/queryEngineTool.svg new file mode 100644 index 00000000..d49d8375 --- /dev/null +++ b/packages/components/nodes/tools/QueryEngineTool/queryEngineTool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts index a584fede..c0b2e5c1 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone_LlamaIndex.ts @@ -13,7 +13,7 @@ import { import { FetchResponse, Index, Pinecone, ScoredPineconeRecord } from '@pinecone-database/pinecone' import { flatten } from 'lodash' import { Document as LCDocument } from 'langchain/document' -import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' import { flattenObject, getCredentialData, getCredentialParam } from '../../../src/utils' class PineconeLlamaIndex_VectorStores implements INode { @@ -28,6 +28,7 @@ class PineconeLlamaIndex_VectorStores implements INode { baseClasses: string[] inputs: INodeParams[] credential: INodeParams + outputs: INodeOutputsValue[] constructor() { this.label = 'Pinecone' @@ -93,6 +94,18 @@ class PineconeLlamaIndex_VectorStores implements INode { optional: true } ] + this.outputs = [ + { + label: 'Pinecone Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Pinecone Vector Store Index', + name: 'vectorStore', + baseClasses: [this.type, 'VectorStoreIndex'] + } + ] } //@ts-ignore @@ -155,8 +168,10 @@ class PineconeLlamaIndex_VectorStores implements INode { } if (pineconeNamespace) obj.namespace = pineconeNamespace + + let metadatafilter = {} if (pineconeMetadataFilter) { - const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter) + metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter) obj.queryFilter = metadatafilter } @@ -171,11 +186,21 @@ class PineconeLlamaIndex_VectorStores implements INode { serviceContext }) - const retriever = index.asRetriever() - retriever.similarityTopK = k - ;(retriever as any).serviceContext = serviceContext + const output = nodeData.outputs?.output as string - return retriever + if (output === 'retriever') { + const retriever = index.asRetriever() + retriever.similarityTopK = k + ;(retriever as any).serviceContext = serviceContext + return retriever + } else if (output === 'vectorStore') { + ;(index as any).k = k + if (metadatafilter) { + ;(index as any).metadatafilter = metadatafilter + } + return index + } + return index } } diff --git a/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts b/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts index eeef6f69..36c383e9 100644 --- a/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts +++ b/packages/components/nodes/vectorstores/SimpleStore/SimpleStore.ts @@ -63,6 +63,18 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode { optional: true } ] + this.outputs = [ + { + label: 'SimpleStore Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'SimpleStore Vector Store Index', + name: 'vectorStore', + baseClasses: [this.type, 'VectorStoreIndex'] + } + ] } //@ts-ignore @@ -114,10 +126,19 @@ class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode { const storageContext = await storageContextFromDefaults({ persistDir: filePath }) const index = await VectorStoreIndex.init({ storageContext, serviceContext }) - const retriever = index.asRetriever() - retriever.similarityTopK = k - return retriever + const output = nodeData.outputs?.output as string + + if (output === 'retriever') { + const retriever = index.asRetriever() + retriever.similarityTopK = k + ;(retriever as any).serviceContext = serviceContext + return retriever + } else if (output === 'vectorStore') { + ;(index as any).k = k + return index + } + return index } } diff --git a/packages/server/marketplaces/chatflows/Context Chat Engine.json b/packages/server/marketplaces/chatflows/Context Chat Engine.json index 7608a550..475c6b3a 100644 --- a/packages/server/marketplaces/chatflows/Context Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Context Chat Engine.json @@ -181,6 +181,28 @@ "credentialNames": ["openAIApi"], "id": "openAIEmbedding_LlamaIndex_0-input-credential-credential" }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "text-embedding-3-large", + "name": "text-embedding-3-large" + }, + { + "label": "text-embedding-3-small", + "name": "text-embedding-3-small" + }, + { + "label": "text-embedding-ada-002", + "name": "text-embedding-ada-002" + } + ], + "default": "text-embedding-ada-002", + "optional": true, + "id": "openAIEmbedding_LlamaIndex_0-input-modelName-options" + }, { "label": "Timeout", "name": "timeout", @@ -315,13 +337,29 @@ }, "outputAnchors": [ { - "id": "pineconeLlamaIndex_0-output-pineconeLlamaIndex-Pinecone|VectorIndexRetriever", - "name": "pineconeLlamaIndex", - "label": "Pinecone", - "type": "Pinecone | VectorIndexRetriever" + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "pineconeLlamaIndex_0-output-retriever-Pinecone|VectorIndexRetriever", + "name": "retriever", + "label": "Pinecone Retriever", + "type": "Pinecone | VectorIndexRetriever" + }, + { + "id": "pineconeLlamaIndex_0-output-retriever-Pinecone|VectorStoreIndex", + "name": "vectorStore", + "label": "Pinecone Vector Store Index", + "type": "Pinecone | VectorStoreIndex" + } + ], + "default": "retriever" } ], - "outputs": {}, + "outputs": { + "output": "retriever" + }, "selected": false }, "selected": false, @@ -367,6 +405,14 @@ "label": "gpt-4", "name": "gpt-4" }, + { + "label": "gpt-4-turbo-preview", + "name": "gpt-4-turbo-preview" + }, + { + "label": "gpt-4-0125-preview", + "name": "gpt-4-0125-preview" + }, { "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" @@ -672,6 +718,14 @@ "label": "gpt-4", "name": "gpt-4" }, + { + "label": "gpt-4-turbo-preview", + "name": "gpt-4-turbo-preview" + }, + { + "label": "gpt-4-0125-preview", + "name": "gpt-4-0125-preview" + }, { "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" diff --git a/packages/server/marketplaces/chatflows/Query Engine.json b/packages/server/marketplaces/chatflows/Query Engine.json index 625097cc..82553333 100644 --- a/packages/server/marketplaces/chatflows/Query Engine.json +++ b/packages/server/marketplaces/chatflows/Query Engine.json @@ -163,13 +163,29 @@ }, "outputAnchors": [ { - "id": "pineconeLlamaIndex_0-output-pineconeLlamaIndex-Pinecone|VectorIndexRetriever", - "name": "pineconeLlamaIndex", - "label": "Pinecone", - "type": "Pinecone | VectorIndexRetriever" + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "pineconeLlamaIndex_0-output-retriever-Pinecone|VectorIndexRetriever", + "name": "retriever", + "label": "Pinecone Retriever", + "type": "Pinecone | VectorIndexRetriever" + }, + { + "id": "pineconeLlamaIndex_0-output-retriever-Pinecone|VectorStoreIndex", + "name": "vectorStore", + "label": "Pinecone Vector Store Index", + "type": "Pinecone | VectorStoreIndex" + } + ], + "default": "retriever" } ], - "outputs": {}, + "outputs": { + "output": "retriever" + }, "selected": false }, "selected": false, @@ -206,6 +222,28 @@ "credentialNames": ["openAIApi"], "id": "openAIEmbedding_LlamaIndex_0-input-credential-credential" }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "text-embedding-3-large", + "name": "text-embedding-3-large" + }, + { + "label": "text-embedding-3-small", + "name": "text-embedding-3-small" + }, + { + "label": "text-embedding-ada-002", + "name": "text-embedding-ada-002" + } + ], + "default": "text-embedding-ada-002", + "optional": true, + "id": "openAIEmbedding_LlamaIndex_0-input-modelName-options" + }, { "label": "Timeout", "name": "timeout", diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index d0063343..9a14429d 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -844,7 +844,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod const whitelistAgents = ['openAIFunctionAgent', 'csvAgent', 'airtableAgent', 'conversationalRetrievalAgent'] isValidChainOrAgent = whitelistAgents.includes(endingNodeData.name) } else if (endingNodeData.category === 'Engine') { - const whitelistEngine = ['contextChatEngine', 'simpleChatEngine', 'queryEngine'] + const whitelistEngine = ['contextChatEngine', 'simpleChatEngine', 'queryEngine', 'subQuestionQueryEngine'] isValidChainOrAgent = whitelistEngine.includes(endingNodeData.name) } From 71f456af90ab0d9855f9a1f3207e6589decc034b Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 25 Jan 2024 11:00:58 -0500 Subject: [PATCH 343/502] Switched to specifying Airtable fields to include rather than exclude - this helps reduce the amount of data fetched by the DocumentLoader when there are massive numbers of fields in an Airtable table. --- .../documentloaders/Airtable/Airtable.ts | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 0f212a0a..78ad7a66 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -65,13 +65,13 @@ class Airtable_DocumentLoaders implements INode { optional: true }, { - label: 'Exclude Field Names', - name: 'excludeFieldNames', + label: 'Fields', + name: 'fields', type: 'string', - placeholder: 'Name, Assignee', + placeholder: 'Name, Assignee, fld1u0qUz0SoOQ9Gg, fldew39v6LBN5CjUl', optional: true, additionalParams: true, - description: 'Comma-separated list of field names to exclude' + description: 'Comma-separated list of field names or IDs to include. Use field IDs if field names contain commas.' }, { label: 'Return All', @@ -102,7 +102,8 @@ class Airtable_DocumentLoaders implements INode { const baseId = nodeData.inputs?.baseId as string const tableId = nodeData.inputs?.tableId as string const viewId = nodeData.inputs?.viewId as string - const excludeFieldNames = nodeData.inputs?.excludeFieldNames as string + const fieldsInput = nodeData.inputs?.fields as string + const fields = fieldsInput ? fieldsInput.split(',').map((field) => field.trim()) : [] const returnAll = nodeData.inputs?.returnAll as boolean const limit = nodeData.inputs?.limit as string const textSplitter = nodeData.inputs?.textSplitter as TextSplitter @@ -115,7 +116,7 @@ class Airtable_DocumentLoaders implements INode { baseId, tableId, viewId, - excludeFieldNames: excludeFieldNames ? excludeFieldNames.split(',').map((id) => id.trim()) : [], + fields, returnAll, accessToken, limit: limit ? parseInt(limit, 10) : 100 @@ -156,7 +157,7 @@ interface AirtableLoaderParams { tableId: string accessToken: string viewId?: string - excludeFieldNames?: string[] + fields?: string[] limit?: number returnAll?: boolean } @@ -179,7 +180,7 @@ class AirtableLoader extends BaseDocumentLoader { public readonly viewId?: string - public readonly excludeFieldNames: string[] + public readonly fields: string[] public readonly accessToken: string @@ -187,12 +188,12 @@ class AirtableLoader extends BaseDocumentLoader { public readonly returnAll: boolean - constructor({ baseId, tableId, viewId, excludeFieldNames = [], accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { + constructor({ baseId, tableId, viewId, fields = [], accessToken, limit = 100, returnAll = false }: AirtableLoaderParams) { super() this.baseId = baseId this.tableId = tableId this.viewId = viewId - this.excludeFieldNames = excludeFieldNames + this.fields = fields this.accessToken = accessToken this.limit = limit this.returnAll = returnAll @@ -205,14 +206,14 @@ class AirtableLoader extends BaseDocumentLoader { return this.loadLimit() } - protected async fetchAirtableData(url: string, params: ICommonObject): Promise { + protected async fetchAirtableData(url: string, data: any): Promise { try { const headers = { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', Accept: 'application/json' } - const response = await axios.get(url, { params, headers }) + const response = await axios.get(url, data, { headers }) return response.data } catch (error) { throw new Error(`Failed to fetch ${url} from Airtable: ${error}`) @@ -222,12 +223,6 @@ class AirtableLoader extends BaseDocumentLoader { private createDocumentFromPage(page: AirtableLoaderPage): Document { // Generate the URL const pageUrl = `https://api.airtable.com/v0/${this.baseId}/${this.tableId}/${page.id}` - const fields = { ...page.fields } - - // Exclude any specified fields - this.excludeFieldNames.forEach((id) => { - delete fields[id] - }) // Return a langchain document return new Document({ @@ -239,24 +234,40 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadLimit(): Promise { - const params = { maxRecords: this.limit, view: this.viewId } - const data = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, params) - if (data.records.length === 0) { + const data = { + maxRecords: this.limit, + view: this.viewId + } + + if (this.fields.length > 0) { + data.fields = this.fields + } + + const response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, data) + if (response.records.length === 0) { return [] } - return data.records.map((page) => this.createDocumentFromPage(page)) + return response.records.map((page) => this.createDocumentFromPage(page)) } private async loadAll(): Promise { - const params: ICommonObject = { pageSize: 100, view: this.viewId } - let data: AirtableLoaderResponse + const data = { + pageSize: 100, + view: this.viewId + } + + if (this.fields.length > 0) { + data.fields = this.fields + } + + let response: AirtableLoaderResponse let returnPages: AirtableLoaderPage[] = [] do { - data = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, params) - returnPages.push.apply(returnPages, data.records) - params.offset = data.offset - } while (data.offset !== undefined) + response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, data) + returnPages.push.apply(returnPages, response.records) + params.offset = response.offset + } while (response.offset !== undefined) return returnPages.map((page) => this.createDocumentFromPage(page)) } } From ae64854baedca9c6f224136578bb87a1df93afa4 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 25 Jan 2024 11:29:06 -0500 Subject: [PATCH 344/502] Fixing a bunch of build errors --- .../nodes/documentloaders/Airtable/Airtable.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 78ad7a66..6fdb070a 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -162,6 +162,12 @@ interface AirtableLoaderParams { returnAll?: boolean } +interface AirtableLoaderRequest { + maxRecords: number + view: string | undefined + fields?: string[] +} + interface AirtableLoaderResponse { records: AirtableLoaderPage[] offset?: string @@ -213,7 +219,7 @@ class AirtableLoader extends BaseDocumentLoader { 'Content-Type': 'application/json', Accept: 'application/json' } - const response = await axios.get(url, data, { headers }) + const response = await axios.post(url, data, { headers }) return response.data } catch (error) { throw new Error(`Failed to fetch ${url} from Airtable: ${error}`) @@ -226,7 +232,7 @@ class AirtableLoader extends BaseDocumentLoader { // Return a langchain document return new Document({ - pageContent: JSON.stringify(fields, null, 2), + pageContent: JSON.stringify(page.fields, null, 2), metadata: { url: pageUrl } @@ -234,7 +240,7 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadLimit(): Promise { - const data = { + let data: AirtableLoaderRequest = { maxRecords: this.limit, view: this.viewId } @@ -251,7 +257,7 @@ class AirtableLoader extends BaseDocumentLoader { } private async loadAll(): Promise { - const data = { + let data: AirtableLoaderRequest = { pageSize: 100, view: this.viewId } From 1a7cb5a010039f714fbe8e117a9875985866f2f4 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 25 Jan 2024 11:43:56 -0500 Subject: [PATCH 345/502] Fixing more build errors --- .../components/nodes/documentloaders/Airtable/Airtable.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 6fdb070a..de913b90 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -166,6 +166,7 @@ interface AirtableLoaderRequest { maxRecords: number view: string | undefined fields?: string[] + offset?: string } interface AirtableLoaderResponse { @@ -258,7 +259,7 @@ class AirtableLoader extends BaseDocumentLoader { private async loadAll(): Promise { let data: AirtableLoaderRequest = { - pageSize: 100, + pageSize: this.limit, view: this.viewId } @@ -272,7 +273,7 @@ class AirtableLoader extends BaseDocumentLoader { do { response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, data) returnPages.push.apply(returnPages, response.records) - params.offset = response.offset + data.offset = response.offset } while (response.offset !== undefined) return returnPages.map((page) => this.createDocumentFromPage(page)) } From 72ec7878b609e013f3a7b17b3875beab2f936d0e Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 25 Jan 2024 12:08:52 -0500 Subject: [PATCH 346/502] Added more error checking and also fixed yet more build errors --- .../nodes/documentloaders/Airtable/Airtable.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index de913b90..4558edb5 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -124,6 +124,10 @@ class Airtable_DocumentLoaders implements INode { const loader = new AirtableLoader(airtableOptions) + if (!baseId || !tableId) { + throw new Error('Base ID and Table ID must be provided.') + } + let docs = [] if (textSplitter) { @@ -213,7 +217,7 @@ class AirtableLoader extends BaseDocumentLoader { return this.loadLimit() } - protected async fetchAirtableData(url: string, data: any): Promise { + protected async fetchAirtableData(url: string, data: AirtableLoaderRequest): Promise { try { const headers = { Authorization: `Bearer ${this.accessToken}`, @@ -223,7 +227,11 @@ class AirtableLoader extends BaseDocumentLoader { const response = await axios.post(url, data, { headers }) return response.data } catch (error) { - throw new Error(`Failed to fetch ${url} from Airtable: ${error}`) + if (axios.isAxiosError(error)) { + throw new Error(`Failed to fetch ${url} from Airtable: ${error.message}, status: ${error.response?.status}`) + } else { + throw new Error(`Failed to fetch ${url} from Airtable: ${error}`) + } } } @@ -259,7 +267,7 @@ class AirtableLoader extends BaseDocumentLoader { private async loadAll(): Promise { let data: AirtableLoaderRequest = { - pageSize: this.limit, + maxRecords: this.limit, view: this.viewId } @@ -272,7 +280,7 @@ class AirtableLoader extends BaseDocumentLoader { do { response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, data) - returnPages.push.apply(returnPages, response.records) + returnPages.push(...response.records) data.offset = response.offset } while (response.offset !== undefined) return returnPages.map((page) => this.createDocumentFromPage(page)) From 8ae848110eb98726e30994f4442fa9e4c5544e8a Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 25 Jan 2024 21:36:42 -0500 Subject: [PATCH 347/502] Clarifying the description for the optional fields param --- .../components/nodes/documentloaders/Airtable/Airtable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 4558edb5..ed157264 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -65,13 +65,13 @@ class Airtable_DocumentLoaders implements INode { optional: true }, { - label: 'Fields', + label: 'Include Only Fields', name: 'fields', type: 'string', placeholder: 'Name, Assignee, fld1u0qUz0SoOQ9Gg, fldew39v6LBN5CjUl', optional: true, additionalParams: true, - description: 'Comma-separated list of field names or IDs to include. Use field IDs if field names contain commas.' + description: 'Comma-separated list of field names or IDs to include. If empty, then ALL fields are used. Use field IDs if field names contain commas.' }, { label: 'Return All', From 456dfabc6febea31ab11ce1bab22a4b51ac70c8a Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Fri, 26 Jan 2024 13:48:09 -0500 Subject: [PATCH 348/502] For some reason, Airtable doesn't like the POST operations, so I need to add logging to figure out why this happens --- .../components/nodes/documentloaders/Airtable/Airtable.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index ed157264..d0ed3b44 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -71,7 +71,8 @@ class Airtable_DocumentLoaders implements INode { placeholder: 'Name, Assignee, fld1u0qUz0SoOQ9Gg, fldew39v6LBN5CjUl', optional: true, additionalParams: true, - description: 'Comma-separated list of field names or IDs to include. If empty, then ALL fields are used. Use field IDs if field names contain commas.' + description: + 'Comma-separated list of field names or IDs to include. If empty, then ALL fields are used. Use field IDs if field names contain commas.' }, { label: 'Return All', @@ -224,12 +225,15 @@ class AirtableLoader extends BaseDocumentLoader { 'Content-Type': 'application/json', Accept: 'application/json' } + console.log('Sending request to Airtable with data: ', data) const response = await axios.post(url, data, { headers }) return response.data } catch (error) { if (axios.isAxiosError(error)) { + console.error('Error response from Airtable:', error.response?.data) throw new Error(`Failed to fetch ${url} from Airtable: ${error.message}, status: ${error.response?.status}`) } else { + console.error('An unexpected error occurred:', error) throw new Error(`Failed to fetch ${url} from Airtable: ${error}`) } } From 3b788e42e1520c193a909001faa8793a35420caa Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Fri, 26 Jan 2024 15:03:30 -0500 Subject: [PATCH 349/502] When you switch from GET to POST, you're supposed to adjust the Airtable URL to use the /listRecords endpoint. I didn't RTFM clearly. This is currently documented here: https://support.airtable.com/docs/enforcement-of-url-length-limit-for-web-api-requests --- .../components/nodes/documentloaders/Airtable/Airtable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index d0ed3b44..7d5e8ad0 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -262,7 +262,7 @@ class AirtableLoader extends BaseDocumentLoader { data.fields = this.fields } - const response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, data) + const response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}/listRecords`, data) if (response.records.length === 0) { return [] } @@ -283,7 +283,7 @@ class AirtableLoader extends BaseDocumentLoader { let returnPages: AirtableLoaderPage[] = [] do { - response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}`, data) + response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}/listRecords`, data) returnPages.push(...response.records) data.offset = response.offset } while (response.offset !== undefined) From 2237b1ab165d6ff0acd6252b0be7c1e31591a8ec Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Fri, 26 Jan 2024 17:15:39 -0500 Subject: [PATCH 350/502] Fix worked, removing debug logging, and bumped node version. --- .../components/nodes/documentloaders/Airtable/Airtable.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 7d5e8ad0..76d6e349 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -20,7 +20,7 @@ class Airtable_DocumentLoaders implements INode { constructor() { this.label = 'Airtable' this.name = 'airtable' - this.version = 2.0 + this.version = 3.0 this.type = 'Document' this.icon = 'airtable.svg' this.category = 'Document Loaders' @@ -225,15 +225,12 @@ class AirtableLoader extends BaseDocumentLoader { 'Content-Type': 'application/json', Accept: 'application/json' } - console.log('Sending request to Airtable with data: ', data) const response = await axios.post(url, data, { headers }) return response.data } catch (error) { if (axios.isAxiosError(error)) { - console.error('Error response from Airtable:', error.response?.data) throw new Error(`Failed to fetch ${url} from Airtable: ${error.message}, status: ${error.response?.status}`) } else { - console.error('An unexpected error occurred:', error) throw new Error(`Failed to fetch ${url} from Airtable: ${error}`) } } From 9b71f683fffe916baced5693cbe0f070dd043d12 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 27 Jan 2024 13:58:20 -0500 Subject: [PATCH 351/502] Support pagination even in loadLimit(), so that if a user wants to load more than 100 records but not all of them, they can. Currently, there's a bug where the document loader doesn't work on loading more than 100 records because of internal Airtable API limitations. The Airtable API can only fetch up to 100 records per call - anything more than that requires pagination. --- .../documentloaders/Airtable/Airtable.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 76d6e349..011975a7 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -251,7 +251,7 @@ class AirtableLoader extends BaseDocumentLoader { private async loadLimit(): Promise { let data: AirtableLoaderRequest = { - maxRecords: this.limit, + maxRecords: Math.min(this.limit, 100), // Airtable only returns up to 100 records per request view: this.viewId } @@ -259,11 +259,25 @@ class AirtableLoader extends BaseDocumentLoader { data.fields = this.fields } - const response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}/listRecords`, data) - if (response.records.length === 0) { - return [] + let response: AirtableLoaderResponse + let returnPages: AirtableLoaderPage[] = [] + + // Paginate if the user specifies a limit > 100 (like 200) but not return all. + do { + response = await this.fetchAirtableData(`https://api.airtable.com/v0/${this.baseId}/${this.tableId}/listRecords`, data) + returnPages.push(...response.records) + data.offset = response.offset + + // Stop if we have fetched enough records + if (returnPages.length >= this.limit) break + } while (response.offset !== undefined) + + // Truncate array to the limit if necessary + if (returnPages.length > this.limit) { + returnPages.length = this.limit } - return response.records.map((page) => this.createDocumentFromPage(page)) + + return returnPages.map((page) => this.createDocumentFromPage(page)) } private async loadAll(): Promise { From dc39d7e2bec837d863bde0c8b8b52fcdb1b37078 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 27 Jan 2024 16:33:16 -0500 Subject: [PATCH 352/502] So Airtable API expects a maxRecords value to be the total set of records you want across multiple API calls. If the maxRecords is greater than 100, then it will provide pagination hints accordingly. --- packages/components/nodes/documentloaders/Airtable/Airtable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 011975a7..90a7ba03 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -251,7 +251,7 @@ class AirtableLoader extends BaseDocumentLoader { private async loadLimit(): Promise { let data: AirtableLoaderRequest = { - maxRecords: Math.min(this.limit, 100), // Airtable only returns up to 100 records per request + maxRecords: this.limit, view: this.viewId } From 37945fc998cd1e2818756a7877ffb4613be46094 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 27 Jan 2024 21:25:43 -0800 Subject: [PATCH 353/502] The loadAll() function should ignore any maxRecords specified, because the intention is the load *all* of the records. Also, marking both the Return All and Limit params as optional, so as to not confuse the user. Making them both required adds a lot of confusion that doesn't make sense. Ideally, the user either specifies Return All OR specifies the Limit value but not BOTH. It seems there's no way to define "conditional requirements" in Flowise params, so it's better to make both params optional. --- packages/components/nodes/documentloaders/Airtable/Airtable.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 90a7ba03..8845bbe0 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -78,6 +78,7 @@ class Airtable_DocumentLoaders implements INode { label: 'Return All', name: 'returnAll', type: 'boolean', + optional: true, default: true, additionalParams: true, description: 'If all results should be returned or only up to a given limit' @@ -86,6 +87,7 @@ class Airtable_DocumentLoaders implements INode { label: 'Limit', name: 'limit', type: 'number', + optional: true, default: 100, additionalParams: true, description: 'Number of results to return' @@ -282,7 +284,6 @@ class AirtableLoader extends BaseDocumentLoader { private async loadAll(): Promise { let data: AirtableLoaderRequest = { - maxRecords: this.limit, view: this.viewId } From 66eef8463315a5f045b428478abf71b1b53b9996 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sat, 27 Jan 2024 21:40:05 -0800 Subject: [PATCH 354/502] Forgot to make maxRecords optional now --- packages/components/nodes/documentloaders/Airtable/Airtable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index 8845bbe0..b0478100 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -170,7 +170,7 @@ interface AirtableLoaderParams { } interface AirtableLoaderRequest { - maxRecords: number + maxRecords?: number view: string | undefined fields?: string[] offset?: string From b960f061ebbedaa2cbc0b1cdc802999ab9a948df Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sun, 28 Jan 2024 08:21:02 -0800 Subject: [PATCH 355/502] Clarifying that the Limit value is ignored when Return All is set to true. --- packages/components/nodes/documentloaders/Airtable/Airtable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Airtable/Airtable.ts b/packages/components/nodes/documentloaders/Airtable/Airtable.ts index b0478100..14815297 100644 --- a/packages/components/nodes/documentloaders/Airtable/Airtable.ts +++ b/packages/components/nodes/documentloaders/Airtable/Airtable.ts @@ -90,7 +90,7 @@ class Airtable_DocumentLoaders implements INode { optional: true, default: 100, additionalParams: true, - description: 'Number of results to return' + description: 'Number of results to return. Ignored when Return All is enabled.' }, { label: 'Metadata', From 905c9fc2bed208c7cf5327b077c941533eac8157 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sun, 28 Jan 2024 20:48:08 -0800 Subject: [PATCH 356/502] Reverting version bump --- .../nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts index a459a8ec..9b7b724a 100644 --- a/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/AzureChatOpenAI/AzureChatOpenAI.ts @@ -19,7 +19,7 @@ class AzureChatOpenAI_ChatModels implements INode { constructor() { this.label = 'Azure ChatOpenAI' this.name = 'azureChatOpenAI' - this.version = 2.1 + this.version = 2.0 this.type = 'AzureChatOpenAI' this.icon = 'Azure.svg' this.category = 'Chat Models' From 9f4619e40852b470dc83af88c0f55e2403ca0630 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sun, 28 Jan 2024 21:52:40 -0800 Subject: [PATCH 357/502] Switching default batch size from 1 to 100 in order to support higher volume of document loading into vector stores before timing out. --- .../embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts index b70caa4c..75897e6c 100644 --- a/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts +++ b/packages/components/nodes/embeddings/AzureOpenAIEmbedding/AzureOpenAIEmbedding.ts @@ -35,7 +35,7 @@ class AzureOpenAIEmbedding_Embeddings implements INode { label: 'Batch Size', name: 'batchSize', type: 'number', - default: '1', + default: '100', optional: true, additionalParams: true }, From 63665b37ce346f2f1f98737fcc1d7fde3d3a7560 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sun, 28 Jan 2024 21:58:04 -0800 Subject: [PATCH 358/502] Switching Redis TTL from EX to PX to match TTL in milliseconds (as specified in the input param). --- packages/components/nodes/cache/RedisCache/RedisCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/cache/RedisCache/RedisCache.ts b/packages/components/nodes/cache/RedisCache/RedisCache.ts index c93adf58..cf7b43c9 100644 --- a/packages/components/nodes/cache/RedisCache/RedisCache.ts +++ b/packages/components/nodes/cache/RedisCache/RedisCache.ts @@ -131,7 +131,7 @@ class RedisCache implements INode { for (let i = 0; i < value.length; i += 1) { const key = getCacheKey(prompt, llmKey, String(i)) if (ttl) { - await client.set(key, JSON.stringify(serializeGeneration(value[i])), 'EX', parseInt(ttl, 10)) + await client.set(key, JSON.stringify(serializeGeneration(value[i])), 'PX', parseInt(ttl, 10)) } else { await client.set(key, JSON.stringify(serializeGeneration(value[i]))) } From 1b69ebdb93ac514a31e9a82a9d9eb7aca66233d8 Mon Sep 17 00:00:00 2001 From: niztal Date: Mon, 29 Jan 2024 23:30:38 +0200 Subject: [PATCH 359/502] mysql-ssl --- packages/server/src/DataSource.ts | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 83a7fa2c..bd7e8dd2 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -40,7 +40,19 @@ export const init = async (): Promise => { synchronize: false, migrationsRun: false, entities: Object.values(entities), - migrations: mysqlMigrations + migrations: mysqlMigrations, + ...(process.env.DATABASE_SSL_KEY_BASE64 + ? { + ssl: { + rejectUnauthorized: false, + ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') + } + } + : process.env.DATABASE_SSL === 'true' + ? { + ssl: true + } + : {}), }) break case 'postgres': @@ -53,16 +65,16 @@ export const init = async (): Promise => { database: process.env.DATABASE_NAME, ...(process.env.DATABASE_SSL_KEY_BASE64 ? { - ssl: { - rejectUnauthorized: false, - cert: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') - } - } + ssl: { + rejectUnauthorized: false, + cert: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') + } + } : process.env.DATABASE_SSL === 'true' - ? { - ssl: true - } - : {}), + ? { + ssl: true + } + : {}), synchronize: false, migrationsRun: false, entities: Object.values(entities), From 289b04fb120ebbb0e9b61448d073025a618840ca Mon Sep 17 00:00:00 2001 From: niztal Date: Tue, 30 Jan 2024 00:50:27 +0200 Subject: [PATCH 360/502] fix lint --- packages/server/src/DataSource.ts | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index bd7e8dd2..222dae5b 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -43,16 +43,16 @@ export const init = async (): Promise => { migrations: mysqlMigrations, ...(process.env.DATABASE_SSL_KEY_BASE64 ? { - ssl: { - rejectUnauthorized: false, - ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') - } - } + ssl: { + rejectUnauthorized: false, + ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') + } + } : process.env.DATABASE_SSL === 'true' - ? { - ssl: true - } - : {}), + ? { + ssl: true + } + : {}) }) break case 'postgres': @@ -65,16 +65,16 @@ export const init = async (): Promise => { database: process.env.DATABASE_NAME, ...(process.env.DATABASE_SSL_KEY_BASE64 ? { - ssl: { - rejectUnauthorized: false, - cert: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') - } - } + ssl: { + rejectUnauthorized: false, + cert: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') + } + } : process.env.DATABASE_SSL === 'true' - ? { - ssl: true - } - : {}), + ? { + ssl: true + } + : {}), synchronize: false, migrationsRun: false, entities: Object.values(entities), From 517c2f2916994511abe09d14a88286eccb26a6f1 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 30 Jan 2024 15:30:07 +0530 Subject: [PATCH 361/502] Fix error message when audio recording is not available --- .../ui/src/views/chatmessage/ChatMessage.css | 2 +- .../ui/src/views/chatmessage/ChatMessage.js | 71 ++++++++++--------- .../src/views/chatmessage/audio-recording.css | 2 +- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 6742fbac..b7217909 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -151,7 +151,7 @@ .cloud-dialog { width: 100%; height: auto; - max-height: calc(100% - 72px); + max-height: calc(100% - 54px); overflow-y: scroll; display: flex; justify-content: center; diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index bea1acd1..fdfcb90f 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -287,7 +287,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } const onRecordingCancelled = () => { - cancelAudioRecording() + if (!recordingNotSupported) cancelAudioRecording() setIsRecording(false) setRecordingNotSupported(false) } @@ -855,8 +855,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { )} {isRecording ? ( <> - {recordingNotSupported && ( -
    + {recordingNotSupported ? ( +
    To record audio, use modern browsers like Chrome or Firefox that support audio recording. @@ -872,35 +872,42 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    + ) : ( + +
    + + + + 00:00 + {isLoadingRecording && Sending...} +
    +
    + + + + + + +
    +
    )} - -
    - - - - 00:00 - {isLoadingRecording && Sending...} -
    -
    - - - - - - -
    -
    ) : (
    @@ -936,7 +943,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } endAdornment={ <> - {isChatFlowAvailableForUploads && ( + {isChatFlowAvailableForSpeech && ( onMicrophonePressed()} diff --git a/packages/ui/src/views/chatmessage/audio-recording.css b/packages/ui/src/views/chatmessage/audio-recording.css index 4b8e5566..c5d9fac9 100644 --- a/packages/ui/src/views/chatmessage/audio-recording.css +++ b/packages/ui/src/views/chatmessage/audio-recording.css @@ -115,6 +115,7 @@ } .overlay { width: 100%; + height: '54px'; /*targeting Chrome & Safari*/ display: -webkit-flex; /*targeting IE10*/ @@ -123,7 +124,6 @@ justify-content: center; /*horizontal centering*/ align-items: center; - margin-bottom: 12px; } .overlay.hide { display: none; From 1d122084b9f8a9d66c3058aba675b0d72bfcd76f Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 30 Jan 2024 15:54:14 +0530 Subject: [PATCH 362/502] Fix auto scroll on audio messages --- packages/ui/src/views/chatmessage/ChatMessage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index fdfcb90f..1281889a 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -616,8 +616,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })} )} -
    -
    +
    +
    {messages && messages.map((message, index) => { return ( From 4d6881b506cb19695769550383e88818fbbaee73 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 30 Jan 2024 14:12:55 +0000 Subject: [PATCH 363/502] add return source documens to retriever tool --- .../OpenAIFunctionAgent.ts | 12 ++++++-- .../CustomListOutputParser.ts | 14 ++++++---- .../tools/RetrieverTool/RetrieverTool.ts | 28 +++++++++++++++++-- packages/components/src/agents.ts | 25 ++++++++++++++++- .../Conversational Retrieval Agent.json | 10 ++++++- packages/server/src/index.ts | 11 +++++++- 6 files changed, 86 insertions(+), 14 deletions(-) diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index c21c887a..9c25b2a9 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -64,7 +64,7 @@ class OpenAIFunctionAgent_Agents implements INode { return prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) } - async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory as FlowiseMemory const executor = prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) @@ -72,12 +72,20 @@ class OpenAIFunctionAgent_Agents implements INode { const callbacks = await additionalCallbacks(nodeData, options) let res: ChainValues = {} + let sourceDocuments: ICommonObject[] = [] if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) + if (res.sourceDocuments) { + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', flatten(res.sourceDocuments)) + sourceDocuments = res.sourceDocuments + } } else { res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) + if (res.sourceDocuments) { + sourceDocuments = res.sourceDocuments + } } await memory.addChatMessages( @@ -94,7 +102,7 @@ class OpenAIFunctionAgent_Agents implements INode { this.sessionId ) - return res?.output + return sourceDocuments.length ? { text: res?.output, sourceDocuments: flatten(sourceDocuments) } : res?.output } } diff --git a/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts b/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts index d420a88d..1e44acdb 100644 --- a/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts +++ b/packages/components/nodes/outputparsers/CustomListOutputParser/CustomListOutputParser.ts @@ -29,16 +29,17 @@ class CustomListOutputParser implements INode { label: 'Length', name: 'length', type: 'number', - default: 5, step: 1, - description: 'Number of values to return' + description: 'Number of values to return', + optional: true }, { label: 'Separator', name: 'separator', type: 'string', description: 'Separator between values', - default: ',' + default: ',', + optional: true }, { label: 'Autofix', @@ -54,10 +55,11 @@ class CustomListOutputParser implements INode { const separator = nodeData.inputs?.separator as string const lengthStr = nodeData.inputs?.length as string const autoFix = nodeData.inputs?.autofixParser as boolean - let length = 5 - if (lengthStr) length = parseInt(lengthStr, 10) - const parser = new LangchainCustomListOutputParser({ length: length, separator: separator }) + const parser = new LangchainCustomListOutputParser({ + length: lengthStr ? parseInt(lengthStr, 10) : undefined, + separator: separator + }) Object.defineProperty(parser, 'autoFix', { enumerable: true, configurable: true, diff --git a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts index cc74a015..4e4a4af8 100644 --- a/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts +++ b/packages/components/nodes/tools/RetrieverTool/RetrieverTool.ts @@ -1,8 +1,11 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' import { DynamicTool } from 'langchain/tools' -import { createRetrieverTool } from 'langchain/agents/toolkits' +import { DynamicStructuredTool } from '@langchain/core/tools' +import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager' import { BaseRetriever } from 'langchain/schema/retriever' +import { z } from 'zod' +import { SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents' class Retriever_Tools implements INode { label: string @@ -19,7 +22,7 @@ class Retriever_Tools implements INode { constructor() { this.label = 'Retriever Tool' this.name = 'retrieverTool' - this.version = 1.0 + this.version = 2.0 this.type = 'RetrieverTool' this.icon = 'retrievertool.svg' this.category = 'Tools' @@ -44,6 +47,12 @@ class Retriever_Tools implements INode { label: 'Retriever', name: 'retriever', type: 'BaseRetriever' + }, + { + label: 'Return Source Documents', + name: 'returnSourceDocuments', + type: 'boolean', + optional: true } ] } @@ -52,12 +61,25 @@ class Retriever_Tools implements INode { const name = nodeData.inputs?.name as string const description = nodeData.inputs?.description as string const retriever = nodeData.inputs?.retriever as BaseRetriever + const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const tool = createRetrieverTool(retriever, { + const input = { name, description + } + + const func = async ({ input }: { input: string }, runManager?: CallbackManagerForToolRun) => { + const docs = await retriever.getRelevantDocuments(input, runManager?.getChild('retriever')) + const content = docs.map((doc) => doc.pageContent).join('\n\n') + const sourceDocuments = JSON.stringify(docs) + return returnSourceDocuments ? content + SOURCE_DOCUMENTS_PREFIX + sourceDocuments : content + } + + const schema = z.object({ + input: z.string().describe('query to look up in retriever') }) + const tool = new DynamicStructuredTool({ ...input, func, schema }) return tool } } diff --git a/packages/components/src/agents.ts b/packages/components/src/agents.ts index 5e241d50..ab08097b 100644 --- a/packages/components/src/agents.ts +++ b/packages/components/src/agents.ts @@ -1,5 +1,6 @@ +import { flatten } from 'lodash' import { AgentExecutorInput, BaseSingleActionAgent, BaseMultiActionAgent, RunnableAgent, StoppingMethod } from 'langchain/agents' -import { ChainValues, AgentStep, AgentFinish, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' +import { ChainValues, AgentStep, AgentAction, BaseMessage, FunctionMessage, AIMessage } from 'langchain/schema' import { OutputParserException } from 'langchain/schema/output_parser' import { CallbackManager, CallbackManagerForChainRun, Callbacks } from 'langchain/callbacks' import { ToolInputParsingException, Tool } from '@langchain/core/tools' @@ -7,6 +8,11 @@ import { Runnable } from 'langchain/schema/runnable' import { BaseChain, SerializedLLMChain } from 'langchain/chains' import { Serializable } from '@langchain/core/load/serializable' +export const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n' +type AgentFinish = { + returnValues: Record + log: string +} type AgentExecutorOutput = ChainValues interface AgentExecutorIteratorInput { @@ -315,10 +321,12 @@ export class AgentExecutor extends BaseChain { const steps: AgentStep[] = [] let iterations = 0 + let sourceDocuments: Array = [] const getOutput = async (finishStep: AgentFinish): Promise => { const { returnValues } = finishStep const additional = await this.agent.prepareForOutput(returnValues, steps) + if (sourceDocuments.length) additional.sourceDocuments = flatten(sourceDocuments) if (this.returnIntermediateSteps) { return { ...returnValues, intermediateSteps: steps, ...additional } @@ -406,6 +414,17 @@ export class AgentExecutor extends BaseChain { return { action, observation: observation ?? '' } } } + if (observation?.includes(SOURCE_DOCUMENTS_PREFIX)) { + const observationArray = observation.split(SOURCE_DOCUMENTS_PREFIX) + observation = observationArray[0] + const docs = observationArray[1] + try { + const parsedDocs = JSON.parse(docs) + sourceDocuments.push(parsedDocs) + } catch (e) { + console.error('Error parsing source documents from tool') + } + } return { action, observation: observation ?? '' } }) ) @@ -500,6 +519,10 @@ export class AgentExecutor extends BaseChain { chatId: this.chatId, input: this.input }) + if (observation?.includes(SOURCE_DOCUMENTS_PREFIX)) { + const observationArray = observation.split(SOURCE_DOCUMENTS_PREFIX) + observation = observationArray[0] + } } catch (e) { if (e instanceof ToolInputParsingException) { if (this.handleParsingErrors === true) { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 810c2b35..40c689f5 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -217,6 +217,13 @@ "rows": 3, "placeholder": "Searches and returns documents regarding the state-of-the-union.", "id": "retrieverTool_0-input-description-string" + }, + { + "label": "Return Source Documents", + "name": "returnSourceDocuments", + "type": "boolean", + "optional": true, + "id": "retrieverTool_0-input-returnSourceDocuments-boolean" } ], "inputAnchors": [ @@ -230,7 +237,8 @@ "inputs": { "name": "search_website", "description": "Searches and return documents regarding Jane - a culinary institution that offers top quality coffee, pastries, breakfast, lunch, and a variety of baked goods. They have multiple locations, including Jane on Fillmore, Jane on Larkin, Jane the Bakery, Toy Boat By Jane, and Little Jane on Grant. They emphasize healthy eating with a focus on flavor and quality ingredients. They bake everything in-house and work with local suppliers to source ingredients directly from farmers. They also offer catering services and delivery options.", - "retriever": "{{pinecone_0.data.instance}}" + "retriever": "{{pinecone_0.data.instance}}", + "returnSourceDocuments": true }, "outputAnchors": [ { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b0bb06f5..045e40dd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -473,6 +473,8 @@ export class App { const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) let isStreaming = false + let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode') + for (const endingNode of endingNodes) { const endingNodeData = endingNode.data if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) @@ -488,7 +490,8 @@ export class App { isStreaming = isEndingNode ? false : isFlowValidForStream(nodes, endingNodeData) } - const obj = { isStreaming } + // Once custom function ending node exists, flow is always unavailable to stream + const obj = { isStreaming: isEndingNodeExists ? false : isStreaming } return res.json(obj) }) @@ -1677,6 +1680,9 @@ export class App { if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`) const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) + + let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode') + for (const endingNode of endingNodes) { const endingNodeData = endingNode.data if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) @@ -1704,6 +1710,9 @@ export class App { isStreamValid = isFlowValidForStream(nodes, endingNodeData) } + // Once custom function ending node exists, flow is always unavailable to stream + isStreamValid = isEndingNodeExists ? false : isStreamValid + let chatHistory: IMessage[] = incomingInput.history ?? [] // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node From 2b67346ce0e124428452b78e7fe24ddc5e89b341 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 30 Jan 2024 17:42:48 +0000 Subject: [PATCH 364/502] add fix for not recognizing overrideConfig JSON value --- packages/server/src/utils/index.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index aa3e9ef6..2d3f000a 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -611,28 +611,35 @@ export const resolveVariables = ( export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: ICommonObject) => { const types = 'inputs' - const getParamValues = (paramsObj: ICommonObject) => { + const getParamValues = (inputsObj: ICommonObject) => { for (const config in overrideConfig) { // If overrideConfig[key] is object if (overrideConfig[config] && typeof overrideConfig[config] === 'object') { const nodeIds = Object.keys(overrideConfig[config]) if (nodeIds.includes(flowNodeData.id)) { - paramsObj[config] = overrideConfig[config][flowNodeData.id] + inputsObj[config] = overrideConfig[config][flowNodeData.id] + continue + } else if (nodeIds.some((nodeId) => nodeId.includes(flowNodeData.name))) { + /* + * "systemMessagePrompt": { + * "chatPromptTemplate_0": "You are an assistant" <---- continue for loop if current node is chatPromptTemplate_1 + * } + */ continue } } - let paramValue = overrideConfig[config] ?? paramsObj[config] + let paramValue = overrideConfig[config] ?? inputsObj[config] // Check if boolean if (paramValue === 'true') paramValue = true else if (paramValue === 'false') paramValue = false - paramsObj[config] = paramValue + inputsObj[config] = paramValue } } - const paramsObj = flowNodeData[types] ?? {} + const inputsObj = flowNodeData[types] ?? {} - getParamValues(paramsObj) + getParamValues(inputsObj) return flowNodeData } From 214b312fe59207046f854c9d8909ba1fc03de166 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 30 Jan 2024 17:57:11 +0000 Subject: [PATCH 365/502] add input moderation to conversation chain --- .../ConversationChain/ConversationChain.ts | 27 +++++++++++++++++-- .../marketplaces/chatflows/Claude LLM.json | 12 ++++++++- .../chatflows/Simple Conversation Chain.json | 12 ++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 764f4f0e..fd5a4fff 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -7,6 +7,8 @@ import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from import { RunnableSequence } from 'langchain/schema/runnable' import { StringOutputParser } from 'langchain/schema/output_parser' import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' +import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' +import { formatResponse } from '../../outputparsers/OutputParserHelpers' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` const inputKey = 'input' @@ -26,7 +28,7 @@ class ConversationChain_Chains implements INode { constructor(fields?: { sessionId?: string }) { this.label = 'Conversation Chain' this.name = 'conversationChain' - this.version = 2.0 + this.version = 3.0 this.type = 'ConversationChain' this.icon = 'conv.svg' this.category = 'Chains' @@ -60,6 +62,14 @@ class ConversationChain_Chains implements INode { optional: true, list: true },*/ + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'System Message', name: 'systemMessagePrompt', @@ -80,8 +90,21 @@ class ConversationChain_Chains implements INode { return chain } - async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory + const moderations = nodeData.inputs?.inputModeration as Moderation[] + + if (moderations && moderations.length > 0) { + try { + // Use the output of the moderation chain as input for the LLM chain + input = await checkInputs(moderations, input) + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)) + streamResponse(options.socketIO && options.socketIOClientId, e.message, options.socketIO, options.socketIOClientId) + return formatResponse(e.message) + } + } + const chain = prepareChain(nodeData, this.sessionId, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 5d632ff1..7b32de48 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -70,7 +70,7 @@ "data": { "id": "conversationChain_0", "label": "Conversation Chain", - "version": 2, + "version": 3, "name": "conversationChain", "type": "ConversationChain", "baseClasses": ["ConversationChain", "LLMChain", "BaseChain", "Runnable"], @@ -110,9 +110,19 @@ "description": "Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable", "optional": true, "id": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "conversationChain_0-input-inputModeration-Moderation" } ], "inputs": { + "inputModeration": "", "model": "{{chatAnthropic_0.data.instance}}", "memory": "{{bufferMemory_0.data.instance}}", "chatPromptTemplate": "{{chatPromptTemplate_0.data.instance}}", diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index b17e8a65..1ffbee44 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -269,7 +269,7 @@ "data": { "id": "conversationChain_0", "label": "Conversation Chain", - "version": 2, + "version": 3, "name": "conversationChain", "type": "ConversationChain", "baseClasses": ["ConversationChain", "LLMChain", "BaseChain", "Runnable"], @@ -309,9 +309,19 @@ "description": "Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable", "optional": true, "id": "conversationChain_0-input-chatPromptTemplate-ChatPromptTemplate" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "conversationChain_0-input-inputModeration-Moderation" } ], "inputs": { + "inputModeration": "", "model": "{{chatOpenAI_0.data.instance}}", "memory": "{{bufferMemory_0.data.instance}}", "chatPromptTemplate": "", From 82e78d3e4de48f0bcfb5f883b36e9b32cc11545a Mon Sep 17 00:00:00 2001 From: niztal Date: Tue, 30 Jan 2024 22:06:12 +0200 Subject: [PATCH 366/502] refactor uninfy pg and mysql to use the same SSL config function --- packages/server/src/DataSource.ts | 38 ++++++++++++------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 222dae5b..861bd83b 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -41,18 +41,7 @@ export const init = async (): Promise => { migrationsRun: false, entities: Object.values(entities), migrations: mysqlMigrations, - ...(process.env.DATABASE_SSL_KEY_BASE64 - ? { - ssl: { - rejectUnauthorized: false, - ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') - } - } - : process.env.DATABASE_SSL === 'true' - ? { - ssl: true - } - : {}) + ssl: getDatabaseSSLFromEnv(), }) break case 'postgres': @@ -63,18 +52,7 @@ export const init = async (): Promise => { username: process.env.DATABASE_USER, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME, - ...(process.env.DATABASE_SSL_KEY_BASE64 - ? { - ssl: { - rejectUnauthorized: false, - cert: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') - } - } - : process.env.DATABASE_SSL === 'true' - ? { - ssl: true - } - : {}), + ssl: getDatabaseSSLFromEnv(), synchronize: false, migrationsRun: false, entities: Object.values(entities), @@ -101,3 +79,15 @@ export function getDataSource(): DataSource { } return appDataSource } + +const getDatabaseSSLFromEnv = () => { + if (process.env.DATABASE_SSL_KEY_BASE64) { + return { + rejectUnauthorized: false, + ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') + }; + } else if (process.env.DATABASE_SSL === 'true') { + return true; + } + return {}; +} From a382e230f4817d2efce896a04c60656687cf8acd Mon Sep 17 00:00:00 2001 From: niztal Date: Tue, 30 Jan 2024 22:07:56 +0200 Subject: [PATCH 367/502] fix lint issues --- packages/server/src/DataSource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index 861bd83b..f563dce0 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -41,7 +41,7 @@ export const init = async (): Promise => { migrationsRun: false, entities: Object.values(entities), migrations: mysqlMigrations, - ssl: getDatabaseSSLFromEnv(), + ssl: getDatabaseSSLFromEnv() }) break case 'postgres': @@ -85,9 +85,9 @@ const getDatabaseSSLFromEnv = () => { return { rejectUnauthorized: false, ca: Buffer.from(process.env.DATABASE_SSL_KEY_BASE64, 'base64') - }; + } } else if (process.env.DATABASE_SSL === 'true') { - return true; + return true } - return {}; + return {} } From 4107118673136023d3a644dfff75fb85d12260ff Mon Sep 17 00:00:00 2001 From: niztal Date: Tue, 30 Jan 2024 23:44:42 +0200 Subject: [PATCH 368/502] avoid BWC PGSQLMODE returning empty ssl object --- packages/server/src/DataSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/DataSource.ts b/packages/server/src/DataSource.ts index f563dce0..483c070e 100644 --- a/packages/server/src/DataSource.ts +++ b/packages/server/src/DataSource.ts @@ -89,5 +89,5 @@ const getDatabaseSSLFromEnv = () => { } else if (process.env.DATABASE_SSL === 'true') { return true } - return {} + return undefined } From 4604594c559e815a3cfd324b3f525fa78defc8da Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 30 Jan 2024 21:48:08 -0500 Subject: [PATCH 369/502] SpeechToText: Adding SpeechToText at the Chatflow level. --- .../credentials/AssemblyAI.credential.ts | 23 +++++++++ .../nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 36 +------------- .../ChatOpenAI/FlowiseChatOpenAI.ts | 21 +------- .../speechtotext/assemblyai/AssemblyAI.ts | 33 +++++++++++++ .../speechtotext/assemblyai/assemblyai.png | Bin 0 -> 8677 bytes packages/components/src/MultiModalUtils.ts | 43 ---------------- packages/server/src/NodesPool.ts | 2 +- packages/server/src/index.ts | 46 +++++++++++++++--- packages/server/src/utils/index.ts | 34 ++++++++++++- .../ui-component/dialog/SpeechToTextDialog.js | 10 ++-- 10 files changed, 136 insertions(+), 112 deletions(-) create mode 100644 packages/components/credentials/AssemblyAI.credential.ts create mode 100644 packages/components/nodes/speechtotext/assemblyai/AssemblyAI.ts create mode 100644 packages/components/nodes/speechtotext/assemblyai/assemblyai.png diff --git a/packages/components/credentials/AssemblyAI.credential.ts b/packages/components/credentials/AssemblyAI.credential.ts new file mode 100644 index 00000000..019cd7aa --- /dev/null +++ b/packages/components/credentials/AssemblyAI.credential.ts @@ -0,0 +1,23 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AssemblyAIApi implements INodeCredential { + label: string + name: string + version: number + inputs: INodeParams[] + + constructor() { + this.label = 'AssemblyAI API' + this.name = 'assemblyAIApi' + this.version = 1.0 + this.inputs = [ + { + label: 'AssemblyAI Api Key', + name: 'assemblyAIApiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: AssemblyAIApi } diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 9543f1ee..1cb09f3f 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -162,36 +162,6 @@ class ChatOpenAI_ChatModels implements INode { default: false, optional: true }, - { - label: 'Allow Speech to Text', - name: 'allowSpeechToText', - type: 'boolean', - default: false, - optional: true - }, - // TODO: only show when speechToText is true - { - label: 'Speech to Text Method', - description: 'How to turn audio into text', - name: 'speechToTextMode', - type: 'options', - options: [ - { - label: 'Transcriptions', - name: 'transcriptions', - description: - 'Transcribe audio into whatever language the audio is in. Default method when Speech to Text is turned on.' - }, - { - label: 'Translations', - name: 'translations', - description: 'Translate and transcribe the audio into english.' - } - ], - optional: false, - default: 'transcriptions', - additionalParams: true - }, { label: 'Image Resolution', description: 'This parameter controls the resolution in which the model views the image.', @@ -231,8 +201,6 @@ class ChatOpenAI_ChatModels implements INode { const baseOptions = nodeData.inputs?.baseOptions const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean - const allowSpeechToText = nodeData.inputs?.allowSpeechToText as boolean - const speechToTextMode = nodeData.inputs?.speechToTextMode as string const imageResolution = nodeData.inputs?.imageResolution as string const credentialData = await getCredentialData(nodeData.credential ?? '', options) @@ -270,9 +238,7 @@ class ChatOpenAI_ChatModels implements INode { const multiModal = { allowImageUploads: allowImageUploads ?? false, - allowSpeechToText: allowSpeechToText ?? false, - imageResolution, - speechToTextMode + imageResolution } model.multiModal = multiModal return model diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 8af9c4df..1bf4a286 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -7,8 +7,7 @@ import { ChatOpenAICallOptions } from '@langchain/openai/dist/chat_models' import { BaseMessageChunk, BaseMessageLike, HumanMessage, LLMResult } from 'langchain/schema' import { Callbacks } from '@langchain/core/callbacks/manager' import { ICommonObject, INodeData } from '../../../src' -import { addImagesToMessages, checkSpeechToText } from '../../../src/MultiModalUtils' -import { ChatPromptTemplate, PromptTemplate } from 'langchain/prompts' +import { addImagesToMessages } from '../../../src/MultiModalUtils' export class FlowiseChatOpenAI extends ChatOpenAI { multiModal: {} @@ -38,24 +37,6 @@ export class FlowiseChatOpenAI extends ChatOpenAI { private async injectMultiModalMessages(messages: BaseMessageLike[][]) { const nodeData = FlowiseChatOpenAI.chainNodeData const optionsData = FlowiseChatOpenAI.chainNodeOptions - let audioTrans = await checkSpeechToText(nodeData, optionsData) - if (audioTrans) { - if (messages.length > 0) { - const lastMessage = messages[0].pop() as HumanMessage - if (!nodeData.inputs?.prompt) { - lastMessage.content = audioTrans - } else if (nodeData.inputs?.prompt instanceof ChatPromptTemplate) { - lastMessage.content = audioTrans - } else if (nodeData.inputs?.prompt instanceof PromptTemplate) { - let prompt = nodeData.inputs?.prompt as PromptTemplate - let inputVar = prompt.inputVariables[0] - let formattedValues: any = {} - formattedValues[inputVar] = audioTrans - lastMessage.content = await prompt.format(formattedValues) - } - messages[0].push(lastMessage) - } - } const messageContent = addImagesToMessages(nodeData, optionsData) if (messageContent) { if (messages[0].length > 0 && messages[0][messages[0].length - 1] instanceof HumanMessage) { diff --git a/packages/components/nodes/speechtotext/assemblyai/AssemblyAI.ts b/packages/components/nodes/speechtotext/assemblyai/AssemblyAI.ts new file mode 100644 index 00000000..c5db6619 --- /dev/null +++ b/packages/components/nodes/speechtotext/assemblyai/AssemblyAI.ts @@ -0,0 +1,33 @@ +import { INode, INodeParams } from '../../../src/Interface' + +class AssemblyAI_SpeechToText implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs?: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'AssemblyAI' + this.name = 'assemblyAI' + this.version = 1.0 + this.type = 'AssemblyAI' + this.icon = 'assemblyai.png' + this.category = 'SpeechToText' + this.baseClasses = [this.type] + this.inputs = [] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['assemblyAIApi'] + } + } +} + +module.exports = { nodeClass: AssemblyAI_SpeechToText } diff --git a/packages/components/nodes/speechtotext/assemblyai/assemblyai.png b/packages/components/nodes/speechtotext/assemblyai/assemblyai.png new file mode 100644 index 0000000000000000000000000000000000000000..8919cb18b9c68087fae8cdba5722d44629ab49e1 GIT binary patch literal 8677 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 { - const MODEL_NAME = 'whisper-1' - let input = undefined - let model = nodeData.inputs?.model as BaseChatModel - if (model instanceof ChatOpenAI && (model as any).multiModal) { - const multiModalConfig = (model as any).multiModal - if (options?.uploads) { - if (options.uploads.length === 1 && options.uploads[0].mime === 'audio/webm') { - const upload = options.uploads[0] - //special case, text input is empty, but we have an upload (recorded audio) - if (multiModalConfig.allowSpeechToText) { - const openAIClientOptions: ClientOptions = { - apiKey: model.openAIApiKey, - organization: model.organization - } - const openAIClient = new OpenAIClient(openAIClientOptions) - const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) - - // as the image is stored in the server, read the file and convert it to base64 - const audio_file = fs.createReadStream(filePath) - - if (multiModalConfig.speechToTextMode === 'transcriptions') { - const transcription = await openAIClient.audio.transcriptions.create({ - file: audio_file, - model: MODEL_NAME - }) - return transcription.text - } else if (multiModalConfig.speechToTextMode === 'translations') { - const translation = await openAIClient.audio.translations.create({ - file: audio_file, - model: MODEL_NAME - }) - return translation.text - } - } else { - throw new Error('Speech to text is not selected, but found a recorded audio file. Please fix the chain.') - } - } - } - } - return input -} - export const addImagesToMessages = (nodeData: INodeData, options: ICommonObject): MessageContent => { const imageContent: MessageContent = [] let model = nodeData.inputs?.model as BaseChatModel diff --git a/packages/server/src/NodesPool.ts b/packages/server/src/NodesPool.ts index f4681d4a..8b01e63a 100644 --- a/packages/server/src/NodesPool.ts +++ b/packages/server/src/NodesPool.ts @@ -54,7 +54,7 @@ export class NodesPool { } } - const skipCategories = ['Analytic'] + const skipCategories = ['Analytic', 'SpeechToText'] if (!skipCategories.includes(newNodeInstance.category)) { this.componentNodes[newNodeInstance.name] = newNodeInstance } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index e7816311..7558c689 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -46,7 +46,8 @@ import { getSessionChatHistory, getAllConnectedNodes, clearSessionMemory, - findMemoryNode + findMemoryNode, + convertedSpeechToText } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -58,7 +59,7 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, IMessage, INodeOptionsValue, INodeParams, handleEscapeCharacters } from 'flowise-components' +import { ICommonObject, IMessage, INodeOptionsValue, INodeParams, handleEscapeCharacters, IFileUpload } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' import { sanitizeMiddleware } from './utils/XSS' @@ -473,6 +474,17 @@ export class App { const flowObj = JSON.parse(chatflow.flowData) const allowances: IUploadFileSizeAndTypes[] = [] let allowSpeechToText = false + if (chatflow.speechToText) { + const speechToTextProviders = JSON.parse(chatflow.speechToText) + for (const provider in speechToTextProviders) { + const providerObj = speechToTextProviders[provider] + if (providerObj.status) { + allowSpeechToText = true + break + } + } + } + let allowImageUploads = false flowObj.nodes.forEach((node: IReactFlowNode) => { if (uploadAllowedCategoryNodes.indexOf(node.data.category) > -1) { @@ -488,9 +500,6 @@ export class App { }) allowImageUploads = true } - if (param.name === 'allowSpeechToText' && node.data.inputs?.['allowSpeechToText']) { - allowSpeechToText = true - } }) } }) @@ -1602,7 +1611,8 @@ export class App { if (incomingInput.uploads) { // @ts-ignore - ;(incomingInput.uploads as any[]).forEach((upload: any) => { + const uploads = incomingInput.uploads as IFileUpload[] + for (const upload of uploads) { if (upload.type === 'file' || upload.type === 'audio') { const filename = upload.name const dir = path.join(getUserHome(), '.flowise', 'gptvision', chatId) @@ -1618,7 +1628,29 @@ export class App { upload.data = chatId upload.type = 'stored-file' } - }) + + if (upload.mime === 'audio/webm' && incomingInput.uploads?.length === 1) { + //speechToText + let speechToTextConfig: any = {} + if (chatflow.speechToText) { + const speechToTextProviders = JSON.parse(chatflow.speechToText) + for (const provider in speechToTextProviders) { + const providerObj = speechToTextProviders[provider] + if (providerObj.status) { + speechToTextConfig = providerObj + speechToTextConfig['name'] = provider + break + } + } + } + if (speechToTextConfig) { + const speechToTextResult = await convertedSpeechToText(upload.data, speechToTextConfig) + if (speechToTextResult) { + incomingInput.question = speechToTextResult + } + } + } + } } let isStreamValid = false diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index dafe612c..92f4d450 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -593,7 +593,6 @@ export const resolveVariables = ( } const paramsObj = flowNodeData[types] ?? {} - getParamValues(paramsObj) return flowNodeData @@ -1079,3 +1078,36 @@ export const getAllValuesFromJson = (obj: any): any[] => { extractValues(obj) return values } + +export const convertedSpeechToText = async (upload: any, speechToTextConfig: any) => { + // const MODEL_NAME = 'whisper-1' + if (speechToTextConfig) { + //special case, text input is empty, but we have an upload (recorded audio) + // const openAIClientOptions: ClientOptions = { + // apiKey: model.openAIApiKey, + // organization: model.organization + // } + // const openAIClient = new OpenAIClient(openAIClientOptions) + // const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) + // + // // as the image is stored in the server, read the file and convert it to base64 + // const audio_file = fs.createReadStream(filePath) + // + // if (multiModalConfig.speechToTextMode === 'transcriptions') { + // const transcription = await openAIClient.audio.transcriptions.create({ + // file: audio_file, + // model: MODEL_NAME + // }) + // return transcription.text + // } else if (multiModalConfig.speechToTextMode === 'translations') { + // const translation = await openAIClient.audio.translations.create({ + // file: audio_file, + // model: MODEL_NAME + // }) + // return translation.text + // } + } else { + throw new Error('Speech to text is not selected, but found a recorded audio file. Please fix the chain.') + } + return undefined +} diff --git a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js index fa2b7a78..10b6f076 100644 --- a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js +++ b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js @@ -41,8 +41,8 @@ import chatflowsApi from 'api/chatflows' const speechToTextProviders = [ { - label: 'OpenAI Wisper', - name: 'openAIWisper', + label: 'OpenAI Whisper', + name: 'openAIWhisper', icon: openAISVG, url: 'https://platform.openai.com/docs/guides/speech-to-text', inputs: [ @@ -70,7 +70,7 @@ const speechToTextProviders = [ label: 'Connect Credential', name: 'credential', type: 'credential', - credentialNames: ['assemblyAiApi'] + credentialNames: ['assemblyAIApi'] }, { label: 'On/Off', @@ -101,7 +101,7 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { }) if (saveResp.data) { enqueueSnackbar({ - message: 'Analytic Configuration Saved', + message: 'Speech To Text Configuration Saved', options: { key: new Date().getTime() + Math.random(), variant: 'success', @@ -118,7 +118,7 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Analytic Configuration: ${errorData}`, + message: `Failed to save Speech To Text Configuration: ${errorData}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', From e81927ee132985a51aafb96dd67f174b371a7b08 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 31 Jan 2024 07:48:38 -0500 Subject: [PATCH 370/502] SpeechToText: Adding SpeechToText at the Chatflow level. --- .../ChatOpenAI/FlowiseChatOpenAI.ts | 2 +- packages/components/package.json | 1 + packages/components/src/MultiModalUtils.ts | 1 - packages/components/src/index.ts | 1 + packages/components/src/speechToText.ts | 49 +++++++++++++++++++ packages/server/src/index.ts | 19 +++++-- packages/server/src/utils/index.ts | 33 ------------- 7 files changed, 67 insertions(+), 39 deletions(-) create mode 100644 packages/components/src/speechToText.ts diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 1bf4a286..b25ec0c3 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -38,7 +38,7 @@ export class FlowiseChatOpenAI extends ChatOpenAI { const nodeData = FlowiseChatOpenAI.chainNodeData const optionsData = FlowiseChatOpenAI.chainNodeOptions const messageContent = addImagesToMessages(nodeData, optionsData) - if (messageContent) { + if (messageContent?.length) { if (messages[0].length > 0 && messages[0][messages[0].length - 1] instanceof HumanMessage) { const lastMessage = messages[0].pop() if (lastMessage instanceof HumanMessage) { diff --git a/packages/components/package.json b/packages/components/package.json index c90ea5cc..953a6c4c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -40,6 +40,7 @@ "@upstash/redis": "^1.22.1", "@zilliz/milvus2-sdk-node": "^2.2.24", "apify-client": "^2.7.1", + "assemblyai": "^4.2.2", "axios": "1.6.2", "cheerio": "^1.0.0-rc.12", "chromadb": "^1.5.11", diff --git a/packages/components/src/MultiModalUtils.ts b/packages/components/src/MultiModalUtils.ts index 62e3513c..337cc105 100644 --- a/packages/components/src/MultiModalUtils.ts +++ b/packages/components/src/MultiModalUtils.ts @@ -1,6 +1,5 @@ import { ICommonObject, INodeData } from './Interface' import { BaseChatModel } from 'langchain/chat_models/base' -import { type ClientOptions, OpenAIClient } from '@langchain/openai' import { ChatOpenAI } from 'langchain/chat_models/openai' import path from 'path' import { getUserHome } from './utils' diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index ae2e380e..10cd1036 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -6,3 +6,4 @@ dotenv.config({ path: envPath, override: true }) export * from './Interface' export * from './utils' +export * from './speechToText' diff --git a/packages/components/src/speechToText.ts b/packages/components/src/speechToText.ts new file mode 100644 index 00000000..cc40cf21 --- /dev/null +++ b/packages/components/src/speechToText.ts @@ -0,0 +1,49 @@ +import { ICommonObject } from './Interface' +import { getCredentialData, getUserHome } from './utils' +import { type ClientOptions, OpenAIClient } from '@langchain/openai' +import fs from 'fs' +import path from 'path' +import { AssemblyAI } from 'assemblyai' + +export const convertSpeechToText = async (upload: any, speechToTextConfig: any, options: ICommonObject) => { + if (speechToTextConfig) { + const credentialId = speechToTextConfig.credentialId as string + const credentialData = await getCredentialData(credentialId ?? '', options) + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) + + // as the image is stored in the server, read the file and convert it to base64 + const audio_file = fs.createReadStream(filePath) + + if (speechToTextConfig.name === 'openAIWhisper') { + const openAIClientOptions: ClientOptions = { + apiKey: credentialData.openAIApiKey + } + const openAIClient = new OpenAIClient(openAIClientOptions) + + const transcription = await openAIClient.audio.transcriptions.create({ + file: audio_file, + model: 'whisper-1' + }) + if (transcription?.text) { + return transcription.text + } + } else if (speechToTextConfig.name === 'assemblyAiTranscribe') { + const client = new AssemblyAI({ + apiKey: credentialData.assemblyAIApiKey + }) + + const params = { + audio: audio_file, + speaker_labels: false + } + + const transcription = await client.transcripts.transcribe(params) + if (transcription?.text) { + return transcription.text + } + } + } else { + throw new Error('Speech to text is not selected, but found a recorded audio file. Please fix the chain.') + } + return undefined +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7558c689..17689bcb 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -46,8 +46,7 @@ import { getSessionChatHistory, getAllConnectedNodes, clearSessionMemory, - findMemoryNode, - convertedSpeechToText + findMemoryNode } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -59,7 +58,15 @@ import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { ICommonObject, IMessage, INodeOptionsValue, INodeParams, handleEscapeCharacters, IFileUpload } from 'flowise-components' +import { + ICommonObject, + IMessage, + INodeOptionsValue, + INodeParams, + handleEscapeCharacters, + convertSpeechToText, + IFileUpload +} from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' import { sanitizeMiddleware } from './utils/XSS' @@ -1644,7 +1651,11 @@ export class App { } } if (speechToTextConfig) { - const speechToTextResult = await convertedSpeechToText(upload.data, speechToTextConfig) + const options: ICommonObject = { + appDataSource: this.AppDataSource, + databaseEntities: databaseEntities + } + const speechToTextResult = await convertSpeechToText(upload, speechToTextConfig, options) if (speechToTextResult) { incomingInput.question = speechToTextResult } diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 92f4d450..3ed00785 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1078,36 +1078,3 @@ export const getAllValuesFromJson = (obj: any): any[] => { extractValues(obj) return values } - -export const convertedSpeechToText = async (upload: any, speechToTextConfig: any) => { - // const MODEL_NAME = 'whisper-1' - if (speechToTextConfig) { - //special case, text input is empty, but we have an upload (recorded audio) - // const openAIClientOptions: ClientOptions = { - // apiKey: model.openAIApiKey, - // organization: model.organization - // } - // const openAIClient = new OpenAIClient(openAIClientOptions) - // const filePath = path.join(getUserHome(), '.flowise', 'gptvision', upload.data, upload.name) - // - // // as the image is stored in the server, read the file and convert it to base64 - // const audio_file = fs.createReadStream(filePath) - // - // if (multiModalConfig.speechToTextMode === 'transcriptions') { - // const transcription = await openAIClient.audio.transcriptions.create({ - // file: audio_file, - // model: MODEL_NAME - // }) - // return transcription.text - // } else if (multiModalConfig.speechToTextMode === 'translations') { - // const translation = await openAIClient.audio.translations.create({ - // file: audio_file, - // model: MODEL_NAME - // }) - // return translation.text - // } - } else { - throw new Error('Speech to text is not selected, but found a recorded audio file. Please fix the chain.') - } - return undefined -} From 9ab8c36fd0adfaa9678914d219dadef404b709f9 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 31 Jan 2024 13:33:27 +0000 Subject: [PATCH 371/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.5.3?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index a9d57a10..7a7d2e75 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.5.2", + "version": "1.5.3", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 68bc3c708f518693cbc86a5232cdedbc0dd5a783 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 31 Jan 2024 13:34:28 +0000 Subject: [PATCH 372/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.9=20relea?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 32c20aed..68d78c95 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.8", + "version": "1.4.9", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From c98ef7a8b13192809941b48d00d3763003862dc0 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 31 Jan 2024 13:35:00 +0000 Subject: [PATCH 373/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.12=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 31440848..451f7855 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.11", + "version": "1.4.12", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 0955448d..c7ed13ac 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.11", + "version": "1.4.12", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 5c8f48c2f135711956156661d69f3ac51eb3a412 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 31 Jan 2024 19:03:39 -0500 Subject: [PATCH 374/502] Multimodal: Image Uploads. --- .../ConversationalAgent/ConversationalAgent.ts | 3 +++ .../nodes/agents/MRKLAgentChat/MRKLAgentChat.ts | 2 ++ packages/server/src/index.ts | 12 +++++++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index 7f857b1c..a8c709a2 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -9,6 +9,7 @@ import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } import { AgentExecutor } from '../../../src/agents' import { ChatConversationalAgent } from 'langchain/agents' import { renderTemplate } from '@langchain/core/prompts' +import { injectChainNodeData } from '../../../src/MultiModalUtils' const DEFAULT_PREFIX = `Assistant is a large language model trained by OpenAI. @@ -84,6 +85,8 @@ class ConversationalAgent_Agents implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory as FlowiseMemory + injectChainNodeData(nodeData, options) + const executor = await prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) const loggerHandler = new ConsoleCallbackHandler(options.logger) diff --git a/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts b/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts index 19835e36..6ea8d67e 100644 --- a/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts +++ b/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts @@ -5,6 +5,7 @@ import { Tool } from 'langchain/tools' import { BaseLanguageModel } from 'langchain/base_language' import { flatten } from 'lodash' import { additionalCallbacks } from '../../../src/handler' +import { injectChainNodeData } from '../../../src/MultiModalUtils' class MRKLAgentChat_Agents implements INode { label: string @@ -54,6 +55,7 @@ class MRKLAgentChat_Agents implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const executor = nodeData.instance as AgentExecutor + injectChainNodeData(nodeData, options) const callbacks = await additionalCallbacks(nodeData, options) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 17689bcb..361c00cf 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -475,7 +475,8 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const uploadAllowedCategoryNodes = ['Chat Models'] + const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent'] + const uploadProcessingNodes = ['chatOpenAI'] try { const flowObj = JSON.parse(chatflow.flowData) @@ -494,26 +495,27 @@ export class App { let allowImageUploads = false flowObj.nodes.forEach((node: IReactFlowNode) => { - if (uploadAllowedCategoryNodes.indexOf(node.data.category) > -1) { + if (uploadProcessingNodes.indexOf(node.data.name) > -1) { logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) // there could be multiple components allowing uploads, so we check if it's already added // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties node.data.inputParams.map((param: INodeParams) => { - if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads'] && !allowImageUploads) { + if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) { allowances.push({ fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'), maxUploadSize: 5 }) - allowImageUploads = true } }) + } else if (uploadAllowedNodes.indexOf(node.data.name) > -1 && !allowImageUploads) { + allowImageUploads = true } }) return res.json({ allowSpeechToText: allowSpeechToText, - isUploadAllowed: allowances.length > 0, + isUploadAllowed: allowImageUploads, uploadFileSizeAndTypes: allowances }) } catch (e) { From aa5d1417a1bc9163136f6ae5ac806ad29787907e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 31 Jan 2024 19:16:58 -0500 Subject: [PATCH 375/502] Multimodal: deleting uploads on delete of all chatmessages --- packages/server/src/index.ts | 7 ++++++- packages/server/src/utils/index.ts | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 361c00cf..b7ddac51 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -46,7 +46,8 @@ import { getSessionChatHistory, getAllConnectedNodes, clearSessionMemory, - findMemoryNode + findMemoryNode, + deleteFolderRecursive } from './utils' import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' import { getDataSource } from './DataSource' @@ -618,6 +619,10 @@ export class App { if (sessionId) deleteOptions.sessionId = sessionId if (chatType) deleteOptions.chatType = chatType + /* Delete all multimodal uploads corresponding to this chatflow */ + const directory = path.join(getUserHome(), '.flowise', 'gptvision', chatflowid) + deleteFolderRecursive(directory) + const results = await this.AppDataSource.getRepository(ChatMessage).delete(deleteOptions) return res.json(results) }) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 3ed00785..eb4e1936 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1078,3 +1078,25 @@ export const getAllValuesFromJson = (obj: any): any[] => { extractValues(obj) return values } + +export const deleteFolderRecursive = (directory: string) => { + fs.readdir(directory, (error, files) => { + if (error) throw new Error('Could not read directory') + + files.forEach((file) => { + const file_path = path.join(directory, file) + + fs.stat(file_path, (error, stat) => { + if (error) throw new Error('File do not exist') + + if (!stat.isDirectory()) { + fs.unlink(file_path, (error) => { + if (error) throw new Error('Could not delete file') + }) + } else { + deleteFolderRecursive(file_path) + } + }) + }) + }) +} \ No newline at end of file From eab8c19f8c27adf62b1f20477c3518e55d5de046 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 31 Jan 2024 20:03:54 -0500 Subject: [PATCH 376/502] Multimodal: deleting uploads on delete of all chatmessages or chatflow --- packages/server/src/index.ts | 23 ++++++++++++++++----- packages/server/src/utils/index.ts | 32 ++++++++++++++++-------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b7ddac51..e655406d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -431,6 +431,15 @@ export class App { // Delete chatflow via id this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) + + try { + /* Delete all multimodal uploads corresponding to this chatflow */ + const directory = path.join(getUserHome(), '.flowise', 'gptvision', req.params.id) + deleteFolderRecursive(directory) + } catch (e) { + logger.error(`[server]: Error deleting multimodal uploads: ${e}`) + } + return res.json(results) }) @@ -619,9 +628,13 @@ export class App { if (sessionId) deleteOptions.sessionId = sessionId if (chatType) deleteOptions.chatType = chatType - /* Delete all multimodal uploads corresponding to this chatflow */ - const directory = path.join(getUserHome(), '.flowise', 'gptvision', chatflowid) - deleteFolderRecursive(directory) + try { + /* Delete all multimodal uploads corresponding to this chatflow */ + const directory = path.join(getUserHome(), '.flowise', 'gptvision', chatflowid) + deleteFolderRecursive(directory) + } catch (e) { + logger.error(`[server]: Error deleting multimodal uploads: ${e}`) + } const results = await this.AppDataSource.getRepository(ChatMessage).delete(deleteOptions) return res.json(results) @@ -1629,7 +1642,7 @@ export class App { for (const upload of uploads) { if (upload.type === 'file' || upload.type === 'audio') { const filename = upload.name - const dir = path.join(getUserHome(), '.flowise', 'gptvision', chatId) + const dir = path.join(getUserHome(), '.flowise', 'gptvision', chatflowid) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } @@ -1639,7 +1652,7 @@ export class App { //writes data to a file, replacing the file if it already exists. fs.writeFileSync(filePath, bf) // don't need to store the file contents in chatmessage, just the filename and chatId - upload.data = chatId + upload.data = chatflowid upload.type = 'stored-file' } diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index eb4e1936..2b6a22bd 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1080,23 +1080,25 @@ export const getAllValuesFromJson = (obj: any): any[] => { } export const deleteFolderRecursive = (directory: string) => { - fs.readdir(directory, (error, files) => { - if (error) throw new Error('Could not read directory') + if (fs.existsSync(directory)) { + fs.readdir(directory, (error, files) => { + if (error) throw new Error('Could not read directory') - files.forEach((file) => { - const file_path = path.join(directory, file) + files.forEach((file) => { + const file_path = path.join(directory, file) - fs.stat(file_path, (error, stat) => { - if (error) throw new Error('File do not exist') + fs.stat(file_path, (error, stat) => { + if (error) throw new Error('File do not exist') - if (!stat.isDirectory()) { - fs.unlink(file_path, (error) => { - if (error) throw new Error('Could not delete file') - }) - } else { - deleteFolderRecursive(file_path) - } + if (!stat.isDirectory()) { + fs.unlink(file_path, (error) => { + if (error) throw new Error('Could not delete file') + }) + } else { + deleteFolderRecursive(file_path) + } + }) }) }) - }) -} \ No newline at end of file + } +} From 8d62adec2fbb8449ce650b0489e5ba00c7101326 Mon Sep 17 00:00:00 2001 From: melon Date: Thu, 1 Feb 2024 14:19:39 +0800 Subject: [PATCH 377/502] Refactor session ID assignment in App class --- packages/server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 045e40dd..d58660f0 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1440,7 +1440,7 @@ export class App { chatType, chatId, memoryType: memoryType ?? (chatId ? IsNull() : undefined), - sessionId: sessionId ?? (chatId ? IsNull() : undefined), + sessionId: sessionId ?? undefined, createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined }, order: { From 5c6b5b233c28784161048c3ddc5c32368f2a330a Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 1 Feb 2024 13:14:02 +0000 Subject: [PATCH 378/502] fix top K --- .../nodes/retrievers/CohereRerankRetriever/CohereRerank.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts index f74b8365..f12d1026 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerank.ts @@ -47,7 +47,7 @@ export class CohereRerank extends BaseDocumentCompressor { doc.metadata.relevance_score = result.relevance_score finalResults.push(doc) }) - return finalResults + return finalResults.splice(0, this.k) } catch (error) { return documents } From 7881f295ab0391692abc5255d8dbb70c2f50c9ea Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 1 Feb 2024 19:18:14 +0000 Subject: [PATCH 379/502] update vec 2 doc node --- .../VectorStoreToDocument/VectorStoreToDocument.ts | 2 +- .../chatflows/Prompt Chaining with VectorStore.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts b/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts index becd0ac6..c087e000 100644 --- a/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts +++ b/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts @@ -51,7 +51,7 @@ class VectorStoreToDocument_DocumentLoaders implements INode { { label: 'Document', name: 'document', - baseClasses: this.baseClasses + baseClasses: [...this.baseClasses, 'json'] }, { label: 'Text', diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json index 46e1257d..c2060e79 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json @@ -190,10 +190,10 @@ "type": "options", "options": [ { - "id": "vectorStoreToDocument_0-output-document-Document", + "id": "vectorStoreToDocument_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "vectorStoreToDocument_0-output-text-string|json", From 1522acbf5aa6cf53c1721ac66c21343c4aa718a3 Mon Sep 17 00:00:00 2001 From: Rafael Reis Date: Thu, 1 Feb 2024 16:19:30 -0300 Subject: [PATCH 380/502] Add gpt-3.5-turbo-0125 to ChatOpenAI.ts --- packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 2cb701c1..a0d0bd6e 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -79,6 +79,10 @@ class ChatOpenAI_ChatModels implements INode { label: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo' }, + { + label: 'gpt-3.5-turbo-0125', + name: 'gpt-3.5-turbo-0125' + }, { label: 'gpt-3.5-turbo-1106', name: 'gpt-3.5-turbo-1106' From a219efc91331b083c8a20090881cee3b2466336c Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 2 Feb 2024 13:37:33 +0000 Subject: [PATCH 381/502] Rename MultiModalUtils.ts to multiModalUtils.ts --- .../components/src/{MultiModalUtils.ts => multiModalUtils.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/components/src/{MultiModalUtils.ts => multiModalUtils.ts} (100%) diff --git a/packages/components/src/MultiModalUtils.ts b/packages/components/src/multiModalUtils.ts similarity index 100% rename from packages/components/src/MultiModalUtils.ts rename to packages/components/src/multiModalUtils.ts From c5bd4d41682348ac2b45871a59be3c8a52eb99f8 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 2 Feb 2024 13:52:49 +0000 Subject: [PATCH 382/502] address configuration fix and add BLOB_STORAGE_PATH env variable --- CONTRIBUTING-ZH.md | 1 + CONTRIBUTING.md | 1 + docker/.env.example | 1 + docker/README.md | 1 + docker/docker-compose.yml | 1 + .../nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts | 2 +- packages/server/.env.example | 1 + packages/server/src/commands/start.ts | 4 ++++ 8 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING-ZH.md b/CONTRIBUTING-ZH.md index 7e35d194..b96cb86f 100644 --- a/CONTRIBUTING-ZH.md +++ b/CONTRIBUTING-ZH.md @@ -124,6 +124,7 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | FLOWISE_USERNAME | 登录用户名 | 字符串 | | | FLOWISE_PASSWORD | 登录密码 | 字符串 | | | DEBUG | 打印组件的日志 | 布尔值 | | +| BLOB_STORAGE_PATH | 存储位置 | 字符串 | `your-home-dir/.flowise/storage` | | LOG_PATH | 存储日志文件的位置 | 字符串 | `your-path/Flowise/logs` | | LOG_LEVEL | 日志的不同级别 | 枚举字符串: `error`, `info`, `verbose`, `debug` | `info` | | APIKEY_PATH | 存储 API 密钥的位置 | 字符串 | `your-path/Flowise/packages/server` | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25a27e84..4d90a695 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,6 +128,7 @@ Flowise support different environment variables to configure your instance. You | FLOWISE_USERNAME | Username to login | String | | | FLOWISE_PASSWORD | Password to login | String | | | DEBUG | Print logs from components | Boolean | | +| BLOB_STORAGE_PATH | Location where uploaded files are stored | String | `your-home-dir/.flowise/storage` | | LOG_PATH | Location where log files are stored | String | `your-path/Flowise/logs` | | LOG_LEVEL | Different levels of logs | Enum String: `error`, `info`, `verbose`, `debug` | `info` | | APIKEY_PATH | Location where api keys are saved | String | `your-path/Flowise/packages/server` | diff --git a/docker/.env.example b/docker/.env.example index a4beaf8a..18415673 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -3,6 +3,7 @@ DATABASE_PATH=/root/.flowise APIKEY_PATH=/root/.flowise SECRETKEY_PATH=/root/.flowise LOG_PATH=/root/.flowise/logs +BLOB_STORAGE_PATH=/root/.flowise/storage # CORS_ORIGINS="*" # IFRAME_ORIGINS="*" diff --git a/docker/README.md b/docker/README.md index 11b29cf3..49ce57c0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -31,5 +31,6 @@ If you like to persist your data (flows, logs, apikeys, credentials), set these - APIKEY_PATH=/root/.flowise - LOG_PATH=/root/.flowise/logs - SECRETKEY_PATH=/root/.flowise +- BLOB_STORAGE_PATH=/root/.flowise/storage Flowise also support different environment variables to configure your instance. Read [more](https://docs.flowiseai.com/environment-variables) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 71bcfcfb..cb45f37c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,6 +25,7 @@ services: - FLOWISE_SECRETKEY_OVERWRITE=${FLOWISE_SECRETKEY_OVERWRITE} - LOG_LEVEL=${LOG_LEVEL} - LOG_PATH=${LOG_PATH} + - BLOB_STORAGE_PATH=${BLOB_STORAGE_PATH} - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY} ports: - '${PORT}:${PORT}' diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 562cc9d9..de5739f5 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -22,7 +22,7 @@ export class ChatOpenAI extends LangchainChatOpenAI { /** @deprecated */ configuration?: ClientOptions & LegacyOpenAIInput ) { - super(fields) + super(fields, configuration) this.multiModalOption = fields?.multiModalOption this.configuredModel = fields?.modelName ?? 'gpt-3.5-turbo' this.configuredMaxToken = fields?.maxTokens diff --git a/packages/server/.env.example b/packages/server/.env.example index ebc59cf3..e2eb833f 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -5,6 +5,7 @@ PORT=3000 # APIKEY_PATH=/your_api_key_path/.flowise # SECRETKEY_PATH=/your_api_key_path/.flowise # LOG_PATH=/your_log_path/.flowise/logs +# BLOB_STORAGE_PATH=/your_database_path/.flowise/storage # NUMBER_OF_PROXIES= 1 diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index dfb20766..88713804 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -22,6 +22,7 @@ export default class Start extends Command { CORS_ORIGINS: Flags.string(), IFRAME_ORIGINS: Flags.string(), DEBUG: Flags.string(), + BLOB_STORAGE_PATH: Flags.string(), APIKEY_PATH: Flags.string(), SECRETKEY_PATH: Flags.string(), FLOWISE_SECRETKEY_OVERWRITE: Flags.string(), @@ -91,6 +92,9 @@ export default class Start extends Command { if (flags.FLOWISE_PASSWORD) process.env.FLOWISE_PASSWORD = flags.FLOWISE_PASSWORD if (flags.APIKEY_PATH) process.env.APIKEY_PATH = flags.APIKEY_PATH + // Storage + if (flags.BLOB_STORAGE_PATH) process.env.BLOB_STORAGE_PATH = flags.BLOB_STORAGE_PATH + // Credentials if (flags.SECRETKEY_PATH) process.env.SECRETKEY_PATH = flags.SECRETKEY_PATH if (flags.FLOWISE_SECRETKEY_OVERWRITE) process.env.FLOWISE_SECRETKEY_OVERWRITE = flags.FLOWISE_SECRETKEY_OVERWRITE From a4131dc21b1ed2f5a79ffd31f4e48fc53f2d6eae Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 2 Feb 2024 15:44:05 +0000 Subject: [PATCH 383/502] add fixes for chaining --- packages/server/src/index.ts | 5 ++++- packages/server/src/utils/index.ts | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 02d6bf43..998801eb 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1954,7 +1954,10 @@ export class App { chatflowid, this.AppDataSource, incomingInput?.overrideConfig, - this.cachePool + this.cachePool, + false, + undefined, + incomingInput.uploads ) const nodeToExecute = diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 332819b8..31a5a5f4 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -27,7 +27,8 @@ import { ICommonObject, IDatabaseEntity, IMessage, - FlowiseMemory + FlowiseMemory, + IFileUpload } from 'flowise-components' import { randomBytes } from 'crypto' import { AES, enc } from 'crypto-js' @@ -279,7 +280,8 @@ export const buildLangchain = async ( overrideConfig?: ICommonObject, cachePool?: CachePool, isUpsert?: boolean, - stopNodeId?: string + stopNodeId?: string, + uploads?: IFileUpload[] ) => { const flowNodes = cloneDeep(reactFlowNodes) @@ -325,7 +327,8 @@ export const buildLangchain = async ( appDataSource, databaseEntities, cachePool, - dynamicVariables + dynamicVariables, + uploads }) logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) break @@ -340,7 +343,8 @@ export const buildLangchain = async ( appDataSource, databaseEntities, cachePool, - dynamicVariables + dynamicVariables, + uploads }) // Save dynamic variables From 041bfea94077d808de18a95bfb99bb161c69b573 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 2 Feb 2024 20:07:50 +0000 Subject: [PATCH 384/502] add more params --- packages/components/src/speechToText.ts | 5 ++- .../ui-component/dialog/SpeechToTextDialog.js | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/components/src/speechToText.ts b/packages/components/src/speechToText.ts index 6bb11e95..8524b525 100644 --- a/packages/components/src/speechToText.ts +++ b/packages/components/src/speechToText.ts @@ -21,7 +21,10 @@ export const convertSpeechToText = async (upload: IFileUpload, speechToTextConfi const transcription = await openAIClient.audio.transcriptions.create({ file: audio_file, - model: 'whisper-1' + model: 'whisper-1', + language: speechToTextConfig?.language, + temperature: speechToTextConfig?.temperature ? parseFloat(speechToTextConfig.temperature) : undefined, + prompt: speechToTextConfig?.prompt }) if (transcription?.text) { return transcription.text diff --git a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js index 10b6f076..9fc11a72 100644 --- a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js +++ b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js @@ -29,6 +29,7 @@ import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' import { SwitchInput } from 'ui-component/switch/Switch' import { Input } from 'ui-component/input/Input' import { StyledButton } from 'ui-component/button/StyledButton' +import { Dropdown } from 'ui-component/dropdown/Dropdown' import openAISVG from 'assets/images/openai.svg' import assemblyAIPng from 'assets/images/assemblyai.png' @@ -52,6 +53,31 @@ const speechToTextProviders = [ type: 'credential', credentialNames: ['openAIApi'] }, + { + label: 'Language', + name: 'language', + type: 'string', + description: + 'The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.', + placeholder: 'en', + optional: true + }, + { + label: 'Prompt', + name: 'prompt', + type: 'string', + rows: 4, + description: `An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.`, + optional: true + }, + { + label: 'Temperature', + name: 'temperature', + type: 'number', + step: 0.1, + description: `The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.`, + optional: true + }, { label: 'On/Off', name: 'status', @@ -306,6 +332,19 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { } /> )} + + {providerExpanded[provider.name] && inputParam.type === 'options' && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + speechToText[provider.name] + ? speechToText[provider.name][inputParam.name] + : inputParam.default ?? 'choose an option' + } + /> + )} ))} From c504f91752a401ccefddcc7b833c0ca6fd3a258a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 2 Feb 2024 17:45:12 -0500 Subject: [PATCH 385/502] Multimodal: guard to check for nodeData before image message insertion. --- .../nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index de5739f5..9033b27f 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -33,7 +33,9 @@ export class ChatOpenAI extends LangchainChatOpenAI { } async generate(messages: BaseMessageLike[][], options?: string[] | ChatOpenAICallOptions, callbacks?: Callbacks): Promise { - await this.injectMultiModalMessages(messages) + if (ChatOpenAI.chainNodeData && ChatOpenAI.chainNodeOptions) { + await this.injectMultiModalMessages(messages) + } return super.generate(messages, options, callbacks) } From 6013743705cbc98ef4ce819b013f9314add66a16 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 3 Feb 2024 02:14:43 +0000 Subject: [PATCH 386/502] add subquestion query engine marketplace template --- .../chatflows/SubQuestion Query Engine.json | 1201 +++++++++++++++++ 1 file changed, 1201 insertions(+) create mode 100644 packages/server/marketplaces/chatflows/SubQuestion Query Engine.json diff --git a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json new file mode 100644 index 00000000..f14607da --- /dev/null +++ b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json @@ -0,0 +1,1201 @@ +{ + "description": "Breaks down query into sub questions for each relevant data source, then combine into final response", + "badge": "NEW", + "nodes": [ + { + "width": 300, + "height": 749, + "id": "compactrefineLlamaIndex_0", + "position": { + "x": -1214.7329938486841, + "y": 56.52482754447425 + }, + "type": "customNode", + "data": { + "id": "compactrefineLlamaIndex_0", + "label": "Compact and Refine", + "version": 1, + "name": "compactrefineLlamaIndex", + "type": "CompactRefine", + "baseClasses": ["CompactRefine", "ResponseSynthesizer"], + "tags": ["LlamaIndex"], + "category": "Response Synthesizer", + "description": "CompactRefine is a slight variation of Refine that first compacts the text chunks into the smallest possible number of chunks.", + "inputParams": [ + { + "label": "Refine Prompt", + "name": "refinePrompt", + "type": "string", + "rows": 4, + "default": "The original query is as follows: {query}\nWe have provided an existing answer: {existingAnswer}\nWe have the opportunity to refine the existing answer (only if needed) with some more context below.\n------------\n{context}\n------------\nGiven the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.\nRefined Answer:", + "warning": "Prompt can contains no variables, or up to 3 variables. Variables must be {existingAnswer}, {context} and {query}", + "optional": true, + "id": "compactrefineLlamaIndex_0-input-refinePrompt-string" + }, + { + "label": "Text QA Prompt", + "name": "textQAPrompt", + "type": "string", + "rows": 4, + "default": "Context information is below.\n---------------------\n{context}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query}\nAnswer:", + "warning": "Prompt can contains no variables, or up to 2 variables. Variables must be {context} and {query}", + "optional": true, + "id": "compactrefineLlamaIndex_0-input-textQAPrompt-string" + } + ], + "inputAnchors": [], + "inputs": { + "refinePrompt": "A user has selected a set of SEC filing documents and has asked a question about them.\nThe SEC documents have the following titles:\n- Apple Inc (APPL) FORM 10K 2022\n- Tesla Inc (TSLA) FORM 10K 2022\nThe original query is as follows: {query}\nWe have provided an existing answer: {existingAnswer}\nWe have the opportunity to refine the existing answer (only if needed) with some more context below.\n------------\n{context}\n------------\nGiven the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.\nRefined Answer:", + "textQAPrompt": "A user has selected a set of SEC filing documents and has asked a question about them.\nThe SEC documents have the following titles:\n- Apple Inc (APPL) FORM 10K 2022\n- Tesla Inc (TSLA) FORM 10K 2022\nContext information is below.\n---------------------\n{context}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query}\nAnswer:" + }, + "outputAnchors": [ + { + "id": "compactrefineLlamaIndex_0-output-compactrefineLlamaIndex-CompactRefine|ResponseSynthesizer", + "name": "compactrefineLlamaIndex", + "label": "CompactRefine", + "type": "CompactRefine | ResponseSynthesizer" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -1214.7329938486841, + "y": 56.52482754447425 + }, + "dragging": false + }, + { + "width": 300, + "height": 611, + "id": "pineconeLlamaIndex_0", + "position": { + "x": 37.23548045607484, + "y": -119.7364648743818 + }, + "type": "customNode", + "data": { + "id": "pineconeLlamaIndex_0", + "label": "Pinecone", + "version": 1, + "name": "pineconeLlamaIndex", + "type": "Pinecone", + "baseClasses": ["Pinecone", "VectorIndexRetriever"], + "tags": ["LlamaIndex"], + "category": "Vector Stores", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["pineconeApi"], + "id": "pineconeLlamaIndex_0-input-credential-credential" + }, + { + "label": "Pinecone Index", + "name": "pineconeIndex", + "type": "string", + "id": "pineconeLlamaIndex_0-input-pineconeIndex-string" + }, + { + "label": "Pinecone Namespace", + "name": "pineconeNamespace", + "type": "string", + "placeholder": "my-first-namespace", + "additionalParams": true, + "optional": true, + "id": "pineconeLlamaIndex_0-input-pineconeNamespace-string" + }, + { + "label": "Pinecone Metadata Filter", + "name": "pineconeMetadataFilter", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "pineconeLlamaIndex_0-input-pineconeMetadataFilter-json" + }, + { + "label": "Top K", + "name": "topK", + "description": "Number of top results to fetch. Default to 4", + "placeholder": "4", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pineconeLlamaIndex_0-input-topK-number" + } + ], + "inputAnchors": [ + { + "label": "Document", + "name": "document", + "type": "Document", + "list": true, + "optional": true, + "id": "pineconeLlamaIndex_0-input-document-Document" + }, + { + "label": "Chat Model", + "name": "model", + "type": "BaseChatModel_LlamaIndex", + "id": "pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex" + }, + { + "label": "Embeddings", + "name": "embeddings", + "type": "BaseEmbedding_LlamaIndex", + "id": "pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex" + } + ], + "inputs": { + "document": [], + "model": "{{chatOpenAI_LlamaIndex_0.data.instance}}", + "embeddings": "{{openAIEmbedding_LlamaIndex_0.data.instance}}", + "pineconeIndex": "flowiseindex", + "pineconeNamespace": "pinecone-form10k", + "pineconeMetadataFilter": "{\"source\":\"tesla\"}", + "topK": "" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "pineconeLlamaIndex_0-output-retriever-Pinecone|VectorIndexRetriever", + "name": "retriever", + "label": "Pinecone Retriever", + "type": "Pinecone | VectorIndexRetriever" + }, + { + "id": "pineconeLlamaIndex_0-output-vectorStore-Pinecone|VectorStoreIndex", + "name": "vectorStore", + "label": "Pinecone Vector Store Index", + "type": "Pinecone | VectorStoreIndex" + } + ], + "default": "retriever" + } + ], + "outputs": { + "output": "vectorStore" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 37.23548045607484, + "y": -119.7364648743818 + }, + "dragging": false + }, + { + "width": 300, + "height": 529, + "id": "chatOpenAI_LlamaIndex_0", + "position": { + "x": -455.232655468177, + "y": -711.0080711676725 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_LlamaIndex_0", + "label": "ChatOpenAI", + "version": 1, + "name": "chatOpenAI_LlamaIndex", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel_LlamaIndex", "BaseLLM"], + "tags": ["LlamaIndex"], + "category": "Chat Models", + "description": "Wrapper around OpenAI Chat LLM specific for LlamaIndex", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_LlamaIndex_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_LlamaIndex_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_LlamaIndex_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_LlamaIndex_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_LlamaIndex_0-input-topP-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_LlamaIndex_0-input-timeout-number" + } + ], + "inputAnchors": [], + "inputs": { + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0", + "maxTokens": "", + "topP": "", + "timeout": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", + "name": "chatOpenAI_LlamaIndex", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel_LlamaIndex | BaseLLM" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -455.232655468177, + "y": -711.0080711676725 + }, + "dragging": false + }, + { + "width": 300, + "height": 334, + "id": "openAIEmbedding_LlamaIndex_0", + "position": { + "x": -451.0082548287243, + "y": -127.15143353229783 + }, + "type": "customNode", + "data": { + "id": "openAIEmbedding_LlamaIndex_0", + "label": "OpenAI Embedding", + "version": 1, + "name": "openAIEmbedding_LlamaIndex", + "type": "OpenAIEmbedding", + "baseClasses": ["OpenAIEmbedding", "BaseEmbedding_LlamaIndex", "BaseEmbedding"], + "tags": ["LlamaIndex"], + "category": "Embeddings", + "description": "OpenAI Embedding specific for LlamaIndex", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "openAIEmbedding_LlamaIndex_0-input-credential-credential" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAIEmbedding_LlamaIndex_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "openAIEmbedding_LlamaIndex_0-input-basepath-string" + } + ], + "inputAnchors": [], + "inputs": { + "timeout": "", + "basepath": "" + }, + "outputAnchors": [ + { + "id": "openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", + "name": "openAIEmbedding_LlamaIndex", + "label": "OpenAIEmbedding", + "type": "OpenAIEmbedding | BaseEmbedding_LlamaIndex | BaseEmbedding" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": -451.0082548287243, + "y": -127.15143353229783 + } + }, + { + "width": 300, + "height": 511, + "id": "queryEngineToolLlamaIndex_0", + "position": { + "x": 460.37559236135905, + "y": -565.6224030941121 + }, + "type": "customNode", + "data": { + "id": "queryEngineToolLlamaIndex_0", + "label": "QueryEngine Tool", + "version": 1, + "name": "queryEngineToolLlamaIndex", + "type": "QueryEngineTool", + "baseClasses": ["QueryEngineTool"], + "tags": ["LlamaIndex"], + "category": "Tools", + "description": "Execute actions using ChatGPT Plugin Url", + "inputParams": [ + { + "label": "Tool Name", + "name": "toolName", + "type": "string", + "description": "Tool name must be small capital letter with underscore. Ex: my_tool", + "id": "queryEngineToolLlamaIndex_0-input-toolName-string" + }, + { + "label": "Tool Description", + "name": "toolDesc", + "type": "string", + "rows": 4, + "id": "queryEngineToolLlamaIndex_0-input-toolDesc-string" + } + ], + "inputAnchors": [ + { + "label": "Vector Store Index", + "name": "vectorStoreIndex", + "type": "VectorStoreIndex", + "id": "queryEngineToolLlamaIndex_0-input-vectorStoreIndex-VectorStoreIndex" + } + ], + "inputs": { + "vectorStoreIndex": "{{pineconeLlamaIndex_1.data.instance}}", + "toolName": "apple_tool", + "toolDesc": "A SEC Form 10K filing describing the financials of Apple Inc (APPL) for the 2022 time period." + }, + "outputAnchors": [ + { + "id": "queryEngineToolLlamaIndex_0-output-queryEngineToolLlamaIndex-QueryEngineTool", + "name": "queryEngineToolLlamaIndex", + "label": "QueryEngineTool", + "type": "QueryEngineTool" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 460.37559236135905, + "y": -565.6224030941121 + }, + "dragging": false + }, + { + "width": 300, + "height": 611, + "id": "pineconeLlamaIndex_1", + "position": { + "x": 42.17855025460784, + "y": -839.8824444107056 + }, + "type": "customNode", + "data": { + "id": "pineconeLlamaIndex_1", + "label": "Pinecone", + "version": 1, + "name": "pineconeLlamaIndex", + "type": "Pinecone", + "baseClasses": ["Pinecone", "VectorIndexRetriever"], + "tags": ["LlamaIndex"], + "category": "Vector Stores", + "description": "Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["pineconeApi"], + "id": "pineconeLlamaIndex_1-input-credential-credential" + }, + { + "label": "Pinecone Index", + "name": "pineconeIndex", + "type": "string", + "id": "pineconeLlamaIndex_1-input-pineconeIndex-string" + }, + { + "label": "Pinecone Namespace", + "name": "pineconeNamespace", + "type": "string", + "placeholder": "my-first-namespace", + "additionalParams": true, + "optional": true, + "id": "pineconeLlamaIndex_1-input-pineconeNamespace-string" + }, + { + "label": "Pinecone Metadata Filter", + "name": "pineconeMetadataFilter", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "pineconeLlamaIndex_1-input-pineconeMetadataFilter-json" + }, + { + "label": "Top K", + "name": "topK", + "description": "Number of top results to fetch. Default to 4", + "placeholder": "4", + "type": "number", + "additionalParams": true, + "optional": true, + "id": "pineconeLlamaIndex_1-input-topK-number" + } + ], + "inputAnchors": [ + { + "label": "Document", + "name": "document", + "type": "Document", + "list": true, + "optional": true, + "id": "pineconeLlamaIndex_1-input-document-Document" + }, + { + "label": "Chat Model", + "name": "model", + "type": "BaseChatModel_LlamaIndex", + "id": "pineconeLlamaIndex_1-input-model-BaseChatModel_LlamaIndex" + }, + { + "label": "Embeddings", + "name": "embeddings", + "type": "BaseEmbedding_LlamaIndex", + "id": "pineconeLlamaIndex_1-input-embeddings-BaseEmbedding_LlamaIndex" + } + ], + "inputs": { + "document": [], + "model": "{{chatOpenAI_LlamaIndex_0.data.instance}}", + "embeddings": "{{openAIEmbedding_LlamaIndex_0.data.instance}}", + "pineconeIndex": "flowiseindex", + "pineconeNamespace": "pinecone-form10k", + "pineconeMetadataFilter": "{\"source\":\"apple\"}", + "topK": "" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "pineconeLlamaIndex_1-output-retriever-Pinecone|VectorIndexRetriever", + "name": "retriever", + "label": "Pinecone Retriever", + "type": "Pinecone | VectorIndexRetriever" + }, + { + "id": "pineconeLlamaIndex_1-output-vectorStore-Pinecone|VectorStoreIndex", + "name": "vectorStore", + "label": "Pinecone Vector Store Index", + "type": "Pinecone | VectorStoreIndex" + } + ], + "default": "retriever" + } + ], + "outputs": { + "output": "vectorStore" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 42.17855025460784, + "y": -839.8824444107056 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "queryEngineToolLlamaIndex_1", + "position": { + "x": 462.16721384216123, + "y": -17.750065363429798 + }, + "type": "customNode", + "data": { + "id": "queryEngineToolLlamaIndex_1", + "label": "QueryEngine Tool", + "version": 1, + "name": "queryEngineToolLlamaIndex", + "type": "QueryEngineTool", + "baseClasses": ["QueryEngineTool"], + "tags": ["LlamaIndex"], + "category": "Tools", + "description": "Execute actions using ChatGPT Plugin Url", + "inputParams": [ + { + "label": "Tool Name", + "name": "toolName", + "type": "string", + "description": "Tool name must be small capital letter with underscore. Ex: my_tool", + "id": "queryEngineToolLlamaIndex_1-input-toolName-string" + }, + { + "label": "Tool Description", + "name": "toolDesc", + "type": "string", + "rows": 4, + "id": "queryEngineToolLlamaIndex_1-input-toolDesc-string" + } + ], + "inputAnchors": [ + { + "label": "Vector Store Index", + "name": "vectorStoreIndex", + "type": "VectorStoreIndex", + "id": "queryEngineToolLlamaIndex_1-input-vectorStoreIndex-VectorStoreIndex" + } + ], + "inputs": { + "vectorStoreIndex": "{{pineconeLlamaIndex_0.data.instance}}", + "toolName": "tesla_tool", + "toolDesc": "A SEC Form 10K filing describing the financials of Tesla Inc (TSLA) for the 2022 time period." + }, + "outputAnchors": [ + { + "id": "queryEngineToolLlamaIndex_1-output-queryEngineToolLlamaIndex-QueryEngineTool", + "name": "queryEngineToolLlamaIndex", + "label": "QueryEngineTool", + "type": "QueryEngineTool" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 462.16721384216123, + "y": -17.750065363429798 + }, + "dragging": false + }, + { + "width": 300, + "height": 484, + "id": "subQuestionQueryEngine_0", + "position": { + "x": 982.7583030231563, + "y": 349.50858200305896 + }, + "type": "customNode", + "data": { + "id": "subQuestionQueryEngine_0", + "label": "Sub Question Query Engine", + "version": 1, + "name": "subQuestionQueryEngine", + "type": "SubQuestionQueryEngine", + "baseClasses": ["SubQuestionQueryEngine"], + "tags": ["LlamaIndex"], + "category": "Engine", + "description": "Simple query engine built to answer question over your data, without memory", + "inputParams": [ + { + "label": "Return Source Documents", + "name": "returnSourceDocuments", + "type": "boolean", + "optional": true, + "id": "subQuestionQueryEngine_0-input-returnSourceDocuments-boolean" + } + ], + "inputAnchors": [ + { + "label": "QueryEngine Tools", + "name": "queryEngineTools", + "type": "QueryEngineTool", + "list": true, + "id": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" + }, + { + "label": "Chat Model", + "name": "model", + "type": "BaseChatModel_LlamaIndex", + "id": "subQuestionQueryEngine_0-input-model-BaseChatModel_LlamaIndex" + }, + { + "label": "Embeddings", + "name": "embeddings", + "type": "BaseEmbedding_LlamaIndex", + "id": "subQuestionQueryEngine_0-input-embeddings-BaseEmbedding_LlamaIndex" + }, + { + "label": "Response Synthesizer", + "name": "responseSynthesizer", + "type": "ResponseSynthesizer", + "description": "ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See more", + "optional": true, + "id": "subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer" + } + ], + "inputs": { + "queryEngineTools": ["{{queryEngineToolLlamaIndex_1.data.instance}}", "{{queryEngineToolLlamaIndex_0.data.instance}}"], + "model": "{{chatOpenAI_LlamaIndex_1.data.instance}}", + "embeddings": "{{openAIEmbedding_LlamaIndex_1.data.instance}}", + "responseSynthesizer": "{{compactrefineLlamaIndex_0.data.instance}}", + "returnSourceDocuments": true + }, + "outputAnchors": [ + { + "id": "subQuestionQueryEngine_0-output-subQuestionQueryEngine-SubQuestionQueryEngine", + "name": "subQuestionQueryEngine", + "label": "SubQuestionQueryEngine", + "type": "SubQuestionQueryEngine" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 982.7583030231563, + "y": 349.50858200305896 + }, + "dragging": false + }, + { + "width": 300, + "height": 529, + "id": "chatOpenAI_LlamaIndex_1", + "position": { + "x": -846.9087470244615, + "y": 23.446501495097493 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_LlamaIndex_1", + "label": "ChatOpenAI", + "version": 1, + "name": "chatOpenAI_LlamaIndex", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel_LlamaIndex", "BaseLLM"], + "tags": ["LlamaIndex"], + "category": "Chat Models", + "description": "Wrapper around OpenAI Chat LLM specific for LlamaIndex", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_LlamaIndex_1-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_LlamaIndex_1-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_LlamaIndex_1-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_LlamaIndex_1-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_LlamaIndex_1-input-topP-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_LlamaIndex_1-input-timeout-number" + } + ], + "inputAnchors": [], + "inputs": { + "modelName": "gpt-3.5-turbo-16k", + "temperature": "0", + "maxTokens": "", + "topP": "", + "timeout": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_LlamaIndex_1-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", + "name": "chatOpenAI_LlamaIndex", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel_LlamaIndex | BaseLLM" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -846.9087470244615, + "y": 23.446501495097493 + }, + "dragging": false + }, + { + "width": 300, + "height": 334, + "id": "openAIEmbedding_LlamaIndex_1", + "position": { + "x": -437.3136244622061, + "y": 329.99986619821175 + }, + "type": "customNode", + "data": { + "id": "openAIEmbedding_LlamaIndex_1", + "label": "OpenAI Embedding", + "version": 1, + "name": "openAIEmbedding_LlamaIndex", + "type": "OpenAIEmbedding", + "baseClasses": ["OpenAIEmbedding", "BaseEmbedding_LlamaIndex", "BaseEmbedding"], + "tags": ["LlamaIndex"], + "category": "Embeddings", + "description": "OpenAI Embedding specific for LlamaIndex", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "openAIEmbedding_LlamaIndex_1-input-credential-credential" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "optional": true, + "additionalParams": true, + "id": "openAIEmbedding_LlamaIndex_1-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "openAIEmbedding_LlamaIndex_1-input-basepath-string" + } + ], + "inputAnchors": [], + "inputs": { + "timeout": "", + "basepath": "" + }, + "outputAnchors": [ + { + "id": "openAIEmbedding_LlamaIndex_1-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", + "name": "openAIEmbedding_LlamaIndex", + "label": "OpenAIEmbedding", + "type": "OpenAIEmbedding | BaseEmbedding_LlamaIndex | BaseEmbedding" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": -437.3136244622061, + "y": 329.99986619821175 + } + }, + { + "width": 300, + "height": 82, + "id": "stickyNote_0", + "position": { + "x": 35.90892935132143, + "y": -936.1282632923861 + }, + "type": "stickyNote", + "data": { + "id": "stickyNote_0", + "label": "Sticky Note", + "version": 1, + "name": "stickyNote", + "type": "StickyNote", + "baseClasses": ["StickyNote"], + "category": "Utilities", + "description": "Add a sticky note", + "inputParams": [ + { + "label": "", + "name": "note", + "type": "string", + "rows": 1, + "placeholder": "Type something here", + "optional": true, + "id": "stickyNote_0-input-note-string" + } + ], + "inputAnchors": [], + "inputs": { + "note": "Query previously upserted documents with corresponding metadata key value pair - \n{ source: \"apple\"}" + }, + "outputAnchors": [ + { + "id": "stickyNote_0-output-stickyNote-StickyNote", + "name": "stickyNote", + "label": "StickyNote", + "type": "StickyNote" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 35.90892935132143, + "y": -936.1282632923861 + }, + "dragging": false + }, + { + "width": 300, + "height": 82, + "id": "stickyNote_1", + "position": { + "x": 37.74909394815296, + "y": -215.17456133022054 + }, + "type": "stickyNote", + "data": { + "id": "stickyNote_1", + "label": "Sticky Note", + "version": 1, + "name": "stickyNote", + "type": "StickyNote", + "baseClasses": ["StickyNote"], + "category": "Utilities", + "description": "Add a sticky note", + "inputParams": [ + { + "label": "", + "name": "note", + "type": "string", + "rows": 1, + "placeholder": "Type something here", + "optional": true, + "id": "stickyNote_1-input-note-string" + } + ], + "inputAnchors": [], + "inputs": { + "note": "Query previously upserted documents with corresponding metadata key value pair - \n{ source: \"tesla\"}" + }, + "outputAnchors": [ + { + "id": "stickyNote_1-output-stickyNote-StickyNote", + "name": "stickyNote", + "label": "StickyNote", + "type": "StickyNote" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 37.74909394815296, + "y": -215.17456133022054 + }, + "dragging": false + }, + { + "width": 300, + "height": 163, + "id": "stickyNote_2", + "position": { + "x": 984.9543031068163, + "y": 171.04264459503852 + }, + "type": "stickyNote", + "data": { + "id": "stickyNote_2", + "label": "Sticky Note", + "version": 1, + "name": "stickyNote", + "type": "StickyNote", + "baseClasses": ["StickyNote"], + "category": "Utilities", + "description": "Add a sticky note", + "inputParams": [ + { + "label": "", + "name": "note", + "type": "string", + "rows": 1, + "placeholder": "Type something here", + "optional": true, + "id": "stickyNote_2-input-note-string" + } + ], + "inputAnchors": [], + "inputs": { + "note": "Break questions into subqueries, then retrieve corresponding context using queryengine tool.\n\nThis implementation does not contains memory, we can use OpenAI Agent to function call this flow" + }, + "outputAnchors": [ + { + "id": "stickyNote_2-output-stickyNote-StickyNote", + "name": "stickyNote", + "label": "StickyNote", + "type": "StickyNote" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 984.9543031068163, + "y": 171.04264459503852 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "chatOpenAI_LlamaIndex_0", + "sourceHandle": "chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", + "target": "pineconeLlamaIndex_1", + "targetHandle": "pineconeLlamaIndex_1-input-model-BaseChatModel_LlamaIndex", + "type": "buttonedge", + "id": "chatOpenAI_LlamaIndex_0-chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM-pineconeLlamaIndex_1-pineconeLlamaIndex_1-input-model-BaseChatModel_LlamaIndex" + }, + { + "source": "openAIEmbedding_LlamaIndex_0", + "sourceHandle": "openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", + "target": "pineconeLlamaIndex_1", + "targetHandle": "pineconeLlamaIndex_1-input-embeddings-BaseEmbedding_LlamaIndex", + "type": "buttonedge", + "id": "openAIEmbedding_LlamaIndex_0-openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding-pineconeLlamaIndex_1-pineconeLlamaIndex_1-input-embeddings-BaseEmbedding_LlamaIndex" + }, + { + "source": "pineconeLlamaIndex_1", + "sourceHandle": "pineconeLlamaIndex_1-output-vectorStore-Pinecone|VectorStoreIndex", + "target": "queryEngineToolLlamaIndex_0", + "targetHandle": "queryEngineToolLlamaIndex_0-input-vectorStoreIndex-VectorStoreIndex", + "type": "buttonedge", + "id": "pineconeLlamaIndex_1-pineconeLlamaIndex_1-output-vectorStore-Pinecone|VectorStoreIndex-queryEngineToolLlamaIndex_0-queryEngineToolLlamaIndex_0-input-vectorStoreIndex-VectorStoreIndex" + }, + { + "source": "pineconeLlamaIndex_0", + "sourceHandle": "pineconeLlamaIndex_0-output-vectorStore-Pinecone|VectorStoreIndex", + "target": "queryEngineToolLlamaIndex_1", + "targetHandle": "queryEngineToolLlamaIndex_1-input-vectorStoreIndex-VectorStoreIndex", + "type": "buttonedge", + "id": "pineconeLlamaIndex_0-pineconeLlamaIndex_0-output-vectorStore-Pinecone|VectorStoreIndex-queryEngineToolLlamaIndex_1-queryEngineToolLlamaIndex_1-input-vectorStoreIndex-VectorStoreIndex" + }, + { + "source": "queryEngineToolLlamaIndex_1", + "sourceHandle": "queryEngineToolLlamaIndex_1-output-queryEngineToolLlamaIndex-QueryEngineTool", + "target": "subQuestionQueryEngine_0", + "targetHandle": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool", + "type": "buttonedge", + "id": "queryEngineToolLlamaIndex_1-queryEngineToolLlamaIndex_1-output-queryEngineToolLlamaIndex-QueryEngineTool-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" + }, + { + "source": "queryEngineToolLlamaIndex_0", + "sourceHandle": "queryEngineToolLlamaIndex_0-output-queryEngineToolLlamaIndex-QueryEngineTool", + "target": "subQuestionQueryEngine_0", + "targetHandle": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool", + "type": "buttonedge", + "id": "queryEngineToolLlamaIndex_0-queryEngineToolLlamaIndex_0-output-queryEngineToolLlamaIndex-QueryEngineTool-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" + }, + { + "source": "chatOpenAI_LlamaIndex_1", + "sourceHandle": "chatOpenAI_LlamaIndex_1-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", + "target": "subQuestionQueryEngine_0", + "targetHandle": "subQuestionQueryEngine_0-input-model-BaseChatModel_LlamaIndex", + "type": "buttonedge", + "id": "chatOpenAI_LlamaIndex_1-chatOpenAI_LlamaIndex_1-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-model-BaseChatModel_LlamaIndex" + }, + { + "source": "openAIEmbedding_LlamaIndex_1", + "sourceHandle": "openAIEmbedding_LlamaIndex_1-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", + "target": "subQuestionQueryEngine_0", + "targetHandle": "subQuestionQueryEngine_0-input-embeddings-BaseEmbedding_LlamaIndex", + "type": "buttonedge", + "id": "openAIEmbedding_LlamaIndex_1-openAIEmbedding_LlamaIndex_1-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-embeddings-BaseEmbedding_LlamaIndex" + }, + { + "source": "compactrefineLlamaIndex_0", + "sourceHandle": "compactrefineLlamaIndex_0-output-compactrefineLlamaIndex-CompactRefine|ResponseSynthesizer", + "target": "subQuestionQueryEngine_0", + "targetHandle": "subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer", + "type": "buttonedge", + "id": "compactrefineLlamaIndex_0-compactrefineLlamaIndex_0-output-compactrefineLlamaIndex-CompactRefine|ResponseSynthesizer-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer" + }, + { + "source": "openAIEmbedding_LlamaIndex_0", + "sourceHandle": "openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", + "target": "pineconeLlamaIndex_0", + "targetHandle": "pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex", + "type": "buttonedge", + "id": "openAIEmbedding_LlamaIndex_0-openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex" + }, + { + "source": "chatOpenAI_LlamaIndex_0", + "sourceHandle": "chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", + "target": "pineconeLlamaIndex_0", + "targetHandle": "pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex", + "type": "buttonedge", + "id": "chatOpenAI_LlamaIndex_0-chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex" + } + ] +} From 288e451161fa206265d5830cb583f0b18f13307c Mon Sep 17 00:00:00 2001 From: Kenny Vaneetvelde Date: Sat, 3 Feb 2024 19:43:44 +0100 Subject: [PATCH 387/502] Improve flexibility of the structured output parser by allowing the user to input an example JSON and automatically converting it to a zod scheme --- .../StructuredOutputParser.ts | 48 +++----- packages/components/package.json | 1 + .../chatflows/Structured Output Parser.json | 114 +++++++----------- 3 files changed, 64 insertions(+), 99 deletions(-) diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts index fc28fd1c..849d825d 100644 --- a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts +++ b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts @@ -1,8 +1,9 @@ -import { convertSchemaToZod, getBaseClasses, INode, INodeData, INodeParams } from '../../../src' +import { getBaseClasses, INode, INodeData, INodeParams } from '../../../src' import { BaseOutputParser } from 'langchain/schema/output_parser' import { StructuredOutputParser as LangchainStructuredOutputParser } from 'langchain/output_parsers' import { CATEGORY } from '../OutputParserHelpers' import { z } from 'zod' +import { jsonToZod } from 'json-to-zod' class StructuredOutputParser implements INode { label: string @@ -34,44 +35,31 @@ class StructuredOutputParser implements INode { description: 'In the event that the first call fails, will make another call to the model to fix any errors.' }, { - label: 'JSON Structure', - name: 'jsonStructure', - type: 'datagrid', - description: 'JSON structure for LLM to return', - datagrid: [ - { field: 'property', headerName: 'Property', editable: true }, - { - field: 'type', - headerName: 'Type', - type: 'singleSelect', - valueOptions: ['string', 'number', 'boolean'], - editable: true - }, - { field: 'description', headerName: 'Description', editable: true, flex: 1 } - ], - default: [ - { - property: 'answer', - type: 'string', - description: `answer to the user's question` - }, - { - property: 'source', - type: 'string', - description: `sources used to answer the question, should be websites` - } - ], + label: 'Example JSON', + name: 'exampleJson', + type: 'string', + description: 'Example JSON structure for LLM to return', + rows: 10, + default: '{"answer": "the answer", "followupQuestions": ["question1", "question2"]}', additionalParams: true } ] } async init(nodeData: INodeData): Promise { - const jsonStructure = nodeData.inputs?.jsonStructure as string + const exampleJson = nodeData.inputs?.exampleJson as string const autoFix = nodeData.inputs?.autofixParser as boolean + const jsonToZodString = jsonToZod(JSON.parse(exampleJson)) + const splitString = jsonToZodString.split('const schema = ') + const schemaString = splitString[1].trim() + + const fnString = `function proxyFn(z){ return ${schemaString} }` + const zodSchemaFunction = new Function('z', `return ${schemaString}`) + const zodSchema = zodSchemaFunction(z) + try { - const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(z.object(convertSchemaToZod(jsonStructure))) + const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(zodSchema) // NOTE: When we change Flowise to return a json response, the following has to be changed to: JsonStructuredOutputParser Object.defineProperty(structuredOutputParser, 'autoFix', { diff --git a/packages/components/package.json b/packages/components/package.json index bcb746b0..2efb2143 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -56,6 +56,7 @@ "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", + "json-to-zod": "1.1.2", "langchain": "^0.0.214", "langfuse": "2.0.2", "langfuse-langchain": "2.3.3", diff --git a/packages/server/marketplaces/chatflows/Structured Output Parser.json b/packages/server/marketplaces/chatflows/Structured Output Parser.json index 92336443..93bb96bb 100644 --- a/packages/server/marketplaces/chatflows/Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Structured Output Parser.json @@ -4,7 +4,7 @@ "nodes": [ { "width": 300, - "height": 574, + "height": 576, "id": "chatOpenAI_0", "position": { "x": 845.3961479115309, @@ -17,7 +17,12 @@ "version": 3, "name": "chatOpenAI", "type": "ChatOpenAI", - "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "baseClasses": [ + "ChatOpenAI", + "BaseChatModel", + "BaseLanguageModel", + "Runnable" + ], "category": "Chat Models", "description": "Wrapper around OpenAI large language models that use the Chat endpoint", "inputParams": [ @@ -25,7 +30,9 @@ "label": "Connect Credential", "name": "credential", "type": "credential", - "credentialNames": ["openAIApi"], + "credentialNames": [ + "openAIApi" + ], "id": "chatOpenAI_0-input-credential-credential" }, { @@ -202,7 +209,7 @@ }, { "width": 300, - "height": 456, + "height": 508, "id": "llmChain_0", "position": { "x": 1229.1699649849293, @@ -215,7 +222,11 @@ "version": 3, "name": "llmChain", "type": "LLMChain", - "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "baseClasses": [ + "LLMChain", + "BaseChain", + "Runnable" + ], "category": "Chains", "description": "Chain to run queries against LLMs", "inputParams": [ @@ -300,7 +311,7 @@ }, { "width": 300, - "height": 652, + "height": 690, "id": "chatPromptTemplate_0", "position": { "x": 501.1597501123828, @@ -313,7 +324,12 @@ "version": 1, "name": "chatPromptTemplate", "type": "ChatPromptTemplate", - "baseClasses": ["ChatPromptTemplate", "BaseChatPromptTemplate", "BasePromptTemplate", "Runnable"], + "baseClasses": [ + "ChatPromptTemplate", + "BaseChatPromptTemplate", + "BasePromptTemplate", + "Runnable" + ], "category": "Prompts", "description": "Schema to represent a chat prompt", "inputParams": [ @@ -369,11 +385,11 @@ }, { "width": 300, - "height": 328, + "height": 329, "id": "structuredOutputParser_0", "position": { - "x": 170.3869571939727, - "y": 343.9298288967859 + "x": 498.2326128526694, + "y": 566.5473204649535 }, "type": "customNode", "data": { @@ -382,7 +398,11 @@ "version": 1, "name": "structuredOutputParser", "type": "StructuredOutputParser", - "baseClasses": ["StructuredOutputParser", "BaseLLMOutputParser", "Runnable"], + "baseClasses": [ + "StructuredOutputParser", + "BaseLLMOutputParser", + "Runnable" + ], "category": "Output Parsers", "description": "Parse the output of an LLM call into a given (JSON) structure.", "inputParams": [ @@ -395,61 +415,20 @@ "id": "structuredOutputParser_0-input-autofixParser-boolean" }, { - "label": "JSON Structure", - "name": "jsonStructure", - "type": "datagrid", - "description": "JSON structure for LLM to return", - "datagrid": [ - { - "field": "property", - "headerName": "Property", - "editable": true - }, - { - "field": "type", - "headerName": "Type", - "type": "singleSelect", - "valueOptions": ["string", "number", "boolean"], - "editable": true - }, - { - "field": "description", - "headerName": "Description", - "editable": true, - "flex": 1 - } - ], - "default": [ - { - "property": "answer", - "type": "string", - "description": "answer to the user's question" - }, - { - "property": "source", - "type": "string", - "description": "sources used to answer the question, should be websites" - } - ], + "label": "Example JSON", + "name": "exampleJson", + "type": "string", + "description": "Example JSON structure for LLM to return", + "rows": 10, + "default": "{\"answer\": \"the answer\", \"followupQuestions\": [\"question1\", \"question2\"]}", "additionalParams": true, - "id": "structuredOutputParser_0-input-jsonStructure-datagrid" + "id": "structuredOutputParser_0-input-exampleJson-string" } ], "inputAnchors": [], "inputs": { "autofixParser": true, - "jsonStructure": [ - { - "property": "answer", - "type": "string", - "description": "answer to the user's question" - }, - { - "property": "source", - "type": "string", - "description": "sources used to answer the question, should be websites" - } - ] + "exampleJson": "{\"answer\": \"the answer\", \"followupQuestions\": [\"question1\", \"question2\"]}" }, "outputAnchors": [ { @@ -463,11 +442,11 @@ "selected": false }, "selected": false, + "dragging": false, "positionAbsolute": { - "x": 170.3869571939727, - "y": 343.9298288967859 - }, - "dragging": false + "x": 498.2326128526694, + "y": 566.5473204649535 + } } ], "edges": [ @@ -499,10 +478,7 @@ "target": "llmChain_0", "targetHandle": "llmChain_0-input-outputParser-BaseLLMOutputParser", "type": "buttonedge", - "id": "structuredOutputParser_0-structuredOutputParser_0-output-structuredOutputParser-StructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser", - "data": { - "label": "" - } + "id": "structuredOutputParser_0-structuredOutputParser_0-output-structuredOutputParser-StructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser" } ] -} +} \ No newline at end of file From 95b251f02acfdb6e55f2d9f511732b262e0c0370 Mon Sep 17 00:00:00 2001 From: Kenny Vaneetvelde Date: Sat, 3 Feb 2024 21:22:49 +0100 Subject: [PATCH 388/502] Make it a separate node instead --- .../StructuredOutputParserAdvanced.ts | 80 +++ .../structure.svg | 8 + packages/components/package.json | 1 - .../Advanced Structured Output Parser.json | 463 ++++++++++++++++++ 4 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts create mode 100644 packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/structure.svg create mode 100644 packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json diff --git a/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts b/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts new file mode 100644 index 00000000..b0fad136 --- /dev/null +++ b/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts @@ -0,0 +1,80 @@ +import { getBaseClasses, INode, INodeData, INodeParams } from '../../../src' +import { BaseOutputParser } from 'langchain/schema/output_parser' +import { StructuredOutputParser as LangchainStructuredOutputParser } from 'langchain/output_parsers' +import { CATEGORY } from '../OutputParserHelpers' +import { z } from 'zod' + +class AdvancedStructuredOutputParser implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'Advanced Structured Output Parser' + this.name = 'advancedStructuredOutputParser' + this.version = 1.0 + this.type = 'AdvancedStructuredOutputParser' + this.description = 'Parse the output of an LLM call into a given structure by providing a Zod schema.' + this.icon = 'structure.svg' + this.category = CATEGORY + this.baseClasses = [this.type, ...getBaseClasses(BaseOutputParser)] + this.inputs = [ + { + label: 'Autofix', + name: 'autofixParser', + type: 'boolean', + optional: true, + description: 'In the event that the first call fails, will make another call to the model to fix any errors.' + }, + { + label: 'Example JSON', + name: 'exampleJson', + type: 'string', + description: 'Zod schema for the output of the model', + rows: 10, + default: `z.object({ + title: z.string(), // Title of the movie as a string + yearOfRelease: z.number().int(), // Release year as an integer number, + genres: z.enum([ + "Action", "Comedy", "Drama", "Fantasy", "Horror", + "Mystery", "Romance", "Science Fiction", "Thriller", "Documentary" + ]).array().max(2), // Array of genres, max of 2 from the defined enum + shortDescription: z.string().max(500) // Short description, max 150 characters +})`, + additionalParams: true + } + ] + } + + async init(nodeData: INodeData): Promise { + const schemaString = nodeData.inputs?.exampleJson as string + const autoFix = nodeData.inputs?.autofixParser as boolean + + const zodSchemaFunction = new Function('z', `return ${schemaString}`) + const zodSchema = zodSchemaFunction(z) + + try { + const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(zodSchema) + + // NOTE: When we change Flowise to return a json response, the following has to be changed to: JsonStructuredOutputParser + Object.defineProperty(structuredOutputParser, 'autoFix', { + enumerable: true, + configurable: true, + writable: true, + value: autoFix + }) + return structuredOutputParser + } catch (exception) { + throw new Error('Error parsing Zod Schema: ' + exception) + } + } +} + +module.exports = { nodeClass: AdvancedStructuredOutputParser } diff --git a/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/structure.svg b/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/structure.svg new file mode 100644 index 00000000..3875982d --- /dev/null +++ b/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/structure.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/components/package.json b/packages/components/package.json index 2efb2143..bcb746b0 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -56,7 +56,6 @@ "html-to-text": "^9.0.5", "husky": "^8.0.3", "ioredis": "^5.3.2", - "json-to-zod": "1.1.2", "langchain": "^0.0.214", "langfuse": "2.0.2", "langfuse-langchain": "2.3.3", diff --git a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json new file mode 100644 index 00000000..41cbb08f --- /dev/null +++ b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json @@ -0,0 +1,463 @@ +{ + "nodes": [ + { + "width": 300, + "height": 508, + "id": "llmChain_0", + "position": { + "x": 1229.1699649849293, + "y": 245.55173505632646 + }, + "type": "customNode", + "data": { + "id": "llmChain_0", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_0-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_0-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_0-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_0-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_0-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_0.data.instance}}", + "prompt": "{{chatPromptTemplate_0.data.instance}}", + "outputParser": "{{advancedStructuredOutputParser_0.data.instance}}", + "chainName": "", + "inputModeration": "" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_0-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_0-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "positionAbsolute": { + "x": 1229.1699649849293, + "y": 245.55173505632646 + }, + "selected": false + }, + { + "width": 300, + "height": 690, + "id": "chatPromptTemplate_0", + "position": { + "x": 493.26582927222483, + "y": -156.20470841335592 + }, + "type": "customNode", + "data": { + "id": "chatPromptTemplate_0", + "label": "Chat Prompt Template", + "version": 1, + "name": "chatPromptTemplate", + "type": "ChatPromptTemplate", + "baseClasses": ["ChatPromptTemplate", "BaseChatPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a chat prompt", + "inputParams": [ + { + "label": "System Message", + "name": "systemMessagePrompt", + "type": "string", + "rows": 4, + "placeholder": "You are a helpful assistant that translates {input_language} to {output_language}.", + "id": "chatPromptTemplate_0-input-systemMessagePrompt-string" + }, + { + "label": "Human Message", + "name": "humanMessagePrompt", + "type": "string", + "rows": 4, + "placeholder": "{text}", + "id": "chatPromptTemplate_0-input-humanMessagePrompt-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "chatPromptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "systemMessagePrompt": "This AI is designed to only output information in JSON format without exception. This AI can only output JSON and will never output any other text.\n\nWhen asked to correct itself, this AI will only output the corrected JSON and never any other text.", + "humanMessagePrompt": "{text}", + "promptValues": "" + }, + "outputAnchors": [ + { + "id": "chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable", + "name": "chatPromptTemplate", + "label": "ChatPromptTemplate", + "type": "ChatPromptTemplate | BaseChatPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 493.26582927222483, + "y": -156.20470841335592 + }, + "dragging": false + }, + { + "width": 300, + "height": 329, + "id": "advancedStructuredOutputParser_0", + "position": { + "x": 494.20163170226266, + "y": 568.3420937517054 + }, + "type": "customNode", + "data": { + "id": "advancedStructuredOutputParser_0", + "label": "Advanced Structured Output Parser", + "version": 1, + "name": "advancedStructuredOutputParser", + "type": "AdvancedStructuredOutputParser", + "baseClasses": ["AdvancedStructuredOutputParser", "BaseLLMOutputParser", "Runnable"], + "category": "Output Parsers", + "description": "Parse the output of an LLM call into a given structure by providing a Zod schema.", + "inputParams": [ + { + "label": "Autofix", + "name": "autofixParser", + "type": "boolean", + "optional": true, + "description": "In the event that the first call fails, will make another call to the model to fix any errors.", + "id": "advancedStructuredOutputParser_0-input-autofixParser-boolean" + }, + { + "label": "Example JSON", + "name": "exampleJson", + "type": "string", + "description": "Zod schema for the output of the model", + "rows": 10, + "default": "z.object({\n title: z.string(), // Title of the movie as a string\n yearOfRelease: z.number().int(), // Release year as an integer number,\n genres: z.enum([\n \"Action\", \"Comedy\", \"Drama\", \"Fantasy\", \"Horror\",\n \"Mystery\", \"Romance\", \"Science Fiction\", \"Thriller\", \"Documentary\"\n ]).array().max(2), // Array of genres, max of 2 from the defined enum\n shortDescription: z.string().max(500) // Short description, max 150 characters\n})", + "additionalParams": true, + "id": "advancedStructuredOutputParser_0-input-exampleJson-string" + } + ], + "inputAnchors": [], + "inputs": { + "autofixParser": true, + "exampleJson": "z.object({\n title: z.string(), // Title of the movie as a string\n yearOfRelease: z.number().int(), // Release year as an integer number,\n genres: z.enum([\n \"Action\", \"Comedy\", \"Drama\", \"Fantasy\", \"Horror\",\n \"Mystery\", \"Romance\", \"Science Fiction\", \"Thriller\", \"Documentary\"\n ]).array().max(2), // Array of genres, max of 2 from the defined enum\n shortDescription: z.string().max(500) // Short description, max 150 characters\n})" + }, + "outputAnchors": [ + { + "id": "advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable", + "name": "advancedStructuredOutputParser", + "label": "AdvancedStructuredOutputParser", + "type": "AdvancedStructuredOutputParser | BaseLLMOutputParser | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 494.20163170226266, + "y": 568.3420937517054 + }, + "dragging": false + }, + { + "width": 300, + "height": 576, + "id": "chatOpenAI_0", + "position": { + "x": 860.555928011636, + "y": -355.71028569475095 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 3, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-turbo-preview", + "name": "gpt-4-turbo-preview" + }, + { + "label": "gpt-4-0125-preview", + "name": "gpt-4-0125-preview" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "", + "temperature": "0", + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "http://localhost:8901/v1", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 860.555928011636, + "y": -355.71028569475095 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "chatPromptTemplate_0", + "sourceHandle": "chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "chatPromptTemplate_0-chatPromptTemplate_0-output-chatPromptTemplate-ChatPromptTemplate|BaseChatPromptTemplate|BasePromptTemplate|Runnable-llmChain_0-llmChain_0-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + }, + { + "source": "advancedStructuredOutputParser_0", + "sourceHandle": "advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-outputParser-BaseLLMOutputParser", + "type": "buttonedge", + "id": "advancedStructuredOutputParser_0-advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser" + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_0-llmChain_0-input-model-BaseLanguageModel" + } + ] +} From 62f70ab6efe46f6be2d2cddefc2cc12d8d008672 Mon Sep 17 00:00:00 2001 From: Kenny Vaneetvelde Date: Sat, 3 Feb 2024 21:25:26 +0100 Subject: [PATCH 389/502] Revert the changes to the original parser & template --- .../StructuredOutputParser.ts | 48 +++++--- .../chatflows/Structured Output Parser.json | 114 +++++++++++------- 2 files changed, 99 insertions(+), 63 deletions(-) diff --git a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts index 849d825d..fc28fd1c 100644 --- a/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts +++ b/packages/components/nodes/outputparsers/StructuredOutputParser/StructuredOutputParser.ts @@ -1,9 +1,8 @@ -import { getBaseClasses, INode, INodeData, INodeParams } from '../../../src' +import { convertSchemaToZod, getBaseClasses, INode, INodeData, INodeParams } from '../../../src' import { BaseOutputParser } from 'langchain/schema/output_parser' import { StructuredOutputParser as LangchainStructuredOutputParser } from 'langchain/output_parsers' import { CATEGORY } from '../OutputParserHelpers' import { z } from 'zod' -import { jsonToZod } from 'json-to-zod' class StructuredOutputParser implements INode { label: string @@ -35,31 +34,44 @@ class StructuredOutputParser implements INode { description: 'In the event that the first call fails, will make another call to the model to fix any errors.' }, { - label: 'Example JSON', - name: 'exampleJson', - type: 'string', - description: 'Example JSON structure for LLM to return', - rows: 10, - default: '{"answer": "the answer", "followupQuestions": ["question1", "question2"]}', + label: 'JSON Structure', + name: 'jsonStructure', + type: 'datagrid', + description: 'JSON structure for LLM to return', + datagrid: [ + { field: 'property', headerName: 'Property', editable: true }, + { + field: 'type', + headerName: 'Type', + type: 'singleSelect', + valueOptions: ['string', 'number', 'boolean'], + editable: true + }, + { field: 'description', headerName: 'Description', editable: true, flex: 1 } + ], + default: [ + { + property: 'answer', + type: 'string', + description: `answer to the user's question` + }, + { + property: 'source', + type: 'string', + description: `sources used to answer the question, should be websites` + } + ], additionalParams: true } ] } async init(nodeData: INodeData): Promise { - const exampleJson = nodeData.inputs?.exampleJson as string + const jsonStructure = nodeData.inputs?.jsonStructure as string const autoFix = nodeData.inputs?.autofixParser as boolean - const jsonToZodString = jsonToZod(JSON.parse(exampleJson)) - const splitString = jsonToZodString.split('const schema = ') - const schemaString = splitString[1].trim() - - const fnString = `function proxyFn(z){ return ${schemaString} }` - const zodSchemaFunction = new Function('z', `return ${schemaString}`) - const zodSchema = zodSchemaFunction(z) - try { - const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(zodSchema) + const structuredOutputParser = LangchainStructuredOutputParser.fromZodSchema(z.object(convertSchemaToZod(jsonStructure))) // NOTE: When we change Flowise to return a json response, the following has to be changed to: JsonStructuredOutputParser Object.defineProperty(structuredOutputParser, 'autoFix', { diff --git a/packages/server/marketplaces/chatflows/Structured Output Parser.json b/packages/server/marketplaces/chatflows/Structured Output Parser.json index 93bb96bb..92336443 100644 --- a/packages/server/marketplaces/chatflows/Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Structured Output Parser.json @@ -4,7 +4,7 @@ "nodes": [ { "width": 300, - "height": 576, + "height": 574, "id": "chatOpenAI_0", "position": { "x": 845.3961479115309, @@ -17,12 +17,7 @@ "version": 3, "name": "chatOpenAI", "type": "ChatOpenAI", - "baseClasses": [ - "ChatOpenAI", - "BaseChatModel", - "BaseLanguageModel", - "Runnable" - ], + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], "category": "Chat Models", "description": "Wrapper around OpenAI large language models that use the Chat endpoint", "inputParams": [ @@ -30,9 +25,7 @@ "label": "Connect Credential", "name": "credential", "type": "credential", - "credentialNames": [ - "openAIApi" - ], + "credentialNames": ["openAIApi"], "id": "chatOpenAI_0-input-credential-credential" }, { @@ -209,7 +202,7 @@ }, { "width": 300, - "height": 508, + "height": 456, "id": "llmChain_0", "position": { "x": 1229.1699649849293, @@ -222,11 +215,7 @@ "version": 3, "name": "llmChain", "type": "LLMChain", - "baseClasses": [ - "LLMChain", - "BaseChain", - "Runnable" - ], + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], "category": "Chains", "description": "Chain to run queries against LLMs", "inputParams": [ @@ -311,7 +300,7 @@ }, { "width": 300, - "height": 690, + "height": 652, "id": "chatPromptTemplate_0", "position": { "x": 501.1597501123828, @@ -324,12 +313,7 @@ "version": 1, "name": "chatPromptTemplate", "type": "ChatPromptTemplate", - "baseClasses": [ - "ChatPromptTemplate", - "BaseChatPromptTemplate", - "BasePromptTemplate", - "Runnable" - ], + "baseClasses": ["ChatPromptTemplate", "BaseChatPromptTemplate", "BasePromptTemplate", "Runnable"], "category": "Prompts", "description": "Schema to represent a chat prompt", "inputParams": [ @@ -385,11 +369,11 @@ }, { "width": 300, - "height": 329, + "height": 328, "id": "structuredOutputParser_0", "position": { - "x": 498.2326128526694, - "y": 566.5473204649535 + "x": 170.3869571939727, + "y": 343.9298288967859 }, "type": "customNode", "data": { @@ -398,11 +382,7 @@ "version": 1, "name": "structuredOutputParser", "type": "StructuredOutputParser", - "baseClasses": [ - "StructuredOutputParser", - "BaseLLMOutputParser", - "Runnable" - ], + "baseClasses": ["StructuredOutputParser", "BaseLLMOutputParser", "Runnable"], "category": "Output Parsers", "description": "Parse the output of an LLM call into a given (JSON) structure.", "inputParams": [ @@ -415,20 +395,61 @@ "id": "structuredOutputParser_0-input-autofixParser-boolean" }, { - "label": "Example JSON", - "name": "exampleJson", - "type": "string", - "description": "Example JSON structure for LLM to return", - "rows": 10, - "default": "{\"answer\": \"the answer\", \"followupQuestions\": [\"question1\", \"question2\"]}", + "label": "JSON Structure", + "name": "jsonStructure", + "type": "datagrid", + "description": "JSON structure for LLM to return", + "datagrid": [ + { + "field": "property", + "headerName": "Property", + "editable": true + }, + { + "field": "type", + "headerName": "Type", + "type": "singleSelect", + "valueOptions": ["string", "number", "boolean"], + "editable": true + }, + { + "field": "description", + "headerName": "Description", + "editable": true, + "flex": 1 + } + ], + "default": [ + { + "property": "answer", + "type": "string", + "description": "answer to the user's question" + }, + { + "property": "source", + "type": "string", + "description": "sources used to answer the question, should be websites" + } + ], "additionalParams": true, - "id": "structuredOutputParser_0-input-exampleJson-string" + "id": "structuredOutputParser_0-input-jsonStructure-datagrid" } ], "inputAnchors": [], "inputs": { "autofixParser": true, - "exampleJson": "{\"answer\": \"the answer\", \"followupQuestions\": [\"question1\", \"question2\"]}" + "jsonStructure": [ + { + "property": "answer", + "type": "string", + "description": "answer to the user's question" + }, + { + "property": "source", + "type": "string", + "description": "sources used to answer the question, should be websites" + } + ] }, "outputAnchors": [ { @@ -442,11 +463,11 @@ "selected": false }, "selected": false, - "dragging": false, "positionAbsolute": { - "x": 498.2326128526694, - "y": 566.5473204649535 - } + "x": 170.3869571939727, + "y": 343.9298288967859 + }, + "dragging": false } ], "edges": [ @@ -478,7 +499,10 @@ "target": "llmChain_0", "targetHandle": "llmChain_0-input-outputParser-BaseLLMOutputParser", "type": "buttonedge", - "id": "structuredOutputParser_0-structuredOutputParser_0-output-structuredOutputParser-StructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser" + "id": "structuredOutputParser_0-structuredOutputParser_0-output-structuredOutputParser-StructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser", + "data": { + "label": "" + } } ] -} \ No newline at end of file +} From 113415e2c91a24b9d8a46d66f4106430c8fc8338 Mon Sep 17 00:00:00 2001 From: Kenny Vaneetvelde Date: Sat, 3 Feb 2024 21:32:00 +0100 Subject: [PATCH 390/502] remove basepath value from template --- .../chatflows/Advanced Structured Output Parser.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json index 41cbb08f..a48f5b2a 100644 --- a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json @@ -1,4 +1,6 @@ { + "description": "Return response as a JSON structure as specified by a Zod schema", + "badge": "NEW", "nodes": [ { "width": 300, @@ -409,7 +411,7 @@ "frequencyPenalty": "", "presencePenalty": "", "timeout": "", - "basepath": "http://localhost:8901/v1", + "basepath": "", "baseOptions": "" }, "outputAnchors": [ From 5da3e3cc3e62efbf9894f4eb46a6e3958d15ab35 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Sun, 4 Feb 2024 12:27:18 -0500 Subject: [PATCH 391/502] Adding proper TLS/SSL support to poolOptions so that the similaritySearchVectorWithScore function actually connects to Postgres via TLS/SSL when specified in the additionalConfig. --- packages/components/nodes/vectorstores/Postgres/Postgres.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 4e8bae32..375728e8 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -253,7 +253,8 @@ const similaritySearchVectorWithScore = async ( port: postgresConnectionOptions.port, user: postgresConnectionOptions.username, password: postgresConnectionOptions.password, - database: postgresConnectionOptions.database + database: postgresConnectionOptions.database, + ssl: postgresConnectionOptions.extra?.ssl } const pool = new Pool(poolOptions) const conn = await pool.connect() From 5543ef3de457c72170e66203db8a09d99a3022e4 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sun, 4 Feb 2024 14:29:43 -0500 Subject: [PATCH 392/502] Marketplace: Revamped UI --- packages/server/src/index.ts | 44 ++- packages/ui/craco.config.js | 3 +- packages/ui/src/api/marketplaces.js | 4 +- .../ui-component/table/MarketplaceTable.js | 177 +++++++++ packages/ui/src/views/marketplaces/index.js | 351 +++++++++++++----- 5 files changed, 477 insertions(+), 102 deletions(-) create mode 100644 packages/ui/src/ui-component/table/MarketplaceTable.js diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index dbb5717d..8878da94 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1223,7 +1223,6 @@ export class App { // Marketplaces // ---------------------------------------- - // Get all chatflows for marketplaces this.app.get('/api/v1/marketplaces/chatflows', async (req: Request, res: Response) => { const marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') const jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') @@ -1250,6 +1249,49 @@ export class App { return res.json(templates) }) + // Get all chatflows for marketplaces + this.app.get('/api/v1/marketplaces/templates', async (req: Request, res: Response) => { + let marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') + let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') + let templates: any[] = [] + jsonsInDir.forEach((file, index) => { + const filePath = path.join(__dirname, '..', 'marketplaces', 'chatflows', file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + id: index, + templateName: file.split('.json')[0], + flowData: fileData.toString(), + badge: fileDataObj?.badge, + type: 'Chatflow', + description: fileDataObj?.description || '' + } + templates.push(template) + }) + + marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools') + jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') + jsonsInDir.forEach((file, index) => { + const filePath = path.join(__dirname, '..', 'marketplaces', 'tools', file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + ...fileDataObj, + id: index, + type: 'Tool', + templateName: file.split('.json')[0] + } + templates.push(template) + }) + const FlowiseDocsQnA = templates.find((tmp) => tmp.name === 'Flowise Docs QnA') + const FlowiseDocsQnAIndex = templates.findIndex((tmp) => tmp.name === 'Flowise Docs QnA') + if (FlowiseDocsQnA && FlowiseDocsQnAIndex > 0) { + templates.splice(FlowiseDocsQnAIndex, 1) + templates.unshift(FlowiseDocsQnA) + } + return res.json(templates.sort((a, b) => a.templateName.localeCompare(b.templateName))) + }) + // Get all tools for marketplaces this.app.get('/api/v1/marketplaces/tools', async (req: Request, res: Response) => { const marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools') diff --git a/packages/ui/craco.config.js b/packages/ui/craco.config.js index 142305e0..093e5ece 100644 --- a/packages/ui/craco.config.js +++ b/packages/ui/craco.config.js @@ -10,7 +10,8 @@ module.exports = { } } ] - } + }, + ignoreWarnings: [/Failed to parse source map/] // Ignore warnings about source maps } } } diff --git a/packages/ui/src/api/marketplaces.js b/packages/ui/src/api/marketplaces.js index 3fd4ae87..bba914a7 100644 --- a/packages/ui/src/api/marketplaces.js +++ b/packages/ui/src/api/marketplaces.js @@ -2,8 +2,10 @@ import client from './client' const getAllChatflowsMarketplaces = () => client.get('/marketplaces/chatflows') const getAllToolsMarketplaces = () => client.get('/marketplaces/tools') +const getAllTemplatesFromMarketplaces = () => client.get('/marketplaces/templates') export default { getAllChatflowsMarketplaces, - getAllToolsMarketplaces + getAllToolsMarketplaces, + getAllTemplatesFromMarketplaces } diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.js b/packages/ui/src/ui-component/table/MarketplaceTable.js new file mode 100644 index 00000000..135d19e9 --- /dev/null +++ b/packages/ui/src/ui-component/table/MarketplaceTable.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types' +import { useNavigate } from 'react-router-dom' +import { styled } from '@mui/material/styles' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Paper from '@mui/material/Paper' +import Chip from '@mui/material/Chip' +import { Button, Typography } from '@mui/material' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, filterByType }) => { + const navigate = useNavigate() + const openTemplate = (selectedTemplate) => { + if (selectedTemplate.flowData) { + goToCanvas(selectedTemplate) + } else { + goToTool(selectedTemplate) + } + } + + const goToTool = (selectedTool) => { + const dialogProp = { + title: selectedTool.templateName, + type: 'TEMPLATE', + data: selectedTool + } + setToolDialogProps(dialogProp) + setShowToolDialog(true) + } + + const goToCanvas = (selectedChatflow) => { + navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) + } + + return ( + <> + + + + + + Name + + + Type + + + Description + + + Nodes + + +   + + + + + {data + .filter(filterByBadge) + .filter(filterByType) + .filter(filterFunction) + .map((row, index) => ( + + + + + + + + {row.type} + + + + {row.description || ''} + + + + {row.type === 'Chatflow' && images[row.id] && ( +
    + {images[row.id] + .slice(0, images[row.id].length > 5 ? 5 : images[row.id].length) + .map((img) => ( +
    + +
    + ))} + {images[row.id].length > 5 && ( + + + {images[row.id].length - 5} More + + )} +
    + )} +
    + + + {row.badge && + row.badge + .split(';') + .map((tag, index) => ( + + ))} + + +
    + ))} +
    +
    +
    + + ) +} + +MarketplaceTable.propTypes = { + data: PropTypes.array, + images: PropTypes.object, + filterFunction: PropTypes.func, + filterByBadge: PropTypes.func, + filterByType: PropTypes.func +} diff --git a/packages/ui/src/views/marketplaces/index.js b/packages/ui/src/views/marketplaces/index.js index 665341c4..ac018663 100644 --- a/packages/ui/src/views/marketplaces/index.js +++ b/packages/ui/src/views/marketplaces/index.js @@ -4,9 +4,25 @@ import { useSelector } from 'react-redux' import PropTypes from 'prop-types' // material-ui -import { Grid, Box, Stack, Tabs, Tab, Badge } from '@mui/material' +import { + Grid, + Box, + Stack, + Badge, + Toolbar, + TextField, + InputAdornment, + ButtonGroup, + ToggleButton, + InputLabel, + FormControl, + Select, + OutlinedInput, + Checkbox, + ListItemText +} from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconHierarchy, IconTool } from '@tabler/icons' +import { IconLayoutGrid, IconList, IconSearch } from '@tabler/icons' // project imports import MainCard from 'ui-component/cards/MainCard' @@ -23,6 +39,10 @@ import useApi from 'hooks/useApi' // const import { baseURL } from 'store/constant' +import * as React from 'react' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { MarketplaceTable } from '../../ui-component/table/MarketplaceTable' +import MenuItem from '@mui/material/MenuItem' function TabPanel(props) { const { children, value, index, ...other } = props @@ -45,6 +65,18 @@ TabPanel.propTypes = { value: PropTypes.number.isRequired } +const ITEM_HEIGHT = 48 +const ITEM_PADDING_TOP = 8 +const badges = ['POPULAR', 'NEW'] +const types = ['Chatflow', 'Tool'] +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } +} // ==============================|| Marketplace ||============================== // const Marketplace = () => { @@ -53,16 +85,62 @@ const Marketplace = () => { const theme = useTheme() const customization = useSelector((state) => state.customization) - const [isChatflowsLoading, setChatflowsLoading] = useState(true) - const [isToolsLoading, setToolsLoading] = useState(true) + const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) - const tabItems = ['Chatflows', 'Tools'] - const [value, setValue] = useState(0) + const [showToolDialog, setShowToolDialog] = useState(false) const [toolDialogProps, setToolDialogProps] = useState({}) - const getAllChatflowsMarketplacesApi = useApi(marketplacesApi.getAllChatflowsMarketplaces) - const getAllToolsMarketplacesApi = useApi(marketplacesApi.getAllToolsMarketplaces) + const getAllTemplatesMarketplacesApi = useApi(marketplacesApi.getAllTemplatesFromMarketplaces) + + const [view, setView] = React.useState(localStorage.getItem('mpDisplayStyle') || 'card') + const [search, setSearch] = useState('') + + const [badgeFilter, setBadgeFilter] = useState([]) + const [typeFilter, setTypeFilter] = useState([]) + + const handleBadgeFilterChange = (event) => { + const { + target: { value } + } = event + setBadgeFilter( + // On autofill we get a stringified value. + typeof value === 'string' ? value.split(',') : value + ) + } + const handleTypeFilterChange = (event) => { + const { + target: { value } + } = event + setTypeFilter( + // On autofill we get a stringified value. + typeof value === 'string' ? value.split(',') : value + ) + } + + const handleViewChange = (event, nextView) => { + localStorage.setItem('mpDisplayStyle', nextView) + setView(nextView) + } + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterFlows(data) { + return ( + data.templateName.toLowerCase().indexOf(search.toLowerCase()) > -1 || + (data.description && data.description.toLowerCase().indexOf(search.toLowerCase()) > -1) + ) + } + + function filterByBadge(data) { + return badgeFilter.length > 0 ? badgeFilter.includes(data.badge) : true + } + + function filterByType(data) { + return typeFilter.length > 0 ? typeFilter.includes(data.type) : true + } const onUseTemplate = (selectedTool) => { const dialogProp = { @@ -90,39 +168,33 @@ const Marketplace = () => { navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) } - const handleChange = (event, newValue) => { - setValue(newValue) - } - useEffect(() => { - getAllChatflowsMarketplacesApi.request() - getAllToolsMarketplacesApi.request() + getAllTemplatesMarketplacesApi.request() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { - setChatflowsLoading(getAllChatflowsMarketplacesApi.loading) - }, [getAllChatflowsMarketplacesApi.loading]) + setLoading(getAllTemplatesMarketplacesApi.loading) + }, [getAllTemplatesMarketplacesApi.loading]) useEffect(() => { - setToolsLoading(getAllToolsMarketplacesApi.loading) - }, [getAllToolsMarketplacesApi.loading]) - - useEffect(() => { - if (getAllChatflowsMarketplacesApi.data) { + if (getAllTemplatesMarketplacesApi.data) { try { - const chatflows = getAllChatflowsMarketplacesApi.data + const flows = getAllTemplatesMarketplacesApi.data + const images = {} - for (let i = 0; i < chatflows.length; i += 1) { - const flowDataStr = chatflows[i].flowData - const flowData = JSON.parse(flowDataStr) - const nodes = flowData.nodes || [] - images[chatflows[i].id] = [] - for (let j = 0; j < nodes.length; j += 1) { - const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}` - if (!images[chatflows[i].id].includes(imageSrc)) { - images[chatflows[i].id].push(imageSrc) + for (let i = 0; i < flows.length; i += 1) { + if (flows[i].flowData) { + const flowDataStr = flows[i].flowData + const flowData = JSON.parse(flowDataStr) + const nodes = flowData.nodes || [] + images[flows[i].id] = [] + for (let j = 0; j < nodes.length; j += 1) { + const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}` + if (!images[flows[i].id].includes(imageSrc)) { + images[flows[i].id].push(imageSrc) + } } } } @@ -131,80 +203,161 @@ const Marketplace = () => { console.error(e) } } - }, [getAllChatflowsMarketplacesApi.data]) + }, [getAllTemplatesMarketplacesApi.data]) return ( <> - -

    Marketplace

    -
    - - {tabItems.map((item, index) => ( - : } - iconPosition='start' - label={{item}} + + +

    Marketplace

    + + + + ) + }} /> - ))} -
    - {tabItems.map((item, index) => ( - - {item === 'Chatflows' && ( - - {!isChatflowsLoading && - getAllChatflowsMarketplacesApi.data && - getAllChatflowsMarketplacesApi.data.map((data, index) => ( - - {data.badge && ( - + + + Type + + + + + + Tag + + + + + + + + + + + + + + + + + + + {!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && ( + <> + + {getAllTemplatesMarketplacesApi.data + .filter(filterByBadge) + .filter(filterByType) + .filter(filterFlows) + .map((data, index) => ( + + {data.badge && ( + + {data.type === 'Chatflow' && ( goToCanvas(data)} data={data} images={images[data.id]} /> - - )} - {!data.badge && ( - goToCanvas(data)} data={data} images={images[data.id]} /> - )} - - ))} - - )} - {item === 'Tools' && ( - - {!isToolsLoading && - getAllToolsMarketplacesApi.data && - getAllToolsMarketplacesApi.data.map((data, index) => ( - - {data.badge && ( - - goToTool(data)} /> - - )} - {!data.badge && goToTool(data)} />} - - ))} - - )} - - ))} - {((!isChatflowsLoading && (!getAllChatflowsMarketplacesApi.data || getAllChatflowsMarketplacesApi.data.length === 0)) || - (!isToolsLoading && (!getAllToolsMarketplacesApi.data || getAllToolsMarketplacesApi.data.length === 0))) && ( + )} + {data.type === 'Tool' && goToTool(data)} />} + + )} + {!data.badge && data.type === 'Chatflow' && ( + goToCanvas(data)} data={data} images={images[data.id]} /> + )} + {!data.badge && data.type === 'Tool' && goToTool(data)} />} + + ))} + + + )} + {!isLoading && view === 'list' && getAllTemplatesMarketplacesApi.data && ( + + )} + + {!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && ( Date: Sun, 4 Feb 2024 15:49:34 -0500 Subject: [PATCH 393/502] Marketplace: removing unused server API --- packages/server/src/index.ts | 47 +----------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8878da94..e69e15ba 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1223,33 +1223,7 @@ export class App { // Marketplaces // ---------------------------------------- - this.app.get('/api/v1/marketplaces/chatflows', async (req: Request, res: Response) => { - const marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') - const jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') - const templates: any[] = [] - jsonsInDir.forEach((file, index) => { - const filePath = path.join(__dirname, '..', 'marketplaces', 'chatflows', file) - const fileData = fs.readFileSync(filePath) - const fileDataObj = JSON.parse(fileData.toString()) - const template = { - id: index, - name: file.split('.json')[0], - flowData: fileData.toString(), - badge: fileDataObj?.badge, - description: fileDataObj?.description || '' - } - templates.push(template) - }) - const FlowiseDocsQnA = templates.find((tmp) => tmp.name === 'Flowise Docs QnA') - const FlowiseDocsQnAIndex = templates.findIndex((tmp) => tmp.name === 'Flowise Docs QnA') - if (FlowiseDocsQnA && FlowiseDocsQnAIndex > 0) { - templates.splice(FlowiseDocsQnAIndex, 1) - templates.unshift(FlowiseDocsQnA) - } - return res.json(templates) - }) - - // Get all chatflows for marketplaces + // Get all templates for marketplaces this.app.get('/api/v1/marketplaces/templates', async (req: Request, res: Response) => { let marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') @@ -1292,25 +1266,6 @@ export class App { return res.json(templates.sort((a, b) => a.templateName.localeCompare(b.templateName))) }) - // Get all tools for marketplaces - this.app.get('/api/v1/marketplaces/tools', async (req: Request, res: Response) => { - const marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools') - const jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') - const templates: any[] = [] - jsonsInDir.forEach((file, index) => { - const filePath = path.join(__dirname, '..', 'marketplaces', 'tools', file) - const fileData = fs.readFileSync(filePath) - const fileDataObj = JSON.parse(fileData.toString()) - const template = { - ...fileDataObj, - id: index, - templateName: file.split('.json')[0] - } - templates.push(template) - }) - return res.json(templates) - }) - // ---------------------------------------- // Variables // ---------------------------------------- From 011a0a75c332257278c8ae5d8b42d5a596f3060f Mon Sep 17 00:00:00 2001 From: Ilyes Tascou Date: Mon, 5 Feb 2024 17:20:05 +0100 Subject: [PATCH 394/502] add kotlin files to folder-loader --- packages/components/nodes/documentloaders/Folder/Folder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/nodes/documentloaders/Folder/Folder.ts b/packages/components/nodes/documentloaders/Folder/Folder.ts index f5d0c640..f8346e3c 100644 --- a/packages/components/nodes/documentloaders/Folder/Folder.ts +++ b/packages/components/nodes/documentloaders/Folder/Folder.ts @@ -70,6 +70,7 @@ class Folder_DocumentLoaders implements INode { '.css': (path) => new TextLoader(path), '.go': (path) => new TextLoader(path), // Go '.h': (path) => new TextLoader(path), // C++ Header files + '.kt': (path) => new TextLoader(path), // Kotlin '.java': (path) => new TextLoader(path), // Java '.js': (path) => new TextLoader(path), // JavaScript '.less': (path) => new TextLoader(path), // Less files From dcacb02a4758b07d161d9a14c3fbae2a32e871aa Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Feb 2024 03:01:47 +0800 Subject: [PATCH 395/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.6.0?= =?UTF-8?q?=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index bcb746b0..62ffba94 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.5.3", + "version": "1.6.0", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 9189b7013127e6b533376ddad596e831a1cdb6eb Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Feb 2024 03:02:16 +0800 Subject: [PATCH 396/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.5.0=20relea?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 68d78c95..eb3bebda 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.9", + "version": "1.5.0", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From 7faaf13ccc3705030a4dd87d05cd60e9f69772c3 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Feb 2024 03:02:52 +0800 Subject: [PATCH 397/502] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.5.0=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 451f7855..5f5f5812 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.12", + "version": "1.5.0", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index c7ed13ac..0d7dea77 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.12", + "version": "1.5.0", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From 8990b78e104d20d511335844046b9fc81e7d8c94 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 5 Feb 2024 15:09:44 -0500 Subject: [PATCH 398/502] Marketplace : Added categories to chatflows --- .../chatflows/API Agent OpenAI.json | 1 + .../marketplaces/chatflows/API Agent.json | 1 + .../marketplaces/chatflows/Antonym.json | 1 + .../marketplaces/chatflows/AutoGPT.json | 1 + .../marketplaces/chatflows/BabyAGI.json | 1 + .../marketplaces/chatflows/CSV Agent.json | 1 + .../chatflows/Chat with a Podcast.json | 1 + .../marketplaces/chatflows/ChatGPTPlugin.json | 1 + .../marketplaces/chatflows/Claude LLM.json | 1 + .../chatflows/Context Chat Engine.json | 1 + .../chatflows/Conversational Agent.json | 1 + .../Conversational Retrieval Agent.json | 1 + .../Conversational Retrieval QA Chain.json | 1 + .../chatflows/Flowise Docs QnA.json | 1 + .../chatflows/HuggingFace LLM Chain.json | 1 + .../server/marketplaces/chatflows/IfElse.json | 1 + .../chatflows/Image Generation.json | 1 + .../chatflows/Input Moderation.json | 1 + .../chatflows/List Output Parser.json | 1 + .../marketplaces/chatflows/Local QnA.json | 1 + .../chatflows/Long Term Memory.json | 1 + .../chatflows/Metadata Filter.json | 1 + .../chatflows/Multi Prompt Chain.json | 1 + .../chatflows/Multi Retrieval QA Chain.json | 1 + .../chatflows/Multiple VectorDB.json | 1 + .../marketplaces/chatflows/OpenAI Agent.json | 1 + .../chatflows/OpenAI Assistant.json | 1 + .../Prompt Chaining with VectorStore.json | 1 + .../chatflows/Prompt Chaining.json | 1 + .../marketplaces/chatflows/Query Engine.json | 1 + .../marketplaces/chatflows/ReAct Agent.json | 1 + .../marketplaces/chatflows/Replicate LLM.json | 1 + .../marketplaces/chatflows/SQL DB Chain.json | 1 + .../marketplaces/chatflows/SQL Prompt.json | 1 + .../chatflows/Simple Chat Engine.json | 1 + .../chatflows/Simple Conversation Chain.json | 1 + .../chatflows/Simple LLM Chain.json | 1 + .../chatflows/Structured Output Parser.json | 1 + .../chatflows/SubQuestion Query Engine.json | 1 + .../marketplaces/chatflows/Translator.json | 1 + .../chatflows/Vectara RAG Chain.json | 2 + .../marketplaces/chatflows/WebBrowser.json | 1 + .../marketplaces/chatflows/WebPage QnA.json | 1 + packages/server/src/index.ts | 2 + .../ui-component/table/MarketplaceTable.js | 58 +++++++------------ packages/ui/src/views/marketplaces/index.js | 1 + 46 files changed, 67 insertions(+), 38 deletions(-) diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index 87f6d6d2..c0497a29 100644 --- a/packages/server/marketplaces/chatflows/API Agent OpenAI.json +++ b/packages/server/marketplaces/chatflows/API Agent OpenAI.json @@ -1,5 +1,6 @@ { "description": "Use OpenAI Function Agent and Chain to automatically decide which API to call, generating url and body request from conversation", + "categories": "Buffer Memory,ChainTool,API Chain,ChatOpenAI,OpenAI Function Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index af99be9d..32469df9 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -1,5 +1,6 @@ { "description": "Given API docs, agent automatically decide which API to call, generating url and body request from conversation", + "categories": "Buffer Memory,ChainTool,API Chain,ChatOpenAI,Conversational Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Antonym.json b/packages/server/marketplaces/chatflows/Antonym.json index ef997feb..1240bf30 100644 --- a/packages/server/marketplaces/chatflows/Antonym.json +++ b/packages/server/marketplaces/chatflows/Antonym.json @@ -1,5 +1,6 @@ { "description": "Output antonym of given user input using few-shot prompt template built with examples", + "categories": "Few Shot Prompt,ChatOpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/AutoGPT.json b/packages/server/marketplaces/chatflows/AutoGPT.json index 4edbf823..e03a7408 100644 --- a/packages/server/marketplaces/chatflows/AutoGPT.json +++ b/packages/server/marketplaces/chatflows/AutoGPT.json @@ -1,5 +1,6 @@ { "description": "Use AutoGPT - Autonomous agent with chain of thoughts for self-guided task completion", + "categories": "AutoGPT,SERP Tool,File Read/Write,ChatOpenAI,Pinecone,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/BabyAGI.json b/packages/server/marketplaces/chatflows/BabyAGI.json index 3137d511..52198b90 100644 --- a/packages/server/marketplaces/chatflows/BabyAGI.json +++ b/packages/server/marketplaces/chatflows/BabyAGI.json @@ -1,5 +1,6 @@ { "description": "Use BabyAGI to create tasks and reprioritize for a given objective", + "categories": "BabyAGI,ChatOpenAI,Pinecone,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/CSV Agent.json b/packages/server/marketplaces/chatflows/CSV Agent.json index e16377d2..d23e93ad 100644 --- a/packages/server/marketplaces/chatflows/CSV Agent.json +++ b/packages/server/marketplaces/chatflows/CSV Agent.json @@ -1,5 +1,6 @@ { "description": "Analyse and summarize CSV data", + "categories": "CSV Agent,ChatOpenAI,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Chat with a Podcast.json b/packages/server/marketplaces/chatflows/Chat with a Podcast.json index f8d8d26c..c1f216e6 100644 --- a/packages/server/marketplaces/chatflows/Chat with a Podcast.json +++ b/packages/server/marketplaces/chatflows/Chat with a Podcast.json @@ -1,5 +1,6 @@ { "description": "Engage with data sources such as YouTube Transcripts, Google, and more through intelligent Q&A interactions", + "categories": "Memory Vector Store,SearchAPI,ChatOpenAI,Conversational Retrieval QA Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/ChatGPTPlugin.json b/packages/server/marketplaces/chatflows/ChatGPTPlugin.json index 12bea993..d9390f57 100644 --- a/packages/server/marketplaces/chatflows/ChatGPTPlugin.json +++ b/packages/server/marketplaces/chatflows/ChatGPTPlugin.json @@ -1,5 +1,6 @@ { "description": "Use ChatGPT Plugins within LangChain abstractions with GET and POST Tools", + "categories": "ChatGPT Plugin,HTTP GET/POST,ChatOpenAI,MRKL Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 7b32de48..39d1e6c6 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -1,5 +1,6 @@ { "description": "Use Anthropic Claude with 200k context window to ingest whole document for QnA", + "categories": "Buffer Memory,Prompt Template,Conversation Chain,ChatAnthropic,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Context Chat Engine.json b/packages/server/marketplaces/chatflows/Context Chat Engine.json index 475c6b3a..26efcb1c 100644 --- a/packages/server/marketplaces/chatflows/Context Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Context Chat Engine.json @@ -1,5 +1,6 @@ { "description": "Answer question based on retrieved documents (context) with built-in memory to remember conversation using LlamaIndex", + "categories": "Text File,Prompt Template,ChatOpenAI,Conversation Chain,Pinecone,LlamaIndex,Redis", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 031a29c0..419e9a79 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -1,5 +1,6 @@ { "description": "A conversational agent for a chat model which utilize chat specific prompts", + "categories": "Calculator Tool,Buffer Memory,SerpAPI,ChatOpenAI,Conversational Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 40c689f5..37dd3759 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -1,5 +1,6 @@ { "description": "Agent optimized for vector retrieval during conversation and answering questions based on previous dialogue.", + "categories": "Retriever Tool,Buffer Memory,ChatOpenAI,Conversational Retrieval Agent, Pinecone,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index e73a9d28..61bc6e39 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -1,5 +1,6 @@ { "description": "Text file QnA using conversational retrieval QA chain", + "categories": "TextFile,ChatOpenAI,Conversational Retrieval QA Chain,Pinecone,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json index 6975fc68..6eca2944 100644 --- a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json +++ b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json @@ -1,5 +1,6 @@ { "description": "Flowise Docs Github QnA using conversational retrieval QA chain", + "categories": "Memory Vector Store,Github Loader,ChatOpenAI,Conversational Retrieval QA Chain,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json b/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json index 93009574..f87bf65a 100644 --- a/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json +++ b/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json @@ -1,5 +1,6 @@ { "description": "Simple LLM Chain using HuggingFace Inference API on falcon-7b-instruct model", + "categories": "HuggingFace,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/IfElse.json b/packages/server/marketplaces/chatflows/IfElse.json index f3fddebf..c856eac0 100644 --- a/packages/server/marketplaces/chatflows/IfElse.json +++ b/packages/server/marketplaces/chatflows/IfElse.json @@ -1,5 +1,6 @@ { "description": "Split flows based on if else condition", + "categories": "IfElse Function,ChatOpenAI,OpenAI,LLM Chain,Langchain", "badge": "new", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Image Generation.json b/packages/server/marketplaces/chatflows/Image Generation.json index 7dafcedf..8b07ae86 100644 --- a/packages/server/marketplaces/chatflows/Image Generation.json +++ b/packages/server/marketplaces/chatflows/Image Generation.json @@ -1,6 +1,7 @@ { "description": "Generate image using Replicate Stability text-to-image generative AI model", "badge": "NEW", + "categories": "Replicate,ChatOpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Input Moderation.json b/packages/server/marketplaces/chatflows/Input Moderation.json index ed823a21..cd7f2cb5 100644 --- a/packages/server/marketplaces/chatflows/Input Moderation.json +++ b/packages/server/marketplaces/chatflows/Input Moderation.json @@ -1,6 +1,7 @@ { "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", "badge": "NEW", + "categories": "Moderation,ChatOpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/List Output Parser.json b/packages/server/marketplaces/chatflows/List Output Parser.json index eaf56dff..a67fb72b 100644 --- a/packages/server/marketplaces/chatflows/List Output Parser.json +++ b/packages/server/marketplaces/chatflows/List Output Parser.json @@ -1,6 +1,7 @@ { "description": "Return response as a list (array) instead of a string/text", "badge": "NEW", + "categories": "CSV Output Parser,ChatOpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index 6f78cb05..11deae15 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -1,6 +1,7 @@ { "description": "QnA chain using Ollama local LLM, LocalAI embedding model, and Faiss local vector store", "badge": "POPULAR", + "categories": "Text File,ChatOllama,Conversational Retrieval QA Chain,Faiss,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index 1b3e48e1..ba2d2330 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -1,5 +1,6 @@ { "description": "Use long term memory like Zep to differentiate conversations between users with sessionId", + "categories": "ChatOpenAI,Conversational Retrieval QA Chain,Zep Memory,Qdrant,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index ef928854..13ca8745 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -1,5 +1,6 @@ { "description": "Upsert multiple files with metadata and filter by it using conversational retrieval QA chain", + "categories": "Text File,PDF File,ChatOpenAI,Conversational Retrieval QA Chain,Pinecone,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Multi Prompt Chain.json b/packages/server/marketplaces/chatflows/Multi Prompt Chain.json index 314e24a6..89935fcd 100644 --- a/packages/server/marketplaces/chatflows/Multi Prompt Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Prompt Chain.json @@ -1,5 +1,6 @@ { "description": "A chain that automatically picks an appropriate prompt from multiple prompts", + "categories": "ChatOpenAI,Multi Prompt Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json index 8c9e8537..63a02edb 100644 --- a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json @@ -1,5 +1,6 @@ { "description": "A chain that automatically picks an appropriate retriever from multiple different vector databases", + "categories": "ChatOpenAI,Multi Retrieval QA Chain,Pinecone,Chroma,Supabase,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index e5a16caa..7ffc040a 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -1,5 +1,6 @@ { "description": "Use the agent to choose between multiple different vector databases, with the ability to use other tools", + "categories": "Buffer Memory,ChatOpenAI,Chain Tool,Retrieval QA Chain,Redis,Faiss,Conversational Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index e3e80dcc..c4cf11f2 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Agent.json +++ b/packages/server/marketplaces/chatflows/OpenAI Agent.json @@ -1,5 +1,6 @@ { "description": "An agent that uses OpenAI's Function Calling functionality to pick the tool and args to call", + "categories": "Buffer Memory,Custom Tool, SerpAPI,OpenAI Function,Calculator Tool,ChatOpenAI,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/OpenAI Assistant.json b/packages/server/marketplaces/chatflows/OpenAI Assistant.json index e9311c97..f1595fd6 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Assistant.json +++ b/packages/server/marketplaces/chatflows/OpenAI Assistant.json @@ -1,5 +1,6 @@ { "description": "OpenAI Assistant that has instructions and can leverage models, tools, and knowledge to respond to user queries", + "categories": "Custom Tool, SerpAPI,OpenAI Assistant,Calculator Tool,Langchain", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json index c2060e79..3b8507b9 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json @@ -1,5 +1,6 @@ { "description": "Use chat history to rephrase user question, and answer the rephrased question using retrieved docs from vector store", + "categories": "ChatOpenAI,LLM Chain,SingleStore,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining.json b/packages/server/marketplaces/chatflows/Prompt Chaining.json index 267d8222..3ed5f95c 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining.json @@ -1,5 +1,6 @@ { "description": "Use output from a chain as prompt for another chain", + "categories": "Custom Tool,OpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Query Engine.json b/packages/server/marketplaces/chatflows/Query Engine.json index 82553333..5697735b 100644 --- a/packages/server/marketplaces/chatflows/Query Engine.json +++ b/packages/server/marketplaces/chatflows/Query Engine.json @@ -1,5 +1,6 @@ { "description": "Stateless query engine designed to answer question over your data using LlamaIndex", + "categories": "ChatAnthropic,Compact and Refine,Pinecone,LlamaIndex", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/ReAct Agent.json b/packages/server/marketplaces/chatflows/ReAct Agent.json index e4a7fab8..9831bcf6 100644 --- a/packages/server/marketplaces/chatflows/ReAct Agent.json +++ b/packages/server/marketplaces/chatflows/ReAct Agent.json @@ -1,5 +1,6 @@ { "description": "An agent that uses ReAct logic to decide what action to take", + "categories": "Calculator Tool,SerpAPI,ChatOpenAI,MRKL Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Replicate LLM.json b/packages/server/marketplaces/chatflows/Replicate LLM.json index 832e85c7..ef8ab7f2 100644 --- a/packages/server/marketplaces/chatflows/Replicate LLM.json +++ b/packages/server/marketplaces/chatflows/Replicate LLM.json @@ -1,5 +1,6 @@ { "description": "Use Replicate API that runs Llama 13b v2 model with LLMChain", + "categories": "Replicate,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/SQL DB Chain.json b/packages/server/marketplaces/chatflows/SQL DB Chain.json index 92e42178..3c143a33 100644 --- a/packages/server/marketplaces/chatflows/SQL DB Chain.json +++ b/packages/server/marketplaces/chatflows/SQL DB Chain.json @@ -1,5 +1,6 @@ { "description": "Answer questions over a SQL database", + "categories": "ChatOpenAI,Sql Database Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/SQL Prompt.json b/packages/server/marketplaces/chatflows/SQL Prompt.json index 406c2e52..e79a95d0 100644 --- a/packages/server/marketplaces/chatflows/SQL Prompt.json +++ b/packages/server/marketplaces/chatflows/SQL Prompt.json @@ -1,5 +1,6 @@ { "description": "Manually construct prompts to query a SQL database", + "categories": "IfElse Function,Variable Set/Get,Custom JS Function,ChatOpenAI,LLM Chain,Langchain", "badge": "new", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Simple Chat Engine.json b/packages/server/marketplaces/chatflows/Simple Chat Engine.json index 630b6833..5510873d 100644 --- a/packages/server/marketplaces/chatflows/Simple Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Simple Chat Engine.json @@ -1,5 +1,6 @@ { "description": "Simple chat engine to handle back and forth conversations using LlamaIndex", + "categories": "BufferMemory,AzureChatOpenAI,LlamaIndex", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index 1ffbee44..e6c934ac 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -1,5 +1,6 @@ { "description": "Basic example of Conversation Chain with built-in memory - works exactly like ChatGPT", + "categories": "Buffer Memory,ChatOpenAI,Conversation Chain,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Simple LLM Chain.json b/packages/server/marketplaces/chatflows/Simple LLM Chain.json index f2e3a4a2..12298441 100644 --- a/packages/server/marketplaces/chatflows/Simple LLM Chain.json +++ b/packages/server/marketplaces/chatflows/Simple LLM Chain.json @@ -1,5 +1,6 @@ { "description": "Basic example of stateless (no memory) LLM Chain with a Prompt Template and LLM Model", + "categories": "OpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Structured Output Parser.json b/packages/server/marketplaces/chatflows/Structured Output Parser.json index 92336443..20385a4b 100644 --- a/packages/server/marketplaces/chatflows/Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Structured Output Parser.json @@ -1,5 +1,6 @@ { "description": "Return response as a specified JSON structure instead of a string/text", + "categories": "Structured Output Parser,ChatOpenAI,LLM Chain,Langchain", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json index f14607da..2042625b 100644 --- a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json +++ b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json @@ -1,5 +1,6 @@ { "description": "Breaks down query into sub questions for each relevant data source, then combine into final response", + "categories": "Sub Question Query Engine,Sticky Note,QueryEngine Tool,Compact and Refine,ChatOpenAI,Pinecone,LlamaIndex", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Translator.json b/packages/server/marketplaces/chatflows/Translator.json index 0bf49252..118457ef 100644 --- a/packages/server/marketplaces/chatflows/Translator.json +++ b/packages/server/marketplaces/chatflows/Translator.json @@ -1,5 +1,6 @@ { "description": "Language translation using LLM Chain with a Chat Prompt Template and Chat Model", + "categories": "Chat Prompt Template,ChatOpenAI,LLM Chain,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Vectara RAG Chain.json b/packages/server/marketplaces/chatflows/Vectara RAG Chain.json index d3bb5bf8..82e341c4 100644 --- a/packages/server/marketplaces/chatflows/Vectara RAG Chain.json +++ b/packages/server/marketplaces/chatflows/Vectara RAG Chain.json @@ -1,4 +1,6 @@ { + "description": "QA chain for Vectara", + "categories": "Vectara QA Chain,Vectara,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 2376e29e..75e3cf0a 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -1,5 +1,6 @@ { "description": "Conversational Agent with ability to visit a website and extract information", + "categories": "Buffer Memory,Web Browser,ChatOpenAI,Conversational Agent,Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index a5a53233..089d195d 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -1,5 +1,6 @@ { "description": "Scrape web pages for QnA with long term memory Motorhead and return source documents", + "categories": "HtmlToMarkdown,Cheerio Web Scraper,ChatOpenAI,Redis,Pinecone,Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index e69e15ba..a65fabf3 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1237,6 +1237,7 @@ export class App { templateName: file.split('.json')[0], flowData: fileData.toString(), badge: fileDataObj?.badge, + categories: fileDataObj?.categories, type: 'Chatflow', description: fileDataObj?.description || '' } @@ -1253,6 +1254,7 @@ export class App { ...fileDataObj, id: index, type: 'Tool', + categories: '', templateName: file.split('.json')[0] } templates.push(template) diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.js b/packages/ui/src/ui-component/table/MarketplaceTable.js index 135d19e9..714f10b0 100644 --- a/packages/ui/src/ui-component/table/MarketplaceTable.js +++ b/packages/ui/src/ui-component/table/MarketplaceTable.js @@ -103,45 +103,27 @@ export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, - {row.type === 'Chatflow' && images[row.id] && ( -
    - {images[row.id] - .slice(0, images[row.id].length > 5 ? 5 : images[row.id].length) - .map((img) => ( -
    - -
    +
    + {row.categories && + row.categories + .split(',') + .map((tag, index) => ( + ))} - {images[row.id].length > 5 && ( - - + {images[row.id].length - 5} More - - )} -
    - )} +
    diff --git a/packages/ui/src/views/marketplaces/index.js b/packages/ui/src/views/marketplaces/index.js index ac018663..f2055da6 100644 --- a/packages/ui/src/views/marketplaces/index.js +++ b/packages/ui/src/views/marketplaces/index.js @@ -129,6 +129,7 @@ const Marketplace = () => { function filterFlows(data) { return ( + data.categories?.toLowerCase().indexOf(search.toLowerCase()) > -1 || data.templateName.toLowerCase().indexOf(search.toLowerCase()) > -1 || (data.description && data.description.toLowerCase().indexOf(search.toLowerCase()) > -1) ) From 842d70bf0de1e8fb974bca2fa09aabd37adcfc23 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 5 Feb 2024 18:09:46 -0500 Subject: [PATCH 399/502] Marketplace: Adding filters and a collapsible panel show/hide --- .../chatflows/API Agent OpenAI.json | 1 + .../marketplaces/chatflows/API Agent.json | 1 + .../marketplaces/chatflows/Antonym.json | 1 + .../marketplaces/chatflows/AutoGPT.json | 1 + .../marketplaces/chatflows/BabyAGI.json | 1 + .../marketplaces/chatflows/CSV Agent.json | 1 + .../chatflows/Chat with a Podcast.json | 1 + .../marketplaces/chatflows/ChatGPTPlugin.json | 1 + .../marketplaces/chatflows/Claude LLM.json | 1 + .../chatflows/Context Chat Engine.json | 1 + .../chatflows/Conversational Agent.json | 1 + .../Conversational Retrieval Agent.json | 1 + .../Conversational Retrieval QA Chain.json | 1 + .../chatflows/Flowise Docs QnA.json | 1 + .../chatflows/HuggingFace LLM Chain.json | 1 + .../server/marketplaces/chatflows/IfElse.json | 1 + .../chatflows/Image Generation.json | 1 + .../chatflows/Input Moderation.json | 1 + .../chatflows/List Output Parser.json | 1 + .../marketplaces/chatflows/Local QnA.json | 1 + .../chatflows/Long Term Memory.json | 1 + .../chatflows/Metadata Filter.json | 1 + .../chatflows/Multi Prompt Chain.json | 1 + .../chatflows/Multi Retrieval QA Chain.json | 1 + .../chatflows/Multiple VectorDB.json | 1 + .../marketplaces/chatflows/OpenAI Agent.json | 1 + .../chatflows/OpenAI Assistant.json | 1 + .../Prompt Chaining with VectorStore.json | 1 + .../chatflows/Prompt Chaining.json | 1 + .../marketplaces/chatflows/Query Engine.json | 1 + .../marketplaces/chatflows/ReAct Agent.json | 1 + .../marketplaces/chatflows/Replicate LLM.json | 1 + .../marketplaces/chatflows/SQL DB Chain.json | 1 + .../marketplaces/chatflows/SQL Prompt.json | 1 + .../chatflows/Simple Chat Engine.json | 1 + .../chatflows/Simple Conversation Chain.json | 1 + .../chatflows/Simple LLM Chain.json | 1 + .../chatflows/Structured Output Parser.json | 1 + .../chatflows/SubQuestion Query Engine.json | 1 + .../marketplaces/chatflows/Translator.json | 1 + .../chatflows/Vectara RAG Chain.json | 1 + .../marketplaces/chatflows/WebBrowser.json | 3 +- .../marketplaces/chatflows/WebPage QnA.json | 1 + .../tools/Add Hubspot Contact.json | 1 + .../tools/Create Airtable Record.json | 1 + .../tools/Get Current DateTime.json | 1 + .../marketplaces/tools/Get Stock Mover.json | 1 + .../marketplaces/tools/Make Webhook.json | 1 + .../tools/Send Discord Message.json | 1 + .../tools/Send Slack Message.json | 1 + .../tools/Send Teams Message.json | 1 + .../marketplaces/tools/SendGrid Email.json | 1 + packages/server/src/index.ts | 3 + .../ui-component/table/MarketplaceTable.js | 32 +++- packages/ui/src/views/marketplaces/index.js | 170 ++++++++++++------ 55 files changed, 199 insertions(+), 60 deletions(-) diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index c0497a29..621529fc 100644 --- a/packages/server/marketplaces/chatflows/API Agent OpenAI.json +++ b/packages/server/marketplaces/chatflows/API Agent OpenAI.json @@ -1,6 +1,7 @@ { "description": "Use OpenAI Function Agent and Chain to automatically decide which API to call, generating url and body request from conversation", "categories": "Buffer Memory,ChainTool,API Chain,ChatOpenAI,OpenAI Function Agent,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index 32469df9..9d5a6c54 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -1,6 +1,7 @@ { "description": "Given API docs, agent automatically decide which API to call, generating url and body request from conversation", "categories": "Buffer Memory,ChainTool,API Chain,ChatOpenAI,Conversational Agent,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Antonym.json b/packages/server/marketplaces/chatflows/Antonym.json index 1240bf30..97c5af71 100644 --- a/packages/server/marketplaces/chatflows/Antonym.json +++ b/packages/server/marketplaces/chatflows/Antonym.json @@ -1,6 +1,7 @@ { "description": "Output antonym of given user input using few-shot prompt template built with examples", "categories": "Few Shot Prompt,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/AutoGPT.json b/packages/server/marketplaces/chatflows/AutoGPT.json index e03a7408..c0ed0807 100644 --- a/packages/server/marketplaces/chatflows/AutoGPT.json +++ b/packages/server/marketplaces/chatflows/AutoGPT.json @@ -1,6 +1,7 @@ { "description": "Use AutoGPT - Autonomous agent with chain of thoughts for self-guided task completion", "categories": "AutoGPT,SERP Tool,File Read/Write,ChatOpenAI,Pinecone,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/BabyAGI.json b/packages/server/marketplaces/chatflows/BabyAGI.json index 52198b90..14976ad3 100644 --- a/packages/server/marketplaces/chatflows/BabyAGI.json +++ b/packages/server/marketplaces/chatflows/BabyAGI.json @@ -1,6 +1,7 @@ { "description": "Use BabyAGI to create tasks and reprioritize for a given objective", "categories": "BabyAGI,ChatOpenAI,Pinecone,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/CSV Agent.json b/packages/server/marketplaces/chatflows/CSV Agent.json index d23e93ad..3439625b 100644 --- a/packages/server/marketplaces/chatflows/CSV Agent.json +++ b/packages/server/marketplaces/chatflows/CSV Agent.json @@ -1,6 +1,7 @@ { "description": "Analyse and summarize CSV data", "categories": "CSV Agent,ChatOpenAI,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Chat with a Podcast.json b/packages/server/marketplaces/chatflows/Chat with a Podcast.json index c1f216e6..c87b3f2c 100644 --- a/packages/server/marketplaces/chatflows/Chat with a Podcast.json +++ b/packages/server/marketplaces/chatflows/Chat with a Podcast.json @@ -1,6 +1,7 @@ { "description": "Engage with data sources such as YouTube Transcripts, Google, and more through intelligent Q&A interactions", "categories": "Memory Vector Store,SearchAPI,ChatOpenAI,Conversational Retrieval QA Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/ChatGPTPlugin.json b/packages/server/marketplaces/chatflows/ChatGPTPlugin.json index d9390f57..3777b637 100644 --- a/packages/server/marketplaces/chatflows/ChatGPTPlugin.json +++ b/packages/server/marketplaces/chatflows/ChatGPTPlugin.json @@ -1,6 +1,7 @@ { "description": "Use ChatGPT Plugins within LangChain abstractions with GET and POST Tools", "categories": "ChatGPT Plugin,HTTP GET/POST,ChatOpenAI,MRKL Agent,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 39d1e6c6..a0fd4e1a 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -1,6 +1,7 @@ { "description": "Use Anthropic Claude with 200k context window to ingest whole document for QnA", "categories": "Buffer Memory,Prompt Template,Conversation Chain,ChatAnthropic,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Context Chat Engine.json b/packages/server/marketplaces/chatflows/Context Chat Engine.json index 26efcb1c..15d3dade 100644 --- a/packages/server/marketplaces/chatflows/Context Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Context Chat Engine.json @@ -1,6 +1,7 @@ { "description": "Answer question based on retrieved documents (context) with built-in memory to remember conversation using LlamaIndex", "categories": "Text File,Prompt Template,ChatOpenAI,Conversation Chain,Pinecone,LlamaIndex,Redis", + "framework": "LlamaIndex", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 419e9a79..4cb736a0 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -1,6 +1,7 @@ { "description": "A conversational agent for a chat model which utilize chat specific prompts", "categories": "Calculator Tool,Buffer Memory,SerpAPI,ChatOpenAI,Conversational Agent,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index 37dd3759..a4ec6b5b 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -2,6 +2,7 @@ "description": "Agent optimized for vector retrieval during conversation and answering questions based on previous dialogue.", "categories": "Retriever Tool,Buffer Memory,ChatOpenAI,Conversational Retrieval Agent, Pinecone,Langchain", "badge": "POPULAR", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index 61bc6e39..e360141d 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -2,6 +2,7 @@ "description": "Text file QnA using conversational retrieval QA chain", "categories": "TextFile,ChatOpenAI,Conversational Retrieval QA Chain,Pinecone,Langchain", "badge": "POPULAR", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json index 6eca2944..31d65c48 100644 --- a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json +++ b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json @@ -2,6 +2,7 @@ "description": "Flowise Docs Github QnA using conversational retrieval QA chain", "categories": "Memory Vector Store,Github Loader,ChatOpenAI,Conversational Retrieval QA Chain,Langchain", "badge": "POPULAR", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json b/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json index f87bf65a..6e7154b7 100644 --- a/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json +++ b/packages/server/marketplaces/chatflows/HuggingFace LLM Chain.json @@ -1,6 +1,7 @@ { "description": "Simple LLM Chain using HuggingFace Inference API on falcon-7b-instruct model", "categories": "HuggingFace,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/IfElse.json b/packages/server/marketplaces/chatflows/IfElse.json index c856eac0..e3b66f44 100644 --- a/packages/server/marketplaces/chatflows/IfElse.json +++ b/packages/server/marketplaces/chatflows/IfElse.json @@ -1,6 +1,7 @@ { "description": "Split flows based on if else condition", "categories": "IfElse Function,ChatOpenAI,OpenAI,LLM Chain,Langchain", + "framework": "Langchain", "badge": "new", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Image Generation.json b/packages/server/marketplaces/chatflows/Image Generation.json index 8b07ae86..46cb79ec 100644 --- a/packages/server/marketplaces/chatflows/Image Generation.json +++ b/packages/server/marketplaces/chatflows/Image Generation.json @@ -2,6 +2,7 @@ "description": "Generate image using Replicate Stability text-to-image generative AI model", "badge": "NEW", "categories": "Replicate,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Input Moderation.json b/packages/server/marketplaces/chatflows/Input Moderation.json index cd7f2cb5..bd449777 100644 --- a/packages/server/marketplaces/chatflows/Input Moderation.json +++ b/packages/server/marketplaces/chatflows/Input Moderation.json @@ -2,6 +2,7 @@ "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", "badge": "NEW", "categories": "Moderation,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/List Output Parser.json b/packages/server/marketplaces/chatflows/List Output Parser.json index a67fb72b..0eb269b4 100644 --- a/packages/server/marketplaces/chatflows/List Output Parser.json +++ b/packages/server/marketplaces/chatflows/List Output Parser.json @@ -2,6 +2,7 @@ "description": "Return response as a list (array) instead of a string/text", "badge": "NEW", "categories": "CSV Output Parser,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index 11deae15..2637f259 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -2,6 +2,7 @@ "description": "QnA chain using Ollama local LLM, LocalAI embedding model, and Faiss local vector store", "badge": "POPULAR", "categories": "Text File,ChatOllama,Conversational Retrieval QA Chain,Faiss,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index ba2d2330..c5681d3d 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -1,6 +1,7 @@ { "description": "Use long term memory like Zep to differentiate conversations between users with sessionId", "categories": "ChatOpenAI,Conversational Retrieval QA Chain,Zep Memory,Qdrant,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index 13ca8745..38ad9211 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -2,6 +2,7 @@ "description": "Upsert multiple files with metadata and filter by it using conversational retrieval QA chain", "categories": "Text File,PDF File,ChatOpenAI,Conversational Retrieval QA Chain,Pinecone,Langchain", "badge": "POPULAR", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Multi Prompt Chain.json b/packages/server/marketplaces/chatflows/Multi Prompt Chain.json index 89935fcd..97cca308 100644 --- a/packages/server/marketplaces/chatflows/Multi Prompt Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Prompt Chain.json @@ -1,6 +1,7 @@ { "description": "A chain that automatically picks an appropriate prompt from multiple prompts", "categories": "ChatOpenAI,Multi Prompt Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json index 63a02edb..6b8f2c33 100644 --- a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json @@ -1,6 +1,7 @@ { "description": "A chain that automatically picks an appropriate retriever from multiple different vector databases", "categories": "ChatOpenAI,Multi Retrieval QA Chain,Pinecone,Chroma,Supabase,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index 7ffc040a..e8141ad7 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -1,6 +1,7 @@ { "description": "Use the agent to choose between multiple different vector databases, with the ability to use other tools", "categories": "Buffer Memory,ChatOpenAI,Chain Tool,Retrieval QA Chain,Redis,Faiss,Conversational Agent,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index c4cf11f2..6f35e595 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Agent.json +++ b/packages/server/marketplaces/chatflows/OpenAI Agent.json @@ -1,6 +1,7 @@ { "description": "An agent that uses OpenAI's Function Calling functionality to pick the tool and args to call", "categories": "Buffer Memory,Custom Tool, SerpAPI,OpenAI Function,Calculator Tool,ChatOpenAI,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/OpenAI Assistant.json b/packages/server/marketplaces/chatflows/OpenAI Assistant.json index f1595fd6..73c01413 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Assistant.json +++ b/packages/server/marketplaces/chatflows/OpenAI Assistant.json @@ -1,6 +1,7 @@ { "description": "OpenAI Assistant that has instructions and can leverage models, tools, and knowledge to respond to user queries", "categories": "Custom Tool, SerpAPI,OpenAI Assistant,Calculator Tool,Langchain", + "framework": "Langchain", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json index 3b8507b9..d225e41a 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json @@ -2,6 +2,7 @@ "description": "Use chat history to rephrase user question, and answer the rephrased question using retrieved docs from vector store", "categories": "ChatOpenAI,LLM Chain,SingleStore,Langchain", "badge": "POPULAR", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining.json b/packages/server/marketplaces/chatflows/Prompt Chaining.json index 3ed5f95c..42debac8 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining.json @@ -1,6 +1,7 @@ { "description": "Use output from a chain as prompt for another chain", "categories": "Custom Tool,OpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Query Engine.json b/packages/server/marketplaces/chatflows/Query Engine.json index 5697735b..b3a3c292 100644 --- a/packages/server/marketplaces/chatflows/Query Engine.json +++ b/packages/server/marketplaces/chatflows/Query Engine.json @@ -2,6 +2,7 @@ "description": "Stateless query engine designed to answer question over your data using LlamaIndex", "categories": "ChatAnthropic,Compact and Refine,Pinecone,LlamaIndex", "badge": "NEW", + "framework": "LlamaIndex", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/ReAct Agent.json b/packages/server/marketplaces/chatflows/ReAct Agent.json index 9831bcf6..5fd191fe 100644 --- a/packages/server/marketplaces/chatflows/ReAct Agent.json +++ b/packages/server/marketplaces/chatflows/ReAct Agent.json @@ -1,6 +1,7 @@ { "description": "An agent that uses ReAct logic to decide what action to take", "categories": "Calculator Tool,SerpAPI,ChatOpenAI,MRKL Agent,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Replicate LLM.json b/packages/server/marketplaces/chatflows/Replicate LLM.json index ef8ab7f2..578983cf 100644 --- a/packages/server/marketplaces/chatflows/Replicate LLM.json +++ b/packages/server/marketplaces/chatflows/Replicate LLM.json @@ -1,6 +1,7 @@ { "description": "Use Replicate API that runs Llama 13b v2 model with LLMChain", "categories": "Replicate,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/SQL DB Chain.json b/packages/server/marketplaces/chatflows/SQL DB Chain.json index 3c143a33..ec9d465d 100644 --- a/packages/server/marketplaces/chatflows/SQL DB Chain.json +++ b/packages/server/marketplaces/chatflows/SQL DB Chain.json @@ -1,6 +1,7 @@ { "description": "Answer questions over a SQL database", "categories": "ChatOpenAI,Sql Database Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/SQL Prompt.json b/packages/server/marketplaces/chatflows/SQL Prompt.json index e79a95d0..8d2691c6 100644 --- a/packages/server/marketplaces/chatflows/SQL Prompt.json +++ b/packages/server/marketplaces/chatflows/SQL Prompt.json @@ -1,6 +1,7 @@ { "description": "Manually construct prompts to query a SQL database", "categories": "IfElse Function,Variable Set/Get,Custom JS Function,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "badge": "new", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Simple Chat Engine.json b/packages/server/marketplaces/chatflows/Simple Chat Engine.json index 5510873d..fd17ded1 100644 --- a/packages/server/marketplaces/chatflows/Simple Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Simple Chat Engine.json @@ -1,6 +1,7 @@ { "description": "Simple chat engine to handle back and forth conversations using LlamaIndex", "categories": "BufferMemory,AzureChatOpenAI,LlamaIndex", + "framework": "LlamaIndex", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index e6c934ac..53cfeace 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -1,6 +1,7 @@ { "description": "Basic example of Conversation Chain with built-in memory - works exactly like ChatGPT", "categories": "Buffer Memory,ChatOpenAI,Conversation Chain,Langchain", + "framework": "Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Simple LLM Chain.json b/packages/server/marketplaces/chatflows/Simple LLM Chain.json index 12298441..36a5a8d8 100644 --- a/packages/server/marketplaces/chatflows/Simple LLM Chain.json +++ b/packages/server/marketplaces/chatflows/Simple LLM Chain.json @@ -1,6 +1,7 @@ { "description": "Basic example of stateless (no memory) LLM Chain with a Prompt Template and LLM Model", "categories": "OpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Structured Output Parser.json b/packages/server/marketplaces/chatflows/Structured Output Parser.json index 20385a4b..9801a90f 100644 --- a/packages/server/marketplaces/chatflows/Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Structured Output Parser.json @@ -1,6 +1,7 @@ { "description": "Return response as a specified JSON structure instead of a string/text", "categories": "Structured Output Parser,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json index 2042625b..620712c4 100644 --- a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json +++ b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json @@ -1,6 +1,7 @@ { "description": "Breaks down query into sub questions for each relevant data source, then combine into final response", "categories": "Sub Question Query Engine,Sticky Note,QueryEngine Tool,Compact and Refine,ChatOpenAI,Pinecone,LlamaIndex", + "framework": "LlamaIndex", "badge": "NEW", "nodes": [ { diff --git a/packages/server/marketplaces/chatflows/Translator.json b/packages/server/marketplaces/chatflows/Translator.json index 118457ef..5c8a3cc5 100644 --- a/packages/server/marketplaces/chatflows/Translator.json +++ b/packages/server/marketplaces/chatflows/Translator.json @@ -1,6 +1,7 @@ { "description": "Language translation using LLM Chain with a Chat Prompt Template and Chat Model", "categories": "Chat Prompt Template,ChatOpenAI,LLM Chain,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/Vectara RAG Chain.json b/packages/server/marketplaces/chatflows/Vectara RAG Chain.json index 82e341c4..2ef1474a 100644 --- a/packages/server/marketplaces/chatflows/Vectara RAG Chain.json +++ b/packages/server/marketplaces/chatflows/Vectara RAG Chain.json @@ -1,6 +1,7 @@ { "description": "QA chain for Vectara", "categories": "Vectara QA Chain,Vectara,Langchain", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 75e3cf0a..232bd83e 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -1,6 +1,7 @@ { "description": "Conversational Agent with ability to visit a website and extract information", - "categories": "Buffer Memory,Web Browser,ChatOpenAI,Conversational Agent,Langchain", + "categories": "Buffer Memory,Web Browser,ChatOpenAI,Conversational Agent", + "framework": "Langchain", "nodes": [ { "width": 300, diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index 089d195d..50806161 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -1,6 +1,7 @@ { "description": "Scrape web pages for QnA with long term memory Motorhead and return source documents", "categories": "HtmlToMarkdown,Cheerio Web Scraper,ChatOpenAI,Redis,Pinecone,Langchain", + "framework": "Langchain", "badge": "POPULAR", "nodes": [ { diff --git a/packages/server/marketplaces/tools/Add Hubspot Contact.json b/packages/server/marketplaces/tools/Add Hubspot Contact.json index 584df4c3..f8715dcd 100644 --- a/packages/server/marketplaces/tools/Add Hubspot Contact.json +++ b/packages/server/marketplaces/tools/Add Hubspot Contact.json @@ -1,5 +1,6 @@ { "name": "add_contact_hubspot", + "framework": "Langchain", "description": "Add new contact to Hubspot", "color": "linear-gradient(rgb(85,198,123), rgb(0,230,99))", "iconSrc": "https://cdn.worldvectorlogo.com/logos/hubspot-1.svg", diff --git a/packages/server/marketplaces/tools/Create Airtable Record.json b/packages/server/marketplaces/tools/Create Airtable Record.json index c52c9199..5471b650 100644 --- a/packages/server/marketplaces/tools/Create Airtable Record.json +++ b/packages/server/marketplaces/tools/Create Airtable Record.json @@ -1,5 +1,6 @@ { "name": "add_airtable", + "framework": "Langchain", "description": "Add column1, column2 to Airtable", "color": "linear-gradient(rgb(125,71,222), rgb(128,102,23))", "iconSrc": "https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg", diff --git a/packages/server/marketplaces/tools/Get Current DateTime.json b/packages/server/marketplaces/tools/Get Current DateTime.json index b6860b30..b8279e33 100644 --- a/packages/server/marketplaces/tools/Get Current DateTime.json +++ b/packages/server/marketplaces/tools/Get Current DateTime.json @@ -1,5 +1,6 @@ { "name": "todays_date_time", + "framework": "Langchain", "description": "Useful to get todays day, date and time.", "color": "linear-gradient(rgb(117,118,129), rgb(230,10,250))", "iconSrc": "https://raw.githubusercontent.com/gilbarbara/logos/main/logos/javascript.svg", diff --git a/packages/server/marketplaces/tools/Get Stock Mover.json b/packages/server/marketplaces/tools/Get Stock Mover.json index 9108cc50..27d444b2 100644 --- a/packages/server/marketplaces/tools/Get Stock Mover.json +++ b/packages/server/marketplaces/tools/Get Stock Mover.json @@ -1,5 +1,6 @@ { "name": "get_stock_movers", + "framework": "Langchain", "description": "Get the stocks that has biggest price/volume moves, e.g. actives, gainers, losers, etc.", "iconSrc": "https://rapidapi.com/cdn/images?url=https://rapidapi-prod-apis.s3.amazonaws.com/9c/e743343bdd41edad39a3fdffd5b974/016c33699f51603ae6fe4420c439124b.png", "color": "linear-gradient(rgb(191,202,167), rgb(143,202,246))", diff --git a/packages/server/marketplaces/tools/Make Webhook.json b/packages/server/marketplaces/tools/Make Webhook.json index 24d00900..93e67a3f 100644 --- a/packages/server/marketplaces/tools/Make Webhook.json +++ b/packages/server/marketplaces/tools/Make Webhook.json @@ -1,5 +1,6 @@ { "name": "make_webhook", + "framework": "Langchain", "description": "Useful when you need to send message to Discord", "color": "linear-gradient(rgb(19,94,2), rgb(19,124,59))", "iconSrc": "https://github.com/FlowiseAI/Flowise/assets/26460777/517fdab2-8a6e-4781-b3c8-fb92cc78aa0b", diff --git a/packages/server/marketplaces/tools/Send Discord Message.json b/packages/server/marketplaces/tools/Send Discord Message.json index bbfaaa90..2d7adcac 100644 --- a/packages/server/marketplaces/tools/Send Discord Message.json +++ b/packages/server/marketplaces/tools/Send Discord Message.json @@ -1,5 +1,6 @@ { "name": "send_message_to_discord_channel", + "framework": "Langchain", "description": "Send message to Discord channel", "color": "linear-gradient(rgb(155,190,84), rgb(176,69,245))", "iconSrc": "https://raw.githubusercontent.com/gilbarbara/logos/main/logos/discord-icon.svg", diff --git a/packages/server/marketplaces/tools/Send Slack Message.json b/packages/server/marketplaces/tools/Send Slack Message.json index f15d4050..5516b69a 100644 --- a/packages/server/marketplaces/tools/Send Slack Message.json +++ b/packages/server/marketplaces/tools/Send Slack Message.json @@ -1,5 +1,6 @@ { "name": "send_message_to_slack_channel", + "framework": "Langchain", "description": "Send message to Slack channel", "color": "linear-gradient(rgb(155,190,84), rgb(176,69,245))", "iconSrc": "https://raw.githubusercontent.com/gilbarbara/logos/main/logos/slack-icon.svg", diff --git a/packages/server/marketplaces/tools/Send Teams Message.json b/packages/server/marketplaces/tools/Send Teams Message.json index 1af8111b..8ec32abd 100644 --- a/packages/server/marketplaces/tools/Send Teams Message.json +++ b/packages/server/marketplaces/tools/Send Teams Message.json @@ -1,5 +1,6 @@ { "name": "send_message_to_teams_channel", + "framework": "Langchain", "description": "Send message to Teams channel", "color": "linear-gradient(rgb(155,190,84), rgb(176,69,245))", "iconSrc": "https://raw.githubusercontent.com/gilbarbara/logos/main/logos/microsoft-teams.svg", diff --git a/packages/server/marketplaces/tools/SendGrid Email.json b/packages/server/marketplaces/tools/SendGrid Email.json index 8a6bf993..b454f2c5 100644 --- a/packages/server/marketplaces/tools/SendGrid Email.json +++ b/packages/server/marketplaces/tools/SendGrid Email.json @@ -1,5 +1,6 @@ { "name": "sendgrid_email", + "framework": "Langchain", "description": "Send email using SendGrid", "color": "linear-gradient(rgb(230,108,70), rgb(222,4,98))", "iconSrc": "https://raw.githubusercontent.com/gilbarbara/logos/main/logos/sendgrid-icon.svg", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index a65fabf3..86595dc1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1237,6 +1237,7 @@ export class App { templateName: file.split('.json')[0], flowData: fileData.toString(), badge: fileDataObj?.badge, + framework: fileDataObj?.framework, categories: fileDataObj?.categories, type: 'Chatflow', description: fileDataObj?.description || '' @@ -1254,6 +1255,8 @@ export class App { ...fileDataObj, id: index, type: 'Tool', + framework: fileDataObj?.framework, + badge: fileDataObj?.badge, categories: '', templateName: file.split('.json')[0] } diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.js b/packages/ui/src/ui-component/table/MarketplaceTable.js index 714f10b0..4fe4aee6 100644 --- a/packages/ui/src/ui-component/table/MarketplaceTable.js +++ b/packages/ui/src/ui-component/table/MarketplaceTable.js @@ -10,6 +10,8 @@ import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' import Chip from '@mui/material/Chip' import { Button, Typography } from '@mui/material' +import langchainPNG from 'assets/images/langchain.png' +import llamaIndexPNG from 'assets/images/llamaindex.png' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -31,7 +33,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })) -export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, filterByType }) => { +export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterByType, filterByFramework }) => { const navigate = useNavigate() const openTemplate = (selectedTemplate) => { if (selectedTemplate.flowData) { @@ -61,10 +63,13 @@ export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, + + {''} + Name - + Type @@ -83,9 +88,20 @@ export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, .filter(filterByBadge) .filter(filterByType) .filter(filterFunction) + .filter(filterByFramework) .map((row, index) => ( + + {row.framework === 'Langchain' && ( + langchain + )} + {row.framework === 'LlamaIndex' && ( + llamaIndex + )} + + + @@ -94,15 +110,15 @@ export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, - + {row.type} - + {row.description || ''} - +
    - + {row.badge && row.badge @@ -152,8 +168,8 @@ export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, MarketplaceTable.propTypes = { data: PropTypes.array, - images: PropTypes.object, filterFunction: PropTypes.func, filterByBadge: PropTypes.func, - filterByType: PropTypes.func + filterByType: PropTypes.func, + filterByFramework: PropTypes.func } diff --git a/packages/ui/src/views/marketplaces/index.js b/packages/ui/src/views/marketplaces/index.js index f2055da6..68258a47 100644 --- a/packages/ui/src/views/marketplaces/index.js +++ b/packages/ui/src/views/marketplaces/index.js @@ -19,10 +19,11 @@ import { Select, OutlinedInput, Checkbox, - ListItemText + ListItemText, + Button } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconLayoutGrid, IconList, IconSearch } from '@tabler/icons' +import { IconChevronsDown, IconChevronsUp, IconLayoutGrid, IconList, IconSearch } from '@tabler/icons' // project imports import MainCard from 'ui-component/cards/MainCard' @@ -69,6 +70,7 @@ const ITEM_HEIGHT = 48 const ITEM_PADDING_TOP = 8 const badges = ['POPULAR', 'NEW'] const types = ['Chatflow', 'Tool'] +const framework = ['Langchain', 'LlamaIndex'] const MenuProps = { PaperProps: { style: { @@ -98,7 +100,8 @@ const Marketplace = () => { const [badgeFilter, setBadgeFilter] = useState([]) const [typeFilter, setTypeFilter] = useState([]) - + const [frameworkFilter, setFrameworkFilter] = useState([]) + const [open, setOpen] = useState(false) const handleBadgeFilterChange = (event) => { const { target: { value } @@ -117,6 +120,15 @@ const Marketplace = () => { typeof value === 'string' ? value.split(',') : value ) } + const handleFrameworkFilterChange = (event) => { + const { + target: { value } + } = event + setFrameworkFilter( + // On autofill we get a stringified value. + typeof value === 'string' ? value.split(',') : value + ) + } const handleViewChange = (event, nextView) => { localStorage.setItem('mpDisplayStyle', nextView) @@ -143,6 +155,10 @@ const Marketplace = () => { return typeFilter.length > 0 ? typeFilter.includes(data.type) : true } + function filterByFramework(data) { + return frameworkFilter.length > 0 ? frameworkFilter.includes(data.framework) : true + } + const onUseTemplate = (selectedTool) => { const dialogProp = { title: 'Add New Tool', @@ -224,9 +240,11 @@ const Marketplace = () => {

    Marketplace

    { ) }} /> - - - Type - - - - - - Tag - - - + @@ -313,6 +293,93 @@ const Marketplace = () => { + {open && ( + + + + + Tag + + + + + + Type + + + + + + Framework + + + + + + )} + {!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && ( <> @@ -320,6 +387,7 @@ const Marketplace = () => { .filter(filterByBadge) .filter(filterByType) .filter(filterFlows) + .filter(filterByFramework) .map((data, index) => ( {data.badge && ( @@ -351,10 +419,10 @@ const Marketplace = () => { )} From 8e80b582bbb5a91f7c6383af23ee4343b56d5a9e Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Tue, 6 Feb 2024 12:02:35 +0800 Subject: [PATCH 400/502] update return msg to correct url --- packages/server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index dbb5717d..4d06f297 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -184,7 +184,7 @@ export class App { this.app.get('/api/v1/ip', (request, response) => { response.send({ ip: request.ip, - msg: 'See the returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 until the IP address matches your own. Visit https://docs.flowiseai.com/deployment#rate-limit-setup-guide for more information.' + msg: 'Check the returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#rate-limit-setup-guide for more information.' }) }) From 7486d33237c46bc0112fb7b3fb3e48f6cd1c89bf Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 6 Feb 2024 10:36:27 +0530 Subject: [PATCH 401/502] Fix issue with relativeLinksMethod and limit not applying to manage links --- packages/server/src/index.ts | 3 ++- packages/ui/src/api/scraper.js | 6 +++--- .../src/ui-component/dialog/ManageScrapedLinksDialog.js | 2 +- packages/ui/src/views/canvas/NodeInputHandler.js | 8 ++++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index dbb5717d..7ceba556 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1148,8 +1148,9 @@ export class App { this.app.get('/api/v1/fetch-links', async (req: Request, res: Response) => { const url = decodeURIComponent(req.query.url as string) const relativeLinksMethod = req.query.relativeLinksMethod as string + const limit = parseInt(req.query.limit as string) if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) - const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, 0) : await xmlScrape(url, 0) + const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) res.json({ status: 'OK', links }) }) diff --git a/packages/ui/src/api/scraper.js b/packages/ui/src/api/scraper.js index 382a9263..89333156 100644 --- a/packages/ui/src/api/scraper.js +++ b/packages/ui/src/api/scraper.js @@ -1,8 +1,8 @@ import client from './client' -const fetchAllLinks = (url, relativeLinksMethod) => - client.get(`/fetch-links?url=${encodeURIComponent(url)}&relativeLinksMethod=${relativeLinksMethod}`) +const fetchLinks = (url, relativeLinksMethod, relativeLinksLimit) => + client.get(`/fetch-links?url=${encodeURIComponent(url)}&relativeLinksMethod=${relativeLinksMethod}&limit=${relativeLinksLimit}`) export default { - fetchAllLinks + fetchLinks } diff --git a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js index a707d82e..9a846ce9 100644 --- a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js +++ b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js @@ -53,7 +53,7 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => { const handleFetchLinks = async () => { setLoading(true) - const fetchLinksResp = await scraperApi.fetchAllLinks(url, 'webCrawl') + const fetchLinksResp = await scraperApi.fetchLinks(url, dialogProps.relativeLinksMethod, dialogProps.limit) if (fetchLinksResp.data) { setSelectedLinks(fetchLinksResp.data.links) } diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index bc877c9f..560fb34e 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -91,9 +91,11 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA } } - const onManageLinksDialogClicked = (url, selectedLinks) => { + const onManageLinksDialogClicked = (url, selectedLinks, relativeLinksMethod, limit) => { const dialogProps = { url, + relativeLinksMethod, + limit, selectedLinks, confirmButtonName: 'Save', cancelButtonName: 'Cancel' @@ -475,7 +477,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA onClick={() => onManageLinksDialogClicked( data.inputs[inputParam.name] ?? inputParam.default ?? '', - data.inputs.selectedLinks + data.inputs.selectedLinks, + data.inputs['relativeLinksMethod'] ?? 'webCrawl', + parseInt(data.inputs['limit']) ?? 0 ) } > From 8c494cf17e5f853d70fb56d1a9937d383a8a7455 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 6 Feb 2024 12:59:40 +0530 Subject: [PATCH 402/502] Fix UI issues - chat window height, image & audio styling, and image + audio not sending together --- .../ui-component/cards/StarterPromptsCard.css | 3 -- .../ui-component/cards/StarterPromptsCard.js | 5 ++- .../src/views/chatmessage/ChatExpandDialog.js | 14 +++++-- .../ui/src/views/chatmessage/ChatMessage.css | 11 ++++-- .../ui/src/views/chatmessage/ChatMessage.js | 38 +++++++++---------- .../ui/src/views/chatmessage/ChatPopUp.js | 5 ++- 6 files changed, 45 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/ui-component/cards/StarterPromptsCard.css b/packages/ui/src/ui-component/cards/StarterPromptsCard.css index 028b8b34..8fc6c07c 100644 --- a/packages/ui/src/ui-component/cards/StarterPromptsCard.css +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.css @@ -1,6 +1,4 @@ .button-container { - position: absolute; - z-index: 1000; display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */ @@ -9,5 +7,4 @@ .button { flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ - margin: 5px; /* Adjust as needed for spacing between buttons */ } diff --git a/packages/ui/src/ui-component/cards/StarterPromptsCard.js b/packages/ui/src/ui-component/cards/StarterPromptsCard.js index cfec4ba4..bb3fbdfa 100644 --- a/packages/ui/src/ui-component/cards/StarterPromptsCard.js +++ b/packages/ui/src/ui-component/cards/StarterPromptsCard.js @@ -5,7 +5,10 @@ import './StarterPromptsCard.css' const StarterPromptsCard = ({ isGrid, starterPrompts, sx, onPromptClick }) => { return ( - + {starterPrompts.map((sp, index) => ( onPromptClick(sp.prompt, e)} /> ))} diff --git a/packages/ui/src/views/chatmessage/ChatExpandDialog.js b/packages/ui/src/views/chatmessage/ChatExpandDialog.js index e2044ea3..9b526e56 100644 --- a/packages/ui/src/views/chatmessage/ChatExpandDialog.js +++ b/packages/ui/src/views/chatmessage/ChatExpandDialog.js @@ -7,7 +7,7 @@ import { ChatMessage } from './ChatMessage' import { StyledButton } from 'ui-component/button/StyledButton' import { IconEraser } from '@tabler/icons' -const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => { +const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel, previews, setPreviews }) => { const portalElement = document.getElementById('portal') const customization = useSelector((state) => state.customization) @@ -47,7 +47,13 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => { className='cloud-dialog-wrapper' sx={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column', p: 0 }} > - + ) : null @@ -59,7 +65,9 @@ ChatExpandDialog.propTypes = { show: PropTypes.bool, dialogProps: PropTypes.object, onClear: PropTypes.func, - onCancel: PropTypes.func + onCancel: PropTypes.func, + previews: PropTypes.array, + setPreviews: PropTypes.func } export default ChatExpandDialog diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 91bb9efa..e1646c80 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -115,14 +115,14 @@ padding: 12px; } -.cloud-wrapper, -.cloud-dialog-wrapper { +.cloud-wrapper { width: 400px; - height: calc(100vh - 260px); + height: calc(100vh - 180px); } .cloud-dialog-wrapper { width: 100%; + height: calc(100vh - 120px); } .cloud-wrapper > div, @@ -198,3 +198,8 @@ z-index: 2000; /* Ensure it's above other content */ border: 2px dashed #0094ff; /* Example style */ } + +.center audio { + height: 100%; + border-radius: 0; +} diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 1a9e6d35..89fd941a 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -58,7 +58,7 @@ const messageImageStyle = { objectFit: 'cover' } -export const ChatMessage = ({ open, chatflowid, isDialog }) => { +export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews }) => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -90,7 +90,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // drag & drop and file input const fileUploadRef = useRef(null) const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) - const [previews, setPreviews] = useState([]) const [isDragActive, setIsDragActive] = useState(false) // recording @@ -353,7 +352,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (e) e.preventDefault() if (!promptStarterInput && userInput.trim() === '') { - if (!(previews.length === 1 && previews[0].type === 'audio')) { + const containsAudio = previews.filter((item) => item.type === 'audio').length > 0 + if (!(previews.length > 1 && containsAudio)) { return } } @@ -584,7 +584,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { useEffect(() => { // wait for audio recording to load and then send - if (previews.length === 1 && previews[0].type === 'audio') { + const containsAudio = previews.filter((item) => item.type === 'audio').length > 0 + if (previews.length > 1 && containsAudio) { setIsRecording(false) setRecordingNotSupported(false) handlePromptClick('') @@ -669,7 +670,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { display: 'flex', flexWrap: 'wrap', flexDirection: 'row', - width: '100%' + width: '100%', + gap: '4px' }} > {message.fileUploads.map((item, index) => { @@ -788,23 +790,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
    -
    - {messages && messages.length === 1 && ( + {messages && messages.length === 1 && starterPrompts.length > 0 && ( +
    0 ? 70 : 0 }} starterPrompts={starterPrompts || []} onPromptClick={handlePromptClick} isGrid={isDialog} /> - )} - -
    +
    + )}
    {previews && previews.length > 0 && ( - + {previews.map((item, index) => ( {item.mime.startsWith('image/') ? ( @@ -827,23 +828,18 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { ) : ( - + handleDeletePreview(item)} size='small'> @@ -993,5 +989,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { ChatMessage.propTypes = { open: PropTypes.bool, chatflowid: PropTypes.string, - isDialog: PropTypes.bool + isDialog: PropTypes.bool, + previews: PropTypes.array, + setPreviews: PropTypes.func } diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.js b/packages/ui/src/views/chatmessage/ChatPopUp.js index 91ba73e2..74d4b908 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.js +++ b/packages/ui/src/views/chatmessage/ChatPopUp.js @@ -35,6 +35,7 @@ export const ChatPopUp = ({ chatflowid }) => { const [open, setOpen] = useState(false) const [showExpandDialog, setShowExpandDialog] = useState(false) const [expandDialogProps, setExpandDialogProps] = useState({}) + const [previews, setPreviews] = useState([]) const anchorRef = useRef(null) const prevOpen = useRef(open) @@ -199,7 +200,7 @@ export const ChatPopUp = ({ chatflowid }) => { boxShadow shadow={theme.shadows[16]} > - + @@ -211,6 +212,8 @@ export const ChatPopUp = ({ chatflowid }) => { dialogProps={expandDialogProps} onClear={clearChat} onCancel={() => setShowExpandDialog(false)} + previews={previews} + setPreviews={setPreviews} > ) From c2ae7e138cbf8a4355cafc109df5b7b5a0eb0a21 Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 6 Feb 2024 14:40:19 +0530 Subject: [PATCH 403/502] Apply limit to selectedLinks even when relative links method is not specified --- packages/components/nodes/documentloaders/Cheerio/Cheerio.ts | 2 +- .../components/nodes/documentloaders/Playwright/Playwright.ts | 2 +- .../components/nodes/documentloaders/Puppeteer/Puppeteer.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts index 3eba0ece..6af1f9a9 100644 --- a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts +++ b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts @@ -143,7 +143,7 @@ class Cheerio_DocumentLoaders implements INode { } else if (selectedLinks && selectedLinks.length > 0) { if (process.env.DEBUG === 'true') options.logger.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) - for (const page of selectedLinks) { + for (const page of selectedLinks.slice(0, limit)) { docs.push(...(await cheerioLoader(page))) } } else { diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index 2de166ce..2ba60d0f 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -184,7 +184,7 @@ class Playwright_DocumentLoaders implements INode { } else if (selectedLinks && selectedLinks.length > 0) { if (process.env.DEBUG === 'true') options.logger.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) - for (const page of selectedLinks) { + for (const page of selectedLinks.slice(0, limit)) { docs.push(...(await playwrightLoader(page))) } } else { diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 3d28f310..1f8c8f3f 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -185,7 +185,7 @@ class Puppeteer_DocumentLoaders implements INode { } else if (selectedLinks && selectedLinks.length > 0) { if (process.env.DEBUG === 'true') options.logger.info(`pages: ${JSON.stringify(selectedLinks)}, length: ${selectedLinks.length}`) - for (const page of selectedLinks) { + for (const page of selectedLinks.slice(0, limit)) { docs.push(...(await puppeteerLoader(page))) } } else { From 90e6a804e462dc9ea1db6fd598840830b142364b Mon Sep 17 00:00:00 2001 From: Kenny Vaneetvelde Date: Tue, 6 Feb 2024 13:02:30 +0100 Subject: [PATCH 404/502] Make the zod schema a main parameter instead of an additional param --- .../StructuredOutputParserAdvanced.ts | 5 +- .../Advanced Structured Output Parser.json | 137 +++++++++--------- 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts b/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts index b0fad136..e7fe8ea7 100644 --- a/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts +++ b/packages/components/nodes/outputparsers/StructuredOutputParserAdvanced/StructuredOutputParserAdvanced.ts @@ -46,9 +46,8 @@ class AdvancedStructuredOutputParser implements INode { "Action", "Comedy", "Drama", "Fantasy", "Horror", "Mystery", "Romance", "Science Fiction", "Thriller", "Documentary" ]).array().max(2), // Array of genres, max of 2 from the defined enum - shortDescription: z.string().max(500) // Short description, max 150 characters -})`, - additionalParams: true + shortDescription: z.string().max(500) // Short description, max 500 characters +})` } ] } diff --git a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json index a48f5b2a..3fd71988 100644 --- a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json @@ -169,67 +169,6 @@ }, "dragging": false }, - { - "width": 300, - "height": 329, - "id": "advancedStructuredOutputParser_0", - "position": { - "x": 494.20163170226266, - "y": 568.3420937517054 - }, - "type": "customNode", - "data": { - "id": "advancedStructuredOutputParser_0", - "label": "Advanced Structured Output Parser", - "version": 1, - "name": "advancedStructuredOutputParser", - "type": "AdvancedStructuredOutputParser", - "baseClasses": ["AdvancedStructuredOutputParser", "BaseLLMOutputParser", "Runnable"], - "category": "Output Parsers", - "description": "Parse the output of an LLM call into a given structure by providing a Zod schema.", - "inputParams": [ - { - "label": "Autofix", - "name": "autofixParser", - "type": "boolean", - "optional": true, - "description": "In the event that the first call fails, will make another call to the model to fix any errors.", - "id": "advancedStructuredOutputParser_0-input-autofixParser-boolean" - }, - { - "label": "Example JSON", - "name": "exampleJson", - "type": "string", - "description": "Zod schema for the output of the model", - "rows": 10, - "default": "z.object({\n title: z.string(), // Title of the movie as a string\n yearOfRelease: z.number().int(), // Release year as an integer number,\n genres: z.enum([\n \"Action\", \"Comedy\", \"Drama\", \"Fantasy\", \"Horror\",\n \"Mystery\", \"Romance\", \"Science Fiction\", \"Thriller\", \"Documentary\"\n ]).array().max(2), // Array of genres, max of 2 from the defined enum\n shortDescription: z.string().max(500) // Short description, max 150 characters\n})", - "additionalParams": true, - "id": "advancedStructuredOutputParser_0-input-exampleJson-string" - } - ], - "inputAnchors": [], - "inputs": { - "autofixParser": true, - "exampleJson": "z.object({\n title: z.string(), // Title of the movie as a string\n yearOfRelease: z.number().int(), // Release year as an integer number,\n genres: z.enum([\n \"Action\", \"Comedy\", \"Drama\", \"Fantasy\", \"Horror\",\n \"Mystery\", \"Romance\", \"Science Fiction\", \"Thriller\", \"Documentary\"\n ]).array().max(2), // Array of genres, max of 2 from the defined enum\n shortDescription: z.string().max(500) // Short description, max 150 characters\n})" - }, - "outputAnchors": [ - { - "id": "advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable", - "name": "advancedStructuredOutputParser", - "label": "AdvancedStructuredOutputParser", - "type": "AdvancedStructuredOutputParser | BaseLLMOutputParser | Runnable" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 494.20163170226266, - "y": 568.3420937517054 - }, - "dragging": false - }, { "width": 300, "height": 576, @@ -431,6 +370,66 @@ "y": -355.71028569475095 }, "dragging": false + }, + { + "width": 300, + "height": 454, + "id": "advancedStructuredOutputParser_0", + "position": { + "x": 489.3637511211284, + "y": 580.0628053662244 + }, + "type": "customNode", + "data": { + "id": "advancedStructuredOutputParser_0", + "label": "Advanced Structured Output Parser", + "version": 1, + "name": "advancedStructuredOutputParser", + "type": "AdvancedStructuredOutputParser", + "baseClasses": ["AdvancedStructuredOutputParser", "BaseLLMOutputParser", "Runnable"], + "category": "Output Parsers", + "description": "Parse the output of an LLM call into a given structure by providing a Zod schema.", + "inputParams": [ + { + "label": "Autofix", + "name": "autofixParser", + "type": "boolean", + "optional": true, + "description": "In the event that the first call fails, will make another call to the model to fix any errors.", + "id": "advancedStructuredOutputParser_0-input-autofixParser-boolean" + }, + { + "label": "Example JSON", + "name": "exampleJson", + "type": "string", + "description": "Zod schema for the output of the model", + "rows": 10, + "default": "z.object({\n title: z.string(), // Title of the movie as a string\n yearOfRelease: z.number().int(), // Release year as an integer number,\n genres: z.enum([\n \"Action\", \"Comedy\", \"Drama\", \"Fantasy\", \"Horror\",\n \"Mystery\", \"Romance\", \"Science Fiction\", \"Thriller\", \"Documentary\"\n ]).array().max(2), // Array of genres, max of 2 from the defined enum\n shortDescription: z.string().max(500) // Short description, max 500 characters\n})", + "id": "advancedStructuredOutputParser_0-input-exampleJson-string" + } + ], + "inputAnchors": [], + "inputs": { + "autofixParser": "", + "exampleJson": "z.object({\n title: z.string(), // Title of the movie as a string\n yearOfRelease: z.number().int(), // Release year as an integer number,\n genres: z.enum([\n \"Action\", \"Comedy\", \"Drama\", \"Fantasy\", \"Horror\",\n \"Mystery\", \"Romance\", \"Science Fiction\", \"Thriller\", \"Documentary\"\n ]).array().max(2), // Array of genres, max of 2 from the defined enum\n shortDescription: z.string().max(500) // Short description, max 500 characters\n})" + }, + "outputAnchors": [ + { + "id": "advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable", + "name": "advancedStructuredOutputParser", + "label": "AdvancedStructuredOutputParser", + "type": "AdvancedStructuredOutputParser | BaseLLMOutputParser | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 489.3637511211284, + "y": 580.0628053662244 + } } ], "edges": [ @@ -445,14 +444,6 @@ "label": "" } }, - { - "source": "advancedStructuredOutputParser_0", - "sourceHandle": "advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable", - "target": "llmChain_0", - "targetHandle": "llmChain_0-input-outputParser-BaseLLMOutputParser", - "type": "buttonedge", - "id": "advancedStructuredOutputParser_0-advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser" - }, { "source": "chatOpenAI_0", "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", @@ -460,6 +451,14 @@ "targetHandle": "llmChain_0-input-model-BaseLanguageModel", "type": "buttonedge", "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_0-llmChain_0-input-model-BaseLanguageModel" + }, + { + "source": "advancedStructuredOutputParser_0", + "sourceHandle": "advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-outputParser-BaseLLMOutputParser", + "type": "buttonedge", + "id": "advancedStructuredOutputParser_0-advancedStructuredOutputParser_0-output-advancedStructuredOutputParser-AdvancedStructuredOutputParser|BaseLLMOutputParser|Runnable-llmChain_0-llmChain_0-input-outputParser-BaseLLMOutputParser" } ] } From 2bb2a7588a0172a937bde34aefce5173e24f34a7 Mon Sep 17 00:00:00 2001 From: Ilyes Tascou Date: Tue, 6 Feb 2024 14:25:40 +0100 Subject: [PATCH 405/502] add recursive option for folder-loader --- .../nodes/documentloaders/Folder/Folder.ts | 96 +++++++++++-------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/packages/components/nodes/documentloaders/Folder/Folder.ts b/packages/components/nodes/documentloaders/Folder/Folder.ts index f8346e3c..ab770562 100644 --- a/packages/components/nodes/documentloaders/Folder/Folder.ts +++ b/packages/components/nodes/documentloaders/Folder/Folder.ts @@ -34,6 +34,12 @@ class Folder_DocumentLoaders implements INode { type: 'string', placeholder: '' }, + { + label: 'Recursive', + name: 'recursive', + type: 'boolean', + additionalParams: false + }, { label: 'Text Splitter', name: 'textSplitter', @@ -54,49 +60,55 @@ class Folder_DocumentLoaders implements INode { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const folderPath = nodeData.inputs?.folderPath as string const metadata = nodeData.inputs?.metadata + const recursive = nodeData.inputs?.recursive as boolean - const loader = new DirectoryLoader(folderPath, { - '.json': (path) => new JSONLoader(path), - '.txt': (path) => new TextLoader(path), - '.csv': (path) => new CSVLoader(path), - '.docx': (path) => new DocxLoader(path), - // @ts-ignore - '.pdf': (path) => new PDFLoader(path, { pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') }), - '.aspx': (path) => new TextLoader(path), - '.asp': (path) => new TextLoader(path), - '.cpp': (path) => new TextLoader(path), // C++ - '.c': (path) => new TextLoader(path), - '.cs': (path) => new TextLoader(path), - '.css': (path) => new TextLoader(path), - '.go': (path) => new TextLoader(path), // Go - '.h': (path) => new TextLoader(path), // C++ Header files - '.kt': (path) => new TextLoader(path), // Kotlin - '.java': (path) => new TextLoader(path), // Java - '.js': (path) => new TextLoader(path), // JavaScript - '.less': (path) => new TextLoader(path), // Less files - '.ts': (path) => new TextLoader(path), // TypeScript - '.php': (path) => new TextLoader(path), // PHP - '.proto': (path) => new TextLoader(path), // Protocol Buffers - '.python': (path) => new TextLoader(path), // Python - '.py': (path) => new TextLoader(path), // Python - '.rst': (path) => new TextLoader(path), // reStructuredText - '.ruby': (path) => new TextLoader(path), // Ruby - '.rb': (path) => new TextLoader(path), // Ruby - '.rs': (path) => new TextLoader(path), // Rust - '.scala': (path) => new TextLoader(path), // Scala - '.sc': (path) => new TextLoader(path), // Scala - '.scss': (path) => new TextLoader(path), // Sass - '.sol': (path) => new TextLoader(path), // Solidity - '.sql': (path) => new TextLoader(path), //SQL - '.swift': (path) => new TextLoader(path), // Swift - '.markdown': (path) => new TextLoader(path), // Markdown - '.md': (path) => new TextLoader(path), // Markdown - '.tex': (path) => new TextLoader(path), // LaTeX - '.ltx': (path) => new TextLoader(path), // LaTeX - '.html': (path) => new TextLoader(path), // HTML - '.vb': (path) => new TextLoader(path), // Visual Basic - '.xml': (path) => new TextLoader(path) // XML - }) + console.log('Recursive: ', recursive) + const loader = new DirectoryLoader( + folderPath, + { + '.json': (path) => new JSONLoader(path), + '.txt': (path) => new TextLoader(path), + '.csv': (path) => new CSVLoader(path), + '.docx': (path) => new DocxLoader(path), + // @ts-ignore + '.pdf': (path) => new PDFLoader(path, { pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') }), + '.aspx': (path) => new TextLoader(path), + '.asp': (path) => new TextLoader(path), + '.cpp': (path) => new TextLoader(path), // C++ + '.c': (path) => new TextLoader(path), + '.cs': (path) => new TextLoader(path), + '.css': (path) => new TextLoader(path), + '.go': (path) => new TextLoader(path), // Go + '.h': (path) => new TextLoader(path), // C++ Header files + '.kt': (path) => new TextLoader(path), // Kotlin + '.java': (path) => new TextLoader(path), // Java + '.js': (path) => new TextLoader(path), // JavaScript + '.less': (path) => new TextLoader(path), // Less files + '.ts': (path) => new TextLoader(path), // TypeScript + '.php': (path) => new TextLoader(path), // PHP + '.proto': (path) => new TextLoader(path), // Protocol Buffers + '.python': (path) => new TextLoader(path), // Python + '.py': (path) => new TextLoader(path), // Python + '.rst': (path) => new TextLoader(path), // reStructuredText + '.ruby': (path) => new TextLoader(path), // Ruby + '.rb': (path) => new TextLoader(path), // Ruby + '.rs': (path) => new TextLoader(path), // Rust + '.scala': (path) => new TextLoader(path), // Scala + '.sc': (path) => new TextLoader(path), // Scala + '.scss': (path) => new TextLoader(path), // Sass + '.sol': (path) => new TextLoader(path), // Solidity + '.sql': (path) => new TextLoader(path), //SQL + '.swift': (path) => new TextLoader(path), // Swift + '.markdown': (path) => new TextLoader(path), // Markdown + '.md': (path) => new TextLoader(path), // Markdown + '.tex': (path) => new TextLoader(path), // LaTeX + '.ltx': (path) => new TextLoader(path), // LaTeX + '.html': (path) => new TextLoader(path), // HTML + '.vb': (path) => new TextLoader(path), // Visual Basic + '.xml': (path) => new TextLoader(path) // XML + }, + recursive + ) let docs = [] if (textSplitter) { From 19fb13baf05642a09e10a57ed302bca8e339e2dd Mon Sep 17 00:00:00 2001 From: Ilyes Tascou Date: Tue, 6 Feb 2024 14:36:32 +0100 Subject: [PATCH 406/502] fix for linting --- packages/components/nodes/documentloaders/Folder/Folder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/nodes/documentloaders/Folder/Folder.ts b/packages/components/nodes/documentloaders/Folder/Folder.ts index ab770562..fb3db8e8 100644 --- a/packages/components/nodes/documentloaders/Folder/Folder.ts +++ b/packages/components/nodes/documentloaders/Folder/Folder.ts @@ -62,7 +62,6 @@ class Folder_DocumentLoaders implements INode { const metadata = nodeData.inputs?.metadata const recursive = nodeData.inputs?.recursive as boolean - console.log('Recursive: ', recursive) const loader = new DirectoryLoader( folderPath, { From f14039736dd5147418e2f28425f3ffc64347daa2 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 6 Feb 2024 11:29:50 -0500 Subject: [PATCH 407/502] Marketplace : removing icon column --- .../ui-component/table/MarketplaceTable.js | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.js b/packages/ui/src/ui-component/table/MarketplaceTable.js index 4fe4aee6..ebbc8537 100644 --- a/packages/ui/src/ui-component/table/MarketplaceTable.js +++ b/packages/ui/src/ui-component/table/MarketplaceTable.js @@ -10,8 +10,6 @@ import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' import Chip from '@mui/material/Chip' import { Button, Typography } from '@mui/material' -import langchainPNG from 'assets/images/langchain.png' -import llamaIndexPNG from 'assets/images/llamaindex.png' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -63,9 +61,6 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy
    - - {''} - Name @@ -92,16 +87,6 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy .map((row, index) => ( - - {row.framework === 'Langchain' && ( - langchain - )} - {row.framework === 'LlamaIndex' && ( - llamaIndex - )} - - - @@ -110,15 +95,15 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy - + {row.type} - + {row.description || ''} - +
    - + {row.badge && row.badge From 17a27d92a5e4c09b5b1ba865e7727fd560fb9f0c Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 7 Feb 2024 14:52:10 +0800 Subject: [PATCH 408/502] disable warnings --- packages/components/package.json | 4 ++-- packages/server/src/commands/start.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 7ab8009b..f50d3fed 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -61,9 +61,9 @@ "husky": "^8.0.3", "ioredis": "^5.3.2", "jsonpointer": "^5.0.1", - "langchain": "^0.1.7", + "langchain": "^0.1.14", "langfuse": "2.0.2", - "langfuse-langchain": "2.6.0", + "langfuse-langchain": "^2.6.2-alpha.0", "langsmith": "0.0.63", "linkifyjs": "^4.1.1", "llamaindex": "^0.0.48", diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index dfb20766..12907bbe 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -123,6 +123,9 @@ export default class Start extends Command { // Telemetry if (flags.DISABLE_FLOWISE_TELEMETRY) process.env.DISABLE_FLOWISE_TELEMETRY = flags.DISABLE_FLOWISE_TELEMETRY + // Disable langchain warnings + process.env.LANGCHAIN_SUPPRESS_MIGRATION_WARNINGS = 'true' + await (async () => { try { logger.info('Starting Flowise...') From 4be28c4050135b6fc2354e43efccf30d3787acbc Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Wed, 7 Feb 2024 19:32:48 +0800 Subject: [PATCH 409/502] add finish log --- packages/server/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7ceba556..bd44b739 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1151,6 +1151,7 @@ export class App { const limit = parseInt(req.query.limit as string) if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) + if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) res.json({ status: 'OK', links }) }) From 08c07802f5680c6de447ee1577dda498ba67699c Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 7 Feb 2024 07:08:55 -0500 Subject: [PATCH 410/502] Fix for Tool Opening. --- .../ui-component/table/MarketplaceTable.js | 22 ++++--------------- packages/ui/src/views/marketplaces/index.js | 2 ++ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.js b/packages/ui/src/ui-component/table/MarketplaceTable.js index ebbc8537..3b66409b 100644 --- a/packages/ui/src/ui-component/table/MarketplaceTable.js +++ b/packages/ui/src/ui-component/table/MarketplaceTable.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types' -import { useNavigate } from 'react-router-dom' import { styled } from '@mui/material/styles' import Table from '@mui/material/Table' import TableBody from '@mui/material/TableBody' @@ -31,8 +30,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })) -export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterByType, filterByFramework }) => { - const navigate = useNavigate() +export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterByType, filterByFramework, goToCanvas, goToTool }) => { const openTemplate = (selectedTemplate) => { if (selectedTemplate.flowData) { goToCanvas(selectedTemplate) @@ -41,20 +39,6 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy } } - const goToTool = (selectedTool) => { - const dialogProp = { - title: selectedTool.templateName, - type: 'TEMPLATE', - data: selectedTool - } - setToolDialogProps(dialogProp) - setShowToolDialog(true) - } - - const goToCanvas = (selectedChatflow) => { - navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) - } - return ( <> @@ -156,5 +140,7 @@ MarketplaceTable.propTypes = { filterFunction: PropTypes.func, filterByBadge: PropTypes.func, filterByType: PropTypes.func, - filterByFramework: PropTypes.func + filterByFramework: PropTypes.func, + goToTool: PropTypes.func, + goToCanvas: PropTypes.func } diff --git a/packages/ui/src/views/marketplaces/index.js b/packages/ui/src/views/marketplaces/index.js index 68258a47..e5a65cb9 100644 --- a/packages/ui/src/views/marketplaces/index.js +++ b/packages/ui/src/views/marketplaces/index.js @@ -423,6 +423,8 @@ const Marketplace = () => { filterByType={filterByType} filterByBadge={filterByBadge} filterByFramework={filterByFramework} + goToTool={goToTool} + goToCanvas={goToCanvas} /> )} From 3f0f0e4d28ff0aad1a74720153cf8235b1067161 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Wed, 7 Feb 2024 20:18:06 -0600 Subject: [PATCH 411/502] DynamoDB Chat Memory fix Fixes #1624 Please note comment in class BufferMemoryExtended for further discussion, if necessary --- .../components/nodes/memory/DynamoDb/DynamoDb.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 91c1d369..864ff7c8 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -142,18 +142,26 @@ interface DynamoDBSerializedChatMessage { } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { + tableName = '' sessionId = '' dynamodbClient: DynamoDBClient + dynamoKey = '' + partitionKey = '' constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) this.sessionId = fields.sessionId this.dynamodbClient = fields.dynamodbClient + + // These fields are coming in on chatHistory, but should they be on the dynamodbClient instead? + this.partitionKey = (fields?.chatHistory as unknown as { partitionKey: string }).partitionKey + this.dynamoKey = (fields?.chatHistory as unknown as { dynamoKey: string }).dynamoKey + this.tableName = (fields?.chatHistory as unknown as { tableName: string }).tableName } overrideDynamoKey(overrideSessionId = '') { - const existingDynamoKey = (this as any).dynamoKey - const partitionKey = (this as any).partitionKey + const existingDynamoKey = this.dynamoKey + const partitionKey = this.partitionKey let newDynamoKey: Record = {} @@ -210,7 +218,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { if (!this.dynamodbClient) return [] const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey - const tableName = (this as any).tableName + const tableName = this.tableName const messageAttributeName = (this as any).messageAttributeName const params: GetItemCommandInput = { From bc054d2fe1aa06de3fb44cc865fa39336041f103 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 8 Feb 2024 11:55:30 +0800 Subject: [PATCH 412/502] add fix for override session id --- .../components/nodes/memory/MongoDBMemory/MongoDBMemory.ts | 6 +++--- .../nodes/memory/MotorheadMemory/MotorheadMemory.ts | 6 +++--- .../memory/RedisBackedChatMemory/RedisBackedChatMemory.ts | 6 +++--- .../UpstashRedisBackedChatMemory.ts | 6 +++--- packages/components/nodes/memory/ZepMemory/ZepMemory.ts | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index b7309dcd..e2ee9f44 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -154,7 +154,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.collection) return [] - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId const document = await this.collection.findOne({ sessionId: id }) const messages = document?.messages || [] const baseMessages = messages.map(mapStoredMessageToChatMessage) @@ -164,7 +164,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { if (!this.collection) return - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId const input = msgArray.find((msg) => msg.type === 'userMessage') const output = msgArray.find((msg) => msg.type === 'apiMessage') @@ -196,7 +196,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async clearChatMessages(overrideSessionId = ''): Promise { if (!this.collection) return - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? 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 19506fc1..0b8f3800 100644 --- a/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts +++ b/packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts @@ -141,7 +141,7 @@ class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { } async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId try { const resp = await this.caller.call(fetch, `${this.url}/sessions/${id}/memory`, { //@ts-ignore @@ -172,7 +172,7 @@ class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { } async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? 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 } @@ -182,7 +182,7 @@ class MotorheadMemoryExtended extends MotorheadMemory implements MemoryMethods { } async clearChatMessages(overrideSessionId = ''): Promise { - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId await this.clear(id) } } diff --git a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts index c54e07b5..965b6760 100644 --- a/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts @@ -189,7 +189,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.redisClient) return [] - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId const rawStoredMessages = await this.redisClient.lrange(id, this.windowSize ? this.windowSize * -1 : 0, -1) const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message)) const baseMessages = orderedMessages.map(mapStoredMessageToChatMessage) @@ -199,7 +199,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { if (!this.redisClient) return - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId const input = msgArray.find((msg) => msg.type === 'userMessage') const output = msgArray.find((msg) => msg.type === 'apiMessage') @@ -219,7 +219,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async clearChatMessages(overrideSessionId = ''): Promise { if (!this.redisClient) return - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId await this.redisClient.del(id) await this.clear() } diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 3d7f6dbf..98a704ab 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -114,7 +114,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.redisClient) return [] - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? 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) @@ -125,7 +125,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { if (!this.redisClient) return - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId const input = msgArray.find((msg) => msg.type === 'userMessage') const output = msgArray.find((msg) => msg.type === 'apiMessage') @@ -145,7 +145,7 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async clearChatMessages(overrideSessionId = ''): Promise { if (!this.redisClient) return - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? 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 597eee8a..360a76d4 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -163,14 +163,14 @@ class ZepMemoryExtended extends ZepMemory implements MemoryMethods { } async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? 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 id = overrideSessionId ? 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 } @@ -180,7 +180,7 @@ class ZepMemoryExtended extends ZepMemory implements MemoryMethods { } async clearChatMessages(overrideSessionId = ''): Promise { - const id = overrideSessionId ?? this.sessionId + const id = overrideSessionId ? overrideSessionId : this.sessionId await this.clear(id) } } From d7f9c0738151368b990ec24044528c51f4c7a036 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Wed, 7 Feb 2024 22:07:12 -0600 Subject: [PATCH 413/502] return tableName, partiionKey, dynamoKey from initalizeDynamoDB --- .../nodes/memory/DynamoDb/DynamoDb.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 864ff7c8..c2085b91 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -117,7 +117,10 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P memoryKey: memoryKey ?? 'chat_history', chatHistory: dynamoDb, sessionId, - dynamodbClient: client + dynamodbClient: client, + tableName, + partitionKey, + dynamoKey: { [partitionKey]: { S: sessionId } } }) return memory } @@ -125,6 +128,9 @@ const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): P interface BufferMemoryExtendedInput { dynamodbClient: DynamoDBClient sessionId: string + tableName: string + partitionKey: string + dynamoKey: Record } interface DynamoDBSerializedChatMessage { @@ -143,10 +149,10 @@ interface DynamoDBSerializedChatMessage { class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { tableName = '' + partitionKey = '' + dynamoKey: Record sessionId = '' dynamodbClient: DynamoDBClient - dynamoKey = '' - partitionKey = '' constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) { super(fields) @@ -154,9 +160,9 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { this.dynamodbClient = fields.dynamodbClient // These fields are coming in on chatHistory, but should they be on the dynamodbClient instead? - this.partitionKey = (fields?.chatHistory as unknown as { partitionKey: string }).partitionKey - this.dynamoKey = (fields?.chatHistory as unknown as { dynamoKey: string }).dynamoKey - this.tableName = (fields?.chatHistory as unknown as { tableName: string }).tableName + this.tableName = fields.tableName + this.partitionKey = fields.partitionKey + this.dynamoKey = fields.dynamoKey } overrideDynamoKey(overrideSessionId = '') { From d0b1980482968b3cc4c165f391a28f81c00cf518 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Wed, 7 Feb 2024 22:08:41 -0600 Subject: [PATCH 414/502] Removes prior comment --- packages/components/nodes/memory/DynamoDb/DynamoDb.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index c2085b91..02ecc31f 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -158,8 +158,6 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { super(fields) this.sessionId = fields.sessionId this.dynamodbClient = fields.dynamodbClient - - // These fields are coming in on chatHistory, but should they be on the dynamodbClient instead? this.tableName = fields.tableName this.partitionKey = fields.partitionKey this.dynamoKey = fields.dynamoKey From dd89af8a098e48ef42556593127ad6b4b4e0b560 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Thu, 8 Feb 2024 09:04:46 -0600 Subject: [PATCH 415/502] refactor (this as any) usage --- .../nodes/memory/DynamoDb/DynamoDb.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts index 02ecc31f..22da396e 100644 --- a/packages/components/nodes/memory/DynamoDb/DynamoDb.ts +++ b/packages/components/nodes/memory/DynamoDb/DynamoDb.ts @@ -148,9 +148,10 @@ interface DynamoDBSerializedChatMessage { } class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { - tableName = '' - partitionKey = '' - dynamoKey: Record + private tableName = '' + private partitionKey = '' + private dynamoKey: Record + private messageAttributeName: string sessionId = '' dynamodbClient: DynamoDBClient @@ -221,9 +222,9 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { if (!this.dynamodbClient) return [] - const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey + const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : this.dynamoKey const tableName = this.tableName - const messageAttributeName = (this as any).messageAttributeName + const messageAttributeName = this.messageAttributeName const params: GetItemCommandInput = { TableName: tableName, @@ -248,9 +249,9 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { 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 dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : this.dynamoKey + const tableName = this.tableName + const messageAttributeName = this.messageAttributeName const input = msgArray.find((msg) => msg.type === 'userMessage') const output = msgArray.find((msg) => msg.type === 'apiMessage') @@ -271,8 +272,8 @@ class BufferMemoryExtended extends FlowiseMemory implements MemoryMethods { 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 dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : this.dynamoKey + const tableName = this.tableName const params: DeleteItemCommandInput = { TableName: tableName, From e110a49a32a4423e7d0127bd1f31f6995f84cdf0 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 8 Feb 2024 10:46:30 -0500 Subject: [PATCH 416/502] Migrating all TLS/SSL settings into additionalConfig so that there is consistency in how every function connects with Postgres in the same manner. --- .../nodes/vectorstores/Postgres/Postgres.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 375728e8..78e9cd75 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -60,13 +60,6 @@ class Postgres_VectorStores implements INode { name: 'database', type: 'string' }, - { - label: 'SSL Connection', - name: 'sslConnection', - type: 'boolean', - default: false, - optional: false - }, { label: 'Port', name: 'port', @@ -124,7 +117,6 @@ class Postgres_VectorStores implements INode { const docs = nodeData.inputs?.document as Document[] const embeddings = nodeData.inputs?.embeddings as Embeddings const additionalConfig = nodeData.inputs?.additionalConfig as string - const sslConnection = nodeData.inputs?.sslConnection as boolean let additionalConfiguration = {} if (additionalConfig) { @@ -143,7 +135,6 @@ class Postgres_VectorStores implements INode { username: user, password: password, database: nodeData.inputs?.database as string, - ssl: sslConnection } const args = { @@ -248,15 +239,7 @@ const similaritySearchVectorWithScore = async ( ORDER BY "_distance" ASC LIMIT $3;` - const poolOptions = { - host: postgresConnectionOptions.host, - port: postgresConnectionOptions.port, - user: postgresConnectionOptions.username, - password: postgresConnectionOptions.password, - database: postgresConnectionOptions.database, - ssl: postgresConnectionOptions.extra?.ssl - } - const pool = new Pool(poolOptions) + const pool = new Pool(postgresConnectionOptions) const conn = await pool.connect() const documents = await conn.query(queryString, [embeddingString, _filter, k]) From 9af4eaaa8c5a23e0421213bb9c82e49ff2bb2ee6 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 8 Feb 2024 10:53:33 -0500 Subject: [PATCH 417/502] Fixing linting issues. --- packages/components/nodes/vectorstores/Postgres/Postgres.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 78e9cd75..5c5ef813 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -134,7 +134,7 @@ class Postgres_VectorStores implements INode { port: nodeData.inputs?.port as number, username: user, password: password, - database: nodeData.inputs?.database as string, + database: nodeData.inputs?.database as string } const args = { From 4a75396325d4bcba3aa589a5fc41e3912cbd3ee4 Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 8 Feb 2024 11:26:45 -0500 Subject: [PATCH 418/502] So apparently TypeORMVectorStore requires the field 'username' while Pool expects it to be 'user'. Need to populate both to avoid error messages. --- packages/components/nodes/vectorstores/Postgres/Postgres.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index 5c5ef813..b3cc7918 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -189,7 +189,8 @@ class Postgres_VectorStores implements INode { type: 'postgres', host: nodeData.inputs?.host as string, port: nodeData.inputs?.port as number, - username: user, + username: user, // Required by TypeORMVectorStore + user: user, // Required by Pool in similaritySearchVectorWithScore password: password, database: nodeData.inputs?.database as string } From 907d5c7ef7a0124d6d4a3a44d33db521fcd5b49a Mon Sep 17 00:00:00 2001 From: Darien Kindlund Date: Thu, 8 Feb 2024 12:06:50 -0500 Subject: [PATCH 419/502] Bumping version since this will change might break legacy Postgres chatflows that used TLS/SSL. This will help users remind them to upgrade their corresponding nodes. --- packages/components/nodes/vectorstores/Postgres/Postgres.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/nodes/vectorstores/Postgres/Postgres.ts b/packages/components/nodes/vectorstores/Postgres/Postgres.ts index b3cc7918..be7784cc 100644 --- a/packages/components/nodes/vectorstores/Postgres/Postgres.ts +++ b/packages/components/nodes/vectorstores/Postgres/Postgres.ts @@ -24,7 +24,7 @@ class Postgres_VectorStores implements INode { constructor() { this.label = 'Postgres' this.name = 'postgres' - this.version = 2.0 + this.version = 3.0 this.type = 'Postgres' this.icon = 'postgres.svg' this.category = 'Vector Stores' From cf79176ca63fe47b570368a2caaadf8bf9ed4f50 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Fri, 9 Feb 2024 12:00:55 +0800 Subject: [PATCH 420/502] add missing steps into rate limit msg --- packages/server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 99bae048..48f93249 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -184,7 +184,7 @@ export class App { this.app.get('/api/v1/ip', (request, response) => { response.send({ ip: request.ip, - msg: 'Check the returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#rate-limit-setup-guide for more information.' + msg: 'Check returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 and restart Cloud-Hosted Flowise until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#cloud-hosted-rate-limit-setup-guide for more information.' }) }) From caf54bf31b2025d02283d357c1750a20c1ab8d49 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 9 Feb 2024 16:07:34 +0800 Subject: [PATCH 421/502] add document json output --- .../nodes/documentloaders/PlainText/PlainText.ts | 4 +++- .../nodes/documentloaders/Text/Text.ts | 4 +++- .../VectorStoreToDocument.ts | 2 ++ .../CohereRerankRetriever.ts | 4 +++- .../EmbeddingsFilterRetriever.ts | 4 +++- .../retrievers/HydeRetriever/HydeRetriever.ts | 4 +++- .../LLMFilterCompressionRetriever.ts | 4 +++- .../retrievers/RRFRetriever/RRFRetriever.ts | 4 +++- .../SimilarityThresholdRetriever.ts | 4 +++- .../marketplaces/chatflows/Claude LLM.json | 4 ++-- .../chatflows/Context Chat Engine.json | 8 ++++---- .../Conversational Retrieval QA Chain.json | 8 ++++---- .../server/marketplaces/chatflows/Local QnA.json | 8 ++++---- .../marketplaces/chatflows/Metadata Filter.json | 8 ++++---- .../chatflows/Multiple VectorDB.json | 16 ++++++++-------- packages/ui/src/utils/genericHelper.js | 3 +++ 16 files changed, 55 insertions(+), 34 deletions(-) diff --git a/packages/components/nodes/documentloaders/PlainText/PlainText.ts b/packages/components/nodes/documentloaders/PlainText/PlainText.ts index c2adceeb..c0c697a3 100644 --- a/packages/components/nodes/documentloaders/PlainText/PlainText.ts +++ b/packages/components/nodes/documentloaders/PlainText/PlainText.ts @@ -51,11 +51,13 @@ class PlainText_DocumentLoaders implements INode { { label: 'Document', name: 'document', - baseClasses: this.baseClasses + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/documentloaders/Text/Text.ts b/packages/components/nodes/documentloaders/Text/Text.ts index e41c5a9f..1eea709e 100644 --- a/packages/components/nodes/documentloaders/Text/Text.ts +++ b/packages/components/nodes/documentloaders/Text/Text.ts @@ -51,11 +51,13 @@ class Text_DocumentLoaders implements INode { { label: 'Document', name: 'document', - baseClasses: this.baseClasses + description: 'Array of document objects containing metadata and pageContent', + baseClasses: [...this.baseClasses, 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts b/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts index c087e000..27ef36f5 100644 --- a/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts +++ b/packages/components/nodes/documentloaders/VectorStoreToDocument/VectorStoreToDocument.ts @@ -51,11 +51,13 @@ class VectorStoreToDocument_DocumentLoaders implements INode { { label: 'Document', name: 'document', + description: 'Array of document objects containing metadata and pageContent', baseClasses: [...this.baseClasses, 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts index 442fdc7a..5e92505e 100644 --- a/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts +++ b/packages/components/nodes/retrievers/CohereRerankRetriever/CohereRerankRetriever.ts @@ -94,11 +94,13 @@ class CohereRerankRetriever_Retrievers implements INode { { label: 'Document', name: 'document', - baseClasses: ['Document'] + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts index d1049fa4..16d40790 100644 --- a/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts +++ b/packages/components/nodes/retrievers/EmbeddingsFilterRetriever/EmbeddingsFilterRetriever.ts @@ -78,11 +78,13 @@ class EmbeddingsFilterRetriever_Retrievers implements INode { { label: 'Document', name: 'document', - baseClasses: ['Document'] + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts index 10fff764..a7cd9829 100644 --- a/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts +++ b/packages/components/nodes/retrievers/HydeRetriever/HydeRetriever.ts @@ -140,11 +140,13 @@ Passage:` { label: 'Document', name: 'document', - baseClasses: ['Document'] + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts index 6b710cf3..9bace712 100644 --- a/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts +++ b/packages/components/nodes/retrievers/LLMFilterRetriever/LLMFilterCompressionRetriever.ts @@ -58,11 +58,13 @@ class LLMFilterCompressionRetriever_Retrievers implements INode { { label: 'Document', name: 'document', - baseClasses: ['Document'] + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts index ed15ed24..9788f095 100644 --- a/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts +++ b/packages/components/nodes/retrievers/RRFRetriever/RRFRetriever.ts @@ -89,11 +89,13 @@ class RRFRetriever_Retrievers implements INode { { label: 'Document', name: 'document', - baseClasses: ['Document'] + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts index 5f5a9ed0..6a6976a5 100644 --- a/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts +++ b/packages/components/nodes/retrievers/SimilarityThresholdRetriever/SimilarityThresholdRetriever.ts @@ -74,11 +74,13 @@ class SimilarityThresholdRetriever_Retrievers implements INode { { label: 'Document', name: 'document', - baseClasses: ['Document'] + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] }, { label: 'Text', name: 'text', + description: 'Concatenated string from pageContent of documents', baseClasses: ['string', 'json'] } ] diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index a0fd4e1a..48be286d 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -441,10 +441,10 @@ "type": "options", "options": [ { - "id": "plainText_0-output-document-Document", + "id": "plainText_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "plainText_0-output-text-string|json", diff --git a/packages/server/marketplaces/chatflows/Context Chat Engine.json b/packages/server/marketplaces/chatflows/Context Chat Engine.json index 15d3dade..3f1152f2 100644 --- a/packages/server/marketplaces/chatflows/Context Chat Engine.json +++ b/packages/server/marketplaces/chatflows/Context Chat Engine.json @@ -59,10 +59,10 @@ "type": "options", "options": [ { - "id": "textFile_0-output-document-Document", + "id": "textFile_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "textFile_0-output-text-string|json", @@ -851,11 +851,11 @@ }, { "source": "textFile_0", - "sourceHandle": "textFile_0-output-document-Document", + "sourceHandle": "textFile_0-output-document-Document|json", "target": "pineconeLlamaIndex_0", "targetHandle": "pineconeLlamaIndex_0-input-document-Document", "type": "buttonedge", - "id": "textFile_0-textFile_0-output-document-Document-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-document-Document", + "id": "textFile_0-textFile_0-output-document-Document|json-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-document-Document", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index e360141d..f76e89e6 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -235,10 +235,10 @@ "type": "options", "options": [ { - "id": "textFile_0-output-document-Document", + "id": "textFile_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "textFile_0-output-text-string|json", @@ -732,11 +732,11 @@ }, { "source": "textFile_0", - "sourceHandle": "textFile_0-output-document-Document", + "sourceHandle": "textFile_0-output-document-Document|json", "target": "pinecone_0", "targetHandle": "pinecone_0-input-document-Document", "type": "buttonedge", - "id": "textFile_0-textFile_0-output-document-Document-pinecone_0-pinecone_0-input-document-Document", + "id": "textFile_0-textFile_0-output-document-Document|json-pinecone_0-pinecone_0-input-document-Document", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Local QnA.json b/packages/server/marketplaces/chatflows/Local QnA.json index 2637f259..3e8b93f6 100644 --- a/packages/server/marketplaces/chatflows/Local QnA.json +++ b/packages/server/marketplaces/chatflows/Local QnA.json @@ -226,10 +226,10 @@ "type": "options", "options": [ { - "id": "textFile_0-output-document-Document", + "id": "textFile_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "textFile_0-output-text-string|json", @@ -651,11 +651,11 @@ }, { "source": "textFile_0", - "sourceHandle": "textFile_0-output-document-Document", + "sourceHandle": "textFile_0-output-document-Document|json", "target": "faiss_0", "targetHandle": "faiss_0-input-document-Document", "type": "buttonedge", - "id": "textFile_0-textFile_0-output-document-Document-faiss_0-faiss_0-input-document-Document", + "id": "textFile_0-textFile_0-output-document-Document|json-faiss_0-faiss_0-input-document-Document", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index 38ad9211..ed2efb9f 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -128,10 +128,10 @@ "type": "options", "options": [ { - "id": "textFile_0-output-document-Document", + "id": "textFile_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "textFile_0-output-text-string|json", @@ -838,11 +838,11 @@ }, { "source": "textFile_0", - "sourceHandle": "textFile_0-output-document-Document", + "sourceHandle": "textFile_0-output-document-Document|json", "target": "pinecone_0", "targetHandle": "pinecone_0-input-document-Document", "type": "buttonedge", - "id": "textFile_0-textFile_0-output-document-Document-pinecone_0-pinecone_0-input-document-Document", + "id": "textFile_0-textFile_0-output-document-Document|json-pinecone_0-pinecone_0-input-document-Document", "data": { "label": "" } diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index e8141ad7..b76270e7 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -966,10 +966,10 @@ "type": "options", "options": [ { - "id": "plainText_0-output-document-Document", + "id": "plainText_0-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "plainText_0-output-text-string|json", @@ -1503,10 +1503,10 @@ "type": "options", "options": [ { - "id": "plainText_1-output-document-Document", + "id": "plainText_1-output-document-Document|json", "name": "document", "label": "Document", - "type": "Document" + "type": "Document | json" }, { "id": "plainText_1-output-text-string|json", @@ -1723,11 +1723,11 @@ }, { "source": "plainText_0", - "sourceHandle": "plainText_0-output-document-Document", + "sourceHandle": "plainText_0-output-document-Document|json", "target": "redis_0", "targetHandle": "redis_0-input-document-Document", "type": "buttonedge", - "id": "plainText_0-plainText_0-output-document-Document-redis_0-redis_0-input-document-Document", + "id": "plainText_0-plainText_0-output-document-Document|json-redis_0-redis_0-input-document-Document", "data": { "label": "" } @@ -1778,11 +1778,11 @@ }, { "source": "plainText_1", - "sourceHandle": "plainText_1-output-document-Document", + "sourceHandle": "plainText_1-output-document-Document|json", "target": "faiss_0", "targetHandle": "faiss_0-input-document-Document", "type": "buttonedge", - "id": "plainText_1-plainText_1-output-document-Document-faiss_0-faiss_0-input-document-Document", + "id": "plainText_1-plainText_1-output-document-Document|json-faiss_0-faiss_0-input-document-Document", "data": { "label": "" } diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index eadbdb88..74dc9578 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -99,6 +99,7 @@ export const initNode = (nodeData, newNodeId) => { id: `${newNodeId}-output-${nodeData.outputs[j].name}-${baseClasses}`, name: nodeData.outputs[j].name, label: nodeData.outputs[j].label, + description: nodeData.outputs[j].description ?? '', type } options.push(newOutputOption) @@ -107,6 +108,7 @@ export const initNode = (nodeData, newNodeId) => { name: 'output', label: 'Output', type: 'options', + description: nodeData.outputs[0].description ?? '', options, default: nodeData.outputs[0].name } @@ -116,6 +118,7 @@ export const initNode = (nodeData, newNodeId) => { id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`, name: nodeData.name, label: nodeData.type, + description: nodeData.description ?? '', type: nodeData.baseClasses.join(' | ') } outputAnchors.push(newOutput) From e3c899230c036bbaa817952c07e8960e8fbdf5e6 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 10 Feb 2024 12:02:21 +0800 Subject: [PATCH 422/502] add FLOWISE_FILE_SIZE_LIMIT variable --- packages/server/.env.example | 1 + packages/server/src/index.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/server/.env.example b/packages/server/.env.example index ebc59cf3..a7a93345 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -20,6 +20,7 @@ PORT=3000 # FLOWISE_USERNAME=user # FLOWISE_PASSWORD=1234 # FLOWISE_SECRETKEY_OVERWRITE=myencryptionkey +# FLOWISE_FILE_SIZE_LIMIT=50mb # DEBUG=true # LOG_LEVEL=debug (error | warn | info | verbose | debug) # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 48f93249..973ce1ea 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -120,8 +120,9 @@ export class App { async config(socketIO?: Server) { // Limit is needed to allow sending/receiving base64 encoded string - this.app.use(express.json({ limit: '50mb' })) - this.app.use(express.urlencoded({ limit: '50mb', extended: true })) + const flowise_file_size_limit = process.env.FLOWISE_FILE_SIZE_LIMIT ?? '50mb' + this.app.use(express.json({ limit: flowise_file_size_limit })) + this.app.use(express.urlencoded({ limit: flowise_file_size_limit, extended: true })) if (process.env.NUMBER_OF_PROXIES && parseInt(process.env.NUMBER_OF_PROXIES) > 0) this.app.set('trust proxy', parseInt(process.env.NUMBER_OF_PROXIES)) From 702b8c1aab8474e09373874657efb3c061b71ada Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 10 Feb 2024 12:12:32 +0800 Subject: [PATCH 423/502] add FLOWISE_FILE_SIZE_LIMIT into docker env --- docker/.env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/.env.example b/docker/.env.example index a4beaf8a..84019299 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -21,6 +21,7 @@ LOG_PATH=/root/.flowise/logs # FLOWISE_USERNAME=user # FLOWISE_PASSWORD=1234 # FLOWISE_SECRETKEY_OVERWRITE=myencryptionkey +# FLOWISE_FILE_SIZE_LIMIT=50mb # DEBUG=true # LOG_LEVEL=debug (error | warn | info | verbose | debug) # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs From a132f51727559c82fa157bcb37047cb727599b92 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sun, 11 Feb 2024 17:37:48 +0800 Subject: [PATCH 424/502] add more FLOWISE_FILE_SIZE_LIMIT --- CONTRIBUTING-ZH.md | 1 + CONTRIBUTING.md | 1 + docker/docker-compose.yml | 1 + packages/server/src/commands/start.ts | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/CONTRIBUTING-ZH.md b/CONTRIBUTING-ZH.md index 7e35d194..e000da4f 100644 --- a/CONTRIBUTING-ZH.md +++ b/CONTRIBUTING-ZH.md @@ -123,6 +123,7 @@ Flowise 支持不同的环境变量来配置您的实例。您可以在 `package | PORT | Flowise 运行的 HTTP 端口 | 数字 | 3000 | | FLOWISE_USERNAME | 登录用户名 | 字符串 | | | FLOWISE_PASSWORD | 登录密码 | 字符串 | | +| FLOWISE_FILE_SIZE_LIMIT | 上传文件大小限制 | 字符串 | 50mb | | DEBUG | 打印组件的日志 | 布尔值 | | | LOG_PATH | 存储日志文件的位置 | 字符串 | `your-path/Flowise/logs` | | LOG_LEVEL | 日志的不同级别 | 枚举字符串: `error`, `info`, `verbose`, `debug` | `info` | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25a27e84..fdeb848b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,6 +127,7 @@ Flowise support different environment variables to configure your instance. You | IFRAME_ORIGINS | The allowed origins for iframe src embedding | String | | | FLOWISE_USERNAME | Username to login | String | | | FLOWISE_PASSWORD | Password to login | String | | +| FLOWISE_FILE_SIZE_LIMIT | Upload File Size Limit | String | 50mb | | DEBUG | Print logs from components | Boolean | | | LOG_PATH | Location where log files are stored | String | `your-path/Flowise/logs` | | LOG_LEVEL | Different levels of logs | Enum String: `error`, `info`, `verbose`, `debug` | `info` | diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 71bcfcfb..4da945fe 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,6 +10,7 @@ services: - IFRAME_ORIGINS=${IFRAME_ORIGINS} - FLOWISE_USERNAME=${FLOWISE_USERNAME} - FLOWISE_PASSWORD=${FLOWISE_PASSWORD} + - FLOWISE_FILE_SIZE_LIMIT=${FLOWISE_FILE_SIZE_LIMIT} - DEBUG=${DEBUG} - DATABASE_PATH=${DATABASE_PATH} - DATABASE_TYPE=${DATABASE_TYPE} diff --git a/packages/server/src/commands/start.ts b/packages/server/src/commands/start.ts index dfb20766..a649dba6 100644 --- a/packages/server/src/commands/start.ts +++ b/packages/server/src/commands/start.ts @@ -18,6 +18,7 @@ export default class Start extends Command { static flags = { FLOWISE_USERNAME: Flags.string(), FLOWISE_PASSWORD: Flags.string(), + FLOWISE_FILE_SIZE_LIMIT: Flags.string(), PORT: Flags.string(), CORS_ORIGINS: Flags.string(), IFRAME_ORIGINS: Flags.string(), @@ -91,6 +92,9 @@ export default class Start extends Command { if (flags.FLOWISE_PASSWORD) process.env.FLOWISE_PASSWORD = flags.FLOWISE_PASSWORD if (flags.APIKEY_PATH) process.env.APIKEY_PATH = flags.APIKEY_PATH + //API Configuration + if (flags.FLOWISE_FILE_SIZE_LIMIT) process.env.FLOWISE_FILE_SIZE_LIMIT = flags.FLOWISE_FILE_SIZE_LIMIT + // Credentials if (flags.SECRETKEY_PATH) process.env.SECRETKEY_PATH = flags.SECRETKEY_PATH if (flags.FLOWISE_SECRETKEY_OVERWRITE) process.env.FLOWISE_SECRETKEY_OVERWRITE = flags.FLOWISE_SECRETKEY_OVERWRITE From 5471a4c9aa48068a7aecf6433dc7e7da6bd8d973 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 12 Feb 2024 12:01:19 +0530 Subject: [PATCH 425/502] Show error when relative links method is not set and allow 0 as limit value --- .../nodes/documentloaders/Cheerio/Cheerio.ts | 4 +- .../documentloaders/Playwright/Playwright.ts | 4 +- .../documentloaders/Puppeteer/Puppeteer.ts | 4 +- packages/server/src/index.ts | 4 ++ .../dialog/ManageScrapedLinksDialog.js | 50 +++++++++++++++++-- 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts index 6af1f9a9..48ae85bc 100644 --- a/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts +++ b/packages/components/nodes/documentloaders/Cheerio/Cheerio.ts @@ -126,7 +126,9 @@ class Cheerio_DocumentLoaders implements INode { let docs = [] if (relativeLinksMethod) { if (process.env.DEBUG === 'true') options.logger.info(`Start ${relativeLinksMethod}`) - if (!limit) limit = 10 + // if limit is 0 we don't want it to default to 10 so we check explicitly for null or undefined + // so when limit is 0 we can fetch all the links + if (limit === null || limit === undefined) limit = 10 else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = selectedLinks && selectedLinks.length > 0 diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index 2ba60d0f..55fa9608 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -167,7 +167,9 @@ class Playwright_DocumentLoaders implements INode { let docs = [] if (relativeLinksMethod) { if (process.env.DEBUG === 'true') options.logger.info(`Start ${relativeLinksMethod}`) - if (!limit) limit = 10 + // if limit is 0 we don't want it to default to 10 so we check explicitly for null or undefined + // so when limit is 0 we can fetch all the links + if (limit === null || limit === undefined) limit = 10 else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = selectedLinks && selectedLinks.length > 0 diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 1f8c8f3f..90b5a277 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -168,7 +168,9 @@ class Puppeteer_DocumentLoaders implements INode { let docs = [] if (relativeLinksMethod) { if (process.env.DEBUG === 'true') options.logger.info(`Start ${relativeLinksMethod}`) - if (!limit) limit = 10 + // if limit is 0 we don't want it to default to 10 so we check explicitly for null or undefined + // so when limit is 0 we can fetch all the links + if (limit === null || limit === undefined) limit = 10 else if (limit < 0) throw new Error('Limit cannot be less than 0') const pages: string[] = selectedLinks && selectedLinks.length > 0 diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7ceba556..b994ba62 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1148,6 +1148,10 @@ export class App { this.app.get('/api/v1/fetch-links', async (req: Request, res: Response) => { const url = decodeURIComponent(req.query.url as string) const relativeLinksMethod = req.query.relativeLinksMethod as string + if (!relativeLinksMethod) { + return res.status(500).send('Please choose a Relative Links Method in Additional Parameters.') + } + const limit = parseInt(req.query.limit as string) if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) diff --git a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js index 9a846ce9..a4199504 100644 --- a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js +++ b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.js @@ -16,7 +16,7 @@ import { Stack, Typography } from '@mui/material' -import { IconTrash } from '@tabler/icons' +import { IconTrash, IconX } from '@tabler/icons' import PerfectScrollbar from 'react-perfect-scrollbar' import { BackdropLoader } from 'ui-component/loading/BackdropLoader' @@ -24,12 +24,23 @@ import { StyledButton } from 'ui-component/button/StyledButton' import scraperApi from 'api/scraper' -import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import useNotifier from 'utils/useNotifier' + +import { + HIDE_CANVAS_DIALOG, + SHOW_CANVAS_DIALOG, + enqueueSnackbar as enqueueSnackbarAction, + closeSnackbar as closeSnackbarAction +} from 'store/actions' const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() + useNotifier() + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const [loading, setLoading] = useState(false) const [selectedLinks, setSelectedLinks] = useState([]) const [url, setUrl] = useState('') @@ -53,9 +64,38 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => { const handleFetchLinks = async () => { setLoading(true) - const fetchLinksResp = await scraperApi.fetchLinks(url, dialogProps.relativeLinksMethod, dialogProps.limit) - if (fetchLinksResp.data) { - setSelectedLinks(fetchLinksResp.data.links) + try { + const fetchLinksResp = await scraperApi.fetchLinks(url, dialogProps.relativeLinksMethod, dialogProps.limit) + if (fetchLinksResp.data) { + setSelectedLinks(fetchLinksResp.data.links) + enqueueSnackbar({ + message: 'Successfully fetched links', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) } setLoading(false) } From 9072e694ca2c01f079088306f88f9e4565850075 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 12 Feb 2024 13:19:17 +0530 Subject: [PATCH 426/502] Return uploads config in public chatbot config endpoint --- packages/server/src/index.ts | 130 +++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 998801eb..218ae100 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -404,8 +404,9 @@ export class App { if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) if (chatflow.chatbotConfig) { try { + const uploadsConfig = await this.areUploadsEnabled(req.params.id) const parsedConfig = JSON.parse(chatflow.chatbotConfig) - return res.json(parsedConfig) + return res.json({ ...parsedConfig, ...uploadsConfig }) } catch (e) { return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) } @@ -521,66 +522,9 @@ export class App { // Check if chatflow valid for uploads this.app.get('/api/v1/chatflows-uploads/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - - const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent'] - const uploadProcessingNodes = ['chatOpenAI'] - try { - const flowObj = JSON.parse(chatflow.flowData) - const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = [] - - let isSpeechToTextEnabled = false - if (chatflow.speechToText) { - const speechToTextProviders = JSON.parse(chatflow.speechToText) - for (const provider in speechToTextProviders) { - const providerObj = speechToTextProviders[provider] - if (providerObj.status) { - isSpeechToTextEnabled = true - break - } - } - } - - let isImageUploadAllowed = false - const nodes: IReactFlowNode[] = flowObj.nodes - - /* - * Condition for isImageUploadAllowed - * 1.) one of the uploadAllowedNodes exists - * 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON - */ - if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) { - return res.json({ - isSpeechToTextEnabled, - isImageUploadAllowed: false, - imgUploadSizeAndTypes - }) - } - - nodes.forEach((node: IReactFlowNode) => { - if (uploadProcessingNodes.indexOf(node.data.name) > -1) { - // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties - node.data.inputParams.map((param: INodeParams) => { - if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) { - imgUploadSizeAndTypes.push({ - fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'), - maxUploadSize: 5 - }) - isImageUploadAllowed = true - } - }) - } - }) - - return res.json({ - isSpeechToTextEnabled, - isImageUploadAllowed, - imgUploadSizeAndTypes - }) + const uploadsConfig = await this.areUploadsEnabled(req.params.id) + return res.json(uploadsConfig) } catch (e) { return res.status(500).send(e) } @@ -1542,6 +1486,72 @@ export class App { return false } + /** + * Method that checks if uploads are enabled in the chatflow + * @param {string} chatflowid + */ + async areUploadsEnabled(chatflowid: string): Promise { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowid + }) + if (!chatflow) return `Chatflow ${chatflowid} not found` + + const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent'] + const uploadProcessingNodes = ['chatOpenAI'] + + const flowObj = JSON.parse(chatflow.flowData) + const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = [] + + let isSpeechToTextEnabled = false + if (chatflow.speechToText) { + const speechToTextProviders = JSON.parse(chatflow.speechToText) + for (const provider in speechToTextProviders) { + const providerObj = speechToTextProviders[provider] + if (providerObj.status) { + isSpeechToTextEnabled = true + break + } + } + } + + let isImageUploadAllowed = false + const nodes: IReactFlowNode[] = flowObj.nodes + + /* + * Condition for isImageUploadAllowed + * 1.) one of the uploadAllowedNodes exists + * 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON + */ + if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) { + return { + isSpeechToTextEnabled, + isImageUploadAllowed: false, + imgUploadSizeAndTypes + } + } + + nodes.forEach((node: IReactFlowNode) => { + if (uploadProcessingNodes.indexOf(node.data.name) > -1) { + // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties + node.data.inputParams.map((param: INodeParams) => { + if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) { + imgUploadSizeAndTypes.push({ + fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'), + maxUploadSize: 5 + }) + isImageUploadAllowed = true + } + }) + } + }) + + return { + isSpeechToTextEnabled, + isImageUploadAllowed, + imgUploadSizeAndTypes + } + } + /** * Method that get chat messages. * @param {string} chatflowid From 0a54db71c121c8c24d89f1a671df6a0296dd38ae Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 12 Feb 2024 23:56:46 +0530 Subject: [PATCH 427/502] Update how uploads config is sent --- packages/server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 218ae100..44c6eb34 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -406,7 +406,7 @@ export class App { try { const uploadsConfig = await this.areUploadsEnabled(req.params.id) const parsedConfig = JSON.parse(chatflow.chatbotConfig) - return res.json({ ...parsedConfig, ...uploadsConfig }) + return res.json({ ...parsedConfig, uploads: uploadsConfig }) } catch (e) { return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) } From 55c2a8612b9065a1ee5d2cbafb0dfb34fed53710 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Mon, 12 Feb 2024 20:12:54 -0600 Subject: [PATCH 428/502] fixes bug where querying by chatId results in no records previous logic for where clause of memoryType checked for existence of chatId and would query for memoryType is null This where clause logic results in an empty result for any request to the endpoint that filters by chatId --- packages/server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 07797f32..a6566a5b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1453,7 +1453,7 @@ export class App { chatflowid, chatType, chatId, - memoryType: memoryType ?? (chatId ? IsNull() : undefined), + memoryType: memoryType ?? undefined, sessionId: sessionId ?? undefined, createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined }, From a6abd593a62c743f1c2949e9b012b7017aff6e37 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Mon, 12 Feb 2024 20:53:40 -0600 Subject: [PATCH 429/502] Allows query chatmessage API endpoint by messageId This will be useful when the exact message is required by another system. --- packages/server/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 07797f32..284c88d6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -511,6 +511,7 @@ export class App { const chatId = req.query?.chatId as string | undefined const memoryType = req.query?.memoryType as string | undefined const sessionId = req.query?.sessionId as string | undefined + const messageId = req.query?.messageId as string | undefined const startDate = req.query?.startDate as string | undefined const endDate = req.query?.endDate as string | undefined let chatTypeFilter = req.query?.chatType as chatType | undefined @@ -538,7 +539,8 @@ export class App { memoryType, sessionId, startDate, - endDate + endDate, + messageId ) return res.json(chatmessages) }) @@ -1440,7 +1442,8 @@ export class App { memoryType?: string, sessionId?: string, startDate?: string, - endDate?: string + endDate?: string, + messageId?: string ): Promise { let fromDate if (startDate) fromDate = new Date(startDate) @@ -1455,7 +1458,8 @@ export class App { chatId, memoryType: memoryType ?? (chatId ? IsNull() : undefined), sessionId: sessionId ?? undefined, - createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined + createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined, + id: messageId ?? undefined }, order: { createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' From 11219c65490f289c4106337461a72fa0337a2f1d Mon Sep 17 00:00:00 2001 From: Ilango Date: Tue, 13 Feb 2024 16:23:12 +0530 Subject: [PATCH 430/502] Fix audio recording not sending when recording stops --- packages/ui/src/views/chatmessage/ChatMessage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 89fd941a..73d1c4a6 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -353,7 +353,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews if (!promptStarterInput && userInput.trim() === '') { const containsAudio = previews.filter((item) => item.type === 'audio').length > 0 - if (!(previews.length > 1 && containsAudio)) { + if (!(previews.length >= 1 && containsAudio)) { return } } @@ -585,7 +585,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews useEffect(() => { // wait for audio recording to load and then send const containsAudio = previews.filter((item) => item.type === 'audio').length > 0 - if (previews.length > 1 && containsAudio) { + if (previews.length >= 1 && containsAudio) { setIsRecording(false) setRecordingNotSupported(false) handlePromptClick('') From 6de1e8aceccccf86e2bf78a13d7c533ea2a0679b Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Tue, 13 Feb 2024 10:44:05 -0600 Subject: [PATCH 431/502] handles invalid values for startDate, endDate also sets time window to beginning of the date or end of the date. This will be helpful with the timezone gap between the server (usually UTC) and the client (localized timezone) Ideal date solution to consider for the future would be to adjust the timestamp query based upon the server timezone setup. This becomes particularly helpful when the client is filtering by the end date but they are in a timezone behind UTC after the UTC has advanced to the next date. For example, being in Pacific time and querying for the current date after 4PM will result in not finding records that have been created after 4PM Pacific b/c the server timestamp (in UTC) will be the next calendar date. --- packages/server/src/index.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index a6566a5b..d3453320 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1442,11 +1442,20 @@ export class App { startDate?: string, endDate?: string ): Promise { + const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { + const date = new Date(dateTimeStr) + if (isNaN(date.getTime())) { + return undefined + } + setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) + return date + } + let fromDate - if (startDate) fromDate = new Date(startDate) + if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start') let toDate - if (endDate) toDate = new Date(endDate) + if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end') return await this.AppDataSource.getRepository(ChatMessage).find({ where: { From 4d7c7d6ef5df5db53576ddd9e80bbffbd6acd952 Mon Sep 17 00:00:00 2001 From: Jared Tracy Date: Tue, 13 Feb 2024 11:11:53 -0600 Subject: [PATCH 432/502] Changes getMessage date logic from Between to >= and <= to fix issue if invalid startDate is passed in also cleans up imports from typeorm lib --- packages/server/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d3453320..f401bbc1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -10,7 +10,7 @@ import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' import { v4 as uuidv4 } from 'uuid' import OpenAI from 'openai' -import { Between, IsNull, FindOptionsWhere } from 'typeorm' +import { FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual } from 'typeorm' import { IChatFlow, IncomingInput, @@ -1464,7 +1464,8 @@ export class App { chatId, memoryType: memoryType ?? undefined, sessionId: sessionId ?? undefined, - createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined + ...(fromDate && { createdDate: MoreThanOrEqual(fromDate) }), + ...(toDate && { createdDate: LessThanOrEqual(toDate) }) }, order: { createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' From 205670375d43123b2cddf5f6c038a06e030d28ba Mon Sep 17 00:00:00 2001 From: Ilango Date: Wed, 14 Feb 2024 14:37:12 +0530 Subject: [PATCH 433/502] Check if uploads are enabled/changed on chatflow save and update chatbot config --- packages/server/src/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 44c6eb34..73e8d112 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -402,9 +402,9 @@ export class App { id: req.params.id }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + const uploadsConfig = await this.getUploadsConfig(req.params.id) if (chatflow.chatbotConfig) { try { - const uploadsConfig = await this.areUploadsEnabled(req.params.id) const parsedConfig = JSON.parse(chatflow.chatbotConfig) return res.json({ ...parsedConfig, uploads: uploadsConfig }) } catch (e) { @@ -447,6 +447,14 @@ export class App { const updateChatFlow = new ChatFlow() Object.assign(updateChatFlow, body) + // check if image uploads or speech have been enabled and update chatbotConfig + const uploadsConfig = await this.getUploadsConfig(req.params.id) + if (uploadsConfig) { + // if there's existing chatbotConfig, merge uploadsConfig with it + // if not just add uploadsConfig to chatbotConfig + Object.assign(updateChatFlow, { chatbotConfig: { ...((chatflow.chatbotConfig ?? {}) as object), ...uploadsConfig } }) + } + updateChatFlow.id = chatflow.id createRateLimiter(updateChatFlow) @@ -523,7 +531,7 @@ export class App { // Check if chatflow valid for uploads this.app.get('/api/v1/chatflows-uploads/:id', async (req: Request, res: Response) => { try { - const uploadsConfig = await this.areUploadsEnabled(req.params.id) + const uploadsConfig = await this.getUploadsConfig(req.params.id) return res.json(uploadsConfig) } catch (e) { return res.status(500).send(e) @@ -1490,7 +1498,7 @@ export class App { * Method that checks if uploads are enabled in the chatflow * @param {string} chatflowid */ - async areUploadsEnabled(chatflowid: string): Promise { + async getUploadsConfig(chatflowid: string): Promise { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid }) From 56b21862a3444fbe8a52aecb24ced0c3cd0194ca Mon Sep 17 00:00:00 2001 From: Ilango Date: Wed, 14 Feb 2024 15:07:13 +0530 Subject: [PATCH 434/502] Send uploads config if available, even when chatbot config is not available --- packages/server/src/index.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 73e8d112..be1e1cb8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -403,9 +403,11 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) const uploadsConfig = await this.getUploadsConfig(req.params.id) - if (chatflow.chatbotConfig) { + // even if chatbotConfig is not set but uploads are enabled + // send uploadsConfig to the chatbot + if (chatflow.chatbotConfig || uploadsConfig) { try { - const parsedConfig = JSON.parse(chatflow.chatbotConfig) + const parsedConfig = chatflow.chatbotConfig ? JSON.parse(chatflow.chatbotConfig) : {} return res.json({ ...parsedConfig, uploads: uploadsConfig }) } catch (e) { return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) @@ -447,14 +449,6 @@ export class App { const updateChatFlow = new ChatFlow() Object.assign(updateChatFlow, body) - // check if image uploads or speech have been enabled and update chatbotConfig - const uploadsConfig = await this.getUploadsConfig(req.params.id) - if (uploadsConfig) { - // if there's existing chatbotConfig, merge uploadsConfig with it - // if not just add uploadsConfig to chatbotConfig - Object.assign(updateChatFlow, { chatbotConfig: { ...((chatflow.chatbotConfig ?? {}) as object), ...uploadsConfig } }) - } - updateChatFlow.id = chatflow.id createRateLimiter(updateChatFlow) From 778e024c02f5fd292546b3416f9edc77dbba558b Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 14 Feb 2024 18:40:35 +0800 Subject: [PATCH 435/502] update query engine tool --- .../nodes/engine/QueryEngine/QueryEngine.ts | 94 ++- .../SubQuestionQueryEngine.ts | 160 ++-- .../tools/QueryEngineTool/QueryEngineTool.ts | 21 +- .../marketplaces/chatflows/Query Engine.json | 8 +- .../chatflows/SubQuestion Query Engine.json | 766 ++++++++++-------- 5 files changed, 581 insertions(+), 468 deletions(-) diff --git a/packages/components/nodes/engine/QueryEngine/QueryEngine.ts b/packages/components/nodes/engine/QueryEngine/QueryEngine.ts index bd6e040d..8ced3fcc 100644 --- a/packages/components/nodes/engine/QueryEngine/QueryEngine.ts +++ b/packages/components/nodes/engine/QueryEngine/QueryEngine.ts @@ -28,12 +28,12 @@ class QueryEngine_LlamaIndex implements INode { constructor(fields?: { sessionId?: string }) { this.label = 'Query Engine' this.name = 'queryEngine' - this.version = 1.0 + this.version = 2.0 this.type = 'QueryEngine' this.icon = 'query-engine.png' this.category = 'Engine' this.description = 'Simple query engine built to answer question over your data, without memory' - this.baseClasses = [this.type] + this.baseClasses = [this.type, 'BaseQueryEngine'] this.tags = ['LlamaIndex'] this.inputs = [ { @@ -59,52 +59,13 @@ class QueryEngine_LlamaIndex implements INode { this.sessionId = fields?.sessionId } - async init(): Promise { - return null + async init(nodeData: INodeData): Promise { + return prepareEngine(nodeData) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever - const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer - - let queryEngine = new RetrieverQueryEngine(vectorStoreRetriever) - - if (responseSynthesizerObj) { - if (responseSynthesizerObj.type === 'TreeSummarize') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new TreeSummarize(vectorStoreRetriever.serviceContext, responseSynthesizerObj.textQAPromptTemplate), - serviceContext: vectorStoreRetriever.serviceContext - }) - queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) - } else if (responseSynthesizerObj.type === 'CompactAndRefine') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new CompactAndRefine( - vectorStoreRetriever.serviceContext, - responseSynthesizerObj.textQAPromptTemplate, - responseSynthesizerObj.refinePromptTemplate - ), - serviceContext: vectorStoreRetriever.serviceContext - }) - queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) - } else if (responseSynthesizerObj.type === 'Refine') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new Refine( - vectorStoreRetriever.serviceContext, - responseSynthesizerObj.textQAPromptTemplate, - responseSynthesizerObj.refinePromptTemplate - ), - serviceContext: vectorStoreRetriever.serviceContext - }) - queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) - } else if (responseSynthesizerObj.type === 'SimpleResponseBuilder') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new SimpleResponseBuilder(vectorStoreRetriever.serviceContext), - serviceContext: vectorStoreRetriever.serviceContext - }) - queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) - } - } + const queryEngine = prepareEngine(nodeData) let text = '' let sourceDocuments: ICommonObject[] = [] @@ -140,4 +101,49 @@ class QueryEngine_LlamaIndex implements INode { } } +const prepareEngine = (nodeData: INodeData) => { + const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever + const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer + + let queryEngine = new RetrieverQueryEngine(vectorStoreRetriever) + + if (responseSynthesizerObj) { + if (responseSynthesizerObj.type === 'TreeSummarize') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new TreeSummarize(vectorStoreRetriever.serviceContext, responseSynthesizerObj.textQAPromptTemplate), + serviceContext: vectorStoreRetriever.serviceContext + }) + queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) + } else if (responseSynthesizerObj.type === 'CompactAndRefine') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new CompactAndRefine( + vectorStoreRetriever.serviceContext, + responseSynthesizerObj.textQAPromptTemplate, + responseSynthesizerObj.refinePromptTemplate + ), + serviceContext: vectorStoreRetriever.serviceContext + }) + queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) + } else if (responseSynthesizerObj.type === 'Refine') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new Refine( + vectorStoreRetriever.serviceContext, + responseSynthesizerObj.textQAPromptTemplate, + responseSynthesizerObj.refinePromptTemplate + ), + serviceContext: vectorStoreRetriever.serviceContext + }) + queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) + } else if (responseSynthesizerObj.type === 'SimpleResponseBuilder') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new SimpleResponseBuilder(vectorStoreRetriever.serviceContext), + serviceContext: vectorStoreRetriever.serviceContext + }) + queryEngine = new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer) + } + } + + return queryEngine +} + module.exports = { nodeClass: QueryEngine_LlamaIndex } diff --git a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts index a872c0a2..a2a1e029 100644 --- a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts +++ b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts @@ -33,13 +33,13 @@ class SubQuestionQueryEngine_LlamaIndex implements INode { constructor(fields?: { sessionId?: string }) { this.label = 'Sub Question Query Engine' this.name = 'subQuestionQueryEngine' - this.version = 1.0 + this.version = 2.0 this.type = 'SubQuestionQueryEngine' this.icon = 'subQueryEngine.svg' this.category = 'Engine' this.description = 'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response' - this.baseClasses = [this.type] + this.baseClasses = [this.type, 'BaseQueryEngine'] this.tags = ['LlamaIndex'] this.inputs = [ { @@ -76,85 +76,13 @@ class SubQuestionQueryEngine_LlamaIndex implements INode { this.sessionId = fields?.sessionId } - async init(): Promise { - return null + async init(nodeData: INodeData): Promise { + return prepareEngine(nodeData) } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean - const embeddings = nodeData.inputs?.embeddings as BaseEmbedding - const model = nodeData.inputs?.model - - const serviceContext = serviceContextFromDefaults({ - llm: model, - embedModel: embeddings - }) - - let queryEngineTools = nodeData.inputs?.queryEngineTools as QueryEngineTool[] - queryEngineTools = flatten(queryEngineTools) - - let queryEngine = SubQuestionQueryEngine.fromDefaults({ - serviceContext, - queryEngineTools, - questionGen: new LLMQuestionGenerator({ llm: model }) - }) - - const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer - if (responseSynthesizerObj) { - if (responseSynthesizerObj.type === 'TreeSummarize') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new TreeSummarize(serviceContext, responseSynthesizerObj.textQAPromptTemplate), - serviceContext - }) - queryEngine = SubQuestionQueryEngine.fromDefaults({ - responseSynthesizer, - serviceContext, - queryEngineTools, - questionGen: new LLMQuestionGenerator({ llm: model }) - }) - } else if (responseSynthesizerObj.type === 'CompactAndRefine') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new CompactAndRefine( - serviceContext, - responseSynthesizerObj.textQAPromptTemplate, - responseSynthesizerObj.refinePromptTemplate - ), - serviceContext - }) - queryEngine = SubQuestionQueryEngine.fromDefaults({ - responseSynthesizer, - serviceContext, - queryEngineTools, - questionGen: new LLMQuestionGenerator({ llm: model }) - }) - } else if (responseSynthesizerObj.type === 'Refine') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new Refine( - serviceContext, - responseSynthesizerObj.textQAPromptTemplate, - responseSynthesizerObj.refinePromptTemplate - ), - serviceContext - }) - queryEngine = SubQuestionQueryEngine.fromDefaults({ - responseSynthesizer, - serviceContext, - queryEngineTools, - questionGen: new LLMQuestionGenerator({ llm: model }) - }) - } else if (responseSynthesizerObj.type === 'SimpleResponseBuilder') { - const responseSynthesizer = new ResponseSynthesizer({ - responseBuilder: new SimpleResponseBuilder(serviceContext), - serviceContext - }) - queryEngine = SubQuestionQueryEngine.fromDefaults({ - responseSynthesizer, - serviceContext, - queryEngineTools, - questionGen: new LLMQuestionGenerator({ llm: model }) - }) - } - } + const queryEngine = prepareEngine(nodeData) let text = '' let sourceDocuments: ICommonObject[] = [] @@ -190,4 +118,82 @@ class SubQuestionQueryEngine_LlamaIndex implements INode { } } +const prepareEngine = (nodeData: INodeData) => { + const embeddings = nodeData.inputs?.embeddings as BaseEmbedding + const model = nodeData.inputs?.model + + const serviceContext = serviceContextFromDefaults({ + llm: model, + embedModel: embeddings + }) + + let queryEngineTools = nodeData.inputs?.queryEngineTools as QueryEngineTool[] + queryEngineTools = flatten(queryEngineTools) + + let queryEngine = SubQuestionQueryEngine.fromDefaults({ + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + + const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer + if (responseSynthesizerObj) { + if (responseSynthesizerObj.type === 'TreeSummarize') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new TreeSummarize(serviceContext, responseSynthesizerObj.textQAPromptTemplate), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } else if (responseSynthesizerObj.type === 'CompactAndRefine') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new CompactAndRefine( + serviceContext, + responseSynthesizerObj.textQAPromptTemplate, + responseSynthesizerObj.refinePromptTemplate + ), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } else if (responseSynthesizerObj.type === 'Refine') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new Refine( + serviceContext, + responseSynthesizerObj.textQAPromptTemplate, + responseSynthesizerObj.refinePromptTemplate + ), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } else if (responseSynthesizerObj.type === 'SimpleResponseBuilder') { + const responseSynthesizer = new ResponseSynthesizer({ + responseBuilder: new SimpleResponseBuilder(serviceContext), + serviceContext + }) + queryEngine = SubQuestionQueryEngine.fromDefaults({ + responseSynthesizer, + serviceContext, + queryEngineTools, + questionGen: new LLMQuestionGenerator({ llm: model }) + }) + } + } + + return queryEngine +} + module.exports = { nodeClass: SubQuestionQueryEngine_LlamaIndex } diff --git a/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts b/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts index 163eff76..cc25fa6f 100644 --- a/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts +++ b/packages/components/nodes/tools/QueryEngineTool/QueryEngineTool.ts @@ -1,5 +1,5 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { VectorStoreIndex } from 'llamaindex' +import { BaseQueryEngine } from 'llamaindex' class QueryEngine_Tools implements INode { label: string @@ -16,7 +16,7 @@ class QueryEngine_Tools implements INode { constructor() { this.label = 'QueryEngine Tool' this.name = 'queryEngineToolLlamaIndex' - this.version = 1.0 + this.version = 2.0 this.type = 'QueryEngineTool' this.icon = 'queryEngineTool.svg' this.category = 'Tools' @@ -25,9 +25,9 @@ class QueryEngine_Tools implements INode { this.baseClasses = [this.type] this.inputs = [ { - label: 'Vector Store Index', - name: 'vectorStoreIndex', - type: 'VectorStoreIndex' + label: 'Base QueryEngine', + name: 'baseQueryEngine', + type: 'BaseQueryEngine' }, { label: 'Tool Name', @@ -45,20 +45,15 @@ class QueryEngine_Tools implements INode { } async init(nodeData: INodeData): Promise { - const vectorStoreIndex = nodeData.inputs?.vectorStoreIndex as VectorStoreIndex + const baseQueryEngine = nodeData.inputs?.baseQueryEngine as BaseQueryEngine const toolName = nodeData.inputs?.toolName as string const toolDesc = nodeData.inputs?.toolDesc as string const queryEngineTool = { - queryEngine: vectorStoreIndex.asQueryEngine({ - preFilters: { - ...(vectorStoreIndex as any).metadatafilter - } - }), + queryEngine: baseQueryEngine, metadata: { name: toolName, description: toolDesc - }, - vectorStoreIndex + } } return queryEngineTool diff --git a/packages/server/marketplaces/chatflows/Query Engine.json b/packages/server/marketplaces/chatflows/Query Engine.json index b3a3c292..63bbabfe 100644 --- a/packages/server/marketplaces/chatflows/Query Engine.json +++ b/packages/server/marketplaces/chatflows/Query Engine.json @@ -16,10 +16,10 @@ "data": { "id": "queryEngine_0", "label": "Query Engine", - "version": 1, + "version": 2, "name": "queryEngine", "type": "QueryEngine", - "baseClasses": ["QueryEngine"], + "baseClasses": ["QueryEngine", "BaseQueryEngine"], "tags": ["LlamaIndex"], "category": "Engine", "description": "Simple query engine built to answer question over your data, without memory", @@ -55,10 +55,10 @@ }, "outputAnchors": [ { - "id": "queryEngine_0-output-queryEngine-QueryEngine", + "id": "queryEngine_0-output-queryEngine-QueryEngine|BaseQueryEngine", "name": "queryEngine", "label": "QueryEngine", - "type": "QueryEngine" + "type": "QueryEngine | BaseQueryEngine" } ], "outputs": {}, diff --git a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json index 620712c4..f8f6f430 100644 --- a/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json +++ b/packages/server/marketplaces/chatflows/SubQuestion Query Engine.json @@ -9,8 +9,8 @@ "height": 749, "id": "compactrefineLlamaIndex_0", "position": { - "x": -1214.7329938486841, - "y": 56.52482754447425 + "x": -443.9012456561584, + "y": 826.6100190232154 }, "type": "customNode", "data": { @@ -63,8 +63,8 @@ }, "selected": false, "positionAbsolute": { - "x": -1214.7329938486841, - "y": 56.52482754447425 + "x": -443.9012456561584, + "y": 826.6100190232154 }, "dragging": false }, @@ -73,8 +73,8 @@ "height": 611, "id": "pineconeLlamaIndex_0", "position": { - "x": 37.23548045607484, - "y": -119.7364648743818 + "x": 35.45798119088212, + "y": -132.1789597307308 }, "type": "customNode", "data": { @@ -183,14 +183,14 @@ } ], "outputs": { - "output": "vectorStore" + "output": "retriever" }, "selected": false }, "selected": false, "positionAbsolute": { - "x": 37.23548045607484, - "y": -119.7364648743818 + "x": 35.45798119088212, + "y": -132.1789597307308 }, "dragging": false }, @@ -405,79 +405,13 @@ "y": -127.15143353229783 } }, - { - "width": 300, - "height": 511, - "id": "queryEngineToolLlamaIndex_0", - "position": { - "x": 460.37559236135905, - "y": -565.6224030941121 - }, - "type": "customNode", - "data": { - "id": "queryEngineToolLlamaIndex_0", - "label": "QueryEngine Tool", - "version": 1, - "name": "queryEngineToolLlamaIndex", - "type": "QueryEngineTool", - "baseClasses": ["QueryEngineTool"], - "tags": ["LlamaIndex"], - "category": "Tools", - "description": "Execute actions using ChatGPT Plugin Url", - "inputParams": [ - { - "label": "Tool Name", - "name": "toolName", - "type": "string", - "description": "Tool name must be small capital letter with underscore. Ex: my_tool", - "id": "queryEngineToolLlamaIndex_0-input-toolName-string" - }, - { - "label": "Tool Description", - "name": "toolDesc", - "type": "string", - "rows": 4, - "id": "queryEngineToolLlamaIndex_0-input-toolDesc-string" - } - ], - "inputAnchors": [ - { - "label": "Vector Store Index", - "name": "vectorStoreIndex", - "type": "VectorStoreIndex", - "id": "queryEngineToolLlamaIndex_0-input-vectorStoreIndex-VectorStoreIndex" - } - ], - "inputs": { - "vectorStoreIndex": "{{pineconeLlamaIndex_1.data.instance}}", - "toolName": "apple_tool", - "toolDesc": "A SEC Form 10K filing describing the financials of Apple Inc (APPL) for the 2022 time period." - }, - "outputAnchors": [ - { - "id": "queryEngineToolLlamaIndex_0-output-queryEngineToolLlamaIndex-QueryEngineTool", - "name": "queryEngineToolLlamaIndex", - "label": "QueryEngineTool", - "type": "QueryEngineTool" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 460.37559236135905, - "y": -565.6224030941121 - }, - "dragging": false - }, { "width": 300, "height": 611, "id": "pineconeLlamaIndex_1", "position": { - "x": 42.17855025460784, - "y": -839.8824444107056 + "x": 43.95604951980056, + "y": -783.0024679245387 }, "type": "customNode", "data": { @@ -586,162 +520,14 @@ } ], "outputs": { - "output": "vectorStore" + "output": "retriever" }, "selected": false }, "selected": false, "positionAbsolute": { - "x": 42.17855025460784, - "y": -839.8824444107056 - }, - "dragging": false - }, - { - "width": 300, - "height": 511, - "id": "queryEngineToolLlamaIndex_1", - "position": { - "x": 462.16721384216123, - "y": -17.750065363429798 - }, - "type": "customNode", - "data": { - "id": "queryEngineToolLlamaIndex_1", - "label": "QueryEngine Tool", - "version": 1, - "name": "queryEngineToolLlamaIndex", - "type": "QueryEngineTool", - "baseClasses": ["QueryEngineTool"], - "tags": ["LlamaIndex"], - "category": "Tools", - "description": "Execute actions using ChatGPT Plugin Url", - "inputParams": [ - { - "label": "Tool Name", - "name": "toolName", - "type": "string", - "description": "Tool name must be small capital letter with underscore. Ex: my_tool", - "id": "queryEngineToolLlamaIndex_1-input-toolName-string" - }, - { - "label": "Tool Description", - "name": "toolDesc", - "type": "string", - "rows": 4, - "id": "queryEngineToolLlamaIndex_1-input-toolDesc-string" - } - ], - "inputAnchors": [ - { - "label": "Vector Store Index", - "name": "vectorStoreIndex", - "type": "VectorStoreIndex", - "id": "queryEngineToolLlamaIndex_1-input-vectorStoreIndex-VectorStoreIndex" - } - ], - "inputs": { - "vectorStoreIndex": "{{pineconeLlamaIndex_0.data.instance}}", - "toolName": "tesla_tool", - "toolDesc": "A SEC Form 10K filing describing the financials of Tesla Inc (TSLA) for the 2022 time period." - }, - "outputAnchors": [ - { - "id": "queryEngineToolLlamaIndex_1-output-queryEngineToolLlamaIndex-QueryEngineTool", - "name": "queryEngineToolLlamaIndex", - "label": "QueryEngineTool", - "type": "QueryEngineTool" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 462.16721384216123, - "y": -17.750065363429798 - }, - "dragging": false - }, - { - "width": 300, - "height": 484, - "id": "subQuestionQueryEngine_0", - "position": { - "x": 982.7583030231563, - "y": 349.50858200305896 - }, - "type": "customNode", - "data": { - "id": "subQuestionQueryEngine_0", - "label": "Sub Question Query Engine", - "version": 1, - "name": "subQuestionQueryEngine", - "type": "SubQuestionQueryEngine", - "baseClasses": ["SubQuestionQueryEngine"], - "tags": ["LlamaIndex"], - "category": "Engine", - "description": "Simple query engine built to answer question over your data, without memory", - "inputParams": [ - { - "label": "Return Source Documents", - "name": "returnSourceDocuments", - "type": "boolean", - "optional": true, - "id": "subQuestionQueryEngine_0-input-returnSourceDocuments-boolean" - } - ], - "inputAnchors": [ - { - "label": "QueryEngine Tools", - "name": "queryEngineTools", - "type": "QueryEngineTool", - "list": true, - "id": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" - }, - { - "label": "Chat Model", - "name": "model", - "type": "BaseChatModel_LlamaIndex", - "id": "subQuestionQueryEngine_0-input-model-BaseChatModel_LlamaIndex" - }, - { - "label": "Embeddings", - "name": "embeddings", - "type": "BaseEmbedding_LlamaIndex", - "id": "subQuestionQueryEngine_0-input-embeddings-BaseEmbedding_LlamaIndex" - }, - { - "label": "Response Synthesizer", - "name": "responseSynthesizer", - "type": "ResponseSynthesizer", - "description": "ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See more", - "optional": true, - "id": "subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer" - } - ], - "inputs": { - "queryEngineTools": ["{{queryEngineToolLlamaIndex_1.data.instance}}", "{{queryEngineToolLlamaIndex_0.data.instance}}"], - "model": "{{chatOpenAI_LlamaIndex_1.data.instance}}", - "embeddings": "{{openAIEmbedding_LlamaIndex_1.data.instance}}", - "responseSynthesizer": "{{compactrefineLlamaIndex_0.data.instance}}", - "returnSourceDocuments": true - }, - "outputAnchors": [ - { - "id": "subQuestionQueryEngine_0-output-subQuestionQueryEngine-SubQuestionQueryEngine", - "name": "subQuestionQueryEngine", - "label": "SubQuestionQueryEngine", - "type": "SubQuestionQueryEngine" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 982.7583030231563, - "y": 349.50858200305896 + "x": 43.95604951980056, + "y": -783.0024679245387 }, "dragging": false }, @@ -750,8 +536,8 @@ "height": 529, "id": "chatOpenAI_LlamaIndex_1", "position": { - "x": -846.9087470244615, - "y": 23.446501495097493 + "x": -446.80851289432655, + "y": 246.8790997755625 }, "type": "customNode", "data": { @@ -884,8 +670,8 @@ }, "selected": false, "positionAbsolute": { - "x": -846.9087470244615, - "y": 23.446501495097493 + "x": -446.80851289432655, + "y": 246.8790997755625 }, "dragging": false }, @@ -894,8 +680,8 @@ "height": 334, "id": "openAIEmbedding_LlamaIndex_1", "position": { - "x": -437.3136244622061, - "y": 329.99986619821175 + "x": -37.812177549447284, + "y": 577.9112529482311 }, "type": "customNode", "data": { @@ -952,17 +738,370 @@ "selected": false, "dragging": false, "positionAbsolute": { - "x": -437.3136244622061, - "y": 329.99986619821175 + "x": -37.812177549447284, + "y": 577.9112529482311 } }, + { + "width": 300, + "height": 382, + "id": "queryEngine_0", + "position": { + "x": 416.2466817793368, + "y": -600.1335182096643 + }, + "type": "customNode", + "data": { + "id": "queryEngine_0", + "label": "Query Engine", + "version": 2, + "name": "queryEngine", + "type": "QueryEngine", + "baseClasses": ["QueryEngine", "BaseQueryEngine"], + "tags": ["LlamaIndex"], + "category": "Engine", + "description": "Simple query engine built to answer question over your data, without memory", + "inputParams": [ + { + "label": "Return Source Documents", + "name": "returnSourceDocuments", + "type": "boolean", + "optional": true, + "id": "queryEngine_0-input-returnSourceDocuments-boolean" + } + ], + "inputAnchors": [ + { + "label": "Vector Store Retriever", + "name": "vectorStoreRetriever", + "type": "VectorIndexRetriever", + "id": "queryEngine_0-input-vectorStoreRetriever-VectorIndexRetriever" + }, + { + "label": "Response Synthesizer", + "name": "responseSynthesizer", + "type": "ResponseSynthesizer", + "description": "ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See more", + "optional": true, + "id": "queryEngine_0-input-responseSynthesizer-ResponseSynthesizer" + } + ], + "inputs": { + "vectorStoreRetriever": "{{pineconeLlamaIndex_1.data.instance}}", + "responseSynthesizer": "", + "returnSourceDocuments": "" + }, + "outputAnchors": [ + { + "id": "queryEngine_0-output-queryEngine-QueryEngine|BaseQueryEngine", + "name": "queryEngine", + "label": "QueryEngine", + "description": "Simple query engine built to answer question over your data, without memory", + "type": "QueryEngine | BaseQueryEngine" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 416.2466817793368, + "y": -600.1335182096643 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "queryEngineToolLlamaIndex_2", + "position": { + "x": 766.9839000102993, + "y": -654.6926410455919 + }, + "type": "customNode", + "data": { + "id": "queryEngineToolLlamaIndex_2", + "label": "QueryEngine Tool", + "version": 2, + "name": "queryEngineToolLlamaIndex", + "type": "QueryEngineTool", + "baseClasses": ["QueryEngineTool"], + "tags": ["LlamaIndex"], + "category": "Tools", + "description": "Tool used to invoke query engine", + "inputParams": [ + { + "label": "Tool Name", + "name": "toolName", + "type": "string", + "description": "Tool name must be small capital letter with underscore. Ex: my_tool", + "id": "queryEngineToolLlamaIndex_2-input-toolName-string" + }, + { + "label": "Tool Description", + "name": "toolDesc", + "type": "string", + "rows": 4, + "id": "queryEngineToolLlamaIndex_2-input-toolDesc-string" + } + ], + "inputAnchors": [ + { + "label": "Base QueryEngine", + "name": "baseQueryEngine", + "type": "BaseQueryEngine", + "id": "queryEngineToolLlamaIndex_2-input-baseQueryEngine-BaseQueryEngine" + } + ], + "inputs": { + "baseQueryEngine": "{{queryEngine_0.data.instance}}", + "toolName": "apple_tool", + "toolDesc": "A SEC Form 10K filing describing the financials of Apple Inc (APPL) for the 2022 time period." + }, + "outputAnchors": [ + { + "id": "queryEngineToolLlamaIndex_2-output-queryEngineToolLlamaIndex-QueryEngineTool", + "name": "queryEngineToolLlamaIndex", + "label": "QueryEngineTool", + "description": "Tool used to invoke query engine", + "type": "QueryEngineTool" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 766.9839000102993, + "y": -654.6926410455919 + }, + "dragging": false + }, + { + "width": 300, + "height": 511, + "id": "queryEngineToolLlamaIndex_1", + "position": { + "x": 771.5434180813253, + "y": -109.03650423344013 + }, + "type": "customNode", + "data": { + "id": "queryEngineToolLlamaIndex_1", + "label": "QueryEngine Tool", + "version": 2, + "name": "queryEngineToolLlamaIndex", + "type": "QueryEngineTool", + "baseClasses": ["QueryEngineTool"], + "tags": ["LlamaIndex"], + "category": "Tools", + "description": "Tool used to invoke query engine", + "inputParams": [ + { + "label": "Tool Name", + "name": "toolName", + "type": "string", + "description": "Tool name must be small capital letter with underscore. Ex: my_tool", + "id": "queryEngineToolLlamaIndex_1-input-toolName-string" + }, + { + "label": "Tool Description", + "name": "toolDesc", + "type": "string", + "rows": 4, + "id": "queryEngineToolLlamaIndex_1-input-toolDesc-string" + } + ], + "inputAnchors": [ + { + "label": "Base QueryEngine", + "name": "baseQueryEngine", + "type": "BaseQueryEngine", + "id": "queryEngineToolLlamaIndex_1-input-baseQueryEngine-BaseQueryEngine" + } + ], + "inputs": { + "baseQueryEngine": "{{queryEngine_1.data.instance}}", + "toolName": "tesla_tool", + "toolDesc": "A SEC Form 10K filing describing the financials of Tesla Inc (TSLA) for the 2022 time period." + }, + "outputAnchors": [ + { + "id": "queryEngineToolLlamaIndex_1-output-queryEngineToolLlamaIndex-QueryEngineTool", + "name": "queryEngineToolLlamaIndex", + "label": "QueryEngineTool", + "description": "Tool used to invoke query engine", + "type": "QueryEngineTool" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 771.5434180813253, + "y": -109.03650423344013 + }, + "dragging": false + }, + { + "width": 300, + "height": 382, + "id": "queryEngine_1", + "position": { + "x": 411.8632262885343, + "y": -68.91392354277994 + }, + "type": "customNode", + "data": { + "id": "queryEngine_1", + "label": "Query Engine", + "version": 2, + "name": "queryEngine", + "type": "QueryEngine", + "baseClasses": ["QueryEngine", "BaseQueryEngine"], + "tags": ["LlamaIndex"], + "category": "Engine", + "description": "Simple query engine built to answer question over your data, without memory", + "inputParams": [ + { + "label": "Return Source Documents", + "name": "returnSourceDocuments", + "type": "boolean", + "optional": true, + "id": "queryEngine_1-input-returnSourceDocuments-boolean" + } + ], + "inputAnchors": [ + { + "label": "Vector Store Retriever", + "name": "vectorStoreRetriever", + "type": "VectorIndexRetriever", + "id": "queryEngine_1-input-vectorStoreRetriever-VectorIndexRetriever" + }, + { + "label": "Response Synthesizer", + "name": "responseSynthesizer", + "type": "ResponseSynthesizer", + "description": "ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See more", + "optional": true, + "id": "queryEngine_1-input-responseSynthesizer-ResponseSynthesizer" + } + ], + "inputs": { + "vectorStoreRetriever": "{{pineconeLlamaIndex_0.data.instance}}", + "responseSynthesizer": "", + "returnSourceDocuments": "" + }, + "outputAnchors": [ + { + "id": "queryEngine_1-output-queryEngine-QueryEngine|BaseQueryEngine", + "name": "queryEngine", + "label": "QueryEngine", + "description": "Simple query engine built to answer question over your data, without memory", + "type": "QueryEngine | BaseQueryEngine" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 411.8632262885343, + "y": -68.91392354277994 + }, + "dragging": false + }, + { + "width": 300, + "height": 484, + "id": "subQuestionQueryEngine_0", + "position": { + "x": 1204.489328490966, + "y": 347.2090726754211 + }, + "type": "customNode", + "data": { + "id": "subQuestionQueryEngine_0", + "label": "Sub Question Query Engine", + "version": 2, + "name": "subQuestionQueryEngine", + "type": "SubQuestionQueryEngine", + "baseClasses": ["SubQuestionQueryEngine", "BaseQueryEngine"], + "tags": ["LlamaIndex"], + "category": "Engine", + "description": "Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response", + "inputParams": [ + { + "label": "Return Source Documents", + "name": "returnSourceDocuments", + "type": "boolean", + "optional": true, + "id": "subQuestionQueryEngine_0-input-returnSourceDocuments-boolean" + } + ], + "inputAnchors": [ + { + "label": "QueryEngine Tools", + "name": "queryEngineTools", + "type": "QueryEngineTool", + "list": true, + "id": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" + }, + { + "label": "Chat Model", + "name": "model", + "type": "BaseChatModel_LlamaIndex", + "id": "subQuestionQueryEngine_0-input-model-BaseChatModel_LlamaIndex" + }, + { + "label": "Embeddings", + "name": "embeddings", + "type": "BaseEmbedding_LlamaIndex", + "id": "subQuestionQueryEngine_0-input-embeddings-BaseEmbedding_LlamaIndex" + }, + { + "label": "Response Synthesizer", + "name": "responseSynthesizer", + "type": "ResponseSynthesizer", + "description": "ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See more", + "optional": true, + "id": "subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer" + } + ], + "inputs": { + "queryEngineTools": ["{{queryEngineToolLlamaIndex_2.data.instance}}", "{{queryEngineToolLlamaIndex_1.data.instance}}"], + "model": "{{chatOpenAI_LlamaIndex_1.data.instance}}", + "embeddings": "{{openAIEmbedding_LlamaIndex_1.data.instance}}", + "responseSynthesizer": "{{compactrefineLlamaIndex_0.data.instance}}", + "returnSourceDocuments": true + }, + "outputAnchors": [ + { + "id": "subQuestionQueryEngine_0-output-subQuestionQueryEngine-SubQuestionQueryEngine|BaseQueryEngine", + "name": "subQuestionQueryEngine", + "label": "SubQuestionQueryEngine", + "description": "Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response", + "type": "SubQuestionQueryEngine | BaseQueryEngine" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 1204.489328490966, + "y": 347.2090726754211 + }, + "dragging": false + }, { "width": 300, "height": 82, "id": "stickyNote_0", "position": { - "x": 35.90892935132143, - "y": -936.1282632923861 + "x": 1208.1786832265154, + "y": 238.26647262900994 }, "type": "stickyNote", "data": { @@ -987,13 +1126,14 @@ ], "inputAnchors": [], "inputs": { - "note": "Query previously upserted documents with corresponding metadata key value pair - \n{ source: \"apple\"}" + "note": "Break questions into subqueries, then retrieve corresponding context using queryengine tools" }, "outputAnchors": [ { "id": "stickyNote_0-output-stickyNote-StickyNote", "name": "stickyNote", "label": "StickyNote", + "description": "Add a sticky note", "type": "StickyNote" } ], @@ -1002,8 +1142,8 @@ }, "selected": false, "positionAbsolute": { - "x": 35.90892935132143, - "y": -936.1282632923861 + "x": 1208.1786832265154, + "y": 238.26647262900994 }, "dragging": false }, @@ -1012,8 +1152,8 @@ "height": 82, "id": "stickyNote_1", "position": { - "x": 37.74909394815296, - "y": -215.17456133022054 + "x": 416.8958270395809, + "y": -179.9680840754678 }, "type": "stickyNote", "data": { @@ -1038,13 +1178,14 @@ ], "inputAnchors": [], "inputs": { - "note": "Query previously upserted documents with corresponding metadata key value pair - \n{ source: \"tesla\"}" + "note": "Query previously upserted documents with corresponding metadata key value pair - \n{ source: \"\"}" }, "outputAnchors": [ { "id": "stickyNote_1-output-stickyNote-StickyNote", "name": "stickyNote", "label": "StickyNote", + "description": "Add a sticky note", "type": "StickyNote" } ], @@ -1053,59 +1194,8 @@ }, "selected": false, "positionAbsolute": { - "x": 37.74909394815296, - "y": -215.17456133022054 - }, - "dragging": false - }, - { - "width": 300, - "height": 163, - "id": "stickyNote_2", - "position": { - "x": 984.9543031068163, - "y": 171.04264459503852 - }, - "type": "stickyNote", - "data": { - "id": "stickyNote_2", - "label": "Sticky Note", - "version": 1, - "name": "stickyNote", - "type": "StickyNote", - "baseClasses": ["StickyNote"], - "category": "Utilities", - "description": "Add a sticky note", - "inputParams": [ - { - "label": "", - "name": "note", - "type": "string", - "rows": 1, - "placeholder": "Type something here", - "optional": true, - "id": "stickyNote_2-input-note-string" - } - ], - "inputAnchors": [], - "inputs": { - "note": "Break questions into subqueries, then retrieve corresponding context using queryengine tool.\n\nThis implementation does not contains memory, we can use OpenAI Agent to function call this flow" - }, - "outputAnchors": [ - { - "id": "stickyNote_2-output-stickyNote-StickyNote", - "name": "stickyNote", - "label": "StickyNote", - "type": "StickyNote" - } - ], - "outputs": {}, - "selected": false - }, - "selected": false, - "positionAbsolute": { - "x": 984.9543031068163, - "y": 171.04264459503852 + "x": 416.8958270395809, + "y": -179.9680840754678 }, "dragging": false } @@ -1128,20 +1218,60 @@ "id": "openAIEmbedding_LlamaIndex_0-openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding-pineconeLlamaIndex_1-pineconeLlamaIndex_1-input-embeddings-BaseEmbedding_LlamaIndex" }, { - "source": "pineconeLlamaIndex_1", - "sourceHandle": "pineconeLlamaIndex_1-output-vectorStore-Pinecone|VectorStoreIndex", - "target": "queryEngineToolLlamaIndex_0", - "targetHandle": "queryEngineToolLlamaIndex_0-input-vectorStoreIndex-VectorStoreIndex", + "source": "openAIEmbedding_LlamaIndex_0", + "sourceHandle": "openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", + "target": "pineconeLlamaIndex_0", + "targetHandle": "pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex", "type": "buttonedge", - "id": "pineconeLlamaIndex_1-pineconeLlamaIndex_1-output-vectorStore-Pinecone|VectorStoreIndex-queryEngineToolLlamaIndex_0-queryEngineToolLlamaIndex_0-input-vectorStoreIndex-VectorStoreIndex" + "id": "openAIEmbedding_LlamaIndex_0-openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex" + }, + { + "source": "chatOpenAI_LlamaIndex_0", + "sourceHandle": "chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", + "target": "pineconeLlamaIndex_0", + "targetHandle": "pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex", + "type": "buttonedge", + "id": "chatOpenAI_LlamaIndex_0-chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex" + }, + { + "source": "pineconeLlamaIndex_1", + "sourceHandle": "pineconeLlamaIndex_1-output-retriever-Pinecone|VectorIndexRetriever", + "target": "queryEngine_0", + "targetHandle": "queryEngine_0-input-vectorStoreRetriever-VectorIndexRetriever", + "type": "buttonedge", + "id": "pineconeLlamaIndex_1-pineconeLlamaIndex_1-output-retriever-Pinecone|VectorIndexRetriever-queryEngine_0-queryEngine_0-input-vectorStoreRetriever-VectorIndexRetriever" + }, + { + "source": "queryEngine_0", + "sourceHandle": "queryEngine_0-output-queryEngine-QueryEngine|BaseQueryEngine", + "target": "queryEngineToolLlamaIndex_2", + "targetHandle": "queryEngineToolLlamaIndex_2-input-baseQueryEngine-BaseQueryEngine", + "type": "buttonedge", + "id": "queryEngine_0-queryEngine_0-output-queryEngine-QueryEngine|BaseQueryEngine-queryEngineToolLlamaIndex_2-queryEngineToolLlamaIndex_2-input-baseQueryEngine-BaseQueryEngine" }, { "source": "pineconeLlamaIndex_0", - "sourceHandle": "pineconeLlamaIndex_0-output-vectorStore-Pinecone|VectorStoreIndex", - "target": "queryEngineToolLlamaIndex_1", - "targetHandle": "queryEngineToolLlamaIndex_1-input-vectorStoreIndex-VectorStoreIndex", + "sourceHandle": "pineconeLlamaIndex_0-output-retriever-Pinecone|VectorIndexRetriever", + "target": "queryEngine_1", + "targetHandle": "queryEngine_1-input-vectorStoreRetriever-VectorIndexRetriever", "type": "buttonedge", - "id": "pineconeLlamaIndex_0-pineconeLlamaIndex_0-output-vectorStore-Pinecone|VectorStoreIndex-queryEngineToolLlamaIndex_1-queryEngineToolLlamaIndex_1-input-vectorStoreIndex-VectorStoreIndex" + "id": "pineconeLlamaIndex_0-pineconeLlamaIndex_0-output-retriever-Pinecone|VectorIndexRetriever-queryEngine_1-queryEngine_1-input-vectorStoreRetriever-VectorIndexRetriever" + }, + { + "source": "queryEngine_1", + "sourceHandle": "queryEngine_1-output-queryEngine-QueryEngine|BaseQueryEngine", + "target": "queryEngineToolLlamaIndex_1", + "targetHandle": "queryEngineToolLlamaIndex_1-input-baseQueryEngine-BaseQueryEngine", + "type": "buttonedge", + "id": "queryEngine_1-queryEngine_1-output-queryEngine-QueryEngine|BaseQueryEngine-queryEngineToolLlamaIndex_1-queryEngineToolLlamaIndex_1-input-baseQueryEngine-BaseQueryEngine" + }, + { + "source": "queryEngineToolLlamaIndex_2", + "sourceHandle": "queryEngineToolLlamaIndex_2-output-queryEngineToolLlamaIndex-QueryEngineTool", + "target": "subQuestionQueryEngine_0", + "targetHandle": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool", + "type": "buttonedge", + "id": "queryEngineToolLlamaIndex_2-queryEngineToolLlamaIndex_2-output-queryEngineToolLlamaIndex-QueryEngineTool-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" }, { "source": "queryEngineToolLlamaIndex_1", @@ -1151,14 +1281,6 @@ "type": "buttonedge", "id": "queryEngineToolLlamaIndex_1-queryEngineToolLlamaIndex_1-output-queryEngineToolLlamaIndex-QueryEngineTool-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" }, - { - "source": "queryEngineToolLlamaIndex_0", - "sourceHandle": "queryEngineToolLlamaIndex_0-output-queryEngineToolLlamaIndex-QueryEngineTool", - "target": "subQuestionQueryEngine_0", - "targetHandle": "subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool", - "type": "buttonedge", - "id": "queryEngineToolLlamaIndex_0-queryEngineToolLlamaIndex_0-output-queryEngineToolLlamaIndex-QueryEngineTool-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-queryEngineTools-QueryEngineTool" - }, { "source": "chatOpenAI_LlamaIndex_1", "sourceHandle": "chatOpenAI_LlamaIndex_1-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", @@ -1182,22 +1304,6 @@ "targetHandle": "subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer", "type": "buttonedge", "id": "compactrefineLlamaIndex_0-compactrefineLlamaIndex_0-output-compactrefineLlamaIndex-CompactRefine|ResponseSynthesizer-subQuestionQueryEngine_0-subQuestionQueryEngine_0-input-responseSynthesizer-ResponseSynthesizer" - }, - { - "source": "openAIEmbedding_LlamaIndex_0", - "sourceHandle": "openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding", - "target": "pineconeLlamaIndex_0", - "targetHandle": "pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex", - "type": "buttonedge", - "id": "openAIEmbedding_LlamaIndex_0-openAIEmbedding_LlamaIndex_0-output-openAIEmbedding_LlamaIndex-OpenAIEmbedding|BaseEmbedding_LlamaIndex|BaseEmbedding-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-embeddings-BaseEmbedding_LlamaIndex" - }, - { - "source": "chatOpenAI_LlamaIndex_0", - "sourceHandle": "chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM", - "target": "pineconeLlamaIndex_0", - "targetHandle": "pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex", - "type": "buttonedge", - "id": "chatOpenAI_LlamaIndex_0-chatOpenAI_LlamaIndex_0-output-chatOpenAI_LlamaIndex-ChatOpenAI|BaseChatModel_LlamaIndex|BaseLLM-pineconeLlamaIndex_0-pineconeLlamaIndex_0-input-model-BaseChatModel_LlamaIndex" } ] } From 86da67f4674b191d4f4e749c4d915805fbfb253a Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 14 Feb 2024 20:20:43 +0800 Subject: [PATCH 436/502] add missing human text when image presents --- .../chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 9033b27f..7b39f1ed 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -45,17 +45,13 @@ export class ChatOpenAI extends LangchainChatOpenAI { const messageContent = addImagesToMessages(nodeData, optionsData, this.multiModalOption) if (messageContent?.length) { if (messages[0].length > 0 && messages[0][messages[0].length - 1] instanceof HumanMessage) { - const lastMessage = messages[0].pop() - if (lastMessage instanceof HumanMessage) { - lastMessage.content = messageContent + // Change model to gpt-4-vision + this.modelName = 'gpt-4-vision-preview' - // Change model to gpt-4-vision - this.modelName = 'gpt-4-vision-preview' + // Change default max token to higher when using gpt-4-vision + this.maxTokens = 1024 - // Change default max token to higher when using gpt-4-vision - this.maxTokens = 1024 - } - messages[0].push(lastMessage as HumanMessage) + messages[0].push(new HumanMessage({ content: messageContent })) } } else { // revert to previous values if image upload is empty From 44c1f54d05e948184229c9fc60c08739bfb3ff50 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 14 Feb 2024 13:14:46 -0500 Subject: [PATCH 437/502] Showing image/audio files in the View Messages Dialog --- .../ui-component/dialog/ViewMessagesDialog.js | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index cadd4abd..6e206885 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -21,7 +21,9 @@ import { DialogTitle, ListItem, ListItemText, - Chip + Chip, + Card, + CardMedia } from '@mui/material' import { useTheme } from '@mui/material/styles' import DatePicker from 'react-datepicker' @@ -69,6 +71,12 @@ DatePickerCustomInput.propTypes = { onClick: PropTypes.func } +const messageImageStyle = { + width: '128px', + height: '128px', + objectFit: 'cover' +} + const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() @@ -249,6 +257,14 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { }) } } + if (chatmsg.fileUploads) { + chatmsg.fileUploads = JSON.parse(chatmsg.fileUploads) + chatmsg.fileUploads.forEach((file) => { + if (file.type === 'stored-file') { + file.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatmsg.chatflowid}&chatId=${chatmsg.chatId}&fileName=${file.name}` + } + }) + } const obj = { ...chatmsg, message: chatmsg.content, @@ -672,6 +688,51 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.message}
    + {message.fileUploads && message.fileUploads.length > 0 && ( +
    + {message.fileUploads.map((item, index) => { + return ( + <> + {item.mime.startsWith('image/') ? ( + + + + ) : ( + // eslint-disable-next-line jsx-a11y/media-has-caption + + )} + + ) + })} +
    + )} {message.fileAnnotations && (
    {message.fileAnnotations.map((fileAnnotation, index) => { From a71c5a109d07b108674d0196b46183c408941b4a Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 14 Feb 2024 13:16:51 -0500 Subject: [PATCH 438/502] fix for concurrent requests for media handling --- .../nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 2 +- .../ChatOpenAI/FlowiseChatOpenAI.ts | 27 ++++++++++++++----- packages/components/src/multiModalUtils.ts | 3 +-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index d94cc3a1..221d5e17 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -244,7 +244,7 @@ class ChatOpenAI_ChatModels implements INode { } } - const model = new ChatOpenAI(obj, { + const model = new ChatOpenAI(nodeData.id, obj, { baseURL: basePath, baseOptions: parsedBaseOptions }) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 7b39f1ed..396d7433 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -9,20 +9,33 @@ import { Callbacks } from '@langchain/core/callbacks/manager' import { ICommonObject, IMultiModalOption, INodeData } from '../../../src' import { addImagesToMessages } from '../../../src/multiModalUtils' +interface MultiModalOptions { + chainNodeData: INodeData + chainNodeOptions: ICommonObject +} + export class ChatOpenAI extends LangchainChatOpenAI { //TODO: Should be class variables and not static - public static chainNodeData: INodeData - public static chainNodeOptions: ICommonObject + // public static nodeData: INodeData + // public static nodeOptions: ICommonObject + private static chainNodeDataOptions: Map = new Map() configuredModel: string configuredMaxToken?: number multiModalOption?: IMultiModalOption + id: string + + public static injectChainNodeData(nodeData: INodeData, options: ICommonObject) { + ChatOpenAI.chainNodeDataOptions.set(nodeData.id, { chainNodeData: nodeData, chainNodeOptions: options }) + } constructor( + id: string, fields?: Partial & BaseChatModelParams & { openAIApiKey?: string; multiModalOption?: IMultiModalOption }, /** @deprecated */ configuration?: ClientOptions & LegacyOpenAIInput ) { super(fields, configuration) + this.id = id this.multiModalOption = fields?.multiModalOption this.configuredModel = fields?.modelName ?? 'gpt-3.5-turbo' this.configuredMaxToken = fields?.maxTokens @@ -33,15 +46,15 @@ export class ChatOpenAI extends LangchainChatOpenAI { } async generate(messages: BaseMessageLike[][], options?: string[] | ChatOpenAICallOptions, callbacks?: Callbacks): Promise { - if (ChatOpenAI.chainNodeData && ChatOpenAI.chainNodeOptions) { - await this.injectMultiModalMessages(messages) + if (ChatOpenAI.chainNodeDataOptions.has(this.id)) { + await this.injectMultiModalMessages(messages, ChatOpenAI.chainNodeDataOptions.get(this.id) as MultiModalOptions) } return super.generate(messages, options, callbacks) } - private async injectMultiModalMessages(messages: BaseMessageLike[][]) { - const nodeData = ChatOpenAI.chainNodeData - const optionsData = ChatOpenAI.chainNodeOptions + private async injectMultiModalMessages(messages: BaseMessageLike[][], nodeOptions: MultiModalOptions) { + const nodeData = nodeOptions.chainNodeData + const optionsData = nodeOptions.chainNodeOptions const messageContent = addImagesToMessages(nodeData, optionsData, this.multiModalOption) if (messageContent?.length) { if (messages[0].length > 0 && messages[0][messages[0].length - 1] instanceof HumanMessage) { diff --git a/packages/components/src/multiModalUtils.ts b/packages/components/src/multiModalUtils.ts index 6cbd75a0..c321f088 100644 --- a/packages/components/src/multiModalUtils.ts +++ b/packages/components/src/multiModalUtils.ts @@ -12,8 +12,7 @@ export const injectChainNodeData = (nodeData: INodeData, options: ICommonObject) if (model instanceof ChatOpenAI) { // TODO: this should not be static, need to figure out how to pass the nodeData and options to the invoke method - ChatOpenAI.chainNodeOptions = options - ChatOpenAI.chainNodeData = nodeData + ChatOpenAI.injectChainNodeData(nodeData, options) } } From 85809a9ecc82486a90a1f4cc39515dd28b796c04 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 15 Feb 2024 04:03:10 +0800 Subject: [PATCH 439/502] fix for concurrency --- .../ConversationChain/ConversationChain.ts | 49 +++++++++++++------ packages/components/src/Interface.ts | 17 +++++++ packages/components/src/multiModalUtils.ts | 11 +++-- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index ad49d829..22a32c7b 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -2,14 +2,15 @@ import { FlowiseMemory, ICommonObject, INode, INodeData, INodeParams } from '../ import { ConversationChain } from 'langchain/chains' import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' -import { BaseChatModel } from 'langchain/chat_models/base' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { RunnableSequence } from 'langchain/schema/runnable' import { StringOutputParser } from 'langchain/schema/output_parser' +import { HumanMessage } from 'langchain/schema' import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' import { formatResponse } from '../../outputparsers/OutputParserHelpers' -import { injectChainNodeData } from '../../../src/multiModalUtils' +import { addImagesToMessages } from '../../../src/multiModalUtils' +import { ChatOpenAI } from '../../chatmodels/ChatOpenAI/FlowiseChatOpenAI' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` const inputKey = 'input' @@ -93,7 +94,7 @@ class ConversationChain_Chains implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory - injectChainNodeData(nodeData, options) + // injectChainNodeData(nodeData, options) const chain = prepareChain(nodeData, options, this.sessionId) const moderations = nodeData.inputs?.inputModeration as Moderation[] @@ -145,7 +146,7 @@ class ConversationChain_Chains implements INode { } } -const prepareChatPrompt = (nodeData: INodeData) => { +const prepareChatPrompt = (nodeData: INodeData, humanImageMessages: HumanMessage[]) => { const memory = nodeData.inputs?.memory as FlowiseMemory const prompt = nodeData.inputs?.systemMessagePrompt as string const chatPromptTemplate = nodeData.inputs?.chatPromptTemplate as ChatPromptTemplate @@ -153,12 +154,10 @@ const prepareChatPrompt = (nodeData: INodeData) => { if (chatPromptTemplate && chatPromptTemplate.promptMessages.length) { const sysPrompt = chatPromptTemplate.promptMessages[0] const humanPrompt = chatPromptTemplate.promptMessages[chatPromptTemplate.promptMessages.length - 1] - const chatPrompt = ChatPromptTemplate.fromMessages([ - sysPrompt, - new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), - humanPrompt - ]) + const messages = [sysPrompt, new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), humanPrompt] + if (humanImageMessages.length) messages.push(...humanImageMessages) + const chatPrompt = ChatPromptTemplate.fromMessages(messages) if ((chatPromptTemplate as any).promptValues) { // @ts-ignore chatPrompt.promptValues = (chatPromptTemplate as any).promptValues @@ -167,22 +166,44 @@ const prepareChatPrompt = (nodeData: INodeData) => { return chatPrompt } - const chatPrompt = ChatPromptTemplate.fromMessages([ + const messages = [ SystemMessagePromptTemplate.fromTemplate(prompt ? prompt : systemMessage), new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) - ]) + ] + if (humanImageMessages.length) messages.push(...(humanImageMessages as any[])) + + const chatPrompt = ChatPromptTemplate.fromMessages(messages) return chatPrompt } const prepareChain = (nodeData: INodeData, options: ICommonObject, sessionId?: string) => { const chatHistory = options.chatHistory - const model = nodeData.inputs?.model as BaseChatModel + let model = nodeData.inputs?.model as ChatOpenAI const memory = nodeData.inputs?.memory as FlowiseMemory const memoryKey = memory.memoryKey ?? 'chat_history' - const chatPrompt = prepareChatPrompt(nodeData) + const messageContent = addImagesToMessages(nodeData, options, model.multiModalOption) + let humanImageMessages: HumanMessage[] = [] + + if (messageContent?.length) { + // Change model to gpt-4-vision + model.modelName = 'gpt-4-vision-preview' + + // Change default max token to higher when using gpt-4-vision + model.maxTokens = 1024 + + for (const msg of messageContent) { + humanImageMessages.push(new HumanMessage({ content: [msg] })) + } + } else { + // revert to previous values if image upload is empty + model.modelName = model.configuredModel + model.maxTokens = model.configuredMaxToken + } + + const chatPrompt = prepareChatPrompt(nodeData, humanImageMessages) let promptVariables = {} const promptValuesRaw = (chatPrompt as any).promptValues if (promptValuesRaw) { @@ -206,7 +227,7 @@ const prepareChain = (nodeData: INodeData, options: ICommonObject, sessionId?: s }, ...promptVariables }, - prepareChatPrompt(nodeData), + prepareChatPrompt(nodeData, humanImageMessages), model, new StringOutputParser() ]) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 44818733..62cd3ba9 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -21,6 +21,8 @@ export type CommonType = string | number | boolean | undefined | null export type MessageType = 'apiMessage' | 'userMessage' +export type ImageDetail = 'auto' | 'low' | 'high' + /** * Others */ @@ -158,6 +160,21 @@ export interface IMultiModalOption { audio?: Record } +export type MessageContentText = { + type: 'text' + text: string +} + +export type MessageContentImageUrl = { + type: 'image_url' + image_url: + | string + | { + url: string + detail?: ImageDetail + } +} + /** * Classes */ diff --git a/packages/components/src/multiModalUtils.ts b/packages/components/src/multiModalUtils.ts index c321f088..246821db 100644 --- a/packages/components/src/multiModalUtils.ts +++ b/packages/components/src/multiModalUtils.ts @@ -1,10 +1,9 @@ -import { ICommonObject, IFileUpload, IMultiModalOption, INodeData } from './Interface' +import { ICommonObject, IFileUpload, IMultiModalOption, INodeData, MessageContentImageUrl } from './Interface' import { BaseChatModel } from 'langchain/chat_models/base' import { ChatOpenAI as LangchainChatOpenAI } from 'langchain/chat_models/openai' import path from 'path' import { getStoragePath } from './utils' import fs from 'fs' -import { MessageContent } from '@langchain/core/dist/messages' import { ChatOpenAI } from '../nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI' export const injectChainNodeData = (nodeData: INodeData, options: ICommonObject) => { @@ -16,8 +15,12 @@ export const injectChainNodeData = (nodeData: INodeData, options: ICommonObject) } } -export const addImagesToMessages = (nodeData: INodeData, options: ICommonObject, multiModalOption?: IMultiModalOption): MessageContent => { - const imageContent: MessageContent = [] +export const addImagesToMessages = ( + nodeData: INodeData, + options: ICommonObject, + multiModalOption?: IMultiModalOption +): MessageContentImageUrl[] => { + const imageContent: MessageContentImageUrl[] = [] let model = nodeData.inputs?.model if (model instanceof LangchainChatOpenAI && multiModalOption) { From 6acc921095b0b0cdb261c5501e25561d2fb4a770 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 14 Feb 2024 17:04:53 -0500 Subject: [PATCH 440/502] ViewMessages->Export Messages. Add Fullpath of the image/audio file. --- packages/server/src/index.ts | 6 +++++ packages/ui/src/api/chatmessage.js | 4 ++- .../ui-component/dialog/ViewMessagesDialog.js | 27 +++++++++++++++++-- packages/ui/src/utils/genericHelper.js | 22 +++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 851da8c8..4ad79d97 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1150,6 +1150,12 @@ export class App { } }) + this.app.get('/api/v1/get-upload-path', async (req: Request, res: Response) => { + return res.json({ + storagePath: getStoragePath() + }) + }) + // stream uploaded image this.app.get('/api/v1/get-upload-file', async (req: Request, res: Response) => { if (!req.query.chatflowId || !req.query.chatId || !req.query.fileName) { diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index 5f1a4bad..f1651247 100644 --- a/packages/ui/src/api/chatmessage.js +++ b/packages/ui/src/api/chatmessage.js @@ -4,10 +4,12 @@ const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmes const getAllChatmessageFromChatflow = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'DESC', ...params } }) const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', ...params } }) const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } }) +const getStoragePath = () => client.get(`/get-upload-path`) export default { getInternalChatmessageFromChatflow, getAllChatmessageFromChatflow, getChatmessageFromPK, - deleteChatmessage + deleteChatmessage, + getStoragePath } diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 6e206885..2df501fb 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -49,7 +49,7 @@ import useApi from 'hooks/useApi' import useConfirm from 'hooks/useConfirm' // Utils -import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' +import { getOS, isValidURL, removeDuplicateURL } from 'utils/genericHelper' import useNotifier from 'utils/useNotifier' import { baseURL } from 'store/constant' @@ -100,6 +100,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) + const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath) + let storagePath = '' const onStartDateSelected = (date) => { setStartDate(date) @@ -128,16 +130,35 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { }) } - const exportMessages = () => { + const exportMessages = async () => { + if (!storagePath && getStoragePathFromServer.data) { + storagePath = getStoragePathFromServer.data.storagePath + } const obj = {} + let fileSeparator = '/' + if ('windows' === getOS()) { + fileSeparator = '\\' + } for (let i = 0; i < allChatlogs.length; i += 1) { const chatmsg = allChatlogs[i] const chatPK = getChatPK(chatmsg) + let filePaths = [] + if (chatmsg.fileUploads) { + chatmsg.fileUploads = JSON.parse(chatmsg.fileUploads) + chatmsg.fileUploads.forEach((file) => { + if (file.type === 'stored-file') { + filePaths.push( + `${storagePath}${fileSeparator}${chatmsg.chatflowid}${fileSeparator}${chatmsg.chatId}${fileSeparator}${file.name}` + ) + } + }) + } const msg = { content: chatmsg.content, role: chatmsg.role === 'apiMessage' ? 'bot' : 'user', time: chatmsg.createdDate } + if (filePaths.length) msg.filePaths = filePaths if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools) if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) @@ -373,6 +394,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { useEffect(() => { if (getChatmessageApi.data) { + getStoragePathFromServer.request() + setAllChatLogs(getChatmessageApi.data) const chatPK = processChatLogs(getChatmessageApi.data) setSelectedMessageIndex(0) diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 74dc9578..645435d2 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -607,3 +607,25 @@ export const getConfigExamplesForCurl = (configData, bodyType, isMultiple, stopN } return finalStr } + +export const getOS = () => { + let userAgent = window.navigator.userAgent.toLowerCase(), + macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i, + windowsPlatforms = /(win32|win64|windows|wince)/i, + iosPlatforms = /(iphone|ipad|ipod)/i, + os = null + + if (macosPlatforms.test(userAgent)) { + os = 'macos' + } else if (iosPlatforms.test(userAgent)) { + os = 'ios' + } else if (windowsPlatforms.test(userAgent)) { + os = 'windows' + } else if (/android/.test(userAgent)) { + os = 'android' + } else if (!os && /linux/.test(userAgent)) { + os = 'linux' + } + + return os +} From 9c874bb49aa57617ebdb41ae748a0638e1530412 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 15 Feb 2024 09:08:49 -0500 Subject: [PATCH 441/502] Concurrency fixes - correcting wrong id --- .../nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 396d7433..3943cfe3 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -25,7 +25,9 @@ export class ChatOpenAI extends LangchainChatOpenAI { id: string public static injectChainNodeData(nodeData: INodeData, options: ICommonObject) { - ChatOpenAI.chainNodeDataOptions.set(nodeData.id, { chainNodeData: nodeData, chainNodeOptions: options }) + if (nodeData.inputs?.model.id) { + ChatOpenAI.chainNodeDataOptions.set(nodeData.inputs?.model.id, { chainNodeData: nodeData, chainNodeOptions: options }) + } } constructor( From 52ffa1772bf29264fe1712ec95a846d3df307cfc Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 15 Feb 2024 18:18:36 -0500 Subject: [PATCH 442/502] Multimodal Fixes...removing all static methods/variables. --- .../ConversationalAgent.ts | 4 +- .../agents/MRKLAgentChat/MRKLAgentChat.ts | 6 +-- .../ConversationChain/ConversationChain.ts | 50 ++++++------------- .../nodes/chains/LLMChain/LLMChain.ts | 6 +-- .../ChatOpenAI/FlowiseChatOpenAI.ts | 27 +++------- packages/components/src/multiModalUtils.ts | 47 ++++++++++++++--- 6 files changed, 70 insertions(+), 70 deletions(-) diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index c26063cf..e9bdd94b 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -9,7 +9,7 @@ import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } import { AgentExecutor } from '../../../src/agents' import { ChatConversationalAgent } from 'langchain/agents' import { renderTemplate } from '@langchain/core/prompts' -import { injectChainNodeData } from '../../../src/multiModalUtils' +import { injectAgentExecutorNodeData } from '../../../src/multiModalUtils' const DEFAULT_PREFIX = `Assistant is a large language model trained by OpenAI. @@ -85,9 +85,9 @@ class ConversationalAgent_Agents implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory as FlowiseMemory - injectChainNodeData(nodeData, options) const executor = await prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) + injectAgentExecutorNodeData(executor, nodeData, options) const loggerHandler = new ConsoleCallbackHandler(options.logger) const callbacks = await additionalCallbacks(nodeData, options) diff --git a/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts b/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts index f3e9db3a..c14c9341 100644 --- a/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts +++ b/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts @@ -5,7 +5,7 @@ import { Tool } from 'langchain/tools' import { BaseLanguageModel } from 'langchain/base_language' import { flatten } from 'lodash' import { additionalCallbacks } from '../../../src/handler' -import { injectChainNodeData } from '../../../src/multiModalUtils' +import { injectLcAgentExecutorNodeData } from '../../../src/multiModalUtils' class MRKLAgentChat_Agents implements INode { label: string @@ -48,14 +48,14 @@ class MRKLAgentChat_Agents implements INode { tools = flatten(tools) const executor = await initializeAgentExecutorWithOptions(tools, model, { agentType: 'chat-zero-shot-react-description', - verbose: process.env.DEBUG === 'true' ? true : false + verbose: process.env.DEBUG === 'true' }) return executor } async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const executor = nodeData.instance as AgentExecutor - injectChainNodeData(nodeData, options) + injectLcAgentExecutorNodeData(executor, nodeData, options) const callbacks = await additionalCallbacks(nodeData, options) diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 22a32c7b..fce36b89 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -2,15 +2,14 @@ import { FlowiseMemory, ICommonObject, INode, INodeData, INodeParams } from '../ import { ConversationChain } from 'langchain/chains' import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' +import { BaseChatModel } from 'langchain/chat_models/base' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { RunnableSequence } from 'langchain/schema/runnable' import { StringOutputParser } from 'langchain/schema/output_parser' -import { HumanMessage } from 'langchain/schema' import { ConsoleCallbackHandler as LCConsoleCallbackHandler } from '@langchain/core/tracers/console' import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' import { formatResponse } from '../../outputparsers/OutputParserHelpers' -import { addImagesToMessages } from '../../../src/multiModalUtils' -import { ChatOpenAI } from '../../chatmodels/ChatOpenAI/FlowiseChatOpenAI' +import { injectRunnableNodeData } from '../../../src/multiModalUtils' let systemMessage = `The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.` const inputKey = 'input' @@ -94,9 +93,10 @@ class ConversationChain_Chains implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const memory = nodeData.inputs?.memory - // injectChainNodeData(nodeData, options) const chain = prepareChain(nodeData, options, this.sessionId) + injectRunnableNodeData(chain, nodeData, options) + const moderations = nodeData.inputs?.inputModeration as Moderation[] if (moderations && moderations.length > 0) { @@ -146,7 +146,7 @@ class ConversationChain_Chains implements INode { } } -const prepareChatPrompt = (nodeData: INodeData, humanImageMessages: HumanMessage[]) => { +const prepareChatPrompt = (nodeData: INodeData) => { const memory = nodeData.inputs?.memory as FlowiseMemory const prompt = nodeData.inputs?.systemMessagePrompt as string const chatPromptTemplate = nodeData.inputs?.chatPromptTemplate as ChatPromptTemplate @@ -154,10 +154,12 @@ const prepareChatPrompt = (nodeData: INodeData, humanImageMessages: HumanMessage if (chatPromptTemplate && chatPromptTemplate.promptMessages.length) { const sysPrompt = chatPromptTemplate.promptMessages[0] const humanPrompt = chatPromptTemplate.promptMessages[chatPromptTemplate.promptMessages.length - 1] - const messages = [sysPrompt, new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), humanPrompt] - if (humanImageMessages.length) messages.push(...humanImageMessages) + const chatPrompt = ChatPromptTemplate.fromMessages([ + sysPrompt, + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + humanPrompt + ]) - const chatPrompt = ChatPromptTemplate.fromMessages(messages) if ((chatPromptTemplate as any).promptValues) { // @ts-ignore chatPrompt.promptValues = (chatPromptTemplate as any).promptValues @@ -166,44 +168,22 @@ const prepareChatPrompt = (nodeData: INodeData, humanImageMessages: HumanMessage return chatPrompt } - const messages = [ + const chatPrompt = ChatPromptTemplate.fromMessages([ SystemMessagePromptTemplate.fromTemplate(prompt ? prompt : systemMessage), new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) - ] - if (humanImageMessages.length) messages.push(...(humanImageMessages as any[])) - - const chatPrompt = ChatPromptTemplate.fromMessages(messages) + ]) return chatPrompt } const prepareChain = (nodeData: INodeData, options: ICommonObject, sessionId?: string) => { const chatHistory = options.chatHistory - let model = nodeData.inputs?.model as ChatOpenAI + const model = nodeData.inputs?.model as BaseChatModel const memory = nodeData.inputs?.memory as FlowiseMemory const memoryKey = memory.memoryKey ?? 'chat_history' - const messageContent = addImagesToMessages(nodeData, options, model.multiModalOption) - let humanImageMessages: HumanMessage[] = [] - - if (messageContent?.length) { - // Change model to gpt-4-vision - model.modelName = 'gpt-4-vision-preview' - - // Change default max token to higher when using gpt-4-vision - model.maxTokens = 1024 - - for (const msg of messageContent) { - humanImageMessages.push(new HumanMessage({ content: [msg] })) - } - } else { - // revert to previous values if image upload is empty - model.modelName = model.configuredModel - model.maxTokens = model.configuredMaxToken - } - - const chatPrompt = prepareChatPrompt(nodeData, humanImageMessages) + const chatPrompt = prepareChatPrompt(nodeData) let promptVariables = {} const promptValuesRaw = (chatPrompt as any).promptValues if (promptValuesRaw) { @@ -227,7 +207,7 @@ const prepareChain = (nodeData: INodeData, options: ICommonObject, sessionId?: s }, ...promptVariables }, - prepareChatPrompt(nodeData, humanImageMessages), + prepareChatPrompt(nodeData), model, new StringOutputParser() ]) diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 18048283..1bc2f338 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -8,7 +8,7 @@ import { formatResponse, injectOutputParser } from '../../outputparsers/OutputPa import { BaseLLMOutputParser } from 'langchain/schema/output_parser' import { OutputFixingParser } from 'langchain/output_parsers' import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' -import { injectChainNodeData } from '../../../src/multiModalUtils' +import { injectLLMChainNodeData } from '../../../src/multiModalUtils' class LLMChain_Chains implements INode { label: string @@ -108,7 +108,7 @@ class LLMChain_Chains implements INode { verbose: process.env.DEBUG === 'true' }) const inputVariables = chain.prompt.inputVariables as string[] // ["product"] - injectChainNodeData(nodeData, options) + injectLLMChainNodeData(nodeData, options) promptValues = injectOutputParser(this.outputParser, chain, promptValues) const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) // eslint-disable-next-line no-console @@ -138,7 +138,7 @@ class LLMChain_Chains implements INode { if (!this.outputParser && outputParser) { this.outputParser = outputParser } - injectChainNodeData(nodeData, options) + injectLLMChainNodeData(nodeData, options) promptValues = injectOutputParser(this.outputParser, chain, promptValues) const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) // eslint-disable-next-line no-console diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 3943cfe3..3884ae7d 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -6,30 +6,15 @@ import { BaseLanguageModelInput } from 'langchain/base_language' import { ChatOpenAICallOptions } from '@langchain/openai/dist/chat_models' import { BaseMessageChunk, BaseMessageLike, HumanMessage, LLMResult } from 'langchain/schema' import { Callbacks } from '@langchain/core/callbacks/manager' -import { ICommonObject, IMultiModalOption, INodeData } from '../../../src' -import { addImagesToMessages } from '../../../src/multiModalUtils' - -interface MultiModalOptions { - chainNodeData: INodeData - chainNodeOptions: ICommonObject -} +import { IMultiModalOption } from '../../../src' +import { addImagesToMessages, MultiModalOptions } from '../../../src/multiModalUtils' export class ChatOpenAI extends LangchainChatOpenAI { - //TODO: Should be class variables and not static - // public static nodeData: INodeData - // public static nodeOptions: ICommonObject - private static chainNodeDataOptions: Map = new Map() configuredModel: string configuredMaxToken?: number multiModalOption?: IMultiModalOption id: string - public static injectChainNodeData(nodeData: INodeData, options: ICommonObject) { - if (nodeData.inputs?.model.id) { - ChatOpenAI.chainNodeDataOptions.set(nodeData.inputs?.model.id, { chainNodeData: nodeData, chainNodeOptions: options }) - } - } - constructor( id: string, fields?: Partial & BaseChatModelParams & { openAIApiKey?: string; multiModalOption?: IMultiModalOption }, @@ -48,15 +33,15 @@ export class ChatOpenAI extends LangchainChatOpenAI { } async generate(messages: BaseMessageLike[][], options?: string[] | ChatOpenAICallOptions, callbacks?: Callbacks): Promise { - if (ChatOpenAI.chainNodeDataOptions.has(this.id)) { - await this.injectMultiModalMessages(messages, ChatOpenAI.chainNodeDataOptions.get(this.id) as MultiModalOptions) + if (this.lc_kwargs.chainData) { + await this.injectMultiModalMessages(messages, this.lc_kwargs.chainData) } return super.generate(messages, options, callbacks) } private async injectMultiModalMessages(messages: BaseMessageLike[][], nodeOptions: MultiModalOptions) { - const nodeData = nodeOptions.chainNodeData - const optionsData = nodeOptions.chainNodeOptions + const nodeData = nodeOptions.nodeData + const optionsData = nodeOptions.nodeOptions const messageContent = addImagesToMessages(nodeData, optionsData, this.multiModalOption) if (messageContent?.length) { if (messages[0].length > 0 && messages[0][messages[0].length - 1] instanceof HumanMessage) { diff --git a/packages/components/src/multiModalUtils.ts b/packages/components/src/multiModalUtils.ts index 246821db..81ae1999 100644 --- a/packages/components/src/multiModalUtils.ts +++ b/packages/components/src/multiModalUtils.ts @@ -1,20 +1,55 @@ import { ICommonObject, IFileUpload, IMultiModalOption, INodeData, MessageContentImageUrl } from './Interface' -import { BaseChatModel } from 'langchain/chat_models/base' import { ChatOpenAI as LangchainChatOpenAI } from 'langchain/chat_models/openai' import path from 'path' import { getStoragePath } from './utils' import fs from 'fs' import { ChatOpenAI } from '../nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI' +import { LLMChain } from 'langchain/chains' +import { RunnableBinding, RunnableSequence } from 'langchain/schema/runnable' +import { AgentExecutor as LcAgentExecutor, ChatAgent, RunnableAgent } from 'langchain/agents' +import { AgentExecutor } from './agents' -export const injectChainNodeData = (nodeData: INodeData, options: ICommonObject) => { - let model = nodeData.inputs?.model as BaseChatModel +export interface MultiModalOptions { + nodeData: INodeData + nodeOptions: ICommonObject +} - if (model instanceof ChatOpenAI) { - // TODO: this should not be static, need to figure out how to pass the nodeData and options to the invoke method - ChatOpenAI.injectChainNodeData(nodeData, options) +export const injectLLMChainNodeData = (nodeData: INodeData, options: ICommonObject) => { + let llmChain = nodeData.instance as LLMChain + ;(llmChain.llm as ChatOpenAI).lc_kwargs.chainData = { nodeData: nodeData, nodeOptions: options } +} + +export const injectAgentExecutorNodeData = (agentExecutor: AgentExecutor, nodeData: INodeData, options: ICommonObject) => { + if (agentExecutor.agent instanceof RunnableAgent && agentExecutor.agent.runnable instanceof RunnableSequence) { + let rs = agentExecutor.agent.runnable as RunnableSequence + injectRunnableNodeData(rs, nodeData, options) } } +export const injectLcAgentExecutorNodeData = (agentExecutor: LcAgentExecutor, nodeData: INodeData, options: ICommonObject) => { + if (agentExecutor.agent instanceof ChatAgent) { + let llmChain = agentExecutor.agent.llmChain as LLMChain + ;(llmChain.llm as ChatOpenAI).lc_kwargs.chainData = { nodeData: nodeData, nodeOptions: options } + } +} + +export const injectRunnableNodeData = (runnableSequence: RunnableSequence, nodeData: INodeData, options: ICommonObject) => { + runnableSequence.steps.forEach((step) => { + if (step instanceof ChatOpenAI) { + ;(step as ChatOpenAI).lc_kwargs.chainData = { nodeData: nodeData, nodeOptions: options } + } + + if (step instanceof RunnableBinding) { + if ((step as RunnableBinding).bound instanceof ChatOpenAI) { + ;((step as RunnableBinding).bound as ChatOpenAI).lc_kwargs.chainData = { + nodeData: nodeData, + nodeOptions: options + } + } + } + }) +} + export const addImagesToMessages = ( nodeData: INodeData, options: ICommonObject, From d67c265c0a1842ac82eb77a7c71f1cb07b95337b Mon Sep 17 00:00:00 2001 From: Andres Pegam Date: Fri, 16 Feb 2024 11:43:32 +0100 Subject: [PATCH 443/502] use URL constructor to handle all types of links This should enable to handle all types of relative URLs defined in RFC: https://www.rfc-editor.org/rfc/rfc3986#section-5.4 --- packages/components/src/utils.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 548632ea..e7977809 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -290,22 +290,12 @@ function getURLsFromHTML(htmlBody: string, baseURL: string): string[] { const linkElements = dom.window.document.querySelectorAll('a') const urls: string[] = [] for (const linkElement of linkElements) { - if (linkElement.href.slice(0, 1) === '/') { - try { - const urlObj = new URL(baseURL + linkElement.href) - urls.push(urlObj.href) //relative - } catch (err) { - if (process.env.DEBUG === 'true') console.error(`error with relative url: ${err.message}`) - continue - } - } else { - try { - const urlObj = new URL(linkElement.href) - urls.push(urlObj.href) //absolute - } catch (err) { - if (process.env.DEBUG === 'true') console.error(`error with absolute url: ${err.message}`) - continue - } + try { + const urlObj = new URL(linkElement.href, baseURL) + urls.push(urlObj.href) + } catch (err) { + if (process.env.DEBUG === 'true') console.error(`error with scraped URL: ${err.message}`) + continue } } return urls @@ -365,7 +355,7 @@ async function crawl(baseURL: string, currentURL: string, pages: string[], limit } const htmlBody = await resp.text() - const nextURLs = getURLsFromHTML(htmlBody, baseURL) + const nextURLs = getURLsFromHTML(htmlBody, currentURL) for (const nextURL of nextURLs) { pages = await crawl(baseURL, nextURL, pages, limit) } From 342adff8cb551c0166c75a6b10d66e7832dcef95 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Fri, 16 Feb 2024 20:20:17 +0800 Subject: [PATCH 444/502] fix weird error --- packages/components/nodes/tools/CustomTool/core.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 19be88f1..baaddb42 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -100,6 +100,7 @@ export class DynamicStructuredTool< return result } + // @ts-ignore protected async _call( arg: z.output, _?: CallbackManagerForToolRun, From 10fc1bf08d20d41db20b566961dad19e1af79db9 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 16 Feb 2024 08:18:58 -0500 Subject: [PATCH 445/502] Multimodal Fixes for cyclic (circular) dependencies during langsmith analysis... --- .../ChatOpenAI/FlowiseChatOpenAI.ts | 7 +- packages/components/src/multiModalUtils.ts | 64 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts index 3884ae7d..acfc064c 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/FlowiseChatOpenAI.ts @@ -39,10 +39,9 @@ export class ChatOpenAI extends LangchainChatOpenAI { return super.generate(messages, options, callbacks) } - private async injectMultiModalMessages(messages: BaseMessageLike[][], nodeOptions: MultiModalOptions) { - const nodeData = nodeOptions.nodeData - const optionsData = nodeOptions.nodeOptions - const messageContent = addImagesToMessages(nodeData, optionsData, this.multiModalOption) + private async injectMultiModalMessages(messages: BaseMessageLike[][], options: MultiModalOptions) { + const optionsData = options.nodeOptions + const messageContent = addImagesToMessages(optionsData, this.multiModalOption) if (messageContent?.length) { if (messages[0].length > 0 && messages[0][messages[0].length - 1] instanceof HumanMessage) { // Change model to gpt-4-vision diff --git a/packages/components/src/multiModalUtils.ts b/packages/components/src/multiModalUtils.ts index 81ae1999..a7262324 100644 --- a/packages/components/src/multiModalUtils.ts +++ b/packages/components/src/multiModalUtils.ts @@ -1,5 +1,4 @@ import { ICommonObject, IFileUpload, IMultiModalOption, INodeData, MessageContentImageUrl } from './Interface' -import { ChatOpenAI as LangchainChatOpenAI } from 'langchain/chat_models/openai' import path from 'path' import { getStoragePath } from './utils' import fs from 'fs' @@ -10,13 +9,12 @@ import { AgentExecutor as LcAgentExecutor, ChatAgent, RunnableAgent } from 'lang import { AgentExecutor } from './agents' export interface MultiModalOptions { - nodeData: INodeData nodeOptions: ICommonObject } export const injectLLMChainNodeData = (nodeData: INodeData, options: ICommonObject) => { let llmChain = nodeData.instance as LLMChain - ;(llmChain.llm as ChatOpenAI).lc_kwargs.chainData = { nodeData: nodeData, nodeOptions: options } + ;(llmChain.llm as ChatOpenAI).lc_kwargs.chainData = { nodeOptions: getUploadsFromOptions(options) } } export const injectAgentExecutorNodeData = (agentExecutor: AgentExecutor, nodeData: INodeData, options: ICommonObject) => { @@ -29,56 +27,58 @@ export const injectAgentExecutorNodeData = (agentExecutor: AgentExecutor, nodeDa export const injectLcAgentExecutorNodeData = (agentExecutor: LcAgentExecutor, nodeData: INodeData, options: ICommonObject) => { if (agentExecutor.agent instanceof ChatAgent) { let llmChain = agentExecutor.agent.llmChain as LLMChain - ;(llmChain.llm as ChatOpenAI).lc_kwargs.chainData = { nodeData: nodeData, nodeOptions: options } + ;(llmChain.llm as ChatOpenAI).lc_kwargs.chainData = { nodeOptions: getUploadsFromOptions(options) } } } export const injectRunnableNodeData = (runnableSequence: RunnableSequence, nodeData: INodeData, options: ICommonObject) => { runnableSequence.steps.forEach((step) => { if (step instanceof ChatOpenAI) { - ;(step as ChatOpenAI).lc_kwargs.chainData = { nodeData: nodeData, nodeOptions: options } + ;(step as ChatOpenAI).lc_kwargs.chainData = { nodeOptions: getUploadsFromOptions(options) } } if (step instanceof RunnableBinding) { if ((step as RunnableBinding).bound instanceof ChatOpenAI) { ;((step as RunnableBinding).bound as ChatOpenAI).lc_kwargs.chainData = { - nodeData: nodeData, - nodeOptions: options + nodeOptions: getUploadsFromOptions(options) } } } }) } -export const addImagesToMessages = ( - nodeData: INodeData, - options: ICommonObject, - multiModalOption?: IMultiModalOption -): MessageContentImageUrl[] => { +const getUploadsFromOptions = (options: ICommonObject): ICommonObject => { + if (options?.uploads) { + return { + uploads: options.uploads, + chatflowid: options.chatflowid, + chatId: options.chatId + } + } + return {} +} + +export const addImagesToMessages = (options: ICommonObject, multiModalOption?: IMultiModalOption): MessageContentImageUrl[] => { const imageContent: MessageContentImageUrl[] = [] - let model = nodeData.inputs?.model - if (model instanceof LangchainChatOpenAI && multiModalOption) { - // Image Uploaded - if (multiModalOption.image && multiModalOption.image.allowImageUploads && options?.uploads && options?.uploads.length > 0) { - const imageUploads = getImageUploads(options.uploads) - for (const upload of imageUploads) { - let bf = upload.data - if (upload.type == 'stored-file') { - const filePath = path.join(getStoragePath(), options.chatflowid, options.chatId, upload.name) + // Image Uploaded + if (multiModalOption?.image && multiModalOption?.image.allowImageUploads && options?.uploads && options?.uploads.length > 0) { + const imageUploads = getImageUploads(options.uploads) + for (const upload of imageUploads) { + if (upload.type == 'stored-file') { + const filePath = path.join(getStoragePath(), options.chatflowid, options.chatId, upload.name) - // as the image is stored in the server, read the file and convert it to base64 - const contents = fs.readFileSync(filePath) - bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64') + // as the image is stored in the server, read the file and convert it to base64 + const contents = fs.readFileSync(filePath) + let bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64') - imageContent.push({ - type: 'image_url', - image_url: { - url: bf, - detail: multiModalOption.image.imageResolution ?? 'low' - } - }) - } + imageContent.push({ + type: 'image_url', + image_url: { + url: bf, + detail: multiModalOption?.image.imageResolution ?? 'low' + } + }) } } } From a035940d1d8e0e1680729755302db7422799f8c0 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 17 Feb 2024 20:25:20 +0800 Subject: [PATCH 446/502] add gpt-4-1106-vision-preview into ChatOpenAI node --- packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index a0d0bd6e..d50c0012 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -59,6 +59,10 @@ class ChatOpenAI_ChatModels implements INode { label: 'gpt-4-1106-preview', name: 'gpt-4-1106-preview' }, + { + label: 'gpt-4-1106-vision-preview', + name: 'gpt-4-1106-vision-preview' + }, { label: 'gpt-4-vision-preview', name: 'gpt-4-vision-preview' From dd64bd028bae1a992ef979f0c5fa01725840a8e5 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 17 Feb 2024 21:11:27 +0800 Subject: [PATCH 447/502] add gpt-4-1106-vision-preview into ChatOpenAI marketplace --- .../chatflows/API Agent OpenAI.json | 16 +++++++++++++ .../marketplaces/chatflows/API Agent.json | 24 +++++++++++++++++++ .../Advanced Structured Output Parser.json | 4 ++++ .../marketplaces/chatflows/Antonym.json | 8 +++++++ .../marketplaces/chatflows/AutoGPT.json | 8 +++++++ .../marketplaces/chatflows/BabyAGI.json | 8 +++++++ .../marketplaces/chatflows/CSV Agent.json | 8 +++++++ .../chatflows/Chat with a Podcast.json | 8 +++++++ .../marketplaces/chatflows/ChatGPTPlugin.json | 8 +++++++ .../chatflows/Conversational Agent.json | 8 +++++++ .../Conversational Retrieval Agent.json | 8 +++++++ .../Conversational Retrieval QA Chain.json | 8 +++++++ .../chatflows/Flowise Docs QnA.json | 8 +++++++ .../server/marketplaces/chatflows/IfElse.json | 8 +++++++ .../chatflows/Image Generation.json | 8 +++++++ .../chatflows/Input Moderation.json | 8 +++++++ .../chatflows/List Output Parser.json | 8 +++++++ .../chatflows/Long Term Memory.json | 8 +++++++ .../chatflows/Metadata Filter.json | 8 +++++++ .../chatflows/Multi Prompt Chain.json | 8 +++++++ .../chatflows/Multi Retrieval QA Chain.json | 8 +++++++ .../chatflows/Multiple VectorDB.json | 24 +++++++++++++++++++ .../marketplaces/chatflows/OpenAI Agent.json | 8 +++++++ .../Prompt Chaining with VectorStore.json | 16 +++++++++++++ .../marketplaces/chatflows/ReAct Agent.json | 8 +++++++ .../marketplaces/chatflows/SQL DB Chain.json | 8 +++++++ .../marketplaces/chatflows/SQL Prompt.json | 24 +++++++++++++++++++ .../chatflows/Simple Conversation Chain.json | 8 +++++++ .../chatflows/Structured Output Parser.json | 8 +++++++ .../marketplaces/chatflows/Translator.json | 8 +++++++ .../marketplaces/chatflows/WebBrowser.json | 16 +++++++++++++ .../marketplaces/chatflows/WebPage QnA.json | 8 +++++++ 32 files changed, 324 insertions(+) diff --git a/packages/server/marketplaces/chatflows/API Agent OpenAI.json b/packages/server/marketplaces/chatflows/API Agent OpenAI.json index 621529fc..f3b5636e 100644 --- a/packages/server/marketplaces/chatflows/API Agent OpenAI.json +++ b/packages/server/marketplaces/chatflows/API Agent OpenAI.json @@ -125,6 +125,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -145,6 +149,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -464,6 +472,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -484,6 +496,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/API Agent.json b/packages/server/marketplaces/chatflows/API Agent.json index 9d5a6c54..31679a92 100644 --- a/packages/server/marketplaces/chatflows/API Agent.json +++ b/packages/server/marketplaces/chatflows/API Agent.json @@ -433,6 +433,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -453,6 +457,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -624,6 +632,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -644,6 +656,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -815,6 +831,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -835,6 +855,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json index 3fd71988..bf477ba7 100644 --- a/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Advanced Structured Output Parser.json @@ -216,6 +216,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" diff --git a/packages/server/marketplaces/chatflows/Antonym.json b/packages/server/marketplaces/chatflows/Antonym.json index 97c5af71..a88b16cc 100644 --- a/packages/server/marketplaces/chatflows/Antonym.json +++ b/packages/server/marketplaces/chatflows/Antonym.json @@ -212,6 +212,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -232,6 +236,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/AutoGPT.json b/packages/server/marketplaces/chatflows/AutoGPT.json index c0ed0807..c2ec12b3 100644 --- a/packages/server/marketplaces/chatflows/AutoGPT.json +++ b/packages/server/marketplaces/chatflows/AutoGPT.json @@ -288,6 +288,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -308,6 +312,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/BabyAGI.json b/packages/server/marketplaces/chatflows/BabyAGI.json index 14976ad3..618a4920 100644 --- a/packages/server/marketplaces/chatflows/BabyAGI.json +++ b/packages/server/marketplaces/chatflows/BabyAGI.json @@ -381,6 +381,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -401,6 +405,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/CSV Agent.json b/packages/server/marketplaces/chatflows/CSV Agent.json index 3439625b..4d755155 100644 --- a/packages/server/marketplaces/chatflows/CSV Agent.json +++ b/packages/server/marketplaces/chatflows/CSV Agent.json @@ -106,6 +106,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -126,6 +130,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Chat with a Podcast.json b/packages/server/marketplaces/chatflows/Chat with a Podcast.json index c87b3f2c..e8f8399c 100644 --- a/packages/server/marketplaces/chatflows/Chat with a Podcast.json +++ b/packages/server/marketplaces/chatflows/Chat with a Podcast.json @@ -231,6 +231,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -251,6 +255,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/ChatGPTPlugin.json b/packages/server/marketplaces/chatflows/ChatGPTPlugin.json index 3777b637..2b87b51d 100644 --- a/packages/server/marketplaces/chatflows/ChatGPTPlugin.json +++ b/packages/server/marketplaces/chatflows/ChatGPTPlugin.json @@ -251,6 +251,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -271,6 +275,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Conversational Agent.json b/packages/server/marketplaces/chatflows/Conversational Agent.json index 4cb736a0..13b479af 100644 --- a/packages/server/marketplaces/chatflows/Conversational Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Agent.json @@ -193,6 +193,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -213,6 +217,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json index a4ec6b5b..4120ef17 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval Agent.json @@ -524,6 +524,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -544,6 +548,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json index f76e89e6..32e0c762 100644 --- a/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Conversational Retrieval QA Chain.json @@ -406,6 +406,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -426,6 +430,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json index 31d65c48..8ed4ac56 100644 --- a/packages/server/marketplaces/chatflows/Flowise Docs QnA.json +++ b/packages/server/marketplaces/chatflows/Flowise Docs QnA.json @@ -412,6 +412,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -432,6 +436,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/IfElse.json b/packages/server/marketplaces/chatflows/IfElse.json index e3b66f44..bcb2cd96 100644 --- a/packages/server/marketplaces/chatflows/IfElse.json +++ b/packages/server/marketplaces/chatflows/IfElse.json @@ -945,6 +945,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -965,6 +969,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Image Generation.json b/packages/server/marketplaces/chatflows/Image Generation.json index 46cb79ec..4b82dcf3 100644 --- a/packages/server/marketplaces/chatflows/Image Generation.json +++ b/packages/server/marketplaces/chatflows/Image Generation.json @@ -489,6 +489,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -509,6 +513,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Input Moderation.json b/packages/server/marketplaces/chatflows/Input Moderation.json index bd449777..2b78bdb2 100644 --- a/packages/server/marketplaces/chatflows/Input Moderation.json +++ b/packages/server/marketplaces/chatflows/Input Moderation.json @@ -201,6 +201,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -221,6 +225,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/List Output Parser.json b/packages/server/marketplaces/chatflows/List Output Parser.json index 0eb269b4..5bdfff71 100644 --- a/packages/server/marketplaces/chatflows/List Output Parser.json +++ b/packages/server/marketplaces/chatflows/List Output Parser.json @@ -260,6 +260,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -280,6 +284,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Long Term Memory.json b/packages/server/marketplaces/chatflows/Long Term Memory.json index c5681d3d..7b54b146 100644 --- a/packages/server/marketplaces/chatflows/Long Term Memory.json +++ b/packages/server/marketplaces/chatflows/Long Term Memory.json @@ -543,6 +543,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -563,6 +567,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Metadata Filter.json b/packages/server/marketplaces/chatflows/Metadata Filter.json index ed2efb9f..a37764df 100644 --- a/packages/server/marketplaces/chatflows/Metadata Filter.json +++ b/packages/server/marketplaces/chatflows/Metadata Filter.json @@ -490,6 +490,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -510,6 +514,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Multi Prompt Chain.json b/packages/server/marketplaces/chatflows/Multi Prompt Chain.json index 97cca308..8b933e8f 100644 --- a/packages/server/marketplaces/chatflows/Multi Prompt Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Prompt Chain.json @@ -314,6 +314,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -334,6 +338,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json index 6b8f2c33..0fc6ff77 100644 --- a/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json +++ b/packages/server/marketplaces/chatflows/Multi Retrieval QA Chain.json @@ -425,6 +425,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -445,6 +449,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Multiple VectorDB.json b/packages/server/marketplaces/chatflows/Multiple VectorDB.json index b76270e7..3d8a22f3 100644 --- a/packages/server/marketplaces/chatflows/Multiple VectorDB.json +++ b/packages/server/marketplaces/chatflows/Multiple VectorDB.json @@ -522,6 +522,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -542,6 +546,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -1040,6 +1048,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -1060,6 +1072,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -1238,6 +1254,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -1258,6 +1278,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/OpenAI Agent.json b/packages/server/marketplaces/chatflows/OpenAI Agent.json index 6f35e595..fa9184d2 100644 --- a/packages/server/marketplaces/chatflows/OpenAI Agent.json +++ b/packages/server/marketplaces/chatflows/OpenAI Agent.json @@ -316,6 +316,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -336,6 +340,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json index d225e41a..b2db86c4 100644 --- a/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json +++ b/packages/server/marketplaces/chatflows/Prompt Chaining with VectorStore.json @@ -464,6 +464,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -484,6 +488,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -662,6 +670,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -682,6 +694,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/ReAct Agent.json b/packages/server/marketplaces/chatflows/ReAct Agent.json index 5fd191fe..151583a2 100644 --- a/packages/server/marketplaces/chatflows/ReAct Agent.json +++ b/packages/server/marketplaces/chatflows/ReAct Agent.json @@ -136,6 +136,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -156,6 +160,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/SQL DB Chain.json b/packages/server/marketplaces/chatflows/SQL DB Chain.json index ec9d465d..0d6a656f 100644 --- a/packages/server/marketplaces/chatflows/SQL DB Chain.json +++ b/packages/server/marketplaces/chatflows/SQL DB Chain.json @@ -50,6 +50,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -70,6 +74,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/SQL Prompt.json b/packages/server/marketplaces/chatflows/SQL Prompt.json index 8d2691c6..aeb62416 100644 --- a/packages/server/marketplaces/chatflows/SQL Prompt.json +++ b/packages/server/marketplaces/chatflows/SQL Prompt.json @@ -210,6 +210,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -230,6 +234,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -408,6 +416,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -428,6 +440,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -1332,6 +1348,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -1352,6 +1372,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json index 53cfeace..5bda9580 100644 --- a/packages/server/marketplaces/chatflows/Simple Conversation Chain.json +++ b/packages/server/marketplaces/chatflows/Simple Conversation Chain.json @@ -51,6 +51,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -71,6 +75,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Structured Output Parser.json b/packages/server/marketplaces/chatflows/Structured Output Parser.json index 9801a90f..2e693ee5 100644 --- a/packages/server/marketplaces/chatflows/Structured Output Parser.json +++ b/packages/server/marketplaces/chatflows/Structured Output Parser.json @@ -51,6 +51,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -71,6 +75,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/Translator.json b/packages/server/marketplaces/chatflows/Translator.json index 5c8a3cc5..a9a8b570 100644 --- a/packages/server/marketplaces/chatflows/Translator.json +++ b/packages/server/marketplaces/chatflows/Translator.json @@ -119,6 +119,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -139,6 +143,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/WebBrowser.json b/packages/server/marketplaces/chatflows/WebBrowser.json index 232bd83e..600639d8 100644 --- a/packages/server/marketplaces/chatflows/WebBrowser.json +++ b/packages/server/marketplaces/chatflows/WebBrowser.json @@ -162,6 +162,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -182,6 +186,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" @@ -460,6 +468,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -480,6 +492,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" diff --git a/packages/server/marketplaces/chatflows/WebPage QnA.json b/packages/server/marketplaces/chatflows/WebPage QnA.json index 50806161..24b13b87 100644 --- a/packages/server/marketplaces/chatflows/WebPage QnA.json +++ b/packages/server/marketplaces/chatflows/WebPage QnA.json @@ -429,6 +429,10 @@ "label": "gpt-4-1106-preview", "name": "gpt-4-1106-preview" }, + { + "label": "gpt-4-1106-vision-preview", + "name": "gpt-4-1106-vision-preview" + }, { "label": "gpt-4-vision-preview", "name": "gpt-4-vision-preview" @@ -449,6 +453,10 @@ "label": "gpt-3.5-turbo", "name": "gpt-3.5-turbo" }, + { + "label": "gpt-3.5-turbo-0125", + "name": "gpt-3.5-turbo-0125" + }, { "label": "gpt-3.5-turbo-1106", "name": "gpt-3.5-turbo-1106" From 81c07dc8c1cc770b954e1278c127524cb9ff0561 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 19 Feb 2024 11:49:01 +0530 Subject: [PATCH 448/502] Update UI of speech to text dialog --- .../ui-component/dialog/SpeechToTextDialog.js | 291 ++++++++---------- .../ui-component/dropdown/AsyncDropdown.js | 6 +- .../views/canvas/CredentialInputHandler.js | 6 +- 3 files changed, 145 insertions(+), 158 deletions(-) diff --git a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js index 9fc11a72..60634664 100644 --- a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js +++ b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js @@ -13,14 +13,13 @@ import { DialogContent, DialogTitle, DialogActions, - Accordion, - AccordionSummary, - AccordionDetails, + FormControl, ListItem, ListItemAvatar, - ListItemText + ListItemText, + MenuItem, + Select } from '@mui/material' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { IconX } from '@tabler/icons' // Project import @@ -40,8 +39,8 @@ import useNotifier from 'utils/useNotifier' // API import chatflowsApi from 'api/chatflows' -const speechToTextProviders = [ - { +const speechToTextProviders = { + openAIWhisper: { label: 'OpenAI Whisper', name: 'openAIWhisper', icon: openAISVG, @@ -77,16 +76,10 @@ const speechToTextProviders = [ step: 0.1, description: `The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.`, optional: true - }, - { - label: 'On/Off', - name: 'status', - type: 'boolean', - optional: true } ] }, - { + assemblyAiTranscribe: { label: 'Assembly AI', name: 'assemblyAiTranscribe', icon: assemblyAIPng, @@ -97,16 +90,10 @@ const speechToTextProviders = [ name: 'credential', type: 'credential', credentialNames: ['assemblyAIApi'] - }, - { - label: 'On/Off', - name: 'status', - type: 'boolean', - optional: true } ] } -] +} const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') @@ -118,7 +105,7 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const [speechToText, setSpeechToText] = useState({}) - const [providerExpanded, setProviderExpanded] = useState({}) + const [selectedProvider, setSelectedProvider] = useState('openAIWhisper') const onSave = async () => { try { @@ -169,8 +156,9 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { newVal[providerName][inputParamName] = value if (inputParamName === 'status' && value === true) { - //ensure that the others are turned off - speechToTextProviders.forEach((provider) => { + // ensure that the others are turned off + Object.keys(speechToTextProviders).forEach((key) => { + const provider = speechToTextProviders[key] if (provider.name !== providerName) { newVal[provider.name] = { ...speechToText[provider.name], status: false } } @@ -179,10 +167,9 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { setSpeechToText(newVal) } - const handleAccordionChange = (providerName) => (event, isExpanded) => { - const accordionProviders = { ...providerExpanded } - accordionProviders[providerName] = isExpanded - setProviderExpanded(accordionProviders) + const handleProviderChange = (event) => { + setSelectedProvider(event.target.value) + setValue(true, event.target.value, 'status') } useEffect(() => { @@ -197,7 +184,6 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { return () => { setSpeechToText({}) - setProviderExpanded({}) } }, [dialogProps]) @@ -220,136 +206,129 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { Speech To Text Configuration - {speechToTextProviders.map((provider, index) => ( - - } aria-controls={provider.name} id={provider.name}> - - -
    - AI -
    -
    - - {provider.url} - + + Speech To Text Providers + + + + + <> + + +
    + AI +
    +
    + + {speechToTextProviders[selectedProvider].url} + + } + /> + {speechToText[selectedProvider] && speechToText[selectedProvider].status && ( +
    +
    + ON +
    + )} + + {speechToTextProviders[selectedProvider].inputs.map((inputParam, index) => ( + +
    + + {inputParam.label} + {!inputParam.optional &&  *} + {inputParam.description && ( + + )} + +
    + {inputParam.type === 'credential' && ( + setValue(newValue, selectedProvider, 'credentialId')} + /> + )} + {inputParam.type === 'boolean' && ( + setValue(newValue, selectedProvider, inputParam.name)} + value={ + speechToText[selectedProvider] + ? speechToText[selectedProvider][inputParam.name] + : inputParam.default ?? false } /> - {speechToText[provider.name] && speechToText[provider.name].status && ( -
    -
    - ON -
    - )} - - - - {provider.inputs.map((inputParam, index) => ( - -
    - - {inputParam.label} - {!inputParam.optional &&  *} - {inputParam.description && ( - - )} - -
    - {providerExpanded[provider.name] && inputParam.type === 'credential' && ( - setValue(newValue, provider.name, 'credentialId')} - /> - )} - {inputParam.type === 'boolean' && ( - setValue(newValue, provider.name, inputParam.name)} - value={ - speechToText[provider.name] - ? speechToText[provider.name][inputParam.name] - : inputParam.default ?? false - } - /> - )} - {providerExpanded[provider.name] && - (inputParam.type === 'string' || - inputParam.type === 'password' || - inputParam.type === 'number') && ( - setValue(newValue, provider.name, inputParam.name)} - value={ - speechToText[provider.name] - ? speechToText[provider.name][inputParam.name] - : inputParam.default ?? '' - } - /> - )} + )} + {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( + setValue(newValue, selectedProvider, inputParam.name)} + value={ + speechToText[selectedProvider] + ? speechToText[selectedProvider][inputParam.name] + : inputParam.default ?? '' + } + /> + )} - {providerExpanded[provider.name] && inputParam.type === 'options' && ( - setValue(newValue, provider.name, inputParam.name)} - value={ - speechToText[provider.name] - ? speechToText[provider.name][inputParam.name] - : inputParam.default ?? 'choose an option' - } - /> - )} -
    - ))} -
    - - ))} + {inputParam.type === 'options' && ( + setValue(newValue, selectedProvider, inputParam.name)} + value={ + speechToText[selectedProvider] + ? speechToText[selectedProvider][inputParam.name] + : inputParam.default ?? 'choose an option' + } + /> + )} + + ))} + diff --git a/packages/ui/src/ui-component/dropdown/AsyncDropdown.js b/packages/ui/src/ui-component/dropdown/AsyncDropdown.js index b24fa02b..b98410a8 100644 --- a/packages/ui/src/ui-component/dropdown/AsyncDropdown.js +++ b/packages/ui/src/ui-component/dropdown/AsyncDropdown.js @@ -105,7 +105,11 @@ export const AsyncDropdown = ({ })() // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [credentialNames]) + + useEffect(() => { + setInternalValue(value) + }, [value]) return ( <> diff --git a/packages/ui/src/views/canvas/CredentialInputHandler.js b/packages/ui/src/views/canvas/CredentialInputHandler.js index 4f874719..8285a00d 100644 --- a/packages/ui/src/views/canvas/CredentialInputHandler.js +++ b/packages/ui/src/views/canvas/CredentialInputHandler.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' // material-ui import { IconButton } from '@mui/material' @@ -88,6 +88,10 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false } setShowSpecificCredentialDialog(true) } + useEffect(() => { + setCredentialId(data?.credential ?? '') + }, [data]) + return (
    {inputParam && ( From 5aa991ae56d910120b3bbb27e1ead14a81e79559 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 19 Feb 2024 12:15:43 +0530 Subject: [PATCH 449/502] Update how uploads are shown in view messages dialog --- .../ui-component/dialog/ViewMessagesDialog.js | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 2df501fb..a28f2c68 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -632,8 +632,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { sx={{ background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : '', - pl: 1, - pr: 1 + py: '1rem', + px: '1.5rem' }} key={index} style={{ display: 'flex', justifyContent: 'center', alignContent: 'center' }} @@ -683,34 +683,6 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { })}
    )} -
    - {/* Messages are being rendered in Markdown format */} - - ) : ( - - {children} - - ) - } - }} - > - {message.message} - -
    {message.fileUploads && message.fileUploads.length > 0 && (
    { })}
    )} +
    + {/* Messages are being rendered in Markdown format */} + + ) : ( + + {children} + + ) + } + }} + > + {message.message} + +
    {message.fileAnnotations && (
    {message.fileAnnotations.map((fileAnnotation, index) => { From 4773a13042bc583c2411b7ba29e8683c05b204bb Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 19 Feb 2024 18:49:35 +0800 Subject: [PATCH 450/502] update lc version --- .../nodes/vectorstores/Pinecone/Pinecone.ts | 2 ++ packages/components/package.json | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts index cb204820..3ef8a8e8 100644 --- a/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts +++ b/packages/components/nodes/vectorstores/Pinecone/Pinecone.ts @@ -148,6 +148,8 @@ class Pinecone_VectorStores implements INode { apiKey: pineconeApiKey }) + await client.describeIndex(index) + const pineconeIndex = client.Index(index) const flattenDocs = docs && docs.length ? flatten(docs) : [] diff --git a/packages/components/package.json b/packages/components/package.json index f50d3fed..b53945bd 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -28,13 +28,13 @@ "@google-ai/generativelanguage": "^0.2.1", "@google/generative-ai": "^0.1.3", "@huggingface/inference": "^2.6.1", - "@langchain/anthropic": "^0.0.9", - "@langchain/cohere": "^0.0.2", - "@langchain/community": "^0.0.20", - "@langchain/google-genai": "^0.0.7", - "@langchain/mistralai": "^0.0.6", - "@langchain/openai": "^0.0.12", - "@langchain/pinecone": "^0.0.1", + "@langchain/anthropic": "^0.0.10", + "@langchain/cohere": "^0.0.5", + "@langchain/community": "^0.0.30", + "@langchain/google-genai": "^0.0.10", + "@langchain/mistralai": "^0.0.7", + "@langchain/openai": "^0.0.14", + "@langchain/pinecone": "^0.0.3", "@notionhq/client": "^2.2.8", "@opensearch-project/opensearch": "^1.2.0", "@pinecone-database/pinecone": "^2.0.1", @@ -61,7 +61,7 @@ "husky": "^8.0.3", "ioredis": "^5.3.2", "jsonpointer": "^5.0.1", - "langchain": "^0.1.14", + "langchain": "^0.1.20", "langfuse": "2.0.2", "langfuse-langchain": "^2.6.2-alpha.0", "langsmith": "0.0.63", From d313dc67546932e949baec2291e55f7839d50566 Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 19 Feb 2024 19:20:07 +0530 Subject: [PATCH 451/502] Show transcribed audio inputs as message along with audio clip in internal chat --- packages/server/src/index.ts | 5 ++++- .../src/ui-component/dialog/ViewMessagesDialog.js | 4 ++-- packages/ui/src/views/chatmessage/ChatMessage.js | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 22185170..38edfd8f 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1811,7 +1811,7 @@ export class App { } // Run Speech to Text conversion - if (upload.mime === 'audio/webm' && incomingInput.uploads?.length === 1) { + if (upload.mime === 'audio/webm') { let speechToTextConfig: ICommonObject = {} if (chatflow.speechToText) { const speechToTextProviders = JSON.parse(chatflow.speechToText) @@ -2111,6 +2111,9 @@ export class App { }) // Prepare response + // return the question in the response + // this is used when input text is empty but question is in audio format + result.question = incomingInput.question result.chatId = chatId result.chatMessageId = chatMessage.id if (sessionId) result.sessionId = sessionId diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index a28f2c68..7456aa81 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -688,9 +688,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { style={{ display: 'flex', flexWrap: 'wrap', - flexDirection: 'row', + flexDirection: 'column', width: '100%', - gap: '4px' + gap: '8px' }} > {message.fileUploads.map((item, index) => { diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 73d1c4a6..75a466d3 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -391,6 +391,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews if (!chatId) setChatId(data.chatId) + if (input === '' && data.question) { + // the response contains the question even if it was in an audio format + // so if input is empty but the response contains the question, update the user message to show the question + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)] + if (allMessages[allMessages.length - 2].type === 'apiMessage') return allMessages + allMessages[allMessages.length - 2].message = data.question + return allMessages + }) + } + if (!isChatFlowAvailableToStream) { let text = '' if (data.text) text = data.text @@ -669,9 +680,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews style={{ display: 'flex', flexWrap: 'wrap', - flexDirection: 'row', + flexDirection: 'column', width: '100%', - gap: '4px' + gap: '8px' }} > {message.fileUploads.map((item, index) => { From 8bad360796e0a2d3c6c9c9902846b3b33bd123cb Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 19 Feb 2024 19:28:09 +0530 Subject: [PATCH 452/502] Remove status indicator in speech to text configuration --- .../ui-component/dialog/SpeechToTextDialog.js | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js index 60634664..30fc9f60 100644 --- a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js +++ b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.js @@ -247,32 +247,6 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { } /> - {speechToText[selectedProvider] && speechToText[selectedProvider].status && ( -
    -
    - ON -
    - )} {speechToTextProviders[selectedProvider].inputs.map((inputParam, index) => ( From a323c65ec4a35b60bb01b9716b773ff91f549f5b Mon Sep 17 00:00:00 2001 From: Marc Klingen Date: Mon, 19 Feb 2024 15:53:06 +0100 Subject: [PATCH 453/502] chore(deps): upgrade Langfuse to 3.1.0 --- packages/components/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index b53945bd..93504279 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -62,8 +62,8 @@ "ioredis": "^5.3.2", "jsonpointer": "^5.0.1", "langchain": "^0.1.20", - "langfuse": "2.0.2", - "langfuse-langchain": "^2.6.2-alpha.0", + "langfuse": "3.1.0", + "langfuse-langchain": "^3.1.0", "langsmith": "0.0.63", "linkifyjs": "^4.1.1", "llamaindex": "^0.0.48", From 6c1f0f7330ab57d6a563070b12df0436d7696dc7 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 20 Feb 2024 01:32:21 +0800 Subject: [PATCH 454/502] update wordings --- CONTRIBUTING-ZH.md | 4 ++-- CONTRIBUTING.md | 4 ++-- README-ZH.md | 2 +- README.md | 2 +- packages/server/src/index.ts | 8 ++++---- packages/server/src/utils/index.ts | 2 +- packages/ui/public/index.html | 10 ++++++++-- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING-ZH.md b/CONTRIBUTING-ZH.md index e000da4f..25f48387 100644 --- a/CONTRIBUTING-ZH.md +++ b/CONTRIBUTING-ZH.md @@ -30,7 +30,7 @@ 不确定要贡献什么?一些想法: -- 从 Langchain 创建新组件 +- 从 `packages/components` 创建新组件 - 更新现有组件,如扩展功能、修复错误 - 添加新的 Chatflow 想法 @@ -40,7 +40,7 @@ Flowise 在一个单一的单体存储库中有 3 个不同的模块。 - `server`:用于提供 API 逻辑的 Node 后端 - `ui`:React 前端 -- `components`:Langchain 组件 +- `components`:Langchain/LlamaIndex 组件 #### 先决条件 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdeb848b..57fee8fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ Found an issue? [Report it](https://github.com/FlowiseAI/Flowise/issues/new/choo Not sure what to contribute? Some ideas: -- Create new components from Langchain +- Create new components from `packages/components` - Update existing components such as extending functionality, fixing bugs - Add new chatflow ideas @@ -40,7 +40,7 @@ Flowise has 3 different modules in a single mono repository. - `server`: Node backend to serve API logics - `ui`: React frontend -- `components`: Langchain components +- `components`: Third-party nodes integrations #### Prerequisite diff --git a/README-ZH.md b/README-ZH.md index 8750ebc7..b6f389ed 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -71,7 +71,7 @@ Flowise 在一个单一的代码库中有 3 个不同的模块。 - `server`:用于提供 API 逻辑的 Node 后端 - `ui`:React 前端 -- `components`:Langchain 组件 +- `components`:第三方节点集成 ### 先决条件 diff --git a/README.md b/README.md index 3e6b7e56..438647e8 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Flowise has 3 different modules in a single mono repository. - `server`: Node backend to serve API logics - `ui`: React frontend -- `components`: Langchain components +- `components`: Third-party nodes integrations ### Prerequisite diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6ddaa484..20102a42 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -26,7 +26,7 @@ import { import { getNodeModulesPackagePath, getStartingNodes, - buildLangchain, + buildFlow, getEndingNodes, constructGraphs, resolveVariables, @@ -441,7 +441,7 @@ export class App { // chatFlowPool is initialized only when a flow is opened // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined if (this.chatflowPool) { - // Update chatflowpool inSync to false, to build Langchain again because data has been changed + // Update chatflowpool inSync to false, to build flow from scratch again because data has been changed this.chatflowPool.updateInSync(chatflow.id, false) } @@ -1570,7 +1570,7 @@ export class App { const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId) - await buildLangchain( + await buildFlow( startingNodeIds, nodes, edges, @@ -1787,7 +1787,7 @@ export class App { logger.debug(`[server]: Start building chatflow ${chatflowid}`) /*** BFS to traverse from Starting Nodes to Ending Node ***/ - const reactFlowNodes = await buildLangchain( + const reactFlowNodes = await buildFlow( startingNodeIds, nodes, edges, diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 421a15ec..4f222151 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -263,7 +263,7 @@ export const getEndingNodes = (nodeDependencies: INodeDependencies, graph: INode * @param {ICommonObject} overrideConfig * @param {CachePool} cachePool */ -export const buildLangchain = async ( +export const buildFlow = async ( startingNodeIds: string[], reactFlowNodes: IReactFlowNode[], reactFlowEdges: IReactFlowEdge[], diff --git a/packages/ui/public/index.html b/packages/ui/public/index.html index b4ec9ea1..61fd6fe5 100644 --- a/packages/ui/public/index.html +++ b/packages/ui/public/index.html @@ -18,13 +18,19 @@ - + - +