diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts new file mode 100644 index 00000000..d3ff6f4d --- /dev/null +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -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 { + return ['AgentExecutor'] + } + + async init(nodeData: INodeData): Promise { + 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 { + const executor = nodeData.instance + const result = await executor.call({ input }) + + return result?.output + } +} + +module.exports = { nodeClass: ConversationalAgent_Agents } diff --git a/packages/components/nodes/agents/ConversationalAgent/agent.svg b/packages/components/nodes/agents/ConversationalAgent/agent.svg new file mode 100644 index 00000000..c87861e5 --- /dev/null +++ b/packages/components/nodes/agents/ConversationalAgent/agent.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/agents/MRLKAgentChat/MRLKAgentChat.ts b/packages/components/nodes/agents/MRLKAgentChat/MRLKAgentChat.ts new file mode 100644 index 00000000..207f9145 --- /dev/null +++ b/packages/components/nodes/agents/MRLKAgentChat/MRLKAgentChat.ts @@ -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 { + return ['AgentExecutor'] + } + + async init(nodeData: INodeData): Promise { + 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 { + const executor = nodeData.instance + const result = await executor.call({ input }) + + return result?.output + } +} + +module.exports = { nodeClass: MRLKAgentChat_Agents } diff --git a/packages/components/nodes/agents/MRLKAgentChat/agent.svg b/packages/components/nodes/agents/MRLKAgentChat/agent.svg new file mode 100644 index 00000000..c87861e5 --- /dev/null +++ b/packages/components/nodes/agents/MRLKAgentChat/agent.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/agents/MRLKAgentLLM/MRLKAgentLLM.ts b/packages/components/nodes/agents/MRLKAgentLLM/MRLKAgentLLM.ts index 00ea31dd..1f5c636b 100644 --- a/packages/components/nodes/agents/MRLKAgentLLM/MRLKAgentLLM.ts +++ b/packages/components/nodes/agents/MRLKAgentLLM/MRLKAgentLLM.ts @@ -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 } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts new file mode 100644 index 00000000..c23a4d92 --- /dev/null +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -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 { + const { ConversationalRetrievalQAChain } = await import('langchain/chains') + return getBaseClasses(ConversationalRetrievalQAChain) + } + + async init(nodeData: INodeData): Promise { + 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 { + 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 } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/chain.svg b/packages/components/nodes/chains/ConversationalRetrievalQAChain/chain.svg new file mode 100644 index 00000000..a5b32f90 --- /dev/null +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/chain.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 1dab8ea4..18376545 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -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 { - 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 + } } } diff --git a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts new file mode 100644 index 00000000..3bfce6fc --- /dev/null +++ b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts @@ -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 { + return ['BaseChain'] + } + + async init(nodeData: INodeData): Promise { + 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 { + const chain = nodeData.instance + const obj = { + query: input + } + const res = await chain.call(obj) + return res?.text + } +} + +module.exports = { nodeClass: RetrievalQAChain_Chains } diff --git a/packages/components/nodes/chains/RetrievalQAChain/chain.svg b/packages/components/nodes/chains/RetrievalQAChain/chain.svg new file mode 100644 index 00000000..a5b32f90 --- /dev/null +++ b/packages/components/nodes/chains/RetrievalQAChain/chain.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index e69de29b..9cb12592 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -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 { + const { ChatOpenAI } = await import('langchain/chat_models') + return getBaseClasses(ChatOpenAI) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/documentloaders/Github/Github.ts b/packages/components/nodes/documentloaders/Github/Github.ts new file mode 100644 index 00000000..ff4a5a7e --- /dev/null +++ b/packages/components/nodes/documentloaders/Github/Github.ts @@ -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: '', + optional: true + }, + { + label: 'Text Splitter', + name: 'textSplitter', + type: 'TextSplitter', + optional: true + } + ] + } + + async getBaseClasses(): Promise { + return ['Document'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/documentloaders/Github/github.png b/packages/components/nodes/documentloaders/Github/github.png new file mode 100644 index 00000000..e4400818 Binary files /dev/null and b/packages/components/nodes/documentloaders/Github/github.png differ diff --git a/packages/components/nodes/documentloaders/Pdf/Pdf.ts b/packages/components/nodes/documentloaders/Pdf/Pdf.ts new file mode 100644 index 00000000..10e12226 --- /dev/null +++ b/packages/components/nodes/documentloaders/Pdf/Pdf.ts @@ -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 { + return ['Document'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/documentloaders/Pdf/pdf.svg b/packages/components/nodes/documentloaders/Pdf/pdf.svg new file mode 100644 index 00000000..20af94f8 --- /dev/null +++ b/packages/components/nodes/documentloaders/Pdf/pdf.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Text/Text.ts b/packages/components/nodes/documentloaders/Text/Text.ts new file mode 100644 index 00000000..b174e230 --- /dev/null +++ b/packages/components/nodes/documentloaders/Text/Text.ts @@ -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 { + return ['Document'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/documentloaders/Text/textFile.svg b/packages/components/nodes/documentloaders/Text/textFile.svg new file mode 100644 index 00000000..200be563 --- /dev/null +++ b/packages/components/nodes/documentloaders/Text/textFile.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts new file mode 100644 index 00000000..414259fe --- /dev/null +++ b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts @@ -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 { + const { OpenAIEmbeddings } = await import('langchain/embeddings') + return getBaseClasses(OpenAIEmbeddings) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/openai.png b/packages/components/nodes/embeddings/OpenAIEmbedding/openai.png new file mode 100644 index 00000000..de08a05b Binary files /dev/null and b/packages/components/nodes/embeddings/OpenAIEmbedding/openai.png differ diff --git a/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts b/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts new file mode 100644 index 00000000..5141781e --- /dev/null +++ b/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts @@ -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 { + const { HuggingFaceInference } = await import('langchain/llms') + return getBaseClasses(HuggingFaceInference) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/llms/HuggingFaceInference/huggingface.png b/packages/components/nodes/llms/HuggingFaceInference/huggingface.png new file mode 100644 index 00000000..f8f202a4 Binary files /dev/null and b/packages/components/nodes/llms/HuggingFaceInference/huggingface.png differ diff --git a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts new file mode 100644 index 00000000..7a668432 --- /dev/null +++ b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts @@ -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 { + const { BufferMemory } = await import('langchain/memory') + return getBaseClasses(BufferMemory) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/memory/BufferMemory/memory.svg b/packages/components/nodes/memory/BufferMemory/memory.svg new file mode 100644 index 00000000..ca8e17da --- /dev/null +++ b/packages/components/nodes/memory/BufferMemory/memory.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts index e69de29b..c5084a55 100644 --- a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts +++ b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts @@ -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 { + const { ChatPromptTemplate } = await import('langchain/prompts') + return getBaseClasses(ChatPromptTemplate) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg b/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg new file mode 100644 index 00000000..7e486118 --- /dev/null +++ b/packages/components/nodes/prompts/ChatPromptTemplate/prompt.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/prompts/FewShotPromptTemplate/FewShotPromptTemplate.ts b/packages/components/nodes/prompts/FewShotPromptTemplate/FewShotPromptTemplate.ts new file mode 100644 index 00000000..d2c3e1af --- /dev/null +++ b/packages/components/nodes/prompts/FewShotPromptTemplate/FewShotPromptTemplate.ts @@ -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 { + const { FewShotPromptTemplate } = await import('langchain/prompts') + return getBaseClasses(FewShotPromptTemplate) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg b/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg new file mode 100644 index 00000000..7e486118 --- /dev/null +++ b/packages/components/nodes/prompts/FewShotPromptTemplate/prompt.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/prompts/HumanMessagePromptTemplate/HumanMessagePromptTemplate.ts b/packages/components/nodes/prompts/HumanMessagePromptTemplate/HumanMessagePromptTemplate.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/components/nodes/prompts/PromptTemplate/PromptTemplate.ts b/packages/components/nodes/prompts/PromptTemplate/PromptTemplate.ts index 11fd26f1..672362a5 100644 --- a/packages/components/nodes/prompts/PromptTemplate/PromptTemplate.ts +++ b/packages/components/nodes/prompts/PromptTemplate/PromptTemplate.ts @@ -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) diff --git a/packages/components/nodes/prompts/SysmtemMessagePromptTemplate/SysmtemMessagePromptTemplate.ts b/packages/components/nodes/prompts/SysmtemMessagePromptTemplate/SysmtemMessagePromptTemplate.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts new file mode 100644 index 00000000..d19ba342 --- /dev/null +++ b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts @@ -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 { + const { RecursiveCharacterTextSplitter } = await import('langchain/text_splitter') + return getBaseClasses(RecursiveCharacterTextSplitter) + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg new file mode 100644 index 00000000..73145e2d --- /dev/null +++ b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/textsplitter.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts b/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts new file mode 100644 index 00000000..c8fd81af --- /dev/null +++ b/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts @@ -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 { + return ['BaseRetriever'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/vectorstores/Chroma_Existing/chroma.svg b/packages/components/nodes/vectorstores/Chroma_Existing/chroma.svg new file mode 100644 index 00000000..64090685 --- /dev/null +++ b/packages/components/nodes/vectorstores/Chroma_Existing/chroma.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts b/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts new file mode 100644 index 00000000..8b039bd2 --- /dev/null +++ b/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts @@ -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 { + return ['BaseRetriever'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/vectorstores/Chroma_Upsert/chroma.svg b/packages/components/nodes/vectorstores/Chroma_Upsert/chroma.svg new file mode 100644 index 00000000..64090685 --- /dev/null +++ b/packages/components/nodes/vectorstores/Chroma_Upsert/chroma.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts b/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts new file mode 100644 index 00000000..2d4f459d --- /dev/null +++ b/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts @@ -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 { + return ['BaseRetriever'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/vectorstores/Pinecone_Existing/pinecone.png b/packages/components/nodes/vectorstores/Pinecone_Existing/pinecone.png new file mode 100644 index 00000000..1ae189fd Binary files /dev/null and b/packages/components/nodes/vectorstores/Pinecone_Existing/pinecone.png differ diff --git a/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts b/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts new file mode 100644 index 00000000..14921ac2 --- /dev/null +++ b/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts @@ -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 { + return ['BaseRetriever'] + } + + async init(nodeData: INodeData): Promise { + 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 } diff --git a/packages/components/nodes/vectorstores/Pinecone_Upsert/pinecone.png b/packages/components/nodes/vectorstores/Pinecone_Upsert/pinecone.png new file mode 100644 index 00000000..1ae189fd Binary files /dev/null and b/packages/components/nodes/vectorstores/Pinecone_Upsert/pinecone.png differ diff --git a/packages/components/package.json b/packages/components/package.json index d5e3f653..0a211553 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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": { diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 6d1d8ea0..7e4159ed 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -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 getInstance?(nodeData: INodeData): Promise - run?(nodeData: INodeData, input: string): Promise + run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise } export interface INodeData extends INodeProperties { inputs?: ICommonObject instance?: any } + +export interface IMessage { + message: string + type: MessageType +} diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 9e462572..7a2a4d25 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -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[] = [] diff --git a/packages/server/src/ChatflowPool.ts b/packages/server/src/ChatflowPool.ts new file mode 100644 index 00000000..cc738e03 --- /dev/null +++ b/packages/server/src/ChatflowPool.ts @@ -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] + } + } +} diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 3419b781..08906d44 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -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 + } } diff --git a/packages/server/src/NodesPool.ts b/packages/server/src/NodesPool.ts index 8b630458..1485ffe5 100644 --- a/packages/server/src/NodesPool.ts +++ b/packages/server/src/NodesPool.ts @@ -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 diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0794d701..ec953d3b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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) { diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 316218b6..bc50aff2 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -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[]) => { diff --git a/packages/ui/src/store/actions.js b/packages/ui/src/store/actions.js index e0600ffd..306c5cb0 100644 --- a/packages/ui/src/store/actions.js +++ b/packages/ui/src/store/actions.js @@ -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' diff --git a/packages/ui/src/store/context/ReactFlowContext.js b/packages/ui/src/store/context/ReactFlowContext.js index 1620c2ea..b8b32606 100644 --- a/packages/ui/src/store/context/ReactFlowContext.js +++ b/packages/ui/src/store/context/ReactFlowContext.js @@ -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 ( {children} diff --git a/packages/ui/src/store/reducers/canvasReducer.js b/packages/ui/src/store/reducers/canvasReducer.js index c16904aa..e98805bb 100644 --- a/packages/ui/src/store/reducers/canvasReducer.js +++ b/packages/ui/src/store/reducers/canvasReducer.js @@ -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, diff --git a/packages/ui/src/ui-component/file/File.js b/packages/ui/src/ui-component/file/File.js new file mode 100644 index 00000000..1a467fe9 --- /dev/null +++ b/packages/ui/src/ui-component/file/File.js @@ -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 ( + + + {myValue ? getFileName(myValue) : 'Choose a file to upload'} + + + + ) +} + +File.propTypes = { + value: PropTypes.string, + fileType: PropTypes.string, + onChange: PropTypes.func +} diff --git a/packages/ui/src/views/canvas/ButtonEdge.js b/packages/ui/src/views/canvas/ButtonEdge.js index 827d51ec..0d819f81 100644 --- a/packages/ui/src/views/canvas/ButtonEdge.js +++ b/packages/ui/src/views/canvas/ButtonEdge.js @@ -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 ( diff --git a/packages/ui/src/views/canvas/CanvasNode.js b/packages/ui/src/views/canvas/CanvasNode.js index a264c924..1d473521 100644 --- a/packages/ui/src/views/canvas/CanvasNode.js +++ b/packages/ui/src/views/canvas/CanvasNode.js @@ -47,7 +47,7 @@ const CanvasNode = ({ data }) => { >
- +
{ {inputParam.label} {!inputParam.optional &&  *} + {inputParam.type === 'file' && ( + (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') && ( { ) 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 diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index fd7c5ab1..45cd3873 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -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([ {