Add more nodes for agents, loaders

This commit is contained in:
Henry
2023-04-10 13:56:44 +01:00
parent 05c86ff9c5
commit 58e06718d1
57 changed files with 1584 additions and 89 deletions
@@ -0,0 +1,64 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class ConversationalAgent_Agents implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Conversational Agent'
this.name = 'conversationalAgent'
this.type = 'AgentExecutor'
this.category = 'Agents'
this.icon = 'agent.svg'
this.description = 'Conversational agent for a chat model. It will utilize chat specific prompts'
this.inputs = [
{
label: 'Allowed Tools',
name: 'tools',
type: 'Tool',
list: true
},
{
label: 'Chat Model',
name: 'model',
type: 'BaseChatModel'
},
{
label: 'Memory',
name: 'memory',
type: 'BaseChatMemory'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['AgentExecutor']
}
async init(nodeData: INodeData): Promise<any> {
const { initializeAgentExecutor } = await import('langchain/agents')
const model = nodeData.inputs?.model
const tools = nodeData.inputs?.tools
const memory = nodeData.inputs?.memory
const executor = await initializeAgentExecutor(tools, model, 'chat-conversational-react-description', true)
executor.memory = memory
return executor
}
async run(nodeData: INodeData, input: string): Promise<string> {
const executor = nodeData.instance
const result = await executor.call({ input })
return result?.output
}
}
module.exports = { nodeClass: ConversationalAgent_Agents }
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-robot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M7 7h10a2 2 0 0 1 2 2v1l1 1v3l-1 1v3a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-3l-1 -1v-3l1 -1v-1a2 2 0 0 1 2 -2z"></path>
<path d="M10 16h4"></path>
<circle cx="8.5" cy="11.5" r=".5" fill="currentColor"></circle>
<circle cx="15.5" cy="11.5" r=".5" fill="currentColor"></circle>
<path d="M9 7l-1 -4"></path>
<path d="M15 7l1 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 650 B

@@ -0,0 +1,58 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class MRLKAgentChat_Agents implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'MRLK Agent for Chat Models'
this.name = 'mrlkAgentChat'
this.type = 'AgentExecutor'
this.category = 'Agents'
this.icon = 'agent.svg'
this.description = 'Agent that uses the ReAct Framework to decide what action to take, optimized to be used with Chat Models'
this.inputs = [
{
label: 'Allowed Tools',
name: 'tools',
type: 'Tool',
list: true
},
{
label: 'Chat Model',
name: 'model',
type: 'BaseChatModel'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['AgentExecutor']
}
async init(nodeData: INodeData): Promise<any> {
const { initializeAgentExecutor } = await import('langchain/agents')
const model = nodeData.inputs?.model
const tools = nodeData.inputs?.tools
const executor = await initializeAgentExecutor(tools, model, 'chat-zero-shot-react-description', true)
return executor
}
async run(nodeData: INodeData, input: string): Promise<string> {
const executor = nodeData.instance
const result = await executor.call({ input })
return result?.output
}
}
module.exports = { nodeClass: MRLKAgentChat_Agents }
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-robot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M7 7h10a2 2 0 0 1 2 2v1l1 1v3l-1 1v3a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-3l-1 -1v-3l1 -1v-1a2 2 0 0 1 2 -2z"></path>
<path d="M10 16h4"></path>
<circle cx="8.5" cy="11.5" r=".5" fill="currentColor"></circle>
<circle cx="15.5" cy="11.5" r=".5" fill="currentColor"></circle>
<path d="M9 7l-1 -4"></path>
<path d="M15 7l1 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 650 B

