From 0f464fddb8c787334385320db768f991fbc6bb2d Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 07:22:08 +0530 Subject: [PATCH 01/39] 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 02/39] 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 03/39] 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 04/39] 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 05/39] 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 4a3e1784d8f8b4532587bfb5ddce61eb29c35977 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 27 Nov 2023 06:06:18 +0530 Subject: [PATCH 06/39] 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 07/39] 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 59308665c2b8c83b2b77ecd2a0753a86847ea037 Mon Sep 17 00:00:00 2001 From: takuabonn Date: Wed, 6 Dec 2023 07:32:45 +0900 Subject: [PATCH 18/39] 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 19/39] 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 20/39] 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 8122377bbb794564d89fa79096fcff6a209a2f9e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 12:53:43 +0530 Subject: [PATCH 21/39] 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 22/39] 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 23/39] 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 e67c43157a53cc208776431c1fad829f5170d9fd Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 7 Dec 2023 16:06:32 +0530 Subject: [PATCH 24/39] 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 7578183ac25e74518e07f06a61b3eec8609ac2ed Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 7 Dec 2023 18:46:03 +0000 Subject: [PATCH 25/39] 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 26/39] 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 da2fe78e4491a3d611266325ba9b2b2e06c1b732 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 8 Dec 2023 12:09:29 +0000 Subject: [PATCH 27/39] Update handler.ts --- packages/components/src/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index ae5a9de0..29aff3e2 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -9,7 +9,7 @@ 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 { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse' import monitor from 'llmonitor' import { v4 as uuidv4 } from 'uuid' From 99bc9d64fbd79aacfd2d488ace0044dcb61fb391 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 8 Dec 2023 18:50:58 +0530 Subject: [PATCH 28/39] XSS: replacing deprecated sanitize-js with sanitize-html --- packages/server/package.json | 2 +- packages/server/src/utils/XSS.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 97a95d43..013e6007 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -61,9 +61,9 @@ "mysql": "^2.18.1", "pg": "^8.11.1", "reflect-metadata": "^0.1.13", + "sanitize-html": "^2.11.0", "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/utils/XSS.ts b/packages/server/src/utils/XSS.ts index a69cde21..329c2ed2 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,10 +1,12 @@ import { Request, Response, NextFunction } from 'express' -let stripJs = require('strip-js') +const sanitizeHtml = require('sanitize-html') export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { - req.url = stripJs(req.url) + // 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] = stripJs(req.query[p]) + req.query[p] = sanitizeHtml(req.query[p]) } next() From b91cf551288016855407c5f22ac9cc1027e0f855 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 14:14:22 +0000 Subject: [PATCH 29/39] =?UTF-8?q?=F0=9F=A5=B3=20flowise-components@1.4.6?= =?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 a775e630..66e6d6d9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "1.4.5", + "version": "1.4.6", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", From 1e5a37ad0f673d1ee8e2ed2d13b3b5711b86578f Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 14:16:04 +0000 Subject: [PATCH 30/39] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.2=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 76369ab2..2b1f49e9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "1.4.1", + "version": "1.4.2", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { From a0c2b8b26a3721fff503aa17a210cb298ffee1fb Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 14:19:57 +0000 Subject: [PATCH 31/39] =?UTF-8?q?=F0=9F=A5=B3=20flowise@1.4.4=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 649a9a47..1993c7e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.3", + "version": "1.4.4", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/server/package.json b/packages/server/package.json index 6f4ccaf4..f2ec5c09 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "1.4.3", + "version": "1.4.4", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", From d2d21c45fe36128a0d299f2eb349c93520902c45 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 8 Dec 2023 18:51:40 +0000 Subject: [PATCH 32/39] fix upser vector API --- packages/server/src/ChatflowPool.ts | 2 +- packages/server/src/Interface.ts | 2 +- packages/server/src/index.ts | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/server/src/ChatflowPool.ts b/packages/server/src/ChatflowPool.ts index d296dcfe..325fac56 100644 --- a/packages/server/src/ChatflowPool.ts +++ b/packages/server/src/ChatflowPool.ts @@ -16,7 +16,7 @@ export class ChatflowPool { * @param {IReactFlowNode[]} startingNodes * @param {ICommonObject} overrideConfig */ - add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { + add(chatflowid: string, endingNodeData: INodeData | undefined, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { this.activeChatflows[chatflowid] = { startingNodes, endingNodeData, diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index d5890ab6..f82c6690 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -172,7 +172,7 @@ export interface IncomingInput { export interface IActiveChatflows { [key: string]: { startingNodes: IReactFlowNode[] - endingNodeData: INodeData + endingNodeData?: INodeData inSync: boolean overrideConfig?: ICommonObject } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0262bff4..95b8aa01 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1394,16 +1394,19 @@ export class App { const nodes = parsedFlowData.nodes const edges = parsedFlowData.edges - /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation) when all these conditions met: + /* 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) * - 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 = () => { return ( Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) && this.chatflowPool.activeChatflows[chatflowid].inSync && + this.chatflowPool.activeChatflows[chatflowid].endingNodeData && isSameOverrideConfig( isInternal, this.chatflowPool.activeChatflows[chatflowid].overrideConfig, @@ -1415,7 +1418,7 @@ export class App { } if (isFlowReusable()) { - nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData + nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData) logger.debug( `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})` @@ -1466,6 +1469,7 @@ export class App { const constructedObj = constructGraphs(nodes, edges, true) const nonDirectedGraph = constructedObj.graph const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId) + const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) logger.debug(`[server]: Start building chatflow ${chatflowid}`) /*** BFS to traverse from Starting Nodes to Ending Node ***/ @@ -1485,13 +1489,18 @@ export class App { isUpsert, incomingInput.stopNodeId ) - if (isUpsert) return res.status(201).send('Successfully Upserted') + if (isUpsert) { + this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) + return res.status(201).send('Successfully Upserted') + } const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId) if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`) - if (incomingInput.overrideConfig) + if (incomingInput.overrideConfig) { nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig) + } + const reactFlowNodeData: INodeData = resolveVariables( nodeToExecute.data, reactFlowNodes, @@ -1500,7 +1509,6 @@ export class App { ) nodeToExecuteData = reactFlowNodeData - const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) } From 9a5d5720f9fa67f6f90d0cb5b482967a61d1d16f Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 13:49:53 +0000 Subject: [PATCH 33/39] get rid of credential for langchain hub --- packages/server/src/index.ts | 27 +- .../dialog/PromptLangsmithHubDialog.js | 301 ++++++++---------- .../ui/src/views/canvas/NodeInputHandler.js | 2 +- 3 files changed, 140 insertions(+), 190 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 95b8aa01..805a0fec 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1044,18 +1044,9 @@ export class App { // ---------------------------------------- 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 }) + let hub = new Client() 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 }) @@ -1064,22 +1055,10 @@ export class App { this.app.post('/api/v1/prompts-list', 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) - - const headers = {} - // @ts-ignore - headers['x-api-key'] = decryptedCredentialData.langsmithApiKey - const tags = req.body.tags ? `tags=${req.body.tags}` : '' // 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) => { + const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}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 }) } diff --git a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js index e6f06e20..35b4ead7 100644 --- a/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js +++ b/packages/ui/src/ui-component/dialog/PromptLangsmithHubDialog.js @@ -42,12 +42,12 @@ import ClearIcon from '@mui/icons-material/Clear' import { styled } from '@mui/material/styles' //Project Import -import CredentialInputHandler from 'views/canvas/CredentialInputHandler' import { StyledButton } from 'ui-component/button/StyledButton' import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' import { CodeBlock } from 'ui-component/markdown/CodeBlock' import promptEmptySVG from 'assets/images/prompt_empty.svg' +import useApi from 'hooks/useApi' import promptApi from 'api/prompt' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' @@ -89,6 +89,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const portalElement = document.getElementById('portal') const dispatch = useDispatch() const customization = useSelector((state) => state.customization) + const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -98,6 +99,22 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [show, dispatch]) + useEffect(() => { + if (promptType) { + getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [promptType]) + + useEffect(() => { + if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) { + setAvailablePrompNameList(getAvailablePromptsApi.data.repos) + if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAvailablePromptsApi.data]) + const ITEM_HEIGHT = 48 const ITEM_PADDING_TOP = 8 const MenuProps = { @@ -156,7 +173,6 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { const [availablePrompNameList, setAvailablePrompNameList] = useState([]) const [selectedPrompt, setSelectedPrompt] = useState({}) - const [credentialId, setCredentialId] = useState('') const [accordionExpanded, setAccordionExpanded] = useState(['prompt']) const handleAccordionChange = (accordionName) => (event, isExpanded) => { @@ -173,7 +189,6 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { if (!prompt.detailed) { const createResp = await promptApi.getPrompt({ - credential: credentialId, promptName: prompt.full_name }) if (createResp.data) { @@ -194,14 +209,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { language.forEach((item) => { tags += `tags=${item.name}&` }) - const createResp = await promptApi.getAvailablePrompts({ - credential: credentialId, - tags: tags - }) - if (createResp.data) { - setAvailablePrompNameList(createResp.data.repos) - if (createResp.data.repos?.length) await handleListItemClick(0, createResp.data.repos) - } + getAvailablePromptsApi.request({ tags: tags }) } const removeDuplicates = (value) => { @@ -238,176 +246,139 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => { setLanguage(removeDuplicates(value)) } - const clear = () => { - setModelName([]) - setUsecase([]) - setLanguage([]) - setSelectedPrompt({}) - setAvailablePrompNameList([]) - setAccordionExpanded(['prompt']) - } - const component = show ? ( - Load Prompts from Langsmith Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) + Langchain Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'}) - - - Langsmith Credential   - * - - - + + + Model + + + + + + Usecase + + + + + + Language + + + + + - {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 34/39] 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 35/39] =?UTF-8?q?=F0=9F=A5=B3=20flowise-ui@1.4.3=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 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 36/39] =?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 f51c1d5b7a53cd877b000786d221298ed1766187 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 11 Dec 2023 20:35:30 +0000 Subject: [PATCH 37/39] 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 8e1ef2d5337f8140e8789830b7c5844ee854cc24 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Dec 2023 12:44:42 +0000 Subject: [PATCH 38/39] 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 2110e1146b5d7ca024411f5ec26be3dcee540972 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Dec 2023 23:14:52 +0000 Subject: [PATCH 39/39] 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