diff --git a/package.json b/package.json index 2385ab0a1..7b1f42ab9 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "clean": "npm exec -ws -- rimraf dist build", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "test": "turbo run test", - "lint": "eslint \"**/*.{js,jsx,ts,tsx,json,md}\" && cd packages/embed && yarn lint", - "lint-fix": "yarn lint --fix && cd packages/embed && yarn lint-fix", + "lint": "eslint \"**/*.{js,jsx,ts,tsx,json,md}\"", + "lint-fix": "yarn lint --fix", "quick": "pretty-quick --staged", "postinstall": "husky install" }, diff --git a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts index 3641f0bd7..0390ba67f 100644 --- a/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts +++ b/packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts @@ -62,6 +62,33 @@ class ConversationalAgent_Agents implements INode { return result?.output } + + jsCodeImport(): string { + return `import { initializeAgentExecutorWithOptions } from 'langchain/agents'` + } + + jsCode(nodeData: INodeData): string { + const tools = nodeData.inputs?.tools as string + const model = nodeData.inputs?.model as string + const memory = nodeData.inputs?.memory as string + + const code = `const input = "" +const tools = ${tools} +const model = ${model} +const memory = ${memory} + +const executor = await initializeAgentExecutorWithOptions(tools, model, { + agentType: 'chat-conversational-react-description', + verbose: true +}) +executor.memory = memory + +const result = await executor.call({ input }) + +console.log(result) +` + return code + } } module.exports = { nodeClass: ConversationalAgent_Agents } diff --git a/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts b/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts index 1a619e6ac..3916521ab 100644 --- a/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts +++ b/packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts @@ -53,6 +53,30 @@ class MRKLAgentChat_Agents implements INode { return result?.output } + + jsCodeImport(): string { + return `import { initializeAgentExecutorWithOptions } from 'langchain/agents'` + } + + jsCode(nodeData: INodeData): string { + const tools = nodeData.inputs?.tools as string + const model = nodeData.inputs?.model as string + + const code = `const input = "" +const tools = ${tools} +const model = ${model} + +const executor = await initializeAgentExecutorWithOptions(tools, model, { + agentType: 'chat-zero-shot-react-description', + verbose: true +}) + +const result = await executor.call({ input }) + +console.log(result) +` + return code + } } module.exports = { nodeClass: MRKLAgentChat_Agents } diff --git a/packages/components/nodes/agents/MRKLAgentLLM/MRKLAgentLLM.ts b/packages/components/nodes/agents/MRKLAgentLLM/MRKLAgentLLM.ts index 499b93834..4edeee452 100644 --- a/packages/components/nodes/agents/MRKLAgentLLM/MRKLAgentLLM.ts +++ b/packages/components/nodes/agents/MRKLAgentLLM/MRKLAgentLLM.ts @@ -54,6 +54,30 @@ class MRKLAgentLLM_Agents implements INode { return result?.output } + + jsCodeImport(): string { + return `import { initializeAgentExecutorWithOptions } from 'langchain/agents'` + } + + jsCode(nodeData: INodeData): string { + const tools = nodeData.inputs?.tools as string + const model = nodeData.inputs?.model as string + + const code = `const input = "" +const tools = ${tools} +const model = ${model} + +const executor = await initializeAgentExecutorWithOptions(tools, model, { + agentType: 'zero-shot-react-description', + verbose: true +}) + +const result = await executor.call({ input }) + +console.log(result) +` + return code + } } module.exports = { nodeClass: MRKLAgentLLM_Agents } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts index 616e2a9b9..690a9e978 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -66,6 +66,30 @@ class ConversationalRetrievalQAChain_Chains implements INode { return res?.text } + + jsCodeImport(): string { + return `import { ConversationalRetrievalQAChain } from 'langchain/chains'` + } + + jsCode(nodeData: INodeData): string { + const llm = nodeData.inputs?.llm as string + const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever as string + + const code = `const input = "" +const chatHistory = "" +const llm = ${llm} +${vectorStoreRetriever} + +const chain = await ConversationalRetrievalQAChain.fromLLM(llm, vectorStoreRetriever) +const result = await chain.call({ + question: input, + chat_history: chatHistory ? chatHistory : [] +}) + +console.log(result) +` + return code + } } module.exports = { nodeClass: ConversationalRetrievalQAChain_Chains } diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 66104c9ee..4d27abd1a 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -77,6 +77,23 @@ class ChatOpenAI_ChatModels implements INode { }) return model } + + jsCodeImport(): string { + return `import { ChatOpenAI } from 'langchain/chat_models/openai'` + } + + jsCode(nodeData: INodeData): string { + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const openAIApiKey = nodeData.inputs?.openAIApiKey as string + + const code = `new ChatOpenAI({ + temperature: ${temperature}, + modelName: "${modelName}", + openAIApiKey: "${openAIApiKey}" +})` + return code + } } module.exports = { nodeClass: ChatOpenAI_ChatModels } diff --git a/packages/components/nodes/documentloaders/Pdf/Pdf.ts b/packages/components/nodes/documentloaders/Pdf/Pdf.ts index 7933e6a04..7ffb948a6 100644 --- a/packages/components/nodes/documentloaders/Pdf/Pdf.ts +++ b/packages/components/nodes/documentloaders/Pdf/Pdf.ts @@ -84,6 +84,36 @@ class Pdf_DocumentLoaders implements INode { } } } + + jsCodeImport(): string { + return `import { PDFLoader } from 'langchain/document_loaders/fs/pdf'` + } + + jsCode(nodeData: INodeData): string { + const textSplitter = nodeData.inputs?.textSplitter as TextSplitter + const usage = nodeData.inputs?.usage as string + const pdfFileBase64 = nodeData.inputs?.pdfFile as string + + const splitDataURI = pdfFileBase64.split(',') + const filePath = splitDataURI.pop() + const fileName = filePath?.split('filename:')[1] ?? '' + + if (usage === 'perFile') { + const code = `const loader = new PDFLoader("${fileName}", { splitPages: false })` + if (textSplitter) { + return `${code}\nconst docs = await loader.loadAndSplit(${textSplitter})` + } else { + return `${code}\nconst docs = await loader.load()` + } + } else { + const code = `const loader = new PDFLoader("${fileName}")` + if (textSplitter) { + return `${code}\nconst docs = await loader.loadAndSplit(${textSplitter})` + } else { + return `${code}\nconst docs = await loader.load()` + } + } + } } module.exports = { nodeClass: Pdf_DocumentLoaders } diff --git a/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts b/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts index 923cf6c64..d73732551 100644 --- a/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts +++ b/packages/components/nodes/embeddings/CohereEmbedding/CohereEmbedding.ts @@ -35,6 +35,16 @@ class CohereEmbedding_Embeddings implements INode { const model = new CohereEmbeddings({ apiKey }) return model } + + jsCodeImport(): string { + return `import { CohereEmbeddings } from 'langchain/embeddings/cohere'` + } + + jsCode(nodeData: INodeData): string { + const apiKey = nodeData.inputs?.cohereApiKey as string + const code = `new CohereEmbeddings({ apiKey: "${apiKey}" })` + return code + } } module.exports = { nodeClass: CohereEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts index f361be63f..d129c25d7 100644 --- a/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts +++ b/packages/components/nodes/embeddings/OpenAIEmbedding/OpenAIEmbedding.ts @@ -35,6 +35,16 @@ class OpenAIEmbedding_Embeddings implements INode { const model = new OpenAIEmbeddings({ openAIApiKey }) return model } + + jsCodeImport(): string { + return `import { OpenAIEmbeddings } from 'langchain/embeddings/openai'` + } + + jsCode(nodeData: INodeData): string { + const openAIApiKey = nodeData.inputs?.openAIApiKey as string + const code = `new OpenAIEmbeddings({ openAIApiKey: "${openAIApiKey}" })` + return code + } } module.exports = { nodeClass: OpenAIEmbedding_Embeddings } diff --git a/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts b/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts index 6aa3f4f4f..f6cceb111 100644 --- a/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts +++ b/packages/components/nodes/llms/HuggingFaceInference/HuggingFaceInference.ts @@ -45,6 +45,21 @@ class HuggingFaceInference_LLMs implements INode { }) return huggingFace } + + jsCodeImport(): string { + return `import { HuggingFaceInference } from 'langchain/llms/hf'` + } + + jsCode(nodeData: INodeData): string { + const model = nodeData.inputs?.model as string + const apiKey = nodeData.inputs?.apiKey as string + + const code = `new HuggingFaceInference({ + model: ${model}, + apiKey: "${apiKey}" +})` + return code + } } module.exports = { nodeClass: HuggingFaceInference_LLMs } diff --git a/packages/components/nodes/llms/OpenAI/OpenAI.ts b/packages/components/nodes/llms/OpenAI/OpenAI.ts index 7ec179ed4..308b0ccb0 100644 --- a/packages/components/nodes/llms/OpenAI/OpenAI.ts +++ b/packages/components/nodes/llms/OpenAI/OpenAI.ts @@ -73,6 +73,23 @@ class OpenAI_LLMs implements INode { }) return model } + + jsCodeImport(): string { + return `import { OpenAI } from 'langchain/llms/openai'` + } + + jsCode(nodeData: INodeData): string { + const temperature = nodeData.inputs?.temperature as string + const modelName = nodeData.inputs?.modelName as string + const openAIApiKey = nodeData.inputs?.openAIApiKey as string + + const code = `new OpenAI({ + temperature: ${temperature}, + modelName: "${modelName}", + openAIApiKey: "${openAIApiKey}" +})` + return code + } } module.exports = { nodeClass: OpenAI_LLMs } diff --git a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts index fd635ff47..91f7b2e83 100644 --- a/packages/components/nodes/memory/BufferMemory/BufferMemory.ts +++ b/packages/components/nodes/memory/BufferMemory/BufferMemory.ts @@ -45,6 +45,22 @@ class BufferMemory_Memory implements INode { inputKey }) } + + jsCodeImport(): string { + return `import { BufferMemory } from 'langchain/memory'` + } + + jsCode(nodeData: INodeData): string { + const memoryKey = nodeData.inputs?.memoryKey as string + const inputKey = nodeData.inputs?.inputKey as string + + const code = `new BufferMemory({ + returnMessages: true, + memoryKey: "${memoryKey}", + inputKey: "${inputKey}" +})` + return code + } } module.exports = { nodeClass: BufferMemory_Memory } diff --git a/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts index d74b29039..3fb0da232 100644 --- a/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts +++ b/packages/components/nodes/textsplitters/RecursiveCharacterTextSplitter/RecursiveCharacterTextSplitter.ts @@ -50,6 +50,23 @@ class RecursiveCharacterTextSplitter_TextSplitters implements INode { return splitter } + + jsCodeImport(): string { + return `import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'` + } + + jsCode(nodeData: INodeData): string { + const chunkSize = nodeData.inputs?.chunkSize as string + const chunkOverlap = nodeData.inputs?.chunkOverlap as string + + const obj = {} as RecursiveCharacterTextSplitterParams + + if (chunkSize) obj.chunkSize = parseInt(chunkSize, 10) + if (chunkOverlap) obj.chunkOverlap = parseInt(chunkOverlap, 10) + + const code = `new RecursiveCharacterTextSplitter(${JSON.stringify(obj)})` + return code + } } module.exports = { nodeClass: RecursiveCharacterTextSplitter_TextSplitters } diff --git a/packages/components/nodes/tools/AIPlugin/AIPlugin.ts b/packages/components/nodes/tools/AIPlugin/AIPlugin.ts index ad21f8dbc..238ac81db 100644 --- a/packages/components/nodes/tools/AIPlugin/AIPlugin.ts +++ b/packages/components/nodes/tools/AIPlugin/AIPlugin.ts @@ -36,6 +36,17 @@ class AIPlugin implements INode { return aiplugin } + + jsCodeImport(): string { + return `import { AIPluginTool } from 'langchain/tools'` + } + + jsCode(nodeData: INodeData): string { + const pluginUrl = nodeData.inputs?.pluginUrl as string + + const code = `await AIPluginTool.fromPluginUrl("${pluginUrl}")` + return code + } } module.exports = { nodeClass: AIPlugin } diff --git a/packages/components/nodes/tools/Calculator/Calculator.ts b/packages/components/nodes/tools/Calculator/Calculator.ts index 85284f0fd..1cb5ae8e9 100644 --- a/packages/components/nodes/tools/Calculator/Calculator.ts +++ b/packages/components/nodes/tools/Calculator/Calculator.ts @@ -24,6 +24,15 @@ class Calculator_Tools implements INode { async init(): Promise { return new Calculator() } + + jsCodeImport(): string { + return `import { Calculator } from 'langchain/tools/calculator'` + } + + jsCode(): string { + const code = `new Calculator()` + return code + } } module.exports = { nodeClass: Calculator_Tools } diff --git a/packages/components/nodes/tools/SerpAPI/SerpAPI.ts b/packages/components/nodes/tools/SerpAPI/SerpAPI.ts index 694324083..abfa25492 100644 --- a/packages/components/nodes/tools/SerpAPI/SerpAPI.ts +++ b/packages/components/nodes/tools/SerpAPI/SerpAPI.ts @@ -33,6 +33,15 @@ class SerpAPI_Tools implements INode { const apiKey = nodeData.inputs?.apiKey as string return new SerpAPI(apiKey) } + + jsCodeImport(): string { + return `import { SerpAPI } from 'langchain/tools'` + } + + jsCode(): string { + const code = `new SerpAPI()` + return code + } } module.exports = { nodeClass: SerpAPI_Tools } diff --git a/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts b/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts index ec9d28a17..402b8495c 100644 --- a/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts +++ b/packages/components/nodes/vectorstores/Chroma_Existing/Chroma_Existing.ts @@ -65,6 +65,26 @@ class Chroma_Existing_VectorStores implements INode { } return vectorStore } + + jsCodeImport(): string { + return `import { Chroma } from 'langchain/vectorstores/chroma'` + } + + jsCode(nodeData: INodeData): string { + const collectionName = nodeData.inputs?.collectionName as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const output = nodeData.outputs?.output as string + + const code = `const embeddings = ${embeddings} + +const vectorStore = await Chroma.fromExistingCollection(embeddings, { + collectionName: "${collectionName}" +})` + if (output === 'retriever') { + return `${code}\nconst vectorStoreRetriever = vectorStore.asRetriever()` + } + return code + } } module.exports = { nodeClass: Chroma_Existing_VectorStores } diff --git a/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts b/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts index 3f3001709..116356f53 100644 --- a/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts +++ b/packages/components/nodes/vectorstores/Chroma_Upsert/Chroma_Upsert.ts @@ -77,6 +77,29 @@ class ChromaUpsert_VectorStores implements INode { } return vectorStore } + + jsCodeImport(): string { + return `import { Chroma } from 'langchain/vectorstores/chroma'` + } + + jsCode(nodeData: INodeData): string { + const collectionName = nodeData.inputs?.collectionName as string + const docs = nodeData.inputs?.document as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const output = nodeData.outputs?.output as string + + const code = `${docs} + +const embeddings = ${embeddings} + +const vectorStore = await Chroma.fromDocuments(docs, embeddings, { + collectionName: "${collectionName}" +})` + if (output === 'retriever') { + return `${code}\nconst vectorStoreRetriever = vectorStore.asRetriever()` + } + return code + } } module.exports = { nodeClass: ChromaUpsert_VectorStores } diff --git a/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts b/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts index 7b3223c89..b44baa7f6 100644 --- a/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts +++ b/packages/components/nodes/vectorstores/Pinecone_Existing/Pinecone_Existing.ts @@ -86,6 +86,37 @@ class Pinecone_Existing_VectorStores implements INode { } return vectorStore } + + jsCodeImport(): string { + return `import { PineconeClient } from '@pinecone-database/pinecone' +import { PineconeStore } from 'langchain/vectorstores/pinecone'` + } + + jsCode(nodeData: INodeData): string { + 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 as Embeddings + const output = nodeData.outputs?.output as string + + const code = `const client = new PineconeClient() +await client.init({ + apiKey: "${pineconeApiKey}", + environment: "${pineconeEnv}" +}) + +const pineconeIndex = client.Index("${index}") + +const embeddings = ${embeddings} + +const vectorStore = await PineconeStore.fromExistingIndex(embeddings, { + pineconeIndex +})` + if (output === 'retriever') { + return `${code}\nconst vectorStoreRetriever = vectorStore.asRetriever()` + } + return code + } } module.exports = { nodeClass: Pinecone_Existing_VectorStores } diff --git a/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts b/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts index 6f72eb83a..f13fcdd58 100644 --- a/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts +++ b/packages/components/nodes/vectorstores/Pinecone_Upsert/Pinecone_Upsert.ts @@ -98,6 +98,40 @@ class PineconeUpsert_VectorStores implements INode { } return vectorStore } + + jsCodeImport(): string { + return `import { PineconeClient } from '@pinecone-database/pinecone' +import { PineconeStore } from 'langchain/vectorstores/pinecone'` + } + + jsCode(nodeData: INodeData): string { + 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 as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + const output = nodeData.outputs?.output as string + + const code = `const client = new PineconeClient() +await client.init({ + apiKey: "${pineconeApiKey}", + environment: "${pineconeEnv}" +}) + +const pineconeIndex = client.Index("${index}") + +${docs} + +const embeddings = ${embeddings} + +const vectorStore = await PineconeStore.fromDocuments(docs, embeddings, { + pineconeIndex +})` + if (output === 'retriever') { + return `${code}\nconst vectorStoreRetriever = vectorStore.asRetriever()` + } + return code + } } module.exports = { nodeClass: PineconeUpsert_VectorStores } diff --git a/packages/components/src/Interface.ts b/packages/components/src/Interface.ts index 44c87d746..b754de004 100644 --- a/packages/components/src/Interface.ts +++ b/packages/components/src/Interface.ts @@ -75,6 +75,8 @@ export interface INode extends INodeProperties { output?: INodeOutputsValue[] init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise + jsCodeImport?(): string + jsCode?(nodeData: INodeData, input: string, options?: ICommonObject): string } export interface INodeData extends INodeProperties { diff --git a/packages/embed/src/components/Bot.tsx b/packages/embed/src/components/Bot.tsx index 2dbb10a32..3858149af 100644 --- a/packages/embed/src/components/Bot.tsx +++ b/packages/embed/src/components/Bot.tsx @@ -64,8 +64,6 @@ export const Bot = (props: BotProps) => { } }) - console.log(data) - if (data) { setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }]) setLoading(false) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index e5eb41610..5a9ed6f7d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -12,7 +12,8 @@ import { getEndingNode, constructGraphs, resolveVariables, - isStartNodeDependOnInput + isStartNodeDependOnInput, + buildLangchainCode } from './utils' import { cloneDeep } from 'lodash' import { getDataSource } from './DataSource' @@ -284,6 +285,56 @@ export class App { } }) + this.app.get('/api/v1/exportcode/:id', async (req: Request, res: Response) => { + try { + const chatflowid = req.params.id + + /*** Get chatflows and prepare data ***/ + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowid + }) + if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) + + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const edges = parsedFlowData.edges + + /*** Get Ending Node with Directed Graph ***/ + const { graph, nodeDependencies } = constructGraphs(nodes, edges) + const directedGraph = graph + const endingNodeId = getEndingNode(nodeDependencies, directedGraph) + if (!endingNodeId) return res.status(500).send(`Ending node must be either a Chain or Agent`) + + const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data + if (!endingNodeData) return res.status(500).send(`Ending node must be either a Chain or Agent`) + + if ( + endingNodeData.outputs && + Object.keys(endingNodeData.outputs).length && + !Object.values(endingNodeData.outputs).includes(endingNodeData.name) + ) { + return res + .status(500) + .send( + `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction` + ) + } + + /*** Get Starting Nodes with Non-Directed Graph ***/ + const constructedObj = constructGraphs(nodes, edges, true) + const nonDirectedGraph = constructedObj.graph + const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId) + + /*** BFS to traverse from Starting Nodes to Ending Node ***/ + const finalCode = await buildLangchainCode(startingNodeIds, nodes, graph, depthQueue, this.nodesPool.componentNodes) + + return res.send(finalCode) + } catch (e: any) { + return res.status(500).send(e.message) + } + }) + // ---------------------------------------- // Marketplaces // ---------------------------------------- diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 1e2a91e96..90f6869f3 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -362,3 +362,202 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[]): boole } return false } + +/** + * Build langchain from start to end + * @param {string} startingNodeId + * @param {IReactFlowNode[]} reactFlowNodes + * @param {INodeDirectedGraph} graph + * @param {IDepthQueue} depthQueue + * @param {IComponentNodes} componentNodes + */ +export const buildLangchainCode = async ( + startingNodeIds: string[], + reactFlowNodes: IReactFlowNode[], + graph: INodeDirectedGraph, + depthQueue: IDepthQueue, + componentNodes: IComponentNodes +) => { + const flowNodes = cloneDeep(reactFlowNodes) + const jsCodeReference: ICommonObject = {} + let finalCode = '' + let finalImports = '' + + // Create a Queue and add our initial node in it + const nodeQueue = [] as INodeQueue[] + const exploredNode = {} as IExploredNode + + // In the case of infinite loop, only max 3 loops will be executed + const maxLoop = 3 + + for (let i = 0; i < startingNodeIds.length; i += 1) { + nodeQueue.push({ nodeId: startingNodeIds[i], depth: 0 }) + exploredNode[startingNodeIds[i]] = { remainingLoop: maxLoop, lastSeenDepth: 0 } + } + + while (nodeQueue.length) { + const { nodeId, depth } = nodeQueue.shift() as INodeQueue + + const reactFlowNode = flowNodes.find((nd) => nd.id === nodeId) + const nodeIndex = flowNodes.findIndex((nd) => nd.id === nodeId) + if (!reactFlowNode || reactFlowNode === undefined || nodeIndex < 0) continue + + try { + const nodeInstanceFilePath = componentNodes[reactFlowNode.data.name].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + const reactFlowNodeData: INodeData = resolveJsCode(reactFlowNode.data, flowNodes, jsCodeReference) + + if (newNodeInstance.jsCodeImport) { + finalImports += newNodeInstance.jsCodeImport(reactFlowNodeData) + `\n` + } + + if (newNodeInstance.jsCode) { + jsCodeReference[reactFlowNodeData.id] = newNodeInstance.jsCode(reactFlowNodeData) + finalCode = newNodeInstance.jsCode(reactFlowNodeData) + } + } catch (e: any) { + console.error(e) + throw new Error(e) + } + + const neighbourNodeIds = graph[nodeId] + const nextDepth = depth + 1 + + // Find other nodes that are on the same depth level + const sameDepthNodeIds = Object.keys(depthQueue).filter((key) => depthQueue[key] === nextDepth) + + for (const id of sameDepthNodeIds) { + if (neighbourNodeIds.includes(id)) continue + neighbourNodeIds.push(id) + } + + for (let i = 0; i < neighbourNodeIds.length; i += 1) { + const neighNodeId = neighbourNodeIds[i] + + // If nodeId has been seen, cycle detected + if (Object.prototype.hasOwnProperty.call(exploredNode, neighNodeId)) { + const { remainingLoop, lastSeenDepth } = exploredNode[neighNodeId] + + if (lastSeenDepth === nextDepth) continue + + if (remainingLoop === 0) { + break + } + const remainingLoopMinusOne = remainingLoop - 1 + exploredNode[neighNodeId] = { remainingLoop: remainingLoopMinusOne, lastSeenDepth: nextDepth } + nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth }) + } else { + exploredNode[neighNodeId] = { remainingLoop: maxLoop, lastSeenDepth: nextDepth } + nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth }) + } + } + } + return `${finalImports}\n${finalCode}` +} + +/** + * Resolve only for isAcceptVariable + * @param {string} paramValue + * @param {IReactFlowNode[]} reactFlowNodes + * @param {boolean} isAcceptVariable + * @returns {string} + */ +export const getJsCode = ( + paramValue: string, + reactFlowNodes: IReactFlowNode[], + jsCodeReference: ICommonObject, + isAcceptVariable = false +) => { + let returnVal = paramValue + const variableStack = [] + const variableDict = {} as IVariableDict + let startIdx = 0 + const endIdx = returnVal.length - 1 + + while (startIdx < endIdx) { + const substr = returnVal.substring(startIdx, startIdx + 2) + + // Store the opening double curly bracket + if (substr === '{{') { + variableStack.push({ substr, startIdx: startIdx + 2 }) + } + + // Found the complete variable + if (substr === '}}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{{') { + const variableStartIdx = variableStack[variableStack.length - 1].startIdx + const variableEndIdx = startIdx + const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx) + + if (isAcceptVariable && variableFullPath === QUESTION_VAR_PREFIX) { + variableDict[`{{${variableFullPath}}}`] = `` + } + + // Split by first occurence of '.' to get just nodeId + const [variableNodeId, _] = variableFullPath.split('.') + const executedNode = reactFlowNodes.find((nd) => nd.id === variableNodeId) + if (executedNode) { + if (isAcceptVariable) { + variableDict[`{{${variableFullPath}}}`] = `` + } else { + const variableValue = get(jsCodeReference, variableNodeId) + returnVal = variableValue + } + } + variableStack.pop() + } + startIdx += 1 + } + + if (isAcceptVariable) { + const variablePaths = Object.keys(variableDict) + variablePaths.sort() // Sort by length of variable path because longer path could possibly contains nested variable + variablePaths.forEach((path) => { + const variableValue = variableDict[path] + // Replace all occurence + returnVal = returnVal.split(path).join(variableValue) + }) + return returnVal + } + return returnVal +} + +/** + * Loop through each inputs and resolve variable if neccessary + * @param {INodeData} reactFlowNodeData + * @param {IReactFlowNode[]} reactFlowNodes + * @returns {INodeData} + */ +export const resolveJsCode = ( + reactFlowNodeData: INodeData, + reactFlowNodes: IReactFlowNode[], + jsCodeReference: ICommonObject +): INodeData => { + const flowNodeData = cloneDeep(reactFlowNodeData) + const types = 'inputs' + + const getParamValues = (paramsObj: ICommonObject) => { + for (const key in paramsObj) { + const paramValue: string = paramsObj[key] + if (Array.isArray(paramValue)) { + const resolvedCodes = [] + for (const param of paramValue) { + const code = getJsCode(param, reactFlowNodes, jsCodeReference) + resolvedCodes.push(code) + } + paramsObj[key] = `[${resolvedCodes}]` + } else { + const isAcceptVariable = reactFlowNodeData.inputParams.find((param) => param.name === key)?.acceptVariable ?? false + const resolvedCodes = getJsCode(paramValue, reactFlowNodes, jsCodeReference, isAcceptVariable) + paramsObj[key] = resolvedCodes + } + } + } + + const paramsObj = (flowNodeData as any)[types] + + getParamValues(paramsObj) + + return flowNodeData +} diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index eae010eda..e94c111f2 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -10,10 +10,13 @@ const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body) const deleteChatflow = (id) => client.delete(`/chatflows/${id}`) +const exportCode = (id) => client.get(`/exportcode/${id}`) + export default { getAllChatflows, getSpecificChatflow, createNewChatflow, updateChatflow, - deleteChatflow + deleteChatflow, + exportCode } diff --git a/packages/ui/src/ui-component/dialog/APICodeDialog.js b/packages/ui/src/ui-component/dialog/APICodeDialog.js index e2c730d15..53b288ccb 100644 --- a/packages/ui/src/ui-component/dialog/APICodeDialog.js +++ b/packages/ui/src/ui-component/dialog/APICodeDialog.js @@ -40,7 +40,7 @@ function a11yProps(index) { const APICodeDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') - const codes = ['Embed', 'Python', 'JavaScript', 'cURL'] + const codes = ['Embed', 'Python API', 'JavaScript API', 'JavaScript Code', 'cURL'] const [value, setValue] = useState(0) const handleChange = (event, newValue) => { @@ -48,7 +48,7 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => { } const getCode = (codeLang) => { - if (codeLang === 'Python') { + if (codeLang === 'Python API') { return `import requests API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" @@ -61,7 +61,7 @@ output = query({ "question": "Hey, how are you?", }) ` - } else if (codeLang === 'JavaScript') { + } else if (codeLang === 'JavaScript API') { return `async function query(data) { const response = await fetch( "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", @@ -89,14 +89,16 @@ output = query({ return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ -X POST \\ -d '{"question": "Hey, how are you?"}'` + } else if (codeLang === 'JavaScript Code') { + return dialogProps ? dialogProps.exportedCode : '' } return '' } const getLang = (codeLang) => { - if (codeLang === 'Python') { + if (codeLang === 'Python API') { return 'python' - } else if (codeLang === 'JavaScript' || codeLang === 'Embed') { + } else if (codeLang === 'JavaScript API' || codeLang === 'Embed' || codeLang === 'JavaScript Code') { return 'javascript' } else if (codeLang === 'cURL') { return 'bash' @@ -105,9 +107,9 @@ output = query({ } const getSVG = (codeLang) => { - if (codeLang === 'Python') { + if (codeLang === 'Python API') { return pythonSVG - } else if (codeLang === 'JavaScript') { + } else if (codeLang === 'JavaScript API' || codeLang === 'JavaScript Code') { return javascriptSVG } else if (codeLang === 'Embed') { return EmbedSVG diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index ad6d4d4c8..33f4f9a5c 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -8,7 +8,7 @@ import { useTheme } from '@mui/material/styles' import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material' // icons -import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconWorldWww } from '@tabler/icons' +import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconCode } from '@tabler/icons' // project imports import Settings from 'views/settings' @@ -79,10 +79,18 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl } } - const onAPIDialogClick = () => { + const onAPIDialogClick = async () => { + let exportedCode = '' + try { + const response = await chatflowsApi.exportCode(chatflow.id) + exportedCode = response.data + } catch (error) { + console.error(error) + } setAPIDialogProps({ - title: 'Embed in your application or use as API', - chatflowid: chatflow.id + title: 'Embed in your application, use as API, or export as code', + chatflowid: chatflow.id, + exportedCode }) setAPIDialogOpen(true) } @@ -248,7 +256,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl color='inherit' onClick={onAPIDialogClick} > - + )}