@@ -1,6 +1,6 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class MRLKAgentLLM implements INode {
class MRLKAgentLLM_Agents implements INode {
label: string
name: string
description: string
@@ -55,4 +55,4 @@ class MRLKAgentLLM implements INode {
}
}
module.exports = { nodeClass: MRLKAgentLLM }
module.exports = { nodeClass: MRLKAgentLLM_Agents }
@@ -0,0 +1,74 @@
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class ConversationalRetrievalQAChain_Chains implements INode {
label: string
name: string
type: string
icon: string
category: string
baseClasses: string[]
description: string
inputs: INodeParams[]
constructor() {
this.label = 'Conversational Retrieval QA Chain'
this.name = 'conversationalRetrievalQAChain'
this.type = 'ConversationalRetrievalQAChain'
this.icon = 'chain.svg'
this.category = 'Chains'
this.description = 'Document QA - built on RetrievalQAChain to provide a chat history component'
this.inputs = [
{
label: 'LLM',
name: 'llm',
type: 'BaseLanguageModel'
},
{
label: 'Vector Store Retriever',
name: 'vectorStoreRetriever',
type: 'BaseRetriever'
}
]
}
async getBaseClasses(): Promise<string[]> {
const { ConversationalRetrievalQAChain } = await import('langchain/chains')
return getBaseClasses(ConversationalRetrievalQAChain)
}
async init(nodeData: INodeData): Promise<any> {
const { ConversationalRetrievalQAChain } = await import('langchain/chains')
const llm = nodeData.inputs?.llm
const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever
const chain = ConversationalRetrievalQAChain.fromLLM(llm, vectorStoreRetriever)
return chain
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const chain = nodeData.instance
let chatHistory = ''
if (options && options.chatHistory) {
const histories: IMessage[] = options.chatHistory
chatHistory = histories
.map((item) => {
return item.message
})
.join('')
}
const obj = {
question: input,
chat_history: chatHistory ? chatHistory : []
}
const res = await chain.call(obj)
return res?.text
}
}
module.exports = { nodeClass: ConversationalRetrievalQAChain_Chains }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dna" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"></path>
<path d="M9.172 20.485a4 4 0 1 0 -5.657 -5.657"></path>
<path d="M14.828 3.515a4 4 0 0 0 5.657 5.657"></path>
</svg>

After

Width:  |  Height:  |  Size: 489 B

@@ -28,6 +28,17 @@ class LLMChain_Chains implements INode {
label: 'Prompt',
name: 'prompt',
type: 'BasePromptTemplate'
},
{
label: 'Format Prompt Values',
name: 'promptValues',
type: 'string',
rows: 5,
placeholder: `{
"input_language": "English",
"output_language": "French"
}`,
optional: true
}
]
}
@@ -48,13 +59,39 @@ class LLMChain_Chains implements INode {
}
async run(nodeData: INodeData, input: string): Promise<string> {
const prompt = nodeData.instance.prompt.inputVariables // ["product"]
if (prompt.length > 1) throw new Error('Prompt can only contains 1 literal string {}. Multiples are found')
const inputVariables = nodeData.instance.prompt.inputVariables // ["product"]
const chain = nodeData.instance
const res = await chain.run(input)
return res
if (inputVariables.length === 1) {
const res = await chain.run(input)
return res
} else if (inputVariables.length > 1) {
const promptValuesStr = nodeData.inputs?.promptValues as string
if (!promptValuesStr) throw new Error('Please provide Prompt Values')
const promptValues = JSON.parse(promptValuesStr.replace(/\s/g, ''))
let seen = []
for (const variable of inputVariables) {
seen.push(variable)
if (promptValues[variable]) {
seen.pop()
}
}
if (seen.length === 1) {
const options = {
...promptValues,
[seen.pop()]: input
}
const res = await chain.call(options)
return res?.text
} else throw new Error('Please provide Prompt Values')
} else {
const res = await chain.run(input)
return res
}
}
}
@@ -0,0 +1,57 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class RetrievalQAChain_Chains implements INode {
label: string
name: string
type: string
icon: string
category: string
baseClasses: string[]
description: string
inputs: INodeParams[]
constructor() {
this.label = 'RetrievalQA Chain'
this.name = 'retrievalQAChain'
this.type = 'RetrievalQAChain'
this.icon = 'chain.svg'
this.category = 'Chains'
this.description = 'QA chain to answer a question based on the retrieved documents'
this.inputs = [
{
label: 'LLM',
name: 'llm',
type: 'BaseLanguageModel'
},
{
label: 'Vector Store Retriever',
name: 'vectorStoreRetriever',
type: 'BaseRetriever'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['BaseChain']
}
async init(nodeData: INodeData): Promise<any> {
const { RetrievalQAChain } = await import('langchain/chains')
const llm = nodeData.inputs?.llm
const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever
const chain = RetrievalQAChain.fromLLM(llm, vectorStoreRetriever)
return chain
}
async run(nodeData: INodeData, input: string): Promise<string> {
const chain = nodeData.instance
const obj = {
query: input
}
const res = await chain.call(obj)
return res?.text
}
}
module.exports = { nodeClass: RetrievalQAChain_Chains }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dna" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"></path>
<path d="M9.172 20.485a4 4 0 1 0 -5.657 -5.657"></path>
<path d="M14.828 3.515a4 4 0 0 0 5.657 5.657"></path>
</svg>

After

Width:  |  Height:  |  Size: 489 B

@@ -0,0 +1,75 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class ChatOpenAI_ChatModels implements INode {
label: string
name: string
type: string
icon: string
category: string
description: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'ChatOpenAI'
this.name = 'chatOpenAI'
this.type = 'ChatOpenAI'
this.icon = 'openai.png'
this.category = 'Chat Models'
this.description = 'Wrapper around OpenAI large language models that use the Chat endpoint'
this.inputs = [
{
label: 'OpenAI Api Key',
name: 'openAIApiKey',
type: 'password'
},
{
label: 'Model Name',
name: 'modelName',
type: 'options',
options: [
{
label: 'gpt-3.5-turbo',
name: 'gpt-3.5-turbo'
},
{
label: 'gpt-3.5-turbo-0301',
name: 'gpt-3.5-turbo-0301'
}
],
default: 'gpt-3.5-turbo',
optional: true
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
default: 0.9,
optional: true
}
]
}
async getBaseClasses(): Promise<string[]> {
const { ChatOpenAI } = await import('langchain/chat_models')
return getBaseClasses(ChatOpenAI)
}
async init(nodeData: INodeData): Promise<any> {
const { ChatOpenAI } = await import('langchain/chat_models')
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const openAIApiKey = nodeData.inputs?.openAIApiKey as string
const model = new ChatOpenAI({
temperature: parseInt(temperature, 10),
modelName,
openAIApiKey
})
return model
}
}
module.exports = { nodeClass: ChatOpenAI_ChatModels }
@@ -0,0 +1,81 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class Github_DocumentLoaders implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Github'
this.name = 'github'
this.type = 'Github'
this.icon = 'github.png'
this.category = 'Document Loaders'
this.description = `Load data from a GitHub repository`
this.inputs = [
{
label: 'Repo Link',
name: 'repoLink',
type: 'string',
placeholder: 'https://github.com/FlowiseAI/Flowise'
},
{
label: 'Branch',
name: 'branch',
type: 'string',
default: 'main'
},
{
label: 'Access Token',
name: 'accessToken',
type: 'password',
placeholder: '<GITHUB_ACCESS_TOKEN>',
optional: true
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['Document']
}
async init(nodeData: INodeData): Promise<any> {
const { GithubRepoLoader } = await import('langchain/document_loaders')
const repoLink = nodeData.inputs?.repoLink as string
const branch = nodeData.inputs?.branch as string
const accessToken = nodeData.inputs?.accessToken as string
const textSplitter = nodeData.inputs?.textSplitter
const options = {
branch,
recursive: false,
unknown: 'warn'
} as any
if (accessToken) options.accessToken = accessToken
const loader = new GithubRepoLoader(repoLink, options)
if (textSplitter) {
const docs = await loader.loadAndSplit(textSplitter)
return docs
} else {
const docs = await loader.load()
return docs
}
}
}
module.exports = { nodeClass: Github_DocumentLoaders }
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,90 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class Pdf_DocumentLoaders implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Pdf File'
this.name = 'pdfFile'
this.type = 'PDF'
this.icon = 'pdf.svg'
this.category = 'Document Loaders'
this.description = `Load data from PDF files`
this.inputs = [
{
label: 'Pdf File',
name: 'pdfFile',
type: 'file',
fileType: '.pdf'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Usage',
name: 'usage',
type: 'options',
options: [
{
label: 'One document per page',
name: 'perPage'
},
{
label: 'One document per file',
name: 'perFile'
}
],
default: 'perPage'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['Document']
}
async init(nodeData: INodeData): Promise<any> {
const { PDFLoader } = await import('langchain/document_loaders')
const textSplitter = nodeData.inputs?.textSplitter
const pdfFileBase64 = nodeData.inputs?.pdfFile as string
const usage = nodeData.inputs?.usage as string
const splitDataURI = pdfFileBase64.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const blob = new Blob([bf])
if (usage === 'perFile') {
const loader = new PDFLoader(blob, { splitPages: false })
if (textSplitter) {
const docs = await loader.loadAndSplit(textSplitter)
return docs
} else {
const docs = await loader.load()
return docs
}
} else {
const loader = new PDFLoader(blob)
if (textSplitter) {
const docs = await loader.loadAndSplit(textSplitter)
return docs
} else {
const docs = await loader.load()
return docs
}
}
}
}
module.exports = { nodeClass: Pdf_DocumentLoaders }
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pdf" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 8v8h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-2z"></path>
<path d="M3 12h2a2 2 0 1 0 0 -4h-2v8"></path>
<path d="M17 12h3"></path>
<path d="M21 8h-4v8"></path>
</svg>

After

Width:  |  Height:  |  Size: 470 B

@@ -0,0 +1,61 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class Text_DocumentLoaders implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Text File'
this.name = 'textFile'
this.type = 'Text'
this.icon = 'textFile.svg'
this.category = 'Document Loaders'
this.description = `Load data from text files`
this.inputs = [
{
label: 'Txt File',
name: 'txtFile',
type: 'file',
fileType: '.txt'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['Document']
}
async init(nodeData: INodeData): Promise<any> {
const { TextLoader } = await import('langchain/document_loaders')
const textSplitter = nodeData.inputs?.textSplitter
const txtFileBase64 = nodeData.inputs?.txtFile as string
const splitDataURI = txtFileBase64.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const blob = new Blob([bf])
const loader = new TextLoader(blob)
if (textSplitter) {
const docs = await loader.loadAndSplit(textSplitter)
return docs
} else {
const docs = await loader.load()
return docs
}
}
}
module.exports = { nodeClass: Text_DocumentLoaders }
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard-text" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"></path>
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12h6"></path>
<path d="M9 16h6"></path>
</svg>

After

Width:  |  Height:  |  Size: 559 B

@@ -0,0 +1,44 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class OpenAIEmbedding_Embeddings implements INode {
label: string
name: string
type: string
icon: string
category: string
description: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'OpenAI Embeddings'
this.name = 'openAIEmbeddings'
this.type = 'OpenAIEmbeddings'
this.icon = 'openai.png'
this.category = 'Embeddings'
this.description = 'OpenAI API to generate embeddings for a given text'
this.inputs = [
{
label: 'OpenAI Api Key',
name: 'openAIApiKey',
type: 'password'
}
]
}
async getBaseClasses(): Promise<string[]> {
const { OpenAIEmbeddings } = await import('langchain/embeddings')
return getBaseClasses(OpenAIEmbeddings)
}
async init(nodeData: INodeData): Promise<any> {
const { OpenAIEmbeddings } = await import('langchain/embeddings')
const openAIApiKey = nodeData.inputs?.openAIApiKey as string
const model = new OpenAIEmbeddings({ openAIApiKey })
return model
}
}
module.exports = { nodeClass: OpenAIEmbedding_Embeddings }
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,64 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class HuggingFaceInference_LLMs implements INode {
label: string
name: string
type: string
icon: string
category: string
description: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'HuggingFace Inference'
this.name = 'huggingFaceInference_LLMs'
this.type = 'HuggingFaceInference'
this.icon = 'huggingface.png'
this.category = 'LLMs'
this.description = 'Wrapper around OpenAI large language models'
this.inputs = [
{
label: 'Model',
name: 'model',
type: 'options',
options: [
{
label: 'gpt2',
name: 'gpt2'
}
],
default: 'gpt2',
optional: true
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
default: 0.7,
optional: true
}
]
}
async getBaseClasses(): Promise<string[]> {
const { HuggingFaceInference } = await import('langchain/llms')
return getBaseClasses(HuggingFaceInference)
}
async init(nodeData: INodeData): Promise<any> {
const { HuggingFaceInference } = await import('langchain/llms')
const temperature = nodeData.inputs?.temperature as string
const model = nodeData.inputs?.model as string
const huggingFace = new HuggingFaceInference({
temperature: parseInt(temperature, 10),
model
})
return huggingFace
}
}
module.exports = { nodeClass: HuggingFaceInference_LLMs }
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

