Merge pull request #908 from FlowiseAI/feature/Analytic

Feature/Add analytic
This commit is contained in:
Henry Heng 2023-09-13 08:46:24 +01:00 committed by GitHub
commit 3d35536de5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 742 additions and 82 deletions

View File

@ -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 <a target="_blank" href="https://langfuse.com/docs/get-started/">official guide</a> 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 }

View File

@ -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 <a target="_blank" href="https://docs.smith.langchain.com/">official guide</a> on how to get API key on Langsmith'
this.inputs = [
{
label: 'API Key',
name: 'langSmithApiKey',
type: 'password',
placeholder: '<LANGSMITH_API_KEY>'
},
{
label: 'Endpoint',
name: 'langSmithEndpoint',
type: 'string',
default: 'https://api.smith.langchain.com'
}
]
}
}
module.exports = { credClass: LangsmithApi }

View File

@ -4,7 +4,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../
import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core' import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core'
import { LLMChain } from 'langchain/chains' import { LLMChain } from 'langchain/chains'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
import axios from 'axios' import axios from 'axios'
class Airtable_Agents implements INode { class Airtable_Agents implements INode {
@ -102,6 +102,7 @@ class Airtable_Agents implements INode {
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
const callbacks = await additionalCallbacks(nodeData, options)
const pyodide = await LoadPyodide() const pyodide = await LoadPyodide()
@ -141,7 +142,7 @@ json.dumps(my_dict)`
dict: dataframeColDict, dict: dataframeColDict,
question: input question: input
} }
const res = await chain.call(inputs, [loggerHandler]) const res = await chain.call(inputs, [loggerHandler, ...callbacks])
pythonCode = res?.text pythonCode = res?.text
} }
@ -169,10 +170,10 @@ json.dumps(my_dict)`
} }
if (options.socketIO && options.socketIOClientId) { 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 return result?.text
} else { } else {
const result = await chain.call(inputs, [loggerHandler]) const result = await chain.call(inputs, [loggerHandler, ...callbacks])
return result?.text return result?.text
} }
} }

View File

@ -4,7 +4,7 @@ import { getBaseClasses } from '../../../src/utils'
import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core' import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core'
import { LLMChain } from 'langchain/chains' import { LLMChain } from 'langchain/chains'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class CSV_Agents implements INode { class CSV_Agents implements INode {
label: string label: string
@ -63,6 +63,7 @@ class CSV_Agents implements INode {
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
const callbacks = await additionalCallbacks(nodeData, options)
let files: string[] = [] let files: string[] = []
@ -119,7 +120,7 @@ json.dumps(my_dict)`
dict: dataframeColDict, dict: dataframeColDict,
question: input question: input
} }
const res = await chain.call(inputs, [loggerHandler]) const res = await chain.call(inputs, [loggerHandler, ...callbacks])
pythonCode = res?.text pythonCode = res?.text
} }
@ -149,10 +150,10 @@ json.dumps(my_dict)`
} }
if (options.socketIO && options.socketIOClientId) { 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 return result?.text
} else { } else {
const result = await chain.call(inputs, [loggerHandler]) const result = await chain.call(inputs, [loggerHandler, ...callbacks])
return result?.text return result?.text
} }
} }

View File

@ -3,7 +3,7 @@ import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/age
import { getBaseClasses, mapChatHistory } from '../../../src/utils' import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { flatten } from 'lodash' import { flatten } from 'lodash'
import { BaseChatMemory } from 'langchain/memory' 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.` 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 loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(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 return result?.output
} else { } else {
const result = await executor.call({ input }, [loggerHandler]) const result = await executor.call({ input }, [loggerHandler, ...callbacks])
return result?.output return result?.output
} }
} }

View File

@ -4,7 +4,7 @@ import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { flatten } from 'lodash' import { flatten } from 'lodash'
import { BaseChatMemory } from 'langchain/memory' import { BaseChatMemory } from 'langchain/memory'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class OpenAIFunctionAgent_Agents implements INode { class OpenAIFunctionAgent_Agents implements INode {
label: string label: string
@ -86,13 +86,14 @@ class OpenAIFunctionAgent_Agents implements INode {
} }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(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 return result
} else { } else {
const result = await executor.run(input, [loggerHandler]) const result = await executor.run(input, [loggerHandler, ...callbacks])
return result return result
} }
} }

View File

@ -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 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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 }

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -3,7 +3,7 @@ import { APIChain } from 'langchain/chains'
import { getBaseClasses } from '../../../src/utils' import { getBaseClasses } from '../../../src/utils'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { PromptTemplate } from 'langchain/prompts' 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: export const API_URL_RAW_PROMPT_TEMPLATE = `You are given the below API Documentation:
{api_docs} {api_docs}
@ -99,13 +99,14 @@ class GETApiChain_Chains implements INode {
const chain = await getAPIChain(apiDocs, model, headers, urlPrompt, ansPrompt) const chain = await getAPIChain(apiDocs, model, headers, urlPrompt, ansPrompt)
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) 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 return res
} else { } else {
const res = await chain.run(input, [loggerHandler]) const res = await chain.run(input, [loggerHandler, ...callbacks])
return res return res
} }
} }

