diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a747d0f4..94c929492 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,17 +1,13 @@ name: Node CI - on: push: branches: - main - pull_request: branches: - '*' - permissions: contents: read - jobs: build: strategy: @@ -31,12 +27,22 @@ jobs: with: node-version: ${{ matrix.node-version }} check-latest: false - cache: 'pnpm' - - run: npm i -g pnpm - - run: pnpm install - + - run: ./node_modules/.bin/cypress install - run: pnpm lint - - run: pnpm build + - name: Install dependencies + uses: cypress-io/github-action@v6 + with: + working-directory: ./ + runTests: false + - name: Cypress test + uses: cypress-io/github-action@v6 + with: + install: false + working-directory: packages/server + start: pnpm start + wait-on: 'http://localhost:3000' + wait-on-timeout: 120 + browser: chrome diff --git a/packages/server/README.md b/packages/server/README.md index 049e72617..0b93b6ac2 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -41,6 +41,19 @@ You can also specify the env variables when using `npx`. For example: npx flowise start --PORT=3000 --DEBUG=true ``` +## 📖 Tests + +We use [Cypress](https://github.com/cypress-io) for our e2e testing. If you want to run the test suite in dev mode please follow this guide: + +```sh +cd Flowise/packages/server +pnpm install +./node_modules/.bin/cypress install +pnpm build +#Only for writting new tests on local dev -> pnpm run cypress:open +pnpm run e2e +``` + ## 📖 Documentation [Flowise Docs](https://docs.flowiseai.com/) diff --git a/packages/server/cypress.config.ts b/packages/server/cypress.config.ts new file mode 100644 index 000000000..caa7a0121 --- /dev/null +++ b/packages/server/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + setupNodeEvents() { + // implement node event listeners here + } + } +}) diff --git a/packages/server/cypress/e2e/1-apikey/apikey.cy.js b/packages/server/cypress/e2e/1-apikey/apikey.cy.js new file mode 100644 index 000000000..2ce7a2bbc --- /dev/null +++ b/packages/server/cypress/e2e/1-apikey/apikey.cy.js @@ -0,0 +1,45 @@ +describe('E2E suite for api/v1/apikey API endpoint', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/apikey') + }) + + // DEFAULT TEST ON PAGE LOAD + it('displays 1 api key by default', () => { + cy.get('table.MuiTable-root tbody tr').should('have.length', 1) + cy.get('table.MuiTable-root tbody tr td').first().should('have.text', 'DefaultKey') + }) + + // CREATE + it('can add new api key', () => { + const newApiKeyItem = 'MafiKey' + cy.get('#btn_createApiKey').click() + cy.get('#keyName').type(`${newApiKeyItem}`) + cy.get('#btn_confirmAddingApiKey').click() + cy.get('table.MuiTable-root tbody tr').should('have.length', 2) + cy.get('table.MuiTable-root tbody tr').last().find('td').first().should('have.text', newApiKeyItem) + }) + + // READ + it('can retrieve all api keys', () => { + cy.get('table.MuiTable-root tbody tr').should('have.length', 2) + cy.get('table.MuiTable-root tbody tr').first().find('td').first().should('have.text', 'DefaultKey') + cy.get('table.MuiTable-root tbody tr').last().find('td').first().should('have.text', 'MafiKey') + }) + + // UPDATE + it('can update new api key', () => { + const UpdatedApiKeyItem = 'UpsertCloudKey' + cy.get('table.MuiTable-root tbody tr').last().find('td').eq(4).find('button').click() + cy.get('#keyName').clear().type(`${UpdatedApiKeyItem}`) + cy.get('#btn_confirmEditingApiKey').click() + cy.get('table.MuiTable-root tbody tr').should('have.length', 2) + cy.get('table.MuiTable-root tbody tr').last().find('td').first().should('have.text', UpdatedApiKeyItem) + }) + + // DELETE + it('can delete new api key', () => { + cy.get('table.MuiTable-root tbody tr').last().find('td').eq(5).find('button').click() + cy.get('.MuiDialog-scrollPaper .MuiDialogActions-spacing button').last().click() + cy.get('table.MuiTable-root tbody tr').should('have.length', 1) + }) +}) diff --git a/packages/server/cypress/e2e/2-variables/variables.cy.js b/packages/server/cypress/e2e/2-variables/variables.cy.js new file mode 100644 index 000000000..bcb1ec5c4 --- /dev/null +++ b/packages/server/cypress/e2e/2-variables/variables.cy.js @@ -0,0 +1,49 @@ +describe('E2E suite for api/v1/variables API endpoint', () => { + beforeEach(() => { + cy.visit('http://localhost:3000/variables') + }) + + // DEFAULT TEST ON PAGE LOAD + it('displays no variables by default', () => { + cy.get('.MuiCardContent-root .MuiStack-root').last().find('div').last().should('have.text', 'No Variables Yet') + }) + + // CREATE + it('can add new variable', () => { + const newVariableName = 'MafiVariable' + const newVariableValue = 'shh!!! secret value' + cy.get('#btn_createVariable').click() + cy.get('#txtInput_variableName').type(`${newVariableName}`) + cy.get('#txtInput_variableValue').type(`${newVariableValue}`) + cy.get('.MuiDialogActions-spacing button').click() + cy.get('.MuiTable-root tbody tr').should('have.length', 1) + cy.get('.MuiTable-root tbody tr').last().find('th').first().find('div').first().should('have.text', newVariableName) + }) + + // READ + it('can retrieve all api keys', () => { + const newVariableName = 'MafiVariable' + cy.get('.MuiTable-root tbody tr').should('have.length', 1) + cy.get('.MuiTable-root tbody tr').last().find('th').first().find('div').first().should('have.text', newVariableName) + }) + + // UPDATE + it('can update new api key', () => { + const updatedVariableName = 'PichiVariable' + const updatedVariableValue = 'silence shh! value' + cy.get('.MuiTable-root tbody tr').last().find('td').eq(4).find('button').click() + cy.get('#txtInput_variableName').clear().type(`${updatedVariableName}`) + cy.get('#txtInput_variableValue').clear().type(`${updatedVariableValue}`) + cy.get('.MuiDialogActions-spacing button').click() + cy.get('.MuiTable-root tbody tr').should('have.length', 1) + cy.get('.MuiTable-root tbody tr').last().find('th').first().find('div').first().should('have.text', updatedVariableName) + }) + + // DELETE + it('can delete new api key', () => { + cy.get('.MuiTable-root tbody tr').last().find('td').eq(5).find('button').click() + cy.get('.MuiDialog-scrollPaper .MuiDialogActions-spacing button').last().click() + cy.get('.MuiTable-root tbody tr').should('have.length', 0) + cy.get('.MuiCardContent-root .MuiStack-root').last().find('div').last().should('have.text', 'No Variables Yet') + }) +}) diff --git a/packages/server/cypress/fixtures/.keep b/packages/server/cypress/fixtures/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/cypress/support/commands.ts b/packages/server/cypress/support/commands.ts new file mode 100644 index 000000000..95857aea4 --- /dev/null +++ b/packages/server/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } diff --git a/packages/server/cypress/support/e2e.ts b/packages/server/cypress/support/e2e.ts new file mode 100644 index 000000000..ed5730de1 --- /dev/null +++ b/packages/server/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/packages/server/package.json b/packages/server/package.json index 1fe74eb59..860f5d8fb 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -36,7 +36,11 @@ "typeorm:migration-generate": "pnpm typeorm migration:generate -d ./src/utils/typeormDataSource.ts", "typeorm:migration-run": "pnpm typeorm migration:run -d ./src/utils/typeormDataSource.ts", "watch": "tsc --watch", - "version": "oclif readme && git add README.md" + "version": "oclif readme && git add README.md", + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "e2e": "start-server-and-test dev http://localhost:3000 cypress:run", + "cypress:ci": "START_SERVER_AND_TEST_INSECURE=1 start-server-and-test start https-get://localhost:3000 cypress:run" }, "keywords": [], "homepage": "https://flowiseai.com", @@ -63,6 +67,8 @@ "express-rate-limit": "^6.9.0", "flowise-components": "workspace:^", "flowise-ui": "workspace:^", + "http-errors": "^2.0.0", + "http-status-codes": "^2.3.0", "lodash": "^4.17.21", "moment": "^2.29.3", "moment-timezone": "^0.5.34", @@ -86,11 +92,13 @@ "@types/multer": "^1.4.7", "@types/sanitize-html": "^2.9.5", "concurrently": "^7.1.0", + "cypress": "^13.7.1", "nodemon": "^2.0.22", "oclif": "^3", "rimraf": "^5.0.5", "run-script-os": "^1.1.6", "shx": "^0.3.3", + "start-server-and-test": "^2.0.3", "ts-node": "^10.7.0", "tsc-watch": "^6.0.4", "typescript": "^4.8.4" diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts new file mode 100644 index 000000000..c35a69f8b --- /dev/null +++ b/packages/server/src/controllers/apikey/index.ts @@ -0,0 +1,79 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { ApiError } from '../../errors/apiError' +import apikeyService from '../../services/apikey' + +// Get api keys +const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await apikeyService.getAllApiKeys() + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const createApiKey = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body.keyName === 'undefined' || req.body.keyName === '') { + throw new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.createApiKey - keyName not provided!`) + } + const apiResponse = await apikeyService.createApiKey(req.body.keyName) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Update api key +const updateApiKey = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.updateApiKey - id not provided!`) + } + if (typeof req.body.keyName === 'undefined' || req.body.keyName === '') { + new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.updateApiKey - keyName not provided!`) + } + const apiResponse = await apikeyService.updateApiKey(req.params.id, req.body.keyName) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Delete api key +const deleteApiKey = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.deleteApiKey - id not provided!`) + } + const apiResponse = await apikeyService.deleteApiKey(req.params.id) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Verify api key +const verifyApiKey = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.apiKey === 'undefined' || req.params.apiKey === '') { + new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.verifyApiKey - apiKey not provided!`) + } + const apiResponse = await apikeyService.verifyApiKey(req.params.apiKey) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createApiKey, + deleteApiKey, + getAllApiKeys, + updateApiKey, + verifyApiKey +} diff --git a/packages/server/src/controllers/assistants/index.ts b/packages/server/src/controllers/assistants/index.ts new file mode 100644 index 000000000..eed007520 --- /dev/null +++ b/packages/server/src/controllers/assistants/index.ts @@ -0,0 +1,85 @@ +import { Request, Response, NextFunction } from 'express' +import assistantsService from '../../services/assistants' + +const creatAssistant = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: assistantsController.creatAssistant - body not provided!`) + } + const apiResponse = await assistantsService.creatAssistant(req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const deleteAssistant = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: assistantsController.deleteAssistant - id not provided!`) + } + const apiResponse = await assistantsService.deleteAssistant(req.params.id, req.query.isDeleteBoth) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllAssistants = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await assistantsService.getAllAssistants() + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAssistantById = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: assistantsController.getAssistantById - id not provided!`) + } + const apiResponse = await assistantsService.getAssistantById(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const updateAssistant = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: assistantsController.updateAssistant - id not provided!`) + } + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: assistantsController.updateAssistant - body not provided!`) + } + const apiResponse = await assistantsService.updateAssistant(req.params.id, req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + creatAssistant, + deleteAssistant, + getAllAssistants, + getAssistantById, + updateAssistant +} diff --git a/packages/server/src/controllers/chat-messages/index.ts b/packages/server/src/controllers/chat-messages/index.ts new file mode 100644 index 000000000..02562b3a6 --- /dev/null +++ b/packages/server/src/controllers/chat-messages/index.ts @@ -0,0 +1,156 @@ +import { Request, Response, NextFunction } from 'express' +import { chatType, IReactFlowObject } from '../../Interface' +import chatflowsService from '../../services/chatflows' +import chatMessagesService from '../../services/chat-messages' +import { clearSessionMemory } from '../../utils' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { FindOptionsWhere } from 'typeorm' +import { ChatMessage } from '../../database/entities/ChatMessage' + +const createChatMessage = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error('Error: chatMessagesController.createChatMessage - request body not provided!') + } + const apiResponse = await chatMessagesService.createChatMessage(req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllChatMessages = async (req: Request, res: Response, next: NextFunction) => { + try { + let chatTypeFilter = req.query?.chatType as chatType | undefined + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL + } + } catch (e) { + return res.status(500).send(e) + } + } + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const messageId = req.query?.messageId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + const feedback = req.query?.feedback as boolean | undefined + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatMessageController.getAllChatMessages - id not provided!`) + } + const apiResponse = await chatMessagesService.getAllChatMessages( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate, + messageId, + feedback + ) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllInternalChatMessages = async (req: Request, res: Response, next: NextFunction) => { + try { + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const messageId = req.query?.messageId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + const feedback = req.query?.feedback as boolean | undefined + const apiResponse = await chatMessagesService.getAllInternalChatMessages( + req.params.id, + chatType.INTERNAL, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate, + messageId, + feedback + ) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +//Delete all chatmessages from chatId +const removeAllChatMessages = async (req: Request, res: Response, next: NextFunction) => { + try { + const appServer = getRunningExpressApp() + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error('Error: chatMessagesController.removeAllChatMessages - id not provided!') + } + const chatflowid = req.params.id + const chatflow = await chatflowsService.getChatflowById(req.params.id) + if (!chatflow) { + return res.status(404).send(`Chatflow ${req.params.id} not found`) + } + const chatId = req.query?.chatId as string + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const chatType = req.query?.chatType as string | undefined + const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + try { + await clearSessionMemory( + nodes, + appServer.nodesPool.componentNodes, + chatId, + appServer.AppDataSource, + sessionId, + memoryType, + isClearFromViewMessageDialog + ) + } catch (e) { + return res.status(500).send('Error clearing chat messages') + } + + const deleteOptions: FindOptionsWhere = { chatflowid } + if (chatId) deleteOptions.chatId = chatId + if (memoryType) deleteOptions.memoryType = memoryType + if (sessionId) deleteOptions.sessionId = sessionId + if (chatType) deleteOptions.chatType = chatType + const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions) + if (apiResponse.executionError) { + res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createChatMessage, + getAllChatMessages, + getAllInternalChatMessages, + removeAllChatMessages +} diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts new file mode 100644 index 000000000..b3c6bd50e --- /dev/null +++ b/packages/server/src/controllers/chatflows/index.ts @@ -0,0 +1,170 @@ +import { Request, Response, NextFunction } from 'express' +import chatflowsService from '../../services/chatflows' +import { ChatFlow } from '../../database/entities/ChatFlow' +import { createRateLimiter } from '../../utils/rateLimit' +import { getApiKey } from '../../utils/apiKey' + +const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.checkIfChatflowIsValidForStreaming - id not provided!`) + } + const apiResponse = await chatflowsService.checkIfChatflowIsValidForStreaming(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const checkIfChatflowIsValidForUploads = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.checkIfChatflowIsValidForUploads - id not provided!`) + } + const apiResponse = await chatflowsService.checkIfChatflowIsValidForUploads(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const deleteChatflow = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.deleteChatflow - id not provided!`) + } + const apiResponse = await chatflowsService.deleteChatflow(req.params.id) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await chatflowsService.getAllChatflows() + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Get specific chatflow via api key +const getChatflowByApiKey = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.apiKey === 'undefined' || req.params.apiKey === '') { + throw new Error(`Error: chatflowsRouter.getChatflowById - apiKey not provided!`) + } + const apiKey = await getApiKey(req.params.apiKey) + if (!apiKey) { + return res.status(401).send('Unauthorized') + } + const apiResponse = await chatflowsService.getChatflowByApiKey(apiKey.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getChatflowById = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.getChatflowById - id not provided!`) + } + const apiResponse = await chatflowsService.getChatflowById(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const saveChatflow = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: chatflowsRouter.saveChatflow - body not provided!`) + } + const body = req.body + const newChatFlow = new ChatFlow() + Object.assign(newChatFlow, body) + const apiResponse = await chatflowsService.saveChatflow(newChatFlow) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const updateChatflow = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.updateChatflow - id not provided!`) + } + const chatflow = await chatflowsService.getChatflowById(req.params.id) + if (!chatflow) { + return res.status(404).send(`Chatflow ${req.params.id} not found`) + } + const body = req.body + const updateChatFlow = new ChatFlow() + Object.assign(updateChatFlow, body) + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + const apiResponse = await chatflowsService.updateChatflow(chatflow, updateChatFlow) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getSinglePublicChatflow = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.updateChatflow - id not provided!`) + } + const apiResponse = await chatflowsService.getSinglePublicChatflow(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getSinglePublicChatbotConfig = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: chatflowsRouter.getSinglePublicChatbotConfig - id not provided!`) + } + const apiResponse = await chatflowsService.getSinglePublicChatbotConfig(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + checkIfChatflowIsValidForStreaming, + checkIfChatflowIsValidForUploads, + deleteChatflow, + getAllChatflows, + getChatflowByApiKey, + getChatflowById, + saveChatflow, + updateChatflow, + getSinglePublicChatflow, + getSinglePublicChatbotConfig +} diff --git a/packages/server/src/controllers/components-credentials/index.ts b/packages/server/src/controllers/components-credentials/index.ts new file mode 100644 index 000000000..5f16aa464 --- /dev/null +++ b/packages/server/src/controllers/components-credentials/index.ts @@ -0,0 +1,44 @@ +import { Request, Response, NextFunction } from 'express' +import componentsCredentialsService from '../../services/components-credentials' + +// Get all component credentials +const getAllComponentsCredentials = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await componentsCredentialsService.getAllComponentsCredentials() + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Get component credential via name +const getComponentByName = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.name === 'undefined' || req.params.name === '') { + throw new Error(`Error: componentsCredentialsController.getComponentByName - name not provided!`) + } + const apiResponse = await componentsCredentialsService.getComponentByName(req.params.name) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Returns specific component credential icon via name +const getSingleComponentsCredentialIcon = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.name === 'undefined' || req.params.name === '') { + throw new Error(`Error: componentsCredentialsController.getSingleComponentsCredentialIcon - name not provided!`) + } + const apiResponse = await componentsCredentialsService.getSingleComponentsCredentialIcon(req.params.name) + return res.sendFile(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllComponentsCredentials, + getComponentByName, + getSingleComponentsCredentialIcon +} diff --git a/packages/server/src/controllers/credentials/index.ts b/packages/server/src/controllers/credentials/index.ts new file mode 100644 index 000000000..2bc599a2f --- /dev/null +++ b/packages/server/src/controllers/credentials/index.ts @@ -0,0 +1,79 @@ +import { Request, Response, NextFunction } from 'express' +import credentialsService from '../../services/credentials' + +const createCredential = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: credentialsController.createCredential - body not provided!`) + } + const apiResponse = await credentialsService.createCredential(req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const deleteCredentials = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: credentialsController.deleteCredentials - id not provided!`) + } + const apiResponse = await credentialsService.deleteCredentials(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllCredentials = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await credentialsService.getAllCredentials(req.query.credentialName) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getCredentialById = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: credentialsController.getCredentialById - id not provided!`) + } + const apiResponse = await credentialsService.getCredentialById(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const updateCredential = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: credentialsController.updateCredential - id not provided!`) + } + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: credentialsController.updateCredential - body not provided!`) + } + const apiResponse = await credentialsService.updateCredential(req.params.id, req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createCredential, + deleteCredentials, + getAllCredentials, + getCredentialById, + updateCredential +} diff --git a/packages/server/src/controllers/feedback/index.ts b/packages/server/src/controllers/feedback/index.ts new file mode 100644 index 000000000..08c95aff0 --- /dev/null +++ b/packages/server/src/controllers/feedback/index.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express' +import feedbackService from '../../services/feedback' + +const getAllChatMessageFeedback = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: feedbackController.getAllChatMessageFeedback - id not provided!`) + } + const chatflowid = req.params.id + const chatId = req.query?.chatId as string | undefined + const sortOrder = req.query?.order as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + const apiResponse = await feedbackService.getAllChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const createChatMessageFeedbackForChatflow = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: feedbackController.createChatMessageFeedbackForChatflow - body not provided!`) + } + const apiResponse = await feedbackService.createChatMessageFeedbackForChatflow(req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const updateChatMessageFeedbackForChatflow = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: feedbackController.updateChatMessageFeedbackForChatflow - body not provided!`) + } + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: feedbackController.updateChatMessageFeedbackForChatflow - id not provided!`) + } + const apiResponse = await feedbackService.updateChatMessageFeedbackForChatflow(req.params.id, req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllChatMessageFeedback, + createChatMessageFeedbackForChatflow, + updateChatMessageFeedbackForChatflow +} diff --git a/packages/server/src/controllers/fetch-links/index.ts b/packages/server/src/controllers/fetch-links/index.ts new file mode 100644 index 000000000..586f1159e --- /dev/null +++ b/packages/server/src/controllers/fetch-links/index.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from 'express' +import fetchLinksService from '../../services/fetch-links' + +const getAllLinks = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.query.url === 'undefined' || req.query.url === '') { + throw new Error(`Error: fetchLinksController.getAllLinks - url not provided!`) + } + if (typeof req.query.relativeLinksMethod === 'undefined' || req.query.relativeLinksMethod === '') { + throw new Error(`Error: fetchLinksController.getAllLinks - relativeLinksMethod not provided!`) + } + if (typeof req.query.limit === 'undefined' || req.query.limit === '') { + throw new Error(`Error: fetchLinksController.getAllLinks - limit not provided!`) + } + const apiResponse = await fetchLinksService.getAllLinks( + req.query.url as string, + req.query.relativeLinksMethod as string, + req.query.limit as string + ) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllLinks +} diff --git a/packages/server/src/controllers/flow-configs/index.ts b/packages/server/src/controllers/flow-configs/index.ts new file mode 100644 index 000000000..ce5764976 --- /dev/null +++ b/packages/server/src/controllers/flow-configs/index.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from 'express' +import flowConfigsService from '../../services/flow-configs' + +const getSingleFlowConfig = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: flowConfigsController.getSingleFlowConfig - id not provided!`) + } + const apiResponse = await flowConfigsService.getSingleFlowConfig(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getSingleFlowConfig +} diff --git a/packages/server/src/controllers/get-upload-file/index.ts b/packages/server/src/controllers/get-upload-file/index.ts new file mode 100644 index 000000000..773f08add --- /dev/null +++ b/packages/server/src/controllers/get-upload-file/index.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from 'express' +import path from 'path' +import contentDisposition from 'content-disposition' +import { getStoragePath } from 'flowise-components' +import * as fs from 'fs' + +const streamUploadedImage = async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.query.chatflowId || !req.query.chatId || !req.query.fileName) { + return res.status(500).send(`Invalid file path`) + } + const chatflowId = req.query.chatflowId as string + const chatId = req.query.chatId as string + const fileName = req.query.fileName as string + const filePath = path.join(getStoragePath(), chatflowId, chatId, fileName) + //raise error if file path is not absolute + if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) + //raise error if file path contains '..' + if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) + //only return from the storage folder + if (!filePath.startsWith(getStoragePath())) return res.status(500).send(`Invalid file path`) + + if (fs.existsSync(filePath)) { + res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath))) + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + } else { + return res.status(404).send(`File ${fileName} not found`) + } + } catch (error) { + next(error) + } +} + +export default { + streamUploadedImage +} diff --git a/packages/server/src/controllers/get-upload-path/index.ts b/packages/server/src/controllers/get-upload-path/index.ts new file mode 100644 index 000000000..05e76591f --- /dev/null +++ b/packages/server/src/controllers/get-upload-path/index.ts @@ -0,0 +1,17 @@ +import { Request, Response, NextFunction } from 'express' +import { getStoragePath } from 'flowise-components' + +const getPathForUploads = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = { + storagePath: getStoragePath() + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getPathForUploads +} diff --git a/packages/server/src/controllers/internal-predictions/index.ts b/packages/server/src/controllers/internal-predictions/index.ts new file mode 100644 index 000000000..1fea952d7 --- /dev/null +++ b/packages/server/src/controllers/internal-predictions/index.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express' +import { utilBuildChatflow } from '../../utils/buildChatflow' + +// Send input message and get prediction result (Internal) +const createInternalPrediction = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await utilBuildChatflow(req, req.io, true) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createInternalPrediction +} diff --git a/packages/server/src/controllers/ip/index.ts b/packages/server/src/controllers/ip/index.ts new file mode 100644 index 000000000..98f4ce729 --- /dev/null +++ b/packages/server/src/controllers/ip/index.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from 'express' + +// Configure number of proxies in Host Environment +const configureProxyNrInHostEnv = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.ip === 'undefined' || req.ip) { + throw new Error(`Error: ipController.configureProxyNrInHostEnv - ip not provided!`) + } + const apiResponse = { + ip: req.ip, + msg: `Check returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 and restart Cloud-Hosted Flowise until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#cloud-hosted-rate-limit-setup-guide for more information.` + } + return res.send(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + configureProxyNrInHostEnv +} diff --git a/packages/server/src/controllers/load-prompts/index.ts b/packages/server/src/controllers/load-prompts/index.ts new file mode 100644 index 000000000..6fa38aef6 --- /dev/null +++ b/packages/server/src/controllers/load-prompts/index.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from 'express' +import loadPromptsService from '../../services/load-prompts' + +const createPrompt = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || typeof req.body.promptName === 'undefined' || req.body.promptName === '') { + throw new Error(`Error: loadPromptsController.createPrompt - promptName not provided!`) + } + const apiResponse = await loadPromptsService.createPrompt(req.body.promptName as string) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createPrompt +} diff --git a/packages/server/src/controllers/marketplaces/index.ts b/packages/server/src/controllers/marketplaces/index.ts new file mode 100644 index 000000000..62371a734 --- /dev/null +++ b/packages/server/src/controllers/marketplaces/index.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express' +import marketplacesService from '../../services/marketplaces' + +// Get all templates for marketplaces +const getAllTemplates = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await marketplacesService.getAllTemplates() + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllTemplates +} diff --git a/packages/server/src/controllers/node-configs/index.ts b/packages/server/src/controllers/node-configs/index.ts new file mode 100644 index 000000000..f8f90e1e8 --- /dev/null +++ b/packages/server/src/controllers/node-configs/index.ts @@ -0,0 +1,18 @@ +import { Request, Response, NextFunction } from 'express' +import nodeConfigsService from '../../services/node-configs' + +const getAllNodeConfigs = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: nodeConfigsController.getAllNodeConfigs - body not provided!`) + } + const apiResponse = await nodeConfigsService.getAllNodeConfigs(req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllNodeConfigs +} diff --git a/packages/server/src/controllers/node-icons/index.ts b/packages/server/src/controllers/node-icons/index.ts new file mode 100644 index 000000000..b423a96fe --- /dev/null +++ b/packages/server/src/controllers/node-icons/index.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' + +// Returns specific component node icon via name +const getSingleNodeIcon = async (req: Request, res: Response, next: NextFunction) => { + try { + const appServer = getRunningExpressApp() + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, req.params.name)) { + const nodeInstance = appServer.nodesPool.componentNodes[req.params.name] + if (nodeInstance.icon === undefined) { + throw new Error(`Error: nodeIconController.getSingleNodeIcon - Node ${req.params.name} icon not found`) + } + + if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) { + const filepath = nodeInstance.icon + res.sendFile(filepath) + } else { + throw new Error(`Error: nodeIconController.getSingleNodeIcon - Node ${req.params.name} icon is missing icon`) + } + } else { + throw new Error(`Error: nodeIconController.getSingleNodeIcon - Node ${req.params.name} not found`) + } + } catch (error) { + next(error) + } +} + +export default { + getSingleNodeIcon +} diff --git a/packages/server/src/controllers/nodes/index.ts b/packages/server/src/controllers/nodes/index.ts new file mode 100644 index 000000000..ac448d933 --- /dev/null +++ b/packages/server/src/controllers/nodes/index.ts @@ -0,0 +1,76 @@ +import { Request, Response, NextFunction } from 'express' +import nodesService from '../../services/nodes' + +const getAllNodes = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await nodesService.getAllNodes() + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getNodeByName = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.name === 'undefined' || req.params.name === '') { + throw new Error(`Error: nodesController.getNodeByName - name not provided!`) + } + const apiResponse = await nodesService.getNodeByName(req.params.name) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getSingleNodeIcon = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.name === 'undefined' || req.params.name === '') { + throw new Error(`Error: nodesController.getSingleNodeIcon - name not provided!`) + } + const apiResponse = await nodesService.getSingleNodeIcon(req.params.name) + return res.sendFile(apiResponse) + } catch (error) { + next(error) + } +} + +const getSingleNodeAsyncOptions = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: nodesController.getSingleNodeAsyncOptions - body not provided!`) + } + if (typeof req.params.name === 'undefined' || req.params.name === '') { + throw new Error(`Error: nodesController.getSingleNodeAsyncOptions - name not provided!`) + } + const apiResponse = await nodesService.getSingleNodeAsyncOptions(req.params.name, req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const executeCustomFunction = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: nodesController.executeCustomFunction - body not provided!`) + } + const apiResponse = await nodesService.executeCustomFunction(req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getAllNodes, + getNodeByName, + getSingleNodeIcon, + getSingleNodeAsyncOptions, + executeCustomFunction +} diff --git a/packages/server/src/controllers/openai-assistants/index.ts b/packages/server/src/controllers/openai-assistants/index.ts new file mode 100644 index 000000000..539f2fbf6 --- /dev/null +++ b/packages/server/src/controllers/openai-assistants/index.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction } from 'express' +import path from 'path' +import * as fs from 'fs' +import openaiAssistantsService from '../../services/openai-assistants' +import { getUserHome } from '../../utils' +import contentDisposition from 'content-disposition' + +// List available assistants +const getAllOpenaiAssistants = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.query.credential === 'undefined' || req.query.credential === '') { + throw new Error(`Error: openaiAssistantsController.getAllOpenaiAssistants - credential not provided!`) + } + const apiResponse = await openaiAssistantsService.getAllOpenaiAssistants(req.query.credential as string) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Get assistant object +const getSingleOpenaiAssistant = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: openaiAssistantsController.getSingleOpenaiAssistant - id not provided!`) + } + if (typeof req.query.credential === 'undefined' || req.query.credential === '') { + throw new Error(`Error: openaiAssistantsController.getSingleOpenaiAssistant - credential not provided!`) + } + const apiResponse = await openaiAssistantsService.getSingleOpenaiAssistant(req.query.credential as string, req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +// Download file from assistant +const getFileFromAssistant = async (req: Request, res: Response, next: NextFunction) => { + try { + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + //raise error if file path is not absolute + if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) + //raise error if file path contains '..' + if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) + //only return from the .flowise openai-assistant folder + if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`) + if (fs.existsSync(filePath)) { + res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath))) + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(res) + } else { + return res.status(404).send(`File ${req.body.fileName} not found`) + } + } catch (error) { + next(error) + } +} + +export default { + getAllOpenaiAssistants, + getSingleOpenaiAssistant, + getFileFromAssistant +} diff --git a/packages/server/src/controllers/predictions/index.ts b/packages/server/src/controllers/predictions/index.ts new file mode 100644 index 000000000..936bfe1cc --- /dev/null +++ b/packages/server/src/controllers/predictions/index.ts @@ -0,0 +1,66 @@ +import { Request, Response, NextFunction } from 'express' +import { getRateLimiter } from '../../utils/rateLimit' +import chatflowsService from '../../services/chatflows' +import logger from '../../utils/logger' +import { utilBuildChatflow } from '../../utils/buildChatflow' + +// Send input message and get prediction result (External) +const createPrediction = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: predictionsController.createPrediction - id not provided!`) + } + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: predictionsController.createPrediction - body not provided!`) + } + const chatflow = await chatflowsService.getChatflowById(req.params.id) + if (!chatflow) { + return res.status(404).send(`Chatflow ${req.params.id} not found`) + } + let isDomainAllowed = true + logger.info(`[server]: Request originated from ${req.headers.origin}`) + if (chatflow.chatbotConfig) { + const parsedConfig = JSON.parse(chatflow.chatbotConfig) + // check whether the first one is not empty. if it is empty that means the user set a value and then removed it. + const isValidAllowedOrigins = parsedConfig.allowedOrigins?.length && parsedConfig.allowedOrigins[0] !== '' + if (isValidAllowedOrigins) { + const originHeader = req.headers.origin as string + const origin = new URL(originHeader).host + isDomainAllowed = + parsedConfig.allowedOrigins.filter((domain: string) => { + try { + const allowedOrigin = new URL(domain).host + return origin === allowedOrigin + } catch (e) { + return false + } + }).length > 0 + } + } + + if (isDomainAllowed) { + const apiResponse = await utilBuildChatflow(req, req.io) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } else { + return res.status(401).send(`This site is not allowed to access this chatbot`) + } + } catch (error) { + next(error) + } +} + +const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + return getRateLimiter(req, res, next) + } catch (error) { + next(error) + } +} + +export default { + createPrediction, + getRateLimiterMiddleware +} diff --git a/packages/server/src/controllers/prompts-lists/index.ts b/packages/server/src/controllers/prompts-lists/index.ts new file mode 100644 index 000000000..88a048cae --- /dev/null +++ b/packages/server/src/controllers/prompts-lists/index.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express' +import promptsListsService from '../../services/prompts-lists' + +// Prompt from Hub +const createPromptsList = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await promptsListsService.createPromptsList(req.body) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createPromptsList +} diff --git a/packages/server/src/controllers/stats/index.ts b/packages/server/src/controllers/stats/index.ts new file mode 100644 index 000000000..e1c97c490 --- /dev/null +++ b/packages/server/src/controllers/stats/index.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from 'express' +import statsService from '../../services/stats' +import { chatType } from '../../Interface' + +const getChatflowStats = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: statsController.getChatflowStats - id not provided!`) + } + const chatflowid = req.params.id + let chatTypeFilter = req.query?.chatType as chatType | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL + } + } catch (e) { + return res.status(500).send(e) + } + } + const apiResponse = await statsService.getChatflowStats(chatflowid, chatTypeFilter, startDate, endDate, '', true) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getChatflowStats +} diff --git a/packages/server/src/controllers/tools/index.ts b/packages/server/src/controllers/tools/index.ts new file mode 100644 index 000000000..49cea2696 --- /dev/null +++ b/packages/server/src/controllers/tools/index.ts @@ -0,0 +1,85 @@ +import { Request, Response, NextFunction } from 'express' +import toolsService from '../../services/tools' + +const creatTool = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: toolsController.creatTool - body not provided!`) + } + const apiResponse = await toolsService.creatTool(req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const deleteTool = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: toolsController.updateTool - id not provided!`) + } + const apiResponse = await toolsService.deleteTool(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllTools = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await toolsService.getAllTools() + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getToolById = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: toolsController.getToolById - id not provided!`) + } + const apiResponse = await toolsService.getToolById(req.params.id) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const updateTool = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error(`Error: toolsController.updateTool - id not provided!`) + } + if (typeof req.body === 'undefined' || req.body === '') { + throw new Error(`Error: toolsController.deleteTool - body not provided!`) + } + const apiResponse = await toolsService.updateTool(req.params.id, req.body) + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + creatTool, + deleteTool, + getAllTools, + getToolById, + updateTool +} diff --git a/packages/server/src/controllers/variables/index.ts b/packages/server/src/controllers/variables/index.ts new file mode 100644 index 000000000..4dfd7492a --- /dev/null +++ b/packages/server/src/controllers/variables/index.ts @@ -0,0 +1,68 @@ +import { Request, Response, NextFunction } from 'express' +import variablesService from '../../services/variables' +import { Variable } from '../../database/entities/Variable' + +const createVariable = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.body === 'undefined') { + throw new Error(`Error: variablesController.createVariable - body not provided!`) + } + const body = req.body + const newVariable = new Variable() + Object.assign(newVariable, body) + const apiResponse = await variablesService.createVariable(newVariable) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const deleteVariable = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error('Error: variablesController.deleteVariable - id not provided!') + } + const apiResponse = await variablesService.deleteVariable(req.params.id) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const getAllVariables = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await variablesService.getAllVariables() + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const updateVariable = async (req: Request, res: Response, next: NextFunction) => { + try { + if (typeof req.params.id === 'undefined' || req.params.id === '') { + throw new Error('Error: variablesController.updateVariable - id not provided!') + } + if (typeof req.body === 'undefined') { + throw new Error('Error: variablesController.updateVariable - body not provided!') + } + const variable = await variablesService.getVariableById(req.params.id) + if (!variable) { + return res.status(404).send(`Variable ${req.params.id} not found in the database`) + } + const body = req.body + const updatedVariable = new Variable() + Object.assign(updatedVariable, body) + const apiResponse = await variablesService.updateVariable(variable, updatedVariable) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + createVariable, + deleteVariable, + getAllVariables, + updateVariable +} diff --git a/packages/server/src/controllers/vectors/index.ts b/packages/server/src/controllers/vectors/index.ts new file mode 100644 index 000000000..21f8e3da9 --- /dev/null +++ b/packages/server/src/controllers/vectors/index.ts @@ -0,0 +1,33 @@ +import { Request, Response, NextFunction } from 'express' +import vectorsService from '../../services/vectors' +import { getRateLimiter } from '../../utils/rateLimit' + +const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + return getRateLimiter(req, res, next) + } catch (error) { + next(error) + } +} + +const upsertVectorMiddleware = async (req: Request, res: Response, next: NextFunction) => { + try { + return await vectorsService.upsertVectorMiddleware(req, res) + } catch (error) { + next(error) + } +} + +const createInternalUpsert = async (req: Request, res: Response, next: NextFunction) => { + try { + return await vectorsService.upsertVectorMiddleware(req, res, true) + } catch (error) { + next(error) + } +} + +export default { + upsertVectorMiddleware, + createInternalUpsert, + getRateLimiterMiddleware +} diff --git a/packages/server/src/controllers/versions/index.ts b/packages/server/src/controllers/versions/index.ts new file mode 100644 index 000000000..7a6f76c46 --- /dev/null +++ b/packages/server/src/controllers/versions/index.ts @@ -0,0 +1,18 @@ +import { Request, Response, NextFunction } from 'express' +import versionsService from '../../services/versions' + +const getVersion = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await versionsService.getVersion() + if (apiResponse.executionError) { + return res.status(apiResponse.status).send(apiResponse.msg) + } + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +export default { + getVersion +} diff --git a/packages/server/src/errors/apiError/index.ts b/packages/server/src/errors/apiError/index.ts new file mode 100644 index 000000000..641e2d5a1 --- /dev/null +++ b/packages/server/src/errors/apiError/index.ts @@ -0,0 +1,9 @@ +export class ApiError extends Error { + statusCode: number + constructor(statusCode: number, message: string) { + super(message) + this.statusCode = statusCode + // capture the stack trace of the error from anywhere in the application + Error.captureStackTrace(this, this.constructor) + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0e32fa627..8330c570d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,89 +1,34 @@ -import express, { NextFunction, Request, Response } from 'express' -import multer from 'multer' +import express from 'express' +import { Request, Response } from 'express' import path from 'path' import cors from 'cors' import http from 'http' -import * as fs from 'fs' import basicAuth from 'express-basic-auth' -import contentDisposition from 'content-disposition' import { Server } from 'socket.io' import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' -import { v4 as uuidv4 } from 'uuid' -import OpenAI from 'openai' -import { DataSource, FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual, Between } from 'typeorm' -import { - IChatFlow, - IncomingInput, - IReactFlowNode, - IReactFlowObject, - INodeData, - ICredentialReturnResponse, - chatType, - IChatMessage, - IChatMessageFeedback, - IDepthQueue, - INodeDirectedGraph, - IUploadFileSizeAndTypes -} from './Interface' -import { - getNodeModulesPackagePath, - getStartingNodes, - buildFlow, - getEndingNodes, - constructGraphs, - resolveVariables, - isStartNodeDependOnInput, - mapMimeTypeToInputField, - findAvailableConfigs, - isSameOverrideConfig, - isFlowValidForStream, - databaseEntities, - transformToCredentialEntity, - decryptCredentialData, - replaceInputsWithConfig, - getEncryptionKey, - getMemorySessionId, - getUserHome, - getSessionChatHistory, - getAllConnectedNodes, - clearSessionMemory, - findMemoryNode, - deleteFolderRecursive, - getTelemetryFlowObj, - getAppVersion -} from './utils' -import { cloneDeep, omit, uniqWith, isEqual } from 'lodash' +import { DataSource } from 'typeorm' +import { IChatFlow } from './Interface' +import { getNodeModulesPackagePath, getEncryptionKey } from './utils' import { getDataSource } from './DataSource' import { NodesPool } from './NodesPool' import { ChatFlow } from './database/entities/ChatFlow' -import { ChatMessage } from './database/entities/ChatMessage' -import { ChatMessageFeedback } from './database/entities/ChatMessageFeedback' -import { Credential } from './database/entities/Credential' -import { Tool } from './database/entities/Tool' -import { Assistant } from './database/entities/Assistant' import { ChatflowPool } from './ChatflowPool' import { CachePool } from './CachePool' -import { - ICommonObject, - IMessage, - INodeOptionsValue, - INodeParams, - handleEscapeCharacters, - convertSpeechToText, - xmlScrape, - webCrawl, - getStoragePath, - IFileUpload -} from 'flowise-components' -import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' -import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' +import { initializeRateLimiter } from './utils/rateLimit' +import { getAPIKeys } from './utils/apiKey' import { sanitizeMiddleware, getCorsOptions, getAllowedIframeOrigins } from './utils/XSS' -import axios from 'axios' -import { Client } from 'langchainhub' -import { parsePrompt } from './utils/hub' import { Telemetry } from './utils/telemetry' -import { Variable } from './database/entities/Variable' +import flowiseApiV1Router from './routes' +import errorHandlerMiddleware from './middlewares/errors' + +declare global { + namespace Express { + interface Request { + io?: Server + } + } +} export class App { app: express.Application @@ -140,7 +85,6 @@ export class App { const flowise_file_size_limit = process.env.FLOWISE_FILE_SIZE_LIMIT ?? '50mb' this.app.use(express.json({ limit: flowise_file_size_limit })) this.app.use(express.urlencoded({ limit: flowise_file_size_limit, extended: true })) - if (process.env.NUMBER_OF_PROXIES && parseInt(process.env.NUMBER_OF_PROXIES) > 0) this.app.set('trust proxy', parseInt(process.env.NUMBER_OF_PROXIES)) @@ -168,6 +112,12 @@ export class App { // Add the sanitizeMiddleware to guard against XSS this.app.use(sanitizeMiddleware) + // Make io accessible to our router on req.io + this.app.use((req, res, next) => { + req.io = socketIO + next() + }) + if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) { const username = process.env.FLOWISE_USERNAME const password = process.env.FLOWISE_PASSWORD @@ -197,1433 +147,7 @@ export class App { }) } - const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` }) - - // ---------------------------------------- - // Configure number of proxies in Host Environment - // ---------------------------------------- - this.app.get('/api/v1/ip', (request, response) => { - response.send({ - ip: request.ip, - msg: 'Check returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 and restart Cloud-Hosted Flowise until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#cloud-hosted-rate-limit-setup-guide for more information.' - }) - }) - - // ---------------------------------------- - // Components - // ---------------------------------------- - - // Get all component nodes - this.app.get('/api/v1/nodes', (req: Request, res: Response) => { - const returnData = [] - for (const nodeName in this.nodesPool.componentNodes) { - const clonedNode = cloneDeep(this.nodesPool.componentNodes[nodeName]) - returnData.push(clonedNode) - } - return res.json(returnData) - }) - - // Get all component credentials - this.app.get('/api/v1/components-credentials', async (req: Request, res: Response) => { - const returnData = [] - for (const credName in this.nodesPool.componentCredentials) { - const clonedCred = cloneDeep(this.nodesPool.componentCredentials[credName]) - returnData.push(clonedCred) - } - return res.json(returnData) - }) - - // Get specific component node via name - this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { - return res.json(this.nodesPool.componentNodes[req.params.name]) - } else { - throw new Error(`Node ${req.params.name} not found`) - } - }) - - // Get component credential via name - this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { - if (!req.params.name.includes('&')) { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { - return res.json(this.nodesPool.componentCredentials[req.params.name]) - } else { - throw new Error(`Credential ${req.params.name} not found`) - } - } else { - const returnResponse = [] - for (const name of req.params.name.split('&')) { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { - returnResponse.push(this.nodesPool.componentCredentials[name]) - } else { - throw new Error(`Credential ${name} not found`) - } - } - return res.json(returnResponse) - } - }) - - // Returns specific component node icon via name - this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { - const nodeInstance = this.nodesPool.componentNodes[req.params.name] - if (nodeInstance.icon === undefined) { - throw new Error(`Node ${req.params.name} icon not found`) - } - - if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) { - const filepath = nodeInstance.icon - res.sendFile(filepath) - } else { - throw new Error(`Node ${req.params.name} icon is missing icon`) - } - } else { - throw new Error(`Node ${req.params.name} not found`) - } - }) - - // Returns specific component credential icon via name - this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { - const credInstance = this.nodesPool.componentCredentials[req.params.name] - if (credInstance.icon === undefined) { - throw new Error(`Credential ${req.params.name} icon not found`) - } - - if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { - const filepath = credInstance.icon - res.sendFile(filepath) - } else { - throw new Error(`Credential ${req.params.name} icon is missing icon`) - } - } else { - throw new Error(`Credential ${req.params.name} not found`) - } - }) - - // load async options - this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { - const nodeData: INodeData = req.body - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { - try { - const nodeInstance = this.nodesPool.componentNodes[req.params.name] - const methodName = nodeData.loadMethod || '' - - const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { - appDataSource: this.AppDataSource, - databaseEntities: databaseEntities - }) - - return res.json(returnOptions) - } catch (error) { - return res.json([]) - } - } else { - res.status(404).send(`Node ${req.params.name} not found`) - return - } - }) - - // execute custom function node - this.app.post('/api/v1/node-custom-function', async (req: Request, res: Response) => { - const body = req.body - const functionInputVariables = Object.fromEntries( - [...(body?.javascriptFunction ?? '').matchAll(/\$([a-zA-Z0-9_]+)/g)].map((g) => [g[1], undefined]) - ) - const nodeData = { inputs: { functionInputVariables, ...body } } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, 'customFunction')) { - try { - const nodeInstanceFilePath = this.nodesPool.componentNodes['customFunction'].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const newNodeInstance = new nodeModule.nodeClass() - - const options: ICommonObject = { - appDataSource: this.AppDataSource, - databaseEntities, - logger - } - - const returnData = await newNodeInstance.init(nodeData, '', options) - const result = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData - - return res.json(result) - } catch (error) { - return res.status(500).send(`Error running custom function: ${error}`) - } - } else { - res.status(404).send(`Node customFunction not found`) - return - } - }) - - // ---------------------------------------- - // Chatflows - // ---------------------------------------- - - // Get all chatflows - this.app.get('/api/v1/chatflows', async (req: Request, res: Response) => { - const chatflows: IChatFlow[] = await getAllChatFlow() - return res.json(chatflows) - }) - - // Get specific chatflow via api key - this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => { - try { - const apiKey = await getApiKey(req.params.apiKey) - if (!apiKey) return res.status(401).send('Unauthorized') - const chatflows = await this.AppDataSource.getRepository(ChatFlow) - .createQueryBuilder('cf') - .where('cf.apikeyid = :apikeyid', { apikeyid: apiKey.id }) - .orWhere('cf.apikeyid IS NULL') - .orWhere('cf.apikeyid = ""') - .orderBy('cf.name', 'ASC') - .getMany() - if (chatflows.length >= 1) return res.status(200).send(chatflows) - return res.status(404).send('Chatflow not found') - } catch (err: any) { - return res.status(500).send(err?.message) - } - }) - - // Get specific chatflow via id - this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (chatflow) return res.json(chatflow) - return res.status(404).send(`Chatflow ${req.params.id} not found`) - }) - - // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) - this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (chatflow && chatflow.isPublic) return res.json(chatflow) - else if (chatflow && !chatflow.isPublic) return res.status(401).send(`Unauthorized`) - return res.status(404).send(`Chatflow ${req.params.id} not found`) - }) - - // Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat) - // Safe as public endpoint as chatbotConfig doesn't contain sensitive credential - this.app.get('/api/v1/public-chatbotConfig/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const uploadsConfig = await this.getUploadsConfig(req.params.id) - // even if chatbotConfig is not set but uploads are enabled - // send uploadsConfig to the chatbot - if (chatflow.chatbotConfig || uploadsConfig) { - try { - const parsedConfig = chatflow.chatbotConfig ? JSON.parse(chatflow.chatbotConfig) : {} - return res.json({ ...parsedConfig, uploads: uploadsConfig }) - } catch (e) { - return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`) - } - } - return res.status(200).send('OK') - }) - - // Save chatflow - this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => { - const body = req.body - const newChatFlow = new ChatFlow() - Object.assign(newChatFlow, body) - - const chatflow = this.AppDataSource.getRepository(ChatFlow).create(newChatFlow) - const results = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - await this.telemetry.sendTelemetry('chatflow_created', { - version: await getAppVersion(), - chatflowId: results.id, - flowGraph: getTelemetryFlowObj(JSON.parse(results.flowData)?.nodes, JSON.parse(results.flowData)?.edges) - }) - - return res.json(results) - }) - - // Update chatflow - this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - - if (!chatflow) { - res.status(404).send(`Chatflow ${req.params.id} not found`) - return - } - - const body = req.body - const updateChatFlow = new ChatFlow() - Object.assign(updateChatFlow, body) - - updateChatFlow.id = chatflow.id - createRateLimiter(updateChatFlow) - - this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) - const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - // chatFlowPool is initialized only when a flow is opened - // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined - if (this.chatflowPool) { - // Update chatflowpool inSync to false, to build flow from scratch again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) - } - - return res.json(result) - }) - - // Delete chatflow via id - this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { - const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) - - try { - // Delete all uploads corresponding to this chatflow - const directory = path.join(getStoragePath(), req.params.id) - deleteFolderRecursive(directory) - } catch (e) { - logger.error(`[server]: Error deleting file storage for chatflow ${req.params.id}: ${e}`) - } - - return res.json(results) - }) - - // Check if chatflow valid for streaming - this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - - /*** Get Ending Node with Directed Graph ***/ - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - const edges = parsedFlowData.edges - const { graph, nodeDependencies } = constructGraphs(nodes, edges) - - const endingNodeIds = getEndingNodes(nodeDependencies, graph) - if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`) - - const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) - - let isStreaming = false - let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode') - - for (const endingNode of endingNodes) { - const endingNodeData = endingNode.data - if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) - - const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode' - - if (!isEndingNode) { - if ( - endingNodeData && - endingNodeData.category !== 'Chains' && - endingNodeData.category !== 'Agents' && - endingNodeData.category !== 'Engine' - ) { - return res.status(500).send(`Ending node must be either a Chain or Agent`) - } - } - - isStreaming = isEndingNode ? false : isFlowValidForStream(nodes, endingNodeData) - } - - // Once custom function ending node exists, flow is always unavailable to stream - const obj = { isStreaming: isEndingNodeExists ? false : isStreaming } - return res.json(obj) - }) - - // Check if chatflow valid for uploads - this.app.get('/api/v1/chatflows-uploads/:id', async (req: Request, res: Response) => { - try { - const uploadsConfig = await this.getUploadsConfig(req.params.id) - return res.json(uploadsConfig) - } catch (e) { - return res.status(500).send(e) - } - }) - - // ---------------------------------------- - // ChatMessage - // ---------------------------------------- - - // Get all chatmessages from chatflowid - this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const messageId = req.query?.messageId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - const feedback = req.query?.feedback as boolean | undefined - let chatTypeFilter = req.query?.chatType as chatType | undefined - - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) - } - } - - const chatmessages = await this.getChatMessage( - req.params.id, - chatTypeFilter, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate, - messageId, - feedback - ) - return res.json(chatmessages) - }) - - // Get internal chatmessages from chatflowid - this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const messageId = req.query?.messageId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - const feedback = req.query?.feedback as boolean | undefined - - const chatmessages = await this.getChatMessage( - req.params.id, - chatType.INTERNAL, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate, - messageId, - feedback - ) - return res.json(chatmessages) - }) - - // Add chatmessages for chatflowid - this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const body = req.body - const results = await this.addChatMessage(body) - return res.json(results) - }) - - // Delete all chatmessages from chatId - this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const chatflowid = req.params.id - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowid - }) - if (!chatflow) { - res.status(404).send(`Chatflow ${chatflowid} not found`) - return - } - const chatId = req.query?.chatId as string - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const chatType = req.query?.chatType as string | undefined - const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined - - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - - try { - await clearSessionMemory( - nodes, - this.nodesPool.componentNodes, - chatId, - this.AppDataSource, - sessionId, - memoryType, - isClearFromViewMessageDialog - ) - } catch (e) { - return res.status(500).send('Error clearing chat messages') - } - - const deleteOptions: FindOptionsWhere = { chatflowid } - if (chatId) deleteOptions.chatId = chatId - if (memoryType) deleteOptions.memoryType = memoryType - if (sessionId) deleteOptions.sessionId = sessionId - if (chatType) deleteOptions.chatType = chatType - - // remove all related feedback records - const feedbackDeleteOptions: FindOptionsWhere = { chatId } - await this.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions) - - // Delete all uploads corresponding to this chatflow/chatId - if (chatId) { - try { - const directory = path.join(getStoragePath(), chatflowid, chatId) - deleteFolderRecursive(directory) - } catch (e) { - logger.error(`[server]: Error deleting file storage for chatflow ${chatflowid}, chatId ${chatId}: ${e}`) - } - } - - const results = await this.AppDataSource.getRepository(ChatMessage).delete(deleteOptions) - return res.json(results) - }) - - // ---------------------------------------- - // Chat Message Feedback - // ---------------------------------------- - - // Get all chatmessage feedback from chatflowid - this.app.get('/api/v1/feedback/:id', async (req: Request, res: Response) => { - const chatflowid = req.params.id - const chatId = req.query?.chatId as string | undefined - const sortOrder = req.query?.order as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - - const feedback = await this.getChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate) - - return res.json(feedback) - }) - - // Add chatmessage feedback for chatflowid - this.app.post('/api/v1/feedback/:id', async (req: Request, res: Response) => { - const body = req.body - const results = await this.addChatMessageFeedback(body) - return res.json(results) - }) - - // Update chatmessage feedback for id - this.app.put('/api/v1/feedback/:id', async (req: Request, res: Response) => { - const id = req.params.id - const body = req.body - await this.updateChatMessageFeedback(id, body) - return res.json({ status: 'OK' }) - }) - - // ---------------------------------------- - // stats - // ---------------------------------------- - // - // get stats for showing in chatflow - this.app.get('/api/v1/stats/:id', async (req: Request, res: Response) => { - const chatflowid = req.params.id - let chatTypeFilter = req.query?.chatType as chatType | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) - } - } - - const chatmessages = (await this.getChatMessage( - chatflowid, - chatTypeFilter, - undefined, - undefined, - undefined, - undefined, - startDate, - endDate, - '', - true - )) as Array - const totalMessages = chatmessages.length - - const totalFeedback = chatmessages.filter((message) => message?.feedback).length - const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length - - const results = { - totalMessages, - totalFeedback, - positiveFeedback - } - - res.json(results) - }) - - // ---------------------------------------- - // Credentials - // ---------------------------------------- - - // Create new credential - this.app.post('/api/v1/credentials', async (req: Request, res: Response) => { - const body = req.body - const newCredential = await transformToCredentialEntity(body) - const credential = this.AppDataSource.getRepository(Credential).create(newCredential) - const results = await this.AppDataSource.getRepository(Credential).save(credential) - return res.json(results) - }) - - // Get all credentials - this.app.get('/api/v1/credentials', async (req: Request, res: Response) => { - if (req.query.credentialName) { - let returnCredentials = [] - if (Array.isArray(req.query.credentialName)) { - for (let i = 0; i < req.query.credentialName.length; i += 1) { - const name = req.query.credentialName[i] as string - const credentials = await this.AppDataSource.getRepository(Credential).findBy({ - credentialName: name - }) - returnCredentials.push(...credentials) - } - } else { - const credentials = await this.AppDataSource.getRepository(Credential).findBy({ - credentialName: req.query.credentialName as string - }) - returnCredentials = [...credentials] - } - return res.json(returnCredentials) - } else { - const credentials = await this.AppDataSource.getRepository(Credential).find() - const returnCredentials = [] - for (const credential of credentials) { - returnCredentials.push(omit(credential, ['encryptedData'])) - } - return res.json(returnCredentials) - } - }) - - // Get specific credential - this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => { - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: req.params.id - }) - - if (!credential) return res.status(404).send(`Credential ${req.params.id} not found`) - - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData( - credential.encryptedData, - credential.credentialName, - this.nodesPool.componentCredentials - ) - const returnCredential: ICredentialReturnResponse = { - ...credential, - plainDataObj: decryptedCredentialData - } - return res.json(omit(returnCredential, ['encryptedData'])) - }) - - // Update credential - this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => { - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: req.params.id - }) - - if (!credential) return res.status(404).send(`Credential ${req.params.id} not found`) - - const body = req.body - const updateCredential = await transformToCredentialEntity(body) - this.AppDataSource.getRepository(Credential).merge(credential, updateCredential) - const result = await this.AppDataSource.getRepository(Credential).save(credential) - - return res.json(result) - }) - - // Delete all credentials from chatflowid - this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { - const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) - return res.json(results) - }) - - // ---------------------------------------- - // Tools - // ---------------------------------------- - - // Get all tools - this.app.get('/api/v1/tools', async (req: Request, res: Response) => { - const tools = await this.AppDataSource.getRepository(Tool).find() - return res.json(tools) - }) - - // Get specific tool - this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { - const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ - id: req.params.id - }) - return res.json(tool) - }) - - // Add tool - this.app.post('/api/v1/tools', async (req: Request, res: Response) => { - const body = req.body - const newTool = new Tool() - Object.assign(newTool, body) - - const tool = this.AppDataSource.getRepository(Tool).create(newTool) - const results = await this.AppDataSource.getRepository(Tool).save(tool) - - await this.telemetry.sendTelemetry('tool_created', { - version: await getAppVersion(), - toolId: results.id, - toolName: results.name - }) - - return res.json(results) - }) - - // Update tool - this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { - const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ - id: req.params.id - }) - - if (!tool) { - res.status(404).send(`Tool ${req.params.id} not found`) - return - } - - const body = req.body - const updateTool = new Tool() - Object.assign(updateTool, body) - - this.AppDataSource.getRepository(Tool).merge(tool, updateTool) - const result = await this.AppDataSource.getRepository(Tool).save(tool) - - return res.json(result) - }) - - // Delete tool - this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { - const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) - return res.json(results) - }) - - // ---------------------------------------- - // Assistant - // ---------------------------------------- - - // Get all assistants - this.app.get('/api/v1/assistants', async (req: Request, res: Response) => { - const assistants = await this.AppDataSource.getRepository(Assistant).find() - return res.json(assistants) - }) - - // Get specific assistant - this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { - const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ - id: req.params.id - }) - return res.json(assistant) - }) - - // Get assistant object - this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) - - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) - const resp = await openai.files.list() - const existingFiles = resp.data ?? [] - - if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { - ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) - } - - return res.json(retrievedAssistant) - }) - - // List available assistants - this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) - - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistants = await openai.beta.assistants.list() - - return res.json(retrievedAssistants.data) - }) - - // Add assistant - this.app.post('/api/v1/assistants', async (req: Request, res: Response) => { - const body = req.body - - if (!body.details) return res.status(500).send(`Invalid request body`) - - const assistantDetails = JSON.parse(body.details) - - try { - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: body.credential - }) - - if (!credential) return res.status(404).send(`Credential ${body.credential} not found`) - - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - - const openai = new OpenAI({ apiKey: openAIApiKey }) - - let tools = [] - if (assistantDetails.tools) { - for (const tool of assistantDetails.tools ?? []) { - tools.push({ - type: tool - }) - } - } - - if (assistantDetails.uploadFiles) { - // Base64 strings - let files: string[] = [] - const fileBase64 = assistantDetails.uploadFiles - if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) { - files = JSON.parse(fileBase64) - } else { - files = [fileBase64] - } - - const uploadedFiles = [] - for (const file of files) { - const splitDataURI = file.split(',') - const filename = splitDataURI.pop()?.split(':')[1] ?? '' - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename) - if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }) - } - if (!fs.existsSync(filePath)) { - fs.writeFileSync(filePath, bf) - } - - const createdFile = await openai.files.create({ - file: fs.createReadStream(filePath), - purpose: 'assistants' - }) - uploadedFiles.push(createdFile) - - fs.unlinkSync(filePath) - } - assistantDetails.files = [...assistantDetails.files, ...uploadedFiles] - } - - if (!assistantDetails.id) { - const newAssistant = await openai.beta.assistants.create({ - name: assistantDetails.name, - description: assistantDetails.description, - instructions: assistantDetails.instructions, - model: assistantDetails.model, - tools, - file_ids: (assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id) - }) - assistantDetails.id = newAssistant.id - } else { - const retrievedAssistant = await openai.beta.assistants.retrieve(assistantDetails.id) - let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual) - filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) - - await openai.beta.assistants.update(assistantDetails.id, { - name: assistantDetails.name, - description: assistantDetails.description ?? '', - instructions: assistantDetails.instructions ?? '', - model: assistantDetails.model, - tools: filteredTools, - file_ids: uniqWith( - [ - ...retrievedAssistant.file_ids, - ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id) - ], - isEqual - ) - }) - } - - const newAssistantDetails = { - ...assistantDetails - } - if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles - - body.details = JSON.stringify(newAssistantDetails) - } catch (error) { - return res.status(500).send(`Error creating new assistant: ${error}`) - } - - const newAssistant = new Assistant() - Object.assign(newAssistant, body) - - const assistant = this.AppDataSource.getRepository(Assistant).create(newAssistant) - const results = await this.AppDataSource.getRepository(Assistant).save(assistant) - - await this.telemetry.sendTelemetry('assistant_created', { - version: await getAppVersion(), - assistantId: results.id - }) - - return res.json(results) - }) - - // Update assistant - this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { - const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ - id: req.params.id - }) - - if (!assistant) { - res.status(404).send(`Assistant ${req.params.id} not found`) - return - } - - try { - const openAIAssistantId = JSON.parse(assistant.details)?.id - - const body = req.body - const assistantDetails = JSON.parse(body.details) - - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: body.credential - }) - - if (!credential) return res.status(404).send(`Credential ${body.credential} not found`) - - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - - const openai = new OpenAI({ apiKey: openAIApiKey }) - - let tools = [] - if (assistantDetails.tools) { - for (const tool of assistantDetails.tools ?? []) { - tools.push({ - type: tool - }) - } - } - - if (assistantDetails.uploadFiles) { - // Base64 strings - let files: string[] = [] - const fileBase64 = assistantDetails.uploadFiles - if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) { - files = JSON.parse(fileBase64) - } else { - files = [fileBase64] - } - - const uploadedFiles = [] - for (const file of files) { - const splitDataURI = file.split(',') - const filename = splitDataURI.pop()?.split(':')[1] ?? '' - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename) - if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }) - } - if (!fs.existsSync(filePath)) { - fs.writeFileSync(filePath, bf) - } - - const createdFile = await openai.files.create({ - file: fs.createReadStream(filePath), - purpose: 'assistants' - }) - uploadedFiles.push(createdFile) - - fs.unlinkSync(filePath) - } - assistantDetails.files = [...assistantDetails.files, ...uploadedFiles] - } - - const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) - let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual) - filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) - - await openai.beta.assistants.update(openAIAssistantId, { - name: assistantDetails.name, - description: assistantDetails.description, - instructions: assistantDetails.instructions, - model: assistantDetails.model, - tools: filteredTools, - file_ids: uniqWith( - [...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)], - isEqual - ) - }) - - const newAssistantDetails = { - ...assistantDetails, - id: openAIAssistantId - } - if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles - - const updateAssistant = new Assistant() - body.details = JSON.stringify(newAssistantDetails) - Object.assign(updateAssistant, body) - - this.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant) - const result = await this.AppDataSource.getRepository(Assistant).save(assistant) - - return res.json(result) - } catch (error) { - return res.status(500).send(`Error updating assistant: ${error}`) - } - }) - - // Delete assistant - this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { - const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ - id: req.params.id - }) - - if (!assistant) { - res.status(404).send(`Assistant ${req.params.id} not found`) - return - } - - try { - const assistantDetails = JSON.parse(assistant.details) - - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: assistant.credential - }) - - if (!credential) return res.status(404).send(`Credential ${assistant.credential} not found`) - - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - - const openai = new OpenAI({ apiKey: openAIApiKey }) - - const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id }) - - if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id) - - return res.json(results) - } catch (error: any) { - if (error.status === 404 && error.type === 'invalid_request_error') return res.send('OK') - return res.status(500).send(`Error deleting assistant: ${error}`) - } - }) - - function streamFileToUser(res: Response, filePath: string) { - const fileStream = fs.createReadStream(filePath) - fileStream.pipe(res) - } - - // Download file from assistant - this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { - const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) - //raise error if file path is not absolute - if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) - //raise error if file path contains '..' - if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) - //only return from the .flowise openai-assistant folder - if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`) - - if (fs.existsSync(filePath)) { - res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath))) - streamFileToUser(res, filePath) - } else { - return res.status(404).send(`File ${req.body.fileName} not found`) - } - }) - - this.app.get('/api/v1/get-upload-path', async (req: Request, res: Response) => { - return res.json({ - storagePath: getStoragePath() - }) - }) - - // stream uploaded image - this.app.get('/api/v1/get-upload-file', async (req: Request, res: Response) => { - try { - if (!req.query.chatflowId || !req.query.chatId || !req.query.fileName) { - return res.status(500).send(`Invalid file path`) - } - const chatflowId = req.query.chatflowId as string - const chatId = req.query.chatId as string - const fileName = req.query.fileName as string - - const filePath = path.join(getStoragePath(), chatflowId, chatId, fileName) - //raise error if file path is not absolute - if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) - //raise error if file path contains '..' - if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) - //only return from the storage folder - if (!filePath.startsWith(getStoragePath())) return res.status(500).send(`Invalid file path`) - - if (fs.existsSync(filePath)) { - res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath))) - streamFileToUser(res, filePath) - } else { - return res.status(404).send(`File ${fileName} not found`) - } - } catch (error) { - return res.status(500).send(`Invalid file path`) - } - }) - - // ---------------------------------------- - // Configuration - // ---------------------------------------- - - this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - const availableConfigs = findAvailableConfigs(nodes, this.nodesPool.componentCredentials) - return res.json(availableConfigs) - }) - - this.app.post('/api/v1/node-config', async (req: Request, res: Response) => { - const nodes = [{ data: req.body }] as IReactFlowNode[] - const availableConfigs = findAvailableConfigs(nodes, this.nodesPool.componentCredentials) - return res.json(availableConfigs) - }) - - this.app.get('/api/v1/version', async (req: Request, res: Response) => { - const getPackageJsonPath = (): string => { - const checkPaths = [ - path.join(__dirname, '..', 'package.json'), - path.join(__dirname, '..', '..', 'package.json'), - path.join(__dirname, '..', '..', '..', 'package.json'), - path.join(__dirname, '..', '..', '..', '..', 'package.json'), - path.join(__dirname, '..', '..', '..', '..', '..', 'package.json') - ] - for (const checkPath of checkPaths) { - if (fs.existsSync(checkPath)) { - return checkPath - } - } - return '' - } - - const packagejsonPath = getPackageJsonPath() - if (!packagejsonPath) return res.status(404).send('Version not found') - try { - const content = await fs.promises.readFile(packagejsonPath, 'utf8') - const parsedContent = JSON.parse(content) - return res.json({ version: parsedContent.version }) - } catch (error) { - return res.status(500).send(`Version not found: ${error}`) - } - }) - - // ---------------------------------------- - // Scraper - // ---------------------------------------- - - this.app.get('/api/v1/fetch-links', async (req: Request, res: Response) => { - try { - const url = decodeURIComponent(req.query.url as string) - const relativeLinksMethod = req.query.relativeLinksMethod as string - if (!relativeLinksMethod) { - return res.status(500).send('Please choose a Relative Links Method in Additional Parameters.') - } - - const limit = parseInt(req.query.limit as string) - if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) - const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) - if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) - - res.json({ status: 'OK', links }) - } catch (e: any) { - return res.status(500).send('Could not fetch links from the URL.') - } - }) - - // ---------------------------------------- - // Upsert - // ---------------------------------------- - - this.app.post( - '/api/v1/vector/upsert/:id', - upload.array('files'), - (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), - async (req: Request, res: Response) => { - await this.upsertVector(req, res) - } - ) - - this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { - await this.upsertVector(req, res, true) - }) - - // ---------------------------------------- - // Prompt from Hub - // ---------------------------------------- - this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => { - try { - let hub = new Client() - const prompt = await hub.pull(req.body.promptName) - const templates = parsePrompt(prompt) - return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates }) - } catch (e: any) { - return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message }) - } - }) - - this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => { - try { - const tags = req.body.tags ? `tags=${req.body.tags}` : '' - // Default to 100, TODO: add pagination and use offset & limit - const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` - axios.get(url).then((response) => { - if (response.data.repos) { - return res.json({ status: 'OK', repos: response.data.repos }) - } - }) - } catch (e: any) { - return res.json({ status: 'ERROR', repos: [] }) - } - }) - - // ---------------------------------------- - // Prediction - // ---------------------------------------- - - // Send input message and get prediction result (External) - this.app.post( - '/api/v1/prediction/:id', - upload.array('files'), - (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), - async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - let isDomainAllowed = true - logger.info(`[server]: Request originated from ${req.headers.origin}`) - if (chatflow.chatbotConfig) { - const parsedConfig = JSON.parse(chatflow.chatbotConfig) - // check whether the first one is not empty. if it is empty that means the user set a value and then removed it. - const isValidAllowedOrigins = parsedConfig.allowedOrigins?.length && parsedConfig.allowedOrigins[0] !== '' - if (isValidAllowedOrigins) { - const originHeader = req.headers.origin as string - const origin = new URL(originHeader).host - isDomainAllowed = - parsedConfig.allowedOrigins.filter((domain: string) => { - try { - const allowedOrigin = new URL(domain).host - return origin === allowedOrigin - } catch (e) { - return false - } - }).length > 0 - } - } - - if (isDomainAllowed) { - await this.buildChatflow(req, res, socketIO) - } else { - return res.status(401).send(`This site is not allowed to access this chatbot`) - } - } - ) - - // Send input message and get prediction result (Internal) - this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { - await this.buildChatflow(req, res, socketIO, true) - }) - - // ---------------------------------------- - // Marketplaces - // ---------------------------------------- - - // Get all templates for marketplaces - this.app.get('/api/v1/marketplaces/templates', async (req: Request, res: Response) => { - let marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') - let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') - let templates: any[] = [] - jsonsInDir.forEach((file, index) => { - const filePath = path.join(__dirname, '..', 'marketplaces', 'chatflows', file) - const fileData = fs.readFileSync(filePath) - const fileDataObj = JSON.parse(fileData.toString()) - const template = { - id: index, - templateName: file.split('.json')[0], - flowData: fileData.toString(), - badge: fileDataObj?.badge, - framework: fileDataObj?.framework, - categories: fileDataObj?.categories, - type: 'Chatflow', - description: fileDataObj?.description || '' - } - templates.push(template) - }) - - marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools') - jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') - jsonsInDir.forEach((file, index) => { - const filePath = path.join(__dirname, '..', 'marketplaces', 'tools', file) - const fileData = fs.readFileSync(filePath) - const fileDataObj = JSON.parse(fileData.toString()) - const template = { - ...fileDataObj, - id: index, - type: 'Tool', - framework: fileDataObj?.framework, - badge: fileDataObj?.badge, - categories: '', - templateName: file.split('.json')[0] - } - templates.push(template) - }) - const sortedTemplates = templates.sort((a, b) => a.templateName.localeCompare(b.templateName)) - const FlowiseDocsQnAIndex = sortedTemplates.findIndex((tmp) => tmp.templateName === 'Flowise Docs QnA') - if (FlowiseDocsQnAIndex > 0) { - sortedTemplates.unshift(sortedTemplates.splice(FlowiseDocsQnAIndex, 1)[0]) - } - return res.json(sortedTemplates) - }) - - // ---------------------------------------- - // Variables - // ---------------------------------------- - this.app.get('/api/v1/variables', async (req: Request, res: Response) => { - const variables = await getDataSource().getRepository(Variable).find() - return res.json(variables) - }) - - // Create new variable - this.app.post('/api/v1/variables', async (req: Request, res: Response) => { - const body = req.body - const newVariable = new Variable() - Object.assign(newVariable, body) - const variable = this.AppDataSource.getRepository(Variable).create(newVariable) - const results = await this.AppDataSource.getRepository(Variable).save(variable) - return res.json(results) - }) - - // Update variable - this.app.put('/api/v1/variables/:id', async (req: Request, res: Response) => { - const variable = await this.AppDataSource.getRepository(Variable).findOneBy({ - id: req.params.id - }) - - if (!variable) return res.status(404).send(`Variable ${req.params.id} not found`) - - const body = req.body - const updateVariable = new Variable() - Object.assign(updateVariable, body) - this.AppDataSource.getRepository(Variable).merge(variable, updateVariable) - const result = await this.AppDataSource.getRepository(Variable).save(variable) - - return res.json(result) - }) - - // Delete variable via id - this.app.delete('/api/v1/variables/:id', async (req: Request, res: Response) => { - const results = await this.AppDataSource.getRepository(Variable).delete({ id: req.params.id }) - return res.json(results) - }) - - // ---------------------------------------- - // API Keys - // ---------------------------------------- - - 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, - category: cf.category, - updatedDate: cf.updatedDate - }) - }) - 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 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 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 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 addChatflowsCount(keys, res) - }) - - // Verify api key - this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => { - try { - const apiKey = await getApiKey(req.params.apiKey) - if (!apiKey) return res.status(401).send('Unauthorized') - return res.status(200).send('OK') - } catch (err: any) { - return res.status(500).send(err?.message) - } - }) + this.app.use('/api/v1', flowiseApiV1Router) // ---------------------------------------- // Serve UI static @@ -1636,750 +160,12 @@ export class App { this.app.use('/', express.static(uiBuildPath)) // All other requests not handled will return React app - this.app.use((req, res) => { + this.app.use((req: Request, res: Response) => { res.sendFile(uiHtmlPath) }) - } - /** - * Validate API Key - * @param {Request} req - * @param {Response} res - * @param {ChatFlow} chatflow - */ - async validateKey(req: Request, chatflow: ChatFlow) { - const chatFlowApiKeyId = chatflow.apikeyid - if (!chatFlowApiKeyId) return true - - const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' - if (chatFlowApiKeyId && !authorizationHeader) return false - - const suppliedKey = authorizationHeader.split(`Bearer `).pop() - if (suppliedKey) { - const keys = await getAPIKeys() - const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret - if (!compareKeys(apiSecret, suppliedKey)) return false - return true - } - return false - } - - /** - * Method that checks if uploads are enabled in the chatflow - * @param {string} chatflowid - */ - async getUploadsConfig(chatflowid: string): Promise { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowid - }) - if (!chatflow) return `Chatflow ${chatflowid} not found` - - const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent'] - const uploadProcessingNodes = ['chatOpenAI', 'chatAnthropic', 'awsChatBedrock', 'azureChatOpenAI'] - - const flowObj = JSON.parse(chatflow.flowData) - const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = [] - - let isSpeechToTextEnabled = false - if (chatflow.speechToText) { - const speechToTextProviders = JSON.parse(chatflow.speechToText) - for (const provider in speechToTextProviders) { - if (provider !== 'none') { - const providerObj = speechToTextProviders[provider] - if (providerObj.status) { - isSpeechToTextEnabled = true - break - } - } - } - } - - let isImageUploadAllowed = false - const nodes: IReactFlowNode[] = flowObj.nodes - - /* - * Condition for isImageUploadAllowed - * 1.) one of the uploadAllowedNodes exists - * 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON - */ - if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) { - return { - isSpeechToTextEnabled, - isImageUploadAllowed: false, - imgUploadSizeAndTypes - } - } - - nodes.forEach((node: IReactFlowNode) => { - if (uploadProcessingNodes.indexOf(node.data.name) > -1) { - // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties - node.data.inputParams.map((param: INodeParams) => { - if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) { - imgUploadSizeAndTypes.push({ - fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'), - maxUploadSize: 5 - }) - isImageUploadAllowed = true - } - }) - } - }) - - return { - isSpeechToTextEnabled, - isImageUploadAllowed, - imgUploadSizeAndTypes - } - } - - /** - * Method that get chat messages. - * @param {string} chatflowid - * @param {chatType} chatType - * @param {string} sortOrder - * @param {string} chatId - * @param {string} memoryType - * @param {string} sessionId - * @param {string} startDate - * @param {string} endDate - * @param {boolean} feedback - */ - async getChatMessage( - chatflowid: string, - chatType: chatType | undefined, - sortOrder: string = 'ASC', - chatId?: string, - memoryType?: string, - sessionId?: string, - startDate?: string, - endDate?: string, - messageId?: string, - feedback?: boolean - ): Promise { - const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { - const date = new Date(dateTimeStr) - if (isNaN(date.getTime())) { - return undefined - } - setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) - return date - } - - const aMonthAgo = () => { - const date = new Date() - date.setMonth(new Date().getMonth() - 1) - return date - } - - let fromDate - if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start') - - let toDate - if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end') - - if (feedback) { - const query = this.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message') - - // do the join with chat message feedback based on messageId for each chat message in the chatflow - query - .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') - .where('chat_message.chatflowid = :chatflowid', { chatflowid }) - - // based on which parameters are available add `andWhere` clauses to the query - if (chatType) { - query.andWhere('chat_message.chatType = :chatType', { chatType }) - } - if (chatId) { - query.andWhere('chat_message.chatId = :chatId', { chatId }) - } - if (memoryType) { - query.andWhere('chat_message.memoryType = :memoryType', { memoryType }) - } - if (sessionId) { - query.andWhere('chat_message.sessionId = :sessionId', { sessionId }) - } - - // set date range - query.andWhere('chat_message.createdDate BETWEEN :fromDate AND :toDate', { - fromDate: fromDate ?? aMonthAgo(), - toDate: toDate ?? new Date() - }) - // sort - query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC') - - const messages = await query.getMany() - return messages - } - - return await this.AppDataSource.getRepository(ChatMessage).find({ - where: { - chatflowid, - chatType, - chatId, - memoryType: memoryType ?? undefined, - sessionId: sessionId ?? undefined, - ...(fromDate && { createdDate: MoreThanOrEqual(fromDate) }), - ...(toDate && { createdDate: LessThanOrEqual(toDate) }), - id: messageId ?? undefined - }, - order: { - createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' - } - }) - } - - /** - * Method that add chat messages. - * @param {Partial} chatMessage - */ - async addChatMessage(chatMessage: Partial): Promise { - const newChatMessage = new ChatMessage() - Object.assign(newChatMessage, chatMessage) - - if (!newChatMessage.createdDate) newChatMessage.createdDate = new Date() - - const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage) - return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) - } - - /** - * Method that get chat messages. - * @param {string} chatflowid - * @param {string} sortOrder - * @param {string} chatId - * @param {string} startDate - * @param {string} endDate - */ - async getChatMessageFeedback( - chatflowid: string, - chatId?: string, - sortOrder: string = 'ASC', - startDate?: string, - endDate?: string - ): Promise { - let fromDate - if (startDate) fromDate = new Date(startDate) - - let toDate - if (endDate) toDate = new Date(endDate) - return await this.AppDataSource.getRepository(ChatMessageFeedback).find({ - where: { - chatflowid, - chatId, - createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined - }, - order: { - createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' - } - }) - } - - /** - * Method that add chat message feedback. - * @param {Partial} chatMessageFeedback - */ - async addChatMessageFeedback(chatMessageFeedback: Partial): Promise { - const newChatMessageFeedback = new ChatMessageFeedback() - Object.assign(newChatMessageFeedback, chatMessageFeedback) - - const feedback = this.AppDataSource.getRepository(ChatMessageFeedback).create(newChatMessageFeedback) - return await this.AppDataSource.getRepository(ChatMessageFeedback).save(feedback) - } - - /** - * Method that updates chat message feedback. - * @param {string} id - * @param {Partial} chatMessageFeedback - */ - async updateChatMessageFeedback(id: string, chatMessageFeedback: Partial) { - const newChatMessageFeedback = new ChatMessageFeedback() - Object.assign(newChatMessageFeedback, chatMessageFeedback) - - await this.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback) - } - - async upsertVector(req: Request, res: Response, isInternal: boolean = false) { - try { - const chatflowid = req.params.id - let incomingInput: IncomingInput = req.body - - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowid - }) - if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) - - if (!isInternal) { - const isKeyValidated = await this.validateKey(req, chatflow) - if (!isKeyValidated) return res.status(401).send('Unauthorized') - } - - const files = (req.files as any[]) || [] - - if (files.length) { - const overrideConfig: ICommonObject = { ...req.body } - for (const file of files) { - const fileData = fs.readFileSync(file.path, { encoding: 'base64' }) - const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}` - - const fileInputField = mapMimeTypeToInputField(file.mimetype) - if (overrideConfig[fileInputField]) { - overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String]) - } else { - overrideConfig[fileInputField] = JSON.stringify([dataBase64String]) - } - } - incomingInput = { - question: req.body.question ?? 'hello', - overrideConfig, - history: [], - stopNodeId: req.body.stopNodeId - } - } - - /*** Get chatflows and prepare data ***/ - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - const edges = parsedFlowData.edges - - let stopNodeId = incomingInput?.stopNodeId ?? '' - let chatHistory = incomingInput?.history - let chatId = incomingInput.chatId ?? '' - let isUpsert = true - - // Get session ID - const memoryNode = findMemoryNode(nodes, edges) - let sessionId = undefined - if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) - - const vsNodes = nodes.filter( - (node) => - node.data.category === 'Vector Stores' && - !node.data.label.includes('Upsert') && - !node.data.label.includes('Load Existing') - ) - if (vsNodes.length > 1 && !stopNodeId) { - return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request') - } else if (vsNodes.length === 1 && !stopNodeId) { - stopNodeId = vsNodes[0].data.id - } else if (!vsNodes.length && !stopNodeId) { - return res.status(500).send('No vector node found') - } - - const { graph } = constructGraphs(nodes, edges, { isReversed: true }) - - const nodeIds = getAllConnectedNodes(graph, stopNodeId) - - const filteredGraph: INodeDirectedGraph = {} - for (const key of nodeIds) { - if (Object.prototype.hasOwnProperty.call(graph, key)) { - filteredGraph[key] = graph[key] - } - } - - const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId) - - await buildFlow( - startingNodeIds, - nodes, - edges, - filteredGraph, - depthQueue, - this.nodesPool.componentNodes, - incomingInput.question, - chatHistory, - chatId, - sessionId ?? '', - chatflowid, - this.AppDataSource, - incomingInput?.overrideConfig, - this.cachePool, - isUpsert, - stopNodeId - ) - - const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id)) - - this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) - - await this.telemetry.sendTelemetry('vector_upserted', { - version: await getAppVersion(), - chatflowId: chatflowid, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, - flowGraph: getTelemetryFlowObj(nodes, edges), - stopNodeId - }) - - return res.status(201).send('Successfully Upserted') - } catch (e: any) { - logger.error('[server]: Error:', e) - return res.status(500).send(e.message) - } - } - - /** - * Build Chatflow - * @param {Request} req - * @param {Response} res - * @param {Server} socketIO - * @param {boolean} isInternal - * @param {boolean} isUpsert - */ - async buildChatflow(req: Request, res: Response, socketIO?: Server, isInternal: boolean = false) { - try { - const chatflowid = req.params.id - let incomingInput: IncomingInput = req.body - - let nodeToExecuteData: INodeData - - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowid - }) - if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) - - const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4() - const userMessageDateTime = new Date() - - if (!isInternal) { - const isKeyValidated = await this.validateKey(req, chatflow) - if (!isKeyValidated) return res.status(401).send('Unauthorized') - } - - let fileUploads: IFileUpload[] = [] - if (incomingInput.uploads) { - fileUploads = incomingInput.uploads - for (let i = 0; i < fileUploads.length; i += 1) { - const upload = fileUploads[i] - if ((upload.type === 'file' || upload.type === 'audio') && upload.data) { - const filename = upload.name - const dir = path.join(getStoragePath(), chatflowid, chatId) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } - const filePath = path.join(dir, filename) - const splitDataURI = upload.data.split(',') - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - fs.writeFileSync(filePath, bf) - - // Omit upload.data since we don't store the content in database - upload.type = 'stored-file' - fileUploads[i] = omit(upload, ['data']) - } - - // Run Speech to Text conversion - if (upload.mime === 'audio/webm') { - let speechToTextConfig: ICommonObject = {} - if (chatflow.speechToText) { - const speechToTextProviders = JSON.parse(chatflow.speechToText) - for (const provider in speechToTextProviders) { - const providerObj = speechToTextProviders[provider] - if (providerObj.status) { - speechToTextConfig = providerObj - speechToTextConfig['name'] = provider - break - } - } - } - if (speechToTextConfig) { - const options: ICommonObject = { - chatId, - chatflowid, - appDataSource: this.AppDataSource, - databaseEntities: databaseEntities - } - const speechToTextResult = await convertSpeechToText(upload, speechToTextConfig, options) - if (speechToTextResult) { - incomingInput.question = speechToTextResult - } - } - } - } - } - - let isStreamValid = false - - const files = (req.files as any[]) || [] - - if (files.length) { - const overrideConfig: ICommonObject = { ...req.body } - for (const file of files) { - const fileData = fs.readFileSync(file.path, { encoding: 'base64' }) - const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}` - - const fileInputField = mapMimeTypeToInputField(file.mimetype) - if (overrideConfig[fileInputField]) { - overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String]) - } else { - overrideConfig[fileInputField] = JSON.stringify([dataBase64String]) - } - } - incomingInput = { - question: req.body.question ?? 'hello', - overrideConfig, - history: [], - socketIOClientId: req.body.socketIOClientId - } - } - - /*** Get chatflows and prepare data ***/ - const flowData = chatflow.flowData - const parsedFlowData: IReactFlowObject = JSON.parse(flowData) - const nodes = parsedFlowData.nodes - const edges = parsedFlowData.edges - - // Get session ID - const memoryNode = findMemoryNode(nodes, edges) - const memoryType = memoryNode?.data.label - let sessionId = undefined - if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) - - /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met: - * - Node Data already exists in pool - * - Still in sync (i.e the flow has not been modified since) - * - Existing overrideConfig and new overrideConfig are the same - * - Flow doesn't start with/contain nodes that depend on incomingInput.question - * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath - ***/ - const isFlowReusable = () => { - return ( - Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) && - this.chatflowPool.activeChatflows[chatflowid].inSync && - this.chatflowPool.activeChatflows[chatflowid].endingNodeData && - isSameOverrideConfig( - isInternal, - this.chatflowPool.activeChatflows[chatflowid].overrideConfig, - incomingInput.overrideConfig - ) && - !isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes, nodes) - ) - } - - if (isFlowReusable()) { - nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData - isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData) - logger.debug( - `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})` - ) - } else { - /*** Get Ending Node with Directed Graph ***/ - const { graph, nodeDependencies } = constructGraphs(nodes, edges) - const directedGraph = graph - const endingNodeIds = getEndingNodes(nodeDependencies, directedGraph) - if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`) - - const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) - - let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode') - - for (const endingNode of endingNodes) { - const endingNodeData = endingNode.data - if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`) - - const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode' - - if (!isEndingNode) { - if ( - endingNodeData && - endingNodeData.category !== 'Chains' && - endingNodeData.category !== 'Agents' && - endingNodeData.category !== 'Engine' - ) { - return res.status(500).send(`Ending node must be either a Chain or Agent`) - } - - if ( - endingNodeData.outputs && - Object.keys(endingNodeData.outputs).length && - !Object.values(endingNodeData.outputs ?? {}).includes(endingNodeData.name) - ) { - return res - .status(500) - .send( - `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction` - ) - } - } - - isStreamValid = isFlowValidForStream(nodes, endingNodeData) - } - - // Once custom function ending node exists, flow is always unavailable to stream - isStreamValid = isEndingNodeExists ? false : isStreamValid - - let chatHistory: IMessage[] = incomingInput.history ?? [] - - // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node - for (const endingNode of endingNodes) { - const endingNodeData = endingNode.data - - if (!endingNodeData.inputs?.memory) continue - - const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') - const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) - - if (!memoryNode) continue - - if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { - chatHistory = await getSessionChatHistory( - memoryNode, - this.nodesPool.componentNodes, - incomingInput, - this.AppDataSource, - databaseEntities, - logger - ) - } - } - - /*** Get Starting Nodes with Reversed Graph ***/ - const constructedObj = constructGraphs(nodes, edges, { isReversed: true }) - const nonDirectedGraph = constructedObj.graph - let startingNodeIds: string[] = [] - let depthQueue: IDepthQueue = {} - for (const endingNodeId of endingNodeIds) { - const res = getStartingNodes(nonDirectedGraph, endingNodeId) - startingNodeIds.push(...res.startingNodeIds) - depthQueue = Object.assign(depthQueue, res.depthQueue) - } - startingNodeIds = [...new Set(startingNodeIds)] - - const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) - - logger.debug(`[server]: Start building chatflow ${chatflowid}`) - /*** BFS to traverse from Starting Nodes to Ending Node ***/ - const reactFlowNodes = await buildFlow( - startingNodeIds, - nodes, - edges, - graph, - depthQueue, - this.nodesPool.componentNodes, - incomingInput.question, - chatHistory, - chatId, - sessionId ?? '', - chatflowid, - this.AppDataSource, - incomingInput?.overrideConfig, - this.cachePool, - false, - undefined, - incomingInput.uploads - ) - - const nodeToExecute = - endingNodeIds.length === 1 - ? reactFlowNodes.find((node: IReactFlowNode) => endingNodeIds[0] === node.id) - : reactFlowNodes[reactFlowNodes.length - 1] - if (!nodeToExecute) return res.status(404).send(`Node not found`) - - if (incomingInput.overrideConfig) { - nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig) - } - - const reactFlowNodeData: INodeData = resolveVariables( - nodeToExecute.data, - reactFlowNodes, - incomingInput.question, - chatHistory - ) - nodeToExecuteData = reactFlowNodeData - - this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) - } - - logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - - const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const nodeInstance = new nodeModule.nodeClass({ sessionId }) - - let result = isStreamValid - ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatId, - chatflowid, - chatHistory: incomingInput.history, - logger, - appDataSource: this.AppDataSource, - databaseEntities, - analytic: chatflow.analytic, - uploads: incomingInput.uploads, - socketIO, - socketIOClientId: incomingInput.socketIOClientId - }) - : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { - chatId, - chatflowid, - chatHistory: incomingInput.history, - logger, - appDataSource: this.AppDataSource, - databaseEntities, - analytic: chatflow.analytic, - uploads: incomingInput.uploads - }) - - result = typeof result === 'string' ? { text: result } : result - - // Retrieve threadId from assistant if exists - if (typeof result === 'object' && result.assistant) { - sessionId = result.assistant.threadId - } - - const userMessage: Omit = { - role: 'userMessage', - content: incomingInput.question, - chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, - chatId, - memoryType, - sessionId, - createdDate: userMessageDateTime, - fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined - } - await this.addChatMessage(userMessage) - - let resultText = '' - if (result.text) resultText = result.text - else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2) - else resultText = JSON.stringify(result, null, 2) - - const apiMessage: Omit = { - role: 'apiMessage', - content: resultText, - chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, - chatId, - memoryType, - sessionId - } - 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) - const chatMessage = await this.addChatMessage(apiMessage) - - logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - await this.telemetry.sendTelemetry('prediction_sent', { - version: await getAppVersion(), - chatflowId: chatflowid, - chatId, - type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, - flowGraph: getTelemetryFlowObj(nodes, edges) - }) - - // Prepare response - // return the question in the response - // this is used when input text is empty but question is in audio format - result.question = incomingInput.question - result.chatId = chatId - result.chatMessageId = chatMessage.id - if (sessionId) result.sessionId = sessionId - if (memoryType) result.memoryType = memoryType - - return res.json(result) - } catch (e: any) { - logger.error('[server]: Error:', e) - return res.status(500).send(e.message) - } + // Error handling + this.app.use(errorHandlerMiddleware) } async stopApp() { diff --git a/packages/server/src/middlewares/errors/index.ts b/packages/server/src/middlewares/errors/index.ts new file mode 100644 index 000000000..3e97daf6d --- /dev/null +++ b/packages/server/src/middlewares/errors/index.ts @@ -0,0 +1,19 @@ +import { NextFunction, Request, Response } from 'express' +import { StatusCodes } from 'http-status-codes' +import { ApiError } from '../../errors/apiError' + +// we need eslint because we have to pass next arg for the error middleware +// eslint-disable-next-line +async function errorHandlerMiddleware(err: ApiError, req: Request, res: Response, next: NextFunction) { + let displayedError = { + statusCode: err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR, + success: false, + message: err.message, + // Provide error stack trace only in development + stack: process.env.NODE_ENV === 'development' ? err.stack : {} + } + res.setHeader('Content-Type', 'application/json') + res.status(displayedError.statusCode).json(displayedError) +} + +export default errorHandlerMiddleware diff --git a/packages/server/src/routes/apikey/index.ts b/packages/server/src/routes/apikey/index.ts new file mode 100644 index 000000000..e82924d87 --- /dev/null +++ b/packages/server/src/routes/apikey/index.ts @@ -0,0 +1,17 @@ +import express from 'express' +import apikeyController from '../../controllers/apikey' +const router = express.Router() + +// CREATE +router.post('/', apikeyController.createApiKey) + +// READ +router.get('/', apikeyController.getAllApiKeys) + +// UPDATE +router.put('/:id', apikeyController.updateApiKey) + +// DELETE +router.delete('/:id', apikeyController.deleteApiKey) + +export default router diff --git a/packages/server/src/routes/assistants/index.ts b/packages/server/src/routes/assistants/index.ts new file mode 100644 index 000000000..494f8f9de --- /dev/null +++ b/packages/server/src/routes/assistants/index.ts @@ -0,0 +1,19 @@ +import express from 'express' +import assistantsController from '../../controllers/assistants' + +const router = express.Router() + +// CREATE +router.post('/', assistantsController.creatAssistant) + +// READ +router.get('/', assistantsController.getAllAssistants) +router.get('/:id', assistantsController.getAssistantById) + +// UPDATE +router.put('/:id', assistantsController.updateAssistant) + +// DELETE +router.delete('/:id', assistantsController.deleteAssistant) + +export default router diff --git a/packages/server/src/routes/chat-messages/index.ts b/packages/server/src/routes/chat-messages/index.ts new file mode 100644 index 000000000..dbb3e9f12 --- /dev/null +++ b/packages/server/src/routes/chat-messages/index.ts @@ -0,0 +1,16 @@ +import express from 'express' +import chatMessageController from '../../controllers/chat-messages' +const router = express.Router() + +// CREATE +router.post('/:id', chatMessageController.createChatMessage) + +// READ +router.get('/:id', chatMessageController.getAllChatMessages) + +// UPDATE + +// DELETE +router.delete('/:id', chatMessageController.removeAllChatMessages) + +export default router diff --git a/packages/server/src/routes/chatflows-streaming/index.ts b/packages/server/src/routes/chatflows-streaming/index.ts new file mode 100644 index 000000000..34ca4e0c7 --- /dev/null +++ b/packages/server/src/routes/chatflows-streaming/index.ts @@ -0,0 +1,9 @@ +import express from 'express' +import chatflowsController from '../../controllers/chatflows' + +const router = express.Router() + +// READ +router.get('/:id', chatflowsController.checkIfChatflowIsValidForStreaming) + +export default router diff --git a/packages/server/src/routes/chatflows-uploads/index.ts b/packages/server/src/routes/chatflows-uploads/index.ts new file mode 100644 index 000000000..b3b3ff03d --- /dev/null +++ b/packages/server/src/routes/chatflows-uploads/index.ts @@ -0,0 +1,9 @@ +import express from 'express' +import chatflowsController from '../../controllers/chatflows' + +const router = express.Router() + +// READ +router.get('/:id', chatflowsController.checkIfChatflowIsValidForUploads) + +export default router diff --git a/packages/server/src/routes/chatflows/index.ts b/packages/server/src/routes/chatflows/index.ts new file mode 100644 index 000000000..8cd3fcc01 --- /dev/null +++ b/packages/server/src/routes/chatflows/index.ts @@ -0,0 +1,19 @@ +import express from 'express' +import chatflowsController from '../../controllers/chatflows' +const router = express.Router() + +// CREATE +router.post('/', chatflowsController.saveChatflow) + +// READ +router.get('/', chatflowsController.getAllChatflows) +router.get('/:id', chatflowsController.getChatflowById) +router.get('/apikey/:apikey', chatflowsController.getChatflowByApiKey) + +// UPDATE +router.put('/:id', chatflowsController.updateChatflow) + +// DELETE +router.delete('/:id', chatflowsController.deleteChatflow) + +export default router diff --git a/packages/server/src/routes/components-credentials-icon/index.ts b/packages/server/src/routes/components-credentials-icon/index.ts new file mode 100644 index 000000000..364a87ba3 --- /dev/null +++ b/packages/server/src/routes/components-credentials-icon/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import componentsCredentialsController from '../../controllers/components-credentials' +const router = express.Router() + +// CREATE + +// READ +router.get('/:name', componentsCredentialsController.getSingleComponentsCredentialIcon) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/components-credentials/index.ts b/packages/server/src/routes/components-credentials/index.ts new file mode 100644 index 000000000..2f7b23646 --- /dev/null +++ b/packages/server/src/routes/components-credentials/index.ts @@ -0,0 +1,9 @@ +import express from 'express' +import componentsCredentialsController from '../../controllers/components-credentials' +const router = express.Router() + +// READ +router.get('/', componentsCredentialsController.getAllComponentsCredentials) +router.get('/:name', componentsCredentialsController.getComponentByName) + +export default router diff --git a/packages/server/src/routes/credentials/index.ts b/packages/server/src/routes/credentials/index.ts new file mode 100644 index 000000000..0229798bf --- /dev/null +++ b/packages/server/src/routes/credentials/index.ts @@ -0,0 +1,18 @@ +import express from 'express' +import credentialsController from '../../controllers/credentials' +const router = express.Router() + +// CREATE +router.post('/', credentialsController.createCredential) + +// READ +router.get('/', credentialsController.getAllCredentials) +router.get('/:id', credentialsController.getCredentialById) + +// UPDATE +router.put('/:id', credentialsController.updateCredential) + +// DELETE +router.delete('/:id', credentialsController.deleteCredentials) + +export default router diff --git a/packages/server/src/routes/feedback/index.ts b/packages/server/src/routes/feedback/index.ts new file mode 100644 index 000000000..3502a9fb3 --- /dev/null +++ b/packages/server/src/routes/feedback/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import feedbackController from '../../controllers/feedback' +const router = express.Router() + +// CREATE +router.post('/:id', feedbackController.createChatMessageFeedbackForChatflow) + +// READ +router.get('/:id', feedbackController.getAllChatMessageFeedback) + +// UPDATE +router.put('/:id', feedbackController.updateChatMessageFeedbackForChatflow) + +export default router diff --git a/packages/server/src/routes/fetch-links/index.ts b/packages/server/src/routes/fetch-links/index.ts new file mode 100644 index 000000000..a02abd588 --- /dev/null +++ b/packages/server/src/routes/fetch-links/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import fetchLinksController from '../../controllers/fetch-links' +const router = express.Router() + +// READ +router.get('/', fetchLinksController.getAllLinks) + +export default router diff --git a/packages/server/src/routes/flow-config/index.ts b/packages/server/src/routes/flow-config/index.ts new file mode 100644 index 000000000..1aa46517e --- /dev/null +++ b/packages/server/src/routes/flow-config/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import flowConfigsController from '../../controllers/flow-configs' +const router = express.Router() + +// CREATE + +// READ +router.get('/:id', flowConfigsController.getSingleFlowConfig) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/get-upload-file/index.ts b/packages/server/src/routes/get-upload-file/index.ts new file mode 100644 index 000000000..cb871ed30 --- /dev/null +++ b/packages/server/src/routes/get-upload-file/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import getUploadFileController from '../../controllers/get-upload-file' +const router = express.Router() + +// READ +router.get('/', getUploadFileController.streamUploadedImage) + +export default router diff --git a/packages/server/src/routes/get-upload-path/index.ts b/packages/server/src/routes/get-upload-path/index.ts new file mode 100644 index 000000000..48827c9a1 --- /dev/null +++ b/packages/server/src/routes/get-upload-path/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import getUploadPathController from '../../controllers/get-upload-path' +const router = express.Router() + +// READ +router.get('/', getUploadPathController.getPathForUploads) + +export default router diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts new file mode 100644 index 000000000..c989880b2 --- /dev/null +++ b/packages/server/src/routes/index.ts @@ -0,0 +1,78 @@ +import express from 'express' +import apikeyRouter from './apikey' +import assistantsRouter from './assistants' +import chatflowsRouter from './chatflows' +import chatflowsStreamingRouter from './chatflows-streaming' +import chatflowsUploadsRouter from './chatflows-uploads' +import chatMessageRouter from './chat-messages' +import componentsCredentialsRouter from './components-credentials' +import componentsCredentialsIconRouter from './components-credentials-icon' +import credentialsRouter from './credentials' +import feedbackRouter from './feedback' +import fetchLinksRouter from './fetch-links' +import flowConfigRouter from './flow-config' +import internalChatmessagesRouter from './internal-chat-messages' +import internalPredictionRouter from './internal-predictions' +import ipRouter from './ip' +import getUploadFileRouter from './get-upload-file' +import getUploadPathRouter from './get-upload-path' +import loadPromptRouter from './load-prompts' +import marketplacesRouter from './marketplaces' +import nodeConfigRouter from './node-configs' +import nodeCustomFunctionRouter from './node-custom-functions' +import nodeIconRouter from './node-icons' +import nodeLoadMethodRouter from './node-load-methods' +import nodesRouter from './nodes' +import openaiAssistantsRouter from './openai-assistants' +import openaiAssistantsFileRouter from './openai-assistants-files' +import predictionRouter from './predictions' +import promptListsRouter from './prompts-lists' +import publicChatbotRouter from './public-chatbots' +import publicChatflowsRouter from './public-chatflows' +import statsRouter from './stats' +import toolsRouter from './tools' +import variablesRouter from './variables' +import vectorRouter from './vectors' +import verifyRouter from './verify' +import versionRouter from './versions' + +const router = express.Router() + +router.use('/apikey', apikeyRouter) +router.use('/assistants', assistantsRouter) +router.use('/chatflows', chatflowsRouter) +router.use('/chatflows-streaming', chatflowsStreamingRouter) +router.use('/chatmessage', chatMessageRouter) +router.use('/components-credentials', componentsCredentialsRouter) +router.use('/components-credentials-icon', componentsCredentialsIconRouter) +router.use('/chatflows-uploads', chatflowsUploadsRouter) +router.use('/credentials', credentialsRouter) +router.use('/feedback', feedbackRouter) +router.use('/fetch-links', fetchLinksRouter) +router.use('/flow-config', flowConfigRouter) +router.use('/internal-chatmessage', internalChatmessagesRouter) +router.use('/internal-prediction', internalPredictionRouter) +router.use('/ip', ipRouter) +router.use('/get-upload-file', getUploadFileRouter) +router.use('/get-upload-path', getUploadPathRouter) +router.use('/load-prompt', loadPromptRouter) +router.use('/marketplaces', marketplacesRouter) +router.use('/node-config', nodeConfigRouter) +router.use('/node-custom-function', nodeCustomFunctionRouter) +router.use('/node-icon', nodeIconRouter) +router.use('/node-load-method', nodeLoadMethodRouter) +router.use('/nodes', nodesRouter) +router.use('/openai-assistants', openaiAssistantsRouter) +router.use('/openai-assistants-file', openaiAssistantsFileRouter) +router.use('/prediction', predictionRouter) +router.use('/prompts-list', promptListsRouter) +router.use('/public-chatbotConfig', publicChatbotRouter) +router.use('/public-chatflows', publicChatflowsRouter) +router.use('/stats', statsRouter) +router.use('/tools', toolsRouter) +router.use('/variables', variablesRouter) +router.use('/vector', vectorRouter) +router.use('/verify', verifyRouter) +router.use('/version', versionRouter) + +export default router diff --git a/packages/server/src/routes/internal-chat-messages/index.ts b/packages/server/src/routes/internal-chat-messages/index.ts new file mode 100644 index 000000000..fe05fdbc9 --- /dev/null +++ b/packages/server/src/routes/internal-chat-messages/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import chatMessagesController from '../../controllers/chat-messages' +const router = express.Router() + +// CREATE + +// READ +router.get('/:id', chatMessagesController.getAllInternalChatMessages) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/internal-predictions/index.ts b/packages/server/src/routes/internal-predictions/index.ts new file mode 100644 index 000000000..6e1c17ba3 --- /dev/null +++ b/packages/server/src/routes/internal-predictions/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import internalPredictionsController from '../../controllers/internal-predictions' +const router = express.Router() + +// CREATE +router.post('/:id', internalPredictionsController.createInternalPrediction) + +export default router diff --git a/packages/server/src/routes/ip/index.ts b/packages/server/src/routes/ip/index.ts new file mode 100644 index 000000000..04de78181 --- /dev/null +++ b/packages/server/src/routes/ip/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import ipController from '../../controllers/ip' +const router = express.Router() + +// CREATE + +// READ +router.get('/', ipController.configureProxyNrInHostEnv) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/load-prompts/index.ts b/packages/server/src/routes/load-prompts/index.ts new file mode 100644 index 000000000..a12afba00 --- /dev/null +++ b/packages/server/src/routes/load-prompts/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import loadPromptsController from '../../controllers/load-prompts' +const router = express.Router() + +// CREATE +router.post('/', loadPromptsController.createPrompt) + +export default router diff --git a/packages/server/src/routes/marketplaces/index.ts b/packages/server/src/routes/marketplaces/index.ts new file mode 100644 index 000000000..c7eae1f3e --- /dev/null +++ b/packages/server/src/routes/marketplaces/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import marketplacesController from '../../controllers/marketplaces' +const router = express.Router() + +// READ +router.get('/templates', marketplacesController.getAllTemplates) + +export default router diff --git a/packages/server/src/routes/node-configs/index.ts b/packages/server/src/routes/node-configs/index.ts new file mode 100644 index 000000000..4c4451388 --- /dev/null +++ b/packages/server/src/routes/node-configs/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import nodeConfigsController from '../../controllers/node-configs' +const router = express.Router() + +// CREATE +router.post('/', nodeConfigsController.getAllNodeConfigs) + +export default router diff --git a/packages/server/src/routes/node-custom-functions/index.ts b/packages/server/src/routes/node-custom-functions/index.ts new file mode 100644 index 000000000..9fa33d42e --- /dev/null +++ b/packages/server/src/routes/node-custom-functions/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import nodesRouter from '../../controllers/nodes' +const router = express.Router() + +// CREATE + +// READ +router.post('/', nodesRouter.executeCustomFunction) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/node-icons/index.ts b/packages/server/src/routes/node-icons/index.ts new file mode 100644 index 000000000..0781226ad --- /dev/null +++ b/packages/server/src/routes/node-icons/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import nodesController from '../../controllers/nodes' +const router = express.Router() + +// CREATE + +// READ +router.get('/:name', nodesController.getSingleNodeIcon) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/node-load-methods/index.ts b/packages/server/src/routes/node-load-methods/index.ts new file mode 100644 index 000000000..941190f1f --- /dev/null +++ b/packages/server/src/routes/node-load-methods/index.ts @@ -0,0 +1,7 @@ +import express from 'express' +import nodesRouter from '../../controllers/nodes' +const router = express.Router() + +router.post('/:name', nodesRouter.getSingleNodeAsyncOptions) + +export default router diff --git a/packages/server/src/routes/nodes/index.ts b/packages/server/src/routes/nodes/index.ts new file mode 100644 index 000000000..b875036ef --- /dev/null +++ b/packages/server/src/routes/nodes/index.ts @@ -0,0 +1,9 @@ +import express from 'express' +import nodesController from '../../controllers/nodes' +const router = express.Router() + +// READ +router.get('/', nodesController.getAllNodes) +router.get('/:name', nodesController.getNodeByName) + +export default router diff --git a/packages/server/src/routes/openai-assistants-files/index.ts b/packages/server/src/routes/openai-assistants-files/index.ts new file mode 100644 index 000000000..3f8b85010 --- /dev/null +++ b/packages/server/src/routes/openai-assistants-files/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import openaiAssistantsController from '../../controllers/openai-assistants' +const router = express.Router() + +// CREATE +router.post('/', openaiAssistantsController.getFileFromAssistant) + +export default router diff --git a/packages/server/src/routes/openai-assistants/index.ts b/packages/server/src/routes/openai-assistants/index.ts new file mode 100644 index 000000000..6e3378fd9 --- /dev/null +++ b/packages/server/src/routes/openai-assistants/index.ts @@ -0,0 +1,15 @@ +import express from 'express' +import openaiAssistantsController from '../../controllers/openai-assistants' +const router = express.Router() + +// CREATE + +// READ +router.get('/', openaiAssistantsController.getAllOpenaiAssistants) +router.get('/:id', openaiAssistantsController.getSingleOpenaiAssistant) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/predictions/index.ts b/packages/server/src/routes/predictions/index.ts new file mode 100644 index 000000000..613c67601 --- /dev/null +++ b/packages/server/src/routes/predictions/index.ts @@ -0,0 +1,13 @@ +import express from 'express' +import multer from 'multer' +import path from 'path' +import predictionsController from '../../controllers/predictions' + +const router = express.Router() + +const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` }) + +// CREATE +router.post('/:id', upload.array('files'), predictionsController.getRateLimiterMiddleware, predictionsController.createPrediction) + +export default router diff --git a/packages/server/src/routes/prompts-lists/index.ts b/packages/server/src/routes/prompts-lists/index.ts new file mode 100644 index 000000000..9b92c365c --- /dev/null +++ b/packages/server/src/routes/prompts-lists/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import promptsListController from '../../controllers/prompts-lists' +const router = express.Router() + +// CREATE +router.post('/', promptsListController.createPromptsList) + +export default router diff --git a/packages/server/src/routes/public-chatbots/index.ts b/packages/server/src/routes/public-chatbots/index.ts new file mode 100644 index 000000000..5a367c683 --- /dev/null +++ b/packages/server/src/routes/public-chatbots/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import chatflowsController from '../../controllers/chatflows' +const router = express.Router() + +// CREATE + +// READ +router.get('/:id', chatflowsController.getSinglePublicChatbotConfig) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/public-chatflows/index.ts b/packages/server/src/routes/public-chatflows/index.ts new file mode 100644 index 000000000..97e23ea79 --- /dev/null +++ b/packages/server/src/routes/public-chatflows/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import chatflowsController from '../../controllers/chatflows' +const router = express.Router() + +// CREATE + +// READ +router.get('/:id', chatflowsController.getSinglePublicChatflow) + +// UPDATE + +// DELETE + +export default router diff --git a/packages/server/src/routes/stats/index.ts b/packages/server/src/routes/stats/index.ts new file mode 100644 index 000000000..ea6d8db3c --- /dev/null +++ b/packages/server/src/routes/stats/index.ts @@ -0,0 +1,9 @@ +import express from 'express' +import statsController from '../../controllers/stats' + +const router = express.Router() + +// READ +router.get('/:id', statsController.getChatflowStats) + +export default router diff --git a/packages/server/src/routes/tools/index.ts b/packages/server/src/routes/tools/index.ts new file mode 100644 index 000000000..4d63b1615 --- /dev/null +++ b/packages/server/src/routes/tools/index.ts @@ -0,0 +1,19 @@ +import express from 'express' +import toolsController from '../../controllers/tools' + +const router = express.Router() + +// CREATE +router.post('/', toolsController.creatTool) + +// READ +router.get('/', toolsController.getAllTools) +router.get('/:id', toolsController.getToolById) + +// UPDATE +router.put('/:id', toolsController.updateTool) + +// DELETE +router.delete('/:id', toolsController.deleteTool) + +export default router diff --git a/packages/server/src/routes/variables/index.ts b/packages/server/src/routes/variables/index.ts new file mode 100644 index 000000000..eece55588 --- /dev/null +++ b/packages/server/src/routes/variables/index.ts @@ -0,0 +1,18 @@ +import express from 'express' +import variablesController from '../../controllers/variables' + +const router = express.Router() + +// CREATE +router.post('/', variablesController.createVariable) + +// READ +router.get('/', variablesController.getAllVariables) + +// UPDATE +router.put('/:id', variablesController.updateVariable) + +// DELETE +router.delete('/:id', variablesController.deleteVariable) + +export default router diff --git a/packages/server/src/routes/vectors/index.ts b/packages/server/src/routes/vectors/index.ts new file mode 100644 index 000000000..21ffd97d4 --- /dev/null +++ b/packages/server/src/routes/vectors/index.ts @@ -0,0 +1,14 @@ +import express from 'express' +import multer from 'multer' +import path from 'path' +import vectorsController from '../../controllers/vectors' + +const router = express.Router() + +const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` }) + +// CREATE +router.post('/upsert/:id', upload.array('files'), vectorsController.getRateLimiterMiddleware, vectorsController.upsertVectorMiddleware) +router.post('/internal-upsert/:id', vectorsController.createInternalUpsert) + +export default router diff --git a/packages/server/src/routes/verify/index.ts b/packages/server/src/routes/verify/index.ts new file mode 100644 index 000000000..414e6d0f3 --- /dev/null +++ b/packages/server/src/routes/verify/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import apikeyController from '../../controllers/apikey' +const router = express.Router() + +// READ +router.get('/apikey/:apikey', apikeyController.verifyApiKey) + +export default router diff --git a/packages/server/src/routes/versions/index.ts b/packages/server/src/routes/versions/index.ts new file mode 100644 index 000000000..8aa60a290 --- /dev/null +++ b/packages/server/src/routes/versions/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import versionsController from '../../controllers/versions' +const router = express.Router() + +// READ +router.get('/', versionsController.getVersion) + +export default router diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts new file mode 100644 index 000000000..5a1bc3344 --- /dev/null +++ b/packages/server/src/services/apikey/index.ts @@ -0,0 +1,69 @@ +import { addAPIKey, deleteAPIKey, getAPIKeys, updateAPIKey } from '../../utils/apiKey' +import { addChatflowsCount } from '../../utils/addChatflowsCount' +import { getApiKey } from '../../utils/apiKey' + +const getAllApiKeys = async () => { + try { + const keys = await getAPIKeys() + const dbResponse = await addChatflowsCount(keys) + return dbResponse + } catch (error) { + throw new Error(`Error: apikeyService.getAllApiKeys - ${error}`) + } +} + +const createApiKey = async (keyName: string) => { + try { + const keys = await addAPIKey(keyName) + const dbResponse = await addChatflowsCount(keys) + return dbResponse + } catch (error) { + throw new Error(`Error: apikeyService.createApiKey - ${error}`) + } +} + +// Update api key +const updateApiKey = async (id: string, keyName: string) => { + try { + const keys = await updateAPIKey(id, keyName) + const dbResponse = await addChatflowsCount(keys) + return dbResponse + } catch (error) { + throw new Error(`Error: apikeyService.updateApiKey - ${error}`) + } +} + +const deleteApiKey = async (id: string) => { + try { + const keys = await deleteAPIKey(id) + const dbResponse = await addChatflowsCount(keys) + return dbResponse + } catch (error) { + throw new Error(`Error: apikeyService.deleteApiKey - ${error}`) + } +} + +const verifyApiKey = async (paramApiKey: string): Promise => { + try { + const apiKey = await getApiKey(paramApiKey) + if (!apiKey) { + return { + executionError: true, + status: 401, + msg: `Unauthorized` + } + } + const dbResponse = 'OK' + return dbResponse + } catch (error) { + throw new Error(`Error: apikeyService.verifyApiKey - ${error}`) + } +} + +export default { + createApiKey, + deleteApiKey, + getAllApiKeys, + updateApiKey, + verifyApiKey +} diff --git a/packages/server/src/services/assistants/index.ts b/packages/server/src/services/assistants/index.ts new file mode 100644 index 000000000..8b5a431f3 --- /dev/null +++ b/packages/server/src/services/assistants/index.ts @@ -0,0 +1,366 @@ +import OpenAI from 'openai' +import path from 'path' +import * as fs from 'fs' +import { uniqWith, isEqual } from 'lodash' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { Assistant } from '../../database/entities/Assistant' +import { Credential } from '../../database/entities/Credential' +import { getUserHome, decryptCredentialData, getAppVersion } from '../../utils' + +const creatAssistant = async (requestBody: any): Promise => { + try { + const appServer = getRunningExpressApp() + if (!requestBody.details) { + return { + executionError: true, + status: 500, + msg: `Invalid request body` + } + } + const assistantDetails = JSON.parse(requestBody.details) + try { + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: requestBody.credential + }) + + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${requestBody.credential} not found` + } + } + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) { + return { + executionError: true, + status: 404, + msg: `OpenAI ApiKey not found` + } + } + const openai = new OpenAI({ apiKey: openAIApiKey }) + + let tools = [] + if (assistantDetails.tools) { + for (const tool of assistantDetails.tools ?? []) { + tools.push({ + type: tool + }) + } + } + + if (assistantDetails.uploadFiles) { + // Base64 strings + let files: string[] = [] + const fileBase64 = assistantDetails.uploadFiles + if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) { + files = JSON.parse(fileBase64) + } else { + files = [fileBase64] + } + + const uploadedFiles = [] + for (const file of files) { + const splitDataURI = file.split(',') + const filename = splitDataURI.pop()?.split(':')[1] ?? '' + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename) + if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + } + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, bf) + } + + const createdFile = await openai.files.create({ + file: fs.createReadStream(filePath), + purpose: 'assistants' + }) + uploadedFiles.push(createdFile) + + fs.unlinkSync(filePath) + } + assistantDetails.files = [...assistantDetails.files, ...uploadedFiles] + } + + if (!assistantDetails.id) { + const newAssistant = await openai.beta.assistants.create({ + name: assistantDetails.name, + description: assistantDetails.description, + instructions: assistantDetails.instructions, + model: assistantDetails.model, + tools, + file_ids: (assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id) + }) + assistantDetails.id = newAssistant.id + } else { + const retrievedAssistant = await openai.beta.assistants.retrieve(assistantDetails.id) + let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual) + filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) + + await openai.beta.assistants.update(assistantDetails.id, { + name: assistantDetails.name, + description: assistantDetails.description ?? '', + instructions: assistantDetails.instructions ?? '', + model: assistantDetails.model, + tools: filteredTools, + file_ids: uniqWith( + [...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)], + isEqual + ) + }) + } + + const newAssistantDetails = { + ...assistantDetails + } + if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles + + requestBody.details = JSON.stringify(newAssistantDetails) + } catch (error) { + return { + executionError: true, + status: 500, + msg: `Error creating new assistant: ${error}` + } + } + const newAssistant = new Assistant() + Object.assign(newAssistant, requestBody) + + const assistant = await appServer.AppDataSource.getRepository(Assistant).create(newAssistant) + const dbResponse = await appServer.AppDataSource.getRepository(Assistant).save(assistant) + + await appServer.telemetry.sendTelemetry('assistant_created', { + version: await getAppVersion(), + assistantId: dbResponse.id + }) + return dbResponse + } catch (error) { + throw new Error(`Error: assistantsService.creatTool - ${error}`) + } +} + +const deleteAssistant = async (assistantId: string, isDeleteBoth: any): Promise => { + try { + const appServer = getRunningExpressApp() + const assistant = await appServer.AppDataSource.getRepository(Assistant).findOneBy({ + id: assistantId + }) + if (!assistant) { + return { + executionError: true, + status: 404, + msg: `Assistant ${assistantId} not found` + } + } + try { + const assistantDetails = JSON.parse(assistant.details) + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: assistant.credential + }) + + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${assistant.credential} not found` + } + } + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) { + return { + executionError: true, + status: 404, + msg: `OpenAI ApiKey not found` + } + } + + const openai = new OpenAI({ apiKey: openAIApiKey }) + const dbResponse = await appServer.AppDataSource.getRepository(Assistant).delete({ id: assistantId }) + if (isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id) + return dbResponse + } catch (error: any) { + if (error.status === 404 && error.type === 'invalid_request_error') { + return 'OK' + } else { + return { + executionError: true, + status: 500, + msg: `Error deleting assistant: ${error}` + } + } + } + } catch (error) { + throw new Error(`Error: assistantsService.deleteTool - ${error}`) + } +} + +const getAllAssistants = async (): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Assistant).find() + return dbResponse + } catch (error) { + throw new Error(`Error: assistantsService.getAllAssistants - ${error}`) + } +} + +const getAssistantById = async (assistantId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Assistant).findOneBy({ + id: assistantId + }) + if (!dbResponse) { + return { + executionError: true, + status: 404, + msg: `Assistant ${assistantId} not found` + } + } + return dbResponse + } catch (error) { + throw new Error(`Error: assistantsService.getAssistantById - ${error}`) + } +} + +const updateAssistant = async (assistantId: string, requestBody: any): Promise => { + try { + const appServer = getRunningExpressApp() + const assistant = await appServer.AppDataSource.getRepository(Assistant).findOneBy({ + id: assistantId + }) + + if (!assistant) { + return { + executionError: true, + status: 404, + msg: `Assistant ${assistantId} not found` + } + } + try { + const openAIAssistantId = JSON.parse(assistant.details)?.id + const body = requestBody + const assistantDetails = JSON.parse(body.details) + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: body.credential + }) + + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${body.credential} not found` + } + } + + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) { + return { + executionError: true, + status: 404, + msg: `OpenAI ApiKey not found` + } + } + + const openai = new OpenAI({ apiKey: openAIApiKey }) + + let tools = [] + if (assistantDetails.tools) { + for (const tool of assistantDetails.tools ?? []) { + tools.push({ + type: tool + }) + } + } + + if (assistantDetails.uploadFiles) { + // Base64 strings + let files: string[] = [] + const fileBase64 = assistantDetails.uploadFiles + if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) { + files = JSON.parse(fileBase64) + } else { + files = [fileBase64] + } + + const uploadedFiles = [] + for (const file of files) { + const splitDataURI = file.split(',') + const filename = splitDataURI.pop()?.split(':')[1] ?? '' + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') + const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename) + if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + } + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, bf) + } + + const createdFile = await openai.files.create({ + file: fs.createReadStream(filePath), + purpose: 'assistants' + }) + uploadedFiles.push(createdFile) + + fs.unlinkSync(filePath) + } + assistantDetails.files = [...assistantDetails.files, ...uploadedFiles] + } + + const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) + let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual) + filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) + + await openai.beta.assistants.update(openAIAssistantId, { + name: assistantDetails.name, + description: assistantDetails.description, + instructions: assistantDetails.instructions, + model: assistantDetails.model, + tools: filteredTools, + file_ids: uniqWith( + [...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)], + isEqual + ) + }) + + const newAssistantDetails = { + ...assistantDetails, + id: openAIAssistantId + } + if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles + + const updateAssistant = new Assistant() + body.details = JSON.stringify(newAssistantDetails) + Object.assign(updateAssistant, body) + + await appServer.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant) + const dbResponse = await appServer.AppDataSource.getRepository(Assistant).save(assistant) + return dbResponse + } catch (error) { + return { + executionError: true, + status: 500, + msg: `Error updating assistant: ${error}` + } + } + } catch (error) { + throw new Error(`Error: assistantsService.updateAssistant - ${error}`) + } +} + +export default { + creatAssistant, + deleteAssistant, + getAllAssistants, + getAssistantById, + updateAssistant +} diff --git a/packages/server/src/services/chat-messages/index.ts b/packages/server/src/services/chat-messages/index.ts new file mode 100644 index 000000000..8b87f9db0 --- /dev/null +++ b/packages/server/src/services/chat-messages/index.ts @@ -0,0 +1,116 @@ +import { FindOptionsWhere } from 'typeorm' +import path from 'path' +import { chatType, IChatMessage } from '../../Interface' +import { utilGetChatMessage } from '../../utils/getChatMessage' +import { utilAddChatMessage } from '../../utils/addChatMesage' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback' +import { getStoragePath } from 'flowise-components' +import { deleteFolderRecursive } from '../../utils' +import logger from '../../utils/logger' +import { ChatMessage } from '../../database/entities/ChatMessage' + +// Add chatmessages for chatflowid +const createChatMessage = async (chatMessage: Partial) => { + try { + const dbResponse = await utilAddChatMessage(chatMessage) + return dbResponse + } catch (error) { + throw new Error(`Error: chatMessagesService.createChatMessage - ${error}`) + } +} + +// Get all chatmessages from chatflowid +const getAllChatMessages = async ( + chatflowId: string, + chatTypeFilter: chatType | undefined, + sortOrder: string = 'ASC', + chatId?: string, + memoryType?: string, + sessionId?: string, + startDate?: string, + endDate?: string, + messageId?: string, + feedback?: boolean +): Promise => { + try { + const dbResponse = await utilGetChatMessage( + chatflowId, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate, + messageId, + feedback + ) + return dbResponse + } catch (error) { + throw new Error(`Error: chatMessagesService.getAllChatMessages - ${error}`) + } +} + +// Get internal chatmessages from chatflowid +const getAllInternalChatMessages = async ( + chatflowId: string, + chatTypeFilter: chatType | undefined, + sortOrder: string = 'ASC', + chatId?: string, + memoryType?: string, + sessionId?: string, + startDate?: string, + endDate?: string, + messageId?: string, + feedback?: boolean +): Promise => { + try { + const dbResponse = await utilGetChatMessage( + chatflowId, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate, + messageId, + feedback + ) + return dbResponse + } catch (error) { + throw new Error(`Error: chatMessagesService.getAllInternalChatMessages - ${error}`) + } +} + +const removeAllChatMessages = async (chatId: string, chatflowid: string, deleteOptions: FindOptionsWhere): Promise => { + try { + const appServer = getRunningExpressApp() + + // remove all related feedback records + const feedbackDeleteOptions: FindOptionsWhere = { chatId } + await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions) + + // Delete all uploads corresponding to this chatflow/chatId + if (chatId) { + try { + const directory = path.join(getStoragePath(), chatflowid, chatId) + deleteFolderRecursive(directory) + } catch (e) { + logger.error(`[server]: Error deleting file storage for chatflow ${chatflowid}, chatId ${chatId}: ${e}`) + } + } + const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(deleteOptions) + return dbResponse + } catch (error) { + throw new Error(`Error: chatMessagesService.removeAllChatMessages - ${error}`) + } +} + +export default { + createChatMessage, + getAllChatMessages, + getAllInternalChatMessages, + removeAllChatMessages +} diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts new file mode 100644 index 000000000..c6ee778a9 --- /dev/null +++ b/packages/server/src/services/chatflows/index.ts @@ -0,0 +1,278 @@ +import path from 'path' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { IChatFlow } from '../../Interface' +import { ChatFlow } from '../../database/entities/ChatFlow' +import { + getAppVersion, + getTelemetryFlowObj, + deleteFolderRecursive, + isFlowValidForStream, + constructGraphs, + getEndingNodes +} from '../../utils' +import logger from '../../utils/logger' +import { getStoragePath } from 'flowise-components' +import { IReactFlowObject } from '../../Interface' +import { utilGetUploadsConfig } from '../../utils/getUploadsConfig' + +// Check if chatflow valid for streaming +const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise => { + try { + const appServer = getRunningExpressApp() + //** + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowId + }) + if (!chatflow) { + return { + executionError: true, + status: 404, + msg: `Chatflow ${chatflowId} not found` + } + } + + /*** Get Ending Node with Directed Graph ***/ + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const edges = parsedFlowData.edges + const { graph, nodeDependencies } = constructGraphs(nodes, edges) + + const endingNodeIds = getEndingNodes(nodeDependencies, graph) + if (!endingNodeIds.length) { + return { + executionError: true, + status: 500, + msg: `Ending nodes not found` + } + } + + const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) + + let isStreaming = false + let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode') + + for (const endingNode of endingNodes) { + const endingNodeData = endingNode.data + if (!endingNodeData) { + return { + executionError: true, + status: 500, + msg: `Ending node ${endingNode.id} data not found` + } + } + + const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode' + + if (!isEndingNode) { + if ( + endingNodeData && + endingNodeData.category !== 'Chains' && + endingNodeData.category !== 'Agents' && + endingNodeData.category !== 'Engine' + ) { + return { + executionError: true, + status: 500, + msg: `Ending node must be either a Chain or Agent` + } + } + } + + isStreaming = isEndingNode ? false : isFlowValidForStream(nodes, endingNodeData) + } + + // Once custom function ending node exists, flow is always unavailable to stream + const dbResponse = { isStreaming: isEndingNodeExists ? false : isStreaming } + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.checkIfChatflowIsValidForStreaming - ${error}`) + } +} + +// Check if chatflow valid for uploads +const checkIfChatflowIsValidForUploads = async (chatflowId: string): Promise => { + try { + const dbResponse = await utilGetUploadsConfig(chatflowId) + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.checkIfChatflowIsValidForUploads - ${error}`) + } +} + +const deleteChatflow = async (chatflowId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).delete({ id: chatflowId }) + try { + // Delete all uploads corresponding to this chatflow + const directory = path.join(getStoragePath(), chatflowId) + deleteFolderRecursive(directory) + } catch (e) { + logger.error(`[server]: Error deleting file storage for chatflow ${chatflowId}: ${e}`) + } + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.getAllChatflows - ${error}`) + } +} + +const getAllChatflows = async (): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).find() + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.getAllChatflows - ${error}`) + } +} + +const getChatflowByApiKey = async (apiKeyId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow) + .createQueryBuilder('cf') + .where('cf.apikeyid = :apikeyid', { apikeyid: apiKeyId }) + .orWhere('cf.apikeyid IS NULL') + .orWhere('cf.apikeyid = ""') + .orderBy('cf.name', 'ASC') + .getMany() + if (dbResponse.length < 1) { + return { + executionError: true, + status: 404, + msg: `Chatflow not found in the database!` + } + } + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.getChatflowByApiKey - ${error}`) + } +} + +const getChatflowById = async (chatflowId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowId + }) + if (!dbResponse) { + return { + executionError: true, + status: 404, + msg: `Chatflow ${chatflowId} not found in the database!` + } + } + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.getAllChatflows - ${error}`) + } +} + +const saveChatflow = async (newChatFlow: ChatFlow): Promise => { + try { + const appServer = getRunningExpressApp() + const newDbChatflow = await appServer.AppDataSource.getRepository(ChatFlow).create(newChatFlow) + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).save(newDbChatflow) + await appServer.telemetry.sendTelemetry('chatflow_created', { + version: await getAppVersion(), + chatflowId: dbResponse.id, + flowGraph: getTelemetryFlowObj(JSON.parse(dbResponse.flowData)?.nodes, JSON.parse(dbResponse.flowData)?.edges) + }) + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.saveChatflow - ${error}`) + } +} + +const updateChatflow = async (chatflow: ChatFlow, updateChatFlow: ChatFlow): Promise => { + try { + const appServer = getRunningExpressApp() + const newDbChatflow = await appServer.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).save(newDbChatflow) + // 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 (appServer.chatflowPool) { + // Update chatflowpool inSync to false, to build flow from scratch again because data has been changed + appServer.chatflowPool.updateInSync(chatflow.id, false) + } + return dbResponse + } catch (error) { + throw new Error(`Error: chatflowsService.updateChatflow - ${error}`) + } +} + +// Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) +const getSinglePublicChatflow = async (chatflowId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowId + }) + if (dbResponse && dbResponse.isPublic) { + return dbResponse + } else if (dbResponse && !dbResponse.isPublic) { + return { + executionError: true, + status: 401, + msg: `Unauthorized` + } + } + return { + executionError: true, + status: 404, + msg: `Chatflow ${chatflowId} not found` + } + } catch (error) { + throw new Error(`Error: chatflowsService.getSinglePublicChatflow - ${error}`) + } +} + +// Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat) +// Safe as public endpoint as chatbotConfig doesn't contain sensitive credential +const getSinglePublicChatbotConfig = async (chatflowId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowId + }) + if (!dbResponse) { + return { + executionError: true, + status: 404, + msg: `Chatflow ${chatflowId} not found` + } + } + const uploadsConfig = await utilGetUploadsConfig(chatflowId) + // even if chatbotConfig is not set but uploads are enabled + // send uploadsConfig to the chatbot + if (dbResponse.chatbotConfig || uploadsConfig) { + try { + const parsedConfig = dbResponse.chatbotConfig ? JSON.parse(dbResponse.chatbotConfig) : {} + return { ...parsedConfig, uploads: uploadsConfig } + } catch (e) { + return { + executionError: true, + status: 500, + msg: `Error parsing Chatbot Config for Chatflow ${chatflowId}` + } + } + } + return 'OK' + } catch (error) { + throw new Error(`Error: chatflowsService.getSinglePublicChatbotConfig - ${error}`) + } +} + +export default { + checkIfChatflowIsValidForStreaming, + checkIfChatflowIsValidForUploads, + deleteChatflow, + getAllChatflows, + getChatflowByApiKey, + getChatflowById, + saveChatflow, + updateChatflow, + getSinglePublicChatflow, + getSinglePublicChatbotConfig +} diff --git a/packages/server/src/services/components-credentials/index.ts b/packages/server/src/services/components-credentials/index.ts new file mode 100644 index 000000000..bbc4128a2 --- /dev/null +++ b/packages/server/src/services/components-credentials/index.ts @@ -0,0 +1,74 @@ +import { cloneDeep } from 'lodash' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' + +// Get all component credentials +const getAllComponentsCredentials = async (): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = [] + for (const credName in appServer.nodesPool.componentCredentials) { + const clonedCred = cloneDeep(appServer.nodesPool.componentCredentials[credName]) + dbResponse.push(clonedCred) + } + return dbResponse + } catch (error) { + throw new Error(`Error: componentsCredentialsService.getAllComponentsCredentials - ${error}`) + } +} + +const getComponentByName = async (credentialName: string) => { + try { + const appServer = getRunningExpressApp() + if (!credentialName.includes('&')) { + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentCredentials, credentialName)) { + return appServer.nodesPool.componentCredentials[credentialName] + } else { + throw new Error( + `Error: componentsCredentialsService.getSingleComponentsCredential - Credential ${credentialName} not found` + ) + } + } else { + const dbResponse = [] + for (const name of credentialName.split('&')) { + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentCredentials, name)) { + dbResponse.push(appServer.nodesPool.componentCredentials[name]) + } else { + throw new Error(`Error: componentsCredentialsService.getSingleComponentsCredential - Credential ${name} not found`) + } + } + return dbResponse + } + } catch (error) { + throw new Error(`Error: componentsCredentialsService.getSingleComponentsCredential - ${error}`) + } +} + +// Returns specific component credential icon via name +const getSingleComponentsCredentialIcon = async (credentialName: string) => { + try { + const appServer = getRunningExpressApp() + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentCredentials, credentialName)) { + const credInstance = appServer.nodesPool.componentCredentials[credentialName] + if (credInstance.icon === undefined) { + throw new Error(`Credential ${credentialName} icon not found`) + } + + if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { + const filepath = credInstance.icon + return filepath + } else { + throw new Error(`Credential ${credentialName} icon is missing icon`) + } + } else { + throw new Error(`Credential ${credentialName} not found`) + } + } catch (error) { + throw new Error(`Error: componentsCredentialsService.getSingleComponentsCredentialIcon - ${error}`) + } +} + +export default { + getAllComponentsCredentials, + getComponentByName, + getSingleComponentsCredentialIcon +} diff --git a/packages/server/src/services/credentials/index.ts b/packages/server/src/services/credentials/index.ts new file mode 100644 index 000000000..5c5d145ea --- /dev/null +++ b/packages/server/src/services/credentials/index.ts @@ -0,0 +1,126 @@ +import { omit } from 'lodash' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { Credential } from '../../database/entities/Credential' +import { transformToCredentialEntity, decryptCredentialData } from '../../utils' +import { ICredentialReturnResponse } from '../../Interface' + +const createCredential = async (requestBody: any) => { + try { + const appServer = getRunningExpressApp() + const newCredential = await transformToCredentialEntity(requestBody) + const credential = await appServer.AppDataSource.getRepository(Credential).create(newCredential) + const dbResponse = await appServer.AppDataSource.getRepository(Credential).save(credential) + return dbResponse + } catch (error) { + throw new Error(`Error: credentialsService.createCredential - ${error}`) + } +} + +// Delete all credentials from chatflowid +const deleteCredentials = async (credentialId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Credential).delete({ id: credentialId }) + if (!dbResponse) { + return { + executionError: true, + status: 404, + msg: `Credential ${credentialId} not found` + } + } + return dbResponse + } catch (error) { + throw new Error(`Error: credentialsService.deleteCredential - ${error}`) + } +} + +const getAllCredentials = async (paramCredentialName: any) => { + try { + const appServer = getRunningExpressApp() + let dbResponse = [] + if (paramCredentialName) { + if (Array.isArray(paramCredentialName)) { + for (let i = 0; i < paramCredentialName.length; i += 1) { + const name = paramCredentialName[i] as string + const credentials = await appServer.AppDataSource.getRepository(Credential).findBy({ + credentialName: name + }) + dbResponse.push(...credentials) + } + } else { + const credentials = await appServer.AppDataSource.getRepository(Credential).findBy({ + credentialName: paramCredentialName as string + }) + dbResponse = [...credentials] + } + } else { + const credentials = await appServer.AppDataSource.getRepository(Credential).find() + for (const credential of credentials) { + dbResponse.push(omit(credential, ['encryptedData'])) + } + } + return dbResponse + } catch (error) { + throw new Error(`Error: credentialsService.getAllCredentials - ${error}`) + } +} + +const getCredentialById = async (credentialId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${credentialId} not found` + } + } + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData( + credential.encryptedData, + credential.credentialName, + appServer.nodesPool.componentCredentials + ) + const returnCredential: ICredentialReturnResponse = { + ...credential, + plainDataObj: decryptedCredentialData + } + const dbResponse = omit(returnCredential, ['encryptedData']) + return dbResponse + } catch (error) { + throw new Error(`Error: credentialsService.createCredential - ${error}`) + } +} + +const updateCredential = async (credentialId: string, requestBody: any): Promise => { + try { + const appServer = getRunningExpressApp() + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${credentialId} not found` + } + } + const updateCredential = await transformToCredentialEntity(requestBody) + await appServer.AppDataSource.getRepository(Credential).merge(credential, updateCredential) + const dbResponse = await appServer.AppDataSource.getRepository(Credential).save(credential) + return dbResponse + } catch (error) { + throw new Error(`Error: credentialsService.updateCredential - ${error}`) + } +} + +export default { + createCredential, + deleteCredentials, + getAllCredentials, + getCredentialById, + updateCredential +} diff --git a/packages/server/src/services/feedback/index.ts b/packages/server/src/services/feedback/index.ts new file mode 100644 index 000000000..9084be613 --- /dev/null +++ b/packages/server/src/services/feedback/index.ts @@ -0,0 +1,46 @@ +import { utilGetChatMessageFeedback } from '../../utils/getChatMessageFeedback' +import { utilAddChatMessageFeedback } from '../../utils/addChatMessageFeedback' +import { utilUpdateChatMessageFeedback } from '../../utils/updateChatMessageFeedback' +import { IChatMessageFeedback } from '../../Interface' + +// Get all chatmessage feedback from chatflowid +const getAllChatMessageFeedback = async ( + chatflowid: string, + chatId: string | undefined, + sortOrder: string | undefined, + startDate: string | undefined, + endDate: string | undefined +) => { + try { + const dbResponse = await utilGetChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate) + return dbResponse + } catch (error) { + throw new Error(`Error: feedbackService.getAllChatMessageFeedback - ${error}`) + } +} + +// Add chatmessage feedback for chatflowid +const createChatMessageFeedbackForChatflow = async (requestBody: Partial): Promise => { + try { + const dbResponse = await utilAddChatMessageFeedback(requestBody) + return dbResponse + } catch (error) { + throw new Error(`Error: feedbackService.createChatMessageFeedbackForChatflow - ${error}`) + } +} + +// Add chatmessage feedback for chatflowid +const updateChatMessageFeedbackForChatflow = async (chatflowId: string, requestBody: Partial): Promise => { + try { + const dbResponse = await utilUpdateChatMessageFeedback(chatflowId, requestBody) + return dbResponse + } catch (error) { + throw new Error(`Error: feedbackService.updateChatMessageFeedbackForChatflow - ${error}`) + } +} + +export default { + getAllChatMessageFeedback, + createChatMessageFeedbackForChatflow, + updateChatMessageFeedbackForChatflow +} diff --git a/packages/server/src/services/fetch-links/index.ts b/packages/server/src/services/fetch-links/index.ts new file mode 100644 index 000000000..58247d72f --- /dev/null +++ b/packages/server/src/services/fetch-links/index.ts @@ -0,0 +1,29 @@ +import { webCrawl, xmlScrape } from 'flowise-components' + +const getAllLinks = async (requestUrl: string, relativeLinksMethod: string, queryLimit: string): Promise => { + try { + const url = decodeURIComponent(requestUrl) + if (!relativeLinksMethod) { + return { + executionError: true, + status: 500, + msg: `Please choose a Relative Links Method in Additional Parameters!` + } + } + const limit = parseInt(queryLimit) + if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`) + const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit) + if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`) + const dbResponse = { + status: 'OK', + links + } + return dbResponse + } catch (error) { + throw new Error(`Error: fetchLinksService.getAllLinks - ${error}`) + } +} + +export default { + getAllLinks +} diff --git a/packages/server/src/services/flow-configs/index.ts b/packages/server/src/services/flow-configs/index.ts new file mode 100644 index 000000000..8dbcd6d3f --- /dev/null +++ b/packages/server/src/services/flow-configs/index.ts @@ -0,0 +1,29 @@ +import { findAvailableConfigs } from '../../utils' +import { IReactFlowObject } from '../../Interface' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import chatflowsService from '../chatflows' + +const getSingleFlowConfig = async (chatflowId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const chatflow = await chatflowsService.getChatflowById(chatflowId) + if (!chatflow) { + return { + executionError: true, + status: 404, + msg: `Chatflow ${chatflowId} not found in the database!` + } + } + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const dbResponse = findAvailableConfigs(nodes, appServer.nodesPool.componentCredentials) + return dbResponse + } catch (error) { + throw new Error(`Error: flowConfigService.getSingleFlowConfig - ${error}`) + } +} + +export default { + getSingleFlowConfig +} diff --git a/packages/server/src/services/load-prompts/index.ts b/packages/server/src/services/load-prompts/index.ts new file mode 100644 index 000000000..0cdc3ffb9 --- /dev/null +++ b/packages/server/src/services/load-prompts/index.ts @@ -0,0 +1,22 @@ +import { Client } from 'langchainhub' +import { parsePrompt } from '../../utils/hub' + +const createPrompt = async (promptName: string): Promise => { + try { + let hub = new Client() + const prompt = await hub.pull(promptName) + const templates = parsePrompt(prompt) + const dbResponse = { + status: 'OK', + prompt: promptName, + templates: templates + } + return dbResponse + } catch (error) { + throw new Error(`Error: loadPromptsService.createPrompt - ${error}`) + } +} + +export default { + createPrompt +} diff --git a/packages/server/src/services/marketplaces/index.ts b/packages/server/src/services/marketplaces/index.ts new file mode 100644 index 000000000..218452dd3 --- /dev/null +++ b/packages/server/src/services/marketplaces/index.ts @@ -0,0 +1,58 @@ +import path from 'path' +import * as fs from 'fs' + +// Get all templates for marketplaces +const getAllTemplates = async () => { + try { + let marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'chatflows') + let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') + let templates: any[] = [] + jsonsInDir.forEach((file, index) => { + const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'chatflows', file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + id: index, + templateName: file.split('.json')[0], + flowData: fileData.toString(), + badge: fileDataObj?.badge, + framework: fileDataObj?.framework, + categories: fileDataObj?.categories, + type: 'Chatflow', + description: fileDataObj?.description || '' + } + templates.push(template) + }) + + marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'tools') + jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') + jsonsInDir.forEach((file, index) => { + const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'tools', file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + ...fileDataObj, + id: index, + type: 'Tool', + framework: fileDataObj?.framework, + badge: fileDataObj?.badge, + categories: '', + templateName: file.split('.json')[0] + } + templates.push(template) + }) + const sortedTemplates = templates.sort((a, b) => a.templateName.localeCompare(b.templateName)) + const FlowiseDocsQnAIndex = sortedTemplates.findIndex((tmp) => tmp.templateName === 'Flowise Docs QnA') + if (FlowiseDocsQnAIndex > 0) { + sortedTemplates.unshift(sortedTemplates.splice(FlowiseDocsQnAIndex, 1)[0]) + } + const dbResponse = sortedTemplates + return dbResponse + } catch (error) { + throw new Error(`Error: marketplacesService.getAllTemplates - ${error}`) + } +} + +export default { + getAllTemplates +} diff --git a/packages/server/src/services/node-configs/index.ts b/packages/server/src/services/node-configs/index.ts new file mode 100644 index 000000000..7eb523e81 --- /dev/null +++ b/packages/server/src/services/node-configs/index.ts @@ -0,0 +1,18 @@ +import { findAvailableConfigs } from '../../utils' +import { IReactFlowNode } from '../../Interface' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' + +const getAllNodeConfigs = async (requestBody: any) => { + try { + const appServer = getRunningExpressApp() + const nodes = [{ data: requestBody }] as IReactFlowNode[] + const dbResponse = findAvailableConfigs(nodes, appServer.nodesPool.componentCredentials) + return dbResponse + } catch (error) { + throw new Error(`Error: nodeConfigsService.getAllNodeConfigs - ${error}`) + } +} + +export default { + getAllNodeConfigs +} diff --git a/packages/server/src/services/nodes/index.ts b/packages/server/src/services/nodes/index.ts new file mode 100644 index 000000000..0bf097b49 --- /dev/null +++ b/packages/server/src/services/nodes/index.ts @@ -0,0 +1,142 @@ +import { cloneDeep } from 'lodash' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { INodeData } from '../../Interface' +import { INodeOptionsValue, ICommonObject, handleEscapeCharacters } from 'flowise-components' +import { databaseEntities } from '../../utils' +import logger from '../../utils/logger' + +// Get all component nodes +const getAllNodes = async () => { + try { + const appServer = getRunningExpressApp() + const dbResponse = [] + for (const nodeName in appServer.nodesPool.componentNodes) { + const clonedNode = cloneDeep(appServer.nodesPool.componentNodes[nodeName]) + dbResponse.push(clonedNode) + } + return dbResponse + } catch (error) { + throw new Error(`Error: nodesService.getAllNodes - ${error}`) + } +} + +// Get specific component node via name +const getNodeByName = async (nodeName: string) => { + try { + const appServer = getRunningExpressApp() + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, nodeName)) { + const dbResponse = appServer.nodesPool.componentNodes[nodeName] + return dbResponse + } else { + throw new Error(`Node ${nodeName} not found`) + } + } catch (error) { + throw new Error(`Error: nodesService.getAllNodes - ${error}`) + } +} + +// Returns specific component node icon via name +const getSingleNodeIcon = async (nodeName: string) => { + try { + const appServer = getRunningExpressApp() + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, nodeName)) { + const nodeInstance = appServer.nodesPool.componentNodes[nodeName] + if (nodeInstance.icon === undefined) { + throw new Error(`Node ${nodeName} icon not found`) + } + + if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) { + const filepath = nodeInstance.icon + return filepath + } else { + throw new Error(`Node ${nodeName} icon is missing icon`) + } + } else { + throw new Error(`Node ${nodeName} not found`) + } + } catch (error) { + throw new Error(`Error: nodesService.getSingleNodeIcon - ${error}`) + } +} + +const getSingleNodeAsyncOptions = async (nodeName: string, requestBody: any): Promise => { + try { + const appServer = getRunningExpressApp() + const nodeData: INodeData = requestBody + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, nodeName)) { + try { + const nodeInstance = appServer.nodesPool.componentNodes[nodeName] + const methodName = nodeData.loadMethod || '' + + const dbResponse: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { + appDataSource: appServer.AppDataSource, + databaseEntities: databaseEntities + }) + + return dbResponse + } catch (error) { + return [] + } + } else { + return { + executionError: true, + status: 404, + msg: `Node ${nodeName} not found` + } + } + } catch (error) { + throw new Error(`Error: nodesService.getSingleNodeAsyncOptions - ${error}`) + } +} + +// execute custom function node +const executeCustomFunction = async (requestBody: any) => { + try { + const appServer = getRunningExpressApp() + const body = requestBody + const functionInputVariables = Object.fromEntries( + [...(body?.javascriptFunction ?? '').matchAll(/\$([a-zA-Z0-9_]+)/g)].map((g) => [g[1], undefined]) + ) + const nodeData = { inputs: { functionInputVariables, ...body } } + if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, 'customFunction')) { + try { + const nodeInstanceFilePath = appServer.nodesPool.componentNodes['customFunction'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + const options: ICommonObject = { + appDataSource: appServer.AppDataSource, + databaseEntities, + logger + } + + const returnData = await newNodeInstance.init(nodeData, '', options) + const dbResponse = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData + + return dbResponse + } catch (error) { + return { + executionError: true, + status: 500, + msg: `Error running custom function: ${error}` + } + } + } else { + return { + executionError: true, + status: 404, + msg: `Node customFunction not found` + } + } + } catch (error) { + throw new Error(`Error: nodesService.executeCustomFunction - ${error}`) + } +} + +export default { + getAllNodes, + getNodeByName, + getSingleNodeIcon, + getSingleNodeAsyncOptions, + executeCustomFunction +} diff --git a/packages/server/src/services/openai-assistants/index.ts b/packages/server/src/services/openai-assistants/index.ts new file mode 100644 index 000000000..cffa2e3f1 --- /dev/null +++ b/packages/server/src/services/openai-assistants/index.ts @@ -0,0 +1,84 @@ +import OpenAI from 'openai' +import { decryptCredentialData } from '../../utils' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { Credential } from '../../database/entities/Credential' + +// ---------------------------------------- +// Assistants +// ---------------------------------------- + +// List available assistants +const getAllOpenaiAssistants = async (credentialId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${credentialId} not found in the database!` + } + } + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) { + return { + executionError: true, + status: 404, + msg: `OpenAI ApiKey not found` + } + } + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistants = await openai.beta.assistants.list() + const dbResponse = retrievedAssistants.data + return dbResponse + } catch (error) { + throw new Error(`Error: openaiAssistantsService.getAllOpenaiAssistants - ${error}`) + } +} + +// Get assistant object +const getSingleOpenaiAssistant = async (credentialId: string, assistantId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) + if (!credential) { + return { + executionError: true, + status: 404, + msg: `Credential ${credentialId} not found in the database!` + } + } + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) { + return { + executionError: true, + status: 404, + msg: `OpenAI ApiKey not found` + } + } + + const openai = new OpenAI({ apiKey: openAIApiKey }) + const dbResponse = await openai.beta.assistants.retrieve(assistantId) + const resp = await openai.files.list() + const existingFiles = resp.data ?? [] + if (dbResponse.file_ids && dbResponse.file_ids.length) { + ;(dbResponse as any).files = existingFiles.filter((file) => dbResponse.file_ids.includes(file.id)) + } + return dbResponse + } catch (error) { + throw new Error(`Error: openaiAssistantsService.getSingleOpenaiAssistant - ${error}`) + } +} + +export default { + getAllOpenaiAssistants, + getSingleOpenaiAssistant +} diff --git a/packages/server/src/services/prompts-lists/index.ts b/packages/server/src/services/prompts-lists/index.ts new file mode 100644 index 000000000..62a97c8b4 --- /dev/null +++ b/packages/server/src/services/prompts-lists/index.ts @@ -0,0 +1,22 @@ +import axios from 'axios' + +const createPromptsList = async (requestBody: any) => { + try { + const tags = requestBody.tags ? `tags=${requestBody.tags}` : '' + // Default to 100, TODO: add pagination and use offset & limit + const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false` + const resp = await axios.get(url) + if (resp.data.repos) { + return { + status: 'OK', + repos: resp.data.repos + } + } + } catch (error) { + return { status: 'ERROR', repos: [] } + } +} + +export default { + createPromptsList +} diff --git a/packages/server/src/services/stats/index.ts b/packages/server/src/services/stats/index.ts new file mode 100644 index 000000000..2f2448a1e --- /dev/null +++ b/packages/server/src/services/stats/index.ts @@ -0,0 +1,45 @@ +import { chatType } from '../../Interface' +import { ChatMessage } from '../../database/entities/ChatMessage' +import { utilGetChatMessage } from '../../utils/getChatMessage' +import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback' + +// get stats for showing in chatflow +const getChatflowStats = async ( + chatflowid: string, + chatTypeFilter: chatType | undefined, + startDate?: string, + endDate?: string, + messageId?: string, + feedback?: boolean +): Promise => { + try { + const chatmessages = (await utilGetChatMessage( + chatflowid, + chatTypeFilter, + undefined, + undefined, + undefined, + undefined, + startDate, + endDate, + messageId, + feedback + )) as Array + const totalMessages = chatmessages.length + const totalFeedback = chatmessages.filter((message) => message?.feedback).length + const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length + const dbResponse = { + totalMessages, + totalFeedback, + positiveFeedback + } + + return dbResponse + } catch (error) { + throw new Error(`Error: statsService.getChatflowStats - ${error}`) + } +} + +export default { + getChatflowStats +} diff --git a/packages/server/src/services/telemetry/index.ts b/packages/server/src/services/telemetry/index.ts new file mode 100644 index 000000000..4c77efd53 --- /dev/null +++ b/packages/server/src/services/telemetry/index.ts @@ -0,0 +1,10 @@ +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' + +const createEvent = async (eventInfo: any) => { + const appServer = getRunningExpressApp() + await appServer.telemetry.sendTelemetry(eventInfo.name, eventInfo.data) +} + +export default { + createEvent +} diff --git a/packages/server/src/services/tools/index.ts b/packages/server/src/services/tools/index.ts new file mode 100644 index 000000000..42a579e1f --- /dev/null +++ b/packages/server/src/services/tools/index.ts @@ -0,0 +1,93 @@ +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { Tool } from '../../database/entities/Tool' +import { getAppVersion } from '../../utils' + +const creatTool = async (requestBody: any): Promise => { + try { + const appServer = getRunningExpressApp() + const newTool = new Tool() + Object.assign(newTool, requestBody) + const tool = await appServer.AppDataSource.getRepository(Tool).create(newTool) + const dbResponse = await appServer.AppDataSource.getRepository(Tool).save(tool) + await appServer.telemetry.sendTelemetry('tool_created', { + version: await getAppVersion(), + toolId: dbResponse.id, + toolName: dbResponse.name + }) + return dbResponse + } catch (error) { + throw new Error(`Error: toolsService.creatTool - ${error}`) + } +} + +const deleteTool = async (toolId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Tool).delete({ + id: toolId + }) + return dbResponse + } catch (error) { + throw new Error(`Error: toolsService.deleteTool - ${error}`) + } +} + +const getAllTools = async (): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Tool).find() + return dbResponse + } catch (error) { + throw new Error(`Error: toolsService.getAllTools - ${error}`) + } +} + +const getToolById = async (toolId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Tool).findOneBy({ + id: toolId + }) + if (!dbResponse) { + return { + executionError: true, + status: 404, + msg: `Tool ${toolId} not found` + } + } + return dbResponse + } catch (error) { + throw new Error(`Error: toolsService.getToolById - ${error}`) + } +} + +const updateTool = async (toolId: string, toolBody: any): Promise => { + try { + const appServer = getRunningExpressApp() + const tool = await appServer.AppDataSource.getRepository(Tool).findOneBy({ + id: toolId + }) + if (!tool) { + return { + executionError: true, + status: 404, + msg: `Tool ${toolId} not found` + } + } + const updateTool = new Tool() + Object.assign(updateTool, toolBody) + await appServer.AppDataSource.getRepository(Tool).merge(tool, updateTool) + const dbResponse = await appServer.AppDataSource.getRepository(Tool).save(tool) + return dbResponse + } catch (error) { + throw new Error(`Error: toolsService.getToolById - ${error}`) + } +} + +export default { + creatTool, + deleteTool, + getAllTools, + getToolById, + updateTool +} diff --git a/packages/server/src/services/variables/index.ts b/packages/server/src/services/variables/index.ts new file mode 100644 index 000000000..7153c940d --- /dev/null +++ b/packages/server/src/services/variables/index.ts @@ -0,0 +1,64 @@ +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import { Variable } from '../../database/entities/Variable' + +const createVariable = async (newVariable: Variable) => { + try { + const appServer = getRunningExpressApp() + const variable = await appServer.AppDataSource.getRepository(Variable).create(newVariable) + const dbResponse = await appServer.AppDataSource.getRepository(Variable).save(variable) + return dbResponse + } catch (error) { + throw new Error(`Error: variablesServices.createVariable - ${error}`) + } +} + +const deleteVariable = async (variableId: string): Promise => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Variable).delete({ id: variableId }) + return dbResponse + } catch (error) { + throw new Error(`Error: variablesServices.createVariable - ${error}`) + } +} + +const getAllVariables = async () => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Variable).find() + return dbResponse + } catch (error) { + throw new Error(`Error: variablesServices.getAllVariables - ${error}`) + } +} + +const getVariableById = async (variableId: string) => { + try { + const appServer = getRunningExpressApp() + const dbResponse = await appServer.AppDataSource.getRepository(Variable).findOneBy({ + id: variableId + }) + return dbResponse + } catch (error) { + throw new Error(`Error: variablesServices.getVariableById - ${error}`) + } +} + +const updateVariable = async (variable: Variable, updatedVariable: Variable) => { + try { + const appServer = getRunningExpressApp() + const tmpUpdatedVariable = await appServer.AppDataSource.getRepository(Variable).merge(variable, updatedVariable) + const dbResponse = await appServer.AppDataSource.getRepository(Variable).save(tmpUpdatedVariable) + return dbResponse + } catch (error) { + throw new Error(`Error: variablesServices.updateVariable - ${error}`) + } +} + +export default { + createVariable, + deleteVariable, + getAllVariables, + getVariableById, + updateVariable +} diff --git a/packages/server/src/services/vectors/index.ts b/packages/server/src/services/vectors/index.ts new file mode 100644 index 000000000..7c55a510d --- /dev/null +++ b/packages/server/src/services/vectors/index.ts @@ -0,0 +1,14 @@ +import { Request, Response } from 'express' +import { upsertVector } from '../../utils/upsertVector' + +const upsertVectorMiddleware = async (req: Request, res: Response, isInternal: boolean = false) => { + try { + await upsertVector(req, res, isInternal) + } catch (error) { + throw new Error(`Error: vectorsService.getRateLimiter - ${error}`) + } +} + +export default { + upsertVectorMiddleware +} diff --git a/packages/server/src/services/versions/index.ts b/packages/server/src/services/versions/index.ts new file mode 100644 index 000000000..391cdb1b8 --- /dev/null +++ b/packages/server/src/services/versions/index.ts @@ -0,0 +1,49 @@ +import path from 'path' +import * as fs from 'fs' + +const getVersion = async () => { + try { + const getPackageJsonPath = (): string => { + const checkPaths = [ + path.join(__dirname, '..', 'package.json'), + path.join(__dirname, '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', '..', 'package.json'), + path.join(__dirname, '..', '..', '..', '..', '..', 'package.json') + ] + for (const checkPath of checkPaths) { + if (fs.existsSync(checkPath)) { + return checkPath + } + } + return '' + } + const packagejsonPath = getPackageJsonPath() + if (!packagejsonPath) { + return { + executionError: true, + status: 404, + msg: 'Version not found' + } + } + try { + const content = await fs.promises.readFile(packagejsonPath, 'utf8') + const parsedContent = JSON.parse(content) + return { + version: parsedContent.version + } + } catch (error) { + return { + executionError: true, + status: 500, + msg: `Version not found: ${error}` + } + } + } catch (error) { + throw new Error(`Error: versionService.getVersion - ${error}`) + } +} + +export default { + getVersion +} diff --git a/packages/server/src/utils/addChatMesage.ts b/packages/server/src/utils/addChatMesage.ts new file mode 100644 index 000000000..887031bfc --- /dev/null +++ b/packages/server/src/utils/addChatMesage.ts @@ -0,0 +1,19 @@ +import { ChatMessage } from '../database/entities/ChatMessage' +import { IChatMessage } from '../Interface' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' + +/** + * Method that add chat messages. + * @param {Partial} chatMessage + */ +export const utilAddChatMessage = async (chatMessage: Partial): Promise => { + const appServer = getRunningExpressApp() + const newChatMessage = new ChatMessage() + Object.assign(newChatMessage, chatMessage) + if (!newChatMessage.createdDate) { + newChatMessage.createdDate = new Date() + } + const chatmessage = await appServer.AppDataSource.getRepository(ChatMessage).create(newChatMessage) + const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).save(chatmessage) + return dbResponse +} diff --git a/packages/server/src/utils/addChatMessageFeedback.ts b/packages/server/src/utils/addChatMessageFeedback.ts new file mode 100644 index 000000000..0a570b1d6 --- /dev/null +++ b/packages/server/src/utils/addChatMessageFeedback.ts @@ -0,0 +1,16 @@ +import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' +import { IChatMessageFeedback } from '../Interface' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' + +/** + * Method that add chat message feedback. + * @param {Partial} chatMessageFeedback + */ + +export const utilAddChatMessageFeedback = async (chatMessageFeedback: Partial): Promise => { + const appServer = getRunningExpressApp() + const newChatMessageFeedback = new ChatMessageFeedback() + Object.assign(newChatMessageFeedback, chatMessageFeedback) + const feedback = await appServer.AppDataSource.getRepository(ChatMessageFeedback).create(newChatMessageFeedback) + return await appServer.AppDataSource.getRepository(ChatMessageFeedback).save(feedback) +} diff --git a/packages/server/src/utils/addChatflowsCount.ts b/packages/server/src/utils/addChatflowsCount.ts new file mode 100644 index 000000000..c0ee664a4 --- /dev/null +++ b/packages/server/src/utils/addChatflowsCount.ts @@ -0,0 +1,33 @@ +import { ChatFlow } from '../database/entities/ChatFlow' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' + +export const addChatflowsCount = async (keys: any) => { + try { + const appServer = getRunningExpressApp() + let tmpResult = keys + if (typeof keys !== 'undefined' && keys.length > 0) { + const updatedKeys: any[] = [] + //iterate through keys and get chatflows + for (const key of keys) { + const chatflows = await appServer.AppDataSource.getRepository(ChatFlow) + .createQueryBuilder('cf') + .where('cf.apikeyid = :apikeyid', { apikeyid: key.id }) + .getMany() + const linkedChatFlows: any[] = [] + chatflows.map((cf) => { + linkedChatFlows.push({ + flowName: cf.name, + category: cf.category, + updatedDate: cf.updatedDate + }) + }) + key.chatFlows = linkedChatFlows + updatedKeys.push(key) + } + tmpResult = updatedKeys + } + return tmpResult + } catch (error) { + throw new Error(`Error: addChatflowsCount - ${error}`) + } +} diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts new file mode 100644 index 000000000..811782bc9 --- /dev/null +++ b/packages/server/src/utils/buildChatflow.ts @@ -0,0 +1,427 @@ +import { Request } from 'express' +import { IFileUpload, getStoragePath, convertSpeechToText, ICommonObject } from 'flowise-components' +import { StatusCodes } from 'http-status-codes' +import { IncomingInput, IMessage, INodeData, IReactFlowObject, IReactFlowNode, IDepthQueue, chatType, IChatMessage } from '../Interface' +import path from 'path' +import { ChatFlow } from '../database/entities/ChatFlow' +import { Server } from 'socket.io' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' +import { + mapMimeTypeToInputField, + isFlowValidForStream, + buildFlow, + getTelemetryFlowObj, + getAppVersion, + resolveVariables, + getSessionChatHistory, + findMemoryNode, + replaceInputsWithConfig, + getStartingNodes, + isStartNodeDependOnInput, + getMemorySessionId, + isSameOverrideConfig, + getEndingNodes, + constructGraphs +} from '../utils' +import { utilValidateKey } from './validateKey' +import { databaseEntities } from '.' +import { v4 as uuidv4 } from 'uuid' +import { omit } from 'lodash' +import * as fs from 'fs' +import logger from './logger' +import { utilAddChatMessage } from './addChatMesage' + +/** + * Build Chatflow + * @param {Request} req + * @param {Server} socketIO + * @param {boolean} isInternal + * @param {boolean} isUpsert + */ +export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInternal: boolean = false): Promise => { + try { + const appServer = getRunningExpressApp() + const chatflowid = req.params.id + let incomingInput: IncomingInput = req.body + let nodeToExecuteData: INodeData + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowid + }) + if (!chatflow) { + return { + executionError: true, + status: StatusCodes.NOT_FOUND, + msg: `Chatflow ${chatflowid} not found` + } + } + + const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4() + const userMessageDateTime = new Date() + + if (!isInternal) { + const isKeyValidated = await utilValidateKey(req, chatflow) + if (!isKeyValidated) { + return { + executionError: true, + status: StatusCodes.UNAUTHORIZED, + msg: `Unauthorized` + } + } + } + + let fileUploads: IFileUpload[] = [] + if (incomingInput.uploads) { + fileUploads = incomingInput.uploads + for (let i = 0; i < fileUploads.length; i += 1) { + const upload = fileUploads[i] + if ((upload.type === 'file' || upload.type === 'audio') && upload.data) { + const filename = upload.name + const dir = path.join(getStoragePath(), chatflowid, chatId) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + const filePath = path.join(dir, filename) + const splitDataURI = upload.data.split(',') + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') + fs.writeFileSync(filePath, bf) + + // Omit upload.data since we don't store the content in database + upload.type = 'stored-file' + fileUploads[i] = omit(upload, ['data']) + } + + // Run Speech to Text conversion + if (upload.mime === 'audio/webm') { + let speechToTextConfig: ICommonObject = {} + if (chatflow.speechToText) { + const speechToTextProviders = JSON.parse(chatflow.speechToText) + for (const provider in speechToTextProviders) { + const providerObj = speechToTextProviders[provider] + if (providerObj.status) { + speechToTextConfig = providerObj + speechToTextConfig['name'] = provider + break + } + } + } + if (speechToTextConfig) { + const options: ICommonObject = { + chatId, + chatflowid, + appDataSource: appServer.AppDataSource, + databaseEntities: databaseEntities + } + const speechToTextResult = await convertSpeechToText(upload, speechToTextConfig, options) + if (speechToTextResult) { + incomingInput.question = speechToTextResult + } + } + } + } + } + + let isStreamValid = false + + const files = (req.files as any[]) || [] + + if (files.length) { + const overrideConfig: ICommonObject = { ...req.body } + for (const file of files) { + const fileData = fs.readFileSync(file.path, { encoding: 'base64' }) + const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}` + + const fileInputField = mapMimeTypeToInputField(file.mimetype) + if (overrideConfig[fileInputField]) { + overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String]) + } else { + overrideConfig[fileInputField] = JSON.stringify([dataBase64String]) + } + } + incomingInput = { + question: req.body.question ?? 'hello', + overrideConfig, + history: [], + socketIOClientId: req.body.socketIOClientId + } + } + + /*** Get chatflows and prepare data ***/ + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const edges = parsedFlowData.edges + + // Get session ID + const memoryNode = findMemoryNode(nodes, edges) + const memoryType = memoryNode?.data.label + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) + + /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met: + * - Node Data already exists in pool + * - Still in sync (i.e the flow has not been modified since) + * - Existing overrideConfig and new overrideConfig are the same + * - Flow doesn't start with/contain nodes that depend on incomingInput.question + * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath + ***/ + const isFlowReusable = () => { + return ( + Object.prototype.hasOwnProperty.call(appServer.chatflowPool.activeChatflows, chatflowid) && + appServer.chatflowPool.activeChatflows[chatflowid].inSync && + appServer.chatflowPool.activeChatflows[chatflowid].endingNodeData && + isSameOverrideConfig( + isInternal, + appServer.chatflowPool.activeChatflows[chatflowid].overrideConfig, + incomingInput.overrideConfig + ) && + !isStartNodeDependOnInput(appServer.chatflowPool.activeChatflows[chatflowid].startingNodes, nodes) + ) + } + + if (isFlowReusable()) { + nodeToExecuteData = appServer.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData + isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData) + logger.debug( + `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})` + ) + } else { + /*** Get Ending Node with Directed Graph ***/ + const { graph, nodeDependencies } = constructGraphs(nodes, edges) + const directedGraph = graph + const endingNodeIds = getEndingNodes(nodeDependencies, directedGraph) + if (!endingNodeIds.length) { + return { + executionError: true, + status: 500, + msg: `Ending nodes not found` + } + } + + const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id)) + + let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode') + + for (const endingNode of endingNodes) { + const endingNodeData = endingNode.data + if (!endingNodeData) { + return { + executionError: true, + status: 500, + msg: `Ending node ${endingNode.id} data not found` + } + } + + const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode' + + if (!isEndingNode) { + if ( + endingNodeData && + endingNodeData.category !== 'Chains' && + endingNodeData.category !== 'Agents' && + endingNodeData.category !== 'Engine' + ) { + return { + executionError: true, + status: 500, + msg: `Ending node must be either a Chain or Agent` + } + } + + if ( + endingNodeData.outputs && + Object.keys(endingNodeData.outputs).length && + !Object.values(endingNodeData.outputs ?? {}).includes(endingNodeData.name) + ) { + return { + executionError: true, + status: 500, + msg: `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction` + } + } + } + + isStreamValid = isFlowValidForStream(nodes, endingNodeData) + } + + // Once custom function ending node exists, flow is always unavailable to stream + isStreamValid = isEndingNodeExists ? false : isStreamValid + + let chatHistory: IMessage[] = incomingInput.history ?? [] + + // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node + for (const endingNode of endingNodes) { + const endingNodeData = endingNode.data + + if (!endingNodeData.inputs?.memory) continue + + const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '') + const memoryNode = nodes.find((node) => node.data.id === memoryNodeId) + + if (!memoryNode) continue + + if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) { + chatHistory = await getSessionChatHistory( + memoryNode, + appServer.nodesPool.componentNodes, + incomingInput, + appServer.AppDataSource, + databaseEntities, + logger + ) + } + } + + /*** Get Starting Nodes with Reversed Graph ***/ + const constructedObj = constructGraphs(nodes, edges, { isReversed: true }) + const nonDirectedGraph = constructedObj.graph + let startingNodeIds: string[] = [] + let depthQueue: IDepthQueue = {} + for (const endingNodeId of endingNodeIds) { + const resx = getStartingNodes(nonDirectedGraph, endingNodeId) + startingNodeIds.push(...resx.startingNodeIds) + depthQueue = Object.assign(depthQueue, resx.depthQueue) + } + startingNodeIds = [...new Set(startingNodeIds)] + + const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) + + logger.debug(`[server]: Start building chatflow ${chatflowid}`) + /*** BFS to traverse from Starting Nodes to Ending Node ***/ + const reactFlowNodes = await buildFlow( + startingNodeIds, + nodes, + edges, + graph, + depthQueue, + appServer.nodesPool.componentNodes, + incomingInput.question, + chatHistory, + chatId, + sessionId ?? '', + chatflowid, + appServer.AppDataSource, + incomingInput?.overrideConfig, + appServer.cachePool, + false, + undefined, + incomingInput.uploads + ) + + const nodeToExecute = + endingNodeIds.length === 1 + ? reactFlowNodes.find((node: IReactFlowNode) => endingNodeIds[0] === node.id) + : reactFlowNodes[reactFlowNodes.length - 1] + if (!nodeToExecute) { + return { + executionError: true, + status: 404, + msg: `Node not found` + } + } + + if (incomingInput.overrideConfig) { + nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig) + } + + const reactFlowNodeData: INodeData = resolveVariables(nodeToExecute.data, reactFlowNodes, incomingInput.question, chatHistory) + nodeToExecuteData = reactFlowNodeData + + appServer.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) + } + + logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) + + const nodeInstanceFilePath = appServer.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const nodeInstance = new nodeModule.nodeClass({ sessionId }) + + let result = isStreamValid + ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, + chatflowid, + chatHistory: incomingInput.history, + logger, + appDataSource: appServer.AppDataSource, + databaseEntities, + analytic: chatflow.analytic, + uploads: incomingInput.uploads, + socketIO, + socketIOClientId: incomingInput.socketIOClientId + }) + : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + chatId, + chatflowid, + chatHistory: incomingInput.history, + logger, + appDataSource: appServer.AppDataSource, + databaseEntities, + analytic: chatflow.analytic, + uploads: incomingInput.uploads + }) + result = typeof result === 'string' ? { text: result } : result + + // Retrieve threadId from assistant if exists + if (typeof result === 'object' && result.assistant) { + sessionId = result.assistant.threadId + } + + const userMessage: Omit = { + role: 'userMessage', + content: incomingInput.question, + chatflowid, + chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatId, + memoryType, + sessionId, + createdDate: userMessageDateTime, + fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined + } + await utilAddChatMessage(userMessage) + + let resultText = '' + if (result.text) resultText = result.text + else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2) + else resultText = JSON.stringify(result, null, 2) + + const apiMessage: Omit = { + role: 'apiMessage', + content: resultText, + chatflowid, + chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatId, + memoryType, + sessionId + } + 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) + const chatMessage = await utilAddChatMessage(apiMessage) + + logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) + await appServer.telemetry.sendTelemetry('prediction_sent', { + version: await getAppVersion(), + chatflowId: chatflowid, + chatId, + type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + flowGraph: getTelemetryFlowObj(nodes, edges) + }) + + // Prepare response + // return the question in the response + // this is used when input text is empty but question is in audio format + result.question = incomingInput.question + result.chatId = chatId + result.chatMessageId = chatMessage.id + if (sessionId) result.sessionId = sessionId + if (memoryType) result.memoryType = memoryType + + return result + } catch (e: any) { + logger.error('[server]: Error:', e) + return { + executionError: true, + status: 500, + msg: e.message + } + } +} diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts new file mode 100644 index 000000000..29335cbf4 --- /dev/null +++ b/packages/server/src/utils/getChatMessage.ts @@ -0,0 +1,101 @@ +import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm' +import { chatType } from '../Interface' +import { ChatMessage } from '../database/entities/ChatMessage' +import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' +/** + * Method that get chat messages. + * @param {string} chatflowid + * @param {chatType} chatType + * @param {string} sortOrder + * @param {string} chatId + * @param {string} memoryType + * @param {string} sessionId + * @param {string} startDate + * @param {string} endDate + * @param {boolean} feedback + */ +export const utilGetChatMessage = async ( + chatflowid: string, + chatType: chatType | undefined, + sortOrder: string = 'ASC', + chatId?: string, + memoryType?: string, + sessionId?: string, + startDate?: string, + endDate?: string, + messageId?: string, + feedback?: boolean +): Promise => { + const appServer = getRunningExpressApp() + const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { + const date = new Date(dateTimeStr) + if (isNaN(date.getTime())) { + return undefined + } + setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999) + return date + } + + const aMonthAgo = () => { + const date = new Date() + date.setMonth(new Date().getMonth() - 1) + return date + } + + let fromDate + if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start') + + let toDate + if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end') + + if (feedback) { + const query = await appServer.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message') + + // do the join with chat message feedback based on messageId for each chat message in the chatflow + query + .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') + .where('chat_message.chatflowid = :chatflowid', { chatflowid }) + + // based on which parameters are available add `andWhere` clauses to the query + if (chatType) { + query.andWhere('chat_message.chatType = :chatType', { chatType }) + } + if (chatId) { + query.andWhere('chat_message.chatId = :chatId', { chatId }) + } + if (memoryType) { + query.andWhere('chat_message.memoryType = :memoryType', { memoryType }) + } + if (sessionId) { + query.andWhere('chat_message.sessionId = :sessionId', { sessionId }) + } + + // set date range + query.andWhere('chat_message.createdDate BETWEEN :fromDate AND :toDate', { + fromDate: fromDate ?? aMonthAgo(), + toDate: toDate ?? new Date() + }) + // sort + query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC') + + const messages = await query.getMany() + return messages + } + + return await appServer.AppDataSource.getRepository(ChatMessage).find({ + where: { + chatflowid, + chatType, + chatId, + memoryType: memoryType ?? undefined, + sessionId: sessionId ?? undefined, + ...(fromDate && { createdDate: MoreThanOrEqual(fromDate) }), + ...(toDate && { createdDate: LessThanOrEqual(toDate) }), + id: messageId ?? undefined + }, + order: { + createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' + } + }) +} diff --git a/packages/server/src/utils/getChatMessageFeedback.ts b/packages/server/src/utils/getChatMessageFeedback.ts new file mode 100644 index 000000000..ebf2b9554 --- /dev/null +++ b/packages/server/src/utils/getChatMessageFeedback.ts @@ -0,0 +1,36 @@ +import { Between } from 'typeorm' +import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' + +/** + * Method that get chat messages. + * @param {string} chatflowid + * @param {string} sortOrder + * @param {string} chatId + * @param {string} startDate + * @param {string} endDate + */ +export const utilGetChatMessageFeedback = async ( + chatflowid: string, + chatId?: string, + sortOrder: string = 'ASC', + startDate?: string, + endDate?: string +): Promise => { + const appServer = getRunningExpressApp() + let fromDate + if (startDate) fromDate = new Date(startDate) + + let toDate + if (endDate) toDate = new Date(endDate) + return await appServer.AppDataSource.getRepository(ChatMessageFeedback).find({ + where: { + chatflowid, + chatId, + createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined + }, + order: { + createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' + } + }) +} diff --git a/packages/server/src/utils/getRunningExpressApp.ts b/packages/server/src/utils/getRunningExpressApp.ts new file mode 100644 index 000000000..e02f988f1 --- /dev/null +++ b/packages/server/src/utils/getRunningExpressApp.ts @@ -0,0 +1,9 @@ +import * as Server from '../index' + +export const getRunningExpressApp = function () { + const runningExpressInstance = Server.getInstance() + if (typeof runningExpressInstance === 'undefined' || typeof runningExpressInstance.nodesPool === 'undefined') { + throw new Error(`Error: getRunningExpressApp failed!`) + } + return runningExpressInstance +} diff --git a/packages/server/src/utils/getUploadsConfig.ts b/packages/server/src/utils/getUploadsConfig.ts new file mode 100644 index 000000000..7be23a6cf --- /dev/null +++ b/packages/server/src/utils/getUploadsConfig.ts @@ -0,0 +1,72 @@ +import { ChatFlow } from '../database/entities/ChatFlow' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' +import { IUploadFileSizeAndTypes, IReactFlowNode } from '../Interface' +import { INodeParams } from 'flowise-components' + +/** + * Method that checks if uploads are enabled in the chatflow + * @param {string} chatflowid + */ +export const utilGetUploadsConfig = async (chatflowid: string): Promise => { + const appServer = getRunningExpressApp() + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowid + }) + if (!chatflow) return `Chatflow ${chatflowid} not found` + + const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent'] + const uploadProcessingNodes = ['chatOpenAI', 'chatAnthropic', 'awsChatBedrock', 'azureChatOpenAI'] + + const flowObj = JSON.parse(chatflow.flowData) + const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = [] + + let isSpeechToTextEnabled = false + if (chatflow.speechToText) { + const speechToTextProviders = JSON.parse(chatflow.speechToText) + for (const provider in speechToTextProviders) { + if (provider !== 'none') { + const providerObj = speechToTextProviders[provider] + if (providerObj.status) { + isSpeechToTextEnabled = true + break + } + } + } + } + + let isImageUploadAllowed = false + const nodes: IReactFlowNode[] = flowObj.nodes + + /* + * Condition for isImageUploadAllowed + * 1.) one of the uploadAllowedNodes exists + * 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON + */ + if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) { + return { + isSpeechToTextEnabled, + isImageUploadAllowed: false, + imgUploadSizeAndTypes + } + } + + nodes.forEach((node: IReactFlowNode) => { + if (uploadProcessingNodes.indexOf(node.data.name) > -1) { + // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties + node.data.inputParams.map((param: INodeParams) => { + if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) { + imgUploadSizeAndTypes.push({ + fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'), + maxUploadSize: 5 + }) + isImageUploadAllowed = true + } + }) + } + }) + return { + isSpeechToTextEnabled, + isImageUploadAllowed, + imgUploadSizeAndTypes + } +} diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 9cff366ea..e41e9f64a 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -492,7 +492,7 @@ export const getVariableValue = ( isAcceptVariable = false ) => { const isObject = typeof paramValue === 'object' - let returnVal = isObject ? JSON.stringify(paramValue) : paramValue + let returnVal = (isObject ? JSON.stringify(paramValue) : paramValue) ?? '' const variableStack = [] const variableDict = {} as IVariableDict let startIdx = 0 diff --git a/packages/server/src/utils/rateLimit.ts b/packages/server/src/utils/rateLimit.ts index 68b5b693b..0c08f1d24 100644 --- a/packages/server/src/utils/rateLimit.ts +++ b/packages/server/src/utils/rateLimit.ts @@ -23,11 +23,8 @@ async function addRateLimiter(id: string, duration: number, limit: number, messa export function getRateLimiter(req: Request, res: Response, next: NextFunction) { const id = req.params.id - if (!rateLimiters[id]) return next() - const idRateLimiter = rateLimiters[id] - return idRateLimiter(req, res, next) } diff --git a/packages/server/src/utils/updateChatMessageFeedback.ts b/packages/server/src/utils/updateChatMessageFeedback.ts new file mode 100644 index 000000000..ef327fa78 --- /dev/null +++ b/packages/server/src/utils/updateChatMessageFeedback.ts @@ -0,0 +1,16 @@ +import { IChatMessageFeedback } from '../Interface' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' +import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' + +/** + * Method that updates chat message feedback. + * @param {string} id + * @param {Partial} chatMessageFeedback + */ +export const utilUpdateChatMessageFeedback = async (id: string, chatMessageFeedback: Partial) => { + const appServer = getRunningExpressApp() + const newChatMessageFeedback = new ChatMessageFeedback() + Object.assign(newChatMessageFeedback, chatMessageFeedback) + await appServer.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback) + return { status: 'OK' } +} diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts new file mode 100644 index 000000000..0fc90eb1c --- /dev/null +++ b/packages/server/src/utils/upsertVector.ts @@ -0,0 +1,139 @@ +import { Request, Response } from 'express' +import * as fs from 'fs' +import { ICommonObject } from 'flowise-components' +import telemetryService from '../services/telemetry' +import logger from '../utils/logger' +import { + buildFlow, + constructGraphs, + getAllConnectedNodes, + mapMimeTypeToInputField, + findMemoryNode, + getMemorySessionId, + getAppVersion, + getTelemetryFlowObj, + getStartingNodes +} from '../utils' +import { utilValidateKey } from './validateKey' +import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface' +import { ChatFlow } from '../database/entities/ChatFlow' +import { getRunningExpressApp } from '../utils/getRunningExpressApp' + +export const upsertVector = async (req: Request, res: Response, isInternal: boolean = false) => { + try { + const appServer = getRunningExpressApp() + const chatflowid = req.params.id + let incomingInput: IncomingInput = req.body + + const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowid + }) + if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) + + if (!isInternal) { + const isKeyValidated = await utilValidateKey(req, chatflow) + if (!isKeyValidated) return res.status(401).send('Unauthorized') + } + + const files = (req.files as any[]) || [] + + if (files.length) { + const overrideConfig: ICommonObject = { ...req.body } + for (const file of files) { + const fileData = fs.readFileSync(file.path, { encoding: 'base64' }) + const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}` + + const fileInputField = mapMimeTypeToInputField(file.mimetype) + if (overrideConfig[fileInputField]) { + overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String]) + } else { + overrideConfig[fileInputField] = JSON.stringify([dataBase64String]) + } + } + incomingInput = { + question: req.body.question ?? 'hello', + overrideConfig, + history: [], + stopNodeId: req.body.stopNodeId + } + } + + /*** Get chatflows and prepare data ***/ + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const edges = parsedFlowData.edges + + let stopNodeId = incomingInput?.stopNodeId ?? '' + let chatHistory = incomingInput?.history + let chatId = incomingInput.chatId ?? '' + let isUpsert = true + + // Get session ID + const memoryNode = findMemoryNode(nodes, edges) + let sessionId = undefined + if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) + + const vsNodes = nodes.filter( + (node) => + node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing') + ) + if (vsNodes.length > 1 && !stopNodeId) { + return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request') + } else if (vsNodes.length === 1 && !stopNodeId) { + stopNodeId = vsNodes[0].data.id + } else if (!vsNodes.length && !stopNodeId) { + return res.status(500).send('No vector node found') + } + + const { graph } = constructGraphs(nodes, edges, { isReversed: true }) + + const nodeIds = getAllConnectedNodes(graph, stopNodeId) + + const filteredGraph: INodeDirectedGraph = {} + for (const key of nodeIds) { + if (Object.prototype.hasOwnProperty.call(graph, key)) { + filteredGraph[key] = graph[key] + } + } + + const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId) + + await buildFlow( + startingNodeIds, + nodes, + edges, + filteredGraph, + depthQueue, + appServer.nodesPool.componentNodes, + incomingInput.question, + chatHistory, + chatId, + sessionId ?? '', + chatflowid, + appServer.AppDataSource, + incomingInput?.overrideConfig, + appServer.cachePool, + isUpsert, + stopNodeId + ) + + const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id)) + + await appServer.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) + await telemetryService.createEvent({ + name: `vector_upserted`, + data: { + version: await getAppVersion(), + chatlowId: chatflowid, + type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + flowGraph: getTelemetryFlowObj(nodes, edges), + stopNodeId + } + }) + return res.status(201).send('Successfully Upserted') + } catch (e: any) { + logger.error('[server]: Error:', e) + return res.status(500).send(e.message) + } +} diff --git a/packages/server/src/utils/validateKey.ts b/packages/server/src/utils/validateKey.ts new file mode 100644 index 000000000..02d36cf28 --- /dev/null +++ b/packages/server/src/utils/validateKey.ts @@ -0,0 +1,26 @@ +import { Request } from 'express' +import { ChatFlow } from '../database/entities/ChatFlow' +import { getAPIKeys, compareKeys } from './apiKey' + +/** + * Validate API Key + * @param {Request} req + * @param {Response} res + * @param {ChatFlow} chatflow + */ +export const utilValidateKey = async (req: Request, chatflow: ChatFlow) => { + const chatFlowApiKeyId = chatflow.apikeyid + if (!chatFlowApiKeyId) return true + + const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' + if (chatFlowApiKeyId && !authorizationHeader) return false + + const suppliedKey = authorizationHeader.split(`Bearer `).pop() + if (suppliedKey) { + const keys = await getAPIKeys() + const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret + if (!compareKeys(apiSecret, suppliedKey)) return false + return true + } + return false +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 5e4c0fcfb..b07446345 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,7 +22,7 @@ "@uiw/codemirror-theme-sublime": "^4.21.21", "@uiw/codemirror-theme-vscode": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", - "axios": "^0.27.2", + "axios": "1.6.2", "clsx": "^1.1.1", "dotenv": "^16.0.0", "flowise-embed": "latest", diff --git a/packages/ui/src/store/reducers/dialogReducer.js b/packages/ui/src/store/reducers/dialogReducer.js index ded576228..decd3922c 100644 --- a/packages/ui/src/store/reducers/dialogReducer.js +++ b/packages/ui/src/store/reducers/dialogReducer.js @@ -5,7 +5,8 @@ export const initialState = { title: '', description: '', confirmButtonName: 'OK', - cancelButtonName: 'Cancel' + cancelButtonName: 'Cancel', + customBtnId: '' } const alertReducer = (state = initialState, action) => { @@ -16,7 +17,8 @@ const alertReducer = (state = initialState, action) => { title: action.payload.title, description: action.payload.description, confirmButtonName: action.payload.confirmButtonName, - cancelButtonName: action.payload.cancelButtonName + cancelButtonName: action.payload.cancelButtonName, + customBtnId: 'btn_confirmDeletingApiKey' } case HIDE_CONFIRM: return initialState diff --git a/packages/ui/src/ui-component/button/FlowListMenu.jsx b/packages/ui/src/ui-component/button/FlowListMenu.jsx index bfee4eb16..a70e72767 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.jsx +++ b/packages/ui/src/ui-component/button/FlowListMenu.jsx @@ -153,9 +153,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { 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, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -192,9 +191,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { 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, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -224,9 +222,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { await chatflowsApi.deleteChatflow(chatflow.id) await updateFlowsApi.request() } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx index 7ce5e15e4..ae9869fa7 100644 --- a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx @@ -82,9 +82,8 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => { }) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index 80d0b45f0..fb672e06e 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -261,9 +261,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { getChatmessageApi.request(chatflowid) getStatsApi.request(chatflowid) // update stats } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/extended/AllowedDomains.jsx b/packages/ui/src/ui-component/extended/AllowedDomains.jsx index e0d01dd36..0f37785f9 100644 --- a/packages/ui/src/ui-component/extended/AllowedDomains.jsx +++ b/packages/ui/src/ui-component/extended/AllowedDomains.jsx @@ -69,9 +69,8 @@ const AllowedDomains = ({ dialogProps }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Allowed Origins: ${errorData}`, + message: `Failed to save Allowed Origins: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx index 4d00a12f9..a31bc6bb6 100644 --- a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx +++ b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx @@ -144,9 +144,8 @@ const AnalyseFlow = ({ dialogProps }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Analytic Configuration: ${errorData}`, + message: `Failed to save Analytic Configuration: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/extended/ChatFeedback.jsx b/packages/ui/src/ui-component/extended/ChatFeedback.jsx index 1706d6241..fcebf8f06 100644 --- a/packages/ui/src/ui-component/extended/ChatFeedback.jsx +++ b/packages/ui/src/ui-component/extended/ChatFeedback.jsx @@ -59,9 +59,8 @@ const ChatFeedback = ({ dialogProps }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Chat Feedback Settings: ${errorData}`, + message: `Failed to save Chat Feedback Settings: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/extended/RateLimit.jsx b/packages/ui/src/ui-component/extended/RateLimit.jsx index 5fc88cae2..5ab8532bd 100644 --- a/packages/ui/src/ui-component/extended/RateLimit.jsx +++ b/packages/ui/src/ui-component/extended/RateLimit.jsx @@ -73,12 +73,8 @@ const RateLimit = () => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - console.error(error) - const errorData = error.response - ? error.response.data || `${error.response.status}: ${error.response.statusText}` - : error.message enqueueSnackbar({ - message: `Failed to save Rate Limit Configuration: ${errorData}`, + message: `Failed to save Rate Limit Configuration: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/extended/SpeechToText.jsx b/packages/ui/src/ui-component/extended/SpeechToText.jsx index b56c8f852..ba875544c 100644 --- a/packages/ui/src/ui-component/extended/SpeechToText.jsx +++ b/packages/ui/src/ui-component/extended/SpeechToText.jsx @@ -112,9 +112,8 @@ const SpeechToText = ({ dialogProps }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Speech To Text Configuration: ${errorData}`, + message: `Failed to save Speech To Text Configuration: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/extended/StarterPrompts.jsx b/packages/ui/src/ui-component/extended/StarterPrompts.jsx index a12c51bbb..f19f326aa 100644 --- a/packages/ui/src/ui-component/extended/StarterPrompts.jsx +++ b/packages/ui/src/ui-component/extended/StarterPrompts.jsx @@ -80,9 +80,8 @@ const StarterPrompts = ({ dialogProps }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Conversation Starter Prompts: ${errorData}`, + message: `Failed to save Conversation Starter Prompts: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/ui-component/switch/Switch.jsx b/packages/ui/src/ui-component/switch/Switch.jsx index 50ace1197..e2d2dd78e 100644 --- a/packages/ui/src/ui-component/switch/Switch.jsx +++ b/packages/ui/src/ui-component/switch/Switch.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import { FormControl, Switch, Typography } from '@mui/material' export const SwitchInput = ({ label, value, onChange, disabled = false }) => { - const [myValue, setMyValue] = useState(!!value ?? false) + const [myValue, setMyValue] = useState(value !== undefined ? !!value : false) useEffect(() => { setMyValue(value) diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index 48262563e..c0326eb9d 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -77,9 +77,8 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => { onConfirm() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to add new API key: ${errorData}`, + message: `Failed to add new API key: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -114,9 +113,8 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => { onConfirm() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save API key: ${errorData}`, + message: `Failed to save API key: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -211,7 +209,11 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => { - (dialogProps.type === 'ADD' ? addNewKey() : saveKey())}> + (dialogProps.type === 'ADD' ? addNewKey() : saveKey())} + id={dialogProps.customBtnId} + > {dialogProps.confirmButtonName} diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index 935064c95..9eeb0a8c8 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -234,7 +234,8 @@ const APIKey = () => { title: 'Add New API Key', type: 'ADD', cancelButtonName: 'Cancel', - confirmButtonName: 'Add' + confirmButtonName: 'Add', + customBtnId: 'btn_confirmAddingApiKey' } setDialogProps(dialogProp) setShowDialog(true) @@ -246,6 +247,7 @@ const APIKey = () => { type: 'EDIT', cancelButtonName: 'Cancel', confirmButtonName: 'Save', + customBtnId: 'btn_confirmEditingApiKey', key } setDialogProps(dialogProp) @@ -260,7 +262,8 @@ const APIKey = () => { ? `Delete key [${key.keyName}] ? ` : `Delete key [${key.keyName}] ?\n There are ${key.chatFlows.length} chatflows using this key.`, confirmButtonName: 'Delete', - cancelButtonName: 'Cancel' + cancelButtonName: 'Cancel', + customBtnId: 'btn_initiateDeleteApiKey' } const isConfirmed = await confirm(confirmPayload) @@ -283,9 +286,8 @@ const APIKey = () => { onConfirm() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to delete API key: ${errorData}`, + message: `Failed to delete API key: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -363,6 +365,7 @@ const APIKey = () => { sx={{ color: 'white', mr: 1, height: 37 }} onClick={addNew} startIcon={} + id='btn_createApiKey' > Create Key diff --git a/packages/ui/src/views/assistants/AssistantDialog.jsx b/packages/ui/src/views/assistants/AssistantDialog.jsx index 88376a82f..da2e54adf 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.jsx +++ b/packages/ui/src/views/assistants/AssistantDialog.jsx @@ -235,9 +235,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { } setLoading(false) } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to add new Assistant: ${errorData}`, + message: `Failed to add new Assistant: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -289,9 +288,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { } setLoading(false) } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Assistant: ${errorData}`, + message: `Failed to save Assistant: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -329,9 +327,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { } setLoading(false) } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to sync Assistant: ${errorData}`, + message: `Failed to sync Assistant: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -376,9 +373,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { onConfirm() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to delete Assistant: ${errorData}`, + message: `Failed to delete Assistant: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index badbb162f..0e690218a 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -172,9 +172,8 @@ const Canvas = () => { localStorage.removeItem(`${chatflow.id}_INTERNAL`) navigate('/') } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -359,9 +358,7 @@ const Canvas = () => { setEdges(initialFlow.edges || []) dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi.error) { - const error = getSpecificChatflowApi.error - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` - errorFailed(`Failed to retrieve chatflow: ${errorData}`) + errorFailed(`Failed to retrieve chatflow: ${error.response.data.message}`) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -375,9 +372,7 @@ const Canvas = () => { saveChatflowSuccess() window.history.replaceState(null, null, `/canvas/${chatflow.id}`) } else if (createNewChatflowApi.error) { - const error = createNewChatflowApi.error - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` - errorFailed(`Failed to save chatflow: ${errorData}`) + errorFailed(`Failed to save chatflow: ${error.response.data.message}`) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -389,9 +384,7 @@ const Canvas = () => { dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi.error) { - const error = updateChatflowApi.error - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` - errorFailed(`Failed to save chatflow: ${errorData}`) + errorFailed(`Failed to save chatflow: ${error.response.data.message}`) } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/ui/src/views/chatflows/ShareChatbot.jsx b/packages/ui/src/views/chatflows/ShareChatbot.jsx index 28ad587b6..35003594c 100644 --- a/packages/ui/src/views/chatflows/ShareChatbot.jsx +++ b/packages/ui/src/views/chatflows/ShareChatbot.jsx @@ -161,10 +161,8 @@ const ShareChatbot = ({ isSessionMemory }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - console.error(error) - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Chatbot Configuration: ${errorData}`, + message: `Failed to save Chatbot Configuration: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -198,10 +196,8 @@ const ShareChatbot = ({ isSessionMemory }) => { dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) } } catch (error) { - console.error(error) - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Chatbot Configuration: ${errorData}`, + message: `Failed to save Chatbot Configuration: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index 90dc1a19a..8626ff13c 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -456,8 +456,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews }, 100) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` - handleError(errorData) + handleError(error.response.data.message) return } } diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.jsx b/packages/ui/src/views/chatmessage/ChatPopUp.jsx index 45557484c..b592643b0 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.jsx +++ b/packages/ui/src/views/chatmessage/ChatPopUp.jsx @@ -105,9 +105,8 @@ export const ChatPopUp = ({ chatflowid }) => { } }) } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx index 64b7590e2..d21ab95ca 100644 --- a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx +++ b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx @@ -118,9 +118,8 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => onConfirm(createResp.data.id) } } catch (error) { - const errorData = typeof err === 'string' ? err : err.response.data || `${err.response.status}: ${err.response.statusText}` enqueueSnackbar({ - message: `Failed to add new Credential: ${errorData}`, + message: `Failed to add new Credential: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -168,9 +167,8 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => onConfirm(saveResp.data.id) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Credential: ${errorData}`, + message: `Failed to save Credential: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/credentials/index.jsx b/packages/ui/src/views/credentials/index.jsx index b22b54604..6e1441015 100644 --- a/packages/ui/src/views/credentials/index.jsx +++ b/packages/ui/src/views/credentials/index.jsx @@ -139,9 +139,8 @@ const Credentials = () => { onConfirm() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to delete Credential: ${errorData}`, + message: `Failed to delete Credential: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/tools/ToolDialog.jsx b/packages/ui/src/views/tools/ToolDialog.jsx index da540ba14..0a314f771 100644 --- a/packages/ui/src/views/tools/ToolDialog.jsx +++ b/packages/ui/src/views/tools/ToolDialog.jsx @@ -225,9 +225,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = linkElement.click() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to export Tool: ${errorData}`, + message: `Failed to export Tool: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -270,9 +269,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = onConfirm(createResp.data.id) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to add new Tool: ${errorData}`, + message: `Failed to add new Tool: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -313,10 +311,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = onConfirm(saveResp.data.id) } } catch (error) { - console.error(error) - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to save Tool: ${errorData}`, + message: `Failed to save Tool: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -360,9 +356,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = onConfirm() } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: `Failed to delete Tool: ${errorData}`, + message: `Failed to delete Tool: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.jsx b/packages/ui/src/views/variables/AddEditVariableDialog.jsx index 295eb3f9b..ba4f45b1a 100644 --- a/packages/ui/src/views/variables/AddEditVariableDialog.jsx +++ b/packages/ui/src/views/variables/AddEditVariableDialog.jsx @@ -111,9 +111,8 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { onConfirm(createResp.data.id) } } catch (err) { - const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}` enqueueSnackbar({ - message: `Failed to add new Variable: ${errorData}`, + message: `Failed to add new Variable: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -154,9 +153,8 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { onConfirm(saveResp.data.id) } } catch (error) { - const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}` enqueueSnackbar({ - message: `Failed to save Variable: ${errorData}`, + message: `Failed to save Variable: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -222,6 +220,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { key='variableName' onChange={(e) => setVariableName(e.target.value)} value={variableName ?? ''} + id='txtInput_variableName' /> @@ -237,6 +236,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { options={variableTypes} onSelect={(newValue) => setVariableType(newValue)} value={variableType ?? 'choose an option'} + id='dropdown_variableType' /> {variableType === 'static' && ( @@ -255,6 +255,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { key='variableValue' onChange={(e) => setVariableValue(e.target.value)} value={variableValue ?? ''} + id='txtInput_variableValue' /> )} @@ -264,6 +265,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { disabled={!variableName || !variableType || (variableType === 'static' && !variableValue)} variant='contained' onClick={() => (dialogType === 'ADD' ? addNewVariable() : saveVariable())} + id='btn_confirmAddingNewVariable' > {dialogProps.confirmButtonName} diff --git a/packages/ui/src/views/variables/index.jsx b/packages/ui/src/views/variables/index.jsx index ce599a315..cc6aee78e 100644 --- a/packages/ui/src/views/variables/index.jsx +++ b/packages/ui/src/views/variables/index.jsx @@ -81,6 +81,7 @@ const Variables = () => { type: 'ADD', cancelButtonName: 'Cancel', confirmButtonName: 'Add', + customBtnId: 'btn_confirmAddingVariable', data: {} } setVariableDialogProps(dialogProp) @@ -126,9 +127,8 @@ const Variables = () => { onConfirm() } } catch (error) { - const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}` enqueueSnackbar({ - message: `Failed to delete Variable: ${errorData}`, + message: `Failed to delete Variable: ${error.response.data.message}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -207,6 +207,7 @@ const Variables = () => { sx={{ color: 'white', mr: 1, height: 37 }} onClick={addNew} startIcon={} + id='btn_createVariable' > Add Variable diff --git a/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx b/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx index 7bc796ebb..6426a08fc 100644 --- a/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx +++ b/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx @@ -291,9 +291,8 @@ query(formData).then((response) => { }) setLoading(false) } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx b/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx index a77d94866..13cd9043c 100644 --- a/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx +++ b/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx @@ -59,9 +59,8 @@ export const VectorStorePopUp = ({ chatflowid }) => { } }) } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ - message: errorData, + message: error.response.data.message, options: { key: new Date().getTime() + Math.random(), variant: 'error', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62b589010..2f9d6aa39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,7 +187,7 @@ importers: version: 4.3.2 axios: specifier: 1.6.2 - version: 1.6.2 + version: 1.6.2(debug@4.3.4) cheerio: specifier: ^1.0.0-rc.12 version: 1.0.0-rc.12 @@ -411,7 +411,7 @@ importers: version: 0.4.1 axios: specifier: 1.6.2 - version: 1.6.2 + version: 1.6.2(debug@4.3.4) content-disposition: specifier: 0.5.4 version: 0.5.4 @@ -439,6 +439,12 @@ importers: flowise-ui: specifier: workspace:^ version: link:../ui + http-errors: + specifier: ^2.0.0 + version: 2.0.0 + http-status-codes: + specifier: ^2.3.0 + version: 2.3.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -503,6 +509,9 @@ importers: concurrently: specifier: ^7.1.0 version: 7.6.0 + cypress: + specifier: ^13.7.1 + version: 13.7.1 nodemon: specifier: ^2.0.22 version: 2.0.22 @@ -518,6 +527,9 @@ importers: shx: specifier: ^0.3.3 version: 0.3.4 + start-server-and-test: + specifier: ^2.0.3 + version: 2.0.3 ts-node: specifier: ^10.7.0 version: 10.9.2(@types/node@20.11.26)(typescript@4.9.5) @@ -573,8 +585,8 @@ importers: specifier: ^4.21.21 version: 4.21.24(@babel/runtime@7.24.0)(@codemirror/autocomplete@6.14.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.5.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.25.1)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0) axios: - specifier: ^0.27.2 - version: 0.27.2 + specifier: 1.6.2 + version: 1.6.2(debug@4.3.4) clsx: specifier: ^1.1.1 version: 1.2.1 @@ -3356,6 +3368,12 @@ packages: w3c-keyname: 2.2.8 dev: false + /@colors/colors@1.5.0: + resolution: { integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== } + engines: { node: '>=0.1.90' } + dev: true + optional: true + /@colors/colors@1.6.0: resolution: { integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== } engines: { node: '>=0.1.90' } @@ -3531,6 +3549,39 @@ packages: postcss-selector-parser: 6.0.15 dev: true + /@cypress/request@3.0.1: + resolution: { integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== } + engines: { node: '>= 6' } + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + http-signature: 1.3.6 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.10.4 + safe-buffer: 5.2.1 + tough-cookie: 4.1.3 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + dev: true + + /@cypress/xvfb@1.2.4(supports-color@8.1.1): + resolution: { integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== } + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /@dabh/diagnostics@2.0.3: resolution: { integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== } dependencies: @@ -3544,7 +3595,7 @@ packages: engines: { node: '>=14.0.0' } hasBin: true dependencies: - axios: 1.6.2 + axios: 1.6.2(debug@4.3.4) bson: 6.4.0 winston: 3.12.0 transitivePeerDependencies: @@ -4289,6 +4340,16 @@ packages: yargs: 17.7.2 dev: false + /@hapi/hoek@9.3.0: + resolution: { integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== } + dev: true + + /@hapi/topo@5.1.0: + resolution: { integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== } + dependencies: + '@hapi/hoek': 9.3.0 + dev: true + /@huggingface/inference@2.6.4: resolution: { integrity: sha512-Xna7arltBSBoKaH3diGi3sYvkExgJMd/pF4T6vl2YbmDccbr1G/X5EPZ2048p+YgrJYG1jTYFCtY6Dr3HvJaow== } engines: { node: '>=18' } @@ -6450,6 +6511,20 @@ packages: resolution: { integrity: sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg== } dev: false + /@sideway/address@4.1.5: + resolution: { integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== } + dependencies: + '@hapi/hoek': 9.3.0 + dev: true + + /@sideway/formula@3.0.1: + resolution: { integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== } + dev: true + + /@sideway/pinpoint@2.0.0: + resolution: { integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== } + dev: true + /@sigstore/bundle@1.1.0: resolution: { integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog== } engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } @@ -8064,6 +8139,14 @@ packages: '@types/node': 20.11.26 dev: true + /@types/sinonjs__fake-timers@8.1.1: + resolution: { integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== } + dev: true + + /@types/sizzle@2.3.8: + resolution: { integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== } + dev: true + /@types/sockjs@0.3.36: resolution: { integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== } dependencies: @@ -8175,7 +8258,6 @@ packages: resolution: { integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== } dependencies: '@types/node': 20.11.26 - dev: false optional: true /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@4.9.5): @@ -8908,7 +8990,6 @@ packages: /ansi-colors@4.1.3: resolution: { integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== } engines: { node: '>=6' } - dev: false /ansi-cyan@0.1.1: resolution: { integrity: sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A== } @@ -9136,6 +9217,10 @@ packages: /aproba@2.0.0: resolution: { integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== } + /arch@2.2.0: + resolution: { integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== } + dev: true + /archy@1.0.0: resolution: { integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== } dev: true @@ -9415,6 +9500,12 @@ packages: /asap@2.0.6: resolution: { integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== } + /asn1@0.2.6: + resolution: { integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== } + dependencies: + safer-buffer: 2.1.2 + dev: true + /assemble-core@0.25.0: resolution: { integrity: sha512-5vS/XZK0ke3gIHoKTyl88brqOR9zw3niz5jJHrEgrDLlZGEri4a1Wr4badallKCx4M4/TWG12GT/O5wABZjaVA== } engines: { node: '>=4.0' } @@ -9504,6 +9595,11 @@ packages: - utf-8-validate dev: false + /assert-plus@1.0.0: + resolution: { integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== } + engines: { node: '>=0.8' } + dev: true + /assign-deep@0.4.8: resolution: { integrity: sha512-uxqXJCnNZDEjPnsaLKVzmh/ST5+Pqoz0wi06HDfHKx1ASNpSbbvz2qW2Gl8ZyHwr5jnm11X2S5eMQaP1lMZmCg== } engines: { node: '>=0.10.0' } @@ -9666,9 +9762,12 @@ packages: xml2js: 0.6.2 dev: true + /aws-sign2@0.7.0: + resolution: { integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== } + dev: true + /aws4@1.12.0: resolution: { integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== } - dev: false /axe-core@4.7.0: resolution: { integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== } @@ -9680,29 +9779,19 @@ packages: engines: { node: '>=4' } dev: false - /axios@0.27.2: - resolution: { integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== } - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - transitivePeerDependencies: - - debug - dev: false - - /axios@1.6.2: + /axios@1.6.2(debug@4.3.4): resolution: { integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== } dependencies: - follow-redirects: 1.15.5 + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false /axios@1.6.7: resolution: { integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== } dependencies: - follow-redirects: 1.15.5 + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -10519,6 +10608,12 @@ packages: resolution: { integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== } dev: true + /bcrypt-pbkdf@1.0.2: + resolution: { integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== } + dependencies: + tweetnacl: 0.14.5 + dev: true + /before-after-hook@2.2.3: resolution: { integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== } dev: true @@ -10589,6 +10684,10 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 + /blob-util@2.0.2: + resolution: { integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== } + dev: true + /bluebird@3.4.7: resolution: { integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== } dev: false @@ -10722,7 +10821,6 @@ packages: /buffer-crc32@0.2.13: resolution: { integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== } - dev: false /buffer-equal-constant-time@1.0.1: resolution: { integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== } @@ -10916,6 +11014,11 @@ packages: responselike: 2.0.1 dev: true + /cachedir@2.4.0: + resolution: { integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== } + engines: { node: '>=6' } + dev: true + /call-bind@1.0.7: resolution: { integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== } engines: { node: '>= 0.4' } @@ -11008,6 +11111,10 @@ packages: engines: { node: '>=4' } dev: true + /caseless@0.12.0: + resolution: { integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== } + dev: true + /catharsis@0.9.0: resolution: { integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== } engines: { node: '>= 10' } @@ -11087,6 +11194,11 @@ packages: resolution: { integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== } dev: false + /check-more-types@2.24.0: + resolution: { integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== } + engines: { node: '>= 0.8.0' } + dev: true + /check-types@11.2.3: resolution: { integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== } dev: true @@ -11313,6 +11425,15 @@ packages: engines: { node: '>=6' } dev: true + /cli-table3@0.6.4: + resolution: { integrity: sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw== } + engines: { node: 10.* || >= 12.* } + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: true + /cli-table@0.3.11: resolution: { integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ== } engines: { node: '>= 0.2.0' } @@ -11320,6 +11441,14 @@ packages: colors: 1.0.3 dev: true + /cli-truncate@2.1.0: + resolution: { integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== } + engines: { node: '>=8' } + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + dev: true + /cli-truncate@3.1.0: resolution: { integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } @@ -11612,6 +11741,11 @@ packages: engines: { node: '>= 6' } dev: true + /commander@6.2.1: + resolution: { integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== } + engines: { node: '>= 6' } + dev: true + /commander@7.1.0: resolution: { integrity: sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== } engines: { node: '>= 10' } @@ -11829,6 +11963,10 @@ packages: resolution: { integrity: sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw== } dev: true + /core-util-is@1.0.2: + resolution: { integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== } + dev: true + /core-util-is@1.0.3: resolution: { integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== } @@ -12208,6 +12346,55 @@ packages: find-pkg: 0.1.2 dev: false + /cypress@13.7.1: + resolution: { integrity: sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w== } + engines: { node: ^16.0.0 || ^18.0.0 || >=20.0.0 } + hasBin: true + dependencies: + '@cypress/request': 3.0.1 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.8 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.4 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.10 + debug: 4.3.4(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.6.0 + supports-color: 8.1.1 + tmp: 0.2.3 + untildify: 4.0.0 + yauzl: 2.10.0 + dev: true + /d3-color@3.1.0: resolution: { integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== } engines: { node: '>=12' } @@ -12299,6 +12486,13 @@ packages: engines: { node: '>=8' } dev: true + /dashdash@1.14.1: + resolution: { integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== } + engines: { node: '>=0.10' } + dependencies: + assert-plus: 1.0.0 + dev: true + /data-store@0.16.1: resolution: { integrity: sha512-tGbl4oVi9UPysie6y6+fuCjUNhaR3KxnuIRV0OMUCwq/wvikmWHXQYALbW/IVQvmxBNbrxUwjG5BWsrjx5v55w== } engines: { node: '>=0.10.0' } @@ -12364,7 +12558,6 @@ packages: /dayjs@1.11.10: resolution: { integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== } - dev: false /debug@2.6.9: resolution: { integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== } @@ -12387,6 +12580,18 @@ packages: ms: 2.1.3 supports-color: 5.5.0 + /debug@3.2.7(supports-color@8.1.1): + resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 8.1.1 + dev: true + /debug@4.3.4(supports-color@5.5.0): resolution: { integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== } engines: { node: '>=6.0' } @@ -12942,6 +13147,13 @@ packages: /eastasianwidth@0.2.0: resolution: { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== } + /ecc-jsbn@0.1.2: + resolution: { integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== } + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: true + /ecdsa-sig-formatter@1.0.11: resolution: { integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== } dependencies: @@ -13118,6 +13330,14 @@ packages: tapable: 2.2.1 dev: true + /enquirer@2.4.1: + resolution: { integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== } + engines: { node: '>=8.6' } + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: true + /entities@2.1.0: resolution: { integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== } dev: false @@ -13884,6 +14104,10 @@ packages: resolution: { integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== } engines: { node: '>=6' } + /eventemitter2@6.4.7: + resolution: { integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== } + dev: true + /eventemitter3@4.0.7: resolution: { integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== } @@ -13955,6 +14179,13 @@ packages: strip-final-newline: 3.0.0 dev: true + /executable@4.1.1: + resolution: { integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== } + engines: { node: '>=4' } + dependencies: + pify: 2.3.0 + dev: true + /exit-hook@1.1.1: resolution: { integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg== } engines: { node: '>=0.10.0' } @@ -14227,6 +14458,25 @@ packages: - supports-color dev: false + /extract-zip@2.0.1(supports-color@8.1.1): + resolution: { integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== } + engines: { node: '>= 10.17.0' } + hasBin: true + dependencies: + debug: 4.3.4(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: true + + /extsprintf@1.3.0: + resolution: { integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== } + engines: { '0': node >=0.6.0 } + dev: true + /faiss-node@0.2.3: resolution: { integrity: sha512-HfGhKFjyXPIyAlatcBNjv66q5ZQ43xfIpv8Uc17mIbEye7gbrmzVqAy+OxdlRy0usuLni+Dk1vSXf/z2yyr1Dg== } engines: { node: '>= 14.0.0' } @@ -14373,7 +14623,6 @@ packages: resolution: { integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== } dependencies: pend: 1.2.0 - dev: false /fecha@4.2.3: resolution: { integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== } @@ -14749,7 +14998,7 @@ packages: resolution: { integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== } dev: false - /follow-redirects@1.15.5: + /follow-redirects@1.15.5(debug@4.3.4): resolution: { integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== } engines: { node: '>=4.0' } peerDependencies: @@ -14757,6 +15006,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.4(supports-color@5.5.0) /for-each@0.3.3: resolution: { integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== } @@ -14794,6 +15045,10 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 + /forever-agent@0.6.1: + resolution: { integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== } + dev: true + /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.0)(typescript@4.9.5)(webpack@5.90.3): resolution: { integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== } engines: { node: '>=10', yarn: '>=1.0.0' } @@ -14830,6 +15085,15 @@ packages: resolution: { integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== } dev: false + /form-data@2.3.3: + resolution: { integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== } + engines: { node: '>= 0.12' } + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + /form-data@3.0.1: resolution: { integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== } engines: { node: '>= 6' } @@ -14845,7 +15109,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false /format@0.2.2: resolution: { integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== } @@ -15236,6 +15499,18 @@ packages: match-file: 0.2.2 dev: false + /getos@3.2.1: + resolution: { integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== } + dependencies: + async: 3.2.5 + dev: true + + /getpass@0.1.7: + resolution: { integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== } + dependencies: + assert-plus: 1.0.0 + dev: true + /git-config-path@1.0.1: resolution: { integrity: sha512-KcJ2dlrrP5DbBnYIZ2nlikALfRhKzNSX0stvv3ImJ+fvC4hXKoV+U+74SV0upg+jlQZbrtQzc0bu6/Zh+7aQbg== } engines: { node: '>=0.10.0' } @@ -15395,6 +15670,13 @@ packages: minimatch: 5.1.6 once: 1.4.0 + /global-dirs@3.0.1: + resolution: { integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== } + engines: { node: '>=10' } + dependencies: + ini: 2.0.0 + dev: true + /global-modules@0.2.3: resolution: { integrity: sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA== } engines: { node: '>=0.10.0' } @@ -16306,12 +16588,25 @@ packages: engines: { node: '>=8.0.0' } dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.5 + follow-redirects: 1.15.5(debug@4.3.4) requires-port: 1.0.0 transitivePeerDependencies: - debug dev: true + /http-signature@1.3.6: + resolution: { integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== } + engines: { node: '>=0.10' } + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + dev: true + + /http-status-codes@2.3.0: + resolution: { integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== } + dev: false + /http2-wrapper@1.0.3: resolution: { integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== } engines: { node: '>=10.19.0' } @@ -16493,6 +16788,11 @@ packages: /ini@1.3.8: resolution: { integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== } + /ini@2.0.0: + resolution: { integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== } + engines: { node: '>=10' } + dev: true + /inline-style-parser@0.1.1: resolution: { integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== } dev: false @@ -16720,6 +17020,13 @@ packages: engines: { node: '>= 0.4' } dev: true + /is-ci@3.0.1: + resolution: { integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== } + hasBin: true + dependencies: + ci-info: 3.9.0 + dev: true + /is-core-module@2.13.1: resolution: { integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== } dependencies: @@ -16854,6 +17161,14 @@ packages: /is-hexadecimal@1.0.4: resolution: { integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== } + /is-installed-globally@0.4.0: + resolution: { integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== } + engines: { node: '>=10' } + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + dev: true + /is-interactive@1.0.0: resolution: { integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== } engines: { node: '>=8' } @@ -17251,6 +17566,10 @@ packages: - encoding dev: false + /isstream@0.1.2: + resolution: { integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== } + dev: true + /istanbul-lib-coverage@3.2.2: resolution: { integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== } engines: { node: '>=8' } @@ -17933,6 +18252,16 @@ packages: engines: { node: '>= 0.6.0' } dev: true + /joi@17.12.2: + resolution: { integrity: sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw== } + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + dev: true + /js-base64@3.7.2: resolution: { integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== } dev: false @@ -17973,6 +18302,10 @@ packages: xmlcreate: 2.0.4 dev: false + /jsbn@0.1.1: + resolution: { integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== } + dev: true + /jsbn@1.1.0: resolution: { integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== } @@ -18173,6 +18506,10 @@ packages: resolution: { integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== } dev: true + /json-stringify-safe@5.0.1: + resolution: { integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== } + dev: true + /json5@0.5.1: resolution: { integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw== } hasBin: true @@ -18226,6 +18563,16 @@ packages: resolution: { integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== } engines: { node: '>=0.10.0' } + /jsprim@2.0.2: + resolution: { integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== } + engines: { '0': node >=0.6.0 } + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: true + /jsx-ast-utils@3.3.5: resolution: { integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== } engines: { node: '>=4.0' } @@ -18527,7 +18874,7 @@ packages: '@supabase/supabase-js': 2.39.8 apify-client: 2.9.3 assemblyai: 4.3.2 - axios: 1.6.2 + axios: 1.6.2(debug@4.3.4) binary-extensions: 2.2.0 cheerio: 1.0.0-rc.12 chromadb: 1.8.1(@google/generative-ai@0.1.3)(cohere-ai@6.2.2)(openai@4.28.4) @@ -18714,6 +19061,11 @@ packages: lazy-cache: 1.0.4 dev: false + /lazy-ass@1.6.0: + resolution: { integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== } + engines: { node: '> 0.8' } + dev: true + /lazy-cache@0.2.7: resolution: { integrity: sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ== } engines: { node: '>=0.10.0' } @@ -18840,6 +19192,26 @@ packages: - supports-color dev: true + /listr2@3.14.0(enquirer@2.4.1): + resolution: { integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== } + engines: { node: '>=10.0.0' } + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + enquirer: 2.4.1 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.3.1 + rxjs: 7.8.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + /listr2@6.6.1: resolution: { integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg== } engines: { node: '>=16.0.0' } @@ -19186,6 +19558,10 @@ packages: /lodash.merge@4.6.2: resolution: { integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== } + /lodash.once@4.1.1: + resolution: { integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== } + dev: true + /lodash.pairs@3.0.1: resolution: { integrity: sha512-lgXvpU43ZNQrZ/pK2cR97YzKeAno3e3HhcyvLKsofljeHKrQcZhT1vW7fg4X61c92tM+mjD/DypoLZYuAKNIkQ== } dependencies: @@ -19246,6 +19622,16 @@ packages: is-unicode-supported: 0.1.0 dev: true + /log-update@4.0.0: + resolution: { integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== } + engines: { node: '>=10' } + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + dev: true + /log-update@5.0.1: resolution: { integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw== } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } @@ -21707,6 +22093,10 @@ packages: engines: { node: '>=0.10.0' } dev: true + /ospath@1.2.2: + resolution: { integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== } + dev: true + /ow@0.28.2: resolution: { integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q== } engines: { node: '>=12' } @@ -22227,7 +22617,6 @@ packages: /pend@1.2.0: resolution: { integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== } - dev: false /perfect-scrollbar@1.5.5: resolution: { integrity: sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g== } @@ -23260,7 +23649,7 @@ packages: resolution: { integrity: sha512-JB+ei0LkwE+rKHyW5z79Nd1jUaGxU6TvkfjFqY9vQaHxU5aU8dRl0UUaEmZdZbHwjp3WmXCBQQRNyimwbNQfCw== } engines: { node: '>=15.0.0' } dependencies: - axios: 1.6.2 + axios: 1.6.2(debug@4.3.4) rusha: 0.8.14 transitivePeerDependencies: - debug @@ -23637,9 +24026,12 @@ packages: - supports-color dev: false + /proxy-from-env@1.0.0: + resolution: { integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== } + dev: true + /proxy-from-env@1.1.0: resolution: { integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== } - dev: false /ps-tree@1.2.0: resolution: { integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA== } @@ -23747,6 +24139,13 @@ packages: engines: { node: '>=0.6.0', teleport: '>=0.2.0' } dev: true + /qs@6.10.4: + resolution: { integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== } + engines: { node: '>=0.6' } + dependencies: + side-channel: 1.0.6 + dev: true + /qs@6.11.0: resolution: { integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== } engines: { node: '>=0.6' } @@ -24933,6 +25332,12 @@ packages: project-name: 0.2.6 dev: false + /request-progress@3.0.0: + resolution: { integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== } + dependencies: + throttleit: 1.0.1 + dev: true + /require-directory@2.1.1: resolution: { integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== } engines: { node: '>=0.10.0' } @@ -25813,6 +26218,15 @@ packages: resolution: { integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== } engines: { node: '>=12' } + /slice-ansi@3.0.0: + resolution: { integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== } + engines: { node: '>=8' } + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + /slice-ansi@4.0.0: resolution: { integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== } engines: { node: '>=10' } @@ -26221,6 +26635,22 @@ packages: hasBin: true dev: false + /sshpk@1.18.0: + resolution: { integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== } + engines: { node: '>=0.10.0' } + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: true + /ssri@10.0.5: resolution: { integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A== } engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } @@ -26264,6 +26694,23 @@ packages: resolution: { integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== } dev: false + /start-server-and-test@2.0.3: + resolution: { integrity: sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg== } + engines: { node: '>=16' } + hasBin: true + dependencies: + arg: 5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.3.4(supports-color@5.5.0) + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 7.2.0(debug@4.3.4) + transitivePeerDependencies: + - supports-color + dev: true + /static-eval@2.0.2: resolution: { integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== } dependencies: @@ -27022,6 +27469,10 @@ packages: resolution: { integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== } dev: true + /throttleit@1.0.1: + resolution: { integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== } + dev: true + /through2-filter@2.0.0: resolution: { integrity: sha512-miwWajb1B80NvIVKXFPN/o7+vJc4jYUvnZCwvhicRAoTxdD9wbcjri70j+BenCrN/JXEPKDjhpw4iY7yiNsCGg== } dependencies: @@ -27101,7 +27552,6 @@ packages: /tmp@0.2.3: resolution: { integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== } engines: { node: '>=14.14' } - dev: false /tmpl@1.0.5: resolution: { integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== } @@ -27446,6 +27896,10 @@ packages: turbo-windows-arm64: 1.10.16 dev: true + /tweetnacl@0.14.5: + resolution: { integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== } + dev: true + /type-check@0.3.2: resolution: { integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== } engines: { node: '>= 0.8.0' } @@ -28326,6 +28780,15 @@ packages: resolution: { integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== } engines: { node: '>= 0.8' } + /verror@1.10.0: + resolution: { integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== } + engines: { '0': node >=0.6.0 } + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: true + /vfile-location@5.0.2: resolution: { integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== } dependencies: @@ -28638,6 +29101,20 @@ packages: xml-name-validator: 4.0.0 dev: false + /wait-on@7.2.0(debug@4.3.4): + resolution: { integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ== } + engines: { node: '>=12.0.0' } + hasBin: true + dependencies: + axios: 1.6.2(debug@4.3.4) + joi: 17.12.2 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.1 + transitivePeerDependencies: + - debug + dev: true + /walk-up-path@1.0.0: resolution: { integrity: sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== } dev: true @@ -29693,7 +30170,6 @@ packages: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - dev: false /yeoman-environment@3.19.3: resolution: { integrity: sha512-/+ODrTUHtlDPRH9qIC0JREH8+7nsRcjDl3Bxn2Xo/rvAaVvixH5275jHwg0C85g4QsF4P6M2ojfScPPAl+pLAg== }