Initial push

This commit is contained in:
Henry
2023-04-06 22:17:34 +01:00
commit 05c86ff9c5
162 changed files with 9112 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
<!-- markdownlint-disable MD030 -->
# Flowise - LangchainJS UI
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
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).
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: '../../babel.config.js'
}
+17
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
@echo off
node "%~dp0\dev" %*
+5
View File
@@ -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'))
+3
View File
@@ -0,0 +1,3 @@
@echo off
node "%~dp0\run" %*
+6
View File
@@ -0,0 +1,6 @@
{
"ignore": ["**/*.spec.ts", ".git", "node_modules"],
"watch": ["commands", "index.ts", "src"],
"exec": "yarn oclif-dev",
"ext": "ts"
}
+70
View File
@@ -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"
}
}
+27
View File
@@ -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
}
+101
View File
@@ -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[]
}
+66
View File
@@ -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)
}
}
+66
View File
@@ -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')
}
})()
}
}
+24
View File
@@ -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
}
+22
View File
@@ -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
}
+146
View File
@@ -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: ''
}
}
]
}
+261
View File
@@ -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
}
+264
View File
@@ -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
}
+18
View File
@@ -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"]
}