View File

@ -2,7 +2,7 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter
import { APIChain, createOpenAPIChain } from 'langchain/chains' import { APIChain, createOpenAPIChain } from 'langchain/chains'
import { getBaseClasses } from '../../../src/utils' import { getBaseClasses } from '../../../src/utils'
import { ChatOpenAI } from 'langchain/chat_models/openai' 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 { class OpenApiChain_Chains implements INode {
label: string label: string
@ -61,13 +61,14 @@ class OpenApiChain_Chains implements INode {
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> { async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const chain = await initChain(nodeData) const chain = await initChain(nodeData)
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(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 return res
} else { } else {
const res = await chain.run(input, [loggerHandler]) const res = await chain.run(input, [loggerHandler, ...callbacks])
return res return res
} }
} }

View File

@ -3,7 +3,7 @@ import { getBaseClasses } from '../../../src/utils'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { PromptTemplate } from 'langchain/prompts' import { PromptTemplate } from 'langchain/prompts'
import { API_RESPONSE_RAW_PROMPT_TEMPLATE, API_URL_RAW_PROMPT_TEMPLATE, APIChain } from './postCore' 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 { class POSTApiChain_Chains implements INode {
label: string label: string
@ -88,13 +88,14 @@ class POSTApiChain_Chains implements INode {
const chain = await getAPIChain(apiDocs, model, headers, urlPrompt, ansPrompt) const chain = await getAPIChain(apiDocs, model, headers, urlPrompt, ansPrompt)
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) 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 return res
} else { } else {
const res = await chain.run(input, [loggerHandler]) const res = await chain.run(input, [loggerHandler, ...callbacks])
return res return res
} }
} }

View File

@ -4,7 +4,7 @@ import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts' import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts'
import { BufferMemory } from 'langchain/memory' import { BufferMemory } from 'langchain/memory'
import { BaseChatModel } from 'langchain/chat_models/base' 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 { flatten } from 'lodash'
import { Document } from 'langchain/document' import { Document } from 'langchain/document'
@ -111,13 +111,14 @@ class ConversationChain_Chains implements INode {
} }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(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 return res?.response
} else { } else {
const res = await chain.call({ input }, [loggerHandler]) const res = await chain.call({ input }, [loggerHandler, ...callbacks])
return res?.response return res?.response
} }
} }

View File