@@ -0,0 +1,54 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class BufferMemory_Memory implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Buffer Memory'
this.name = 'bufferMemory'
this.type = 'BufferMemory'
this.icon = 'memory.svg'
this.category = 'Memory'
this.description = 'Perform calculations on response'
this.inputs = [
{
label: 'Memory Key',
name: 'memoryKey',
type: 'string',
default: 'chat_history'
},
{
label: 'Input Key',
name: 'inputKey',
type: 'string',
default: 'input'
}
]
}
async getBaseClasses(): Promise<string[]> {
const { BufferMemory } = await import('langchain/memory')
return getBaseClasses(BufferMemory)
}
async init(nodeData: INodeData): Promise<any> {
const { BufferMemory } = await import('langchain/memory')
const memoryKey = nodeData.inputs?.memoryKey as string
const inputKey = nodeData.inputs?.inputKey as string
return new BufferMemory({
returnMessages: true,
memoryKey,
inputKey
})
}
}
module.exports = { nodeClass: BufferMemory_Memory }
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-book" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0"></path>
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0"></path>
<path d="M3 6l0 13"></path>
<path d="M12 6l0 13"></path>
<path d="M21 6l0 13"></path>
</svg>

After

Width:  |  Height:  |  Size: 495 B

@@ -0,0 +1,57 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class ChatPromptTemplate_Prompts implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Chat Prompt Template'
this.name = 'chatPromptTemplate'
this.type = 'ChatPromptTemplate'
this.icon = 'prompt.svg'
this.category = 'Prompts'
this.description = 'Schema to represent a chat prompt'
this.inputs = [
{
label: 'System Message',
name: 'systemMessagePrompt',
type: 'string',
rows: 3,
placeholder: `You are a helpful assistant that translates {input_language} to {output_language}.`
},
{
label: 'Human Message',
name: 'humanMessagePrompt',
type: 'string',
rows: 3,
placeholder: `{text}`
}
]
}
async getBaseClasses(): Promise<string[]> {
const { ChatPromptTemplate } = await import('langchain/prompts')
return getBaseClasses(ChatPromptTemplate)
}
async init(nodeData: INodeData): Promise<any> {
const { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } = await import('langchain/prompts')
const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string
const humanMessagePrompt = nodeData.inputs?.humanMessagePrompt as string
const prompt = ChatPromptTemplate.fromPromptMessages([
SystemMessagePromptTemplate.fromTemplate(systemMessagePrompt),
HumanMessagePromptTemplate.fromTemplate(humanMessagePrompt)
])
return prompt
}
}
module.exports = { nodeClass: ChatPromptTemplate_Prompts }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 9l3 3l-3 3"></path>
<path d="M13 15l3 0"></path>
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -0,0 +1,112 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getInputVariables } from '../../../src/utils'
class FewShotPromptTemplate_Prompts implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Few Shot Prompt Template'
this.name = 'fewShotPromptTemplate'
this.type = 'FewShotPromptTemplate'
this.icon = 'prompt.svg'
this.category = 'Prompts'
this.description = 'Prompt template you can build with examples'
this.inputs = [
{
label: 'Examples',
name: 'examples',
type: 'string',
rows: 5,
placeholder: `[
{ "word": "happy", "antonym": "sad" },
{ "word": "tall", "antonym": "short" },
]`
},
{
label: 'Example Prompt',
name: 'examplePrompt',
type: 'BasePromptTemplate'
},
{
label: 'Prefix',
name: 'prefix',
type: 'string',
rows: 3,
placeholder: `Give the antonym of every input`
},
{
label: 'Suffix',
name: 'suffix',
type: 'string',
rows: 3,
placeholder: `Word: {input}\nAntonym:`
},
{
label: 'Example Seperator',
name: 'exampleSeparator',
type: 'string',
placeholder: `\n\n`
},
{
label: 'Template Format',
name: 'templateFormat',
type: 'options',
options: [
{
label: 'f-string',
name: 'f-string'
},
{
label: 'jinja-2',
name: 'jinja-2'
}
],
default: `f-string`
}
]
}
async getBaseClasses(): Promise<string[]> {
const { FewShotPromptTemplate } = await import('langchain/prompts')
return getBaseClasses(FewShotPromptTemplate)
}
async init(nodeData: INodeData): Promise<any> {
const { FewShotPromptTemplate } = await import('langchain/prompts')
const examplesStr = nodeData.inputs?.examples as string
const prefix = nodeData.inputs?.prefix as string
const suffix = nodeData.inputs?.suffix as string
const exampleSeparator = nodeData.inputs?.exampleSeparator as string
const templateFormat = nodeData.inputs?.templateFormat
const examplePrompt = nodeData.inputs?.examplePrompt
const inputVariables = getInputVariables(suffix)
const examples = JSON.parse(examplesStr.replace(/\s/g, ''))
try {
const prompt = new FewShotPromptTemplate({
examples,
examplePrompt,
prefix,
suffix,
inputVariables,
exampleSeparator,
templateFormat
})
return prompt
} catch (e) {
throw new Error(e)
}
}
}
module.exports = { nodeClass: FewShotPromptTemplate_Prompts }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 9l3 3l-3 3"></path>
<path d="M13 15l3 0"></path>
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -17,15 +17,14 @@ class PromptTemplate_Prompts implements INode {
this.type = 'PromptTemplate'
this.icon = 'prompt.svg'
this.category = 'Prompts'
this.description = 'Schema to represent a basic prompt for an LLM. Template can only contains 1 literal string {}'
this.description = 'Schema to represent a basic prompt for an LLM'
this.inputs = [
{
label: 'Template',
name: 'template',
type: 'string',
rows: 5,
default: 'What is a good name for a company that makes {product}?',
placeholder: 'What is a good name for a company that makes {product}?'
placeholder: `What is a good name for a company that makes {product}?`
}
]
}
@@ -42,10 +41,11 @@ class PromptTemplate_Prompts implements INode {
const inputVariables = getInputVariables(template)
try {
const prompt = new PromptTemplate({
const options = {
template,
inputVariables: inputVariables
})
inputVariables
}
const prompt = new PromptTemplate(options)
return prompt
} catch (e) {
throw new Error(e)
@@ -0,0 +1,59 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class RecursiveCharacterTextSplitter_TextSplitters implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Recursive Character Text Splitter'
this.name = 'recursiveCharacterTextSplitter'
this.type = 'RecursiveCharacterTextSplitter'
this.icon = 'textsplitter.svg'
this.category = 'Text Splitters'
this.description = `Split documents recursively by different characters - starting with "\n\n", then "\n", then " "`
this.inputs = [
{
label: 'Chunk Size',
name: 'chunkSize',
type: 'number',
default: 1000,
optional: true
},
{
label: 'Chunk Overlap',
name: 'chunkOverlap',
type: 'number',
optional: true
}
]
}
async getBaseClasses(): Promise<string[]> {
const { RecursiveCharacterTextSplitter } = await import('langchain/text_splitter')
return getBaseClasses(RecursiveCharacterTextSplitter)
}
async init(nodeData: INodeData): Promise<any> {
const { RecursiveCharacterTextSplitter } = await import('langchain/text_splitter')
const chunkSize = nodeData.inputs?.chunkSize as string
const chunkOverlap = nodeData.inputs?.chunkOverlap as string
const obj = {} as any
if (chunkSize) obj.chunkSize = parseInt(chunkSize, 10)
if (chunkOverlap) obj.chunkOverlap = parseInt(chunkOverlap, 10)
const splitter = new RecursiveCharacterTextSplitter(obj)
return splitter
}
}
module.exports = { nodeClass: RecursiveCharacterTextSplitter_TextSplitters }
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-abc" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 16v-6a2 2 0 1 1 4 0v6"></path>
<path d="M3 13h4"></path>
<path d="M10 8v6a2 2 0 1 0 4 0v-1a2 2 0 1 0 -4 0v1"></path>
<path d="M20.732 12a2 2 0 0 0 -3.732 1v1a2 2 0 0 0 3.726 1.01"></path>
</svg>

