mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +03:00
Initial push
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# Flowise - LangchainJS UI
|
||||
|
||||

|
||||
|
||||
Drag & drop UI to build your customized LLM flow using [LangchainJS](https://github.com/hwchase17/langchainjs)
|
||||
|
||||
## ⚡Quick Start
|
||||
|
||||
1. Install Flowise
|
||||
```bash
|
||||
npm install -g flowise
|
||||
```
|
||||
2. Start Flowise
|
||||
|
||||
```bash
|
||||
npx flowise start
|
||||
```
|
||||
|
||||
3. Open [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Coming Soon
|
||||
|
||||
## 💻 Cloud Hosted
|
||||
|
||||
Coming Soon
|
||||
|
||||
## 🌐 Self Host
|
||||
|
||||
Coming Soon
|
||||
|
||||
## 🙋 Support
|
||||
|
||||
Feel free to ask any questions, raise problems, and request new features in [discussion](https://github.com/FlowiseAI/Flowise/discussions)
|
||||
|
||||
## 🙌 Contributing
|
||||
|
||||
See [contributing guide](https://github.com/FlowiseAI/Flowise/blob/master/CONTRIBUTING.md). Reach out to us at [Discord](https://discord.gg/GWcGczPk) if you have any questions or issues.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: '../../babel.config.js'
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core')
|
||||
|
||||
const path = require('path')
|
||||
const project = path.join(__dirname, '..', 'tsconfig.json')
|
||||
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
require('ts-node').register({ project })
|
||||
|
||||
// In dev mode, always show stack traces
|
||||
oclif.settings.debug = true
|
||||
|
||||
// Start the CLI
|
||||
oclif.run().then(oclif.flush).catch(oclif.Errors.handle)
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core')
|
||||
|
||||
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ignore": ["**/*.spec.ts", ".git", "node_modules"],
|
||||
"watch": ["commands", "index.ts", "src"],
|
||||
"exec": "yarn oclif-dev",
|
||||
"ext": "ts"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "flowise",
|
||||
"version": "1.0.0",
|
||||
"description": "Flowiseai Server",
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"flowise": "./bin/run"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist",
|
||||
"npm-shrinkwrap.json",
|
||||
"oclif.manifest.json",
|
||||
"oauth2.html",
|
||||
".env"
|
||||
],
|
||||
"oclif": {
|
||||
"bin": "flowise",
|
||||
"commands": "./dist/commands"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "run-script-os",
|
||||
"start:windows": "cd bin && run start",
|
||||
"start:default": "cd bin && ./run start",
|
||||
"dev": "concurrently \"yarn watch\" \"nodemon\"",
|
||||
"oclif-dev": "run-script-os",
|
||||
"oclif-dev:windows": "cd bin && dev start",
|
||||
"oclif-dev:default": "cd bin && ./dev start",
|
||||
"postpack": "shx rm -f oclif.manifest.json",
|
||||
"prepack": "yarn build && oclif manifest && oclif readme",
|
||||
"typeorm": "typeorm-ts-node-commonjs",
|
||||
"watch": "tsc --watch",
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"keywords": [],
|
||||
"homepage": "https://flowiseai.com",
|
||||
"author": {
|
||||
"name": "Henry Heng",
|
||||
"email": "henryheng@flowiseai.com"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.15.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"@oclif/core": "^1.13.10",
|
||||
"axios": "^0.27.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"flowise-components": "*",
|
||||
"flowise-ui": "*",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sqlite3": "^5.1.6",
|
||||
"typeorm": "^0.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"concurrently": "^7.1.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"oclif": "^3",
|
||||
"run-script-os": "^1.1.6",
|
||||
"shx": "^0.3.3",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'reflect-metadata'
|
||||
import path from 'path'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { ChatFlow } from './entity/ChatFlow'
|
||||
import { ChatMessage } from './entity/ChatMessage'
|
||||
import { getUserHome } from './utils'
|
||||
|
||||
let appDataSource: DataSource
|
||||
|
||||
export const init = async (): Promise<void> => {
|
||||
const homePath = path.join(getUserHome(), '.flowise')
|
||||
|
||||
appDataSource = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: path.resolve(homePath, 'database.sqlite'),
|
||||
synchronize: true,
|
||||
entities: [ChatFlow, ChatMessage],
|
||||
migrations: []
|
||||
})
|
||||
}
|
||||
|
||||
export function getDataSource(): DataSource {
|
||||
if (appDataSource === undefined) {
|
||||
init()
|
||||
}
|
||||
return appDataSource
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { INode, INodeData } from 'flowise-components'
|
||||
|
||||
export type MessageType = 'apiMessage' | 'userMessage'
|
||||
|
||||
/**
|
||||
* Databases
|
||||
*/
|
||||
export interface IChatFlow {
|
||||
id: string
|
||||
name: string
|
||||
flowData: string
|
||||
deployed: boolean
|
||||
updatedDate: Date
|
||||
createdDate: Date
|
||||
}
|
||||
|
||||
export interface IChatMessage {
|
||||
id: string
|
||||
role: MessageType
|
||||
content: string
|
||||
chatflowid: string
|
||||
createdDate: Date
|
||||
}
|
||||
|
||||
export interface IComponentNodesPool {
|
||||
[key: string]: INode
|
||||
}
|
||||
|
||||
export interface IVariableDict {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface INodeDependencies {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export interface INodeDirectedGraph {
|
||||
[key: string]: string[]
|
||||
}
|
||||
|
||||
export interface IReactFlowNode {
|
||||
id: string
|
||||
position: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
type: string
|
||||
data: INodeData
|
||||
positionAbsolute: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
z: number
|
||||
handleBounds: {
|
||||
source: any
|
||||
target: any
|
||||
}
|
||||
width: number
|
||||
height: number
|
||||
selected: boolean
|
||||
dragging: boolean
|
||||
}
|
||||
|
||||
export interface IReactFlowEdge {
|
||||
source: string
|
||||
sourceHandle: string
|
||||
target: string
|
||||
targetHandle: string
|
||||
type: string
|
||||
id: string
|
||||
data: {
|
||||
label: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface IReactFlowObject {
|
||||
nodes: IReactFlowNode[]
|
||||
edges: IReactFlowEdge[]
|
||||
viewport: {
|
||||
x: number
|
||||
y: number
|
||||
zoom: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface IExploredNode {
|
||||
[key: string]: {
|
||||
remainingLoop: number
|
||||
lastSeenDepth: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface INodeQueue {
|
||||
nodeId: string
|
||||
depth: number
|
||||
}
|
||||
|
||||
export interface IncomingInput {
|
||||
question: string
|
||||
history: string[]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { IComponentNodesPool } from './Interface'
|
||||
|
||||
import path from 'path'
|
||||
import { Dirent } from 'fs'
|
||||
import { getNodeModulesPackagePath } from './utils'
|
||||
import { promises } from 'fs'
|
||||
|
||||
export class NodesPool {
|
||||
componentNodes: IComponentNodesPool = {}
|
||||
|
||||
/**
|
||||
* Initialize to get all nodes
|
||||
*/
|
||||
async initialize() {
|
||||
const packagePath = getNodeModulesPackagePath('flowise-components')
|
||||
const nodesPath = path.join(packagePath, 'dist', 'nodes')
|
||||
const nodeFiles = await this.getFiles(nodesPath)
|
||||
return Promise.all(
|
||||
nodeFiles.map(async (file) => {
|
||||
if (file.endsWith('.js')) {
|
||||
const nodeModule = await require(file)
|
||||
try {
|
||||
const newNodeInstance = new nodeModule.nodeClass()
|
||||
newNodeInstance.filePath = file
|
||||
|
||||
const baseClasses = await newNodeInstance.getBaseClasses!.call(newNodeInstance)
|
||||
newNodeInstance.baseClasses = baseClasses
|
||||
|
||||
this.componentNodes[newNodeInstance.name] = newNodeInstance
|
||||
|
||||
// Replace file icon with absolute path
|
||||
if (
|
||||
newNodeInstance.icon &&
|
||||
(newNodeInstance.icon.endsWith('.svg') ||
|
||||
newNodeInstance.icon.endsWith('.png') ||
|
||||
newNodeInstance.icon.endsWith('.jpg'))
|
||||
) {
|
||||
const filePath = file.replace(/\\/g, '/').split('/')
|
||||
filePath.pop()
|
||||
const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}`
|
||||
this.componentNodes[newNodeInstance.name].icon = nodeIconAbsolutePath
|
||||
}
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function to get node files
|
||||
* @param {string} dir
|
||||
* @returns {string[]}
|
||||
*/
|
||||
async getFiles(dir: string): Promise<string[]> {
|
||||
const dirents = await promises.readdir(dir, { withFileTypes: true })
|
||||
const files = await Promise.all(
|
||||
dirents.map((dirent: Dirent) => {
|
||||
const res = path.resolve(dir, dirent.name)
|
||||
return dirent.isDirectory() ? this.getFiles(res) : res
|
||||
})
|
||||
)
|
||||
return Array.prototype.concat(...files)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Command, Flags } from '@oclif/core'
|
||||
import path from 'path'
|
||||
import * as Server from '../index'
|
||||
import * as DataSource from '../DataSource'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, '..', '..', '.env') })
|
||||
|
||||
enum EXIT_CODE {
|
||||
SUCCESS = 0,
|
||||
FAILED = 1
|
||||
}
|
||||
let processExitCode = EXIT_CODE.SUCCESS
|
||||
|
||||
export default class Start extends Command {
|
||||
static flags = {
|
||||
mongourl: Flags.string()
|
||||
}
|
||||
|
||||
static args = []
|
||||
|
||||
async stopProcess() {
|
||||
console.info('Shutting down Flowise...')
|
||||
try {
|
||||
// Shut down the app after timeout if it ever stuck removing pools
|
||||
setTimeout(() => {
|
||||
console.info('Flowise was forced to shut down after 30 secs')
|
||||
process.exit(processExitCode)
|
||||
}, 30000)
|
||||
|
||||
// Removing pools
|
||||
const serverApp = Server.getInstance()
|
||||
if (serverApp) await serverApp.stopApp()
|
||||
} catch (error) {
|
||||
console.error('There was an error shutting down Flowise...', error)
|
||||
}
|
||||
process.exit(processExitCode)
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
process.on('SIGTERM', this.stopProcess)
|
||||
process.on('SIGINT', this.stopProcess)
|
||||
|
||||
// Prevent throw new Error from crashing the app
|
||||
// TODO: Get rid of this and send proper error message to ui
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('uncaughtException: ', err)
|
||||
})
|
||||
|
||||
const { flags } = await this.parse(Start)
|
||||
if (flags.mongourl) process.env.MONGO_URL = flags.mongourl
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
this.log('Starting Flowise...')
|
||||
await DataSource.init()
|
||||
await Server.start()
|
||||
} catch (error) {
|
||||
console.error('There was an error starting Flowise...', error)
|
||||
processExitCode = EXIT_CODE.FAILED
|
||||
// @ts-ignore
|
||||
process.emit('SIGINT')
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable */
|
||||
import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { IChatFlow } from '../Interface'
|
||||
|
||||
@Entity()
|
||||
export class ChatFlow implements IChatFlow {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column()
|
||||
flowData: string
|
||||
|
||||
@Column()
|
||||
deployed: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedDate: Date
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm'
|
||||
import { IChatMessage, MessageType } from '../Interface'
|
||||
|
||||
@Entity()
|
||||
export class ChatMessage implements IChatMessage {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
role: MessageType
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
chatflowid: string
|
||||
|
||||
@Column()
|
||||
content: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate: Date
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
export const workflow1 = {
|
||||
nodes: [
|
||||
{
|
||||
width: 200,
|
||||
height: 66,
|
||||
id: 'promptTemplate_0',
|
||||
position: {
|
||||
x: 295.0571878493141,
|
||||
y: 108.66221078850214
|
||||
},
|
||||
type: 'customNode',
|
||||
data: {
|
||||
label: 'Prompt Template',
|
||||
name: 'promptTemplate',
|
||||
type: 'PromptTemplate',
|
||||
inputAnchors: [],
|
||||
outputAnchors: [
|
||||
{
|
||||
id: 'promptTemplate_0-output-0'
|
||||
}
|
||||
],
|
||||
selected: false,
|
||||
inputs: {
|
||||
template: 'What is a good name for a company that makes {product}?',
|
||||
inputVariables: '["product"]'
|
||||
}
|
||||
},
|
||||
selected: false,
|
||||
positionAbsolute: {
|
||||
x: 295.0571878493141,
|
||||
y: 108.66221078850214
|
||||
},
|
||||
dragging: false
|
||||
},
|
||||
{
|
||||
width: 200,
|
||||
height: 66,
|
||||
id: 'openAI_0',
|
||||
position: {
|
||||
x: 774,
|
||||
y: 97.75
|
||||
},
|
||||
type: 'customNode',
|
||||
data: {
|
||||
label: 'OpenAI',
|
||||
name: 'openAI',
|
||||
type: 'OpenAI',
|
||||
inputAnchors: [],
|
||||
outputAnchors: [
|
||||
{
|
||||
id: 'openAI_0-output-0'
|
||||
}
|
||||
],
|
||||
selected: false,
|
||||
inputs: {
|
||||
modelName: 'text-davinci-003',
|
||||
temperature: '0.7',
|
||||
openAIApiKey: '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'
|
||||
},
|
||||
calls: {
|
||||
prompt: 'Hi, how are you?'
|
||||
}
|
||||
},
|
||||
selected: false,
|
||||
positionAbsolute: {
|
||||
x: 774,
|
||||
y: 97.75
|
||||
},
|
||||
dragging: false
|
||||
},
|
||||
{
|
||||
width: 200,
|
||||
height: 66,
|
||||
id: 'llmChain_0',
|
||||
position: {
|
||||
x: 1034.233162523021,
|
||||
y: 97.59868104260748
|
||||
},
|
||||
type: 'customNode',
|
||||
data: {
|
||||
label: 'LLM Chain',
|
||||
name: 'llmChain',
|
||||
type: 'LLMChain',
|
||||
inputAnchors: [
|
||||
{
|
||||
id: 'llmChain_0-input-0'
|
||||
}
|
||||
],
|
||||
outputAnchors: [
|
||||
{
|
||||
id: 'llmChain_0-output-0'
|
||||
}
|
||||
],
|
||||
selected: false,
|
||||
inputs: {
|
||||
llm: '{{openAI_0.data.instance}}',
|
||||
prompt: '{{promptTemplate_0.data.instance}}'
|
||||
},
|
||||
calls: {
|
||||
variable: '{"product":"colorful socks"}'
|
||||
}
|
||||
},
|
||||
selected: false,
|
||||
positionAbsolute: {
|
||||
x: 1034.233162523021,
|
||||
y: 97.59868104260748
|
||||
},
|
||||
dragging: false
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'nodeJS_0',
|
||||
sourceHandle: 'nodeJS_0-output-0',
|
||||
target: 'nodeJS_1',
|
||||
targetHandle: 'nodeJS_1-input-0',
|
||||
type: 'buttonedge',
|
||||
id: 'nodeJS_0-nodeJS_0-output-0-nodeJS_1-nodeJS_1-input-0',
|
||||
data: {
|
||||
label: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
source: 'webhook_0',
|
||||
sourceHandle: 'webhook_0-output-0',
|
||||
target: 'wait_0',
|
||||
targetHandle: 'wait_0-input-0',
|
||||
type: 'buttonedge',
|
||||
id: 'webhook_0-webhook_0-output-0-wait_0-wait_0-input-0',
|
||||
data: {
|
||||
label: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
source: 'wait_0',
|
||||
sourceHandle: 'wait_0-output-0',
|
||||
target: 'nodeJS_0',
|
||||
targetHandle: 'nodeJS_0-input-0',
|
||||
type: 'buttonedge',
|
||||
id: 'wait_0-wait_0-output-0-nodeJS_0-nodeJS_0-input-0',
|
||||
data: {
|
||||
label: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import path from 'path'
|
||||
import cors from 'cors'
|
||||
import http from 'http'
|
||||
|
||||
import { IChatFlow, IComponentNodesPool, IncomingInput, IReactFlowNode, IReactFlowObject } from './Interface'
|
||||
import { getNodeModulesPackagePath, getStartingNode, buildLangchain, getEndingNode, constructGraphs } from './utils'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getDataSource } from './DataSource'
|
||||
import { NodesPool } from './NodesPool'
|
||||
import { ChatFlow } from './entity/ChatFlow'
|
||||
import { ChatMessage } from './entity/ChatMessage'
|
||||
|
||||
export class App {
|
||||
app: express.Application
|
||||
componentNodes: IComponentNodesPool = {}
|
||||
AppDataSource = getDataSource()
|
||||
|
||||
constructor() {
|
||||
this.app = express()
|
||||
}
|
||||
|
||||
async initDatabase() {
|
||||
// Initialize database
|
||||
this.AppDataSource.initialize()
|
||||
.then(async () => {
|
||||
console.info('📦[server]: Data Source has been initialized!')
|
||||
|
||||
// Initialize node instances
|
||||
const nodesPool = new NodesPool()
|
||||
await nodesPool.initialize()
|
||||
this.componentNodes = nodesPool.componentNodes
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌[server]: Error during Data Source initialization:', err)
|
||||
})
|
||||
}
|
||||
|
||||
async config() {
|
||||
// Limit is needed to allow sending/receiving base64 encoded string
|
||||
this.app.use(express.json({ limit: '50mb' }))
|
||||
this.app.use(express.urlencoded({ limit: '50mb', extended: true }))
|
||||
|
||||
// Allow access from ui when yarn run dev
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.app.use(cors({ credentials: true, origin: 'http://localhost:8080' }))
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Nodes
|
||||
// ----------------------------------------
|
||||
|
||||
// Get all component nodes
|
||||
this.app.get('/api/v1/nodes', (req: Request, res: Response) => {
|
||||
const returnData = []
|
||||
for (const nodeName in this.componentNodes) {
|
||||
const clonedNode = cloneDeep(this.componentNodes[nodeName])
|
||||
returnData.push(clonedNode)
|
||||
}
|
||||
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.componentNodes, req.params.name)) {
|
||||
return res.json(this.componentNodes[req.params.name])
|
||||
} else {
|
||||
throw new Error(`Node ${req.params.name} not found`)
|
||||
}
|
||||
})
|
||||
|
||||
// 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.componentNodes, req.params.name)) {
|
||||
const nodeInstance = this.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`)
|
||||
}
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// Chatflows
|
||||
// ----------------------------------------
|
||||
|
||||
// Get all chatflows
|
||||
this.app.get('/api/v1/chatflows', async (req: Request, res: Response) => {
|
||||
const chatflows: IChatFlow[] = await this.AppDataSource.getRepository(ChatFlow).find()
|
||||
return res.json(chatflows)
|
||||
})
|
||||
|
||||
// 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`)
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
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)
|
||||
|
||||
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
|
||||
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
|
||||
|
||||
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 })
|
||||
return res.json(results)
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// ChatMessage
|
||||
// ----------------------------------------
|
||||
|
||||
// Get all chatmessages from chatflowid
|
||||
this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
|
||||
const chatmessages = await this.AppDataSource.getRepository(ChatMessage).findBy({
|
||||
chatflowid: req.params.id
|
||||
})
|
||||
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 newChatMessage = new ChatMessage()
|
||||
Object.assign(newChatMessage, body)
|
||||
|
||||
const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage)
|
||||
const results = await this.AppDataSource.getRepository(ChatMessage).save(chatmessage)
|
||||
|
||||
return res.json(results)
|
||||
})
|
||||
|
||||
// Delete all chatmessages from chatflowid
|
||||
this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
|
||||
const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatflowid: req.params.id })
|
||||
return res.json(results)
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// Prediction
|
||||
// ----------------------------------------
|
||||
|
||||
// Send input message and get prediction result
|
||||
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const incomingInput: IncomingInput = req.body
|
||||
|
||||
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 { graph, nodeDependencies } = constructGraphs(parsedFlowData.nodes, parsedFlowData.edges)
|
||||
|
||||
const startingNodeIds = getStartingNode(nodeDependencies)
|
||||
const endingNodeId = getEndingNode(nodeDependencies, graph)
|
||||
if (!endingNodeId) return res.status(500).send(`Ending node must be either Chain or Agent`)
|
||||
|
||||
const reactFlowNodes = await buildLangchain(startingNodeIds, parsedFlowData.nodes, graph, this.componentNodes)
|
||||
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
|
||||
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
|
||||
|
||||
const nodeInstanceFilePath = this.componentNodes[nodeToExecute.data.name].filePath as string
|
||||
const nodeModule = await import(nodeInstanceFilePath)
|
||||
const nodeInstance = new nodeModule.nodeClass()
|
||||
|
||||
const result = await nodeInstance.run(nodeToExecute.data, incomingInput.question)
|
||||
|
||||
return res.json(result)
|
||||
} catch (e: any) {
|
||||
return res.status(500).send(e.message)
|
||||
}
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// Serve UI static
|
||||
// ----------------------------------------
|
||||
|
||||
const packagePath = getNodeModulesPackagePath('flowise-ui')
|
||||
const uiBuildPath = path.join(packagePath, 'build')
|
||||
const uiHtmlPath = path.join(packagePath, 'build', 'index.html')
|
||||
|
||||
this.app.use('/', express.static(uiBuildPath))
|
||||
|
||||
// All other requests not handled will return React app
|
||||
this.app.use((req, res) => {
|
||||
res.sendFile(uiHtmlPath)
|
||||
})
|
||||
}
|
||||
|
||||
async stopApp() {
|
||||
try {
|
||||
const removePromises: any[] = []
|
||||
await Promise.all(removePromises)
|
||||
} catch (e) {
|
||||
console.error(`❌[server]: Flowise Server shut down error: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let serverApp: App | undefined
|
||||
|
||||
export async function start(): Promise<void> {
|
||||
serverApp = new App()
|
||||
|
||||
const port = parseInt(process.env.PORT || '', 10) || 3000
|
||||
const server = http.createServer(serverApp.app)
|
||||
|
||||
await serverApp.initDatabase()
|
||||
await serverApp.config()
|
||||
|
||||
server.listen(port, () => {
|
||||
console.info(`⚡️[server]: Flowise Server is listening at ${port}`)
|
||||
})
|
||||
}
|
||||
|
||||
export function getInstance(): App | undefined {
|
||||
return serverApp
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import {
|
||||
IComponentNodesPool,
|
||||
IExploredNode,
|
||||
INodeDependencies,
|
||||
INodeDirectedGraph,
|
||||
INodeQueue,
|
||||
IReactFlowEdge,
|
||||
IReactFlowNode
|
||||
} from '../Interface'
|
||||
import { cloneDeep, get } from 'lodash'
|
||||
import { ICommonObject, INodeData } from 'flowise-components'
|
||||
|
||||
/**
|
||||
* Returns the home folder path of the user if
|
||||
* none can be found it falls back to the current
|
||||
* working directory
|
||||
*
|
||||
*/
|
||||
export const getUserHome = (): string => {
|
||||
let variableName = 'HOME'
|
||||
if (process.platform === 'win32') {
|
||||
variableName = 'USERPROFILE'
|
||||
}
|
||||
|
||||
if (process.env[variableName] === undefined) {
|
||||
// If for some reason the variable does not exist
|
||||
// fall back to current folder
|
||||
return process.cwd()
|
||||
}
|
||||
return process.env[variableName] as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of node modules package
|
||||
* @param {string} packageName
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getNodeModulesPackagePath = (packageName: string): string => {
|
||||
const checkPaths = [
|
||||
path.join(__dirname, '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', '..', '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', '..', '..', '..', 'node_modules', packageName)
|
||||
]
|
||||
for (const checkPath of checkPaths) {
|
||||
if (fs.existsSync(checkPath)) {
|
||||
return checkPath
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct directed graph and node dependencies score
|
||||
* @param {IReactFlowNode[]} reactFlowNodes
|
||||
* @param {IReactFlowEdge[]} reactFlowEdges
|
||||
*/
|
||||
export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges: IReactFlowEdge[]) => {
|
||||
const nodeDependencies = {} as INodeDependencies
|
||||
const graph = {} as INodeDirectedGraph
|
||||
|
||||
for (let i = 0; i < reactFlowNodes.length; i += 1) {
|
||||
const nodeId = reactFlowNodes[i].id
|
||||
nodeDependencies[nodeId] = 0
|
||||
graph[nodeId] = []
|
||||
}
|
||||
|
||||
for (let i = 0; i < reactFlowEdges.length; i += 1) {
|
||||
const source = reactFlowEdges[i].source
|
||||
const target = reactFlowEdges[i].target
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(graph, source)) {
|
||||
graph[source].push(target)
|
||||
} else {
|
||||
graph[source] = [target]
|
||||
}
|
||||
nodeDependencies[target] += 1
|
||||
}
|
||||
|
||||
return { graph, nodeDependencies }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starting node and check if flow is valid
|
||||
* @param {INodeDependencies} nodeDependencies
|
||||
*/
|
||||
export const getStartingNode = (nodeDependencies: INodeDependencies) => {
|
||||
// Find starting node
|
||||
const startingNodeIds = [] as string[]
|
||||
Object.keys(nodeDependencies).forEach((nodeId) => {
|
||||
if (nodeDependencies[nodeId] === 0) {
|
||||
startingNodeIds.push(nodeId)
|
||||
}
|
||||
})
|
||||
return startingNodeIds
|
||||
}
|
||||
|
||||
export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => {
|
||||
// Find starting node
|
||||
let endingNodeId = ''
|
||||
Object.keys(graph).forEach((nodeId) => {
|
||||
if (!graph[nodeId].length && nodeDependencies[nodeId] > 0) {
|
||||
endingNodeId = nodeId
|
||||
}
|
||||
})
|
||||
return endingNodeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Build langchain from start to end
|
||||
* @param {string} startingNodeId
|
||||
* @param {IReactFlowNode[]} reactFlowNodes
|
||||
* @param {IReactFlowEdge[]} reactFlowEdges
|
||||
* @param {INodeDirectedGraph} graph
|
||||
* @param {IComponentNodesPool} componentNodes
|
||||
* @param {string} clientId
|
||||
* @param {any} io
|
||||
*/
|
||||
export const buildLangchain = async (
|
||||
startingNodeIds: string[],
|
||||
reactFlowNodes: IReactFlowNode[],
|
||||
graph: INodeDirectedGraph,
|
||||
componentNodes: IComponentNodesPool
|
||||
) => {
|
||||
const flowNodes = cloneDeep(reactFlowNodes)
|
||||
|
||||
// Create a Queue and add our initial node in it
|
||||
const nodeQueue = [] as INodeQueue[]
|
||||
const exploredNode = {} as IExploredNode
|
||||
|
||||
// In the case of infinite loop, only max 3 loops will be executed
|
||||
const maxLoop = 3
|
||||
|
||||
for (let i = 0; i < startingNodeIds.length; i += 1) {
|
||||
nodeQueue.push({ nodeId: startingNodeIds[i], depth: 0 })
|
||||
exploredNode[startingNodeIds[i]] = { remainingLoop: maxLoop, lastSeenDepth: 0 }
|
||||
}
|
||||
|
||||
while (nodeQueue.length) {
|
||||
const { nodeId, depth } = nodeQueue.shift() as INodeQueue
|
||||
|
||||
const reactFlowNode = flowNodes.find((nd) => nd.id === nodeId)
|
||||
const nodeIndex = flowNodes.findIndex((nd) => nd.id === nodeId)
|
||||
if (!reactFlowNode || reactFlowNode === undefined || nodeIndex < 0) continue
|
||||
|
||||
try {
|
||||
const nodeInstanceFilePath = componentNodes[reactFlowNode.data.name].filePath as string
|
||||
const nodeModule = await import(nodeInstanceFilePath)
|
||||
const newNodeInstance = new nodeModule.nodeClass()
|
||||
|
||||
const reactFlowNodeData: INodeData = resolveVariables(reactFlowNode.data, flowNodes)
|
||||
|
||||
flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData)
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
throw new Error(e)
|
||||
}
|
||||
|
||||
const neighbourNodeIds = graph[nodeId]
|
||||
const nextDepth = depth + 1
|
||||
|
||||
for (let i = 0; i < neighbourNodeIds.length; i += 1) {
|
||||
const neighNodeId = neighbourNodeIds[i]
|
||||
|
||||
// If nodeId has been seen, cycle detected
|
||||
if (Object.prototype.hasOwnProperty.call(exploredNode, neighNodeId)) {
|
||||
const { remainingLoop, lastSeenDepth } = exploredNode[neighNodeId]
|
||||
|
||||
if (lastSeenDepth === nextDepth) continue
|
||||
|
||||
if (remainingLoop === 0) {
|
||||
break
|
||||
}
|
||||
const remainingLoopMinusOne = remainingLoop - 1
|
||||
exploredNode[neighNodeId] = { remainingLoop: remainingLoopMinusOne, lastSeenDepth: nextDepth }
|
||||
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
|
||||
} else {
|
||||
exploredNode[neighNodeId] = { remainingLoop: maxLoop, lastSeenDepth: nextDepth }
|
||||
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
|
||||
}
|
||||
}
|
||||
}
|
||||
return flowNodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variable value from outputResponses.output
|
||||
* @param {string} paramValue
|
||||
* @param {IReactFlowNode[]} reactFlowNodes
|
||||
* @param {string} key
|
||||
* @param {number} loopIndex
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getVariableValue = (paramValue: string, reactFlowNodes: IReactFlowNode[]) => {
|
||||
let returnVal = paramValue
|
||||
const variableStack = []
|
||||
let startIdx = 0
|
||||
const endIdx = returnVal.length - 1
|
||||
|
||||
while (startIdx < endIdx) {
|
||||
const substr = returnVal.substring(startIdx, startIdx + 2)
|
||||
|
||||
// Store the opening double curly bracket
|
||||
if (substr === '{{') {
|
||||
variableStack.push({ substr, startIdx: startIdx + 2 })
|
||||
}
|
||||
|
||||
// Found the complete variable
|
||||
if (substr === '}}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{{') {
|
||||
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
|
||||
const variableEndIdx = startIdx
|
||||
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
|
||||
|
||||
// Split by first occurence of '.' to get just nodeId
|
||||
const [variableNodeId, _] = variableFullPath.split('.')
|
||||
const executedNode = reactFlowNodes.find((nd) => nd.id === variableNodeId)
|
||||
if (executedNode) {
|
||||
const variableInstance = get(executedNode.data, 'instance')
|
||||
returnVal = variableInstance
|
||||
}
|
||||
variableStack.pop()
|
||||
}
|
||||
startIdx += 1
|
||||
}
|
||||
return returnVal
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop through each inputs and resolve variable if neccessary
|
||||
* @param {INodeData} reactFlowNodeData
|
||||
* @param {IReactFlowNode[]} reactFlowNodes
|
||||
* @returns {INodeData}
|
||||
*/
|
||||
export const resolveVariables = (reactFlowNodeData: INodeData, reactFlowNodes: IReactFlowNode[]): INodeData => {
|
||||
const flowNodeData = cloneDeep(reactFlowNodeData)
|
||||
const types = 'inputs'
|
||||
|
||||
const getParamValues = (paramsObj: ICommonObject) => {
|
||||
for (const key in paramsObj) {
|
||||
const paramValue: string = paramsObj[key]
|
||||
|
||||
if (Array.isArray(paramValue)) {
|
||||
const resolvedInstances = []
|
||||
for (const param of paramValue) {
|
||||
const resolvedInstance = getVariableValue(param, reactFlowNodes)
|
||||
resolvedInstances.push(resolvedInstance)
|
||||
}
|
||||
paramsObj[key] = resolvedInstances
|
||||
} else {
|
||||
const resolvedInstance = getVariableValue(paramValue, reactFlowNodes)
|
||||
paramsObj[key] = resolvedInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const paramsObj = (flowNodeData as any)[types]
|
||||
|
||||
getParamValues(paramsObj)
|
||||
|
||||
return flowNodeData
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2017"],
|
||||
"target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
|
||||
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"sourceMap": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user