@ -5,7 +5,7 @@ import { ConversationalRetrievalQAChain, QAChainParams } from 'langchain/chains'
import { BaseRetriever } from 'langchain/schema/retriever' import { BaseRetriever } from 'langchain/schema/retriever'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
import { PromptTemplate } from 'langchain/prompts' import { PromptTemplate } from 'langchain/prompts'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
import { import {
default_map_reduce_template, default_map_reduce_template,
default_qa_template, default_qa_template,
@ -183,6 +183,7 @@ class ConversationalRetrievalQAChain_Chains implements INode {
} }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler( const handler = new CustomChainHandler(
@ -191,7 +192,7 @@ class ConversationalRetrievalQAChain_Chains implements INode {
chainOption === 'refine' ? 4 : undefined, chainOption === 'refine' ? 4 : undefined,
returnSourceDocuments returnSourceDocuments
) )
const res = await chain.call(obj, [loggerHandler, handler]) const res = await chain.call(obj, [loggerHandler, handler, ...callbacks])
if (chainOption === 'refine') { if (chainOption === 'refine') {
if (res.output_text && res.sourceDocuments) { if (res.output_text && res.sourceDocuments) {
return { return {
@ -204,7 +205,7 @@ class ConversationalRetrievalQAChain_Chains implements INode {
if (res.text && res.sourceDocuments) return res if (res.text && res.sourceDocuments) return res
return res?.text return res?.text
} else { } else {
const res = await chain.call(obj, [loggerHandler]) const res = await chain.call(obj, [loggerHandler, ...callbacks])
if (res.text && res.sourceDocuments) return res if (res.text && res.sourceDocuments) return res
return res?.text return res?.text
} }

View File

@ -2,7 +2,7 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from
import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils' import { getBaseClasses, handleEscapeCharacters } from '../../../src/utils'
import { LLMChain } from 'langchain/chains' import { LLMChain } from 'langchain/chains'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class LLMChain_Chains implements INode { class LLMChain_Chains implements INode {
label: string label: string
@ -70,7 +70,7 @@ class LLMChain_Chains implements INode {
} else if (output === 'outputPrediction') { } else if (output === 'outputPrediction') {
const chain = new LLMChain({ llm: model, prompt, verbose: process.env.DEBUG === 'true' ? true : false }) const chain = new LLMChain({ llm: model, prompt, verbose: process.env.DEBUG === 'true' ? true : false })
const inputVariables = chain.prompt.inputVariables as string[] // ["product"] 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 // eslint-disable-next-line no-console
console.log('\x1b[92m\x1b[1m\n*****OUTPUT PREDICTION*****\n\x1b[0m\x1b[0m') console.log('\x1b[92m\x1b[1m\n*****OUTPUT PREDICTION*****\n\x1b[0m\x1b[0m')
// eslint-disable-next-line no-console // 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 inputVariables = nodeData.instance.prompt.inputVariables as string[] // ["product"]
const chain = nodeData.instance as LLMChain const chain = nodeData.instance as LLMChain
const promptValues = nodeData.inputs?.prompt.promptValues as ICommonObject 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 // eslint-disable-next-line no-console
console.log('\x1b[93m\x1b[1m\n*****FINAL RESULT*****\n\x1b[0m\x1b[0m') console.log('\x1b[93m\x1b[1m\n*****FINAL RESULT*****\n\x1b[0m\x1b[0m')
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -102,9 +102,12 @@ const runPrediction = async (
chain: LLMChain, chain: LLMChain,
input: string, input: string,
promptValuesRaw: ICommonObject, promptValuesRaw: ICommonObject,
options: ICommonObject options: ICommonObject,
nodeData: INodeData
) => { ) => {
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
const isStreaming = options.socketIO && options.socketIOClientId const isStreaming = options.socketIO && options.socketIOClientId
const socketIO = isStreaming ? options.socketIO : undefined const socketIO = isStreaming ? options.socketIO : undefined
const socketIOClientId = isStreaming ? options.socketIOClientId : '' const socketIOClientId = isStreaming ? options.socketIOClientId : ''
@ -131,10 +134,10 @@ const runPrediction = async (
const options = { ...promptValues } const options = { ...promptValues }
if (isStreaming) { if (isStreaming) {
const handler = new CustomChainHandler(socketIO, socketIOClientId) 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 return res?.text
} else { } else {
const res = await chain.call(options, [loggerHandler]) const res = await chain.call(options, [loggerHandler, ...callbacks])
return res?.text return res?.text
} }
} else if (seen.length === 1) { } else if (seen.length === 1) {
@ -147,10 +150,10 @@ const runPrediction = async (
} }
if (isStreaming) { if (isStreaming) {
const handler = new CustomChainHandler(socketIO, socketIOClientId) 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 return res?.text
} else { } else {
const res = await chain.call(options, [loggerHandler]) const res = await chain.call(options, [loggerHandler, ...callbacks])
return res?.text return res?.text
} }
} else { } else {
@ -159,10 +162,10 @@ const runPrediction = async (
} else { } else {
if (isStreaming) { if (isStreaming) {
const handler = new CustomChainHandler(socketIO, socketIOClientId) 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 return res
} else { } else {
const res = await chain.run(input, [loggerHandler]) const res = await chain.run(input, [loggerHandler, ...callbacks])
return res return res
} }
} }

View File

@ -2,7 +2,7 @@ import { BaseLanguageModel } from 'langchain/base_language'
import { ICommonObject, INode, INodeData, INodeParams, PromptRetriever } from '../../../src/Interface' import { ICommonObject, INode, INodeData, INodeParams, PromptRetriever } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils' import { getBaseClasses } from '../../../src/utils'
import { MultiPromptChain } from 'langchain/chains' import { MultiPromptChain } from 'langchain/chains'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class MultiPromptChain_Chains implements INode { class MultiPromptChain_Chains implements INode {
label: string label: string
@ -67,13 +67,14 @@ class MultiPromptChain_Chains implements INode {
const obj = { input } const obj = { input }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) 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 return res?.text
} else { } else {
const res = await chain.call(obj, [loggerHandler]) const res = await chain.call(obj, [loggerHandler, ...callbacks])
return res?.text return res?.text
} }
} }

View File

@ -2,7 +2,7 @@ import { BaseLanguageModel } from 'langchain/base_language'
import { ICommonObject, INode, INodeData, INodeParams, VectorStoreRetriever } from '../../../src/Interface' import { ICommonObject, INode, INodeData, INodeParams, VectorStoreRetriever } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils' import { getBaseClasses } from '../../../src/utils'
import { MultiRetrievalQAChain } from 'langchain/chains' import { MultiRetrievalQAChain } from 'langchain/chains'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class MultiRetrievalQAChain_Chains implements INode { class MultiRetrievalQAChain_Chains implements INode {
label: string label: string
@ -75,14 +75,15 @@ class MultiRetrievalQAChain_Chains implements INode {
const obj = { input } const obj = { input }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2, returnSourceDocuments) 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 if (res.text && res.sourceDocuments) return res
return res?.text return res?.text
} else { } else {
const res = await chain.call(obj, [loggerHandler]) const res = await chain.call(obj, [loggerHandler, ...callbacks])
if (res.text && res.sourceDocuments) return res if (res.text && res.sourceDocuments) return res
return res?.text return res?.text
} }

View File

@ -3,7 +3,7 @@ import { RetrievalQAChain } from 'langchain/chains'
import { BaseRetriever } from 'langchain/schema/retriever' import { BaseRetriever } from 'langchain/schema/retriever'
import { getBaseClasses } from '../../../src/utils' import { getBaseClasses } from '../../../src/utils'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class RetrievalQAChain_Chains implements INode { class RetrievalQAChain_Chains implements INode {
label: string label: string
@ -53,13 +53,14 @@ class RetrievalQAChain_Chains implements INode {
query: input query: input
} }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(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 return res?.text
} else { } else {
const res = await chain.call(obj, [loggerHandler]) const res = await chain.call(obj, [loggerHandler, ...callbacks])
return res?.text return res?.text
} }
} }

View File

@ -5,7 +5,7 @@ import { DataSource } from 'typeorm'
import { SqlDatabase } from 'langchain/sql_db' import { SqlDatabase } from 'langchain/sql_db'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { PromptTemplate, PromptTemplateInput } from 'langchain/prompts' 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' import { DataSourceOptions } from 'typeorm/data-source'
type DatabaseType = 'sqlite' | 'postgres' | 'mssql' | 'mysql' type DatabaseType = 'sqlite' | 'postgres' | 'mssql' | 'mysql'
@ -184,13 +184,14 @@ class SqlDatabaseChain_Chains implements INode {
customPrompt customPrompt
) )
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId, 2) 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 return res
} else { } else {
const res = await chain.run(input, [loggerHandler]) const res = await chain.run(input, [loggerHandler, ...callbacks])
return res return res
} }
} }

