Feature: Custom Templates (#3169)

* New Feature: Custom Templates in the marketplace.

* New Feature: Custom Templates in the marketplace.

* Custom Template Delete and Shortcut in the dropdown menu

* auto detect framework

* minor ui fixes

* adding custom template feature for tools

* ui tool dialog save template

---------

Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
Vinod Kiran
2024-09-16 19:14:39 +05:30
committed by GitHub
parent 44b70ca7e2
commit b02bdc74ad
23 changed files with 1217 additions and 170 deletions
+13
View File
@@ -273,5 +273,18 @@ export interface IApiKey {
updatedDate: Date
}
export interface ICustomTemplate {
id: string
name: string
flowData: string
updatedDate: Date
createdDate: Date
description?: string
type?: string
badge?: string
framework?: string
usecases?: string
}
// DocumentStore related
export * from './Interface.DocumentStore'
@@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express'
import marketplacesService from '../../services/marketplaces'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
// Get all templates for marketplaces
const getAllTemplates = async (req: Request, res: Response, next: NextFunction) => {
@@ -11,6 +13,48 @@ const getAllTemplates = async (req: Request, res: Response, next: NextFunction)
}
}
export default {
getAllTemplates
const deleteCustomTemplate = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: marketplacesService.deleteCustomTemplate - id not provided!`
)
}
const apiResponse = await marketplacesService.deleteCustomTemplate(req.params.id)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const getAllCustomTemplates = async (req: Request, res: Response, next: NextFunction) => {
try {
const apiResponse = await marketplacesService.getAllCustomTemplates()
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const saveCustomTemplate = async (req: Request, res: Response, next: NextFunction) => {
try {
if ((!req.body && !(req.body.chatflowId || req.body.tool)) || !req.body.name) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: marketplacesService.saveCustomTemplate - body not provided!`
)
}
const apiResponse = await marketplacesService.saveCustomTemplate(req.body)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
export default {
getAllTemplates,
getAllCustomTemplates,
saveCustomTemplate,
deleteCustomTemplate
}
@@ -0,0 +1,37 @@
import { ICustomTemplate } from '../../Interface'
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
@Entity('custom_template')
export class CustomTemplate implements ICustomTemplate {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
name: string
@Column({ type: 'text' })
flowData: string
@Column({ nullable: true, type: 'text' })
description?: string
@Column({ nullable: true, type: 'text' })
badge?: string
@Column({ nullable: true, type: 'text' })
framework?: string
@Column({ nullable: true, type: 'text' })
usecases?: string
@Column({ nullable: true, type: 'text' })
type?: string
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
}
@@ -10,6 +10,7 @@ import { DocumentStoreFileChunk } from './DocumentStoreFileChunk'
import { Lead } from './Lead'
import { UpsertHistory } from './UpsertHistory'
import { ApiKey } from './ApiKey'
import { CustomTemplate } from './CustomTemplate'
export const entities = {
ChatFlow,
@@ -23,5 +24,6 @@ export const entities = {
DocumentStoreFileChunk,
Lead,
UpsertHistory,
ApiKey
ApiKey,
CustomTemplate
}
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS \`custom_template\` (
\`id\` varchar(36) NOT NULL,
\`name\` varchar(255) NOT NULL,
\`flowData\` text NOT NULL,
\`description\` varchar(255) DEFAULT NULL,
\`badge\` varchar(255) DEFAULT NULL,
\`framework\` varchar(255) DEFAULT NULL,
\`usecases\` varchar(255) DEFAULT NULL,
\`type\` varchar(30) DEFAULT NULL,
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
\`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE custom_template`)
}
}
@@ -23,6 +23,7 @@ import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlo
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { LongTextColumn1722301395521 } from './1722301395521-LongTextColumn'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const mariadbMigrations = [
Init1693840429259,
@@ -49,5 +50,6 @@ export const mariadbMigrations = [
AddTypeToChatFlow1716300000000,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523,
LongTextColumn1722301395521
LongTextColumn1722301395521,
AddCustomTemplate1725629836652
]
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS \`custom_template\` (
\`id\` varchar(36) NOT NULL,
\`name\` varchar(255) NOT NULL,
\`flowData\` text NOT NULL,
\`description\` varchar(255) DEFAULT NULL,
\`badge\` varchar(255) DEFAULT NULL,
\`framework\` varchar(255) DEFAULT NULL,
\`usecases\` varchar(255) DEFAULT NULL,
\`type\` varchar(30) DEFAULT NULL,
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
\`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE custom_template`)
}
}
@@ -24,6 +24,7 @@ import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlo
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { LongTextColumn1722301395521 } from './1722301395521-LongTextColumn'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const mysqlMigrations = [
Init1693840429259,
@@ -51,5 +52,6 @@ export const mysqlMigrations = [
AddVectorStoreConfigToDocStore1715861032479,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523,
LongTextColumn1722301395521
LongTextColumn1722301395521,
AddCustomTemplate1725629836652
]
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS custom_template (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" varchar NOT NULL,
"flowData" text NOT NULL,
"description" varchar NULL,
"badge" varchar NULL,
"framework" varchar NULL,
"usecases" varchar NULL,
"type" varchar NULL,
"createdDate" timestamp NOT NULL DEFAULT now(),
"updatedDate" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "PK_3c7cea7d087ac4b91764574cdbf" PRIMARY KEY (id)
);`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE custom_template`)
}
}
@@ -24,6 +24,7 @@ import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-Add
import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlow'
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const postgresMigrations = [
Init1693891895163,
@@ -51,5 +52,6 @@ export const postgresMigrations = [
AddTypeToChatFlow1716300000000,
AddVectorStoreConfigToDocStore1715861032479,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523
AddActionToChatMessage1721078251523,
AddCustomTemplate1725629836652
]
@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS "custom_template" (
"id" varchar PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"flowData" text NOT NULL,
"description" varchar,
"badge" varchar,
"framework" varchar,
"usecases" varchar,
"type" varchar,
"updatedDate" datetime NOT NULL DEFAULT (datetime('now')),
"createdDate" datetime NOT NULL DEFAULT (datetime('now')));`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "custom_template";`)
}
}
@@ -23,6 +23,7 @@ import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-Add
import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlow'
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const sqliteMigrations = [
Init1693835579790,
@@ -49,5 +50,6 @@ export const sqliteMigrations = [
AddTypeToChatFlow1716300000000,
AddVectorStoreConfigToDocStore1715861032479,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523
AddActionToChatMessage1721078251523,
AddCustomTemplate1725629836652
]
@@ -5,4 +5,12 @@ const router = express.Router()
// READ
router.get('/templates', marketplacesController.getAllTemplates)
router.post('/custom', marketplacesController.saveCustomTemplate)
// READ
router.get('/custom', marketplacesController.getAllCustomTemplates)
// DELETE
router.delete(['/', '/custom/:id'], marketplacesController.deleteCustomTemplate)
export default router
@@ -4,6 +4,11 @@ import { StatusCodes } from 'http-status-codes'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { IReactFlowEdge, IReactFlowNode } from '../../Interface'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { DeleteResult } from 'typeorm'
import { CustomTemplate } from '../../database/entities/CustomTemplate'
import chatflowsService from '../chatflows'
type ITemplate = {
badge: string
@@ -96,6 +101,148 @@ const getAllTemplates = async () => {
}
}
export default {
getAllTemplates
const deleteCustomTemplate = async (templateId: string): Promise<DeleteResult> => {
try {
const appServer = getRunningExpressApp()
return await appServer.AppDataSource.getRepository(CustomTemplate).delete({ id: templateId })
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: marketplacesService.deleteCustomTemplate - ${getErrorMessage(error)}`
)
}
}
const getAllCustomTemplates = async (): Promise<any> => {
try {
const appServer = getRunningExpressApp()
const templates: any[] = await appServer.AppDataSource.getRepository(CustomTemplate).find()
templates.map((template) => {
template.usecases = template.usecases ? JSON.parse(template.usecases) : ''
if (template.type === 'Tool') {
template.flowData = JSON.parse(template.flowData)
template.iconSrc = template.flowData.iconSrc
template.schema = template.flowData.schema
template.func = template.flowData.func
template.categories = []
template.flowData = undefined
} else {
template.categories = getCategories(JSON.parse(template.flowData))
}
if (!template.badge) {
template.badge = ''
}
if (!template.framework) {
template.framework = ''
}
})
return templates
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: marketplacesService.getAllCustomTemplates - ${getErrorMessage(error)}`
)
}
}
const saveCustomTemplate = async (body: any): Promise<any> => {
try {
const appServer = getRunningExpressApp()
let flowDataStr = ''
let derivedFramework = ''
const customTemplate = new CustomTemplate()
Object.assign(customTemplate, body)
if (body.chatflowId) {
const chatflow = await chatflowsService.getChatflowById(body.chatflowId)
const flowData = JSON.parse(chatflow.flowData)
const { framework, exportJson } = _generateExportFlowData(flowData)
flowDataStr = JSON.stringify(exportJson)
customTemplate.framework = framework
} else if (body.tool) {
const flowData = {
iconSrc: body.tool.iconSrc,
schema: body.tool.schema,
func: body.tool.func
}
customTemplate.framework = ''
customTemplate.type = 'Tool'
flowDataStr = JSON.stringify(flowData)
}
customTemplate.framework = derivedFramework
if (customTemplate.usecases) {
customTemplate.usecases = JSON.stringify(customTemplate.usecases)
}
const entity = appServer.AppDataSource.getRepository(CustomTemplate).create(customTemplate)
entity.flowData = flowDataStr
const flowTemplate = await appServer.AppDataSource.getRepository(CustomTemplate).save(entity)
return flowTemplate
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: marketplacesService.saveCustomTemplate - ${getErrorMessage(error)}`
)
}
}
const _generateExportFlowData = (flowData: any) => {
const nodes = flowData.nodes
const edges = flowData.edges
let framework = 'Langchain'
for (let i = 0; i < nodes.length; i += 1) {
nodes[i].selected = false
const node = nodes[i]
const newNodeData = {
id: node.data.id,
label: node.data.label,
version: node.data.version,
name: node.data.name,
type: node.data.type,
baseClasses: node.data.baseClasses,
tags: node.data.tags,
category: node.data.category,
description: node.data.description,
inputParams: node.data.inputParams,
inputAnchors: node.data.inputAnchors,
inputs: {},
outputAnchors: node.data.outputAnchors,
outputs: node.data.outputs,
selected: false
}
if (node.data.tags && node.data.tags.length) {
if (node.data.tags.includes('LlamaIndex')) {
framework = 'LlamaIndex'
}
}
// Remove password, file & folder
if (node.data.inputs && Object.keys(node.data.inputs).length) {
const nodeDataInputs: any = {}
for (const input in node.data.inputs) {
const inputParam = node.data.inputParams.find((inp: any) => inp.name === input)
if (inputParam && inputParam.type === 'password') continue
if (inputParam && inputParam.type === 'file') continue
if (inputParam && inputParam.type === 'folder') continue
nodeDataInputs[input] = node.data.inputs[input]
}
newNodeData.inputs = nodeDataInputs
}
nodes[i].data = newNodeData
}
const exportJson = {
nodes,
edges
}
return { exportJson, framework }
}
export default {
getAllTemplates,
getAllCustomTemplates,
saveCustomTemplate,
deleteCustomTemplate
}