From 1f0cc60fccb993e2d632e17faa42d579eaba6881 Mon Sep 17 00:00:00 2001 From: automaton82 Date: Fri, 10 Nov 2023 15:31:13 -0500 Subject: [PATCH 01/34] Adding ability to hide node param from UI --- packages/components/src/Interface.ts | 1 + packages/ui/src/views/canvas/CanvasNode.js | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 15b98770..c56d8a5f 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -72,6 +72,7 @@ export interface INodeParams { fileType?: string additionalParams?: boolean loadMethod?: string + hidden?: boolean } export interface INodeExecutionData { diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index cabe2329..cfc51fe4 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -207,9 +207,11 @@ const CanvasNode = ({ data }) => { {data.inputAnchors.map((inputAnchor, index) => ( ))} - {data.inputParams.map((inputParam, index) => ( - - ))} + {data.inputParams + .filter((inputParam) => !inputParam.hidden) + .map((inputParam, index) => ( + + ))} {data.inputParams.find((param) => param.additionalParams) && (
Date: Mon, 13 Nov 2023 21:46:36 +0530 Subject: [PATCH 02/34] MongoDB Atlas Integration: Adding MongoDB Memory --- .../credentials/MongoDBUrlApi.credential.ts | 25 ++++ .../memory/MongoDBMemory/MongoDBMemory.ts | 115 ++++++++++++++++++ .../nodes/memory/MongoDBMemory/mongodb.png | Bin 0 -> 3741 bytes packages/components/package.json | 1 + 4 files changed, 141 insertions(+) create mode 100644 packages/components/credentials/MongoDBUrlApi.credential.ts create mode 100644 packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts create mode 100644 packages/components/nodes/memory/MongoDBMemory/mongodb.png diff --git a/packages/components/credentials/MongoDBUrlApi.credential.ts b/packages/components/credentials/MongoDBUrlApi.credential.ts new file mode 100644 index 00000000..2f2cba38 --- /dev/null +++ b/packages/components/credentials/MongoDBUrlApi.credential.ts @@ -0,0 +1,25 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MongoDBUrlApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'MongoDB ATLAS' + this.name = 'mongoDBUrlApi' + this.version = 1.0 + this.inputs = [ + { + label: 'ATLAS Connection URL', + name: 'mongoDBConnectUrl', + type: 'string', + placeholder: 'mongodb+srv://myDatabaseUser:D1fficultP%40ssw0rd@cluster0.example.mongodb.net/?retryWrites=true&w=majority' + } + ] + } +} + +module.exports = { credClass: MongoDBUrlApi } diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts new file mode 100644 index 00000000..4c9e8581 --- /dev/null +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -0,0 +1,115 @@ +import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' +import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' +import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { MongoClient } from 'mongodb' + +class MongoDB_Memory implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'MongoDB Atlas Chat Memory' + this.name = 'MongoDBAtlasChatMemory' + this.version = 1.0 + this.type = 'MongoDBAtlasChatMemory' + this.icon = 'mongodb.png' + this.category = 'Memory' + this.description = 'Stores the conversation in MongoDB Atlas' + this.baseClasses = [this.type, ...getBaseClasses(BufferMemory)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Session ID', + name: 'sessionId', + type: 'string', + default: '5f9cf7c08d5b1a06b80fae61', + description: 'Must be an Hex String of 24 chars. This will be the objectId of the document in MongoDB Atlas' + }, + { + label: 'Memory Key', + name: 'memoryKey', + type: 'string', + default: 'chat_history', + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return initializeMongoDB(nodeData, options) + } + + async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { + const mongodbMemory = await initializeMongoDB(nodeData, options) + const sessionId = nodeData.inputs?.sessionId as string + options.logger.info(`Clearing MongoDB memory session ${sessionId}`) + await mongodbMemory.clear() + options.logger.info(`Successfully cleared MongoDB memory session ${sessionId}`) + } +} + +const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise => { + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const sessionId = nodeData.inputs?.sessionId as string + const memoryKey = nodeData.inputs?.memoryKey as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + const client = new MongoClient(mongoDBConnectUrl) + await client.connect() + const collection = client.db(databaseName).collection(collectionName) + + const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ + collection, + sessionId: sessionId + }) + + return new BufferMemoryExtended({ + memoryKey, + chatHistory: mongoDBChatMessageHistory, + returnMessages: true, + isSessionIdUsingChatMessageId: false + }) +} + +interface BufferMemoryExtendedInput { + isSessionIdUsingChatMessageId: boolean +} + +class BufferMemoryExtended extends BufferMemory { + isSessionIdUsingChatMessageId? = false + + constructor(fields: BufferMemoryInput & Partial) { + super(fields) + this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId + } +} + +module.exports = { nodeClass: MongoDB_Memory } diff --git a/packages/components/nodes/memory/MongoDBMemory/mongodb.png b/packages/components/nodes/memory/MongoDBMemory/mongodb.png new file mode 100644 index 0000000000000000000000000000000000000000..5586fe0ac672f7997014d814389c1d6c436d9d0c GIT binary patch literal 3741 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 literal 0 HcmV?d00001 diff --git a/packages/components/package.json b/packages/components/package.json index 996419ca..ee2adbf9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -55,6 +55,7 @@ "llmonitor": "^0.5.5", "mammoth": "^1.5.1", "moment": "^2.29.3", + "mongodb": "^6.2.0", "mysql2": "^3.5.1", "node-fetch": "^2.6.11", "node-html-markdown": "^1.3.0", From 261e45d74a2f912dd0ab6e356fef90c97b420f3f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 13 Nov 2023 21:56:18 +0530 Subject: [PATCH 03/34] MongoDB Atlas Integration: Adding MongoDB as a Vector Store --- .../vectorstores/MongoDB/MongoDBSearchBase.ts | 145 ++++++++++++++++++ .../vectorstores/MongoDB/MongoDB_Existing.ts | 41 +++++ .../vectorstores/MongoDB/MongoDB_Upsert.ts | 58 +++++++ .../nodes/vectorstores/MongoDB/mongodb.png | Bin 0 -> 3741 bytes 4 files changed, 244 insertions(+) create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts create mode 100644 packages/components/nodes/vectorstores/MongoDB/mongodb.png diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts new file mode 100644 index 00000000..e9ef8e9a --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDBSearchBase.ts @@ -0,0 +1,145 @@ +import { + getBaseClasses, + getCredentialData, + getCredentialParam, + ICommonObject, + INodeData, + INodeOutputsValue, + INodeParams +} from '../../../src' + +import { Embeddings } from 'langchain/embeddings/base' +import { VectorStore } from 'langchain/vectorstores/base' +import { Document } from 'langchain/document' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { Collection, MongoClient } from 'mongodb' + +export abstract class MongoDBSearchBase { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + mongoClient: MongoClient + + protected constructor() { + this.type = 'MongoDB Atlas' + this.icon = 'mongodb.png' + this.category = 'Vector Stores' + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['mongoDBUrlApi'] + } + this.inputs = [ + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Database', + name: 'databaseName', + placeholder: '', + type: 'string' + }, + { + label: 'Collection Name', + name: 'collectionName', + placeholder: '', + type: 'string' + }, + { + label: 'Index Name', + name: 'indexName', + placeholder: '', + type: 'string' + }, + { + label: 'Content Field', + name: 'textKey', + description: 'Name of the field (column) that contains the actual content', + type: 'string', + default: 'text', + additionalParams: true, + optional: true + }, + { + label: 'Embedded Field', + name: 'embeddingKey', + description: 'Name of the field (column) that contains the Embedding', + type: 'string', + default: 'embedding', + additionalParams: true, + optional: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'MongoDB Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'MongoDB Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(MongoDBAtlasVectorSearch)] + } + ] + } + + abstract constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + docs: Document>[] | undefined + ): Promise + + async init(nodeData: INodeData, _: string, options: ICommonObject, docs: Document>[] | undefined): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const databaseName = nodeData.inputs?.databaseName as string + const collectionName = nodeData.inputs?.collectionName as string + const indexName = nodeData.inputs?.indexName as string + let textKey = nodeData.inputs?.textKey as string + let embeddingKey = nodeData.inputs?.embeddingKey as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : 4 + const output = nodeData.outputs?.output as string + + let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) + + this.mongoClient = new MongoClient(mongoDBConnectUrl) + const collection = this.mongoClient.db(databaseName).collection(collectionName) + if (!textKey || textKey === '') textKey = 'text' + if (!embeddingKey || embeddingKey === '') embeddingKey = 'embedding' + const vectorStore = await this.constructVectorStore(embeddings, collection, indexName, textKey, embeddingKey, docs) + + if (output === 'retriever') { + return vectorStore.asRetriever(k) + } else if (output === 'vectorStore') { + ;(vectorStore as any).k = k + return vectorStore + } + return vectorStore + } +} diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts new file mode 100644 index 00000000..3cbb36b8 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts @@ -0,0 +1,41 @@ +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Embeddings } from 'langchain/embeddings/base' +import { VectorStore } from 'langchain/vectorstores/base' +import { Document } from 'langchain/document' + +import { MongoDBSearchBase } from './MongoDBSearchBase' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' + +class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { + constructor() { + super() + this.label = 'MongoDB Atlas Load Existing Index' + this.name = 'MongoDBIndex' + this.version = 1.0 + this.description = 'Load existing data from MongoDB Atlas (i.e: Document has been upserted)' + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return super.init(nodeData, _, options, undefined) + } + + constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + _: Document>[] | undefined + ): Promise { + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + collection: collection, + indexName: indexName, + textKey: textKey, + embeddingKey: embeddingKey + }) + return Promise.resolve(mongoDBAtlasVectorSearch) + } +} + +module.exports = { nodeClass: MongoDBExisting_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts new file mode 100644 index 00000000..80dfbf19 --- /dev/null +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts @@ -0,0 +1,58 @@ +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' + +import { flatten } from 'lodash' +import { VectorStore } from 'langchain/vectorstores/base' +import { MongoDBSearchBase } from './MongoDBSearchBase' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' + +class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { + constructor() { + super() + this.label = 'MongoDB Upsert Document' + this.name = 'MongoDBUpsert' + this.version = 1.0 + this.description = 'Upsert documents to MongoDB Atlas' + this.inputs.unshift({ + label: 'Document', + name: 'document', + type: 'Document', + list: true + }) + } + + constructVectorStore( + embeddings: Embeddings, + collection: Collection, + indexName: string, + textKey: string, + embeddingKey: string, + docs: Document>[] + ): Promise { + return MongoDBAtlasVectorSearch.fromDocuments(docs, embeddings, { + collection: collection, + indexName: indexName, + textKey: textKey, + embeddingKey: embeddingKey + }) + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const docs = nodeData.inputs?.document as Document[] + + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + const document = new Document(flattenDocs[i]) + finalDocs.push(document) + } + } + + return super.init(nodeData, _, options, flattenDocs) + } +} + +module.exports = { nodeClass: MongoDBUpsert_VectorStores } diff --git a/packages/components/nodes/vectorstores/MongoDB/mongodb.png b/packages/components/nodes/vectorstores/MongoDB/mongodb.png new file mode 100644 index 0000000000000000000000000000000000000000..5586fe0ac672f7997014d814389c1d6c436d9d0c GIT binary patch literal 3741 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 literal 0 HcmV?d00001 From f21f5257cac9a50661e2408017729641f00a69f5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 14:35:47 +0530 Subject: [PATCH 04/34] UX Changes: Ability to view as table and search on the dashboard --- .../src/ui-component/button/StyledButton.js | 8 ++ .../src/ui-component/table/FlowListTable.js | 133 ++++++++++++++++++ .../ui/src/ui-component/toolbar/Toolbar.js | 24 ++++ packages/ui/src/views/chatflows/index.js | 96 ++++++++++--- 4 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 packages/ui/src/ui-component/table/FlowListTable.js create mode 100644 packages/ui/src/ui-component/toolbar/Toolbar.js diff --git a/packages/ui/src/ui-component/button/StyledButton.js b/packages/ui/src/ui-component/button/StyledButton.js index 6e0c7078..29e17f80 100644 --- a/packages/ui/src/ui-component/button/StyledButton.js +++ b/packages/ui/src/ui-component/button/StyledButton.js @@ -1,5 +1,6 @@ import { styled } from '@mui/material/styles' import { Button } from '@mui/material' +import MuiToggleButton from '@mui/material/ToggleButton' export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({ color: 'white', @@ -9,3 +10,10 @@ export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({ backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)` } })) + +export const StyledToggleButton = styled(MuiToggleButton)(({ theme, color = 'primary' }) => ({ + '&.Mui-selected, &.Mui-selected:hover': { + color: 'white', + backgroundColor: theme.palette[color].main + } +})) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js new file mode 100644 index 00000000..819a49cb --- /dev/null +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -0,0 +1,133 @@ +import PropTypes from 'prop-types' +import { useNavigate } from 'react-router-dom' +import { IconEdit } from '@tabler/icons' +import moment from 'moment' +import { styled } from '@mui/material/styles' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Paper from '@mui/material/Paper' +import { Button, Typography } from '@mui/material' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const FlowListTable = ({ data, images, filterFunction }) => { + const navigate = useNavigate() + const goToCanvas = (selectedChatflow) => { + navigate(`/canvas/${selectedChatflow.id}`) + } + return ( + <> + + + + + + Name + + + Nodes + + + Last Modified Date + + + Actions + + + + + {data.filter(filterFunction).map((row, index) => ( + + + + {row.templateName || row.name} + + + + + {images[row.id] && ( +
+ {images[row.id].map((img) => ( +
+ +
+ ))} +
+ )} +
+ {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + + +
+ ))} +
+
+
+ + ) +} + +FlowListTable.propTypes = { + data: PropTypes.object, + images: PropTypes.array, + filterFunction: PropTypes.func +} diff --git a/packages/ui/src/ui-component/toolbar/Toolbar.js b/packages/ui/src/ui-component/toolbar/Toolbar.js new file mode 100644 index 00000000..f72ba339 --- /dev/null +++ b/packages/ui/src/ui-component/toolbar/Toolbar.js @@ -0,0 +1,24 @@ +import * as React from 'react' +import ViewListIcon from '@mui/icons-material/ViewList' +import ViewModuleIcon from '@mui/icons-material/ViewModule' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { StyledToggleButton } from '../button/StyledButton' + +export default function Toolbar() { + const [view, setView] = React.useState('list') + + const handleChange = (event, nextView) => { + setView(nextView) + } + + return ( + + + + + + + + + ) +} diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 6712623e..e01a9373 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' // material-ui -import { Grid, Box, Stack } from '@mui/material' +import { Grid, Box, Stack, Toolbar, ToggleButton, ButtonGroup, Typography, InputAdornment, TextField } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -11,7 +11,6 @@ import MainCard from 'ui-component/cards/MainCard' import ItemCard from 'ui-component/cards/ItemCard' import { gridSpacing } from 'store/constant' import WorkflowEmptySVG from 'assets/images/workflow_empty.svg' -import { StyledButton } from 'ui-component/button/StyledButton' import LoginDialog from 'ui-component/dialog/LoginDialog' // API @@ -24,7 +23,13 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus } from '@tabler/icons' +import { IconPlus, IconSearch } from '@tabler/icons' +import * as React from 'react' +import ViewListIcon from '@mui/icons-material/ViewList' +import ViewModuleIcon from '@mui/icons-material/ViewModule' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { FlowListTable } from '../../ui-component/table/FlowListTable' +import { StyledButton } from '../../ui-component/button/StyledButton' // ==============================|| CHATFLOWS ||============================== // @@ -35,10 +40,24 @@ const Chatflows = () => { const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) + const [search, setSearch] = useState('') const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) + const [view, setView] = React.useState('card') + + const handleChange = (event, nextView) => { + setView(nextView) + } + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterFlows(data) { + return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 + } const onLoginClick = (username, password) => { localStorage.setItem('username', username) @@ -102,26 +121,61 @@ const Chatflows = () => { return ( - -

Chatflows