After

Width:  |  Height:  |  Size: 502 B

@@ -0,0 +1,52 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class Chroma_Existing_VectorStores implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Chroma Load Existing Index'
this.name = 'chromaExistingIndex'
this.type = 'Chroma'
this.icon = 'chroma.svg'
this.category = 'Vector Stores'
this.description = 'Load existing index from Chroma (i.e: Document has been upserted)'
this.inputs = [
{
label: 'Embeddings',
name: 'embeddings',
type: 'Embeddings'
},
{
label: 'Collection Name',
name: 'collectionName',
type: 'string'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['BaseRetriever']
}
async init(nodeData: INodeData): Promise<any> {
const { Chroma } = await import('langchain/vectorstores')
const collectionName = nodeData.inputs?.collectionName as string
const embeddings = nodeData.inputs?.embeddings
const vectorStore = await Chroma.fromExistingCollection(embeddings, {
collectionName
})
const retriever = vectorStore.asRetriever()
return retriever
}
}
module.exports = { nodeClass: Chroma_Existing_VectorStores }
@@ -0,0 +1,7 @@
<svg width="209" height="135" viewBox="0 0 209 135" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="136.019" cy="67.2304" rx="66.6667" ry="64" fill="#FFDE2D"/>
<ellipse cx="69.352" cy="67.2304" rx="66.6667" ry="64" fill="#327EFF"/>
<path d="M2.68528 67.2304C2.68527 31.8842 32.5329 3.23047 69.3519 3.23047L69.3519 67.2304L2.68528 67.2304Z" fill="#327EFF"/>
<path d="M136.019 67.2305C136.019 102.577 106.171 131.23 69.3519 131.23L69.3519 67.2305L136.019 67.2305Z" fill="#FF6446"/>
<path d="M69.352 67.2304C69.352 31.8842 99.1997 3.23047 136.019 3.23047L136.019 67.2304L69.352 67.2304Z" fill="#FF6446"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

