diff --git a/packages/components/credentials/LangfuseApi.credential.ts b/packages/components/credentials/LangfuseApi.credential.ts new file mode 100644 index 000000000..452ca9897 --- /dev/null +++ b/packages/components/credentials/LangfuseApi.credential.ts @@ -0,0 +1,39 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class LangfuseApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Langfuse API' + this.name = 'langfuseApi' + this.version = 1.0 + this.description = + 'Refer to official guide on how to get API key on Langfuse' + this.inputs = [ + { + label: 'Secret Key', + name: 'langFuseSecretKey', + type: 'password', + placeholder: 'sk-lf-abcdefg' + }, + { + label: 'Public Key', + name: 'langFusePublicKey', + type: 'string', + placeholder: 'pk-lf-abcdefg' + }, + { + label: 'Endpoint', + name: 'langFuseEndpoint', + type: 'string', + default: 'https://cloud.langfuse.com' + } + ] + } +} + +module.exports = { credClass: LangfuseApi } diff --git a/packages/components/credentials/LangsmithApi.credential.ts b/packages/components/credentials/LangsmithApi.credential.ts new file mode 100644 index 000000000..09ae6773a --- /dev/null +++ b/packages/components/credentials/LangsmithApi.credential.ts @@ -0,0 +1,33 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class LangsmithApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Langsmith API' + this.name = 'langsmithApi' + this.version = 1.0 + this.description = + 'Refer to official guide on how to get API key on Langsmith' + this.inputs = [ + { + label: 'API Key', + name: 'langSmithApiKey', + type: 'password', + placeholder: '' + }, + { + label: 'Endpoint', + name: 'langSmithEndpoint', + type: 'string', + default: 'https://api.smith.langchain.com' + } + ] + } +} + +module.exports = { credClass: LangsmithApi } diff --git a/packages/components/nodes/agents/AirtableAgent/AirtableAgent.ts b/packages/components/nodes/agents/AirtableAgent/AirtableAgent.ts index 074f39c1b..7bdbb65a2 100644 --- a/packages/components/nodes/agents/AirtableAgent/AirtableAgent.ts +++ b/packages/components/nodes/agents/AirtableAgent/AirtableAgent.ts @@ -4,7 +4,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../ import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core' import { LLMChain } from 'langchain/chains' import { BaseLanguageModel } from 'langchain/base_language' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import axios from 'axios' class Airtable_Agents implements INode { @@ -102,6 +102,7 @@ class Airtable_Agents implements INode { const loggerHandler = new ConsoleCallbackHandler(options.logger) const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) + const callbacks = await additionalCallbacks(nodeData, options) const pyodide = await LoadPyodide() @@ -141,7 +142,7 @@ json.dumps(my_dict)` dict: dataframeColDict, question: input } - const res = await chain.call(inputs, [loggerHandler]) + const res = await chain.call(inputs, [loggerHandler, ...callbacks]) pythonCode = res?.text } @@ -169,10 +170,10 @@ json.dumps(my_dict)` } if (options.socketIO && options.socketIOClientId) { - const result = await chain.call(inputs, [loggerHandler, handler]) + const result = await chain.call(inputs, [loggerHandler, handler, ...callbacks]) return result?.text } else { - const result = await chain.call(inputs, [loggerHandler]) + const result = await chain.call(inputs, [loggerHandler, ...callbacks]) return result?.text } } diff --git a/packages/components/nodes/agents/CSVAgent/CSVAgent.ts b/packages/components/nodes/agents/CSVAgent/CSVAgent.ts index 4a42592ff..905bef02d 100644 --- a/packages/components/nodes/agents/CSVAgent/CSVAgent.ts +++ b/packages/components/nodes/agents/CSVAgent/CSVAgent.ts @@ -4,7 +4,7 @@ import { getBaseClasses } from '../../../src/utils' import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core' import { LLMChain } from 'langchain/chains' import { BaseLanguageModel } from 'langchain/base_language' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class CSV_Agents implements INode { label: string @@ -63,6 +63,7 @@ class CSV_Agents implements INode { const loggerHandler = new ConsoleCallbackHandler(options.logger) const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) + const callbacks = await additionalCallbacks(nodeData, options) let files: string[] = [] @@ -119,7 +120,7 @@ json.dumps(my_dict)` dict: dataframeColDict, question: input } - const res = await chain.call(inputs, [loggerHandler]) + const res = await chain.call(inputs, [loggerHandler, ...callbacks]) pythonCode = res?.text } @@ -149,10 +150,10 @@ json.dumps(my_dict)` } if (options.socketIO && options.socketIOClientId) { - const result = await chain.call(inputs, [loggerHandler, handler]) + const result = await chain.call(inputs, [loggerHandler, handler, ...callbacks]) return result?.text } else { - const result = await chain.call(inputs, [loggerHandler]) + const result = await chain.call(inputs, [loggerHandler, ...callbacks]) return result?.text } } diff --git a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts index c0cef0526..3d70a2d32 100644 --- a/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts +++ b/packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts @@ -3,7 +3,7 @@ import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/age import { getBaseClasses, mapChatHistory } from '../../../src/utils' import { flatten } from 'lodash' import { BaseChatMemory } from 'langchain/memory' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' const defaultMessage = `Do your best to answer the questions. Feel free to use any tools available to look up relevant information, only if necessary.` @@ -86,13 +86,14 @@ class ConversationalRetrievalAgent_Agents implements INode { } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const result = await executor.call({ input }, [loggerHandler, handler]) + const result = await executor.call({ input }, [loggerHandler, handler, ...callbacks]) return result?.output } else { - const result = await executor.call({ input }, [loggerHandler]) + const result = await executor.call({ input }, [loggerHandler, ...callbacks]) return result?.output } } diff --git a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts index 8c182d1ac..c1bd32ec5 100644 --- a/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts +++ b/packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts @@ -4,7 +4,7 @@ import { getBaseClasses, mapChatHistory } from '../../../src/utils' import { BaseLanguageModel } from 'langchain/base_language' import { flatten } from 'lodash' import { BaseChatMemory } from 'langchain/memory' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class OpenAIFunctionAgent_Agents implements INode { label: string @@ -86,13 +86,14 @@ class OpenAIFunctionAgent_Agents implements INode { } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const result = await executor.run(input, [loggerHandler, handler]) + const result = await executor.run(input, [loggerHandler, handler, ...callbacks]) return result } else { - const result = await executor.run(input, [loggerHandler]) + const result = await executor.run(input, [loggerHandler, ...callbacks]) return result } } diff --git a/packages/components/nodes/analytic/LangFuse/LangFuse.ts b/packages/components/nodes/analytic/LangFuse/LangFuse.ts new file mode 100644 index 000000000..dcfc3d2a6 --- /dev/null +++ b/packages/components/nodes/analytic/LangFuse/LangFuse.ts @@ -0,0 +1,33 @@ +import { INode, INodeParams } from '../../../src/Interface' + +class LangFuse_Analytic implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs?: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'LangFuse' + this.name = 'langFuse' + this.version = 1.0 + this.type = 'LangFuse' + this.icon = 'langfuse.png' + this.category = 'Analytic' + this.baseClasses = [this.type] + this.inputs = [] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['langfuseApi'] + } + } +} + +module.exports = { nodeClass: LangFuse_Analytic } diff --git a/packages/components/nodes/analytic/LangFuse/langfuse.png b/packages/components/nodes/analytic/LangFuse/langfuse.png new file mode 100644 index 000000000..df9181b8d Binary files /dev/null and b/packages/components/nodes/analytic/LangFuse/langfuse.png differ diff --git a/packages/components/nodes/analytic/LangSmith/LangSmith.ts b/packages/components/nodes/analytic/LangSmith/LangSmith.ts new file mode 100644 index 000000000..425729f50 --- /dev/null +++ b/packages/components/nodes/analytic/LangSmith/LangSmith.ts @@ -0,0 +1,33 @@ +import { INode, INodeParams } from '../../../src/Interface' + +class LangSmith_Analytic implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs?: INodeParams[] + credential: INodeParams + + constructor() { + this.label = 'LangSmith' + this.name = 'langSmith' + this.version = 1.0 + this.type = 'LangSmith' + this.icon = 'langchain.png' + this.category = 'Analytic' + this.baseClasses = [this.type] + this.inputs = [] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['langsmithApi'] + } + } +} + +module.exports = { nodeClass: LangSmith_Analytic } diff --git a/packages/components/nodes/analytic/LangSmith/langchain.png b/packages/components/nodes/analytic/LangSmith/langchain.png new file mode 100644 index 000000000..587dd3140 Binary files /dev/null and b/packages/components/nodes/analytic/LangSmith/langchain.png differ diff --git a/packages/components/nodes/chains/ApiChain/GETApiChain.ts b/packages/components/nodes/chains/ApiChain/GETApiChain.ts index bd4f3bc0d..89aa2f617 100644 --- a/packages/components/nodes/chains/ApiChain/GETApiChain.ts +++ b/packages/components/nodes/chains/ApiChain/GETApiChain.ts @@ -3,7 +3,7 @@ import { APIChain } from 'langchain/chains' import { getBaseClasses } from '../../../src/utils' import { BaseLanguageModel } from 'langchain/base_language' import { PromptTemplate } from 'langchain/prompts' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' export const API_URL_RAW_PROMPT_TEMPLATE = `You are given the below API Documentation: {api_docs} @@ -99,13 +99,14 @@ class GETApiChain_Chains implements INode { const chain = await getAPIChain(apiDocs, model, headers, urlPrompt, ansPrompt) const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) - const res = await chain.run(input, [loggerHandler, handler]) + const res = await chain.run(input, [loggerHandler, handler, ...callbacks]) return res } else { - const res = await chain.run(input, [loggerHandler]) + const res = await chain.run(input, [loggerHandler, ...callbacks]) return res } } diff --git a/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts b/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts index 9f6c79e4e..08dc3cc8f 100644 --- a/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts +++ b/packages/components/nodes/chains/ApiChain/OpenAPIChain.ts @@ -2,7 +2,7 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter import { APIChain, createOpenAPIChain } from 'langchain/chains' import { getBaseClasses } from '../../../src/utils' import { ChatOpenAI } from 'langchain/chat_models/openai' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class OpenApiChain_Chains implements INode { label: string @@ -61,13 +61,14 @@ class OpenApiChain_Chains implements INode { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { const chain = await initChain(nodeData) const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.run(input, [loggerHandler, handler]) + const res = await chain.run(input, [loggerHandler, handler, ...callbacks]) return res } else { - const res = await chain.run(input, [loggerHandler]) + const res = await chain.run(input, [loggerHandler, ...callbacks]) return res } } diff --git a/packages/components/nodes/chains/ApiChain/POSTApiChain.ts b/packages/components/nodes/chains/ApiChain/POSTApiChain.ts index cba4a2970..70277ee91 100644 --- a/packages/components/nodes/chains/ApiChain/POSTApiChain.ts +++ b/packages/components/nodes/chains/ApiChain/POSTApiChain.ts @@ -3,7 +3,7 @@ import { getBaseClasses } from '../../../src/utils' import { BaseLanguageModel } from 'langchain/base_language' import { PromptTemplate } from 'langchain/prompts' import { API_RESPONSE_RAW_PROMPT_TEMPLATE, API_URL_RAW_PROMPT_TEMPLATE, APIChain } from './postCore' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class POSTApiChain_Chains implements INode { label: string @@ -88,13 +88,14 @@ class POSTApiChain_Chains implements INode { const chain = await getAPIChain(apiDocs, model, headers, urlPrompt, ansPrompt) const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) - const res = await chain.run(input, [loggerHandler, handler]) + const res = await chain.run(input, [loggerHandler, handler, ...callbacks]) return res } else { - const res = await chain.run(input, [loggerHandler]) + const res = await chain.run(input, [loggerHandler, ...callbacks]) return res } } diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 08663395e..b26603e29 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -4,7 +4,7 @@ import { getBaseClasses, mapChatHistory } from '../../../src/utils' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' import { BufferMemory } from 'langchain/memory' import { BaseChatModel } from 'langchain/chat_models/base' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { flatten } from 'lodash' import { Document } from 'langchain/document' @@ -111,13 +111,14 @@ class ConversationChain_Chains implements INode { } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.call({ input }, [loggerHandler, handler]) + const res = await chain.call({ input }, [loggerHandler, handler, ...callbacks]) return res?.response } else { - const res = await chain.call({ input }, [loggerHandler]) + const res = await chain.call({ input }, [loggerHandler, ...callbacks]) return res?.response } } diff --git a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts index c14b292d6..1b4675bda 100644 --- a/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts +++ b/packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts @@ -5,7 +5,7 @@ import { ConversationalRetrievalQAChain, QAChainParams } from 'langchain/chains' import { BaseRetriever } from 'langchain/schema/retriever' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { PromptTemplate } from 'langchain/prompts' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { default_map_reduce_template, default_qa_template, @@ -183,6 +183,7 @@ class ConversationalRetrievalQAChain_Chains implements INode { } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler( @@ -191,7 +192,7 @@ class ConversationalRetrievalQAChain_Chains implements INode { chainOption === 'refine' ? 4 : undefined, returnSourceDocuments ) - const res = await chain.call(obj, [loggerHandler, handler]) + const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) if (chainOption === 'refine') { if (res.output_text && res.sourceDocuments) { return { @@ -204,7 +205,7 @@ class ConversationalRetrievalQAChain_Chains implements INode { if (res.text && res.sourceDocuments) return res return res?.text } else { - const res = await chain.call(obj, [loggerHandler]) + const res = await chain.call(obj, [loggerHandler, ...callbacks]) if (res.text && res.sourceDocuments) return res return res?.text } diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 5088b34d4..63994b132 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -2,7 +2,7 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { LLMChain } from 'langchain/chains' import { BaseLanguageModel } from 'langchain/base_language' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class LLMChain_Chains implements INode { label: string @@ -70,7 +70,7 @@ class LLMChain_Chains implements INode { } else if (output === 'outputPrediction') { const chain = new LLMChain({ llm: model, prompt, verbose: process.env.DEBUG === 'true' ? true : false }) const inputVariables = chain.prompt.inputVariables as string[] // ["product"] - const res = await runPrediction(inputVariables, chain, input, promptValues, options) + const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) // eslint-disable-next-line no-console console.log('\x1b[92m\x1b[1m\n*****OUTPUT PREDICTION*****\n\x1b[0m\x1b[0m') // eslint-disable-next-line no-console @@ -88,7 +88,7 @@ class LLMChain_Chains implements INode { const inputVariables = nodeData.instance.prompt.inputVariables as string[] // ["product"] const chain = nodeData.instance as LLMChain const promptValues = nodeData.inputs?.prompt.promptValues as ICommonObject - const res = await runPrediction(inputVariables, chain, input, promptValues, options) + const res = await runPrediction(inputVariables, chain, input, promptValues, options, nodeData) // eslint-disable-next-line no-console console.log('\x1b[93m\x1b[1m\n*****FINAL RESULT*****\n\x1b[0m\x1b[0m') // eslint-disable-next-line no-console @@ -102,9 +102,12 @@ const runPrediction = async ( chain: LLMChain, input: string, promptValuesRaw: ICommonObject, - options: ICommonObject + options: ICommonObject, + nodeData: INodeData ) => { const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) + const isStreaming = options.socketIO && options.socketIOClientId const socketIO = isStreaming ? options.socketIO : undefined const socketIOClientId = isStreaming ? options.socketIOClientId : '' @@ -131,10 +134,10 @@ const runPrediction = async ( const options = { ...promptValues } if (isStreaming) { const handler = new CustomChainHandler(socketIO, socketIOClientId) - const res = await chain.call(options, [loggerHandler, handler]) + const res = await chain.call(options, [loggerHandler, handler, ...callbacks]) return res?.text } else { - const res = await chain.call(options, [loggerHandler]) + const res = await chain.call(options, [loggerHandler, ...callbacks]) return res?.text } } else if (seen.length === 1) { @@ -147,10 +150,10 @@ const runPrediction = async ( } if (isStreaming) { const handler = new CustomChainHandler(socketIO, socketIOClientId) - const res = await chain.call(options, [loggerHandler, handler]) + const res = await chain.call(options, [loggerHandler, handler, ...callbacks]) return res?.text } else { - const res = await chain.call(options, [loggerHandler]) + const res = await chain.call(options, [loggerHandler, ...callbacks]) return res?.text } } else { @@ -159,10 +162,10 @@ const runPrediction = async ( } else { if (isStreaming) { const handler = new CustomChainHandler(socketIO, socketIOClientId) - const res = await chain.run(input, [loggerHandler, handler]) + const res = await chain.run(input, [loggerHandler, handler, ...callbacks]) return res } else { - const res = await chain.run(input, [loggerHandler]) + const res = await chain.run(input, [loggerHandler, ...callbacks]) return res } } diff --git a/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts b/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts index 0d1377143..d171e5140 100644 --- a/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts +++ b/packages/components/nodes/chains/MultiPromptChain/MultiPromptChain.ts @@ -2,7 +2,7 @@ import { BaseLanguageModel } from 'langchain/base_language' import { ICommonObject, INode, INodeData, INodeParams, PromptRetriever } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' import { MultiPromptChain } from 'langchain/chains' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class MultiPromptChain_Chains implements INode { label: string @@ -67,13 +67,14 @@ class MultiPromptChain_Chains implements INode { const obj = { input } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) - const res = await chain.call(obj, [loggerHandler, handler]) + const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) return res?.text } else { - const res = await chain.call(obj, [loggerHandler]) + const res = await chain.call(obj, [loggerHandler, ...callbacks]) return res?.text } } diff --git a/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts b/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts index 6d1506475..93bf2255a 100644 --- a/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts +++ b/packages/components/nodes/chains/MultiRetrievalQAChain/MultiRetrievalQAChain.ts @@ -2,7 +2,7 @@ import { BaseLanguageModel } from 'langchain/base_language' import { ICommonObject, INode, INodeData, INodeParams, VectorStoreRetriever } from '../../../src/Interface' import { getBaseClasses } from '../../../src/utils' import { MultiRetrievalQAChain } from 'langchain/chains' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class MultiRetrievalQAChain_Chains implements INode { label: string @@ -75,14 +75,15 @@ class MultiRetrievalQAChain_Chains implements INode { const obj = { input } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2, returnSourceDocuments) - const res = await chain.call(obj, [loggerHandler, handler]) + const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) if (res.text && res.sourceDocuments) return res return res?.text } else { - const res = await chain.call(obj, [loggerHandler]) + const res = await chain.call(obj, [loggerHandler, ...callbacks]) if (res.text && res.sourceDocuments) return res return res?.text } diff --git a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts index 935866ca2..bff2a0a78 100644 --- a/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts +++ b/packages/components/nodes/chains/RetrievalQAChain/RetrievalQAChain.ts @@ -3,7 +3,7 @@ import { RetrievalQAChain } from 'langchain/chains' import { BaseRetriever } from 'langchain/schema/retriever' import { getBaseClasses } from '../../../src/utils' import { BaseLanguageModel } from 'langchain/base_language' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class RetrievalQAChain_Chains implements INode { label: string @@ -53,13 +53,14 @@ class RetrievalQAChain_Chains implements INode { query: input } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.call(obj, [loggerHandler, handler]) + const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) return res?.text } else { - const res = await chain.call(obj, [loggerHandler]) + const res = await chain.call(obj, [loggerHandler, ...callbacks]) return res?.text } } diff --git a/packages/components/nodes/chains/SqlDatabaseChain/SqlDatabaseChain.ts b/packages/components/nodes/chains/SqlDatabaseChain/SqlDatabaseChain.ts index f5fd0cccb..5d270fd8e 100644 --- a/packages/components/nodes/chains/SqlDatabaseChain/SqlDatabaseChain.ts +++ b/packages/components/nodes/chains/SqlDatabaseChain/SqlDatabaseChain.ts @@ -5,7 +5,7 @@ import { DataSource } from 'typeorm' import { SqlDatabase } from 'langchain/sql_db' import { BaseLanguageModel } from 'langchain/base_language' import { PromptTemplate, PromptTemplateInput } from 'langchain/prompts' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' import { DataSourceOptions } from 'typeorm/data-source' type DatabaseType = 'sqlite' | 'postgres' | 'mssql' | 'mysql' @@ -184,13 +184,14 @@ class SqlDatabaseChain_Chains implements INode { customPrompt ) const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) - const res = await chain.run(input, [loggerHandler, handler]) + const res = await chain.run(input, [loggerHandler, handler, ...callbacks]) return res } else { - const res = await chain.run(input, [loggerHandler]) + const res = await chain.run(input, [loggerHandler, ...callbacks]) return res } } diff --git a/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts b/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts index 038116827..6c9447d3b 100644 --- a/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts +++ b/packages/components/nodes/chains/VectorDBQAChain/VectorDBQAChain.ts @@ -3,7 +3,7 @@ import { getBaseClasses } from '../../../src/utils' import { VectorDBQAChain } from 'langchain/chains' import { BaseLanguageModel } from 'langchain/base_language' import { VectorStore } from 'langchain/vectorstores' -import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' class VectorDBQAChain_Chains implements INode { label: string @@ -57,13 +57,14 @@ class VectorDBQAChain_Chains implements INode { } const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) if (options.socketIO && options.socketIOClientId) { const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) - const res = await chain.call(obj, [loggerHandler, handler]) + const res = await chain.call(obj, [loggerHandler, handler, ...callbacks]) return res?.text } else { - const res = await chain.call(obj, [loggerHandler]) + const res = await chain.call(obj, [loggerHandler, ...callbacks]) return res?.text } } diff --git a/packages/components/package.json b/packages/components/package.json index d3dd4e532..6e522a5d6 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -42,7 +42,9 @@ "google-auth-library": "^9.0.0", "graphql": "^16.6.0", "html-to-text": "^9.0.5", - "langchain": "^0.0.133", + "langchain": "^0.0.145", + "langfuse-langchain": "^1.0.14-alpha.0", + "langsmith": "^0.0.32", "linkifyjs": "^4.1.1", "mammoth": "^1.5.1", "moment": "^2.29.3", diff --git a/packages/components/src/handler.ts b/packages/components/src/handler.ts index 8e3633617..f13719ce6 100644 --- a/packages/components/src/handler.ts +++ b/packages/components/src/handler.ts @@ -2,6 +2,11 @@ import { BaseTracer, Run, BaseCallbackHandler } from 'langchain/callbacks' import { AgentAction, ChainValues } from 'langchain/schema' import { Logger } from 'winston' import { Server } from 'socket.io' +import { Client } from 'langsmith' +import { LangChainTracer } from 'langchain/callbacks' +import { getCredentialData, getCredentialParam } from './utils' +import { ICommonObject, INodeData } from './Interface' +import CallbackHandler from 'langfuse-langchain' interface AgentRun extends Run { actions: AgentAction[] @@ -178,3 +183,65 @@ export class CustomChainHandler extends BaseCallbackHandler { } } } + +export const additionalCallbacks = async (nodeData: INodeData, options: ICommonObject) => { + try { + if (!options.analytic) return [] + + const analytic = JSON.parse(options.analytic) + const callbacks: any = [] + + for (const provider in analytic) { + const providerStatus = analytic[provider].status as boolean + if (providerStatus) { + if (provider === 'langSmith') { + const credentialId = analytic[provider].credentialId as string + const langSmithProject = analytic[provider].projectName as string + + const credentialData = await getCredentialData(credentialId ?? '', options) + const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, nodeData) + const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, nodeData) + + const client = new Client({ + apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com', + apiKey: langSmithApiKey + }) + + const tracer = new LangChainTracer({ + projectName: langSmithProject ?? 'default', + //@ts-ignore + client + }) + callbacks.push(tracer) + } else if (provider === 'langFuse') { + const credentialId = analytic[provider].credentialId as string + const flushAt = analytic[provider].flushAt as string + const flushInterval = analytic[provider].flushInterval as string + const requestTimeout = analytic[provider].requestTimeout as string + const release = analytic[provider].release as string + + const credentialData = await getCredentialData(credentialId ?? '', options) + const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, nodeData) + const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, nodeData) + const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, nodeData) + + const langFuseOptions: ICommonObject = { + secretKey: langFuseSecretKey, + publicKey: langFusePublicKey, + baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com' + } + if (flushAt) langFuseOptions.flushAt = parseInt(flushAt, 10) + if (flushInterval) langFuseOptions.flushInterval = parseInt(flushInterval, 10) + if (requestTimeout) langFuseOptions.requestTimeout = parseInt(requestTimeout, 10) + if (release) langFuseOptions.release = release + + const handler = new CallbackHandler(langFuseOptions) + callbacks.push(handler) + } + } + } + return callbacks + } catch (e) { + throw new Error(e) + } +} diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 42c1231ad..58740b864 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -14,6 +14,7 @@ export interface IChatFlow { deployed?: boolean isPublic?: boolean apikeyid?: string + analytic?: string chatbotConfig?: string apiConfig?: any } diff --git a/packages/server/src/NodesPool.ts b/packages/server/src/NodesPool.ts index 62db41baf..f4681d4ab 100644 --- a/packages/server/src/NodesPool.ts +++ b/packages/server/src/NodesPool.ts @@ -15,7 +15,7 @@ export class NodesPool { */ async initialize() { await this.initializeNodes() - await this.initializeCrdentials() + await this.initializeCredentials() } /** @@ -34,8 +34,6 @@ export class NodesPool { const newNodeInstance = new nodeModule.nodeClass() newNodeInstance.filePath = file - this.componentNodes[newNodeInstance.name] = newNodeInstance - // Replace file icon with absolute path if ( newNodeInstance.icon && @@ -46,7 +44,7 @@ export class NodesPool { const filePath = file.replace(/\\/g, '/').split('/') filePath.pop() const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}` - this.componentNodes[newNodeInstance.name].icon = nodeIconAbsolutePath + newNodeInstance.icon = nodeIconAbsolutePath // Store icon path for componentCredentials if (newNodeInstance.credential) { @@ -55,6 +53,11 @@ export class NodesPool { } } } + + const skipCategories = ['Analytic'] + if (!skipCategories.includes(newNodeInstance.category)) { + this.componentNodes[newNodeInstance.name] = newNodeInstance + } } } }) @@ -64,7 +67,7 @@ export class NodesPool { /** * Initialize credentials */ - private async initializeCrdentials() { + private async initializeCredentials() { const packagePath = getNodeModulesPackagePath('flowise-components') const nodesPath = path.join(packagePath, 'dist', 'credentials') const nodeFiles = await this.getFiles(nodesPath) diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 29f58e5cd..376a100b4 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -28,6 +28,9 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true, type: 'text' }) apiConfig?: string + @Column({ nullable: true, type: 'text' }) + analytic?: string + @CreateDateColumn() createdDate: Date diff --git a/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts b/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts new file mode 100644 index 000000000..a5e088faf --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAnalytic1694432361423 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`analytic\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`analytic\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index e5c167732..ea26789ef 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -4,6 +4,7 @@ import { ModifyChatMessage1693999022236 } from './1693999022236-ModifyChatMessag import { ModifyCredential1693999261583 } from './1693999261583-ModifyCredential' import { ModifyTool1694001465232 } from './1694001465232-ModifyTool' import { AddApiConfig1694099200729 } from './1694099200729-AddApiConfig' +import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' export const mysqlMigrations = [ Init1693840429259, @@ -11,5 +12,6 @@ export const mysqlMigrations = [ ModifyChatMessage1693999022236, ModifyCredential1693999261583, ModifyTool1694001465232, - AddApiConfig1694099200729 + AddApiConfig1694099200729, + AddAnalytic1694432361423 ] diff --git a/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts b/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts new file mode 100644 index 000000000..ed83c833a --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAnalytic1694432361423 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "analytic" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "analytic";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 04579cd6d..4a78556b0 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -4,6 +4,7 @@ import { ModifyChatMessage1693996694528 } from './1693996694528-ModifyChatMessag import { ModifyCredential1693997070000 } from './1693997070000-ModifyCredential' import { ModifyTool1693997339912 } from './1693997339912-ModifyTool' import { AddApiConfig1694099183389 } from './1694099183389-AddApiConfig' +import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' export const postgresMigrations = [ Init1693891895163, @@ -11,5 +12,6 @@ export const postgresMigrations = [ ModifyChatMessage1693996694528, ModifyCredential1693997070000, ModifyTool1693997339912, - AddApiConfig1694099183389 + AddApiConfig1694099183389, + AddAnalytic1694432361423 ] diff --git a/packages/server/src/database/migrations/sqlite/1694432361423-AddAnalytic.ts b/packages/server/src/database/migrations/sqlite/1694432361423-AddAnalytic.ts new file mode 100644 index 000000000..ed83c833a --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1694432361423-AddAnalytic.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAnalytic1694432361423 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "analytic" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "analytic";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 234dd220b..bff926b0a 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -4,6 +4,7 @@ import { ModifyChatMessage1693921865247 } from './1693921865247-ModifyChatMessag import { ModifyCredential1693923551694 } from './1693923551694-ModifyCredential' import { ModifyTool1693924207475 } from './1693924207475-ModifyTool' import { AddApiConfig1694090982460 } from './1694090982460-AddApiConfig' +import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' export const sqliteMigrations = [ Init1693835579790, @@ -11,5 +12,6 @@ export const sqliteMigrations = [ ModifyChatMessage1693921865247, ModifyCredential1693923551694, ModifyTool1693924207475, - AddApiConfig1694090982460 + AddApiConfig1694090982460, + AddAnalytic1694432361423 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index af6701c09..db5ecf384 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -980,13 +980,15 @@ export class App { socketIOClientId: incomingInput.socketIOClientId, logger, appDataSource: this.AppDataSource, - databaseEntities + databaseEntities, + analytic: chatflow.analytic }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, - databaseEntities + databaseEntities, + analytic: chatflow.analytic }) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 73c748a37..b1f7e5a20 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -448,9 +448,10 @@ export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: // If overrideConfig[key] is object if (overrideConfig[config] && typeof overrideConfig[config] === 'object') { const nodeIds = Object.keys(overrideConfig[config]) - if (!nodeIds.includes(flowNodeData.id)) continue - else paramsObj[config] = overrideConfig[config][flowNodeData.id] - continue + if (nodeIds.includes(flowNodeData.id)) { + paramsObj[config] = overrideConfig[config][flowNodeData.id] + continue + } } let paramValue = overrideConfig[config] ?? paramsObj[config] @@ -877,12 +878,14 @@ export const decryptCredentialData = async ( * @returns {Credential} */ export const transformToCredentialEntity = async (body: ICredentialReqBody): Promise => { - const encryptedData = await encryptCredentialData(body.plainDataObj) - - const credentialBody = { + const credentialBody: ICommonObject = { name: body.name, - credentialName: body.credentialName, - encryptedData + credentialName: body.credentialName + } + + if (body.plainDataObj) { + const encryptedData = await encryptCredentialData(body.plainDataObj) + credentialBody.encryptedData = encryptedData } const newCredential = new Credential() diff --git a/packages/ui/src/assets/images/langchain.png b/packages/ui/src/assets/images/langchain.png new file mode 100644 index 000000000..587dd3140 Binary files /dev/null and b/packages/ui/src/assets/images/langchain.png differ diff --git a/packages/ui/src/assets/images/langfuse.png b/packages/ui/src/assets/images/langfuse.png new file mode 100644 index 000000000..df9181b8d Binary files /dev/null and b/packages/ui/src/assets/images/langfuse.png differ diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 77b9ebf5d..038dc7404 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,8 +1,8 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport, IconCopy } from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch } from '@tabler/icons' // constant -const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy } +const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -32,6 +32,13 @@ const settings = { url: '', icon: icons.IconFileExport }, + { + id: 'analyseChatflow', + title: 'Analyse Chatflow', + type: 'item', + url: '', + icon: icons.IconSearch + }, { id: 'deleteChatflow', title: 'Delete Chatflow', diff --git a/packages/ui/src/store/constant.js b/packages/ui/src/store/constant.js index c0fce49db..43d7fe8be 100644 --- a/packages/ui/src/store/constant.js +++ b/packages/ui/src/store/constant.js @@ -6,3 +6,4 @@ export const maxScroll = 100000 export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000') export const uiBaseURL = window.location.origin export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID' +export const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db' diff --git a/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js b/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js new file mode 100644 index 000000000..1ed68fb58 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.js @@ -0,0 +1,358 @@ +import { createPortal } from 'react-dom' +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' + +// material-ui +import { + Typography, + Box, + Button, + Dialog, + DialogContent, + DialogTitle, + DialogActions, + Accordion, + AccordionSummary, + AccordionDetails, + ListItem, + ListItemAvatar, + ListItemText +} from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { IconX } from '@tabler/icons' + +// Project import +import CredentialInputHandler from 'views/canvas/CredentialInputHandler' +import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' +import { SwitchInput } from 'ui-component/switch/Switch' +import { Input } from 'ui-component/input/Input' +import { StyledButton } from 'ui-component/button/StyledButton' +import langsmithPNG from 'assets/images/langchain.png' +import langfusePNG from 'assets/images/langfuse.png' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +import useNotifier from 'utils/useNotifier' + +// API +import chatflowsApi from 'api/chatflows' + +const analyticProviders = [ + { + label: 'LangSmith', + name: 'langSmith', + icon: langsmithPNG, + url: 'https://smith.langchain.com', + inputs: [ + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['langsmithApi'] + }, + { + label: 'Project Name', + name: 'projectName', + type: 'string', + optional: true, + description: 'If not provided, default will be used', + placeholder: 'default' + }, + { + label: 'On/Off', + name: 'status', + type: 'boolean', + optional: true + } + ] + }, + { + label: 'LangFuse', + name: 'langFuse', + icon: langfusePNG, + url: 'https://langfuse.com', + inputs: [ + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['langfuseApi'] + }, + { + label: 'Flush At', + name: 'flushAt', + type: 'number', + optional: true, + description: 'Number of queued requests' + }, + { + label: 'Flush Interval', + name: 'flushInterval', + type: 'number', + optional: true, + description: 'Interval in ms to flush requests' + }, + { + label: 'Request Timeout', + name: 'requestTimeout', + type: 'number', + optional: true, + description: 'Timeout in ms for requests' + }, + { + label: 'Release', + name: 'release', + type: 'string', + optional: true, + description: 'The release number/hash of the application to provide analytics grouped by release' + }, + { + label: 'On/Off', + name: 'status', + type: 'boolean', + optional: true + } + ] + } +] + +const AnalyseFlowDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [analytic, setAnalytic] = useState({}) + const [providerExpanded, setProviderExpanded] = useState({}) + + const onSave = async () => { + try { + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + analytic: JSON.stringify(analytic) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Analytic Configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + onCancel() + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Analytic Configuration: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const setValue = (value, providerName, inputParamName) => { + let newVal = {} + if (!Object.prototype.hasOwnProperty.call(analytic, providerName)) { + newVal = { ...analytic, [providerName]: {} } + } else { + newVal = { ...analytic } + } + + newVal[providerName][inputParamName] = value + setAnalytic(newVal) + } + + const handleAccordionChange = (providerName) => (event, isExpanded) => { + const accordianProviders = { ...providerExpanded } + accordianProviders[providerName] = isExpanded + setProviderExpanded(accordianProviders) + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.analytic) { + try { + setAnalytic(JSON.parse(dialogProps.chatflow.analytic)) + } catch (e) { + setAnalytic({}) + console.error(e) + } + } + + return () => { + setAnalytic({}) + setProviderExpanded({}) + } + }, [dialogProps]) + + useEffect(() => { + if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) + else dispatch({ type: HIDE_CANVAS_DIALOG }) + return () => dispatch({ type: HIDE_CANVAS_DIALOG }) + }, [show, dispatch]) + + const component = show ? ( + + + Analyse Chatflow + + + {analyticProviders.map((provider, index) => ( + + } aria-controls={provider.name} id={provider.name}> + + +
+ AI +
+
+ + {provider.url} + + } + /> + {analytic[provider.name] && analytic[provider.name].status && ( +
+
+ ON +
+ )} + + + + {provider.inputs.map((inputParam, index) => ( + +
+ + {inputParam.label} + {!inputParam.optional &&  *} + {inputParam.description && ( + + )} + +
+ {providerExpanded[provider.name] && inputParam.type === 'credential' && ( + setValue(newValue, provider.name, 'credentialId')} + /> + )} + {providerExpanded[provider.name] && inputParam.type === 'boolean' && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + analytic[provider.name] + ? analytic[provider.name][inputParam.name] + : inputParam.default ?? false + } + /> + )} + {providerExpanded[provider.name] && + (inputParam.type === 'string' || + inputParam.type === 'password' || + inputParam.type === 'number') && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + analytic[provider.name] + ? analytic[provider.name][inputParam.name] + : inputParam.default ?? '' + } + /> + )} +
+ ))} +
+ + ))} + + + + Save + + +
+ ) : null + + return createPortal(component, portalElement) +} + +AnalyseFlowDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default AnalyseFlowDialog diff --git a/packages/ui/src/ui-component/switch/Switch.js b/packages/ui/src/ui-component/switch/Switch.js index 04ea17048..16a923f1c 100644 --- a/packages/ui/src/ui-component/switch/Switch.js +++ b/packages/ui/src/ui-component/switch/Switch.js @@ -22,7 +22,7 @@ export const SwitchInput = ({ value, onChange, disabled = false }) => { } SwitchInput.propTypes = { - value: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), onChange: PropTypes.func, disabled: PropTypes.bool } diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index d25635325..a9d2f39ec 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -14,6 +14,7 @@ import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, import Settings from 'views/settings' import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog' import APICodeDialog from 'views/chatflows/APICodeDialog' +import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog' // API import chatflowsApi from 'api/chatflows' @@ -41,6 +42,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [flowDialogOpen, setFlowDialogOpen] = useState(false) const [apiDialogOpen, setAPIDialogOpen] = useState(false) const [apiDialogProps, setAPIDialogProps] = useState({}) + const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false) + const [analyseDialogProps, setAnalyseDialogProps] = useState({}) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const canvas = useSelector((state) => state.canvas) @@ -50,6 +53,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl if (setting === 'deleteChatflow') { handleDeleteFlow() + } else if (setting === 'analyseChatflow') { + setAnalyseDialogProps({ + title: 'Analyse Chatflow', + chatflow: chatflow + }) + setAnalyseDialogOpen(true) } else if (setting === 'duplicateChatflow') { try { localStorage.setItem('duplicatedFlowData', chatflow.flowData) @@ -357,6 +366,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl onConfirm={onConfirmSaveName} /> setAPIDialogOpen(false)} /> + setAnalyseDialogOpen(false)} /> ) } diff --git a/packages/ui/src/views/credentials/AddEditCredentialDialog.js b/packages/ui/src/views/credentials/AddEditCredentialDialog.js index 65b72a5fa..b865a7ff3 100644 --- a/packages/ui/src/views/credentials/AddEditCredentialDialog.js +++ b/packages/ui/src/views/credentials/AddEditCredentialDialog.js @@ -26,7 +26,7 @@ import useApi from 'hooks/useApi' import useNotifier from 'utils/useNotifier' // const -import { baseURL } from 'store/constant' +import { baseURL, REDACTED_CREDENTIAL_VALUE } from 'store/constant' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => { @@ -118,7 +118,7 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => onConfirm(createResp.data.id) } } catch (error) { - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + const errorData = typeof err === 'string' ? err : err.response.data || `${err.response.status}: ${err.response.statusText}` enqueueSnackbar({ message: `Failed to add new Credential: ${errorData}`, options: { @@ -138,11 +138,20 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => const saveCredential = async () => { try { - const saveResp = await credentialsApi.updateCredential(credential.id, { + const saveObj = { name, - credentialName: componentCredential.name, - plainDataObj: credentialData - }) + credentialName: componentCredential.name + } + + let plainDataObj = {} + for (const key in credentialData) { + if (credentialData[key] !== REDACTED_CREDENTIAL_VALUE) { + plainDataObj[key] = credentialData[key] + } + } + if (Object.keys(plainDataObj).length) saveObj.plainDataObj = plainDataObj + + const saveResp = await credentialsApi.updateCredential(credential.id, saveObj) if (saveResp.data) { enqueueSnackbar({ message: 'Credential saved',