View File

@ -3,7 +3,7 @@ import { getBaseClasses } from '../../../src/utils'
import { VectorDBQAChain } from 'langchain/chains' import { VectorDBQAChain } from 'langchain/chains'
import { BaseLanguageModel } from 'langchain/base_language' import { BaseLanguageModel } from 'langchain/base_language'
import { VectorStore } from 'langchain/vectorstores' import { VectorStore } from 'langchain/vectorstores'
import { ConsoleCallbackHandler, CustomChainHandler } from '../../../src/handler' import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
class VectorDBQAChain_Chains implements INode { class VectorDBQAChain_Chains implements INode {
label: string label: string
@ -57,13 +57,14 @@ class VectorDBQAChain_Chains implements INode {
} }
const loggerHandler = new ConsoleCallbackHandler(options.logger) const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
if (options.socketIO && options.socketIOClientId) { if (options.socketIO && options.socketIOClientId) {
const handler = new CustomChainHandler(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 return res?.text
} else { } else {
const res = await chain.call(obj, [loggerHandler]) const res = await chain.call(obj, [loggerHandler, ...callbacks])
return res?.text return res?.text
} }
} }

View File

@ -42,7 +42,9 @@
"google-auth-library": "^9.0.0", "google-auth-library": "^9.0.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"html-to-text": "^9.0.5", "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", "linkifyjs": "^4.1.1",
"mammoth": "^1.5.1", "mammoth": "^1.5.1",
"moment": "^2.29.3", "moment": "^2.29.3",