@@ -0,0 +1,65 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class ChromaUpsert_VectorStores implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Chroma Upsert Document'
this.name = 'chromaUpsert'
this.type = 'Chroma'
this.icon = 'chroma.svg'
this.category = 'Vector Stores'
this.description = 'Upsert documents to Chroma'
this.inputs = [
{
label: 'Document',
name: 'document',
type: 'Document'
},
{
label: 'Embeddings',
name: 'embeddings',
type: 'Embeddings'
},
{
label: 'Collection Name',
name: 'collectionName',
type: 'string'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['BaseRetriever']
}
async init(nodeData: INodeData): Promise<any> {
const { Chroma } = await import('langchain/vectorstores')
const { Document } = await import('langchain/document')
const collectionName = nodeData.inputs?.collectionName as string
const docs = nodeData.inputs?.document
const embeddings = nodeData.inputs?.embeddings
const finalDocs = []
for (let i = 0; i < docs.length; i += 1) {
finalDocs.push(new Document(docs[i]))
}
const result = await Chroma.fromDocuments(finalDocs, embeddings, {
collectionName
})
const retriever = result.asRetriever()
return retriever
}
}
module.exports = { nodeClass: ChromaUpsert_VectorStores }
@@ -0,0 +1,7 @@
<svg width="209" height="135" viewBox="0 0 209 135" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="136.019" cy="67.2304" rx="66.6667" ry="64" fill="#FFDE2D"/>
<ellipse cx="69.352" cy="67.2304" rx="66.6667" ry="64" fill="#327EFF"/>
<path d="M2.68528 67.2304C2.68527 31.8842 32.5329 3.23047 69.3519 3.23047L69.3519 67.2304L2.68528 67.2304Z" fill="#327EFF"/>
<path d="M136.019 67.2305C136.019 102.577 106.171 131.23 69.3519 131.23L69.3519 67.2305L136.019 67.2305Z" fill="#FF6446"/>
<path d="M69.352 67.2304C69.352 31.8842 99.1997 3.23047 136.019 3.23047L136.019 67.2304L69.352 67.2304Z" fill="#FF6446"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