- - - - }> - Add New - + + + + + Chatflows + + + + + + ) + }} + /> + + + + + + + + + + + + + + + }> + Add New + + + + + + {!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && ( + + {getAllChatflowsApi.data.filter(filterFlows).map((data, index) => ( + + goToCanvas(data)} data={data} images={images[data.id]} /> + + ))} - + )} + {!isLoading && view === 'list' && getAllChatflowsApi.data && ( + + )}
- - {!isLoading && - getAllChatflowsApi.data && - getAllChatflowsApi.data.map((data, index) => ( - - goToCanvas(data)} data={data} images={images[data.id]} /> - - ))} - + {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( From 77994ce2178e2c7ba456a1a950bf6c89cde83040 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 15:15:34 +0530 Subject: [PATCH 05/34] UX Changes: adding a placeholder for chatflow search. --- packages/ui/src/views/chatflows/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index e01a9373..f7d1497d 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -132,6 +132,7 @@ const Chatflows = () => { size='small' sx={{ width: 400 }} variant='outlined' + placeholder='Search Chatflows' onChange={onSearchChange} InputProps={{ startAdornment: ( From 7ef817bc996200d89c4d5f11d4a556f2ac5a16df Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 15:48:14 +0530 Subject: [PATCH 06/34] UX Changes: limiting display of node icons to 5 and with label to indicate additional. --- packages/ui/src/ui-component/table/FlowListTable.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 819a49cb..896ce3ea 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -37,6 +37,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) } + let nodeCount = 0 return ( <> @@ -53,7 +54,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { Name - Nodes + Nodes (Showing first 5) Last Modified Date @@ -84,7 +85,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { marginTop: 5 }} > - {images[row.id].map((img) => ( + {images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => (
{ />
))} + {images[row.id].length > 5 && ( + + + {images[row.id].length - 5} More + + )}
)} From 57b31130397ca0097ef04db78137cccc79d7b63e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 20:13:24 +0530 Subject: [PATCH 07/34] UX Changes: minor UI tweaks/adjustments and fixes for small screens --- .../src/ui-component/table/FlowListTable.js | 20 ++++++------- packages/ui/src/views/chatflows/index.js | 28 +++++++++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 896ce3ea..9dfc9522 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -37,26 +37,20 @@ export const FlowListTable = ({ data, images, filterFunction }) => { const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) } - let nodeCount = 0 + return ( <> - + Name - + Nodes (Showing first 5) - + Last Modified Date @@ -75,7 +69,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { - + {images[row.id] && (
{
)}
- {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + ) + } + }) + } + } + + const handleDelete = async () => { + setAnchorEl(null) + const confirmPayload = { + title: `Delete`, + description: `Delete chatflow ${chatflow.name}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + await chatflowsApi.deleteChatflow(chatflow.id) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + } + + const handleDuplicate = () => { + setAnchorEl(null) + try { + localStorage.setItem('duplicatedFlowData', chatflow.flowData) + window.open(`${uiBaseURL}/canvas`, '_blank') + } catch (e) { + console.error(e) + } + } + const handleExport = () => { + setAnchorEl(null) + try { + const flowData = JSON.parse(chatflow.flowData) + let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2) + let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) + + let exportFileDefaultName = `${chatflow.name} Chatflow.json` + + let linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } catch (e) { + console.error(e) + } + } + + return ( +
+ + + + + Rename + + + + Duplicate + + + + Export + + + + + Delete + + + + setFlowDialogOpen(false)} + onConfirm={saveFlowRename} + /> +
+ ) +} + +FlowListMenu.propTypes = { + chatflow: PropTypes.object, + updateFlowsApi: PropTypes.object +} diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 9dfc9522..8dc25e96 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types' import { useNavigate } from 'react-router-dom' -import { IconEdit } from '@tabler/icons' import moment from 'moment' import { styled } from '@mui/material/styles' import Table from '@mui/material/Table' @@ -10,7 +9,8 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' -import { Button, Typography } from '@mui/material' +import { Button, Stack, Typography } from '@mui/material' +import FlowListMenu from '../button/FlowListMenu' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -32,7 +32,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })) -export const FlowListTable = ({ data, images, filterFunction }) => { +export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => { const navigate = useNavigate() const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) @@ -44,7 +44,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => {
- + Name @@ -53,7 +53,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { Last Modified Date - + Actions @@ -65,7 +65,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { - {row.templateName || row.name} + @@ -111,15 +111,13 @@ export const FlowListTable = ({ data, images, filterFunction }) => { {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} - - + + + {/**/} + + ))} @@ -133,5 +131,6 @@ export const FlowListTable = ({ data, images, filterFunction }) => { FlowListTable.propTypes = { data: PropTypes.object, images: PropTypes.array, - filterFunction: PropTypes.func + filterFunction: PropTypes.func, + updateFlowsApi: PropTypes.object } diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index b7784a89..f008bfa8 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -23,10 +23,8 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus, IconSearch } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutCards, IconLayoutColumns } from '@tabler/icons' import * as React from 'react' -import ViewListIcon from '@mui/icons-material/ViewList' -import ViewModuleIcon from '@mui/icons-material/ViewModule' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import { FlowListTable } from '../../ui-component/table/FlowListTable' import { StyledButton } from '../../ui-component/button/StyledButton' @@ -159,10 +157,10 @@ const Chatflows = () => { > - + - + @@ -185,7 +183,13 @@ const Chatflows = () => { )} {!isLoading && view === 'list' && getAllChatflowsApi.data && ( - + )} From a7b34848cd3fe814b4e3820c2409c77f0d67c5ca Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 16 Nov 2023 08:29:06 +0530 Subject: [PATCH 09/34] UX Changes: Ability to set category (tags) to each chatflow; corresponding changes to table display and table search --- .../server/src/database/entities/ChatFlow.ts | 3 + .../1699900910291-AddCategoryToChatFlow.ts | 12 +++ .../src/database/migrations/mysql/index.ts | 4 +- .../1699900910291-AddCategoryToChatFlow.ts | 11 +++ .../src/database/migrations/postgres/index.ts | 4 +- .../1699900910291-AddCategoryToChatFlow.ts | 11 +++ .../src/database/migrations/sqlite/index.ts | 4 +- .../src/ui-component/button/FlowListMenu.js | 55 ++++++++++- .../ui/src/ui-component/dialog/TagDialog.js | 91 +++++++++++++++++++ .../src/ui-component/table/FlowListTable.js | 35 +++++-- packages/ui/src/views/chatflows/index.js | 7 +- 11 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/ui/src/ui-component/dialog/TagDialog.js diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 376a100b..b3131c2e 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -36,4 +36,7 @@ export class ChatFlow implements IChatFlow { @UpdateDateColumn() updatedDate: Date + + @Column({ nullable: true, type: 'text' }) + category?: string } diff --git a/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 00000000..424f3b0e --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'category') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`category\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`category\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a95..53d652ee 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 { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 00000000..f5d96439 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b..70642eb6 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 { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 00000000..270b2998 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc40..fe7611ea 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 { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index fb759505..f4ffbd32 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -7,6 +7,7 @@ import Divider from '@mui/material/Divider' 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 Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import PropTypes from 'prop-types' @@ -22,6 +23,7 @@ import ConfirmDialog from '../dialog/ConfirmDialog' import SaveChatflowDialog from '../dialog/SaveChatflowDialog' import { useState } from 'react' import useApi from '../../hooks/useApi' +import TagDialog from '../dialog/TagDialog' const StyledMenu = styled((props) => ( dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) const handleClick = (event) => { @@ -79,12 +83,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { const handleClose = () => { setAnchorEl(null) } - const handleFlowRename = () => { setAnchorEl(null) setFlowDialogOpen(true) } - const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -110,7 +112,39 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } - + const handleFlowCategory = () => { + setAnchorEl(null) + if (chatflow.category) setCategoryValues(chatflow.category.split(';')) + else setCategoryValues([]) + setCategoryDialogOpen(true) + } + const saveFlowCategory = async (categories) => { + // save categories as string + const categoryTags = categories.join(';') + const updateBody = { + category: categoryTags, + chatflow + } + try { + await updateChatflowApi.request(chatflow.id, updateBody) + await updateFlowsApi.request() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: errorData, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } const handleDelete = async () => { setAnchorEl(null) const confirmPayload = { @@ -143,7 +177,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { } } } - const handleDuplicate = () => { setAnchorEl(null) try { @@ -206,6 +239,11 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Export + + + Update Category + + Delete @@ -222,6 +260,13 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { onCancel={() => setFlowDialogOpen(false)} onConfirm={saveFlowRename} /> + setCategoryDialogOpen(false)} + tags={categoryValues} + setTags={setCategoryValues} + onSubmit={saveFlowCategory} + /> ) } diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js new file mode 100644 index 00000000..be133fa4 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -0,0 +1,91 @@ +import { useState } from 'react' +import Dialog from '@mui/material/Dialog' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +import Chip from '@mui/material/Chip' +import PropTypes from 'prop-types' +import { DialogActions, DialogContent, DialogTitle } from '@mui/material' + +const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { + const [inputValue, setInputValue] = useState('') + + const handleInputChange = (event) => { + setInputValue(event.target.value) + } + + const handleInputKeyDown = (event) => { + if (event.key === 'Enter' && inputValue.trim()) { + event.preventDefault() + if (!tags.includes(inputValue)) { + setTags([...tags, inputValue]) + setInputValue('') + } + } + } + + const handleDeleteTag = (tagToDelete) => { + setTags(tags.filter((tag) => tag !== tagToDelete)) + } + + const handleSubmit = (event) => { + event.preventDefault() + onSubmit(tags) + onClose() + } + + return ( + + + Set Chatflow Category Tags + + + +
+
+ {tags.map((tag, index) => ( + handleDeleteTag(tag)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ + +
+
+ + + + +
+ ) +} + +TagDialog.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, + tags: PropTypes.array, + setTags: PropTypes.func, + onSubmit: PropTypes.func +} + +export default TagDialog diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 8dc25e96..08caed57 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -11,6 +11,7 @@ import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' import { Button, Stack, Typography } from '@mui/material' import FlowListMenu from '../button/FlowListMenu' +import Chip from '@mui/material/Chip' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -47,13 +48,16 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Name - - Nodes (Showing first 5) + + Category - Last Modified Date + Nodes (Showing first 5) + Last Modified Date + + Actions @@ -68,8 +72,25 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +
+   + {row.category && + row.category + .split(';') + .map((tag, index) => ( + + ))} +
+
+ {images[row.id] && (
)} - - {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} - + {moment(row.updatedDate).format('MMMM Do, YYYY')} + + {/**/} diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 44c670d6..34c6523b 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -23,7 +23,7 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus, IconSearch, IconLayoutCards, IconLayoutColumns } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutGrid, IconList } from '@tabler/icons' import * as React from 'react' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import { FlowListTable } from '../../ui-component/table/FlowListTable' @@ -138,7 +138,7 @@ const Chatflows = () => {

Chatflows

{ }} /> - + - - - + + + - - + + From 97247713ef7c2f0134c3114c1de84fea040a47f5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 11:24:55 +0530 Subject: [PATCH 11/34] UX Changes: Fixed 2 edge cases, (1) UI-tag not added, when the user clicks the submit without hitting enter and (2) server side error when the update/rename is attempted before any flow is opened --- packages/server/src/index.ts | 8 ++++++-- packages/ui/src/ui-component/dialog/TagDialog.js | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0..5a5b2eda 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -355,8 +355,12 @@ export class App { this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - // Update chatflowpool inSync to false, to build Langchain again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) + // 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) }) diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js index be133fa4..778bf2cc 100644 --- a/packages/ui/src/ui-component/dialog/TagDialog.js +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -5,7 +5,7 @@ import Button from '@mui/material/Button' import TextField from '@mui/material/TextField' import Chip from '@mui/material/Chip' import PropTypes from 'prop-types' -import { DialogActions, DialogContent, DialogTitle } from '@mui/material' +import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const [inputValue, setInputValue] = useState('') @@ -30,6 +30,9 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const handleSubmit = (event) => { event.preventDefault() + if (inputValue.trim() && !tags.includes(inputValue)) { + setTags([...tags, inputValue]) + } onSubmit(tags) onClose() } @@ -67,6 +70,9 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { label='Add a tag' variant='outlined' /> + + Enter a tag and press enter to add it to the list. You can add as many tags as you want. + From a0397c008e035ec64e8063b4b2c7859a5d2eadc8 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 11:25:59 +0530 Subject: [PATCH 12/34] UX Changes: Column display fixes for 'xs' mode --- packages/ui/src/ui-component/table/FlowListTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 68641d44..e33a8ba1 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -48,7 +48,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Name - + Category @@ -72,7 +72,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +
Date: Fri, 17 Nov 2023 12:29:14 +0530 Subject: [PATCH 13/34] UX Changes: Addition of search filters for API Keys and Credentials. --- packages/ui/src/views/apikey/index.js | 71 ++++++++++++++--- packages/ui/src/views/credentials/index.js | 88 ++++++++++++++++++---- 2 files changed, 136 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index a2b2e639..e08baac2 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -16,7 +16,11 @@ import { Paper, IconButton, Popover, - Typography + Typography, + Toolbar, + TextField, + InputAdornment, + ButtonGroup } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -37,7 +41,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff, IconSearch } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' // ==============================|| APIKey ||============================== // @@ -59,6 +63,14 @@ const APIKey = () => { const [showApiKeys, setShowApiKeys] = useState([]) const openPopOver = Boolean(anchorEl) + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterKeys(data) { + return data.keyName.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + const { confirm } = useConfirm() const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys) @@ -171,12 +183,53 @@ const APIKey = () => { <> -

API Keys 

- - - }> - Create Key - + + +

API Keys 

+ + + + ) + }} + /> + + + + } + > + Create Key + + + +
+
{apiKeys.length <= 0 && ( @@ -199,7 +252,7 @@ const APIKey = () => { - {apiKeys.map((key, index) => ( + {apiKeys.filter(filterKeys).map((key, index) => ( {key.keyName} diff --git a/packages/ui/src/views/credentials/index.js b/packages/ui/src/views/credentials/index.js index 9db990a7..31e35831 100644 --- a/packages/ui/src/views/credentials/index.js +++ b/packages/ui/src/views/credentials/index.js @@ -4,7 +4,23 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import moment from 'moment' // material-ui -import { Button, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material' +import { + Button, + Box, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Toolbar, + TextField, + InputAdornment, + ButtonGroup +} from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -25,7 +41,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons' +import { IconTrash, IconEdit, IconX, IconPlus, IconSearch } from '@tabler/icons' import CredentialEmptySVG from 'assets/images/credential_empty.svg' // const @@ -56,6 +72,14 @@ const Credentials = () => { const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials) const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials) + const [search, setSearch] = useState('') + const onSearchChange = (event) => { + setSearch(event.target.value) + } + function filterCredentials(data) { + return data.credentialName.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + const listCredential = () => { const dialogProp = { title: 'Add New Credential', @@ -168,17 +192,53 @@ const Credentials = () => { <> -

Credentials 

- - - } - > - Add Credential - + + +

Credentials 

+ + + + ) + }} + /> + + + + } + > + Add Credential + + + +
+
{credentials.length <= 0 && ( @@ -205,7 +265,7 @@ const Credentials = () => {
- {credentials.map((credential, index) => ( + {credentials.filter(filterCredentials).map((credential, index) => (
Date: Fri, 17 Nov 2023 12:35:01 +0000 Subject: [PATCH 14/34] add fix where tags are not added when submit is clicked without enter --- .../src/ui-component/button/FlowListMenu.js | 57 ++++++++++------- .../ui/src/ui-component/dialog/TagDialog.js | 63 +++++++++++-------- .../src/ui-component/table/FlowListTable.js | 2 +- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index 44192298..b242d2cb 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -1,4 +1,7 @@ -import * as React from 'react' +import { useState } from 'react' +import { useDispatch } from 'react-redux' +import PropTypes from 'prop-types' + import { styled, alpha } from '@mui/material/styles' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' @@ -10,21 +13,22 @@ import FileDeleteIcon from '@mui/icons-material/Delete' import FileCategoryIcon from '@mui/icons-material/Category' import Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' -import PropTypes from 'prop-types' -import { uiBaseURL } from '../../store/constant' -import { generateExportFlowData } from '../../utils/genericHelper' -import chatflowsApi from 'api/chatflows' -import useConfirm from 'hooks/useConfirm' -import useNotifier from '../../utils/useNotifier' -import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' import { IconX } from '@tabler/icons' -import { useDispatch } from 'react-redux' + +import chatflowsApi from 'api/chatflows' + +import useApi from '../../hooks/useApi' +import useConfirm from 'hooks/useConfirm' +import { uiBaseURL } from '../../store/constant' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' + import ConfirmDialog from '../dialog/ConfirmDialog' import SaveChatflowDialog from '../dialog/SaveChatflowDialog' -import { useState } from 'react' -import useApi from '../../hooks/useApi' import TagDialog from '../dialog/TagDialog' +import { generateExportFlowData } from '../../utils/genericHelper' +import useNotifier from '../../utils/useNotifier' + const StyledMenu = styled((props) => ( ( export default function FlowListMenu({ chatflow, updateFlowsApi }) { const { confirm } = useConfirm() const dispatch = useDispatch() - const [flowDialogOpen, setFlowDialogOpen] = useState(false) - const [categoryValues, setCategoryValues] = useState([]) - - const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) - // ==============================|| Snackbar ||============================== // useNotifier() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [anchorEl, setAnchorEl] = React.useState(null) + + const [flowDialogOpen, setFlowDialogOpen] = useState(false) + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) + const [categoryDialogProps, setCategoryDialogProps] = useState({}) + const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) + const handleClick = (event) => { setAnchorEl(event.currentTarget) } + const handleClose = () => { setAnchorEl(null) } + const handleFlowRename = () => { setAnchorEl(null) setFlowDialogOpen(true) } + const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -111,13 +118,19 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } + const handleFlowCategory = () => { setAnchorEl(null) - if (chatflow.category) setCategoryValues(chatflow.category.split(';')) - else setCategoryValues([]) + if (chatflow.category) { + setCategoryDialogProps({ + category: chatflow.category.split(';') + }) + } setCategoryDialogOpen(true) } + const saveFlowCategory = async (categories) => { + setCategoryDialogOpen(false) // save categories as string const categoryTags = categories.join(';') const updateBody = { @@ -144,6 +157,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } + const handleDelete = async () => { setAnchorEl(null) const confirmPayload = { @@ -176,6 +190,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { } } } + const handleDuplicate = () => { setAnchorEl(null) try { @@ -185,6 +200,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { console.error(e) } } + const handleExport = () => { setAnchorEl(null) try { @@ -261,9 +277,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { /> setCategoryDialogOpen(false)} - tags={categoryValues} - setTags={setCategoryValues} onSubmit={saveFlowCategory} />
diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js index 778bf2cc..82c35dde 100644 --- a/packages/ui/src/ui-component/dialog/TagDialog.js +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import Dialog from '@mui/material/Dialog' import Box from '@mui/material/Box' import Button from '@mui/material/Button' @@ -7,8 +7,9 @@ import Chip from '@mui/material/Chip' import PropTypes from 'prop-types' import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' -const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { +const TagDialog = ({ isOpen, dialogProps, onClose, onSubmit }) => { const [inputValue, setInputValue] = useState('') + const [categoryValues, setCategoryValues] = useState([]) const handleInputChange = (event) => { setInputValue(event.target.value) @@ -17,34 +18,44 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const handleInputKeyDown = (event) => { if (event.key === 'Enter' && inputValue.trim()) { event.preventDefault() - if (!tags.includes(inputValue)) { - setTags([...tags, inputValue]) + if (!categoryValues.includes(inputValue)) { + setCategoryValues([...categoryValues, inputValue]) setInputValue('') } } } - const handleDeleteTag = (tagToDelete) => { - setTags(tags.filter((tag) => tag !== tagToDelete)) + const handleDeleteTag = (categoryToDelete) => { + setCategoryValues(categoryValues.filter((category) => category !== categoryToDelete)) } const handleSubmit = (event) => { event.preventDefault() - if (inputValue.trim() && !tags.includes(inputValue)) { - setTags([...tags, inputValue]) + let newCategories = [...categoryValues] + if (inputValue.trim() && !categoryValues.includes(inputValue)) { + newCategories = [...newCategories, inputValue] + setCategoryValues(newCategories) } - onSubmit(tags) - onClose() + onSubmit(newCategories) } + useEffect(() => { + if (dialogProps.category) setCategoryValues(dialogProps.category) + + return () => { + setInputValue('') + setCategoryValues([]) + } + }, [dialogProps]) + return ( Set Chatflow Category Tags @@ -52,17 +63,20 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => {
-
- {tags.map((tag, index) => ( - handleDeleteTag(tag)} - style={{ marginRight: 5, marginBottom: 5 }} - /> - ))} -
+ {categoryValues.length > 0 && ( +
+ {categoryValues.map((category, index) => ( + handleDeleteTag(category)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ )} { label='Add a tag' variant='outlined' /> - + Enter a tag and press enter to add it to the list. You can add as many tags as you want. @@ -88,9 +102,8 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { TagDialog.propTypes = { isOpen: PropTypes.bool, + dialogProps: PropTypes.object, onClose: PropTypes.func, - tags: PropTypes.array, - setTags: PropTypes.func, onSubmit: PropTypes.func } diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index e33a8ba1..c879d82d 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -9,9 +9,9 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' +import Chip from '@mui/material/Chip' import { Button, Stack, Typography } from '@mui/material' import FlowListMenu from '../button/FlowListMenu' -import Chip from '@mui/material/Chip' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { From 0b4bf0193123e955fc990a8180139528ff7f6cd5 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 12:44:16 +0000 Subject: [PATCH 15/34] unhide columns when xs mode --- .../ui/src/ui-component/table/FlowListTable.js | 14 ++++++-------- packages/ui/src/views/chatflows/index.js | 7 +------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index c879d82d..e3baa2e2 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -51,13 +51,13 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Category - + Nodes - + Last Modified Date - + Actions
@@ -90,7 +90,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) ))}
- + {images[row.id] && (
)} - - {moment(row.updatedDate).format('MMMM Do, YYYY')} - - + {moment(row.updatedDate).format('MMMM Do, YYYY')} + diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 34c6523b..7f288a95 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -152,12 +152,7 @@ const Chatflows = () => { /> - + Date: Fri, 17 Nov 2023 13:52:41 +0000 Subject: [PATCH 16/34] fix mongodb vector database where no documents are returned --- .../Elasticsearch/Elasticsearch_Upsert.ts | 2 +- .../vectorstores/MongoDB/MongoDB_Existing.ts | 12 +++++------- .../vectorstores/MongoDB/MongoDB_Upsert.ts | 17 +++++++++-------- .../nodes/vectorstores/Redis/Redis_Upsert.ts | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts index d3965786..a8ccd49a 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/Elasticsearch_Upsert.ts @@ -50,7 +50,7 @@ class ElasicsearchUpsert_VectorStores extends ElasticSearchBase implements INode delete d.metadata.loc }) // end of workaround - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts index 3cbb36b8..7b06814a 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Existing.ts @@ -1,11 +1,10 @@ -import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { Collection } from 'mongodb' +import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' import { Embeddings } from 'langchain/embeddings/base' import { VectorStore } from 'langchain/vectorstores/base' import { Document } from 'langchain/document' - import { MongoDBSearchBase } from './MongoDBSearchBase' -import { Collection } from 'mongodb' -import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { ICommonObject, INode, INodeData } from '../../../src/Interface' class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { constructor() { @@ -20,7 +19,7 @@ class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { return super.init(nodeData, _, options, undefined) } - constructVectorStore( + async constructVectorStore( embeddings: Embeddings, collection: Collection, indexName: string, @@ -28,13 +27,12 @@ class MongoDBExisting_VectorStores extends MongoDBSearchBase implements INode { embeddingKey: string, _: Document>[] | undefined ): Promise { - const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { + return new MongoDBAtlasVectorSearch(embeddings, { collection: collection, indexName: indexName, textKey: textKey, embeddingKey: embeddingKey }) - return Promise.resolve(mongoDBAtlasVectorSearch) } } diff --git a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts index 80dfbf19..7d22f035 100644 --- a/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts +++ b/packages/components/nodes/vectorstores/MongoDB/MongoDB_Upsert.ts @@ -1,12 +1,11 @@ -import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { flatten } from 'lodash' +import { Collection } from 'mongodb' import { Embeddings } from 'langchain/embeddings/base' import { Document } from 'langchain/document' - -import { flatten } from 'lodash' import { VectorStore } from 'langchain/vectorstores/base' -import { MongoDBSearchBase } from './MongoDBSearchBase' -import { Collection } from 'mongodb' import { MongoDBAtlasVectorSearch } from 'langchain/vectorstores/mongodb_atlas' +import { ICommonObject, INode, INodeData } from '../../../src/Interface' +import { MongoDBSearchBase } from './MongoDBSearchBase' class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { constructor() { @@ -23,7 +22,7 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { }) } - constructVectorStore( + async constructVectorStore( embeddings: Embeddings, collection: Collection, indexName: string, @@ -31,12 +30,14 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { embeddingKey: string, docs: Document>[] ): Promise { - return MongoDBAtlasVectorSearch.fromDocuments(docs, embeddings, { + const mongoDBAtlasVectorSearch = new MongoDBAtlasVectorSearch(embeddings, { collection: collection, indexName: indexName, textKey: textKey, embeddingKey: embeddingKey }) + await mongoDBAtlasVectorSearch.addDocuments(docs) + return mongoDBAtlasVectorSearch } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { @@ -51,7 +52,7 @@ class MongoDBUpsert_VectorStores extends MongoDBSearchBase implements INode { } } - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } diff --git a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts index 9d1a4f45..4da58eaf 100644 --- a/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts +++ b/packages/components/nodes/vectorstores/Redis/Redis_Upsert.ts @@ -56,7 +56,7 @@ class RedisUpsert_VectorStores extends RedisSearchBase implements INode { } } - return super.init(nodeData, _, options, flattenDocs) + return super.init(nodeData, _, options, finalDocs) } } From e251bd573d50327d1a26dcee9f51f1367fc58f32 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 14:38:42 +0000 Subject: [PATCH 17/34] changed to enable uuid to be used as sessionId --- .../memory/MongoDBMemory/MongoDBMemory.ts | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts index 4c9e8581..7de2ec34 100644 --- a/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts +++ b/packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts @@ -1,6 +1,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam, ICommonObject, INode, INodeData, INodeParams } from '../../../src' import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' +import { BaseMessage, mapStoredMessageToChatMessage } from 'langchain/schema' import { MongoClient } from 'mongodb' class MongoDB_Memory implements INode { @@ -44,11 +45,13 @@ class MongoDB_Memory implements INode { type: 'string' }, { - label: 'Session ID', + label: 'Session Id', name: 'sessionId', type: 'string', - default: '5f9cf7c08d5b1a06b80fae61', - description: 'Must be an Hex String of 24 chars. This will be the objectId of the document in MongoDB Atlas' + description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId', + default: '', + additionalParams: true, + optional: true }, { label: 'Memory Key', @@ -67,9 +70,10 @@ class MongoDB_Memory implements INode { async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise { const mongodbMemory = await initializeMongoDB(nodeData, options) const sessionId = nodeData.inputs?.sessionId as string - options.logger.info(`Clearing MongoDB memory session ${sessionId}`) + const chatId = options?.chatId as string + options.logger.info(`Clearing MongoDB memory session ${sessionId ? sessionId : chatId}`) await mongodbMemory.clear() - options.logger.info(`Successfully cleared MongoDB memory session ${sessionId}`) + options.logger.info(`Successfully cleared MongoDB memory session ${sessionId ? sessionId : chatId}`) } } @@ -78,6 +82,10 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const collectionName = nodeData.inputs?.collectionName as string const sessionId = nodeData.inputs?.sessionId as string const memoryKey = nodeData.inputs?.memoryKey as string + const chatId = options?.chatId as string + + let isSessionIdUsingChatMessageId = false + if (!sessionId && chatId) isSessionIdUsingChatMessageId = true const credentialData = await getCredentialData(nodeData.credential ?? '', options) let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData) @@ -88,14 +96,37 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({ collection, - sessionId: sessionId + sessionId: sessionId ? sessionId : chatId }) + mongoDBChatMessageHistory.getMessages = async (): Promise => { + const document = await collection.findOne({ + sessionId: (mongoDBChatMessageHistory as any).sessionId + }) + const messages = document?.messages || [] + return messages.map(mapStoredMessageToChatMessage) + } + + mongoDBChatMessageHistory.addMessage = async (message: BaseMessage): Promise => { + const messages = [message].map((msg) => msg.toDict()) + await collection.updateOne( + { sessionId: (mongoDBChatMessageHistory as any).sessionId }, + { + $push: { messages: { $each: messages } } + }, + { upsert: true } + ) + } + + mongoDBChatMessageHistory.clear = async (): Promise => { + await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId }) + } + return new BufferMemoryExtended({ memoryKey, chatHistory: mongoDBChatMessageHistory, returnMessages: true, - isSessionIdUsingChatMessageId: false + isSessionIdUsingChatMessageId }) } From 3bddc95087a81ee2faeea835463841239c8e9b1b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 16:41:49 +0000 Subject: [PATCH 18/34] add user id --- packages/components/package.json | 2 +- packages/components/src/handler.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/package.json b/packages/components/package.json index 996419ca..0df99a11 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -49,7 +49,7 @@ "html-to-text": "^9.0.5", "ioredis": "^5.3.2", "langchain": "^0.0.165", - "langfuse-langchain": "^1.0.14-alpha.0", + "langfuse-langchain": "^1.0.31", "langsmith": "^0.0.32", "linkifyjs": "^4.1.1", "llmonitor": "^0.5.5", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 37075342..456cf39c 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -250,6 +250,7 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' } if (release) langFuseOptions.release = release + if (options.chatId) langFuseOptions.userId = options.chatId const handler = new CallbackHandler(langFuseOptions) callbacks.push(handler) From 28f5d94c1343e4c122d9929fefb6fce1d598cf55 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 18 Nov 2023 16:47:15 +0530 Subject: [PATCH 19/34] API Keys: Displaying the names of the chatflows associated with the keys and Warning the user before deletion. --- packages/server/src/index.ts | 30 +++++- packages/ui/src/views/apikey/index.js | 138 +++++++++++++++----------- 2 files changed, 104 insertions(+), 64 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0..4307946b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1135,28 +1135,50 @@ export class App { // API Keys // ---------------------------------------- + const addChatflowsCount = async (keys: any, res: Response) => { + if (keys) { + const updatedKeys: any[] = [] + //iterate through keys and get chatflows + for (const key of keys) { + const chatflows = await this.AppDataSource.getRepository(ChatFlow) + .createQueryBuilder('cf') + .where('cf.apikeyid = :apikeyid', { apikeyid: key.id }) + .getMany() + const linkedChatFlows: any[] = [] + chatflows.map((cf) => { + linkedChatFlows.push({ + flowName: cf.name + }) + }) + key.chatFlows = linkedChatFlows + updatedKeys.push(key) + } + return res.json(updatedKeys) + } + return res.json(keys) + } // Get api keys this.app.get('/api/v1/apikey', async (req: Request, res: Response) => { const keys = await getAPIKeys() - return res.json(keys) + return addChatflowsCount(keys, res) }) // Add new api key this.app.post('/api/v1/apikey', async (req: Request, res: Response) => { const keys = await addAPIKey(req.body.keyName) - return res.json(keys) + return addChatflowsCount(keys, res) }) // 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 res.json(keys) + return addChatflowsCount(keys, res) }) // Delete new api key this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await deleteAPIKey(req.params.id) - return res.json(keys) + return addChatflowsCount(keys, res) }) // Verify api key diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index a2b2e639..226baaee 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -6,6 +6,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import { Button, Box, + Chip, Stack, Table, TableBody, @@ -37,7 +38,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconCornerDownRight, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' // ==============================|| APIKey ||============================== // @@ -106,7 +107,10 @@ const APIKey = () => { const deleteKey = async (key) => { const confirmPayload = { title: `Delete`, - description: `Delete key ${key.keyName}?`, + description: + key.chatFlows.length === 0 + ? `Delete key [${key.keyName}] ? ` + : `Delete key [${key.keyName}] ?\n There are ${key.chatFlows.length} chatflows using this key.`, confirmButtonName: 'Delete', cancelButtonName: 'Cancel' } @@ -193,6 +197,7 @@ const APIKey = () => { Key Name API Key + Usage Created @@ -200,65 +205,78 @@ const APIKey = () => { {apiKeys.map((key, index) => ( - - - {key.keyName} - - - {showApiKeys.includes(key.apiKey) - ? key.apiKey - : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( - key.apiKey.length - 5 - )}`} - { - navigator.clipboard.writeText(key.apiKey) - setAnchorEl(event.currentTarget) - setTimeout(() => { - handleClosePopOver() - }, 1500) - }} - > - - - onShowApiKeyClick(key.apiKey)}> - {showApiKeys.includes(key.apiKey) ? : } - - - + + + {key.keyName} + + + {showApiKeys.includes(key.apiKey) + ? key.apiKey + : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( + key.apiKey.length - 5 + )}`} + { + navigator.clipboard.writeText(key.apiKey) + setAnchorEl(event.currentTarget) + setTimeout(() => { + handleClosePopOver() + }, 1500) + }} > - Copied! - - - - {key.createdAt} - - edit(key)}> - - - - - deleteKey(key)}> - - - - + + + onShowApiKeyClick(key.apiKey)}> + {showApiKeys.includes(key.apiKey) ? : } + + + + Copied! + + + + {key.chatFlows.length} + {key.createdAt} + + edit(key)}> + + + + + deleteKey(key)}> + + + + + {key.chatFlows.length > 0 && ( + + + {' '} + {key.chatFlows.map((flow, index) => ( + + ))} + + + )} + ))}
From c7add456479fe0e21d80121de6be33f68b55a960 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 00:55:58 +0000 Subject: [PATCH 20/34] add file annotations, sync and delete assistant --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 125 +++++++++++-- packages/server/src/Interface.ts | 1 + .../src/database/entities/ChatMessage.ts | 3 + ...1021237-AddFileAnnotationsToChatMessage.ts | 12 ++ .../src/database/migrations/mysql/index.ts | 4 +- ...1699481607341-AddUsedToolsToChatMessage.ts | 2 +- ...1021237-AddFileAnnotationsToChatMessage.ts | 11 ++ .../src/database/migrations/postgres/index.ts | 4 +- ...1021237-AddFileAnnotationsToChatMessage.ts | 20 +++ .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 16 +- packages/ui/src/api/assistants.js | 3 +- .../ui-component/dialog/ViewMessagesDialog.js | 50 +++++- .../src/views/assistants/AssistantDialog.js | 165 ++++++++++++------ .../views/assistants/DeleteConfirmDialog.js | 47 +++++ .../ui/src/views/chatmessage/ChatMessage.js | 51 +++++- 16 files changed, 436 insertions(+), 82 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/ui/src/views/assistants/DeleteConfirmDialog.js diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 56e1b290..ed7baf7d 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -111,7 +111,7 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) options.logger.info(`Clearing OpenAI Thread ${sessionId}`) - await openai.beta.threads.del(sessionId) + if (sessionId) await openai.beta.threads.del(sessionId) options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) } @@ -135,16 +135,25 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) - // Retrieve assistant try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id + + // Retrieve assistant const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) if (formattedTools.length) { - let filteredTools = uniqWith([...retrievedAssistant.tools, ...formattedTools], isEqual) + let filteredTools = [] + for (const tool of retrievedAssistant.tools) { + if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool) + } + filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual) + // filter out tool with empty function filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) + } else { + let filteredTools = retrievedAssistant.tools.filter((tool) => tool.type !== 'function') + await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ @@ -152,14 +161,45 @@ class OpenAIAssistant_Agents implements INode { }) let threadId = '' + let isNewThread = false if (!chatmessage) { const thread = await openai.beta.threads.create({}) threadId = thread.id + isNewThread = true } else { const thread = await openai.beta.threads.retrieve(chatmessage.sessionId) threadId = thread.id } + // List all runs + if (!isNewThread) { + const promise = (threadId: string) => { + return new Promise((resolve) => { + const timeout = setInterval(async () => { + const allRuns = await openai.beta.threads.runs.list(threadId) + if (allRuns.data && allRuns.data.length) { + const firstRunId = allRuns.data[0].id + const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status + if ( + runStatus && + (runStatus === 'cancelled' || + runStatus === 'completed' || + runStatus === 'expired' || + runStatus === 'failed') + ) { + clearInterval(timeout) + resolve() + } + } else { + clearInterval(timeout) + resolve() + } + }, 500) + }) + } + await promise(threadId) + } + // Add message to thread await openai.beta.threads.messages.create(threadId, { role: 'user', @@ -217,27 +257,41 @@ class OpenAIAssistant_Agents implements INode { }) resolve(state) } else { - reject( - new Error( - `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}. submit_tool_outputs.tool_calls are empty` - ) - ) + await openai.beta.threads.runs.cancel(threadId, runId) + resolve('requires_action_retry') } } } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { clearInterval(timeout) - reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) + reject( + new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`) + ) } }, 500) }) } // Polling run status + let runThreadId = runThread.id let state = await promise(threadId, runThread.id) while (state === 'requires_action') { state = await promise(threadId, runThread.id) } + let retries = 3 + while (state === 'requires_action_retry') { + if (retries > 0) { + retries -= 1 + const newRunThread = await openai.beta.threads.runs.create(threadId, { + assistant_id: retrievedAssistant.id + }) + runThreadId = newRunThread.id + state = await promise(threadId, newRunThread.id) + } else { + throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + } + } + // List messages const messages = await openai.beta.threads.messages.list(threadId) const messageData = messages.data ?? [] @@ -245,12 +299,58 @@ class OpenAIAssistant_Agents implements INode { if (!assistantMessages.length) return '' let returnVal = '' + const fileAnnotations = [] for (let i = 0; i < assistantMessages[0].content.length; i += 1) { if (assistantMessages[0].content[i].type === 'text') { const content = assistantMessages[0].content[i] as MessageContentText - returnVal += content.text.value - //TODO: handle annotations + if (content.text.annotations) { + const message_content = content.text + const annotations = message_content.annotations + + const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant') + + // Iterate over the annotations and add footnotes + for (let index = 0; index < annotations.length; index++) { + const annotation = annotations[index] + let filePath = '' + + // Gather citations based on annotation attributes + const file_citation = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FileCitation) + .file_citation + if (file_citation) { + const cited_file = await openai.files.retrieve(file_citation.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } else { + const file_path = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FilePath).file_path + if (file_path) { + const cited_file = await openai.files.retrieve(file_path.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } + } + + // Replace the text with a footnote + message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`) + } + + returnVal += message_content.value + } else { + returnVal += content.text.value + } } else { const content = assistantMessages[0].content[i] as MessageContentImageFile const fileId = content.image_file.file_id @@ -271,7 +371,8 @@ class OpenAIAssistant_Agents implements INode { return { text: returnVal, usedTools, - assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData } + fileAnnotations, + assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { throw new Error(error) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 8d0965f4..1d272464 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -30,6 +30,7 @@ export interface IChatMessage { chatflowid: string sourceDocuments?: string usedTools?: string + fileAnnotations?: string chatType: string chatId: string memoryType?: string diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index b51aa434..4054a26d 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) usedTools?: string + @Column({ nullable: true, type: 'text' }) + fileAnnotations?: string + @Column() chatType: string diff --git a/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 00000000..a352cde8 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_message', 'fileAnnotations') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileAnnotations\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileAnnotations\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a95..eff089cd 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 { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts index f9f893f8..ae34c813 100644 --- a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts +++ b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts @@ -6,6 +6,6 @@ export class AddUsedToolsToChatMessage1699481607341 implements MigrationInterfac } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "usedTools";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "usedTools";`) } } diff --git a/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 00000000..8824f57d --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileAnnotations" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b..93d02f3e 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 { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 00000000..af29fba4 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc40..edba5930 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 { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0..57245f2c 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -138,6 +138,7 @@ export class App { '/api/v1/node-icon/', '/api/v1/components-credentials-icon/', '/api/v1/chatflows-streaming', + '/api/v1/openai-assistants-file', '/api/v1/ip' ] this.app.use((req, res, next) => { @@ -782,8 +783,8 @@ export class App { await openai.beta.assistants.update(assistantDetails.id, { name: assistantDetails.name, - description: assistantDetails.description, - instructions: assistantDetails.instructions, + description: assistantDetails.description ?? '', + instructions: assistantDetails.instructions ?? '', model: assistantDetails.model, tools: filteredTools, file_ids: uniqWith( @@ -952,7 +953,7 @@ export class App { const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id }) - await openai.beta.assistants.del(assistantDetails.id) + if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id) return res.json(results) } catch (error: any) { @@ -961,6 +962,14 @@ export class App { } }) + // Download file from assistant + this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + }) + // ---------------------------------------- // Configuration // ---------------------------------------- @@ -1499,6 +1508,7 @@ export class App { } if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments) if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools) + if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations) await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) diff --git a/packages/ui/src/api/assistants.js b/packages/ui/src/api/assistants.js index 63dd5e18..ac941126 100644 --- a/packages/ui/src/api/assistants.js +++ b/packages/ui/src/api/assistants.js @@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body) const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body) -const deleteAssistant = (id) => client.delete(`/assistants/${id}`) +const deleteAssistant = (id, isDeleteBoth) => + isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`) export default { getAllAssistants, diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 2e52d596..29a64155 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' // material-ui import { @@ -28,7 +29,7 @@ import DatePicker from 'react-datepicker' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import msgEmptySVG from 'assets/images/message_empty.svg' -import { IconFileExport, IconEraser, IconX } from '@tabler/icons' +import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons' // Project import import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' @@ -48,6 +49,7 @@ import useConfirm from 'hooks/useConfirm' // Utils import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' import useNotifier from 'utils/useNotifier' +import { baseURL } from 'store/constant' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' @@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools) + if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) { obj[chatPK] = { @@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools) + if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) loadedMessages.push(obj) } @@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { window.open(data, '_blank') } + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file`, + { fileName: fileAnnotation.fileName }, + { responseType: 'blob' } + ) + const blob = new Blob([response.data], { type: response.headers['content-type'] }) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = fileAnnotation.fileName + document.body.appendChild(link) + link.click() + link.remove() + } catch (error) { + console.error('Download failed:', error) + } + } + const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -648,6 +672,30 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.message} + {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { diff --git a/packages/ui/src/views/assistants/AssistantDialog.js b/packages/ui/src/views/assistants/AssistantDialog.js index e841fc4f..30087bae 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.js +++ b/packages/ui/src/views/assistants/AssistantDialog.js @@ -9,12 +9,12 @@ import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogConte import { StyledButton } from 'ui-component/button/StyledButton' import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' -import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' import { Dropdown } from 'ui-component/dropdown/Dropdown' import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' import CredentialInputHandler from 'views/canvas/CredentialInputHandler' import { File } from 'ui-component/file/File' import { BackdropLoader } from 'ui-component/loading/BackdropLoader' +import DeleteConfirmDialog from './DeleteConfirmDialog' // Icons import { IconX } from '@tabler/icons' @@ -23,7 +23,6 @@ import { IconX } from '@tabler/icons' import assistantsApi from 'api/assistants' // Hooks -import useConfirm from 'hooks/useConfirm' import useApi from 'hooks/useApi' // utils @@ -71,14 +70,8 @@ const assistantAvailableModels = [ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const portalElement = document.getElementById('portal') - - const dispatch = useDispatch() - - // ==============================|| Snackbar ||============================== // - useNotifier() - const { confirm } = useConfirm() - + const dispatch = useDispatch() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) @@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const [assistantFiles, setAssistantFiles] = useState([]) const [uploadAssistantFiles, setUploadAssistantFiles] = useState('') const [loading, setLoading] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deleteDialogProps, setDeleteDialogProps] = useState({}) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) @@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { useEffect(() => { if (getAssistantObjApi.data) { - setOpenAIAssistantId(getAssistantObjApi.data.id) - setAssistantName(getAssistantObjApi.data.name) - setAssistantDesc(getAssistantObjApi.data.description) - setAssistantModel(getAssistantObjApi.data.model) - setAssistantInstructions(getAssistantObjApi.data.instructions) - setAssistantFiles(getAssistantObjApi.data.files ?? []) - - let tools = [] - if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) { - for (const tool of getAssistantObjApi.data.tools) { - tools.push(tool.type) - } - } - setAssistantTools(tools) + syncData(getAssistantObjApi.data) } }, [getAssistantObjApi.data]) @@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dialogProps]) + const syncData = (data) => { + setOpenAIAssistantId(data.id) + setAssistantName(data.name) + setAssistantDesc(data.description) + setAssistantModel(data.model) + setAssistantInstructions(data.instructions) + setAssistantFiles(data.files ?? []) + + let tools = [] + if (data.tools && data.tools.length) { + for (const tool of data.tools) { + tools.push(tool.type) + } + } + setAssistantTools(tools) + } + const addNewAssistant = async () => { setLoading(true) try { @@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { } } - const deleteAssistant = async () => { - const confirmPayload = { - title: `Delete Assistant`, - description: `Delete Assistant ${assistantName}?`, - confirmButtonName: 'Delete', - cancelButtonName: 'Cancel' - } - const isConfirmed = await confirm(confirmPayload) - - if (isConfirmed) { - try { - const delResp = await assistantsApi.deleteAssistant(assistantId) - if (delResp.data) { - enqueueSnackbar({ - message: 'Assistant deleted', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - onConfirm() - } - } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + const onSyncClick = async () => { + setLoading(true) + try { + const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential) + if (getResp.data) { + syncData(getResp.data) enqueueSnackbar({ - message: `Failed to delete Assistant: ${errorData}`, + message: 'Assistant successfully synced!', options: { key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, + variant: 'success', action: (key) => ( + ) + } + }) + setLoading(false) + } + } + + const onDeleteClick = () => { + setDeleteDialogProps({ + title: `Delete Assistant`, + description: `Delete Assistant ${assistantName}?`, + cancelButtonName: 'Cancel' + }) + setDeleteDialogOpen(true) + } + + const deleteAssistant = async (isDeleteBoth) => { + setDeleteDialogOpen(false) + try { + const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth) + if (delResp.data) { + enqueueSnackbar({ + message: 'Assistant deleted', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + onConfirm() + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to delete Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() } } @@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.type === 'EDIT' && ( - deleteAssistant()}> + onSyncClick()}> + Sync + + )} + {dialogProps.type === 'EDIT' && ( + onDeleteClick()}> Delete )} @@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.confirmButtonName} - + setDeleteDialogOpen(false)} + onDelete={() => deleteAssistant()} + onDeleteBoth={() => deleteAssistant(true)} + /> {loading && } ) : null diff --git a/packages/ui/src/views/assistants/DeleteConfirmDialog.js b/packages/ui/src/views/assistants/DeleteConfirmDialog.js new file mode 100644 index 00000000..f4453631 --- /dev/null +++ b/packages/ui/src/views/assistants/DeleteConfirmDialog.js @@ -0,0 +1,47 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material' +import { StyledButton } from 'ui-component/button/StyledButton' + +const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + {dialogProps.title} + + + {dialogProps.description} +
+ + Delete only from Flowise + + + Delete from both OpenAI and Flowise + + +
+
+
+ ) : null + + return createPortal(component, portalElement) +} + +DeleteConfirmDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onDeleteBoth: PropTypes.func, + onDelete: PropTypes.func, + onCancel: PropTypes.func +} + +export default DeleteConfirmDialog diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0cf5695b..7cfd0474 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' -import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material' +import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconSend } from '@tabler/icons' +import { IconSend, IconDownload } from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' @@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setMessages((prevMessages) => [ ...prevMessages, - { message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' } + { + message: text, + sourceDocuments: data?.sourceDocuments, + usedTools: data?.usedTools, + fileAnnotations: data?.fileAnnotations, + type: 'apiMessage' + } ]) } @@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file`, + { fileName: fileAnnotation.fileName }, + { responseType: 'blob' } + ) + const blob = new Blob([response.data], { type: response.headers['content-type'] }) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = fileAnnotation.fileName + document.body.appendChild(link) + link.click() + link.remove() + } catch (error) { + console.error('Download failed:', error) + } + } + // Get chatmessages successful useEffect(() => { if (getChatmessageApi.data?.length) { @@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) + if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) @@ -331,6 +359,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.message}
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { From a4a1e7d562dff040f9dd00cc51762e426fe2318f Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 20 Nov 2023 13:03:28 +0530 Subject: [PATCH 21/34] API Key: Changes to API Key Dashboard to show usage details (chatflows) --- packages/server/src/index.ts | 3 +- packages/ui/src/views/apikey/index.js | 204 ++++++++++++++++---------- 2 files changed, 131 insertions(+), 76 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4307946b..a8745db7 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1147,7 +1147,8 @@ export class App { const linkedChatFlows: any[] = [] chatflows.map((cf) => { linkedChatFlows.push({ - flowName: cf.name + flowName: cf.name, + updatedDate: cf.updatedDate }) }) key.chatFlows = linkedChatFlows diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index 226baaee..72b73baf 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -6,7 +6,6 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import { Button, Box, - Chip, Stack, Table, TableBody, @@ -17,7 +16,8 @@ import { Paper, IconButton, Popover, - Typography + Typography, + Collapse } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -38,11 +38,118 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconCornerDownRight, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconChevronsUp, IconChevronsDown, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' +import * as PropTypes from 'prop-types' // ==============================|| APIKey ||============================== // +function APIKeyRow(props) { + const [open, setOpen] = useState(false) + return ( + <> + *': { borderBottom: 'unset' } }}> + + {props.apiKey.keyName} + + + {props.showApiKeys.includes(props.apiKey.apiKey) + ? props.apiKey.apiKey + : `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring( + props.apiKey.apiKey.length - 5 + )}`} + + + + + {props.showApiKeys.includes(props.apiKey.apiKey) ? : } + + + + Copied! + + + + + {props.apiKey.chatFlows.length}{' '} + {props.apiKey.chatFlows.length > 0 && ( + setOpen(!open)}> + {props.apiKey.chatFlows.length > 0 && open ? : } + + )} + + {props.apiKey.createdAt} + + + + + + + + + + + + + + + + + + + Chatflow Name + Modified On + Category + + + + {props.apiKey.chatFlows.map((flow, index) => ( + + + {flow.flowName} + + {flow.updatedDate} + + + ))} + +
+
+
+
+
+ + ) +} + +APIKeyRow.propTypes = { + apiKey: PropTypes.any, + showApiKeys: PropTypes.arrayOf(PropTypes.any), + onCopyClick: PropTypes.func, + onShowAPIClick: PropTypes.func, + open: PropTypes.bool, + anchorEl: PropTypes.any, + onClose: PropTypes.func, + theme: PropTypes.any, + onEditClick: PropTypes.func, + onDeleteClick: PropTypes.func +} const APIKey = () => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -205,78 +312,25 @@ const APIKey = () => { {apiKeys.map((key, index) => ( - <> - - - {key.keyName} - - - {showApiKeys.includes(key.apiKey) - ? key.apiKey - : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring( - key.apiKey.length - 5 - )}`} - { - navigator.clipboard.writeText(key.apiKey) - setAnchorEl(event.currentTarget) - setTimeout(() => { - handleClosePopOver() - }, 1500) - }} - > - - - onShowApiKeyClick(key.apiKey)}> - {showApiKeys.includes(key.apiKey) ? : } - - - - Copied! - - - - {key.chatFlows.length} - {key.createdAt} - - edit(key)}> - - - - - deleteKey(key)}> - - - - - {key.chatFlows.length > 0 && ( - - - {' '} - {key.chatFlows.map((flow, index) => ( - - ))} - - - )} - + { + navigator.clipboard.writeText(key.apiKey) + setAnchorEl(event.currentTarget) + setTimeout(() => { + handleClosePopOver() + }, 1500) + }} + onShowAPIClick={() => onShowApiKeyClick(key.apiKey)} + open={openPopOver} + anchorEl={anchorEl} + onClose={handleClosePopOver} + theme={theme} + onEditClick={() => edit(key)} + onDeleteClick={() => deleteKey(key)} + /> ))} From c7bf75e2597809fbdb56c04282059ece22f73f66 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 20 Nov 2023 18:19:25 +0530 Subject: [PATCH 22/34] UX Changes: persist user display choice in localStorage --- packages/ui/src/views/chatflows/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 7f288a95..3c4b8972 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -43,9 +43,10 @@ const Chatflows = () => { const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) - const [view, setView] = React.useState('card') + const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card') const handleChange = (event, nextView) => { + localStorage.setItem('flowDisplayStyle', nextView) setView(nextView) } From 9a3be5f4bf35706adc8cc65466b65c50b85d1f22 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 14:38:33 +0000 Subject: [PATCH 23/34] fix sessionid undefined --- packages/server/src/utils/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9..6eb979f6 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -985,10 +985,14 @@ export const redactCredentialWithPasswordType = ( * @param {any} instance * @param {string} chatId */ -export const checkMemorySessionId = (instance: any, chatId: string): string => { +export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => { if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { instance.memory.sessionId = chatId instance.memory.chatHistory.sessionId = chatId } - return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined + + if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId + else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId) + return instance.memory.chatHistory.sessionId + return undefined } From 40a63008ece274161173daf4c674eb743192fb95 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 19:34:30 +0000 Subject: [PATCH 24/34] add vectara chain --- .../nodes/chains/VectaraChain/VectaraChain.ts | 147 ++++++++++++++++++ .../nodes/chains/VectaraChain/vectara.png | Bin 0 -> 67193 bytes packages/server/src/utils/index.ts | 2 +- .../ui-component/dialog/ViewMessagesDialog.js | 5 +- packages/ui/src/utils/genericHelper.js | 12 +- .../ui/src/views/chatmessage/ChatMessage.js | 5 +- 6 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 packages/components/nodes/chains/VectaraChain/VectaraChain.ts create mode 100644 packages/components/nodes/chains/VectaraChain/vectara.png diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts new file mode 100644 index 00000000..a2fac534 --- /dev/null +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -0,0 +1,147 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' +import { VectorDBQAChain } from 'langchain/chains' +import { Document } from 'langchain/document' +import { VectaraStore } from 'langchain/vectorstores/vectara' +import fetch from 'node-fetch' + +class VectaraChain_Chains implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + baseClasses: string[] + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Vectara QA Chain' + this.name = 'vectaraQAChain' + this.version = 1.0 + this.type = 'VectaraQAChain' + this.icon = 'vectara.png' + this.category = 'Chains' + this.description = 'QA chain for Vectara' + this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] + this.inputs = [ + { + label: 'Vectara Vector Store', + name: 'vectaraStore', + type: 'VectorStore' + } + ] + } + + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string): Promise { + const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore + const topK = (vectorStore as any)?.k ?? 4 + + const headers = await vectorStore.getJsonHeader() + const vectaraFilter = (vectorStore as any).vectaraFilter ?? {} + const corpusId: number[] = (vectorStore as any).corpusId ?? [] + const customerId = (vectorStore as any).customerId ?? '' + + const corpusKeys = corpusId.map((corpusId) => ({ + customerId, + corpusId, + metadataFilter: vectaraFilter?.filter ?? '', + lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } + })) + + let summarizerPromptName = 'vectara-experimental-summary-ext-2023-10-23-med' // can let user select + let responseLang = 'en' // can let user select + let maxSummarizedResults = 5 // can let user specify + + const data = { + query: [ + { + query: input, + start: 0, + numResults: topK, + contextConfig: { + sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, + sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 + }, + corpusKey: corpusKeys, + summary: [ + { + summarizerPromptName, + responseLang, + maxSummarizedResults + } + ] + } + ] + } + + try { + const response = await fetch(`https://api.vectara.io/v1/query`, { + method: 'POST', + headers: headers?.headers, + body: JSON.stringify(data) + }) + + if (response.status !== 200) { + throw new Error(`Vectara API returned status code ${response.status}`) + } + + const result = await response.json() + const responses = result.responseSet[0].response + const documents = result.responseSet[0].document + let summarizedText = '' + + for (let i = 0; i < responses.length; i += 1) { + const responseMetadata = responses[i].metadata + const documentMetadata = documents[responses[i].documentIndex].metadata + const combinedMetadata: Record = {} + + responseMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + documentMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + responses[i].metadata = combinedMetadata + } + + const summaryStatus = result.responseSet[0].summary[0].status + if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { + throw new Error( + `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` + ) + } + + if ( + summaryStatus.length > 0 && + summaryStatus[0].code === 'NOT_FOUND' && + summaryStatus[0].statusDetail === 'Failed to retrieve summarizer.' + ) { + throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) + } + + summarizedText = result.responseSet[0].summary[0]?.text + + const sourceDocuments: Document[] = responses.map( + (response: { text: string; metadata: Record; score: number }) => + new Document({ + pageContent: response.text, + metadata: response.metadata + }) + ) + + return { text: summarizedText, sourceDocuments: sourceDocuments } + } catch (error) { + throw new Error(error) + } + } +} + +module.exports = { nodeClass: VectaraChain_Chains } diff --git a/packages/components/nodes/chains/VectaraChain/vectara.png b/packages/components/nodes/chains/VectaraChain/vectara.png new file mode 100644 index 0000000000000000000000000000000000000000..a13a34e6b837a94f70e01253cd3e17aa02b68f48 GIT binary patch literal 67193 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%0A)QN)(hd`;d-QP6WmNWEHJ!9n^=PgZ@EK~ z1F6cx{I{t`GO-1Kj!^1TE+1 z#$)?uAF;t-ClJb>Vvpst{y1^q&bVTC(%Z`n5!7;(+<&?f7ZQFM44IpV?zr$!C;SWu zdaI%H!3I}vlF1tU`C6xAl_*&+NKddLVfE}9td89MlYCg@vmp|xA0Z~=7lqg26ZML8 z6&v(?D~`EN0j-=KptI)%nOK1*(+-tiZoC~Q@UOKE z?OHla{JZmX?t)dTqpD+51K{$ILWT=w6!`+rjV1^2y322sb46IYLpBR=2aXe;TYz%BTYaIC`R}?pp?}Z9i|00b z*K&Y}yzONVtW<%qSb$!4CV_ZEbfw2{M7B+sn1H_cWmh<0MSO9u>t$b90v6%u%mrLy zAqjWGp7k>b6$j-e{4|q~h(G1WEs4Xz67r2MWZTkigOTCT3z71`Fw_~EV@I9#Flink zh6uNzq|g@M9dEx*`7>L8Y%74S$%&$9E^}w7{!-@%2xFzpDmC{s;F+J^&zk5QzaUVH z1q;IGz>S!7U9YpV#|Z5oRWf6``}B#Dn7J2nkv`6dhrlNF7D}c`%aV`d0cYmhR!(=G z=8B})l^%*Xus!57`stF8J*&L#E0Fp!3+fN{=C|W1vBz=PQm&rpMbXflyO+}*CN3cK=mmj?gXHC<5pjgMg*{_;|(@=`Ws3c?S> zM&Ee?Kee`-O$B-0-tWQBXI#8&j^Vd5>XFwtO;H%&-*E)AZ0zjfQE&myQ3N&zz*-|* zgH6H0kpz`Tx!NfQ{DD9G26FH-i&fjS?tThQrVH`XUC5dS_~RAv{i$Wul1-msLx?ed&hBjh!2p z!yW~RKVB*1ii6&E!R}*M3vGZS!-v6a|Nklj<~Ho{31Uq*GL-$sPxBL!?xGxfwe)7? z*M{V$)N(?|K||6W2>TRU`gkQUk{I!T_1KB4^X!vo=uYbu!5*%zx2tR#g<1HQxNCHw z4xHxQA=&NLppL1lB8!$BF;DLG*4Hb8@Jr+G(6%Pqm`sr~jFh^kl z5FAQ_;id-?i-$*m*#*!->e^_o5C~l|r1#ji`+K3@+N2?Rv{YJ|#8sfj2(rOt>CqYM zdYtg#zqCYJN=d;-cG{#YHpuRk%FsO21v3p(v6Too*-LbZ z7P|R*Z{gaP=FW4E&0>wmq9ptT5j{IJWKP^$P-)q(xUl_MIwt(XrG?0=NRz8^@pTWw zrRo2Xu7&QE@KbJQ8{qq#B5ehlUke0EoYWdS231l+*MyG;3cZ{JzLTu^#3Q$*B`-b_J9Cg{`U3>olqN`- zlz=gx5t>K|y_QxontPtV?DisYiq9{j8`}OG6lDRe@|+L~zjeO6P{8ecX7K?YDG#6> zEsbmqXW|O}0-Zhro?;CJ!qcLZ$MFXm5FTv17K>Tr#f;DIxiU852Ab5~TFOyoSIGwI z#X1}f#oaH`fibun5bw}IeJXCB*cVy;%_>L#pgS^%U}ZlE)Yv@rIN#m zbK8rx7VK6+|%BPU8f7GiuI860dE=!#QdDn z#n(JR5iNx*Huyjo2{u?AL!L3V+gw$L5@EvcpIq`qBJ}1EV}GGRKz$hD%z6XQ{W<|Y31TOA=O1X-{{m%soLbL#GfOBUL z7T}pb6X!nDjzr^@h@?Cd+?H(=p@YeciD*xQZNDCsuS;^jimQ0g|0a?vLtC##3%Q`E zY#TBe+ZQAju$7~f;S-(Mw3WkDuhXr;S^jD&IhR${^`5)+9>yS(vifedv-I`%o=H)L z?IZh(8n5jaR-*$TM=0aI%!~{MsaUJg+vMv->bi>~Y&UEHP$#ySXx!H%$q^g>oERp$ z(%*_54g)naY?7;Vu=s`2<9wYRmu!81hWW~`%E1MEdGamVL$Jh~mpg&Lf};yD!jby; zYf`6tDP&vn6)w8mGW4 zwUhVHVOBCt=s4oXUoT+e$y|xsZER4M^LbcZ1)0x?W=Ja&5F_D_IIBpU`qR|#dA#hM z4q3gKIm*iZn zrqegxHGFR$=WXxEbULs2oiG2l;HkUk5 z#F?EJ@Dv$j;FsiHJyp?_LL6N|?|w1lwZr8?DgF7wVmi4UNLU6md)4xZOK7_cq4K)%xZ3Z0pEDl z&`<%mJYOOKgw7BA^AeFH44XgC@wPLe8z|A@$a-P;G8=p!l8=z5 zrOvRbNV{uK59RV8lyp;`17~=-Au*>o*n)lavQkIF8EioIS6~WmxNB4}{;BlB&hTwW zK#OSVTve3or2xr4gXBjS7-XWYI!X}|_k?Y4YuA*-p!w3?ZzH+2ko$&y4oSyhp4CEx(6&{rGt#!j+&OW)%h?yxECu8#+D394pI^-KK z3+iQccZ~&vUcO85H|^?t|Lj$|o$9zUTU5TSEcNSe+I_~3F@hEG9>3SYzJ|JbzTU}w z%BG-(_cx0F`66?gWI^h0mB|u(;7IfhHUm3hL@mb)+G*hmjBH^@(5--86(g^en9GtL zt-`()lUo_yGZbFbvco0duh0;kCsiY%UU*CM?!W1E0=^1dxg2zB;%V>@?Zh5b(d3@I zvo=xYZhiUvkGFORxcfq2*Y$xysm_VmR@3(?h~*E zhqP;?YRROrrwz`FVVbOn%VrldJ95*%LI(Xtlv^*KW0&$5aLLDliZa08(VH2?Rktr1 z%=IF1LA0*rpq0DzWm1P^6GTqhao!*}(=ptFe=1kLr33c)3KA;X8LVDPy{#di5Wb6- zeaEWNU2`=p=7=&zyYOw((82g5j(2awUiGhPtuM$qSJ|!JW|YC zJDu;Soa};bRfZXs**x>}T6F6+ng3h@q{*pc~8G2EQ2Xn**qrU{JV}p-q6siFS>jlK?0@I@y09?XC zhkn3J8WJPbut8q?UkFLW2}r^XKSr_hIk3}XO{rl5NN6SvI<(TrajgkXra4~6+_7xL zI=r2s6IFfH$0PwNFTm^54FSNE>LwTZfol@#X>uH%C-ls<2 zoPe)-ThljzF9q~&TjLRe_s!w?>c)#^no{xK02E4{)jgAb4IW{`yV7~POSbqjG1u^s z6gurH&FJcjokXbwhB}$g)N&t9=*3CHPuWb!*IJ4n%T|chB%yN7&-Wt$Z}u&*FYjFn zSuj~}dVK$v{;RGtkELDVT2VpGn&jR{YtplNwL%9lEZZAA*2esNWL$rz?g`xuBVw?s z9{0JkA0(X*4yX_*{_7dLI@5#7^-i4LqJ0_a_0lfVlB zHR0~>H1b30^RdPrC5`LPuXoiL`Fs~xqt#3|YyHZ1gddD++xe(1-^w<@b=c$MRC=IX z`Geh`_W=KUSL-i9^z7RoHr!Uuj@m`oRv?MfeEJay4fufmJ67Po;}T;^J5q?70YRXY z9=fFWVR{Xx67bKwILAL+H~}&EZ#tqJC8hx9CjsA}CwhGlwY(6c>0?4f-vvM8b5G-?@lD8=MbwMHRLZUJyv_R6 z7ML84gG6YN6TQi5_9wmQuUVe=dM>>S`aVD8!U9Ceb|u92sF~3|iJBfht{=ip>6@an zD;HQZ)bA{&8ueNCe1T@d|G@OoJFYBOS)2ACTMw8$CM5_>TTghO7sotl)gl?~w?Uq|W!R;==^ z)k1^N#%jgRNmbonm7FL$@0dQCYLId-_m^)81bs1YUPoEGEcmp(l~Z!Cu& zuF!n3<(Td}3zAwc*ewpSZB^Ke8+V`0n8b2@U}y8p2)S0Q`a#NiZh`vqN@oMK3D`ab#GvOIT~n z+iD1zwfddNtS;xpsWnN|(X$0K1g3}u=vpWp3s1g2)K1aRv`{h+8Sf*+2I-|d6}YSP zKz)4Z=McX|)h#}T_MrC*|Fe-&kug zlLSj%^sgKtkNn@icExLmd8q=1L3JCU0-HChqcv+9>PD+Mk6!<~)U;s@KQM6PZ?Wd- z{dupG9u_G1(-yxL&v8x^EUB!Ve&QdY$%=;Fy^m#APaFFP`S+etqiX;s-O4DZ_o}12 z{42I(QNWS+=H|5)Hb+|9u*KG%8l@l8*RUP1+nL>4s{vT{`^+I-0}FHYmv$H`P`}Nk z=Q|dzhK9ED3eHv{dp|eq;LB7fHG+1>8{{a>SHZvFR56==F=GOzJHD<;F(AxNyS?b1 zIay_j+TzFHv9aVWQSU9=+=7yn$OPLp-}54o21>FA2i!#Dd&R99+n{)SqNnGBQGC<1yKM#OW7V>3 z>YogY>lt2bs?9g)+lu8sHVhaPH1e8bm*+h;@IL!&G_eu?N$o7d#z*+8Pwjg2j^#bm z?acB@PX#BRp4ZQ;B82ByTSj|pr2ZL1j-0qfwO}`9ZL!_b*lv&t55$m(#DAHzNsksl z)?3qn051E!_XQ6*ijXAS?5|4@c^YgW^)@3?-I#PetzvI~eo@wJ{{)@~CBEKd86Q;6 z?i-c}P?~Ll9`2-!gT!Me=%l@b5W#B5#btGgFALZU-W(lRxYp8H6QINnXi>THX>U}g zO<_J5Z1pUCWlW2?^g_)u4kFk$lBIIKqm4H-v`Rt{gz3%R!2M%G1Wt8*Q#u5Nz887U zd|hW}TY_%pD`U|HBdD=hypxL${P44ss7F57yHdQkmmut3BJl8{x^T@0c5(|w|GCLU z@+2tXs37Cf49BSH6XBV1t{3@72DcY(hFW~kvfN-t zzsl4tUEvaHjo@}lx_ZU-@1Ohu-5r}}@Av$dRtg@_LUz$uLGhOWgJDOHQK`JbX$-V? zpc-avarZqaj*Tcsbf_RoG!S-5k2@rL4~i9S5|Ivl8VRZE7Wm!6Dd{xv=Hz2_rk+`A z%g=|aEA~dD<@y_XFwXqc=B5OPZQ#ZaLOB#@bj`4i_p7504GW$|ywP1;U;K$wqiNR0 z+~Qj0+)QqcRnz$Zzqrcbb@A+>pG7-X2@$CC_Qm^8;JjB}y2S5q?w_c5S`;xQUv1E? zy6WfbFiasaA>i!vjUr2pxpp!Lw-{+PA2eXnl=<&?+3g9xTO#Fv6$c5~x1^k3Jq~C9QJf#v6 zQFUnq7=7zwMh>0a(BQ`~ma83SF5;oA&HH~j=1)pQTPd%Sbew4%7%QVphSaf?Pp932 zV;z}lPyirM+_!G-_uJycG!TfB+pLW(Fo|IO6vIj7aKW*XDNP7}gf#oz6oggiI_S?`j= zcbVG{17$@67^XEukJZI*Gr!*^Vz8Q=8UjZWb$JLzH_%1gRkQnXAkRKU*8NCd>=on2V=<~gj6y!qbEAFVFB^8 zJ_{%hS^l=W?7dCH1v?d~FKsIq#v8mL!+zUC!<7(|RR@3Dj;0oClEEtR>}uCSq#IiD zabeokjtzvkgYaXn+>l%Pzl^Lq-GZ0A#2wtGfZY{?rdzZvCnc9)nl*`pw90vA)y5W$ z)VUTYRV2oH{XcjNn zw@9jU#ZLHPGl!_;cVX{j&EohQqV7>j55qCK4pYo1{~z@9%#;{GqiY=4!;Zk{NF$iS z1u492>5+45x#H!D%y@7^CtV4s`qQyz;q16O_C5 z^VKsGt)G7^apDF~Gok`tz=5&uUX;&E8V72Zm&p1-3I_!w!_ar~`+I5hXvn)c$$;a_ zBp5q3pL!(QQ9R(Lo|(p&a3gk~7m8kvK&)r`B}kqvx5}IOBEBAb_BABSiUd6SsP*P` zxLm&J;yd;xsZG_@C7#iqH?TR2u|akICns?_^8cJwE=sZI38 zus0;6ikG;3FIIgY3*}fjtHkX`ayW$ErQs5?=_!r3CY_aE`rnn~Hbs})mp#i0YJjp- zE;K%1BF^^&AHz(EEshM4s@@BKC)7f3O$$1WcY)$dLuh@|>I z-bqS>-^tW)a!y`bcf%s5#{uRy`TFYvX)QOsNWaQyGCmxi@bhB>FH>N(F#K6vx39?h zN})Mvx%U30fR`5Xqk7|m>V+x#ZBO6M7_F`d^~Bw3$K6`x+Dto*JuQx$zv+3mM3R?t ztE0IHv0LM5SsGc>^0Elts%9jH9$W@oCDNl&WPMow(v2`TG40PIaDSSj${ zX@)X!ZCiKiw^w5Et!I@T@l%5uFed=`D#JXc!VcRW2mo-W7*N~C2zp!0X~iM*1WiH1 zGr0}&-_qt#k~Is9=D}`X_02!#s$#GdkT9G;0Kg+u*s~t=e5ygG?~-eboJx1S))44s zKGw8Oe=!u94=#>}fZsh(#~;A89m>p5f!m3pzqgHQS7Y&$5$WTtS_nSW%4u}Qd!@@# z6Napqj8F^MWlNJ3ZNMD`cJk7f5N@j_4(hZUx3O`c`y#u1oE{|k6w!w|+V#kR*JJlo zfogcbjAdw<`wIpfPkBt}WVCF?OC*Cu6oVMtoSq1*neaz&63IhN-Tk-(0O01V>ypNy zM9g;5OAO?sgZ88px-M(>_a>9imn&0OMKNf=ewIW>q*O^eGh1}m(HecMLUS4>Sw~;~ z+Mj=YMX1Z5I1)MUOUHYeKZZ7z_KnMYq^Dg83opl3Y)|)-BIDg{g)1{mK&nM||EuGd z;yYa2SsTe&OX@{I-Kh#So0Q)w0d&(wCFAocKB)SrcEXG0ck|ZRJ12F>lz&oi%J<@E zq>`F)pngN=ax#_McLn9*_uaW9kS`hbGYV0#saTh<`R<^x`%|P! z&W$nNc9w=tA}A|ItA~<7GRO7HBP*F)Gm<-|XRnDGh6Ev%@XhUcu4jqv)7WMyl@Cfd zPEm}XiR+>Wwvf&=&fVCFj^0)03U+$9oQG=8Ysa#ku z;I;g(C&Gx83Hcgva}wT|4;v()C5GERp6{*R`b(p!0Wl}i5#H7_0n0MMzt}O7@rX7VR$=T{HV`h|yV80^i&p-2ioUmErvd&e%GMs4V8pfb zIwrgLX>ot3_<%akhp>p@*UyGWL1Piz!FNvx2m7P)7tuKs%Iv5-9KeTZQ_AJlCNP;9 zZ2ORWFO%LYM_K)^!b)Y$3`Sl-(Wg1z)iye4!;%IK%;w08E8RfZeU>+iOoN0avHxVB z5}TsEJ|YgDGSaSb+1B@^;AL&1B`)$V=61h(t36lY+4t|7%Fr<%@3K%@98J$h3(tFS zYFaTSDvt z9Z^q7CfED9n^H>MpW{ipep$etU|@&Avu^zJMy5#Tx$dvKPk*eDYV`Ac4ght@3_@F# z$kC=(RK#yy60YME5^5(DpElW*!TtpDq**TY^+Y$ltt0wzRkW305WL6gBCwurBlbHa zq)vs{>eDq_#;GAU)p;V_KnkmtXswyT>;K3DF0=@rj{oT?d=2mvz-LRn73c3qTznUt zR6e`h7~ESXw;kZxVmSQ=+b|pQA?;!f(@r{+0PZ~cRZj7TMY;Q>=J4+0_@ZP2ExB=k zTErxQpu?b%@0wD()=2mIQ5vc)6~(`uT#noV&9;CaHU}BJH?hUvU{lR^La9o^s!zdn zGs0rZ+|O5n5}?BwMm5)XrIvch`fG?#i>H*94D2d@L&Q`5bzQkVs>HF@R7NgE2_-ot z{Q4pFW#g$8ioZx&frlrfz2`_`Ihz{xgP6^Wtr(;oF{1=(%0>sscqky)0p|r?(Rz+7 z^RW@4MHymCvAuDTZ@jO-Hl5wj`s6bf{Zrv%cEA5yvAGeVu&qi!N`ix{=+u>CONn$S z${-8~Wq9UzsPHZoBp95$E3)`2<;%v)m4oXkLUbs4I(UlZT^1aP>wL4ydRRa zp0Pnm$+gHnKZi-m`~+f_@^EcR-`2EeQ~{LF_4ue~y1pKCwB?1O(%U8>Pa#i@gbY*H z7gl$lKq4(!BuB=(ji;I+`l}<@HL^@mpyq>%E2x6}Y-XbTD(R27Kq#BK{Oj*nc&W;{ zSl9YQNhtUvk-4U}YV*6bfP=?py{(=lHSAh-5)@dl!fes9ai1k*bfYV+c2;ZZ!f{iB zCTa3$HI!ZBrf*d5MQWJr`9%ivO28dfgFbzy?s%3fvS@A+Gt$sg&(lbk@0+%>n~LII zOYRfdVx5?x`7)~xe&=e7S3ACi)7T8yJXDCnVuQ4NGs%B^=n zKjFtt@s3yt+xt%r-_qZ`Fj?s>&Th)wy90(7uLXpafto5pf4E`xFsoBJ4*+kEbM%D~ z?c1~?HyN;3Iwim!RTn(EWJe6n3+xRo{4bEWOj26l^r!?rh?wM*OdpKI%@fT3W(vVQ z-w98>>YaGk^k|V0<-jgFr6-&n&I29be2Xa5nxVWtqMmk^+H>MwWc+u{mp5{BE4f?u+H(az?W}h zG;PqJTMu3yaXZg3M^}G;SWb;rLI!qD2b)okWZ)V)UI{YiOcDf@exC~&LjvB>C*b#W z@J}3^rnR^i{mXVL!;I4IdCn93uJ3by@NS@par&}#hzgL3yefV&bGXjWPifQc$K@6k z9_QmMiD})%BTy4n3m=*EIS%sl4Mi${W!hW4Z3wWp_M@O#;n%K;Z%-hBjdCM}RSdQ| zE6X(uAuNue=&A0oQMH|eN~#Y@L2%@>k1r4+ORR;Z`li zM6g_^XMmGuQdRh4!nCdBJnO~?#4OPX>A80MFDtQ!5VhBNeI7ac`Dlq`i{ z5QAwCJ|gNYgD|;xzr^PRPY*ptzxckt_-YSHy^yuUoFh%vjcEpiN(8p;P*}*f=q}*Fvn?bKye2?rrVa{Y@2QX*Q7(;Jik+VY4AdLM;UhUD za*T0-fMRJl-<_XJQMnOYyT31}4+;1topkxt!e1$KrqttKByifPbn{EU%XgWOVSk_N z^E_xjrOw60$Jc?#mjCZsrwxF=f&w?~2>dORs$DQqj7Jj1k)O}=Gj-kt(zXGl6$47$ z^hdpo=Oz8hIE5fS-82)J>Rn?$g#=a#I+#y&HPt_X2LcPSZ)k>MC%VIqn|BV%8RQ>^ zbeM2u=U{9df7K3dCT0r)?m6J~bK>5rBU4=fz#pu>31e?i*W21&Gr)#?1a0p_o}8D~ z!>IU+352=jogZE(NrF8a#wN6Rgd16PCn5jFb1taxqtD7Z9y8>bd*LyraJ^sLWR+wr z(_`&>{^ZFNY|v&L_evqEVIvwc7>QElK~aOr?h2QU4-JJYMy_al&?a6@(+xPGHt!x#6pDde)s-KadW zX&l55FEi>h(%EsbPiY*YXyIwV@_zERG|mf#SU0>k(YN#z2KX71W6(v<(V-^$oRm!t zzo+Hso7|fk2l|%gaFcZto_QWL(71PD*&p>i^ZZ5fmVQ$W?~S^)zYRm)_jH1*<#FXD za>h#5CwN*s(qK$V)g2_ee))*^{B-pOTTiJ0uH@iu&MO|u`haK`r$|;AY33N~C5qbW z??S;7ryZZ&A3!G8sU!>LJgW%oTZd0@`t29hlvh*bQd){yv{es$Cpp@fTHg2%j;#6< z>7$3#9+sCf#1S6jjD8<+FN`xU5E{Gv1<>?p_IJU7_=7?}Iy6<}Pk(dLroyuvShNR2~oLqAfj7H4hM7ruT#}q10`r)7xkX7(jx$ z?1O!e=?PRYKNgnjU2ofDcXF^N{s0e@Zx42EQ#^@YcMWT0!ohRR*>}VmHfDo~AgMOx*B#sbpF3 zBAwmeM=s*$xWmPqTQF5ZM`KagwyZBWZ{_~zWUaMlVaN?=-113Qt=1Qfe?oXaLHo-& z(VR3MMfycN3NjigrbvqnW6%4Ws-Ldj6s$H$7Kgc_2%b4vJoNP+yIh=L+%khKu|eG; zwG|rN!PXZ$KA+x#O!8kS`+i1x|BY)a=y!3nllhS|ooOYVC{5~Feu9_JHg;DB!PHtC#>uN6HXq5L! zC>Tts4bK!gdhds9+sJqQ*W(VUEtNW_*!8yQGSnN(6jK4ot*ISk25SBm&-)*w`hH=B zuHbjV=?>(+&^ZC&6%kqom=1X&LSIkBvjA-myS`BA69KGj%>6lYibXU7ua{|n0<+=G zFLWWP%007hP2oS&DgI#VG=+Uxj+J}6W@UFgTQXwu|Mg6UejL&3+>%no+){=G`hu=? z2^LwQ2-F@iDch5WSGq1a3*P`xF;~lZw?}gL_vtPPD_Bnr$O( z@koW%Sh;sTjDu|BBKpF_+~78XY~!h>3L$6tb6O_Y=Bhoy5%V(;rWv>uin(U~C+1^Obq7q35<6)yZE?ez94 zP(h_)z>hL9u;kn!Mm&Sv-%sxIvr3B^T?Mg#xTd3zvv`=K{o>q6BTivdqLCslZ0h5L zuQ=nrtcQt9&NC1rgG`J?$BD08@};xXtXPjC>kJ-WUVE;5$_4UEXG^kbP8Kgq{nrI& z3wK7D!PGdnri|Je1-8b_xUQN3g=t%XOT7T=aW7Ee`=nsZks8&QHbzTr{ZQBA?B?qC zH>%C5+p&eTZDTF>%@33|wMosc=?GOu#f`-IEJuZbcS=3eA)Sz3vmDaqbK@;F=R}k6 zI~zn!rDk)pzF(){8Q{?rGiNt=h-QOj&7|o`D1#JIx6;pE9WPnM|N> zj%%FhzGhjx?!!(5J4f{U}B(C%vMb&C9P9C8QzxL)s zyIw;HFO}cX*?Gm4J-aUq&)|6ew^n<{SDgl<5-I`bGbP3a-NL^~MQxfAn}ZDR8ImU( zRUh)70xt`~ZTSJIO%Wqc6@K~@1JSxUPq7KX_>y(@oz`mzX>;OjQ~leDWSdZ0Bp7z$ z4pwGC0NN^USikvv0(4h4U+4HuW-t0vebTU;O}iarPpg)h{ErKF#vUMoH#HcsY?3!i zaTs~)ee}&>91!K<=^5R%ka1V@2x7G{V&Zs4xi-q)RU7&Vif-+ z!R(*%4&A6fx`M-L%1CBYV^Tx}C}5QT4S>q;ed@Xpz~^6A8}G zTbRyrhkS0}F=K7EQqLnFm{Fl;{4Pvsn!;fZ*s%hhj3GK9f1O=J1-%*NgXiI32aT_E z**T+i2EdMJVB+Id{j@Q3?CDB`5UEw0Dgggr@b6nz#F-zAA;-#HZEeHzhSS^ZjqwC1 z`Ae2fC?34F5QO(@;3rqE-pH z1gNTB&^B`QsEu`D`eI$<8OTSG4$-5_>VKV)-qcXIsN;&*q<{&j2goGz724*-ECTBT z79W>f7Ki;2YUTnqvHbl(&kyYS@Tv@>PTgHNaM&+(s{9!4(`0zUx4$tg&xFn~W&g?n zygxC*CkcR17Tak+`$M8n%THw&7cUAtTEtser=Ua2K)aC$TRL>;DAweTZ(!k_r!3Zh z`FOzWty>NICbxPgbCKINm9IP@B`YkrcFQw2gu#AkHpb>er8dYj^O-UfX+x7$WZhBf z>FBAL%y35kvXjBt@{!=3q!FZ7-Hd|tP4;E+cU>RHuiIr7Qf^#(>1+iP^&K>ia*Rd5 z%dc~$TKC{8zI%SEzR9-blX@%ud)F5W4{o&$W^JxE?bUZ$teU)xw>}u0ed&E8f2zC@ zjg&ogzB@R_{k54ZF8>IBB}tImbyi@(w`E>6zJ|I+NdmJK<0YW>(4dz&XQEUy^5-1$ z{u4~M&>6Z89>vbd-XyL|AbkKa+bMo$uIib2IP9-tr#{2GQFh;D;f{%Ko*%M0aH!7j zGf%LLLU)apX>E9&;*p$RO2i;^9(Nne@8BBK<->${9Kpa(n5OKdpY4|&MkJ0E%uOm1AQV; zZ;cc#V@<}lx4bFZ1>&&n!*nck1l`Uf&uB%#56Va_OrzwW3@Z-2_APdj4(%_!pwh;q zBf*aOffgqgricNrvx2sdpjIcCG91H&2THk$3w{gt6k_i)>7m!0@eRCjzn1aUAjCJJ zB)r-}7*LtklJjBRQbux0Oj6?RtPtRiQbGO}klz~>f#XwhN-O@R-yZG8XSR*6)|;2? zVV0)y9|%HrUxC5}tpX{t3zj)OUd94-bQSE8yYLs=?jNwn9#x;Al&Nxq;{SO1>aeE! zFKne7#^{tGpmYeMQ5>M;qX-TL5>rrObT^VxBGRSu2vWKc+W_fq6e%}4r9-;jFVF9N z-+y4&u8V&-pL5QA&biNha*w9y(=(4K5%9kAC|U!Z_^s_LwVW`3o76HX76ZiVJ#F-`!)38#~0EPRb+*c?5$;8ni;I;q)Y8f z&7Y2V;At83rH|!oEd7<8j?$aHuCt8qDbD{h3#}@?bKTISqqXKYysA{4g4ZE2N^c{1 zrA&^*(lffCbn2hKL;T@+^b{X`3c^+h&##?8H*{uBY|xLa=o}|01@--rRa#Q<)vK{P z3ToTgK|cEK>ruA{ET!v%?E75*L`>vbN*5t0`_@MIXiu=MEf>xh-w)t42Jf~C8)vlb zzoe!Kn9i&nEL`OW+gdD98-nwT62cBtG-0PD(8xSa?{k|sYP1Gd1eVKEc?PDM`(lW> z?UFP_-IYX8EqIOI?H%`Iv4O-DWo_~|&)5Ie6Cq|PJ5gWgw-iE zm~CvK17sqp-2er6qvZ+6wvP%~|I63bsH>%YPbU>>9Qq?KXeuASFN~UuMHJ=5I80Oi zNIYjk%H5rqih>NdD$Zpm!ZcfDRPz$=e;OdWIg-&e|L3{tH)+=Hv%~DiwuN*&xFhz+ zrR@x5#$vx^L3v!0bJ&xUr}fXbD9+BdICj4+rBfRAS){Yr7EaB%qdc%&fC31+A@TXb zLO}_al3&Vj;B)y?6Nb6!l2zQ3o?xW@&SqmF%Pp{7Qy$?ryC^NS%P#&#GVH|< zEwnh6$+UgBwRXW`{CyT_-OoxJD?4GHp8mVpyTdbZ8=4&7s#k~?kl#;pLm5Uon?EnX zJxkOmEIqXgN;hwLj&XR_2 zUc05$%QP2?2>(88dap1kGg8c5h+7Lc<5fm_7a3*@kC5*L;H~d{V>n9blHeO+)1y>E zPW&TM#xX@vs;y+*4-Y8IwvMKUq2`}+JLTCWl*^T&gVKb&`!G#k{g%e}qB##F?zCUD z3oGJ(aYR#1CbO2iI=v9%u^OQf8Let7_R?0YpQ5r?-~hn53a*Bnh^-1$!KgHrE2(C( z*P-i`0IbB@4{+bMoD#+AvgWUfx(WJl@01Ud5IY=>YnI_&U0n_9rn3`yV?w!G$bCmu z0ESZ&t~yWD*_QzQ*>0Z|#Xyy}{ufGYq43o9&Hoy+GR2#m8;gjZit&b^yh)jm@>H0@ zHiPu(r$<@knZ;2DD-1!xwEB?r7S%5 z#_&q_H?rCt!TCi;-ffW}^ROr}f-Pr(hdv)DdX8|$ZPp@(j9pm#|0Kck zDFlZacckfF%Cfx9VrU*>doA7f@`iR5SLlz9bD7aH{tm=JyUS?3OgEOjQ1Nfuold@t z2`smr&&r!=74PQnlDRuNhl%jU!7gT)?oeV8V#6s zDx3nYO#<}_2d!$!-7j}@$(7kGmyep}0;^gB52f|RlJEClB*0_QunE|WrYDIZ81w0o z#v+DTM97R>wE^O>5NZ%ma=w?#G@69pUrmfbsevXi?4N$`Yk6j00L@7NTZb#A*u=ZF=#%mO?UlzK%(c5xhCX~D{;P>PQuUaX0u73D zuK12nZ9$nY-Bfh1l<8Wt7~82@1vo|=m^wHZ+u*|&8P-1-!R05m70&$n)msUF64j_J z82eDuxde}=eQUqSLzrqJVW>w^?BaihuZQeACM}W@Y!=^Q;c9)HjylS=yvYQ`(dz)J z1}4M6I3>g3b*@|)#y*Yp-~ttU?jhkCI>c*k9VHl&HMe4G%p>9{l9n~IAdAd-HpK;C zp0$!uMO-Z_akMJD^LLt}(Y9)AE<3RBB;&GF`B6yHMY@F*;_IA}BUOjN2wheCYv#Zf z@BLq!Ulp3`O@y}v9S;3ig3$5!^A}+{s)_0sDfsg%QQm7fY*FPpNU@$i9A883;Q@#w zXA>%$+sZ@OBLK;|9|e>+=-gezHo=%m#Hf@-QyLcDEJb;zBPX%1S&}j5ea#Ew)XuQ@~L%lFXd8=q>R)O^&XLL zoyQ|mYqJvF| zVSnk4Y=#z>B~qF05{<&~zpZ!b8Zv$qLhV!zTXMYBWB}Ul);K+_vQK-yWu3KKk|F$e zonYF&SM!_e8yJUwRLVGiG5|A@6OiG!;%`uPC$@Q%nqgf12cL=v!Dj3ZVm;I5b&y;+ zCIrjMFo0>B^%xNeg)BdMX-!-j9653p|*x_nrLzk4=0lb&* zR8!>PKFOBm!(3B+SJT`vY=^w2caWvn(XKx-K?e0zp+h3v+y3+IO>t;p;~h4mvJv{09tFYx?v_bcnha zhBNqjRsmGxQg73uWd?mt1ktwG^8nSPwCK|DO$~TAv44!^cDS3`nXyg_&u82r>3d4< zyD4GvRy!YpCrf+}K|6*`yN4c3L$NMgLFtorvYu&=8_<1Tsg`zA>yYL>h|{vo!xeFl zx@OrVF%{C%?;;a!@I@7xwZLeb+{+tIyIau)VfAPUJU2Gd8|{o1tKdA)QfO&dp{Tlz zWpIA;Eesi(>MB{yHQ+@L(qiQ%0IyZ7NTciiLA>79*asVkN15%%N1xR9s6K| z=EFZq5AOUemSa6%HPj5LpO*e7goFah5&iK@Pa!_Lt;Dl!A-UcSI%b8=8?SB!*t4t7^-B{hvMGlFhf3U9$fbcksK-|P z#G2m)V~0Qo$5Fp|w0qmeMr$g5m%)u2%l6btu3i4Z2Y>OC!eVQ1qa5nChM|Tg)YNlM zylHY4awSex#bz%lkaTX8^C}8mS5!hBvS8;M!q|iB4j%-5^TjM1krN-CMw4ymMk|vX zo^{UW1$vq!=A6at=HD2**hpQVp5CcbuO({ zz-0D^qnO=3rZp!Rn=v*`u}29b_y02jUr_>Iv?x|+lfMF<|wuq~C`4;u3 zuGISALN_C%iia&<{530%VNHsyb*WTqe0N{d>D=x*Pe)&?0%RGsU?DsZ&Yet<@ly64 z$eGqW-r}h~;%biA zj2NNHx#?L%HTg=D9lK8(0j{Nro{jN5<9XD!$%CtX$##Exj_R3ev)&EgSTd>DaiL0< zw4R?n!Qpgi-4#^k3RX)@RTgPFLo`@9+~A{y6Qwt?=CJ_({ern*RhZ-C&&?2}OSp zj`;eWJ3(G*Uw1B@zB1l)C04lng`g(O0b~R%kN}WSHk~4%nF=I2QAx4^Rv+@2_c7UY zH5?0XxLe(}tJF_R{PzB_8I4{G)#9EwpWqYQ2_?43 z&Td|QFXlJ?||pt=O|rEa`QjkDZfyUhyLw|Fbt6H z?xIam39x;$W2i4b5%$M&Qo6p5*6y!sOWCu;jN#5O?yqV~hlKD*QYZAwAv6sd#bMCO`)Qg4g6 zN1G*NeCKXAR^S&p^*v}XUn`;_jscMe(y$|IO2ZqLr|)E-1r)K z@$3lgxzRfzTv^NdI=ysxg~oOT&T|%331GMR{0X6p3Cj~tB}pIRRFr!_fEEC&;d9#j zBx5OfFM>Xh&ALMcQ6~ow*EWOUhk(2@^=+`ibw%K~@0rH;>CN+-4LaYC_KHXhIgZ(U zOxDd77KXY54m_5WHAMm$Yl<4VT~GIvsf?b{9{s{5;<2!{C=gQOpBUOaJThNYjYDEMK652Ja(Hy4t@bVwS*3le<7J+$h_tB~ z$9z?iZO#BFurz>UexJc6{NlR)o$dCU_xbe|c01k=1W5P$vKMCi+G>yGW=sUB*zLQv z=j46O7$Z>cohsYfXi$o;xj3a z5;pIAq!sdZ2#Eo1oKTcB6KC;iE4LO6Kk5<-f!6 znILH1zg~_pgdXZIR))kh!?by9Xi;%!_BNjIYvN@taQc+TX!u9l#+nwsYxwU^`(Au~ zdDG(I)7dxZdN*Xl2^S8R4xGKNnDNt{dA@u*Y|TNC#6B94OO?~?5o*jDX6yz|CM4!3 zJ&D<(4oN&7zAiF77tuMl&=n+c&^hGQW_Yw3ec+ODygmiY9s6K12XzU^ewRZyZS#FXBta23OP1Ap9os%udToS zV|id3)s5;~t);E3^RqTg;>{TIP+2J~)BgH2{R~530*Bbjc${9F+%LnN zn242e*MbUN;7#lO^yAxDPVs5Wq=c<+t%XUWElczAImu$Kv?B9E?;a|C_k<)e z)!cyi#CMScv(jc~$%w98S?+{?_FS0kWB}0*3$nPIo{u6niA$gu>@f}SFi^cuA6${Q zEtxTTc0jmT`3$P}?38-j{a?fwT>c#r8`u!6E+bZTN~1+qKJHDX8)IJF|KPn@NCH4E z!R&B9aYvRCr71=t%Kgvbzsk6ye~-*R!iD4A93A2MM7AC%PIzuqpJ-mVb0-%@qzWcv zkmH9E3CnF8U=Yp+&|PlZ@ItwK%jyo}etBEND1deuXyTCgl+xm$*A?&26>i;~@y&Ap zLk;XnI`^JjGmOf)2Gr%(gXaR8^X)G@o2ch8{~Ya>I}R4}ei!3qZh$w|A8hIFE+F!8 z8lCq~hnZ-MZc3;cH+=Uu?Q}4sFSSeM&|1+3)joRjqU=3urDkBk`$2azhK5H2EiV`8 z(xQHd7IWNR>G&dSTV}2r*RHt#GJZIw1;+*A_~u6eu4<8fbjl*Un$vc3az3NXhsF4B zl66V5jlR5U%%5BaYi)atzV!;m%7l8EqBXw0jipGxNj1!ePDAnaYSOq9yv;)^d-08} zh(%H!jQfyjnSVD!sTH>U_fv(|ztslP0h2UKt>QL2qZCeoojlo7a;-~@`ZjtC*uV-3 zga(2$p`}Rj~5bD8%)K$75p*2J(xhC}GF(H~Eg*%%EI|?#zN!gV(uR0iQ@$v1zbkMKI>pb;jd`NbiF&r%>U@mYy_YUKf@sjm))x@@- zJEdKJghsLoY=Hq>a~HziM!&-Gkrtrf+a8lel<_1PM{NGha-$OTwD-koqbnmM-{cB< z$`Ds=V=Z|wdjAU(dcwZ{p$fXWbO3)^EA%cV?I|R|)Gu));imXMjx3<6xrBm$Y>(-6 zB^vH=+K2(fWxKZi_jnL?b!7TeRO2@;l(SbGKCyAy|2p(Sg$ggvj4R}9OW-c0gQC#R zsuP3;vlv=Ie0`9>43;ikof(Jo8QoU3?A`nIBsj0ZHqo)7a~dEQ3c1!tbWTU?y~lgK z(lCn6ZYbbUl$b006RyHVoFSX}wa}z?w%YCo$qadZ@%}1GY+wKJDtaSoe0Zok?34gN ziRqH>3Zp&dZCPe1a6xU40&as9ZE(#GAcKgRR*J?8cp|ALIW(cB#SuW5h&LXv_d7Bb z0$|2>2VuQd-|FZ3{}{Ew_V+TrfTAB}VKU0R7>uh1ZA%Qv3jahQYo}i6k1;mWy#{Bz z@};X(CbvoaVbcdlWbVbE%S=DlVDsE>|AbP96OoK*2fHm5Q+edwPF>C+RLaj&kG47`9BjU;csP-kw3!`c?4-f#sS1=-Z zTnO_VC_|{MlmQqaPH?#CPGv1A(>HIm46Y(+cqVXp0WsAa(v7vh9kStW+8km?e(9Hb zX8#mKa6qNYx8c&L#&!TriJIS9v^%QYT3L@ zJ02zxadI=VuDchi>|KAqMb6V}eRW4S;q)h+*PDl;%w(*+Ig%3~jI#S}BJ>SxfGJ1y}_a>r!+xgPIO*F6WOQ8Ae zNVy^Qrw790^#JR9%dBaZ)Whj?h;AT1qh-Ic+LgP3ViUlRF{#^U{z~5)%%Ub?D5>N< z&PJ*RFq<{t^wKZ*7!0#t-(@s-A8JC_tr+A9*3rr4u!CO75 zy(8It4Xe9DG{=W5ph{a?(-vK7bIy=S_fMwHqQ`?}a~gG7jY(V=!45UmBRhv8Gp15A z3Wz3WimbwZ*zo&_mwC5l6d21}mK4k{@PtUI*eisPvWUN#Q@90ygrjl7JaBi$=-Z(P zDo6pOIJ*Pn1X%zXU`a)sTx-b;b;u0I=U_7pRwv6x|4uFU=d&r=sIsfyd;-0^q2yh_ z?{i+V(hJSQfnx6{t`^cRbYIwCq3HZWg0GA{2fk>Q#H&jnD9V|>av>X05K34ugP@4Sjy|0=DDBXJ zJOKD`vwDk!Y}c${^y(o2af)s)uV2&ut{1Hl#Kw4=7`S>h!xLF>&i8j;*)|!)J1iExrQn z!utO>J*Urjur4e*g5lDKc&5A;lQFB|Niort(fZ6`{DfAbS z{$rBA)Sstm^;rX)K%guPIq+U(A;^7@Ib#MQc{;M3-SU%Knu^(4W_ zG#ibNdzaoa09`WlAuW^Q3-$2Af!0UB5blJBCtrzH#19*_?WCq=udGRJsW><)7a{o$ zFfz*XQJZ8I;t5@Itf1!(*@~)w?oHi?;+c0pSKP5pZLkxUg$wWW*eQwoY;Y{>ytv?+ z>$og;-G{T3b4A-;a0!VuXk{|p6SGp_OVtzk;1D|mdjiI0k*7S;11zntrDo&EWSy;E zK#BsCDs$u5P$*fVHZ$%U+k7}+V(j1ItBc$L-|MOK$9spLT#RRL|1h2#MF6)$2{VP% zI4JVE;&H6#>5coRnW9c^yaT^<0%+TC2O7&~#bIE(v6ij8dm9Qn%(eS<9X+)ZAGu+r z$T1bqgiuS?x)w=rrQSNEuIaaQ*bub=%VVeJo9%X^ZCo;D#bH-QfAqEGNvf`C)TtG- zB1>JP1??8daYoKvKCtmw?D=$A)Ag$i&O6ub@7n_5$;30=i`eAA)yx|&&yfv|zWq}P zP=q3xYo#@P>^ASf00A<+l}}Wbcc8T#w(NTAbJUh3DnKYaUlLI^BiQ1n*e~Xa)FZSpzzIWzXaLil z0Bay8o_F*GQ2;mgrV>hm1cmg?dV{YHY6sx_#-0d)@cE!~Y6{}oqGR}?=#K{$I$Kc0 z86=W4_KeS_5fiJ&83oC4N?B$ZrKyR2=EWIBx$)%6vG6b4Nm?fuxc)*KQ_W`>D)SLX z8h38X8%YuL&m%=sAGIj?$wX`Cq*mz-KPtO=hNkaH{0e_3DSI!b#ucrhWfU9X`+9ui z`U_6cPMyF+GxbUWyc5bZbH_<6XZknEQwZ*SvVVLhkJURE4a(y;vmO)SMxV;bt@EO2 z@}>j3S_PRr=eE%gCRJ9ukesph0blFIAI{ENJH;Xr!zAbxY$9S!$ zl>r%+#LDo!SIVUI81?U~ur1*5bb?ji#yFBi3KN%6=nGiY+>EwROdc8KE?C;BpK5Lz zX;zY&VX(6FCDl67AGGAaVx4Cv+1n9Vt}i}h*s7V!BL35$g}I(gd?cy$@#~l3(D;@& zh9LMWmc-OtBV_f?!!miLFfZ6Z)hQ?gq_p=e8vPNcA%1#dgKP{!L)=G6eOjZsCK?u~ zRsOST<0?((SkCO<69q-E(in6D12npPJK0oXbCyrD(QB;<+b82B0Wmp;%XZ_?K()X| z>GojAvW7eTRY-m$#`Iv}|F&jC)^>K{T*K`hyV{Nw<^A|Hi~6*Vekm!oI!n5Di09-g zftUZEuXdJ822VY?*_R&<%S(7jDwKnB1~;iNb@h`8KncId_Q-z=l9gO@c70 zz&9&#;oyJ{#m+z9v9ZB>{8Yli@BY~*|A_Y9orYUP+7EV~aGYV1~{@Q6X zy1~&Go1-<@ioWU2eR|aTrpxSbI=Y|qyN!w~<2r?(&6>5*c;h-l=Tj__JK2M8m5v>P z3(64mW>PxDZOq(T@UFKf1&A-p_r-ieoT&4SBxX))l&+RrV4F9RX=l3qGrD*M+;tkw z5|IVz0>10aIlynemnwGRW8%$$1WH$fFD>)5haw!_rNp&`0yX`QRW&i%NER(B8#1{h|Sc7qIfO;>laCa z5=XgHGEorl>cme0I`!!qsW-4kRDVXgIb#}}C zJjhj}2Tc0Hb^bqST=W7k=(FRpPz}lFTClQ=RPqhYr;=RS)&A##io1ynhqg!DFNMYmJn#HNTlQIZBISmuYg#tE08k${QBU849dZ5I%)aDM+<8rx6u8@lb9e=4QiHJgObSNW zI3H6*QFc?li_4sx0p!_6w@sZ0sX^lJ^G<^lucQy%+0A3DS-001?7C~Uh$vg^9DSdF zw@2#Nir$Q(z}RY=23=am7LpNd@h7FNkZLY*({F9I3GU)Q#BH?Tg{eu4FjS2po9Swy z4Qn#ubAE(81vT7}BMykLzi!dk)B85pHNcA!@Jk=a@=g?3b=osx48btIH(@v@3-HmAr+35oj?u6S z(8Q9MQMjZAs+{NeAQ(}dNdY={kQmC1!`YA-HaP?5*q%~ZoF0raSZK@w?adbUaauz5 z_HD>y-o(svyxR56i_VKVMAe;^2TfiX|2A1EhaRBsw^(W1u-g@mH3%jGwgaUU9E1Ya zlUx(H2ll@^w9x;~i@%cJ2UrwuwrD406#3Hby+>I$0K)66wC|-VY2jafMroDwmx1(t ztzuyH6ngR{aLzPv4Xe2p{9|rXDh!p15;g_b$iq+)$q4(5Yr}>ZX+ig;+yeARJu>lx zR?Lr^ET)$C(Z#23PMdCY3a|m}q0RU{b!gD^Yu6j@1d5}U*e)d?OEy1q4VVC{#N+_? zO>Vg7eXDC(qqkr8^WZ*UR@(AeM<@Mf!OI4DV7AqqBo3Gm=t~O1Hg6@X9JHJBTsqJj z23KSCoj_g8Ac_vdoflYx+I^XubUF(=*vpE{-Pu16flWJ@#dfR|^_{;heC zbBFR_ioM{A9`S!Gs*2^32V{LU?F_QRoYLfil-&g zqT!}$4_{I^#F}qJWPU;JN0i@-C{ISqgRo1FASkPyJLm5RR}Bf{xq}_%IqjLLvv(JGPI)7YFV9_t-6dfI8=uw24Ht;9#DS;YEx`RCdau^aF-x>h z2KakT_r`q9s?Ngy?=J>gN4D}i!)7|!s37j0>T>tDm)G2EkCY>T6&b6q_g0>kGAY=S zx9CXfw2k#L9rr(mfoeS>B%_Af6A>{-F6=kLa``nFAGS&Q%~|}bpYysZwlZ1$m4d8r zzvj|{Syz@SSV=s6(I7J{(6sB#a|0b2F@fiw%K~@lo-oOM1ng?@8c$2HG8fK720=1e)jH;w3wb7}usl?0g6IP)ak-um>} z=mLWQC|h)`uG0Xy@uB+>@Qpsk%(k+9DW-pMZ2+7-$i`Gd{)Q3#&kkQ*oR+Q+8vcik zO>DZ2d2tP163D6-OOzc^5P#}uM-Tt_EH0I61g^F4KX)wm78w1r(EJ3-y|yoF0}zqv zAETRZP_w}Xe%@j*%|)V%^;pUh1pjLe1N2J!v3auf0E*VIE~NwN2;=E%0CG*gembi# z#7>|*Zp!WQV^T7L;MQ1u)`i`#pzGSewt#yB45b0BA$1|l&Zr<b4Yf6hrDuV(Z&t#Niori(?mz$o0OJ^}Kmci z=o%1C&xQ8NC0`Q=NaCn>Qvy9ucc%3GWe){kO%rF2*=rH=Y1eY7!{+_`PjAm)=Qg>Y z{iI~elVf%r%#>&i38cXCQ~gdUd}ENZe?DIbkiiCN`&qJu6teq4*}Y@nED;4Or?8eJ zcvm_t^(3S{8eZm`kDqL3hTz^4h_V1-I-BU8DhB`PpImJ#1mwY~CI?JZSNTX;n{v5q zhC{k_y`ex~Ob%zVoQOO3-(l;A3kqDq%_YNrPOChWYmz_EGKt~k56Q5e^qr%qkLWEv z5=zMOgM}P_%I2Cu8~k52GF(tE|Jy{pE*I=oU`YonpBg2jK&1l<%}aj8rbZkzTI13FLgWU zO;tP`p@AjA6X7Tiw+P9wq1;P58D%h1o1-EHUf57v*)%64y8^VsZfP z*pDqH;^}GY>n=Y)e)hk8dcB43u{n&%dqVEPu+3}9Do0UZ26_)zQ#J!tprcs$)KqxE z*7)3bUA9|JSMHg$pr(3N+n*$b8-I}=5h1owa`{=1-AXS&Qu33!Rl zKEVsJH-Qtu-BwcfdcL$j;H{>5H7yQ1St0LwZdR}?81Pqh0Zo+gNFJo==({&Nu5@dz z_rFd)oKwu6wd`Gn3C6)x?hPJPt^CPUXNXI?>&h9en4vABsyTSzk}?%y4LJIN1mPH! zXAT&QhH+FxbZT2d6TOh@w=w|~84A4(8z3``%FaBa&ON0yqJ1!P0`~^Q!7QL8_CG;* z?}hd4dV{i~G_n3l!Rv>V9^>owEy6nGf=?iB@`?y& zgDHq`6Bafrav+zsyZHEmDBI4D&KWBI$G>Y6`FYo}4B}pz>S8UQj41HWv}`i3W-Bvx zi7M769d#T9GGQjThW_^p^w8}vp=82?g|Zl;FvQ+q!AbdZQ(Bj^jBco9I((d4r?q)O zoTo0Y8H=)U-3;ae(qU>>4X9=VHjrCXLTO|&ctMGd>)HT9X>1NOdG9Kc4kRtF219(d z<)J)WtKp$zxlD>q+~G0AA5h#JN3x3Hg5h_^1SW`2R##-Obu5|@I@71K6mv9AXRl$F z^u=gQ;AFTu=GaNzHu{_98M|2W_9D&A;A)NEamAR= zv$rLDkq?pFx(&$MqWGC{hp~hJy%@j|?KuHQY&B(ql0ZFK2OooQ>5*4OhbuU*JP-Jl zLh57eYimDS14!UWT*rSk3ay*edXPxWy%_lU>IAAyvdO4@RFzClkpv?yd{JxO5HXW$ zw~Os(%NATqrmLQAJpis+`rlmZ?htMg{%}B>cKfU+xY=D3XR!k^y>$@-AW@){DECYP z0xTB<;jQZws`21Bb9*{^7bA*g&&PZTWLM^}&TL`Qrw7 z;xDOmf%8(KE^9MEHo<^42;h1k+T>0eZOOZ{UB|b8F2M5z)%O*$rs}qQqHrOBQrnTT zs1fFB<#53FR?SEsIo^C}kPlM$GFc zNupYt`CU7@))qU$y7O50Do7K&GY32_amnppal)LuCniZa;)sOw<^DP0>7 z_qtf(Gsv<5Ae8h!O99C9GTmo(h>4Mm&P`{gl;E}?|C9_=gM>*Q0o=$N?jo_o+Yp}< zXYL44&9)CzV9rtu)_?;&+Ct0_DY=>UK62V>m?zexzBvpJpi(ceW979jJTEC#;ExC`WE z0`3+A_?v0(Y7|%@+|+1@OI--nR`v(%*Ut~eB~fJ${b#I$h8TiM+lKSp0UNL?LXSuk zb78`N6w@MR(BZ1DV&|QWQ0_Iy#B5ojI6$Qgs~gE}q8_xCQthsMdo2gw9^}F;*zj3* zPX`E?8u26O{CrZvV8z0h?rgo7rQJnU%&{%)xF$x+Nyf_Pf#UteqA%YUWygb4o3W2) zzpX>d=6XlYYUJN&RO-M>|B?cIG~-3eb;;3EVi6U-(&PkuyONh-sK+uK?;7VZUt9|? zV14LY2T(<`Kh((T{BuH|4kE|>Z0rYrCE zopLiBhH=hmSS0Q7XF~oIcs;oArGKpcEa8`tslY}`YS)n`xvT~v(aB6N`SkelLQn(Q zahdgzEenHHgsrJ;;K#CxS=xUNj!E8E>YBDEl)aHEJ+3ev4g3YF^0ysr;-WSOCNhdZ=Z0FtDI-Unc%J+sG9KlmMAmV zIIEiE0w2Ii?0F?9OIx7TT?4MkwG^D0kf5up&5wGT#I*j$k0&62_x1l0T@YjQw|tF* z!)eCL>kW#q1(Fzx8NFwUvg6=;Qd?~@5cab-+=jsYG1w+cvdVcaX-ka75w~{?hh1D3 z)Y7cfS$vy~}08zgr#2+h~cp8;8H^?GpbO8-;cJmGJWsleX6 zN!N)lxx2bezNF4nQSaN!KaW|^ErV$vj)vQB9qYdY^{5NFmYoepeqs49WM+(103mab zi^^Qk5#pfP4=a!SK#d0MhV>yI=fGj(TtGNzY&M<3fwCL7F7X-I75S|U0)UlmE{iQH zix84Am--m)HG>&{5dIk(Ug+9bZj)Cfvs(Zc-l`(29rw?xi&4O6ydJ;SrIKt?=7pon zd2z>Nx-Y}7+|D*`f*JcnKR62O;?%-vPW+#KU|=4VV|-y?yl|CzKNFn7S)m%0VU z)}e2>vWY@5$%q_QoT3=YE&G>L6L2cQ4&|mmoO7yacb;Xl@036Rt+E^LiGbVvS-$GD z>^5ODnfO{62f{&4S^;=wzSaLTQ=ophg>1tw^Ye>-cDm6!I4qahXlYK+`B zg5a)~N={HK+oM(|rt?C1+v(pCcpMaR;jDM_J2I@zr$71ZE9c`W%cNh2`pE}V%KtVt zQe^U%X^&Et0-ZDsi`ef;X;HW3{hgf+15GC!8Ca%aBGq7WD5*}2U=EGKyS`ElS#bga>aXRT!a9W$LX+G`xP6Hb4xd1rQ`pt7eq0}B0Nil~Y-QR=#hq7zF1Qp{S%eKE?*zsiYY|g`r+t}<@%AXfDqPQ4R~xgqv?mYtRd(eiTl)Ir`ET7G~kfZ z-PZPs5_Nby7secA?-fy&C(l%=J$iHWOwvuzK>}8uT1}0vzrFQ|PC@u}LjCA#nVGjq>-b{P!bUUnFzSnQ(H)RfPf|%Dh+=w?%I*OSEA5BL(1f;hL1Z+Bi~vmcZg0Hi*LC8NQ~9ze6ppWIc* z9Yvh;c8w!68;|57asY^Llvh#*IGI9X2j6_H=6 zpR6i%`W(Gucpc)7#-M_;^o}E+>c$rM;~Hh$h%&!Dtb zrZuJF`1W$sf#rK0P2^TN3?vrpLaqy+2JMS`r)9h$RoJ{O621P1P96tSGh^y{@isdpc4+jL;`H z5{39b?Y;Rs)P3|n&KS(dGGoooSR-0&BPlzPEnBvV6j{cMHI$64K}6+tXDd{8Sz=yD zO&LpdQ-svKWDKP+qYahP=Q;QNeqZ13AMp9{)73?~F!MUg<2=rJp65B`Tp9z-bkxs2A&r1_9P zeR!i2CN2`)FD#W%7oI4F3anbtPneXSf6ec1U5+uQWB1&}t8gZpDHkS1(biFg_&9?r z$EbE+mdgr%W=oB~ou+&Xk{;gP#p@egs^+icjuW|4Q> zd`fa!bD%z$mug9mx=HcB0Ggwinhf~pD!8rFbP;6Iye3m#JOxkEuBvT(u(gd$1?LBt)@LpPr|J0v=8A1tedlp4m;FGdSaK)|_Y~cC zb+h0TGIZettCeR36lGRgrmBC@_ove8ec^cvK2e2VN{z;kF*3hZc@J;z!Ctr4D_r|2 zuyCJH0Gs?%xRJf%%k0sMkAouIzrB}}?6_j$Q}b=aOZ()z=oXvXO;UBqv8Lfi*=5hB zk4&@)s1ic*n+{PZMjg^qj>DO6rn9QP=s&Q7DWyD&Q@ow3EAQULObgrvk%+AUf<@Y7 z47k8deGq|`Cok(Ux$&r6*u<1b?TyBePHG|uu4yTdio-eGYwwk1(a;5=`2AD~(q^MD zAa+~d6+poG%!hI%_Q!&eJgvV4oVto078J9NPe0FOW4ZAKOvbThatR!WI)LsC8N};8 z_Y_1df<)K-H?FIm`Z&kGyyLW50LY%iO-%?Sv$&^oqk4o|0+#D1F?o#JH~4oENWI+y zY@@^3iM3$d=D^A0i$}i$-Os+;DZ2f}Bg?D16_%2PcxPN#yNk>odl-T9HMN*WeWG(` zqc4=N^eL;JKL=LIX00>wKBe|zT^4YZfQ7UQiX6nZND25y#fSc2;SwHKvU?i4c)ZIV zSLCGB&cEaCw8~oXh(pSxoa|kK)%ytV;p8Z__TwEEoEIX)^5F%)s!nU4n~}7=d)Io8 zZqj)@$JwJ9?Izwek^Y@`}fPx1rB$Uk%67~DJ2k^QQqs>ldU(ghCFcSS_ray+hC-aiG zVHOM|zaYK%NN@CN?UFu$bPQ&#gK!g3C=g8`9mqcPi5;huYDb!$HI>_fS)vI!bk%tx z^6yL*@fAklV*GN^{gJ4HQsm`pCG5hVU!-q|v{?=ojtrk0kn_EJ zRCTd9fB5{Qc+y=zl|P*#+UG_TZ0}yLI$O8HF}ub3NY#~eDu^_F?tB*(JYu!q$p2^~1{-lwX4*F44CVea8}^qIyvBADEu!MMNE>H| zGJT9|I@1D=m}!v*m#@dc^S(RkcN%8oh_kJ-hl|SrNENu>Lic zSXWoAa_BPDY5wEh<%DUAiA=6)f=*6F8c^-zE9?K4Ed#&`1rxy;PYot3LmT+WvFb#| z**jpPXlP}<{(g_Z?tyM#>n&qxTxE1Nv|aTtznCDqzk47F3(X||n64?w>w{VMMaJh& z7786FkW#aAre}9330?{&z0j7sxbKFoMF|Qi2QKVB*}<(e1iGp~{<{kj#U4N5kWnA8 zX&&Zcthl>;sO5JTKfOIb`7oc~aeiH;;jYZ++L}$=K{5JzqCK3(rYJLWzw!kZ;)l^s zGP+bx74jW9{wDWc$x7jYY3b`3QH4fl4cd=)*>RqE56ib>e^~Ey-%Ur@-i@^Qtb1BJ z>6E$S@jbS$8<*Wib&dt*J@t8YGuP{3kDSrn7Aw8Rlz5G$sqasP40DXce96*N zPdz;9@(Tl1f@KEu+n()u(u=ds7g?AxPHRQ4X@NPb$p=}Q6F#&kqaca2lYMA_Tbljc zI~;gfLQ|sxWQKI9%xY4ek)Tueo!Pn$N=8~*MYRn)WeU`TTRc`~gfBy{7g{^#PwOv# zZ00KY47}wyy@l{65dBfGG=Y?WBELNT1k6#Ygu=e*X|kEAcPCVJWm8oA=e^FwGK| zIIJzeTTz3VHWjz;DYq>Ad`1BvmZlcz{xYe2B|%bkrhw_W(T|^*$2?B{sdj%c)#lrm z0biWVWXi75wYT{n4)kK27Fa(yHs>BHf+i=CoZo>(hBVn7eC37vUSQAgDHiS_>h2Yc zvYb-!4|#Y`wtJVaTJ$H)1+gxxN6dWfR3qH_!!I`3%@lgog^E~vSATw29EJ-Up&zaTSXFz+O}>}LUa6ke2u|=_5ptI} z(Yw#WL9|TRq*}AT`K|(}9?+AwLCva>5%1R20c;&jM7+w6H|hsFj)!bMpoz13D9PD3 zw|<0PTn!%o)iM9bqf@bK@kY(VAvQWvCGxCL*KBkYj_}W4v?x^~R}JKj#p~g&Oa)M} z(V5jEB+k+GMgc|MMvnFB6EZZ>LUMnLm7tKzh3` z>(kgs|A9V*?@OnGAExdTB?3h_3|bOMyxE`e@D6D*%qk$}ZCg?xpA3AAIlSPq{UY<$ z6Le;r$Y6boJ@$yy2Tcgf2j1lIz|`o#^YpWhxisZd(_%y z6V%sYtsM`_+*(hx^ZOI!u)0e5hj)Eg?!?6vvVWo33xT8zW3>x~MnVEnR%1@`?!T^R zI38=a_{bE|mX9p>^5}Hk_WMf38P4uTXB8WKN0^;;iuVQLn^Hf88wwxGvYVop3f{do z`DNVkUb}JdlpS5aHP0$@MI6kL#TcrZM#AvVj#NbC;cU5OHT9cQRmTv7rdm*%YjdIJ zW2t6)LBY;s&%6k<(e)|N>ekTA)`kCc5ZhbqCi^RDIR@d-qbVfRFnLC~)XUk_&6_O_9DCk!md6WOCV3fsxMXaZ&q4gwftxhQ< zx72dk?b(t*v+eJjMqwxNGrVigFvAVS0G_>)+u-3$FeD0onh7@M+N^{he5{$! zyo+j_5a<9@F_MhFX?{qwjN*T}Gc!M43;4ugG`JsVfK8JqGB6$rBs5*9zrXWA@u9DO z49;y_dH*N$O5Z2J#js6#`#;i=UsA=&6iobO2^X?Jpa8%lju!;0i4oL%nkgwSR+9(@ z8+iTDhe1g9=_YekuqH(6MT&K9`?PSq$Ego}3`sB{Gpw2GUk+pRG)V ze(y`d-z*ckKp+XOjbm@#$n6(6gfhfmRs;SZH^@3#8-|px7$AWruN8%Y!qF=Ho!@Z0A!?Xf}P6bGD)?ZRlhSy~uQhA|^UnmLb|J-KUiV>+K+*;_dAjBiI@0*Q#kko5#RZ)L5J4U!E(S#V)(CihpWzuSxF(pkSF-6k#=IR5mnx%W`&>Cb4J zA3RQ;!qaI_dEeIsPxAfaW^lqhT=4H|0&O24Q@jyl!=(aEma%^(!-AUIy{nv85WT|& zU&SqZciPZeS{s93{T%Lndu+8B7ukrCTRt?gZz=-kNO@n6L=O@w>r)Ju=0*%A z?K^TIuYY4f0(w0{hQ&jLGWMC`Z9?nS@*-Q+g*T)g=APPfTuzVM&d4GbpQds3I(XP--o{GI|lcFjNU^wfB? zh+o#v7CR6Q^uN$0d+7SHY7#w4zOGoCW-Msj{X#((>0Z7R%d%mftFomNoAc z)S*FTQwvm6MuZPW=3r(UZvF5Pp6$E!<4=Y1Dpsem;W=yl+sUZsgec~e=8*H&QIMH0 z_yZm62blvXO`&09#G(xo2p?>>c$c$e_rH<&hbWAOUH(hyyg+RrZ`zr;{^{@x*z8e8 z`w=z~bmO$qIV0UUdnSm)W;X*fGv4syZ(YsfsfbjV8lhteB+KM_e8Y`PCt+5)2#W}= zjBANJSEcV3uia;>a?fM?<4ac9x28yJg{w-t)9%p()7;oWdrZ{1?RzvYVnSJ$zqf1_ zy_l5AnK@TD5w+_|FWWABBhX=)lk<&ZRy1}}usu1_K0(Z90W1L{xN0qIz}e}QqS`Cn zQnr07ZsQT#98+H3Y}z<|ZKKQ1cGbCVr334Aey97(q4X#Vy~fq3?4JithkfW!%0GX2 zj9$9gWbL#XAI9#Jj%uG`B6_FGXl+jLpuR-2Zm`cl3$}Gxv1iQst#h0I47$|KB#GLK z?X2~4n)n+{lGv*md0B#Zb%R)a`Mb2n$iwjO`kEsTgXKK3hF(_=HqTFibbE7Z3Tt8- z`DGvL^e)zWZTid0Ro!2F6n1#6amH4+m%oTf&EZ&iszIKHaa1g5{ z%p#4eNUI8r;ZL2FuFA%umL&^5xm4j##t9YKX_30Vl5DWh2YpM*M{&y0d25Qv0SfXe zA3mCO@m9-bAv`HxAGo2?TqZN#%eJyJ5erF3wD;an9RA43hB+<6%X62~m0Yw}<|x%l z>)IdNq;8wD-KJhjzcY_=8;dZ|Nr|%Z+NJBc@rF`*?5{fZDe})_$O5Bpd#z92T?6St z-{G;xeksk?{;pM>5O*%1`K#7RFd-R5;sZ(46I^((Nzb$1*T+m)n$-!7x1nY<++h!Kd{ijjcg^8{RxU zWH9N|hfQ5tn|5W2hIu}^;SXDm>{S@ou}t&EB07Qm%w#M9!Y_s3j+%%Jp<#5NP5H!S zdrIm~7g@T>go;p*KoQDhe}+MJ5M?D!E;Qc?b1DuoW;cX+3C?`Ylq~~Fc+}%eDPIS- z91P!H4!L^XVb5pF(kI7mm95y>y$T#&S;}c@F)Ml*B^c8iWq-`XL^3YcGAL0?GJZDu z3P)6%O}O07Nzh(>z%OMU>HhkpK|}J}D@jN4$))^VQ-4vm z)Ys|qS{OSh*SKw)`P3Y)cOrxKIrIKM9$#{&!THfEddo8|a_&zwI zNpPNiuE$AB*864DKys)N=PiBLB*~?fVNxm-f7qQJiIjlm_kF}ym8D-zK7<-`qMiAT z&s-W(Is8ZAvB)Uvk|h!+(odS&E$Loig_-%N9(*Ys8xbe@+0N48!@2E?X3AEC_gR%!bj75=#G5x8<-v@~>bf!m#~#zFdu3`* zdrWWMXLx(Yby%M=@%N1Hu#DdrsryLCf5Y(x{C>t{uajXs)gqQE@{fYv%*cy3Bf?QL zQ}X~(Mm(o(6PAg-Q+@$+JCeLst zE&*9DO`=qY7mc1`9he1S(rOf%;^b57)&$Xlh|oDC*{uF*E@mdAeBAu9Ti@Btj{~@X zU2>t{YB2T6IUPSkF{d!OVf`;Dd-mrX?VqQt#tEY<(uof|RK)WUg^_WX9Tj3KogK>K zz29*)d18BeS8NkXW`;kuZ|fdZ8a8;FbM9oKq+GOpQI2k|bAVg074vXc6-?)q(p;}I zY_l$YBJEi=T|Gz_=+rE8j}9I&u6jfla@Ld|iwu6U^F?qwZ?oq6FoQk){JzM7z`c70 zPWdhqvli#Q+9F^mv!z2G>GqnyS>^l%aT|JgwF4id+V~nd7=QU~TI|-wQX(L{={O%pUY+$gTTRDdzHhqY>|C*9G zRglg;aQ_-7h&rywO4ycyE;k{6b?;6GI9)>ffrWlI=+T?eTS!h}vLa$4f_St~h>YY= z>~^H1VD}L#WW@V<$Xnkf?9}|N1OcnKaiXIZe^pNo>ZP*Z%e93RDGByDAWVh)IAfGOe9$!lJU7FXI2BBmDjnX=zb>hvd(;SopG0S{%7e4 zIY#x{vj%dPnqO-g%1Yo*B0?i`q)%?UCl_r|)@BNlq`}ke`m%S2PMu^8wha4SY=rN% zv@Ii@9kS(%dUhl@O&PStOJDu{cWYbkWr-jIzr2*o{#utI^zX0~yDb5=k%v*=^|eMG zp4{7qge!l;U7@byT2N=d<`Da?B5||0^>;d3=wQbU3PX*xu14DARmz=x89`@M{m%6G#bwp`l%r%mMH!r2sDh%7eZp8lX4pLX3UV$ zHlme*m=-#K;;rd&rdIVDE$gGYYQW-tT--diUOuCvXN-a)jwKgS#E`Sh^lm4Rc@yQ6cmZYz2UoO>8sAISD#|NSULgfwP-d-Bip9))Q zM~?(Hy%;~G=GS|)x;c+uKkju}9{3g+)}a0~zstJ&#qK`!yBL97+-E7?V|g=^(C%+# z_l59vi2cN3{yF-Ig~YJ~UiQ;(B*-TZHi`W8u*%_UG`Dy*uTb%QQ%oJz3c9hVp%~0iL&5fL!m* zsgTWXy}86QX{Vq`B!?T}9~&4s^damgpJ5I&3nC=vGVOS+Pyv+FqApnIrQb)?OBY)x zL3}h!3wK0IDKtJ9ds`wuY(|^@!4BuWAcaM)xK}R(ld|6qo4^_AF2}T27IIiFO4=V63dUW`+@_w0NZGa2 z8OGQZj~RQ)n^q4F^LE<#yN8BcOs>%j+ud$kQzTM)sc9_o9$L@NJIv?}#aPWLV0Sys zHsMJapTW>6HDJNpn}@u7??8hs5{gywnIG=B)pkOYIjwCB#h$N+6K{O1+!=gYTl3d< z5Ae?+hJD#zDdPp!_gsX7<3?4z z7k9Ga)CzCP7kurha!@BeN!BW=PE@E*wlA_{j<4|*RVk}B@si=x3V%1d*K;g2HEiVNEI3WeP?f($HhKc3?8o;QpsvU&w zE&a~Ebmx_>X+`VwsjA-RajPU;TXO*Nkj z2fwVHcfYxsVU^LRy>~i4m~Yp$?Psh1q4B*F;MDD&E^y>C@sx*d4$@>h1zhS3;%xe= zz~YIOv9J>tH8b?Wj9YE<$Rd6sO=G89(K5nK#IX}<`mz{2-?o6$KElS#+$z%+xxeR1 zcd|4S`e~9Tr<(iq_^LG%7O!*UF!w}fQgm?Z;1KEo1@3>Y4sTKI5u6J_en-aU)PLu< zqJQrpFkXuy7G2;3aT77t`yF}Ej$f!iU-cwdItXRu{B% z))H?zUN8&)7U15vn!fM6*2J<|wdE{t{?GNW@!8zdeR^SI9kz{?B5%GK&!;~y(qD>x ztZ^POq`&OT%B|#YS&BOG9=fwDBzt)!TZweiWbH7?X=)!V-CJWnLFXiVI5za{6?1Ez z;yZ*2ND(_;@<4a>&j2_R+U0ykrSdW0UeWXGx}?o4(6G=SRYGA9d=Fx%tLU;c1|O(i zJji83Gnxb{a~Dr-Tm=*ee4KQY$E6#r+f)IB%Il0eVU6A_%}o?09a-LF4BA#+W30@a2`NG?1sS7~Pk)e2|>5d$ig%L?M?fGg{=>Jb;!Y&Q)_ z+f>2kTT5npT!E^;7Rq!Vd3aL9qLO2|uq=)bJSd5Ug3Xn$ePG*Oe@nQwjK!O~dM>eT ziYv_;){sott#&az**w(nLzHz*rCMnmNGBsMO9z_WTOwGNwdm=8APm|DB5IwOam_^A zM70jOm#;K*r?`qPMl+{ zt`o~Y-SAHtXjFWN|9twP`PWC84+W_=&Ti{|k!0Bv&z1 zMkZTRR2<4c>RWsi3hKRNfk@1x@nfMHBxgJD=6WqcC%sufwZRL2TSrjevl$r_ZcA_G z2}K3mN;V#C2*px`%hiZua!=@bSW+pP^6HWrVGrJwdF9?H&Q~`b%2IPSll=Imwy^5F z`1LnKr)cjmP(X#+@JqDCgHcP#58rDXhQtPX9%zrPS=MuoU0deZWwppKvUCE)JePZF z>=FgT?`P@zK0+pY02$Y7idA(b!t7bqe%bo3(C!4h0`TEIKiNft&d>MHQl;WVD2_DUlb+ zX&v`UxDEx{V8~_-aPG;cBVa)b*WQZ#GV3NVRGo57VpYQ_l!u-ro@`d*orW^vg%m&; zAa+TX;_YgEDOreRcm;E}C=H27JZ2zpsC2QgDM@-qX_3R^zKpnLw~(+3yF1>fc2q#u z`ksYwW!uVi>_UXH9eG_V7YQ#bxPD_l`*;=_tf z^Qh>=N2ux;Ln{A`e%J)b>guTRy)d)Ag7?CsZOgh8^~0lrjG z(kVONSDiaa*3Z(TXwG_wA;OvKb6rhqkXjyeW*gSHErtCNW0LSE$z1ThU>|_MyFh?{ zPKb(JjV7>LL5U2M0Cv&Yi=vP@j&M+%%7wmZd*(SQCGjpnkl3$F;4dH&3u%f+?jM2) zn63!af&ka%b_C)GQS@f1M@SA7BSC7^BE;iunJ$ftYL1UX-%zW*{l=3aVH zZM@_UKYO>E@!PZfc(up(Y4f#sGQ>{3d7%Aj-7ahWm?strkJ|MfTvV*6dzqj`V2s^l zYx1=mVxUI=q&~wo`-z6Ve3QG)FNUdq<_)X;jz~)wVbE*D84#KXzqWj+>JZaVkLVH0 zM|iS#D9ZOVq!wY$5%2D`;Z*Ld)>KC%9xo%tf3QqJx=05_tOmKVy!GeSFPMV{5<#_d zKZk>GTC;B!p#MY&glC>nrT|U_h3L){+7AgR1NX1Ru|!x2`Tv34HJYMFxEi6E-YlyH z2w{vlsZot!gwh}e=}4h`c4!eaC~kUox57an)*_fvyr0=!^3!BZj8pt|#8Gh4+E)ip zSBool3P%is3mdRmhWQRGX|gac-WgFVYX!Tg_y9yyvVL$HBV@$qeoHnjc*I(Ja$nkB zy@S^xepFjM6D;;axGQBO(~Q6tDSJ>D6?x&;B6mj0=>DZeo)7FpK2Ty_4!prZqxK{>Xbh$#;ab<2k^FIdnl*rKP=T`aL zvKhQtn{=?&viI4DGmR}x>dmnjSf;b(NWC~#0+wrMM|4RMgNG1_+{oGjBu8%sg>I%W zInQl}@|;cMdvR(cy^F8*uiUTUT%bFI)c38R)em8)u@0~BdBXXqknl__ z)olS_+sIWq8!vMYJtH`l`R@r6XE;`U z*;;ajiPi40f~AJk8sC>vdZqedlW|sMgT?_bh;&;K!GQ7U+-GtLQ1t_TpUEha^Gvqy ze9uo!*iDPLa_6TVd_;@5gmgL2$En*B9}9bF5v+i<36Ni^M7r8eqh>OOuzRZ(EjC(v zGZ`1gL1rsMl-&A@64HreNniBj#jnKI$qqLnPU@dZB3QRJZKnHB5KY?E-q^=j`7+L;xr44(2q7sE49jqgb_ zKSTIM1CVlgPML&$odiB!FuQA!Gr`2QeM)CnZ9fKl90LU7A=l*(Q2YYvfNYkCe z_&86ms3p2iCBqlW6(GTORN1t(Fa6T+6CBZMHvTGc(^Vb~{h7xWg6l@EqvN5ZlyAa> zSqbXuK4yX#=mp~;n;m9yGhc*^4YNS!*ub6vK#Rpmy$TJn@oQ;Dd;D&tf zHH#RbD$Mmf3gbDQT`LOxc7Rhqq%o{n>uJ!87GVQp!4%bnRSDD_hBGT60}2EcUo1#t zb&uf)GCBV8qzDbd2wsn;R$Y(-98ZN$3} zuV?+LeP_?E@CkHzj|mAdq5Bkuz9F!U&WbR12HCrl9p(``z;7fI;2Db}Fo?dBSRYSC zqF20bOT9RLQjb0&ZQ^M>e@BSyp-R;8hML>4iPM2nPIk;`b15u7;dn9BCWs(hRs)9< z7KIiq`dhn5Pq{@;5=A4T?6!r3V`(*K6!XWIDq)-D>Ta6YDqKqf8jK@cyyt9*O1# zPMF6`!M27LsBS6UsbLgqVrA;;AYHO(_0#jExr!}~k&i@9`NN)FkM!P+L`o1k%$|B* zqV15tC%9gx?T7}`jS`P%(b@#T&sey`n=2gv)5<&5Uh(!Q+i{ACn_^19-ELwF%}xM& zdO1turc14S1W=*{wLW=EB&Y5%upmJ5)d)U0j8m+H94L^s1Ci+0mIWC}Q0-XWmkk^{ z_cBMtA#Frr0nJtd+Wrnl*O9@wnBIWiiBlC=f7!x&3t`c6NAYp5Q8jA~GBlu?M?2LB zl-x1wVV-erk|8QsVO}$y@48~7Ms)Os8f}2Z zXqXZ=PGRw(^W^-R?;iiiXXY9yFf2H5G;KKWbX&4tHC<1CnZG42;L>ily z)awd!u+(1H`8!0uM?$Yi0dpXZS;6|?w2;n5kRohB^6e3+Nyw)+S3;94t_wBLO>YD0 zWCuG^{IMYG*$+(RF!Ne$;jMBY2a3NO$;noZ{LJl;jVAfoksRwTM`4ngJv=H-MX{t^ zU@LcZo?1ty=j9qtBLK#>SxZ>Y1|_><9kPcx#cz_Y&STBGZwZJ4_)`u;iS<59x;QY* z>ob_L;37+uIYsLhH<1Z&PtYVVK>MFZ3}|RZ)P`IvbM&W_i3Bk&ndMH=qa-*w;=n$T zoCiA*wfA_%GEuO#FVf}1j)eu0!3kP2eyL3 z$Pk5Q{>2tOR>Du)a8T8@;(0(UsV@#3q6_2mw*IK^Kf1{Kf(d>>q* z2OrV)MULA^X11^ug@L|?rqv69e2Hqol1`J8LeyF@J=bfF4anr{90)t|*{%cpz-wnX z0Sk%&T~{GCP2(LKYFcrG_wm#&IKT>&GIN3sVL;CoQ5%-W$Tfy)T4fWZPzt@Z7EDPK z7FQ@?mT;yqv#LoHc#^RyVxwZGDe)ol(Cq1754l9hda*X;p=#W;8kV|x3PLkLn;4h$ z{99lE=^Y*Lo(Mq>u$5d4;5myQ&md$RnU7Hccq-KZsh>`>)tQ=WVo5NAQ2|XnLwvMW{ zhbcLx_=IPjg6(ba$N(jI>1-9!S1rQkm+i>7d_KGeq6PT4uFv-?+9sooqU!rWSls?vk7cyo|S;3__{t~1JPzwMx z=S4}dcf*`lzksHpEj72F?BuCqapj2qmExGf>T3gRXCD0u=(adDB*a`mUnvQxf1Jm~ zR4D6d5uGhe+Gd^e^rI+EQHwA{;o`z$@a-JT(lvDXmsCTY&8kBHMDlJMPPGo(_gzWi)?i2caq}X zWYZK32-*C)w-uaA0cAK{6+$;%Pp}@T@4SQWqvOY6PeAK%*ZeX zE^WgEk2elV(QNvN)GNtAN|^#wh9C-wLbp`}IR%fUS*M>xg8%H^#Knk((4G{5TU-b& z>rsW-Cpf}N?SR(8AJtnw?sCc2mHIC~e?(W_8S3WV#RRNVyi0dwFPBl-FeTrRfeuCJ z%>~e;I5nK*or6cn+&!9&wB?0ZL$ur4@335icLoUmS(w=ExL8yxdHLM!qmtTK{Owrn zBe^zhG4zwnr^T*VDu$Ji4)NFc2?P`hL&@)WSek+@ht3=okwll{$R2SL)Bb!7A_=KqNz=eUUy=ImZQBp!W;uB8v}g#{CrP9|!x3cZdjDA~ zm^PNCt3u2|woqtN5|H?9)HR)~n!`IJr5tPB>Lt^4ZcEg83)$t5yHDuwSGnZSb^w`V zgn54X!=UXD$J>HRC8$_pOiTiNhyx=0AZw#{nG0|Vgz;vqt}N*FUU@Jm0Ce5L*0MzQ zFxEsB+pYZe8|}E$?IFe%1LXe?BRSW$_!qNh{_w#qSd$;?(oflXmuOiMkOBM_2Z}{j z2HdcmWmoP|Cqo5|`k#^dmTl!wF_z1S55q!5)qn32blD^LG1oQinI_Q`sb4~ShlPHL z{`Wi`bUjc@stFK)KZd56X(D7m>hAs#!vz?@7u6pwdfOhqx8o->opmh06Xx=}GA)t> z*+YdW#6XvW_77FUC_PG^6dNK+cedap(V_r*T2MM*H+ls^KPlQgGk56Y=PsELfT!E+ zzcZ~zAFqPjIO1Te2`@E*&R#Nr0#&%hiDKvp+033b;3{%V-HK(21bnC zJi9&rzwh7DVuqQS!9s=b9I^OKnA^UnNV=+)cw^Z7yCSeRB=dnuYyVwrG3!RV z;Vv+~>bHz*EK@C07DfC0d0msKIs4WR_!(&)ECC5d2b3d79(7jfR{y4Gr|8X@42A@A z+>EEhmf0@#5|jc+PIi^30A8~0kxRC1!G?~K&#OE??|dq$=GJmnKq|MY&QqQEzfS`K z)!AzRVPycV&!w?fdCE5oXi2%W?q_MemQ2C{{_=Bqz19mHaVP~x+_-^oDS-P${o$G% z`&-Ryeg)v;Uw%AT+eJ}M=P!N2;gWQHZ^yE5^SjiGX}wql6(TEfIMCWaXadR+8ecsOQ! zDPU0#>&6|!U2hy{eJUQO{s0<~bi&(!^74wdaUrZs8p_ z8mc^90osWuab^O-pih35A^-&=e5{y`=m1%=t`<=W*>Z*E1!_zPpRWN-gc;A`u73}w z=BmM41E$)WQzk=VtSK-v^+vsrqdCx%qF$$k%1GA}f;>C|fro4?J$b;s7O)R2EU4~Y zXCD*PdJ!Xis^O5#HmOE7P6*ZCk1U)DKS6Q84el z9T+@6FJ2k32zt}@^TY9#^{;XQ%!t$wTK9fIJlHOD9A+uCu3k)HLXv#WEd44`bs zXlK49Q^GR9zyu_-at4I0Mk#V5j!;Z*<~l!6>KUOddUG1|PJ+r!ha-9M{0J`hL6uwW z6_P@gKTeV*Y9L##(TcH948q5fse=vxzrkBrgHX%8u-~Xf6hXGQ?*R+it9{9|9ST;B zK$wMmiSCa!WSI%TDhCa`csSX@pyi#ABFF?a0)_5Sa>@zjr5pF#2M%hS*%Ag~Odl0% zepnqc*F41VHzcPdr!-1Ysi9=ux%DY5bbsjIsx_j-p7{kGKt=^%$-3S^Uq~15f~Y1%?;?DoRUvw3x7Tc~~*^-RE`b4PjyqQa@+$%wDoY z7u6F{GWgZ7f_4f69Y&N4d^Oy>fYh(zBZAdHJ4t!!1ngdBcnGb6!vm8HweRsIF32)} z-9T{mQt`Y{BbXa2R=#Nfx`lw>Ss3|$T4DP&8zJS@Q+C8lgE;q`H1E};U>3o=Vl z0Ug4S1w!TpFT{6&huaU6M@vfJAqWB-)8YZ^1Hg-3IdK)d+H>O)zSSDycYayo7k2Xz zuo9Nyzk{T!Nw8(D|K$pQq4*bY_57d9xAGA+VCEKBPA` z#8l9&hjhVv+z@Cv4}>hy1u+DUeSdTyqjg5+Pt@%n9U!y15y!ccdbvGErZTp2tB-aY z1F5s7M9a@*=>Ab8VxRMHOM#N)ZCju#7Ib<>k`TmWAYy>4r87bHpR`-WAI@wZ`bE8J zC-(!u?vq9L<>z6Bz3K3SNr0;mn$~%6OOvO>32x6R`}LXnYmw(a_rHz`;3qe#6TMt{ zW!ASt*|WSd-z14q`#s))QwCz{a~`U_04$Pm{%q@RrH6mL#r$!;RrG#CjaO@AzFUf< z9KX%K=%kw!ECf~zqKQd3;+L<6S&L`Ph>w$g9^~B!$w)asvX%4^yhV_~yZm3lfl1zlZN$eKFU^ikm|4M#I46`d6PLmJ?Zbcjg=Z r=_yJBf`HDk literal 0 HcmV?d00001 diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9..616454a5 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -842,7 +842,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod let isValidChainOrAgent = false if (endingNodeData.category === 'Chains') { // Chains that are not available to stream - const blacklistChains = ['openApiChain'] + const blacklistChains = ['openApiChain', 'vectaraQAChain'] isValidChainOrAgent = !blacklistChains.includes(endingNodeData.name) } else if (endingNodeData.category === 'Agents') { // Agent that are available to stream diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 29a64155..cadd4abd 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -699,7 +699,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( { if (!message.sourceDocuments) return newSourceDocuments message.sourceDocuments.forEach((source) => { - if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { - visitedURLs.push(source.metadata.source) - newSourceDocuments.push(source) - } else if (!isValidURL(source.metadata.source)) { + if (source.metadata && source.metadata.source) { + if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { + visitedURLs.push(source.metadata.source) + newSourceDocuments.push(source) + } else if (!isValidURL(source.metadata.source)) { + newSourceDocuments.push(source) + } + } else { newSourceDocuments.push(source) } }) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 7cfd0474..7e805f7e 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -379,7 +379,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( Date: Tue, 21 Nov 2023 10:56:32 +0000 Subject: [PATCH 25/34] add more options --- .../nodes/chains/VectaraChain/VectaraChain.ts | 172 +++++++++++++++++- 1 file changed, 166 insertions(+), 6 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index a2fac534..143c6d5b 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -27,9 +27,168 @@ class VectaraChain_Chains implements INode { this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] this.inputs = [ { - label: 'Vectara Vector Store', + label: 'Vectara Store', name: 'vectaraStore', type: 'VectorStore' + }, + { + label: 'Summarizer Prompt Name', + name: 'summarizerPromptName', + description: + 'Summarize the results fetched from Vectara. Read more', + type: 'options', + options: [ + { + label: 'vectara-summary-ext-v1.2.0 (gpt-3.5-turbo)', + name: 'vectara-summary-ext-v1.2.0' + }, + { + label: 'vectara-experimental-summary-ext-2023-10-23-small (gpt-3.5-turbo)', + name: 'vectara-experimental-summary-ext-2023-10-23-small', + description: 'In beta, available to both Growth and Scale Vectara users' + }, + { + label: 'vectara-summary-ext-v1.3.0 (gpt-4.0)', + name: 'vectara-summary-ext-v1.3.0', + description: 'Only available to paying Scale Vectara users' + }, + { + label: 'vectara-experimental-summary-ext-2023-10-23-med (gpt-4.0)', + name: 'vectara-experimental-summary-ext-2023-10-23-med', + description: 'In beta, only available to paying Scale Vectara users' + } + ], + default: 'vectara-summary-ext-v1.2.0' + }, + { + label: 'Response Language', + name: 'responseLang', + description: + 'Return the response in specific language. If not selected, Vectara will automatically detects the language. Read more', + type: 'options', + options: [ + { + label: 'English', + name: 'eng' + }, + { + label: 'German', + name: 'deu' + }, + { + label: 'French', + name: 'fra' + }, + { + label: 'Chinese', + name: 'zho' + }, + { + label: 'Korean', + name: 'kor' + }, + { + label: 'Arabic', + name: 'ara' + }, + { + label: 'Russian', + name: 'rus' + }, + { + label: 'Thai', + name: 'tha' + }, + { + label: 'Dutch', + name: 'nld' + }, + { + label: 'Italian', + name: 'ita' + }, + { + label: 'Portuguese', + name: 'por' + }, + { + label: 'Spanish', + name: 'spa' + }, + { + label: 'Japanese', + name: 'jpn' + }, + { + label: 'Polish', + name: 'pol' + }, + { + label: 'Turkish', + name: 'tur' + }, + { + label: 'Vietnamese', + name: 'vie' + }, + { + label: 'Indonesian', + name: 'ind' + }, + { + label: 'Czech', + name: 'ces' + }, + { + label: 'Ukrainian', + name: 'ukr' + }, + { + label: 'Greek', + name: 'ell' + }, + { + label: 'Hebrew', + name: 'heb' + }, + { + label: 'Farsi/Persian', + name: 'fas' + }, + { + label: 'Hindi', + name: 'hin' + }, + { + label: 'Urdu', + name: 'urd' + }, + { + label: 'Swedish', + name: 'swe' + }, + { + label: 'Bengali', + name: 'ben' + }, + { + label: 'Malay', + name: 'msa' + }, + { + label: 'Romanian', + name: 'ron' + } + ], + optional: true, + default: 'eng' + }, + { + label: 'Max Summarized Results', + name: 'maxSummarizedResults', + description: 'Maximum results used to build the summarized response', + type: 'number', + default: 7 } ] } @@ -40,7 +199,12 @@ class VectaraChain_Chains implements INode { async run(nodeData: INodeData, input: string): Promise { const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore - const topK = (vectorStore as any)?.k ?? 4 + const responseLang = (nodeData.inputs?.responseLang as string) ?? 'auto' + const summarizerPromptName = nodeData.inputs?.summarizerPromptName as string + const maxSummarizedResultsStr = nodeData.inputs?.maxSummarizedResults as string + const maxSummarizedResults = maxSummarizedResultsStr ? parseInt(maxSummarizedResultsStr, 10) : 7 + + const topK = (vectorStore as any)?.k ?? 10 const headers = await vectorStore.getJsonHeader() const vectaraFilter = (vectorStore as any).vectaraFilter ?? {} @@ -54,10 +218,6 @@ class VectaraChain_Chains implements INode { lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } })) - let summarizerPromptName = 'vectara-experimental-summary-ext-2023-10-23-med' // can let user select - let responseLang = 'en' // can let user select - let maxSummarizedResults = 5 // can let user specify - const data = { query: [ { From c716c1972aeebf50aa7a9bc9086061a72a9716a5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 21 Nov 2023 18:40:20 +0530 Subject: [PATCH 26/34] API Key: UX Fixes and adjustments post the dashboard updates --- packages/server/src/index.ts | 1 + packages/ui/src/views/apikey/index.js | 36 +++++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 922aa307..2f7d31e2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1161,6 +1161,7 @@ export class App { chatflows.map((cf) => { linkedChatFlows.push({ flowName: cf.name, + category: cf.category, updatedDate: cf.updatedDate }) }) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index 96b0d1de..73224cb2 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -6,6 +6,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import { Button, Box, + Chip, Stack, Table, TableBody, @@ -56,6 +57,7 @@ import { } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' import * as PropTypes from 'prop-types' +import moment from 'moment/moment' // ==============================|| APIKey ||============================== // @@ -63,10 +65,8 @@ function APIKeyRow(props) { const [open, setOpen] = useState(false) return ( <> - *': { borderBottom: 'unset' } }}> - - {props.apiKey.keyName} - + + {props.apiKey.keyName} {props.showApiKeys.includes(props.apiKey.apiKey) ? props.apiKey.apiKey @@ -118,19 +118,15 @@ function APIKeyRow(props) { - + - +
- Chatflow Name - Modified On - Category + Chatflow Name + Modified On + Category @@ -139,8 +135,16 @@ function APIKeyRow(props) { {flow.flowName} - {flow.updatedDate} - + {moment(flow.updatedDate).format('DD-MMM-YY')} + +   + {flow.category && + flow.category + .split(';') + .map((tag, index) => ( + + ))} + ))} @@ -375,7 +379,7 @@ const APIKey = () => { - {apiKeys.map((key, index) => ( + {apiKeys.filter(filterKeys).map((key, index) => ( Date: Tue, 21 Nov 2023 07:52:00 -0800 Subject: [PATCH 27/34] reorder citations in Vectara response --- .../nodes/chains/VectaraChain/VectaraChain.ts | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 143c6d5b..2f7d09a2 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -5,6 +5,42 @@ import { Document } from 'langchain/document' import { VectaraStore } from 'langchain/vectorstores/vectara' import fetch from 'node-fetch' +// functionality based on https://github.com/vectara/vectara-answer +const reorderCitations = (unorderedSummary: string) => { + const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; + + const uniqueCitations = [...new Set(allCitations)]; + const citationToReplacement: { [key: string]: string } = {}; + uniqueCitations.forEach((citation, index) => { + citationToReplacement[citation] = `[${index + 1}]`; + }); + + return unorderedSummary.replace( + /\[\d+\]/g, + (match) => citationToReplacement[match] + ); +}; +const applyCitationOrder = ( + searchResults: any[], + unorderedSummary: string + ) => { + const orderedSearchResults: any[] = []; + const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; + + const addedIndices = new Set(); + for (let i = 0; i < allCitations.length; i++) { + const citation = allCitations[i]; + const index = Number(citation.slice(1, citation.length - 1)) - 1; + + if (addedIndices.has(index)) continue; + orderedSearchResults.push(searchResults[index]); + addedIndices.add(index); + } + + return orderedSearchResults; +}; + + class VectaraChain_Chains implements INode { label: string name: string @@ -254,7 +290,7 @@ class VectaraChain_Chains implements INode { const result = await response.json() const responses = result.responseSet[0].response const documents = result.responseSet[0].document - let summarizedText = '' + let rawSummarizedText = '' for (let i = 0; i < responses.length; i += 1) { const responseMetadata = responses[i].metadata @@ -287,9 +323,12 @@ class VectaraChain_Chains implements INode { throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) } - summarizedText = result.responseSet[0].summary[0]?.text + rawSummarizedText = result.responseSet[0].summary[0]?.text - const sourceDocuments: Document[] = responses.map( + let summarizedText = reorderCitations(rawSummarizedText); + let summaryResponses = applyCitationOrder(responses, rawSummarizedText); + + const sourceDocuments: Document[] = summaryResponses.map( (response: { text: string; metadata: Record; score: number }) => new Document({ pageContent: response.text, From a4c3250a67ea7a2de10b289b4bc842cd19e194f2 Mon Sep 17 00:00:00 2001 From: Ofer Mendelevitch Date: Tue, 21 Nov 2023 09:14:12 -0800 Subject: [PATCH 28/34] fixed lint issues --- .../nodes/chains/VectaraChain/VectaraChain.ts | 59 ++++++++----------- packages/components/package.json | 4 ++ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts index 2f7d09a2..3799d062 100644 --- a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -7,39 +7,32 @@ import fetch from 'node-fetch' // functionality based on https://github.com/vectara/vectara-answer const reorderCitations = (unorderedSummary: string) => { - const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; - - const uniqueCitations = [...new Set(allCitations)]; - const citationToReplacement: { [key: string]: string } = {}; + const allCitations = unorderedSummary.match(/\[\d+\]/g) || [] + + const uniqueCitations = [...new Set(allCitations)] + const citationToReplacement: { [key: string]: string } = {} uniqueCitations.forEach((citation, index) => { - citationToReplacement[citation] = `[${index + 1}]`; - }); - - return unorderedSummary.replace( - /\[\d+\]/g, - (match) => citationToReplacement[match] - ); -}; -const applyCitationOrder = ( - searchResults: any[], - unorderedSummary: string - ) => { - const orderedSearchResults: any[] = []; - const allCitations = unorderedSummary.match(/\[\d+\]/g) || []; - - const addedIndices = new Set(); + citationToReplacement[citation] = `[${index + 1}]` + }) + + return unorderedSummary.replace(/\[\d+\]/g, (match) => citationToReplacement[match]) +} +const applyCitationOrder = (searchResults: any[], unorderedSummary: string) => { + const orderedSearchResults: any[] = [] + const allCitations = unorderedSummary.match(/\[\d+\]/g) || [] + + const addedIndices = new Set() for (let i = 0; i < allCitations.length; i++) { - const citation = allCitations[i]; - const index = Number(citation.slice(1, citation.length - 1)) - 1; - - if (addedIndices.has(index)) continue; - orderedSearchResults.push(searchResults[index]); - addedIndices.add(index); + const citation = allCitations[i] + const index = Number(citation.slice(1, citation.length - 1)) - 1 + + if (addedIndices.has(index)) continue + orderedSearchResults.push(searchResults[index]) + addedIndices.add(index) } - - return orderedSearchResults; -}; - + + return orderedSearchResults +} class VectaraChain_Chains implements INode { label: string @@ -325,9 +318,9 @@ class VectaraChain_Chains implements INode { rawSummarizedText = result.responseSet[0].summary[0]?.text - let summarizedText = reorderCitations(rawSummarizedText); - let summaryResponses = applyCitationOrder(responses, rawSummarizedText); - + let summarizedText = reorderCitations(rawSummarizedText) + let summaryResponses = applyCitationOrder(responses, rawSummarizedText) + const sourceDocuments: Document[] = summaryResponses.map( (response: { text: string; metadata: Record; score: number }) => new Document({ diff --git a/packages/components/package.json b/packages/components/package.json index c7a29a9f..1d4cea57 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -47,6 +47,7 @@ "google-auth-library": "^9.0.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", + "husky": "^8.0.3", "ioredis": "^5.3.2", "langchain": "^0.0.165", "langfuse-langchain": "^1.0.31", @@ -82,6 +83,9 @@ "@types/object-hash": "^3.0.2", "@types/pg": "^8.10.2", "@types/ws": "^8.5.3", + "eslint-plugin-markdown": "^3.0.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", "gulp": "^4.0.2", "typescript": "^4.8.4" } From 681890a600b804cf116088739ced9dcc3e0d97bc Mon Sep 17 00:00:00 2001 From: tirongi Date: Tue, 21 Nov 2023 19:13:19 +0100 Subject: [PATCH 29/34] Enable inserting custom URL using basic auth --- .../ElectricsearchUserPassword.credential.ts | 2 +- .../Elasticsearch/ElasticSearchBase.ts | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index 6c47f7b1..76b9a0eb 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -15,7 +15,7 @@ class ElasticSearchUserPassword implements INodeCredential { 'Refer to official guide on how to get User Password from ElasticSearch' this.inputs = [ { - label: 'Cloud ID', + label: 'Cloud ID or custom server URL', name: 'cloudId', type: 'string' }, diff --git a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts index 59294b7e..68c8392a 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts @@ -144,15 +144,30 @@ export abstract class ElasticSearchBase { } else if (cloudId) { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) - elasticSearchClientOptions = { - cloud: { - id: cloudId - }, - auth: { - username: username, - password: password + if (cloudId.startsWith('http')) { + let username = getCredentialParam('username', credentialData, nodeData) + let password = getCredentialParam('password', credentialData, nodeData) + elasticSearchClientOptions = { + node: cloudId, + auth: { + username: username, + password: password + }, + tls: { + rejectUnauthorized: false + } } - } + } else{ + elasticSearchClientOptions = { + cloud: { + id: cloudId + }, + auth: { + username: username, + password: password + } + } + } } return elasticSearchClientOptions } From 8358e59df2dfa2e05e6bfc37b0a0821a2169f39f Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 21 Nov 2023 19:18:08 +0000 Subject: [PATCH 30/34] add claude-2.1 --- .../nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts | 7 ++++++- .../server/marketplaces/chatflows/Claude LLM.json | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts index f16968b6..358a15d1 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts @@ -19,7 +19,7 @@ class ChatAnthropic_ChatModels implements INode { constructor() { this.label = 'ChatAnthropic' this.name = 'chatAnthropic' - this.version = 2.0 + this.version = 3.0 this.type = 'ChatAnthropic' this.icon = 'chatAnthropic.png' this.category = 'Chat Models' @@ -48,6 +48,11 @@ class ChatAnthropic_ChatModels implements INode { name: 'claude-2', description: 'Claude 2 latest major version, automatically get updates to the model as they are released' }, + { + label: 'claude-2.1', + name: 'claude-2.1', + description: 'Claude 2 latest full version' + }, { label: 'claude-instant-1', name: 'claude-instant-1', diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index b7989815..0ead3dd8 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -1,5 +1,5 @@ { - "description": "Use Anthropic Claude with 100k context window to ingest whole document for QnA", + "description": "Use Anthropic Claude with 200k context window to ingest whole document for QnA", "nodes": [ { "width": 300, @@ -148,7 +148,7 @@ "id": "chatAnthropic_0", "label": "ChatAnthropic", "name": "chatAnthropic", - "version": 2, + "version": 3, "type": "ChatAnthropic", "baseClasses": ["ChatAnthropic", "BaseChatModel", "BaseLanguageModel"], "category": "Chat Models", @@ -171,6 +171,11 @@ "name": "claude-2", "description": "Claude 2 latest major version, automatically get updates to the model as they are released" }, + { + "label": "claude-2.1", + "name": "claude-2.1", + "description": "Claude 2 latest full version" + }, { "label": "claude-instant-1", "name": "claude-instant-1", @@ -268,7 +273,7 @@ } ], "inputs": { - "modelName": "claude-2", + "modelName": "claude-2.1", "temperature": 0.9, "maxTokensToSample": "", "topP": "", From 88c9514cca6a9ede2ae71365ab2305a3e473bafd Mon Sep 17 00:00:00 2001 From: tirongi Date: Tue, 21 Nov 2023 21:45:51 +0100 Subject: [PATCH 31/34] changes based on suggestions --- .../credentials/ElectricsearchUserPassword.credential.ts | 5 +++-- .../nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index 76b9a0eb..ef4f3490 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -12,10 +12,11 @@ class ElasticSearchUserPassword implements INodeCredential { this.name = 'elasticSearchUserPassword' this.version = 1.0 this.description = - 'Refer to official guide on how to get User Password from ElasticSearch' + `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. + Refer to official guide on how to get User Password from ElasticSearch.` this.inputs = [ { - label: 'Cloud ID or custom server URL', + label: 'Cloud ID', name: 'cloudId', type: 'string' }, diff --git a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts index 68c8392a..a1233c21 100644 --- a/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts +++ b/packages/components/nodes/vectorstores/Elasticsearch/ElasticSearchBase.ts @@ -145,8 +145,6 @@ export abstract class ElasticSearchBase { let username = getCredentialParam('username', credentialData, nodeData) let password = getCredentialParam('password', credentialData, nodeData) if (cloudId.startsWith('http')) { - let username = getCredentialParam('username', credentialData, nodeData) - let password = getCredentialParam('password', credentialData, nodeData) elasticSearchClientOptions = { node: cloudId, auth: { @@ -157,7 +155,7 @@ export abstract class ElasticSearchBase { rejectUnauthorized: false } } - } else{ + } else { elasticSearchClientOptions = { cloud: { id: cloudId @@ -167,7 +165,7 @@ export abstract class ElasticSearchBase { password: password } } - } + } } return elasticSearchClientOptions } From 08443fdb1f93560fd94cceaba8ca103598fb4316 Mon Sep 17 00:00:00 2001 From: tirongi Date: Tue, 21 Nov 2023 21:55:31 +0100 Subject: [PATCH 32/34] linting --- .../credentials/ElectricsearchUserPassword.credential.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/credentials/ElectricsearchUserPassword.credential.ts b/packages/components/credentials/ElectricsearchUserPassword.credential.ts index ef4f3490..c1ac82c1 100644 --- a/packages/components/credentials/ElectricsearchUserPassword.credential.ts +++ b/packages/components/credentials/ElectricsearchUserPassword.credential.ts @@ -11,8 +11,7 @@ class ElasticSearchUserPassword implements INodeCredential { this.label = 'ElasticSearch User Password' this.name = 'elasticSearchUserPassword' this.version = 1.0 - this.description = - `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. + this.description = `Use Cloud ID field to enter your Elastic Cloud ID or the URL of the Elastic server instance. Refer to official guide on how to get User Password from ElasticSearch.` this.inputs = [ { From 75874f0dfa4080a9c9b5fa3240f6b87a72446a7d Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 03:01:57 +0000 Subject: [PATCH 33/34] fix image fetching method --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index ed7baf7d..b119599d 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -358,7 +358,7 @@ class OpenAIAssistant_Agents implements INode { const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant') const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', `${fileObj.filename}.png`) - await downloadFile(fileObj, filePath, dirPath, openAIApiKey) + await downloadImg(openai, fileId, filePath, dirPath) const bitmap = fsDefault.readFileSync(filePath) const base64String = Buffer.from(bitmap).toString('base64') @@ -380,6 +380,22 @@ class OpenAIAssistant_Agents implements INode { } } +const downloadImg = async (openai: OpenAI, fileId: string, filePath: string, dirPath: string) => { + const response = await openai.files.content(fileId) + + // Extract the binary data from the Response object + const image_data = await response.arrayBuffer() + + // Convert the binary data to a Buffer + const image_data_buffer = Buffer.from(image_data) + + // Save the image to a specific location + if (!fsDefault.existsSync(dirPath)) { + fsDefault.mkdirSync(path.dirname(filePath), { recursive: true }) + } + fsDefault.writeFileSync(filePath, image_data_buffer) +} + const downloadFile = async (fileObj: any, filePath: string, dirPath: string, openAIApiKey: string) => { try { const response = await fetch(`https://api.openai.com/v1/files/${fileObj.id}/content`, { From 0d1cc487a7e1e764f6e15eecafdeb67391b9f813 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Nov 2023 23:55:00 +0000 Subject: [PATCH 34/34] slight ui update --- packages/ui/src/views/apikey/index.js | 92 +++++++++++++++------------ 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index 73224cb2..68113af5 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -10,7 +10,6 @@ import { Stack, Table, TableBody, - TableCell, TableContainer, TableHead, TableRow, @@ -24,7 +23,8 @@ import { InputAdornment, ButtonGroup } from '@mui/material' -import { useTheme } from '@mui/material/styles' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import { useTheme, styled } from '@mui/material/styles' // project imports import MainCard from 'ui-component/cards/MainCard' @@ -60,12 +60,24 @@ import * as PropTypes from 'prop-types' import moment from 'moment/moment' // ==============================|| APIKey ||============================== // +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.action.hover + } +})) + +const StyledTableRow = styled(TableRow)(() => ({ + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) function APIKeyRow(props) { const [open, setOpen] = useState(false) return ( <> - + {props.apiKey.keyName} {props.showApiKeys.includes(props.apiKey.apiKey) @@ -100,7 +112,7 @@ function APIKeyRow(props) { {props.apiKey.chatFlows.length}{' '} {props.apiKey.chatFlows.length > 0 && ( - setOpen(!open)}> + setOpen(!open)}> {props.apiKey.chatFlows.length > 0 && open ? : } )} @@ -117,42 +129,44 @@ function APIKeyRow(props) { - - - - -
- - - Chatflow Name - Modified On - Category - - - - {props.apiKey.chatFlows.map((flow, index) => ( - - - {flow.flowName} - - {moment(flow.updatedDate).format('DD-MMM-YY')} - -   - {flow.category && - flow.category - .split(';') - .map((tag, index) => ( - - ))} - + {open && ( + + + + +
+ + + + Chatflow Name + + Modified On + Category - ))} - -
-
-
-
-
+ + + {props.apiKey.chatFlows.map((flow, index) => ( + + {flow.flowName} + {moment(flow.updatedDate).format('DD-MMM-YY')} + +   + {flow.category && + flow.category + .split(';') + .map((tag, index) => ( + + ))} + + + ))} + + + + + +
+ )} ) }