View File

@ -2,6 +2,11 @@ import { BaseTracer, Run, BaseCallbackHandler } from 'langchain/callbacks'
import { AgentAction, ChainValues } from 'langchain/schema' import { AgentAction, ChainValues } from 'langchain/schema'
import { Logger } from 'winston' import { Logger } from 'winston'
import { Server } from 'socket.io' 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 { interface AgentRun extends Run {
actions: AgentAction[] 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)
}
}

View File

@ -14,6 +14,7 @@ export interface IChatFlow {
deployed?: boolean deployed?: boolean
isPublic?: boolean isPublic?: boolean
apikeyid?: string apikeyid?: string
analytic?: string
chatbotConfig?: string chatbotConfig?: string
apiConfig?: any apiConfig?: any
} }

View File

@ -15,7 +15,7 @@ export class NodesPool {
*/ */
async initialize() { async initialize() {
await this.initializeNodes() await this.initializeNodes()
await this.initializeCrdentials() await this.initializeCredentials()
} }
/** /**
@ -34,8 +34,6 @@ export class NodesPool {
const newNodeInstance = new nodeModule.nodeClass() const newNodeInstance = new nodeModule.nodeClass()
newNodeInstance.filePath = file newNodeInstance.filePath = file
this.componentNodes[newNodeInstance.name] = newNodeInstance
// Replace file icon with absolute path // Replace file icon with absolute path
if ( if (
newNodeInstance.icon && newNodeInstance.icon &&
@ -46,7 +44,7 @@ export class NodesPool {
const filePath = file.replace(/\\/g, '/').split('/') const filePath = file.replace(/\\/g, '/').split('/')
filePath.pop() filePath.pop()
const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}` const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}`
this.componentNodes[newNodeInstance.name].icon = nodeIconAbsolutePath newNodeInstance.icon = nodeIconAbsolutePath
// Store icon path for componentCredentials // Store icon path for componentCredentials
if (newNodeInstance.credential) { 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 * Initialize credentials
*/ */
private async initializeCrdentials() { private async initializeCredentials() {
const packagePath = getNodeModulesPackagePath('flowise-components') const packagePath = getNodeModulesPackagePath('flowise-components')
const nodesPath = path.join(packagePath, 'dist', 'credentials') const nodesPath = path.join(packagePath, 'dist', 'credentials')
const nodeFiles = await this.getFiles(nodesPath) const nodeFiles = await this.getFiles(nodesPath)

View File

@ -28,6 +28,9 @@ export class ChatFlow implements IChatFlow {
@Column({ nullable: true, type: 'text' }) @Column({ nullable: true, type: 'text' })
apiConfig?: string apiConfig?: string
@Column({ nullable: true, type: 'text' })
analytic?: string
@CreateDateColumn() @CreateDateColumn()
createdDate: Date createdDate: Date

View File

@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddAnalytic1694432361423 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`analytic\` TEXT;`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`analytic\`;`)
}
}

View File

@ -4,6 +4,7 @@ import { ModifyChatMessage1693999022236 } from './1693999022236-ModifyChatMessag
import { ModifyCredential1693999261583 } from './1693999261583-ModifyCredential' import { ModifyCredential1693999261583 } from './1693999261583-ModifyCredential'
import { ModifyTool1694001465232 } from './1694001465232-ModifyTool' import { ModifyTool1694001465232 } from './1694001465232-ModifyTool'
import { AddApiConfig1694099200729 } from './1694099200729-AddApiConfig' import { AddApiConfig1694099200729 } from './1694099200729-AddApiConfig'
import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
export const mysqlMigrations = [ export const mysqlMigrations = [
Init1693840429259, Init1693840429259,
@ -11,5 +12,6 @@ export const mysqlMigrations = [
ModifyChatMessage1693999022236, ModifyChatMessage1693999022236,
ModifyCredential1693999261583, ModifyCredential1693999261583,
ModifyTool1694001465232, ModifyTool1694001465232,
AddApiConfig1694099200729 AddApiConfig1694099200729,
AddAnalytic1694432361423
] ]

View File

@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddAnalytic1694432361423 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "analytic" TEXT;`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "analytic";`)
}
}