@@ -0,0 +1,73 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { PineconeClient } from '@pinecone-database/pinecone'
class Pinecone_Existing_VectorStores implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Pinecone Load Existing Index'
this.name = 'pineconeExistingIndex'
this.type = 'Pinecone'
this.icon = 'pinecone.png'
this.category = 'Vector Stores'
this.description = 'Load existing index from Pinecone (i.e: Document has been upserted)'
this.inputs = [
{
label: 'Embeddings',
name: 'embeddings',
type: 'Embeddings'
},
{
label: 'Pinecone Api Key',
name: 'pineconeApiKey',
type: 'password'
},
{
label: 'Pinecone Environment',
name: 'pineconeEnv',
type: 'string'
},
{
label: 'Pinecone Index',
name: 'pineconeIndex',
type: 'string'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['BaseRetriever']
}
async init(nodeData: INodeData): Promise<any> {
const { PineconeStore } = await import('langchain/vectorstores')
const pineconeApiKey = nodeData.inputs?.pineconeApiKey as string
const pineconeEnv = nodeData.inputs?.pineconeEnv as string
const index = nodeData.inputs?.pineconeIndex as string
const embeddings = nodeData.inputs?.embeddings
const client = new PineconeClient()
await client.init({
apiKey: pineconeApiKey,
environment: pineconeEnv
})
const pineconeIndex = client.Index(index)
const vectorStore = await PineconeStore.fromExistingIndex(embeddings, {
pineconeIndex
})
const retriever = vectorStore.asRetriever()
return retriever
}
}
module.exports = { nodeClass: Pinecone_Existing_VectorStores }
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,86 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { PineconeClient } from '@pinecone-database/pinecone'
class PineconeUpsert_VectorStores implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Pinecone Upsert Document'
this.name = 'pineconeUpsert'
this.type = 'Pinecone'
this.icon = 'pinecone.png'
this.category = 'Vector Stores'
this.description = 'Upsert documents to Pinecone'
this.inputs = [
{
label: 'Document',
name: 'document',
type: 'Document'
},
{
label: 'Embeddings',
name: 'embeddings',
type: 'Embeddings'
},
{
label: 'Pinecone Api Key',
name: 'pineconeApiKey',
type: 'password'
},
{
label: 'Pinecone Environment',
name: 'pineconeEnv',
type: 'string'
},
{
label: 'Pinecone Index',
name: 'pineconeIndex',
type: 'string'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['BaseRetriever']
}
async init(nodeData: INodeData): Promise<any> {
const { PineconeStore } = await import('langchain/vectorstores')
const { Document } = await import('langchain/document')
const pineconeApiKey = nodeData.inputs?.pineconeApiKey as string
const pineconeEnv = nodeData.inputs?.pineconeEnv as string
const index = nodeData.inputs?.pineconeIndex as string
const docs = nodeData.inputs?.document
const embeddings = nodeData.inputs?.embeddings
const client = new PineconeClient()
await client.init({
apiKey: pineconeApiKey,
environment: pineconeEnv
})
const pineconeIndex = client.Index(index)
const finalDocs = []
for (let i = 0; i < docs.length; i += 1) {
finalDocs.push(new Document(docs[i]))
}
const result = await PineconeStore.fromDocuments(finalDocs, embeddings, {
pineconeIndex
})
const retriever = result.asRetriever()
return retriever
}
}
module.exports = { nodeClass: PineconeUpsert_VectorStores }
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+5
View File
@@ -16,13 +16,18 @@
},
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@dqbd/tiktoken": "^1.0.4",
"@huggingface/inference": "^1.6.3",
"@pinecone-database/pinecone": "^0.0.12",
"axios": "^0.27.2",
"chromadb": "^1.3.1",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"form-data": "^4.0.0",
"langchain": "^0.0.44",
"moment": "^2.29.3",
"node-fetch": "2",
"pdfjs-dist": "^3.5.141",
"ws": "^8.9.0"
},
"devDependencies": {
+9 -1
View File
@@ -17,6 +17,8 @@ export type NodeParamsType =
export type CommonType = string | number | boolean | undefined | null
export type MessageType = 'apiMessage' | 'userMessage'
/**
* Others
*/
@@ -49,6 +51,7 @@ export interface INodeParams {
rows?: number
list?: boolean
placeholder?: string
fileType?: string
}
export interface INodeExecutionData {
@@ -74,10 +77,15 @@ export interface INode extends INodeProperties {
inputs?: INodeParams[]
getBaseClasses?(): Promise<string[]>
getInstance?(nodeData: INodeData): Promise<string>
run?(nodeData: INodeData, input: string): Promise<string>
run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<string>
}
export interface INodeData extends INodeProperties {
inputs?: ICommonObject
instance?: any
}
export interface IMessage {
message: string
type: MessageType
}
+7
View File
@@ -4,6 +4,13 @@ import * as path from 'path'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
/**
* Get base classes of components
*
* @export
* @param {any} targetClass
* @returns {string[]}
*/
export const getBaseClasses = (targetClass: any) => {
const baseClasses: string[] = []
+43
View File
@@ -0,0 +1,43 @@
import { INodeData } from 'flowise-components'
import { IActiveChatflows } from './Interface'
/**
* This pool is to keep track of active test triggers (event listeners),
* so we can clear the event listeners whenever user refresh or exit page
*/
export class ChatflowPool {
activeChatflows: IActiveChatflows = {}
/**
* Add to the pool
* @param {string} chatflowid
* @param {INodeData} endingNodeData
*/
add(chatflowid: string, endingNodeData: INodeData) {
this.activeChatflows[chatflowid] = {
endingNodeData,
inSync: true
}
}
/**
* Update to the pool
* @param {string} chatflowid
* @param {boolean} inSync
*/
updateInSync(chatflowid: string, inSync: boolean) {
if (Object.prototype.hasOwnProperty.call(this.activeChatflows, chatflowid)) {
this.activeChatflows[chatflowid].inSync = inSync
}
}
/**
* Remove from the pool
* @param {string} chatflowid
*/
async remove(chatflowid: string) {
if (Object.prototype.hasOwnProperty.call(this.activeChatflows, chatflowid)) {
delete this.activeChatflows[chatflowid]
}
}
}
+14 -2
View File
@@ -22,7 +22,7 @@ export interface IChatMessage {
createdDate: Date
}
export interface IComponentNodesPool {
export interface IComponentNodes {
[key: string]: INode
}
@@ -95,7 +95,19 @@ export interface INodeQueue {
depth: number
}
export interface IMessage {
message: string
type: MessageType
}
export interface IncomingInput {
question: string
history: string[]
history: IMessage[]
}
export interface IActiveChatflows {
[key: string]: {
endingNodeData: INodeData
inSync: boolean
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { IComponentNodesPool } from './Interface'
import { IComponentNodes } from './Interface'
import path from 'path'
import { Dirent } from 'fs'
@@ -6,7 +6,7 @@ import { getNodeModulesPackagePath } from './utils'
import { promises } from 'fs'
export class NodesPool {
componentNodes: IComponentNodesPool = {}
componentNodes: IComponentNodes = {}
/**
* Initialize to get all nodes
+49 -27
View File
@@ -3,17 +3,20 @@ import path from 'path'
import cors from 'cors'
import http from 'http'
import { IChatFlow, IComponentNodesPool, IncomingInput, IReactFlowNode, IReactFlowObject } from './Interface'
import { IChatFlow, 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'
import { ChatflowPool } from './ChatflowPool'
import { INodeData } from 'flowise-components'
export class App {
app: express.Application
componentNodes: IComponentNodesPool = {}
nodesPool: NodesPool
chatflowPool: ChatflowPool
AppDataSource = getDataSource()
constructor() {
@@ -26,10 +29,11 @@ export class App {
.then(async () => {
console.info('📦[server]: Data Source has been initialized!')
// Initialize node instances
const nodesPool = new NodesPool()
await nodesPool.initialize()
this.componentNodes = nodesPool.componentNodes
// Initialize pools
this.nodesPool = new NodesPool()
await this.nodesPool.initialize()
this.chatflowPool = new ChatflowPool()
})
.catch((err) => {
console.error('❌[server]: Error during Data Source initialization:', err)
@@ -53,8 +57,8 @@ export class App {
// 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])
for (const nodeName in this.nodesPool.componentNodes) {
const clonedNode = cloneDeep(this.nodesPool.componentNodes[nodeName])
returnData.push(clonedNode)
}
return res.json(returnData)
@@ -62,8 +66,8 @@ export class App {
// 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])
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`)
}
@@ -71,8 +75,8 @@ export class App {
// 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 (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`)
}
@@ -137,6 +141,9 @@ export class App {
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
// Update chatflowpool inSync to false, to build Langchain again because data has been changed
this.chatflowPool.updateInSync(chatflow.id, false)
return res.json(result)
})
@@ -183,30 +190,45 @@ export class App {
// Send input message and get prediction result
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
try {
const chatflowid = req.params.id
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`)
let nodeToExecuteData: INodeData
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const { graph, nodeDependencies } = constructGraphs(parsedFlowData.nodes, parsedFlowData.edges)
if (
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
this.chatflowPool.activeChatflows[chatflowid].inSync
) {
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
} else {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: chatflowid
})
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
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 flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const { graph, nodeDependencies } = constructGraphs(parsedFlowData.nodes, parsedFlowData.edges)
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 startingNodeIds = getStartingNode(nodeDependencies)
const endingNodeId = getEndingNode(nodeDependencies, graph)
if (!endingNodeId) return res.status(500).send(`Ending node must be either Chain or Agent`)
const nodeInstanceFilePath = this.componentNodes[nodeToExecute.data.name].filePath as string
const reactFlowNodes = await buildLangchain(startingNodeIds, parsedFlowData.nodes, graph, this.nodesPool.componentNodes)
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
nodeToExecuteData = nodeToExecute.data
this.chatflowPool.add(chatflowid, nodeToExecuteData)
}
const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const nodeInstance = new nodeModule.nodeClass()
const result = await nodeInstance.run(nodeToExecute.data, incomingInput.question)
const result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
return res.json(result)
} catch (e: any) {
+9 -9
View File
@@ -1,7 +1,7 @@
import path from 'path'
import fs from 'fs'
import {
IComponentNodesPool,
IComponentNodes,
IExploredNode,
INodeDependencies,
INodeDirectedGraph,
@@ -98,8 +98,13 @@ export const getStartingNode = (nodeDependencies: INodeDependencies) => {
return startingNodeIds
}
/**
* Get ending node and check if flow is valid
* @param {INodeDependencies} nodeDependencies
* @param {INodeDirectedGraph} graph
*/
export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => {
// Find starting node
// Find ending node
let endingNodeId = ''
Object.keys(graph).forEach((nodeId) => {
if (!graph[nodeId].length && nodeDependencies[nodeId] > 0) {
@@ -113,17 +118,14 @@ export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeD
* 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
* @param {IComponentNodes} componentNodes
*/
export const buildLangchain = async (
startingNodeIds: string[],
reactFlowNodes: IReactFlowNode[],
graph: INodeDirectedGraph,
componentNodes: IComponentNodesPool
componentNodes: IComponentNodes
) => {
const flowNodes = cloneDeep(reactFlowNodes)
@@ -190,8 +192,6 @@ export const buildLangchain = async (
* 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[]) => {
-1
View File
@@ -8,7 +8,6 @@ export const SET_LAYOUT = '@customization/SET_LAYOUT '
export const SET_DARKMODE = '@customization/SET_DARKMODE'
// action - canvas reducer
export const REMOVE_EDGE = '@canvas/REMOVE_EDGE'
export const SET_DIRTY = '@canvas/SET_DIRTY'
export const REMOVE_DIRTY = '@canvas/REMOVE_DIRTY'
export const SET_CHATFLOW = '@canvas/SET_CHATFLOW'
@@ -4,7 +4,8 @@ import PropTypes from 'prop-types'
const initialValue = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {}
deleteNode: () => {},
deleteEdge: () => {}
}
export const flowContext = createContext(initialValue)
@@ -17,12 +18,17 @@ export const ReactFlowContext = ({ children }) => {
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== id && ns.target !== id))
}
const deleteEdge = (id) => {
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((edge) => edge.id !== id))
}
return (
<flowContext.Provider
value={{
reactFlowInstance,
setReactFlowInstance,
deleteNode
deleteNode,
deleteEdge
}}
>
{children}
@@ -2,7 +2,6 @@
import * as actionTypes from '../actions'
export const initialState = {
removeEdgeId: '',
isDirty: false,
chatflow: null
}
@@ -11,11 +10,6 @@ export const initialState = {
const canvasReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.REMOVE_EDGE:
return {
...state,
removeEdgeId: action.edgeId
}
case actionTypes.SET_DIRTY:
return {
...state,
+57
View File
@@ -0,0 +1,57 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
import { FormControl, Button } from '@mui/material'
import { IconUpload } from '@tabler/icons'
import { getFileName } from 'utils/genericHelper'
export const File = ({ value, fileType, onChange }) => {
const theme = useTheme()
const [myValue, setMyValue] = useState(value ?? '')
const handleFileUpload = (e) => {
if (!e.target.files) return
const file = e.target.files[0]
const { name } = file
const reader = new FileReader()
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
const value = result + `,filename:${name}`
setMyValue(value)
onChange(value)
}
reader.readAsDataURL(file)
}
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<span
style={{
fontStyle: 'italic',
color: theme.palette.grey['800'],
marginBottom: '1rem'
}}
>
{myValue ? getFileName(myValue) : 'Choose a file to upload'}
</span>
<Button variant='outlined' component='label' fullWidth startIcon={<IconUpload />} sx={{ marginRight: '1rem' }}>
{'Upload File'}
<input type='file' accept={fileType} hidden onChange={(e) => handleFileUpload(e)} />
</Button>
</FormControl>
)
}
File.propTypes = {
value: PropTypes.string,
fileType: PropTypes.string,
onChange: PropTypes.func
}
+7 -2
View File
@@ -1,7 +1,9 @@
import { getBezierPath, EdgeText } from 'reactflow'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux'
import { REMOVE_EDGE } from 'store/actions'
import { useContext } from 'react'
import { SET_DIRTY } from 'store/actions'
import { flowContext } from 'store/context/ReactFlowContext'
import './index.css'
@@ -17,11 +19,14 @@ const ButtonEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, ta
targetPosition
})
const { deleteEdge } = useContext(flowContext)
const dispatch = useDispatch()
const onEdgeClick = (evt, id) => {
evt.stopPropagation()
dispatch({ type: REMOVE_EDGE, edgeId: `${id}:${Date.now()}` })
deleteEdge(id)
dispatch({ type: SET_DIRTY })
}
return (
+1 -1
View File
@@ -47,7 +47,7 @@ const CanvasNode = ({ data }) => {
>
<Box>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Box item style={{ width: 50, marginRight: 10, padding: 5 }}>
<Box style={{ width: 50, marginRight: 10, padding: 5 }}>
<div
style={{
...theme.typography.commonAvatar,
@@ -8,6 +8,7 @@ import { Box, Typography, Tooltip } from '@mui/material'
import { Dropdown } from 'ui-component/dropdown/Dropdown'
import { Input } from 'ui-component/input/Input'
import { File } from 'ui-component/file/File'
import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection } from 'utils/genericHelper'
@@ -73,6 +74,13 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
{inputParam.label}
{!inputParam.optional && <span style={{ color: 'red' }}>&nbsp;*</span>}
</Typography>
{inputParam.type === 'file' && (
<File
fileType={inputParam.fileType || '*'}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'Choose a file to upload'}
/>
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
inputParam={inputParam}
-13
View File
@@ -124,7 +124,6 @@ const Canvas = () => {
)
setEdges((eds) => addEdge(newEdge, eds))
setDirty()
}
const handleLoadFlow = (file) => {
@@ -389,18 +388,6 @@ const Canvas = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [testChatflowApi.error])
// Listen to edge button click remove redux event
useEffect(() => {
if (reactFlowInstance) {
const edges = reactFlowInstance.getEdges()
const toRemoveEdgeId = canvasDataStore.removeEdgeId.split(':')[0]
setEdges(edges.filter((edge) => edge.id !== toRemoveEdgeId))
setDirty()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasDataStore.removeEdgeId])
useEffect(() => setChatflow(canvasDataStore.chatflow), [canvasDataStore.chatflow])
// Initialization
@@ -46,7 +46,6 @@ export const ChatMessage = ({ chatflowid }) => {
const [open, setOpen] = useState(false)
const [userInput, setUserInput] = useState('')
const [history, setHistory] = useState([])
const [loading, setLoading] = useState(false)
const [messages, setMessages] = useState([
{
@@ -157,7 +156,10 @@ export const ChatMessage = ({ chatflowid }) => {
// Send user question and history to API
try {
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, { question: userInput, history: history })
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, {
question: userInput,
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?')
})
if (response.data) {
const data = response.data
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
@@ -203,13 +205,6 @@ export const ChatMessage = ({ chatflowid }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatmessageApi.data])
// Keep history in sync with messages
useEffect(() => {
if (messages.length >= 3) {
setHistory([[messages[messages.length - 2].message, messages[messages.length - 1].message]])
}
}, [messages])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
@@ -229,7 +224,6 @@ export const ChatMessage = ({ chatflowid }) => {
return () => {
setUserInput('')
setHistory([])
setLoading(false)
setMessages([
{