Merge branch 'main' into feature/LlamaIndex

# Conflicts:
#	packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts
#	packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts
#	packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts
#	packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts
#	packages/components/nodes/chains/ConversationChain/ConversationChain.ts
#	packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts
#	packages/components/nodes/memory/BufferMemory/BufferMemory.ts
#	packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts
#	packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts
#	packages/components/nodes/memory/DynamoDb/DynamoDb.ts
#	packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts
#	packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts
#	packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts
#	packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts
#	packages/components/nodes/memory/ZepMemory/ZepMemory.ts
#	packages/components/src/utils.ts
#	packages/server/marketplaces/chatflows/Long Term Memory.json
#	packages/server/src/index.ts
#	packages/server/src/utils/index.ts
This commit is contained in:
Henry
2024-01-24 03:00:31 +00:00
468 changed files with 13562 additions and 3588 deletions
+20
View File
@@ -0,0 +1,20 @@
import { Request, Response, NextFunction } from 'express'
import sanitizeHtml from 'sanitize-html'
export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void {
// decoding is necessary as the url is encoded by the browser
const decodedURI = decodeURI(req.url)
req.url = sanitizeHtml(decodedURI)
for (let p in req.query) {
if (Array.isArray(req.query[p])) {
const sanitizedQ = []
for (const q of req.query[p] as string[]) {
sanitizedQ.push(sanitizeHtml(q))
}
req.query[p] = sanitizedQ
} else {
req.query[p] = sanitizeHtml(req.query[p] as string)
}
}
next()
}
+36
View File
@@ -0,0 +1,36 @@
export function parsePrompt(prompt: string): any[] {
const promptObj = JSON.parse(prompt)
let response = []
if (promptObj.kwargs.messages) {
promptObj.kwargs.messages.forEach((message: any) => {
let messageType = message.id.includes('SystemMessagePromptTemplate')
? 'systemMessagePrompt'
: message.id.includes('HumanMessagePromptTemplate')
? 'humanMessagePrompt'
: message.id.includes('AIMessagePromptTemplate')
? 'aiMessagePrompt'
: 'template'
let messageTypeDisplay = message.id.includes('SystemMessagePromptTemplate')
? 'System Message'
: message.id.includes('HumanMessagePromptTemplate')
? 'Human Message'
: message.id.includes('AIMessagePromptTemplate')
? 'AI Message'
: 'Message'
let template = message.kwargs.prompt.kwargs.template
response.push({
type: messageType,
typeDisplay: messageTypeDisplay,
template: template
})
})
} else if (promptObj.kwargs.template) {
let template = promptObj.kwargs.template
response.push({
type: 'template',
typeDisplay: 'Prompt',
template: template
})
}
return response
}
+314 -58
View File
@@ -23,9 +23,11 @@ import {
convertChatHistoryToText,
getInputVariables,
handleEscapeCharacters,
getEncryptionKeyPath,
ICommonObject,
IDatabaseEntity,
IMessage
IMessage,
FlowiseMemory
} from 'flowise-components'
import { randomBytes } from 'crypto'
import { AES, enc } from 'crypto-js'
@@ -37,6 +39,7 @@ import { Tool } from '../database/entities/Tool'
import { Assistant } from '../database/entities/Assistant'
import { DataSource } from 'typeorm'
import { CachePool } from '../CachePool'
import { Variable } from '../database/entities/Variable'
const QUESTION_VAR_PREFIX = 'question'
const CHAT_HISTORY_VAR_PREFIX = 'chat_history'
@@ -47,7 +50,8 @@ export const databaseEntities: IDatabaseEntity = {
ChatMessage: ChatMessage,
Tool: Tool,
Credential: Credential,
Assistant: Assistant
Assistant: Assistant,
Variable: Variable
}
/**
@@ -95,9 +99,13 @@ export const getNodeModulesPackagePath = (packageName: string): string => {
* Construct graph and node dependencies score
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IReactFlowEdge[]} reactFlowEdges
* @param {boolean} isNondirected
* @param {{ isNonDirected?: boolean, isReversed?: boolean }} options
*/
export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges: IReactFlowEdge[], isNondirected = false) => {
export const constructGraphs = (
reactFlowNodes: IReactFlowNode[],
reactFlowEdges: IReactFlowEdge[],
options?: { isNonDirected?: boolean; isReversed?: boolean }
) => {
const nodeDependencies = {} as INodeDependencies
const graph = {} as INodeDirectedGraph
@@ -107,6 +115,23 @@ export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges
graph[nodeId] = []
}
if (options && options.isReversed) {
for (let i = 0; i < reactFlowEdges.length; i += 1) {
const source = reactFlowEdges[i].source
const target = reactFlowEdges[i].target
if (Object.prototype.hasOwnProperty.call(graph, target)) {
graph[target].push(source)
} else {
graph[target] = [source]
}
nodeDependencies[target] += 1
}
return { graph, nodeDependencies }
}
for (let i = 0; i < reactFlowEdges.length; i += 1) {
const source = reactFlowEdges[i].source
const target = reactFlowEdges[i].target
@@ -117,7 +142,7 @@ export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges
graph[source] = [target]
}
if (isNondirected) {
if (options && options.isNonDirected) {
if (Object.prototype.hasOwnProperty.call(graph, target)) {
graph[target].push(source)
} else {
@@ -179,21 +204,49 @@ export const getStartingNodes = (graph: INodeDirectedGraph, endNodeId: string) =
return { startingNodeIds, depthQueue: depthQueueReversed }
}
/**
* Get all connected nodes from startnode
* @param {INodeDependencies} graph
* @param {string} startNodeId
*/
export const getAllConnectedNodes = (graph: INodeDirectedGraph, startNodeId: string) => {
const visited = new Set<string>()
const queue: Array<[string]> = [[startNodeId]]
while (queue.length > 0) {
const [currentNode] = queue.shift()!
if (visited.has(currentNode)) {
continue
}
visited.add(currentNode)
for (const neighbor of graph[currentNode]) {
if (!visited.has(neighbor)) {
queue.push([neighbor])
}
}
}
return [...visited]
}
/**
* Get ending node and check if flow is valid
* @param {INodeDependencies} nodeDependencies
* @param {INodeDirectedGraph} graph
*/
export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => {
let endingNodeId = ''
export const getEndingNodes = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => {
const endingNodeIds: string[] = []
Object.keys(graph).forEach((nodeId) => {
if (Object.keys(nodeDependencies).length === 1) {
endingNodeId = nodeId
endingNodeIds.push(nodeId)
} else if (!graph[nodeId].length && nodeDependencies[nodeId] > 0) {
endingNodeId = nodeId
endingNodeIds.push(nodeId)
}
})
return endingNodeId
return endingNodeIds
}
/**
@@ -213,12 +266,14 @@ export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeD
export const buildLangchain = async (
startingNodeIds: string[],
reactFlowNodes: IReactFlowNode[],
reactFlowEdges: IReactFlowEdge[],
graph: INodeDirectedGraph,
depthQueue: IDepthQueue,
componentNodes: IComponentNodes,
question: string,
chatHistory: IMessage[],
chatId: string,
sessionId: string,
chatflowid: string,
appDataSource: DataSource,
overrideConfig?: ICommonObject,
@@ -231,6 +286,8 @@ export const buildLangchain = async (
// Create a Queue and add our initial node in it
const nodeQueue = [] as INodeQueue[]
const exploredNode = {} as IExploredNode
const dynamicVariables = {} as Record<string, unknown>
let ignoreNodeIds: string[] = []
// In the case of infinite loop, only max 3 loops will be executed
const maxLoop = 3
@@ -256,31 +313,72 @@ export const buildLangchain = async (
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question, chatHistory)
if (
isUpsert &&
((stopNodeId && reactFlowNodeData.id === stopNodeId) || (!stopNodeId && reactFlowNodeData.category === 'Vector Stores'))
) {
// TODO: Avoid processing Text Splitter + Doc Loader once Upsert & Load Existing Vector Nodes are deprecated
if (isUpsert && stopNodeId && nodeId === stopNodeId) {
logger.debug(`[server]: Upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, {
chatId,
sessionId,
chatflowid,
chatHistory,
logger,
appDataSource,
databaseEntities,
logger,
cachePool
cachePool,
dynamicVariables
})
logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
break
} else {
logger.debug(`[server]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question, {
let outputResult = await newNodeInstance.init(reactFlowNodeData, question, {
chatId,
sessionId,
chatflowid,
chatHistory,
logger,
appDataSource,
databaseEntities,
logger,
cachePool
cachePool,
dynamicVariables
})
// Save dynamic variables
if (reactFlowNode.data.name === 'setVariable') {
const dynamicVars = outputResult?.dynamicVariables ?? {}
for (const variableKey in dynamicVars) {
dynamicVariables[variableKey] = dynamicVars[variableKey]
}
outputResult = outputResult?.output
}
// Determine which nodes to route next when it comes to ifElse
if (reactFlowNode.data.name === 'ifElseFunction' && typeof outputResult === 'object') {
let sourceHandle = ''
if (outputResult.type === true) {
sourceHandle = `${nodeId}-output-returnFalse-string|number|boolean|json|array`
} else if (outputResult.type === false) {
sourceHandle = `${nodeId}-output-returnTrue-string|number|boolean|json|array`
}
const ifElseEdge = reactFlowEdges.find((edg) => edg.source === nodeId && edg.sourceHandle === sourceHandle)
if (ifElseEdge) {
const { graph } = constructGraphs(
reactFlowNodes,
reactFlowEdges.filter((edg) => !(edg.source === nodeId && edg.sourceHandle === sourceHandle)),
{ isNonDirected: true }
)
ignoreNodeIds.push(ifElseEdge.target, ...getAllConnectedNodes(graph, ifElseEdge.target))
ignoreNodeIds = [...new Set(ignoreNodeIds)]
}
outputResult = outputResult?.output
}
flowNodes[nodeIndex].data.instance = outputResult
logger.debug(`[server]: Finished initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
}
} catch (e: any) {
@@ -288,7 +386,7 @@ export const buildLangchain = async (
throw new Error(e)
}
const neighbourNodeIds = graph[nodeId]
let neighbourNodeIds = graph[nodeId]
const nextDepth = depth + 1
// Find other nodes that are on the same depth level
@@ -299,9 +397,11 @@ export const buildLangchain = async (
neighbourNodeIds.push(id)
}
neighbourNodeIds = neighbourNodeIds.filter((neigh) => !ignoreNodeIds.includes(neigh))
for (let i = 0; i < neighbourNodeIds.length; i += 1) {
const neighNodeId = neighbourNodeIds[i]
if (ignoreNodeIds.includes(neighNodeId)) continue
// If nodeId has been seen, cycle detected
if (Object.prototype.hasOwnProperty.call(exploredNode, neighNodeId)) {
const { remainingLoop, lastSeenDepth } = exploredNode[neighNodeId]
@@ -319,6 +419,12 @@ export const buildLangchain = async (
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
}
}
// Move end node to last
if (!neighbourNodeIds.length) {
const index = flowNodes.findIndex((nd) => nd.data.id === nodeId)
flowNodes.push(flowNodes.splice(index, 1)[0])
}
}
return flowNodes
}
@@ -351,15 +457,25 @@ export const clearSessionMemory = async (
const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
const options: ICommonObject = { chatId, appDataSource, databaseEntities, logger }
// SessionId always take priority first because it is the sessionId used for 3rd party memory node
if (sessionId && node.data.inputs) {
node.data.inputs.sessionId = sessionId
}
const initializedInstance = await newNodeInstance.init(node.data, '', { chatId, appDataSource, databaseEntities, logger })
if (initializedInstance.clearChatMessages) {
await initializedInstance.clearChatMessages()
if (node.data.type === 'OpenAIAssistant') {
await newNodeInstance.clearChatMessages(node.data, options, { type: 'threadId', id: sessionId })
} else {
node.data.inputs.sessionId = sessionId
const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options)
await initializedInstance.clearChatMessages(sessionId)
}
} else if (chatId && node.data.inputs) {
if (node.data.type === 'OpenAIAssistant') {
await newNodeInstance.clearChatMessages(node.data, options, { type: 'chatId', id: chatId })
} else {
node.data.inputs.sessionId = chatId
const initializedInstance: FlowiseMemory = await newNodeInstance.init(node.data, '', options)
await initializedInstance.clearChatMessages(chatId)
}
}
}
}
@@ -434,7 +550,11 @@ export const getVariableValue = (
variablePaths.forEach((path) => {
const variableValue = variableDict[path]
// Replace all occurrence
returnVal = returnVal.split(path).join(variableValue)
if (typeof variableValue === 'object') {
returnVal = returnVal.split(path).join(JSON.stringify(variableValue).replace(/"/g, '\\"'))
} else {
returnVal = returnVal.split(path).join(variableValue)
}
})
return returnVal
}
@@ -533,7 +653,18 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[], nodes:
}
const whitelistNodeNames = ['vectorStoreToDocument', 'autoGPT', 'chatPromptTemplate', 'promptTemplate'] //If these nodes are found, chatflow cannot be reused
for (const node of nodes) {
if (whitelistNodeNames.includes(node.data.name)) return true
if (node.data.name === 'chatPromptTemplate' || node.data.name === 'promptTemplate') {
let promptValues: ICommonObject = {}
const promptValuesRaw = node.data.inputs?.promptValues
if (promptValuesRaw) {
try {
promptValues = typeof promptValuesRaw === 'object' ? promptValuesRaw : JSON.parse(promptValuesRaw)
} catch (exception) {
console.error(exception)
}
}
if (getAllValuesFromJson(promptValues).includes(`{{${QUESTION_VAR_PREFIX}}}`)) return true
} else if (whitelistNodeNames.includes(node.data.name)) return true
}
return false
}
@@ -673,6 +804,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], component
/**
* Check to see if flow valid for stream
* TODO: perform check from component level. i.e: set streaming on component, and check here
* @param {IReactFlowNode[]} reactFlowNodes
* @param {INodeData} endingNodeData
* @returns {boolean}
@@ -686,7 +818,8 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod
'chatAnthropic',
'chatAnthropic_LlamaIndex',
'chatOllama',
'awsChatBedrock'
'awsChatBedrock',
'chatMistralAI'
],
LLMs: ['azureOpenAI', 'openAI', 'ollama']
}
@@ -711,7 +844,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod
const whitelistAgents = ['openAIFunctionAgent', 'csvAgent', 'airtableAgent', 'conversationalRetrievalAgent']
isValidChainOrAgent = whitelistAgents.includes(endingNodeData.name)
} else if (endingNodeData.category === 'Engine') {
const whitelistEngine = ['contextChatEngine', 'simpleChatEngine']
const whitelistEngine = ['contextChatEngine', 'simpleChatEngine', 'queryEngine']
isValidChainOrAgent = whitelistEngine.includes(endingNodeData.name)
}
@@ -727,16 +860,6 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod
return isChatOrLLMsExist && isValidChainOrAgent && !isOutputParserExist
}
/**
* Returns the path of encryption key
* @returns {string}
*/
export const getEncryptionKeyPath = (): string => {
return process.env.SECRETKEY_PATH
? path.join(process.env.SECRETKEY_PATH, 'encryption.key')
: path.join(__dirname, '..', '..', 'encryption.key')
}
/**
* Generate an encryption key
* @returns {string}
@@ -757,7 +880,10 @@ export const getEncryptionKey = async (): Promise<string> => {
return await fs.promises.readFile(getEncryptionKeyPath(), 'utf8')
} catch (error) {
const encryptKey = generateEncryptKey()
await fs.promises.writeFile(getEncryptionKeyPath(), encryptKey)
const defaultLocation = process.env.SECRETKEY_PATH
? path.join(process.env.SECRETKEY_PATH, 'encryption.key')
: path.join(getUserHome(), '.flowise', 'encryption.key')
await fs.promises.writeFile(defaultLocation, encryptKey)
return encryptKey
}
}
@@ -845,21 +971,43 @@ export const redactCredentialWithPasswordType = (
}
/**
* Replace sessionId with new chatId
* Ex: after clear chat history, use the new chatId as sessionId
* Get sessionId
* Hierarchy of sessionId (top down)
* API/Embed:
* (1) Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' }
* (2) Provided in API body - incomingInput.chatId
*
* API/Embed + UI:
* (3) Hard-coded sessionId in UI
* (4) Not specified on UI nor API, default to chatId
* @param {any} instance
* @param {IncomingInput} incomingInput
* @param {string} chatId
*/
export const replaceMemorySessionId = (instance: any, chatId: string): string | undefined => {
if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) {
instance.memory.sessionId = chatId
instance.memory.chatHistory.sessionId = chatId
export const getMemorySessionId = (
memoryNode: IReactFlowNode,
incomingInput: IncomingInput,
chatId: string,
isInternal: boolean
): string | undefined => {
if (!isInternal) {
// Provided in API body - incomingInput.overrideConfig: { sessionId: 'abc' }
if (incomingInput.overrideConfig?.sessionId) {
return incomingInput.overrideConfig?.sessionId
}
// Provided in API body - incomingInput.chatId
if (incomingInput.chatId) {
return incomingInput.chatId
}
}
if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId
else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId)
return instance.memory.chatHistory.sessionId
return undefined
// Hard-coded sessionId in UI
if (memoryNode.data.inputs?.sessionId) {
return memoryNode.data.inputs.sessionId
}
// Default chatId
return chatId
}
/**
@@ -871,27 +1019,135 @@ export const replaceMemorySessionId = (instance: any, chatId: string): string |
* @param {any} logger
* @returns {string}
*/
export const replaceChatHistory = async (
export const getSessionChatHistory = async (
memoryNode: IReactFlowNode,
componentNodes: IComponentNodes,
incomingInput: IncomingInput,
appDataSource: DataSource,
databaseEntities: IDatabaseEntity,
logger: any
): Promise<IMessage[]> => {
const nodeInstanceFilePath = memoryNode.data.filePath as string
const nodeInstanceFilePath = componentNodes[memoryNode.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
// Replace memory's sessionId/chatId
if (incomingInput.overrideConfig?.sessionId && memoryNode.data.inputs) {
memoryNode.data.inputs.sessionId = incomingInput.overrideConfig.sessionId
} else if (incomingInput.chatId && memoryNode.data.inputs) {
memoryNode.data.inputs.sessionId = incomingInput.chatId
}
const initializedInstance = await newNodeInstance.init(memoryNode.data, '', {
chatId: incomingInput.chatId,
const initializedInstance: FlowiseMemory = await newNodeInstance.init(memoryNode.data, '', {
appDataSource,
databaseEntities,
logger
})
return await initializedInstance.getChatMessages()
return (await initializedInstance.getChatMessages()) as IMessage[]
}
/**
* Method that find memory that is connected within chatflow
* In a chatflow, there should only be 1 memory node
* @param {IReactFlowNode[]} nodes
* @param {IReactFlowEdge[]} edges
* @returns {string | undefined}
*/
export const findMemoryNode = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]): IReactFlowNode | undefined => {
const memoryNodes = nodes.filter((node) => node.data.category === 'Memory')
const memoryNodeIds = memoryNodes.map((mem) => mem.data.id)
for (const edge of edges) {
if (memoryNodeIds.includes(edge.source)) {
const memoryNode = nodes.find((node) => node.data.id === edge.source)
return memoryNode
}
}
return undefined
}
/**
* Get all values from a JSON object
* @param {any} obj
* @returns {any[]}
*/
export const getAllValuesFromJson = (obj: any): any[] => {
const values: any[] = []
function extractValues(data: any) {
if (typeof data === 'object' && data !== null) {
if (Array.isArray(data)) {
for (const item of data) {
extractValues(item)
}
} else {
for (const key in data) {
extractValues(data[key])
}
}
} else {
values.push(data)
}
}
extractValues(obj)
return values
}
/**
* Get only essential flow data items for telemetry
* @param {IReactFlowNode[]} nodes
* @param {IReactFlowEdge[]} edges
*/
export const getTelemetryFlowObj = (nodes: IReactFlowNode[], edges: IReactFlowEdge[]) => {
const nodeData = nodes.map((node) => node.id)
const edgeData = edges.map((edge) => ({ source: edge.source, target: edge.target }))
return { nodes: nodeData, edges: edgeData }
}
/**
* Get user settings file
* TODO: move env variables to settings json file, easier configuration
*/
export const getUserSettingsFilePath = () => {
if (process.env.SECRETKEY_PATH) return path.join(process.env.SECRETKEY_PATH, 'settings.json')
const checkPaths = [path.join(getUserHome(), '.flowise', 'settings.json')]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
/**
* Get app current version
*/
export const getAppVersion = async () => {
const getPackageJsonPath = (): string => {
const checkPaths = [
path.join(__dirname, '..', 'package.json'),
path.join(__dirname, '..', '..', 'package.json'),
path.join(__dirname, '..', '..', '..', 'package.json'),
path.join(__dirname, '..', '..', '..', '..', 'package.json'),
path.join(__dirname, '..', '..', '..', '..', '..', 'package.json')
]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
const packagejsonPath = getPackageJsonPath()
if (!packagejsonPath) return ''
try {
const content = await fs.promises.readFile(packagejsonPath, 'utf8')
const parsedContent = JSON.parse(content)
return parsedContent.version
} catch (error) {
return ''
}
}
+52
View File
@@ -0,0 +1,52 @@
import { v4 as uuidv4 } from 'uuid'
import { PostHog } from 'posthog-node'
import path from 'path'
import fs from 'fs'
import { getUserHome, getUserSettingsFilePath } from '.'
export class Telemetry {
postHog?: PostHog
constructor() {
if (process.env.DISABLE_FLOWISE_TELEMETRY !== 'true') {
this.postHog = new PostHog('phc_jEDuFYnOnuXsws986TLWzuisbRjwFqTl9JL8tDMgqme')
} else {
this.postHog = undefined
}
}
async id(): Promise<string> {
try {
const settingsContent = await fs.promises.readFile(getUserSettingsFilePath(), 'utf8')
const settings = JSON.parse(settingsContent)
return settings.instanceId
} catch (error) {
const instanceId = uuidv4()
const settings = {
instanceId
}
const defaultLocation = process.env.SECRETKEY_PATH
? path.join(process.env.SECRETKEY_PATH, 'settings.json')
: path.join(getUserHome(), '.flowise', 'settings.json')
await fs.promises.writeFile(defaultLocation, JSON.stringify(settings, null, 2))
return instanceId
}
}
async sendTelemetry(event: string, properties = {}): Promise<void> {
if (this.postHog) {
const distinctId = await this.id()
this.postHog.capture({
event,
distinctId,
properties
})
}
}
async flush(): Promise<void> {
if (this.postHog) {
await this.postHog.shutdownAsync()
}
}
}