View File

@ -4,6 +4,7 @@ import { ModifyChatMessage1693996694528 } from './1693996694528-ModifyChatMessag
import { ModifyCredential1693997070000 } from './1693997070000-ModifyCredential' import { ModifyCredential1693997070000 } from './1693997070000-ModifyCredential'
import { ModifyTool1693997339912 } from './1693997339912-ModifyTool' import { ModifyTool1693997339912 } from './1693997339912-ModifyTool'
import { AddApiConfig1694099183389 } from './1694099183389-AddApiConfig' import { AddApiConfig1694099183389 } from './1694099183389-AddApiConfig'
import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
export const postgresMigrations = [ export const postgresMigrations = [
Init1693891895163, Init1693891895163,
@ -11,5 +12,6 @@ export const postgresMigrations = [
ModifyChatMessage1693996694528, ModifyChatMessage1693996694528,
ModifyCredential1693997070000, ModifyCredential1693997070000,
ModifyTool1693997339912, ModifyTool1693997339912,
AddApiConfig1694099183389 AddApiConfig1694099183389,
AddAnalytic1694432361423
] ]

View File

@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddAnalytic1694432361423 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "analytic" TEXT;`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "analytic";`)
}
}

View File

@ -4,6 +4,7 @@ import { ModifyChatMessage1693921865247 } from './1693921865247-ModifyChatMessag
import { ModifyCredential1693923551694 } from './1693923551694-ModifyCredential' import { ModifyCredential1693923551694 } from './1693923551694-ModifyCredential'
import { ModifyTool1693924207475 } from './1693924207475-ModifyTool' import { ModifyTool1693924207475 } from './1693924207475-ModifyTool'
import { AddApiConfig1694090982460 } from './1694090982460-AddApiConfig' import { AddApiConfig1694090982460 } from './1694090982460-AddApiConfig'
import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
export const sqliteMigrations = [ export const sqliteMigrations = [
Init1693835579790, Init1693835579790,
@ -11,5 +12,6 @@ export const sqliteMigrations = [
ModifyChatMessage1693921865247, ModifyChatMessage1693921865247,
ModifyCredential1693923551694, ModifyCredential1693923551694,
ModifyTool1693924207475, ModifyTool1693924207475,
AddApiConfig1694090982460 AddApiConfig1694090982460,
AddAnalytic1694432361423
] ]

View File

@ -980,13 +980,15 @@ export class App {
socketIOClientId: incomingInput.socketIOClientId, socketIOClientId: incomingInput.socketIOClientId,
logger, logger,
appDataSource: this.AppDataSource, appDataSource: this.AppDataSource,
databaseEntities databaseEntities,
analytic: chatflow.analytic
}) })
: await nodeInstance.run(nodeToExecuteData, incomingInput.question, { : await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
chatHistory: incomingInput.history, chatHistory: incomingInput.history,
logger, logger,
appDataSource: this.AppDataSource, appDataSource: this.AppDataSource,
databaseEntities databaseEntities,
analytic: chatflow.analytic
}) })
logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)

View File

@ -448,10 +448,11 @@ export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig:
// If overrideConfig[key] is object // If overrideConfig[key] is object
if (overrideConfig[config] && typeof overrideConfig[config] === 'object') { if (overrideConfig[config] && typeof overrideConfig[config] === 'object') {
const nodeIds = Object.keys(overrideConfig[config]) const nodeIds = Object.keys(overrideConfig[config])
if (!nodeIds.includes(flowNodeData.id)) continue if (nodeIds.includes(flowNodeData.id)) {
else paramsObj[config] = overrideConfig[config][flowNodeData.id] paramsObj[config] = overrideConfig[config][flowNodeData.id]
continue continue
} }
}
let paramValue = overrideConfig[config] ?? paramsObj[config] let paramValue = overrideConfig[config] ?? paramsObj[config]
// Check if boolean // Check if boolean
@ -877,12 +878,14 @@ export const decryptCredentialData = async (
* @returns {Credential} * @returns {Credential}
*/ */
export const transformToCredentialEntity = async (body: ICredentialReqBody): Promise<Credential> => { export const transformToCredentialEntity = async (body: ICredentialReqBody): Promise<Credential> => {
const encryptedData = await encryptCredentialData(body.plainDataObj) const credentialBody: ICommonObject = {
const credentialBody = {
name: body.name, name: body.name,
credentialName: body.credentialName, credentialName: body.credentialName
encryptedData }
if (body.plainDataObj) {
const encryptedData = await encryptCredentialData(body.plainDataObj)
credentialBody.encryptedData = encryptedData
} }
const newCredential = new Credential() const newCredential = new Credential()

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,8 +1,8 @@
// assets // assets
import { IconTrash, IconFileUpload, IconFileExport, IconCopy } from '@tabler/icons' import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch } from '@tabler/icons'
// constant // constant
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy } const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch }
// ==============================|| SETTINGS MENU ITEMS ||============================== // // ==============================|| SETTINGS MENU ITEMS ||============================== //
@ -32,6 +32,13 @@ const settings = {
url: '', url: '',
icon: icons.IconFileExport icon: icons.IconFileExport
}, },
{
id: 'analyseChatflow',
title: 'Analyse Chatflow',
type: 'item',
url: '',
icon: icons.IconSearch
},
{ {
id: 'deleteChatflow', id: 'deleteChatflow',
title: 'Delete Chatflow', title: 'Delete Chatflow',

View File

@ -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 baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000')
export const uiBaseURL = window.location.origin export const uiBaseURL = window.location.origin
export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID' export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID'
export const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'

View File

@ -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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
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 ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
Analyse Chatflow
</DialogTitle>
<DialogContent>
{analyticProviders.map((provider, index) => (
<Accordion
expanded={providerExpanded[provider.name] || false}
onChange={handleAccordionChange(provider.name)}
disableGutters
key={index}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={provider.name} id={provider.name}>
<ListItem style={{ padding: 0, margin: 0 }} alignItems='center'>
<ListItemAvatar>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 10,
objectFit: 'contain'
}}
alt='AI'
src={provider.icon}
/>
</div>
</ListItemAvatar>
<ListItemText
sx={{ ml: 1 }}
primary={provider.label}
secondary={
<a target='_blank' rel='noreferrer' href={provider.url}>
{provider.url}
</a>
}
/>
{analytic[provider.name] && analytic[provider.name].status && (
<div
style={{
display: 'flex',
flexDirection: 'row',
alignContent: 'center',
alignItems: 'center',
background: '#d8f3dc',
borderRadius: 15,
padding: 5,
paddingLeft: 7,
paddingRight: 7,
marginRight: 10
}}
>
<div
style={{
width: 15,
height: 15,
borderRadius: '50%',
backgroundColor: '#70e000'
}}
/>
<span style={{ color: '#006400', marginLeft: 10 }}>ON</span>
</div>
)}
</ListItem>
</AccordionSummary>
<AccordionDetails>
{provider.inputs.map((inputParam, index) => (
<Box key={index} sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
{inputParam.label}
{!inputParam.optional && <span style={{ color: 'red' }}>&nbsp;*</span>}
{inputParam.description && (
<TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />
)}
</Typography>
</div>
{providerExpanded[provider.name] && inputParam.type === 'credential' && (
<CredentialInputHandler
data={analytic[provider.name] ? { credential: analytic[provider.name].credentialId } : {}}
inputParam={inputParam}
onSelect={(newValue) => setValue(newValue, provider.name, 'credentialId')}
/>
)}
{providerExpanded[provider.name] && inputParam.type === 'boolean' && (
<SwitchInput
onChange={(newValue) => 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') && (
<Input
inputParam={inputParam}
onChange={(newValue) => setValue(newValue, provider.name, inputParam.name)}
value={
analytic[provider.name]
? analytic[provider.name][inputParam.name]
: inputParam.default ?? ''
}
/>
)}
</Box>
))}
</AccordionDetails>
</Accordion>
))}
</DialogContent>
<DialogActions>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
AnalyseFlowDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func
}
export default AnalyseFlowDialog

View File

@ -22,7 +22,7 @@ export const SwitchInput = ({ value, onChange, disabled = false }) => {
} }
SwitchInput.propTypes = { SwitchInput.propTypes = {
value: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onChange: PropTypes.func, onChange: PropTypes.func,
disabled: PropTypes.bool disabled: PropTypes.bool
} }

View File

@ -14,6 +14,7 @@ import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck,
import Settings from 'views/settings' import Settings from 'views/settings'
import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog' import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
import APICodeDialog from 'views/chatflows/APICodeDialog' import APICodeDialog from 'views/chatflows/APICodeDialog'
import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog'
// API // API
import chatflowsApi from 'api/chatflows' import chatflowsApi from 'api/chatflows'
@ -41,6 +42,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [flowDialogOpen, setFlowDialogOpen] = useState(false) const [flowDialogOpen, setFlowDialogOpen] = useState(false)
const [apiDialogOpen, setAPIDialogOpen] = useState(false) const [apiDialogOpen, setAPIDialogOpen] = useState(false)
const [apiDialogProps, setAPIDialogProps] = useState({}) const [apiDialogProps, setAPIDialogProps] = useState({})
const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false)
const [analyseDialogProps, setAnalyseDialogProps] = useState({})
const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const canvas = useSelector((state) => state.canvas) const canvas = useSelector((state) => state.canvas)
@ -50,6 +53,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
if (setting === 'deleteChatflow') { if (setting === 'deleteChatflow') {
handleDeleteFlow() handleDeleteFlow()
} else if (setting === 'analyseChatflow') {
setAnalyseDialogProps({
title: 'Analyse Chatflow',
chatflow: chatflow
})
setAnalyseDialogOpen(true)
} else if (setting === 'duplicateChatflow') { } else if (setting === 'duplicateChatflow') {
try { try {
localStorage.setItem('duplicatedFlowData', chatflow.flowData) localStorage.setItem('duplicatedFlowData', chatflow.flowData)
@ -357,6 +366,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
onConfirm={onConfirmSaveName} onConfirm={onConfirmSaveName}
/> />
<APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} /> <APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />
<AnalyseFlowDialog show={analyseDialogOpen} dialogProps={analyseDialogProps} onCancel={() => setAnalyseDialogOpen(false)} />
</> </>
) )
} }

View File

@ -26,7 +26,7 @@ import useApi from 'hooks/useApi'
import useNotifier from 'utils/useNotifier' import useNotifier from 'utils/useNotifier'
// const // const
import { baseURL } from 'store/constant' import { baseURL, REDACTED_CREDENTIAL_VALUE } from 'store/constant'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
@ -118,7 +118,7 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
onConfirm(createResp.data.id) onConfirm(createResp.data.id)
} }
} catch (error) { } 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({ enqueueSnackbar({
message: `Failed to add new Credential: ${errorData}`, message: `Failed to add new Credential: ${errorData}`,
options: { options: {
@ -138,11 +138,20 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
const saveCredential = async () => { const saveCredential = async () => {
try { try {
const saveResp = await credentialsApi.updateCredential(credential.id, { const saveObj = {
name, name,
credentialName: componentCredential.name, credentialName: componentCredential.name
plainDataObj: credentialData }
})
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) { if (saveResp.data) {
enqueueSnackbar({ enqueueSnackbar({
message: 'Credential saved', message: 'Credential saved',