Feature/agentflow v2 (#4298)

* agent flow v2

* chat message background

* conditon agent flow

* add sticky note

* update human input dynamic prompt

* add HTTP node

* add default tool icon

* fix export duplicate agentflow v2

* add agentflow v2 marketplaces

* refractor memoization, add iteration nodes

* add agentflow v2 templates

* add agentflow generator

* add migration scripts for mysql, mariadb, posrgres and fix date filters for executions

* update agentflow chat history config

* fix get all flows error after deletion and rename

* add previous nodes from parent node

* update generator prompt

* update run time state when using iteration nodes

* prevent looping connection, prevent duplication of start node, add executeflow node, add nodes agentflow, chat history variable

* update embed

* convert form input to string

* bump openai version

* add react rewards

* add prompt generator to prediction queue

* add array schema to overrideconfig

* UI touchup

* update embedded chat version

* fix node info dialog

* update start node and loop default iteration

* update UI fixes for agentflow v2

* fix async drop down

* add export import to agentflowsv2, executions, fix UI bugs

* add default empty object to flowlisttable

* add ability to share trace link publicly, allow MCP tool use for Agent and Assistant

* add runtime message length to variable, display conditions on UI

* fix array validation

* add ability to add knowledge from vector store and embeddings for agent

* add agent tool require human input

* add ephemeral memory to start node

* update agent flow node to show vs and embeddings icons

* feat: add import chat data functionality for AgentFlowV2

* feat: set chatMessage.executionId to null if not found in import JSON file or database

* fix: MariaDB execution migration script to utf8mb4_unicode_520_ci

---------

Co-authored-by: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com>
Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
Henry Heng
2025-05-10 10:21:26 +08:00
committed by GitHub
parent 82e6f43b5c
commit 7924fbce0d
216 changed files with 33304 additions and 5269 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
Flowise 的应用集成。包含节点和凭据。
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise_agentflow.gif?raw=true)
安装:
+1 -1
View File
@@ -6,7 +6,7 @@ English | [中文](./README-ZH.md)
Apps integration for Flowise. Contain Nodes and Credentials.
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise_agentflow.gif?raw=true)
Install:
@@ -0,0 +1,28 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class HTTPApiKeyCredential implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'HTTP Api Key'
this.name = 'httpApiKey'
this.version = 1.0
this.inputs = [
{
label: 'Key',
name: 'key',
type: 'string'
},
{
label: 'Value',
name: 'value',
type: 'password'
}
]
}
}
module.exports = { credClass: HTTPApiKeyCredential }
@@ -0,0 +1,28 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class HttpBasicAuthCredential implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'HTTP Basic Auth'
this.name = 'httpBasicAuth'
this.version = 1.0
this.inputs = [
{
label: 'Basic Auth Username',
name: 'basicAuthUsername',
type: 'string'
},
{
label: 'Basic Auth Password',
name: 'basicAuthPassword',
type: 'password'
}
]
}
}
module.exports = { credClass: HttpBasicAuthCredential }
@@ -0,0 +1,23 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class HTTPBearerTokenCredential implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'HTTP Bearer Token'
this.name = 'httpBearerToken'
this.version = 1.0
this.inputs = [
{
label: 'Token',
name: 'token',
type: 'password'
}
]
}
}
module.exports = { credClass: HTTPBearerTokenCredential }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,350 @@
import { CommonType, ICommonObject, ICondition, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
class Condition_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
tags: string[]
baseClasses: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Condition'
this.name = 'conditionAgentflow'
this.version = 1.0
this.type = 'Condition'
this.category = 'Agent Flows'
this.description = `Split flows based on If Else conditions`
this.baseClasses = [this.type]
this.color = '#FFB938'
this.inputs = [
{
label: 'Conditions',
name: 'conditions',
type: 'array',
description: 'Values to compare',
acceptVariable: true,
default: [
{
type: 'string',
value1: '',
operation: 'equal',
value2: ''
}
],
array: [
{
label: 'Type',
name: 'type',
type: 'options',
options: [
{
label: 'String',
name: 'string'
},
{
label: 'Number',
name: 'number'
},
{
label: 'Boolean',
name: 'boolean'
}
],
default: 'string'
},
/////////////////////////////////////// STRING ////////////////////////////////////////
{
label: 'Value 1',
name: 'value1',
type: 'string',
default: '',
description: 'First value to be compared with',
acceptVariable: true,
show: {
'conditions[$index].type': 'string'
}
},
{
label: 'Operation',
name: 'operation',
type: 'options',
options: [
{
label: 'Contains',
name: 'contains'
},
{
label: 'Ends With',
name: 'endsWith'
},
{
label: 'Equal',
name: 'equal'
},
{
label: 'Not Contains',
name: 'notContains'
},
{
label: 'Not Equal',
name: 'notEqual'
},
{
label: 'Regex',
name: 'regex'
},
{
label: 'Starts With',
name: 'startsWith'
},
{
label: 'Is Empty',
name: 'isEmpty'
},
{
label: 'Not Empty',
name: 'notEmpty'
}
],
default: 'equal',
description: 'Type of operation',
show: {
'conditions[$index].type': 'string'
}
},
{
label: 'Value 2',
name: 'value2',
type: 'string',
default: '',
description: 'Second value to be compared with',
acceptVariable: true,
show: {
'conditions[$index].type': 'string'
},
hide: {
'conditions[$index].operation': ['isEmpty', 'notEmpty']
}
},
/////////////////////////////////////// NUMBER ////////////////////////////////////////
{
label: 'Value 1',
name: 'value1',
type: 'number',
default: '',
description: 'First value to be compared with',
acceptVariable: true,
show: {
'conditions[$index].type': 'number'
}
},
{
label: 'Operation',
name: 'operation',
type: 'options',
options: [
{
label: 'Smaller',
name: 'smaller'
},
{
label: 'Smaller Equal',
name: 'smallerEqual'
},
{
label: 'Equal',
name: 'equal'
},
{
label: 'Not Equal',
name: 'notEqual'
},
{
label: 'Larger',
name: 'larger'
},
{
label: 'Larger Equal',
name: 'largerEqual'
},
{
label: 'Is Empty',
name: 'isEmpty'
},
{
label: 'Not Empty',
name: 'notEmpty'
}
],
default: 'equal',
description: 'Type of operation',
show: {
'conditions[$index].type': 'number'
}
},
{
label: 'Value 2',
name: 'value2',
type: 'number',
default: 0,
description: 'Second value to be compared with',
acceptVariable: true,
show: {
'conditions[$index].type': 'number'
}
},
/////////////////////////////////////// BOOLEAN ////////////////////////////////////////
{
label: 'Value 1',
name: 'value1',
type: 'boolean',
default: false,
description: 'First value to be compared with',
show: {
'conditions[$index].type': 'boolean'
}
},
{
label: 'Operation',
name: 'operation',
type: 'options',
options: [
{
label: 'Equal',
name: 'equal'
},
{
label: 'Not Equal',
name: 'notEqual'
}
],
default: 'equal',
description: 'Type of operation',
show: {
'conditions[$index].type': 'boolean'
}
},
{
label: 'Value 2',
name: 'value2',
type: 'boolean',
default: false,
description: 'Second value to be compared with',
show: {
'conditions[$index].type': 'boolean'
}
}
]
}
]
this.outputs = [
{
label: '0',
name: '0',
description: 'Condition 0'
},
{
label: '1',
name: '1',
description: 'Else'
}
]
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const state = options.agentflowRuntime?.state as ICommonObject
const compareOperationFunctions: {
[key: string]: (value1: CommonType, value2: CommonType) => boolean
} = {
contains: (value1: CommonType, value2: CommonType) => (value1 || '').toString().includes((value2 || '').toString()),
notContains: (value1: CommonType, value2: CommonType) => !(value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: CommonType, value2: CommonType) => (value1 as string).endsWith(value2 as string),
equal: (value1: CommonType, value2: CommonType) => value1 === value2,
notEqual: (value1: CommonType, value2: CommonType) => value1 !== value2,
larger: (value1: CommonType, value2: CommonType) => (Number(value1) || 0) > (Number(value2) || 0),
largerEqual: (value1: CommonType, value2: CommonType) => (Number(value1) || 0) >= (Number(value2) || 0),
smaller: (value1: CommonType, value2: CommonType) => (Number(value1) || 0) < (Number(value2) || 0),
smallerEqual: (value1: CommonType, value2: CommonType) => (Number(value1) || 0) <= (Number(value2) || 0),
startsWith: (value1: CommonType, value2: CommonType) => (value1 as string).startsWith(value2 as string),
isEmpty: (value1: CommonType) => [undefined, null, ''].includes(value1 as string),
notEmpty: (value1: CommonType) => ![undefined, null, ''].includes(value1 as string)
}
const _conditions = nodeData.inputs?.conditions
const conditions: ICondition[] = typeof _conditions === 'string' ? JSON.parse(_conditions) : _conditions
const initialConditions = { ...conditions }
for (const condition of conditions) {
const _value1 = condition.value1
const _value2 = condition.value2
const operation = condition.operation
let value1: CommonType
let value2: CommonType
switch (condition.type) {
case 'boolean':
value1 = _value1
value2 = _value2
break
case 'number':
value1 = parseFloat(_value1 as string) || 0
value2 = parseFloat(_value2 as string) || 0
break
default: // string
value1 = _value1 as string
value2 = _value2 as string
}
const compareOperationResult = compareOperationFunctions[operation](value1, value2)
if (compareOperationResult) {
// find the matching condition
const conditionIndex = conditions.findIndex((c) => JSON.stringify(c) === JSON.stringify(condition))
// add isFulfilled to the condition
if (conditionIndex > -1) {
conditions[conditionIndex] = { ...condition, isFulfilled: true }
}
break
}
}
// If no condition is fullfilled, add isFulfilled to the ELSE condition
const dummyElseConditionData = {
type: 'string',
value1: '',
operation: 'equal',
value2: ''
}
if (!conditions.some((c) => c.isFulfilled)) {
conditions.push({
...dummyElseConditionData,
isFulfilled: true
})
} else {
conditions.push({
...dummyElseConditionData,
isFulfilled: false
})
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: { conditions: initialConditions },
output: { conditions },
state
}
return returnOutput
}
}
module.exports = { nodeClass: Condition_Agentflow }
@@ -0,0 +1,600 @@
import { AnalyticHandler } from '../../../src/handler'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { AIMessageChunk, BaseMessageLike } from '@langchain/core/messages'
import {
getPastChatHistoryImageMessages,
getUniqueImageMessages,
processMessagesWithImages,
replaceBase64ImagesWithFileReferences
} from '../utils'
import { CONDITION_AGENT_SYSTEM_PROMPT, DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt'
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
class ConditionAgent_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
tags: string[]
baseClasses: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Condition Agent'
this.name = 'conditionAgentAgentflow'
this.version = 1.0
this.type = 'ConditionAgent'
this.category = 'Agent Flows'
this.description = `Utilize an agent to split flows based on dynamic conditions`
this.baseClasses = [this.type]
this.color = '#ff8fab'
this.inputs = [
{
label: 'Model',
name: 'conditionAgentModel',
type: 'asyncOptions',
loadMethod: 'listModels',
loadConfig: true
},
{
label: 'Instructions',
name: 'conditionAgentInstructions',
type: 'string',
description: 'A general instructions of what the condition agent should do',
rows: 4,
acceptVariable: true,
placeholder: 'Determine if the user is interested in learning about AI'
},
{
label: 'Input',
name: 'conditionAgentInput',
type: 'string',
description: 'Input to be used for the condition agent',
rows: 4,
acceptVariable: true,
default: '<p><span class="variable" data-type="mention" data-id="question" data-label="question">{{ question }}</span> </p>'
},
{
label: 'Scenarios',
name: 'conditionAgentScenarios',
description: 'Define the scenarios that will be used as the conditions to split the flow',
type: 'array',
array: [
{
label: 'Scenario',
name: 'scenario',
type: 'string',
placeholder: 'User is asking for a pizza'
}
],
default: [
{
scenario: ''
},
{
scenario: ''
}
]
}
/*{
label: 'Enable Memory',
name: 'conditionAgentEnableMemory',
type: 'boolean',
description: 'Enable memory for the conversation thread',
default: true,
optional: true
},
{
label: 'Memory Type',
name: 'conditionAgentMemoryType',
type: 'options',
options: [
{
label: 'All Messages',
name: 'allMessages',
description: 'Retrieve all messages from the conversation'
},
{
label: 'Window Size',
name: 'windowSize',
description: 'Uses a fixed window size to surface the last N messages'
},
{
label: 'Conversation Summary',
name: 'conversationSummary',
description: 'Summarizes the whole conversation'
},
{
label: 'Conversation Summary Buffer',
name: 'conversationSummaryBuffer',
description: 'Summarize conversations once token limit is reached. Default to 2000'
}
],
optional: true,
default: 'allMessages',
show: {
conditionAgentEnableMemory: true
}
},
{
label: 'Window Size',
name: 'conditionAgentMemoryWindowSize',
type: 'number',
default: '20',
description: 'Uses a fixed window size to surface the last N messages',
show: {
conditionAgentMemoryType: 'windowSize'
}
},
{
label: 'Max Token Limit',
name: 'conditionAgentMemoryMaxTokenLimit',
type: 'number',
default: '2000',
description: 'Summarize conversations once token limit is reached. Default to 2000',
show: {
conditionAgentMemoryType: 'conversationSummaryBuffer'
}
}*/
]
this.outputs = [
{
label: '0',
name: '0',
description: 'Condition 0'
},
{
label: '1',
name: '1',
description: 'Else'
}
]
}
//@ts-ignore
loadMethods = {
async listModels(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const componentNodes = options.componentNodes as {
[key: string]: INode
}
const returnOptions: INodeOptionsValue[] = []
for (const nodeName in componentNodes) {
const componentNode = componentNodes[nodeName]
if (componentNode.category === 'Chat Models') {
if (componentNode.tags?.includes('LlamaIndex')) {
continue
}
returnOptions.push({
label: componentNode.label,
name: nodeName,
imageSrc: componentNode.icon
})
}
}
return returnOptions
}
}
private parseJsonMarkdown(jsonString: string): any {
// Strip whitespace
jsonString = jsonString.trim()
const starts = ['```json', '```', '``', '`', '{']
const ends = ['```', '``', '`', '}']
let startIndex = -1
let endIndex = -1
// Find start of JSON
for (const s of starts) {
startIndex = jsonString.indexOf(s)
if (startIndex !== -1) {
if (jsonString[startIndex] !== '{') {
startIndex += s.length
}
break
}
}
// Find end of JSON
if (startIndex !== -1) {
for (const e of ends) {
endIndex = jsonString.lastIndexOf(e, jsonString.length)
if (endIndex !== -1) {
if (jsonString[endIndex] === '}') {
endIndex += 1
}
break
}
}
}
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
const extractedContent = jsonString.slice(startIndex, endIndex).trim()
try {
return JSON.parse(extractedContent)
} catch (error) {
throw new Error(`Invalid JSON object. Error: ${error}`)
}
}
throw new Error('Could not find JSON block in the output.')
}
async run(nodeData: INodeData, question: string, options: ICommonObject): Promise<any> {
let llmIds: ICommonObject | undefined
let analyticHandlers = options.analyticHandlers as AnalyticHandler
try {
const abortController = options.abortController as AbortController
// Extract input parameters
const model = nodeData.inputs?.conditionAgentModel as string
const modelConfig = nodeData.inputs?.conditionAgentModelConfig as ICommonObject
if (!model) {
throw new Error('Model is required')
}
const conditionAgentInput = nodeData.inputs?.conditionAgentInput as string
let input = conditionAgentInput || question
const conditionAgentInstructions = nodeData.inputs?.conditionAgentInstructions as string
// Extract memory and configuration options
const enableMemory = nodeData.inputs?.conditionAgentEnableMemory as boolean
const memoryType = nodeData.inputs?.conditionAgentMemoryType as string
const _conditionAgentScenarios = nodeData.inputs?.conditionAgentScenarios as { scenario: string }[]
// Extract runtime state and history
const state = options.agentflowRuntime?.state as ICommonObject
const pastChatHistory = (options.pastChatHistory as BaseMessageLike[]) ?? []
const runtimeChatHistory = (options.agentflowRuntime?.chatHistory as BaseMessageLike[]) ?? []
// Initialize the LLM model instance
const nodeInstanceFilePath = options.componentNodes[model].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newLLMNodeInstance = new nodeModule.nodeClass()
const newNodeData = {
...nodeData,
credential: modelConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...modelConfig
}
}
let llmNodeInstance = (await newLLMNodeInstance.init(newNodeData, '', options)) as BaseChatModel
const isStructuredOutput =
_conditionAgentScenarios && Array.isArray(_conditionAgentScenarios) && _conditionAgentScenarios.length > 0
if (!isStructuredOutput) {
throw new Error('Scenarios are required')
}
// Prepare messages array
const messages: BaseMessageLike[] = [
{
role: 'system',
content: CONDITION_AGENT_SYSTEM_PROMPT
},
{
role: 'user',
content: `{"input": "Hello", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}`
},
{
role: 'assistant',
content: `\`\`\`json\n{"output": "default"}\n\`\`\``
},
{
role: 'user',
content: `{"input": "What is AIGC?", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}`
},
{
role: 'assistant',
content: `\`\`\`json\n{"output": "user is asking about AI"}\n\`\`\``
},
{
role: 'user',
content: `{"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "default"], "instruction": "Determine if the user is interested in learning about AI"}`
},
{
role: 'assistant',
content: `\`\`\`json\n{"output": "user is interested in AI topics"}\n\`\`\``
}
]
// Use to store messages with image file references as we do not want to store the base64 data into database
let runtimeImageMessagesWithFileRef: BaseMessageLike[] = []
// Use to keep track of past messages with image file references
let pastImageMessagesWithFileRef: BaseMessageLike[] = []
input = `{"input": ${input}, "scenarios": ${JSON.stringify(
_conditionAgentScenarios.map((scenario) => scenario.scenario)
)}, "instruction": ${conditionAgentInstructions}}`
// Handle memory management if enabled
if (enableMemory) {
await this.handleMemory({
messages,
memoryType,
pastChatHistory,
runtimeChatHistory,
llmNodeInstance,
nodeData,
input,
abortController,
options,
modelConfig,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
})
} else {
/*
* If this is the first node:
* - Add images to messages if exist
*/
if (!runtimeChatHistory.length && options.uploads) {
const imageContents = await getUniqueImageMessages(options, messages, modelConfig)
if (imageContents) {
const { imageMessageWithBase64, imageMessageWithFileRef } = imageContents
messages.push(imageMessageWithBase64)
runtimeImageMessagesWithFileRef.push(imageMessageWithFileRef)
}
}
messages.push({
role: 'user',
content: input
})
}
// Initialize response and determine if streaming is possible
let response: AIMessageChunk = new AIMessageChunk('')
// Start analytics
if (analyticHandlers && options.parentTraceIds) {
const llmLabel = options?.componentNodes?.[model]?.label || model
llmIds = await analyticHandlers.onLLMStart(llmLabel, messages, options.parentTraceIds)
}
// Track execution time
const startTime = Date.now()
response = await llmNodeInstance.invoke(messages, { signal: abortController?.signal })
// Calculate execution time
const endTime = Date.now()
const timeDelta = endTime - startTime
// End analytics tracking
if (analyticHandlers && llmIds) {
await analyticHandlers.onLLMEnd(
llmIds,
typeof response.content === 'string' ? response.content : JSON.stringify(response.content)
)
}
let calledOutputName = 'default'
try {
const parsedResponse = this.parseJsonMarkdown(response.content as string)
if (!parsedResponse.output) {
throw new Error('Missing "output" key in response')
}
calledOutputName = parsedResponse.output
} catch (error) {
console.warn(`Failed to parse LLM response: ${error}. Using default output.`)
}
// Clean up empty inputs
for (const key in nodeData.inputs) {
if (nodeData.inputs[key] === '') {
delete nodeData.inputs[key]
}
}
// Find the first exact match
const matchedScenarioIndex = _conditionAgentScenarios.findIndex(
(scenario) => calledOutputName.toLowerCase() === scenario.scenario.toLowerCase()
)
const conditions = _conditionAgentScenarios.map((scenario, index) => {
return {
output: scenario.scenario,
isFulfilled: index === matchedScenarioIndex
}
})
// Replace the actual messages array with one that includes the file references for images instead of base64 data
const messagesWithFileReferences = replaceBase64ImagesWithFileReferences(
messages,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
)
// Only add to runtime chat history if this is the first node
const inputMessages = []
if (!runtimeChatHistory.length) {
if (runtimeImageMessagesWithFileRef.length) {
inputMessages.push(...runtimeImageMessagesWithFileRef)
}
if (input && typeof input === 'string') {
inputMessages.push({ role: 'user', content: question })
}
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: { messages: messagesWithFileReferences },
output: {
conditions,
content: typeof response.content === 'string' ? response.content : JSON.stringify(response.content),
timeMetadata: {
start: startTime,
end: endTime,
delta: timeDelta
}
},
state,
chatHistory: [...inputMessages]
}
return returnOutput
} catch (error) {
if (options.analyticHandlers && llmIds) {
await options.analyticHandlers.onLLMError(llmIds, error instanceof Error ? error.message : String(error))
}
if (error instanceof Error && error.message === 'Aborted') {
throw error
}
throw new Error(`Error in Condition Agent node: ${error instanceof Error ? error.message : String(error)}`)
}
}
/**
* Handles memory management based on the specified memory type
*/
private async handleMemory({
messages,
memoryType,
pastChatHistory,
runtimeChatHistory,
llmNodeInstance,
nodeData,
input,
abortController,
options,
modelConfig,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
}: {
messages: BaseMessageLike[]
memoryType: string
pastChatHistory: BaseMessageLike[]
runtimeChatHistory: BaseMessageLike[]
llmNodeInstance: BaseChatModel
nodeData: INodeData
input: string
abortController: AbortController
options: ICommonObject
modelConfig: ICommonObject
runtimeImageMessagesWithFileRef: BaseMessageLike[]
pastImageMessagesWithFileRef: BaseMessageLike[]
}): Promise<void> {
const { updatedPastMessages, transformedPastMessages } = await getPastChatHistoryImageMessages(pastChatHistory, options)
pastChatHistory = updatedPastMessages
pastImageMessagesWithFileRef.push(...transformedPastMessages)
let pastMessages = [...pastChatHistory, ...runtimeChatHistory]
if (!runtimeChatHistory.length) {
/*
* If this is the first node:
* - Add images to messages if exist
*/
if (options.uploads) {
const imageContents = await getUniqueImageMessages(options, messages, modelConfig)
if (imageContents) {
const { imageMessageWithBase64, imageMessageWithFileRef } = imageContents
pastMessages.push(imageMessageWithBase64)
runtimeImageMessagesWithFileRef.push(imageMessageWithFileRef)
}
}
}
const { updatedMessages, transformedMessages } = await processMessagesWithImages(pastMessages, options)
pastMessages = updatedMessages
pastImageMessagesWithFileRef.push(...transformedMessages)
if (pastMessages.length > 0) {
if (memoryType === 'windowSize') {
// Window memory: Keep the last N messages
const windowSize = nodeData.inputs?.conditionAgentMemoryWindowSize as number
const windowedMessages = pastMessages.slice(-windowSize * 2)
messages.push(...windowedMessages)
} else if (memoryType === 'conversationSummary') {
// Summary memory: Summarize all past messages
const summary = await llmNodeInstance.invoke(
[
{
role: 'user',
content: DEFAULT_SUMMARIZER_TEMPLATE.replace(
'{conversation}',
pastMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
)
}
],
{ signal: abortController?.signal }
)
messages.push({ role: 'assistant', content: summary.content as string })
} else if (memoryType === 'conversationSummaryBuffer') {
// Summary buffer: Summarize messages that exceed token limit
await this.handleSummaryBuffer(messages, pastMessages, llmNodeInstance, nodeData, abortController)
} else {
// Default: Use all messages
messages.push(...pastMessages)
}
}
messages.push({
role: 'user',
content: input
})
}
/**
* Handles conversation summary buffer memory type
*/
private async handleSummaryBuffer(
messages: BaseMessageLike[],
pastMessages: BaseMessageLike[],
llmNodeInstance: BaseChatModel,
nodeData: INodeData,
abortController: AbortController
): Promise<void> {
const maxTokenLimit = (nodeData.inputs?.conditionAgentMemoryMaxTokenLimit as number) || 2000
// Convert past messages to a format suitable for token counting
const messagesString = pastMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
const tokenCount = await llmNodeInstance.getNumTokens(messagesString)
if (tokenCount > maxTokenLimit) {
// Calculate how many messages to summarize (messages that exceed the token limit)
let currBufferLength = tokenCount
const messagesToSummarize = []
const remainingMessages = [...pastMessages]
// Remove messages from the beginning until we're under the token limit
while (currBufferLength > maxTokenLimit && remainingMessages.length > 0) {
const poppedMessage = remainingMessages.shift()
if (poppedMessage) {
messagesToSummarize.push(poppedMessage)
// Recalculate token count for remaining messages
const remainingMessagesString = remainingMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
currBufferLength = await llmNodeInstance.getNumTokens(remainingMessagesString)
}
}
// Summarize the messages that were removed
const messagesToSummarizeString = messagesToSummarize.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
const summary = await llmNodeInstance.invoke(
[
{
role: 'user',
content: DEFAULT_SUMMARIZER_TEMPLATE.replace('{conversation}', messagesToSummarizeString)
}
],
{ signal: abortController?.signal }
)
// Add summary as a system message at the beginning, then add remaining messages
messages.push({ role: 'system', content: `Previous conversation summary: ${summary.content}` })
messages.push(...remainingMessages)
} else {
// If under token limit, use all messages
messages.push(...pastMessages)
}
}
}
module.exports = { nodeClass: ConditionAgent_Agentflow }
@@ -0,0 +1,241 @@
import { DataSource } from 'typeorm'
import {
ICommonObject,
IDatabaseEntity,
INode,
INodeData,
INodeOptionsValue,
INodeParams,
IServerSideEventStreamer
} from '../../../src/Interface'
import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils'
import { NodeVM } from '@flowiseai/nodevm'
import { updateFlowState } from '../utils'
interface ICustomFunctionInputVariables {
variableName: string
variableValue: string
}
const exampleFunc = `/*
* You can use any libraries imported in Flowise
* You can use properties specified in Input Schema as variables. Ex: Property = userid, Variable = $userid
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input, $flow.state
* You can get custom variables: $vars.<variable-name>
* Must return a string value at the end of function
*/
const fetch = require('node-fetch');
const url = 'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true';
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
try {
const response = await fetch(url, options);
const text = await response.text();
return text;
} catch (error) {
console.error(error);
return '';
}`
class CustomFunction_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
hideOutput: boolean
hint: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Custom Function'
this.name = 'customFunctionAgentflow'
this.version = 1.0
this.type = 'CustomFunction'
this.category = 'Agent Flows'
this.description = 'Execute custom function'
this.baseClasses = [this.type]
this.color = '#E4B7FF'
this.inputs = [
{
label: 'Input Variables',
name: 'customFunctionInputVariables',
description: 'Input variables can be used in the function with prefix $. For example: $foo',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Variable Name',
name: 'variableName',
type: 'string'
},
{
label: 'Variable Value',
name: 'variableValue',
type: 'string',
acceptVariable: true
}
]
},
{
label: 'Javascript Function',
name: 'customFunctionJavascriptFunction',
type: 'code',
codeExample: exampleFunc,
description: 'The function to execute. Must return a string or an object that can be converted to a string.'
},
{
label: 'Update Flow State',
name: 'customFunctionUpdateState',
description: 'Update runtime state during the execution of the workflow',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
name: 'value',
type: 'string',
acceptVariable: true,
acceptNodeOutputAsVariable: true
}
]
}
]
}
//@ts-ignore
loadMethods = {
async listRuntimeStateKeys(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const previousNodes = options.previousNodes as ICommonObject[]
const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow')
const state = startAgentflowNode?.inputs?.startState as ICommonObject[]
return state.map((item) => ({ label: item.key, name: item.key }))
}
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const javascriptFunction = nodeData.inputs?.customFunctionJavascriptFunction as string
const functionInputVariables = nodeData.inputs?.customFunctionInputVariables as ICustomFunctionInputVariables[]
const _customFunctionUpdateState = nodeData.inputs?.customFunctionUpdateState
const state = options.agentflowRuntime?.state as ICommonObject
const chatId = options.chatId as string
const isLastNode = options.isLastNode as boolean
const isStreamable = isLastNode && options.sseStreamer !== undefined
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
// Update flow state if needed
let newState = { ...state }
if (_customFunctionUpdateState && Array.isArray(_customFunctionUpdateState) && _customFunctionUpdateState.length > 0) {
newState = updateFlowState(state, _customFunctionUpdateState)
}
const variables = await getVars(appDataSource, databaseEntities, nodeData)
const flow = {
chatflowId: options.chatflowid,
sessionId: options.sessionId,
chatId: options.chatId,
input
}
let sandbox: any = {
$input: input,
util: undefined,
Symbol: undefined,
child_process: undefined,
fs: undefined,
process: undefined
}
sandbox['$vars'] = prepareSandboxVars(variables)
sandbox['$flow'] = flow
for (const item of functionInputVariables) {
const variableName = item.variableName
const variableValue = item.variableValue
sandbox[`$${variableName}`] = variableValue
}
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
: defaultAllowBuiltInDep
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
const deps = availableDependencies.concat(externalDeps)
const nodeVMOptions = {
console: 'inherit',
sandbox,
require: {
external: { modules: deps },
builtin: builtinDeps
},
eval: false,
wasm: false,
timeout: 10000
} as any
const vm = new NodeVM(nodeVMOptions)
try {
const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname)
let finalOutput = response
if (typeof response === 'object') {
finalOutput = JSON.stringify(response, null, 2)
}
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer
sseStreamer.streamTokenEvent(chatId, finalOutput)
}
// Process template variables in state
if (newState && Object.keys(newState).length > 0) {
for (const key in newState) {
if (newState[key].toString().includes('{{ output }}')) {
newState[key] = finalOutput
}
}
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {
inputVariables: functionInputVariables,
code: javascriptFunction
},
output: {
content: finalOutput
},
state: newState
}
return returnOutput
} catch (e) {
throw new Error(e)
}
}
}
module.exports = { nodeClass: CustomFunction_Agentflow }
@@ -0,0 +1,67 @@
import { ICommonObject, INode, INodeData, INodeParams, IServerSideEventStreamer } from '../../../src/Interface'
class DirectReply_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
hideOutput: boolean
hint: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Direct Reply'
this.name = 'directReplyAgentflow'
this.version = 1.0
this.type = 'DirectReply'
this.category = 'Agent Flows'
this.description = 'Directly reply to the user with a message'
this.baseClasses = [this.type]
this.color = '#4DDBBB'
this.hideOutput = true
this.inputs = [
{
label: 'Message',
name: 'directReplyMessage',
type: 'string',
rows: 4,
acceptVariable: true
}
]
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const directReplyMessage = nodeData.inputs?.directReplyMessage as string
const state = options.agentflowRuntime?.state as ICommonObject
const chatId = options.chatId as string
const isLastNode = options.isLastNode as boolean
const isStreamable = isLastNode && options.sseStreamer !== undefined
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer
sseStreamer.streamTokenEvent(chatId, directReplyMessage)
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {},
output: {
content: directReplyMessage
},
state
}
return returnOutput
}
}
module.exports = { nodeClass: DirectReply_Agentflow }
@@ -0,0 +1,297 @@
import {
ICommonObject,
IDatabaseEntity,
INode,
INodeData,
INodeOptionsValue,
INodeParams,
IServerSideEventStreamer
} from '../../../src/Interface'
import axios, { AxiosRequestConfig } from 'axios'
import { getCredentialData, getCredentialParam } from '../../../src/utils'
import { DataSource } from 'typeorm'
import { BaseMessageLike } from '@langchain/core/messages'
import { updateFlowState } from '../utils'
class ExecuteFlow_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Execute Flow'
this.name = 'executeFlowAgentflow'
this.version = 1.0
this.type = 'ExecuteFlow'
this.category = 'Agent Flows'
this.description = 'Execute another flow'
this.baseClasses = [this.type]
this.color = '#a3b18a'
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['chatflowApi'],
optional: true
}
this.inputs = [
{
label: 'Select Flow',
name: 'executeFlowSelectedFlow',
type: 'asyncOptions',
loadMethod: 'listFlows'
},
{
label: 'Input',
name: 'executeFlowInput',
type: 'string',
rows: 4,
acceptVariable: true
},
{
label: 'Override Config',
name: 'executeFlowOverrideConfig',
description: 'Override the config passed to the flow',
type: 'json',
optional: true
},
{
label: 'Base URL',
name: 'executeFlowBaseURL',
type: 'string',
description:
'Base URL to Flowise. By default, it is the URL of the incoming request. Useful when you need to execute flow through an alternative route.',
placeholder: 'http://localhost:3000',
optional: true
},
{
label: 'Return Response As',
name: 'executeFlowReturnResponseAs',
type: 'options',
options: [
{
label: 'User Message',
name: 'userMessage'
},
{
label: 'Assistant Message',
name: 'assistantMessage'
}
],
default: 'userMessage'
},
{
label: 'Update Flow State',
name: 'executeFlowUpdateState',
description: 'Update runtime state during the execution of the workflow',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
name: 'value',
type: 'string',
acceptVariable: true,
acceptNodeOutputAsVariable: true
}
]
}
]
}
//@ts-ignore
loadMethods = {
async listFlows(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const returnData: INodeOptionsValue[] = []
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
if (appDataSource === undefined || !appDataSource) {
return returnData
}
const chatflows = await appDataSource.getRepository(databaseEntities['ChatFlow']).find()
for (let i = 0; i < chatflows.length; i += 1) {
let cfType = 'Chatflow'
if (chatflows[i].type === 'AGENTFLOW') {
cfType = 'Agentflow V2'
} else if (chatflows[i].type === 'MULTIAGENT') {
cfType = 'Agentflow V1'
}
const data = {
label: chatflows[i].name,
name: chatflows[i].id,
description: cfType
} as INodeOptionsValue
returnData.push(data)
}
// order by label
return returnData.sort((a, b) => a.label.localeCompare(b.label))
},
async listRuntimeStateKeys(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const previousNodes = options.previousNodes as ICommonObject[]
const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow')
const state = startAgentflowNode?.inputs?.startState as ICommonObject[]
return state.map((item) => ({ label: item.key, name: item.key }))
}
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const baseURL = (nodeData.inputs?.executeFlowBaseURL as string) || (options.baseURL as string)
const selectedFlowId = nodeData.inputs?.executeFlowSelectedFlow as string
const flowInput = nodeData.inputs?.executeFlowInput as string
const returnResponseAs = nodeData.inputs?.executeFlowReturnResponseAs as string
const _executeFlowUpdateState = nodeData.inputs?.executeFlowUpdateState
const overrideConfig =
typeof nodeData.inputs?.executeFlowOverrideConfig === 'string' &&
nodeData.inputs.executeFlowOverrideConfig.startsWith('{') &&
nodeData.inputs.executeFlowOverrideConfig.endsWith('}')
? JSON.parse(nodeData.inputs.executeFlowOverrideConfig)
: nodeData.inputs?.executeFlowOverrideConfig
const state = options.agentflowRuntime?.state as ICommonObject
const runtimeChatHistory = (options.agentflowRuntime?.chatHistory as BaseMessageLike[]) ?? []
const isLastNode = options.isLastNode as boolean
const sseStreamer: IServerSideEventStreamer | undefined = options.sseStreamer
try {
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const chatflowApiKey = getCredentialParam('chatflowApiKey', credentialData, nodeData)
if (selectedFlowId === options.chatflowid) throw new Error('Cannot call the same agentflow!')
let headers: Record<string, string> = {
'Content-Type': 'application/json'
}
if (chatflowApiKey) headers = { ...headers, Authorization: `Bearer ${chatflowApiKey}` }
const finalUrl = `${baseURL}/api/v1/prediction/${selectedFlowId}`
const requestConfig: AxiosRequestConfig = {
method: 'POST',
url: finalUrl,
headers,
data: {
question: flowInput,
chatId: options.chatId,
overrideConfig
}
}
const response = await axios(requestConfig)
let resultText = ''
if (response.data.text) resultText = response.data.text
else if (response.data.json) resultText = '```json\n' + JSON.stringify(response.data.json, null, 2)
else resultText = JSON.stringify(response.data, null, 2)
if (isLastNode && sseStreamer) {
sseStreamer.streamTokenEvent(options.chatId, resultText)
}
// Update flow state if needed
let newState = { ...state }
if (_executeFlowUpdateState && Array.isArray(_executeFlowUpdateState) && _executeFlowUpdateState.length > 0) {
newState = updateFlowState(state, _executeFlowUpdateState)
}
// Process template variables in state
if (newState && Object.keys(newState).length > 0) {
for (const key in newState) {
if (newState[key].toString().includes('{{ output }}')) {
newState[key] = resultText
}
}
}
// Only add to runtime chat history if this is the first node
const inputMessages = []
if (!runtimeChatHistory.length) {
inputMessages.push({ role: 'user', content: flowInput })
}
let returnRole = 'user'
if (returnResponseAs === 'assistantMessage') {
returnRole = 'assistant'
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {
messages: [
{
role: 'user',
content: flowInput
}
]
},
output: {
content: resultText
},
state: newState,
chatHistory: [
...inputMessages,
{
role: returnRole,
content: resultText,
name: nodeData?.label ? nodeData?.label.toLowerCase().replace(/\s/g, '_').trim() : nodeData?.id
}
]
}
return returnOutput
} catch (error) {
console.error('ExecuteFlow Error:', error)
// Format error response
const errorResponse: any = {
id: nodeData.id,
name: this.name,
input: {
messages: [
{
role: 'user',
content: flowInput
}
]
},
error: {
name: error.name || 'Error',
message: error.message || 'An error occurred during the execution of the flow'
},
state
}
// Add more error details if available
if (error.response) {
errorResponse.error.status = error.response.status
errorResponse.error.statusText = error.response.statusText
errorResponse.error.data = error.response.data
errorResponse.error.headers = error.response.headers
}
throw new Error(error)
}
}
}
module.exports = { nodeClass: ExecuteFlow_Agentflow }
@@ -0,0 +1,368 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import axios, { AxiosRequestConfig, Method, ResponseType } from 'axios'
import FormData from 'form-data'
import * as querystring from 'querystring'
import { getCredentialData, getCredentialParam } from '../../../src/utils'
class HTTP_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'HTTP'
this.name = 'httpAgentflow'
this.version = 1.0
this.type = 'HTTP'
this.category = 'Agent Flows'
this.description = 'Send a HTTP request'
this.baseClasses = [this.type]
this.color = '#FF7F7F'
this.credential = {
label: 'HTTP Credential',
name: 'credential',
type: 'credential',
credentialNames: ['httpBasicAuth', 'httpBearerToken', 'httpApiKey'],
optional: true
}
this.inputs = [
{
label: 'Method',
name: 'method',
type: 'options',
options: [
{
label: 'GET',
name: 'GET'
},
{
label: 'POST',
name: 'POST'
},
{
label: 'PUT',
name: 'PUT'
},
{
label: 'DELETE',
name: 'DELETE'
},
{
label: 'PATCH',
name: 'PATCH'
}
],
default: 'GET'
},
{
label: 'URL',
name: 'url',
type: 'string'
},
{
label: 'Headers',
name: 'headers',
type: 'array',
array: [
{
label: 'Key',
name: 'key',
type: 'string',
default: ''
},
{
label: 'Value',
name: 'value',
type: 'string',
default: ''
}
],
optional: true
},
{
label: 'Query Params',
name: 'queryParams',
type: 'array',
array: [
{
label: 'Key',
name: 'key',
type: 'string',
default: ''
},
{
label: 'Value',
name: 'value',
type: 'string',
default: ''
}
],
optional: true
},
{
label: 'Body Type',
name: 'bodyType',
type: 'options',
options: [
{
label: 'JSON',
name: 'json'
},
{
label: 'Raw',
name: 'raw'
},
{
label: 'Form Data',
name: 'formData'
},
{
label: 'x-www-form-urlencoded',
name: 'xWwwFormUrlencoded'
}
],
optional: true
},
{
label: 'Body',
name: 'body',
type: 'string',
acceptVariable: true,
rows: 4,
show: {
bodyType: ['raw', 'json']
},
optional: true
},
{
label: 'Body',
name: 'body',
type: 'array',
show: {
bodyType: ['xWwwFormUrlencoded', 'formData']
},
array: [
{
label: 'Key',
name: 'key',
type: 'string',
default: ''
},
{
label: 'Value',
name: 'value',
type: 'string',
default: ''
}
],
optional: true
},
{
label: 'Response Type',
name: 'responseType',
type: 'options',
options: [
{
label: 'JSON',
name: 'json'
},
{
label: 'Text',
name: 'text'
},
{
label: 'Array Buffer',
name: 'arraybuffer'
},
{
label: 'Raw (Base64)',
name: 'base64'
}
],
optional: true
}
]
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const method = nodeData.inputs?.method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
const url = nodeData.inputs?.url as string
const headers = nodeData.inputs?.headers as ICommonObject
const queryParams = nodeData.inputs?.queryParams as ICommonObject
const bodyType = nodeData.inputs?.bodyType as 'json' | 'raw' | 'formData' | 'xWwwFormUrlencoded'
const body = nodeData.inputs?.body as ICommonObject | string | ICommonObject[]
const responseType = nodeData.inputs?.responseType as 'json' | 'text' | 'arraybuffer' | 'base64'
const state = options.agentflowRuntime?.state as ICommonObject
try {
// Prepare headers
const requestHeaders: Record<string, string> = {}
// Add headers from inputs
if (headers && Array.isArray(headers)) {
for (const header of headers) {
if (header.key && header.value) {
requestHeaders[header.key] = header.value
}
}
}
// Add credentials if provided
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
if (credentialData && Object.keys(credentialData).length !== 0) {
const basicAuthUsername = getCredentialParam('username', credentialData, nodeData)
const basicAuthPassword = getCredentialParam('password', credentialData, nodeData)
const bearerToken = getCredentialParam('token', credentialData, nodeData)
const apiKeyName = getCredentialParam('key', credentialData, nodeData)
const apiKeyValue = getCredentialParam('value', credentialData, nodeData)
// Determine which type of auth to use based on available credentials
if (basicAuthUsername && basicAuthPassword) {
// Basic Auth
const auth = Buffer.from(`${basicAuthUsername}:${basicAuthPassword}`).toString('base64')
requestHeaders['Authorization'] = `Basic ${auth}`
} else if (bearerToken) {
// Bearer Token
requestHeaders['Authorization'] = `Bearer ${bearerToken}`
} else if (apiKeyName && apiKeyValue) {
// API Key in header
requestHeaders[apiKeyName] = apiKeyValue
}
}
// Prepare query parameters
let queryString = ''
if (queryParams && Array.isArray(queryParams)) {
const params = new URLSearchParams()
for (const param of queryParams) {
if (param.key && param.value) {
params.append(param.key, param.value)
}
}
queryString = params.toString()
}
// Build final URL with query parameters
const finalUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url
// Prepare request config
const requestConfig: AxiosRequestConfig = {
method: method as Method,
url: finalUrl,
headers: requestHeaders,
responseType: (responseType || 'json') as ResponseType
}
// Handle request body based on body type
if (method !== 'GET' && body) {
switch (bodyType) {
case 'json':
requestConfig.data = typeof body === 'string' ? JSON.parse(body) : body
requestHeaders['Content-Type'] = 'application/json'
break
case 'raw':
requestConfig.data = body
break
case 'formData': {
const formData = new FormData()
if (Array.isArray(body) && body.length > 0) {
for (const item of body) {
formData.append(item.key, item.value)
}
}
requestConfig.data = formData
break
}
case 'xWwwFormUrlencoded':
requestConfig.data = querystring.stringify(typeof body === 'string' ? JSON.parse(body) : body)
requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded'
break
}
}
// Make the HTTP request
const response = await axios(requestConfig)
// Process response based on response type
let responseData
if (responseType === 'base64' && response.data) {
responseData = Buffer.from(response.data, 'binary').toString('base64')
} else {
responseData = response.data
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {
http: {
method,
url,
headers,
queryParams,
bodyType,
body,
responseType
}
},
output: {
http: {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: response.headers
}
},
state
}
return returnOutput
} catch (error) {
console.error('HTTP Request Error:', error)
// Format error response
const errorResponse: any = {
id: nodeData.id,
name: this.name,
input: {
http: {
method,
url,
headers,
queryParams,
bodyType,
body,
responseType
}
},
error: {
name: error.name || 'Error',
message: error.message || 'An error occurred during the HTTP request'
},
state
}
// Add more error details if available
if (error.response) {
errorResponse.error.status = error.response.status
errorResponse.error.statusText = error.response.statusText
errorResponse.error.data = error.response.data
errorResponse.error.headers = error.response.headers
}
throw new Error(error)
}
}
}
module.exports = { nodeClass: HTTP_Agentflow }
@@ -0,0 +1,271 @@
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
import {
ICommonObject,
ICondition,
IHumanInput,
INode,
INodeData,
INodeOptionsValue,
INodeOutputsValue,
INodeParams,
IServerSideEventStreamer
} from '../../../src/Interface'
import { AIMessageChunk, BaseMessageLike } from '@langchain/core/messages'
import { DEFAULT_HUMAN_INPUT_DESCRIPTION, DEFAULT_HUMAN_INPUT_DESCRIPTION_HTML } from '../prompt'
class HumanInput_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Human Input'
this.name = 'humanInputAgentflow'
this.version = 1.0
this.type = 'HumanInput'
this.category = 'Agent Flows'
this.description = 'Request human input, approval or rejection during execution'
this.color = '#6E6EFD'
this.baseClasses = [this.type]
this.inputs = [
{
label: 'Description Type',
name: 'humanInputDescriptionType',
type: 'options',
options: [
{
label: 'Fixed',
name: 'fixed',
description: 'Specify a fixed description'
},
{
label: 'Dynamic',
name: 'dynamic',
description: 'Use LLM to generate a description'
}
]
},
{
label: 'Description',
name: 'humanInputDescription',
type: 'string',
placeholder: 'Are you sure you want to proceed?',
acceptVariable: true,
rows: 4,
show: {
humanInputDescriptionType: 'fixed'
}
},
{
label: 'Model',
name: 'humanInputModel',
type: 'asyncOptions',
loadMethod: 'listModels',
loadConfig: true,
show: {
humanInputDescriptionType: 'dynamic'
}
},
{
label: 'Prompt',
name: 'humanInputModelPrompt',
type: 'string',
default: DEFAULT_HUMAN_INPUT_DESCRIPTION_HTML,
acceptVariable: true,
generateInstruction: true,
rows: 4,
show: {
humanInputDescriptionType: 'dynamic'
}
},
{
label: 'Enable Feedback',
name: 'humanInputEnableFeedback',
type: 'boolean',
default: true
}
]
this.outputs = [
{
label: 'Proceed',
name: 'proceed'
},
{
label: 'Reject',
name: 'reject'
}
]
}
//@ts-ignore
loadMethods = {
async listModels(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const componentNodes = options.componentNodes as {
[key: string]: INode
}
const returnOptions: INodeOptionsValue[] = []
for (const nodeName in componentNodes) {
const componentNode = componentNodes[nodeName]
if (componentNode.category === 'Chat Models') {
if (componentNode.tags?.includes('LlamaIndex')) {
continue
}
returnOptions.push({
label: componentNode.label,
name: nodeName,
imageSrc: componentNode.icon
})
}
}
return returnOptions
}
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const _humanInput = nodeData.inputs?.humanInput
const humanInput: IHumanInput = typeof _humanInput === 'string' ? JSON.parse(_humanInput) : _humanInput
const humanInputEnableFeedback = nodeData.inputs?.humanInputEnableFeedback as boolean
let humanInputDescriptionType = nodeData.inputs?.humanInputDescriptionType as string
const model = nodeData.inputs?.humanInputModel as string
const modelConfig = nodeData.inputs?.humanInputModelConfig as ICommonObject
const _humanInputModelPrompt = nodeData.inputs?.humanInputModelPrompt as string
const humanInputModelPrompt = _humanInputModelPrompt ? _humanInputModelPrompt : DEFAULT_HUMAN_INPUT_DESCRIPTION
// Extract runtime state and history
const state = options.agentflowRuntime?.state as ICommonObject
const pastChatHistory = (options.pastChatHistory as BaseMessageLike[]) ?? []
const runtimeChatHistory = (options.agentflowRuntime?.chatHistory as BaseMessageLike[]) ?? []
const chatId = options.chatId as string
const isStreamable = options.sseStreamer !== undefined
if (humanInput) {
const outcomes: Partial<ICondition>[] & Partial<IHumanInput>[] = [
{
type: 'proceed',
startNodeId: humanInput?.startNodeId,
feedback: humanInputEnableFeedback && humanInput?.feedback ? humanInput.feedback : undefined,
isFulfilled: false
},
{
type: 'reject',
startNodeId: humanInput?.startNodeId,
feedback: humanInputEnableFeedback && humanInput?.feedback ? humanInput.feedback : undefined,
isFulfilled: false
}
]
// Only one outcome can be fulfilled at a time
switch (humanInput?.type) {
case 'proceed':
outcomes[0].isFulfilled = true
break
case 'reject':
outcomes[1].isFulfilled = true
break
}
const messages = [
...pastChatHistory,
...runtimeChatHistory,
{
role: 'user',
content: humanInput.feedback || humanInput.type
}
]
const input = { ...humanInput, messages }
const output = { conditions: outcomes }
const nodeOutput = {
id: nodeData.id,
name: this.name,
input,
output,
state
}
if (humanInput.feedback) {
;(nodeOutput as any).chatHistory = [{ role: 'user', content: humanInput.feedback }]
}
return nodeOutput
} else {
let humanInputDescription = ''
if (humanInputDescriptionType === 'fixed') {
humanInputDescription = (nodeData.inputs?.humanInputDescription as string) || 'Do you want to proceed?'
const messages = [...pastChatHistory, ...runtimeChatHistory]
// Find the last message in the messages array
const lastMessage = (messages[messages.length - 1] as any).content || ''
humanInputDescription = `${lastMessage}\n\n${humanInputDescription}`
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
sseStreamer.streamTokenEvent(chatId, humanInputDescription)
}
} else {
if (model && modelConfig) {
const nodeInstanceFilePath = options.componentNodes[model].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
const newNodeData = {
...nodeData,
credential: modelConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...modelConfig
}
}
const llmNodeInstance = (await newNodeInstance.init(newNodeData, '', options)) as BaseChatModel
const messages = [
...pastChatHistory,
...runtimeChatHistory,
{
role: 'user',
content: humanInputModelPrompt || DEFAULT_HUMAN_INPUT_DESCRIPTION
}
]
let response: AIMessageChunk = new AIMessageChunk('')
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
for await (const chunk of await llmNodeInstance.stream(messages)) {
sseStreamer.streamTokenEvent(chatId, chunk.content.toString())
response = response.concat(chunk)
}
humanInputDescription = response.content as string
} else {
const response = await llmNodeInstance.invoke(messages)
humanInputDescription = response.content as string
}
}
}
const input = { messages: [...pastChatHistory, ...runtimeChatHistory], humanInputEnableFeedback }
const output = { content: humanInputDescription }
const nodeOutput = {
id: nodeData.id,
name: this.name,
input,
output,
state,
chatHistory: [{ role: 'assistant', content: humanInputDescription }]
}
return nodeOutput
}
}
}
module.exports = { nodeClass: HumanInput_Agentflow }
@@ -0,0 +1,17 @@
export interface ILLMMessage {
role: 'system' | 'assistant' | 'user' | 'tool' | 'developer'
content: string
}
export interface IStructuredOutput {
key: string
type: 'string' | 'stringArray' | 'number' | 'boolean' | 'enum' | 'jsonArray'
enumValues?: string
description?: string
jsonSchema?: string
}
export interface IFlowState {
key: string
value: string
}
@@ -0,0 +1,69 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class Iteration_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Iteration'
this.name = 'iterationAgentflow'
this.version = 1.0
this.type = 'Iteration'
this.category = 'Agent Flows'
this.description = 'Execute the nodes within the iteration block through N iterations'
this.baseClasses = [this.type]
this.color = '#9C89B8'
this.inputs = [
{
label: 'Array Input',
name: 'iterationInput',
type: 'string',
description: 'The input array to iterate over',
acceptVariable: true,
rows: 4
}
]
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const iterationInput = nodeData.inputs?.iterationInput
// Helper function to clean JSON strings with redundant backslashes
const cleanJsonString = (str: string): string => {
return str.replace(/\\(["'[\]{}])/g, '$1')
}
const iterationInputArray =
typeof iterationInput === 'string' && iterationInput !== '' ? JSON.parse(cleanJsonString(iterationInput)) : iterationInput
if (!iterationInputArray || !Array.isArray(iterationInputArray)) {
throw new Error('Invalid input array')
}
const state = options.agentflowRuntime?.state as ICommonObject
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {
iterationInput: iterationInputArray
},
output: {},
state
}
return returnOutput
}
}
module.exports = { nodeClass: Iteration_Agentflow }
@@ -0,0 +1,981 @@
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface'
import { AIMessageChunk, BaseMessageLike, MessageContentText } from '@langchain/core/messages'
import { DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt'
import { z } from 'zod'
import { AnalyticHandler } from '../../../src/handler'
import { ILLMMessage, IStructuredOutput } from '../Interface.Agentflow'
import {
getPastChatHistoryImageMessages,
getUniqueImageMessages,
processMessagesWithImages,
replaceBase64ImagesWithFileReferences,
updateFlowState
} from '../utils'
import { get } from 'lodash'
class LLM_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'LLM'
this.name = 'llmAgentflow'
this.version = 1.0
this.type = 'LLM'
this.category = 'Agent Flows'
this.description = 'Large language models to analyze user-provided inputs and generate responses'
this.color = '#64B5F6'
this.baseClasses = [this.type]
this.inputs = [
{
label: 'Model',
name: 'llmModel',
type: 'asyncOptions',
loadMethod: 'listModels',
loadConfig: true
},
{
label: 'Messages',
name: 'llmMessages',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Role',
name: 'role',
type: 'options',
options: [
{
label: 'System',
name: 'system'
},
{
label: 'Assistant',
name: 'assistant'
},
{
label: 'Developer',
name: 'developer'
},
{
label: 'User',
name: 'user'
}
]
},
{
label: 'Content',
name: 'content',
type: 'string',
acceptVariable: true,
generateInstruction: true,
rows: 4
}
]
},
{
label: 'Enable Memory',
name: 'llmEnableMemory',
type: 'boolean',
description: 'Enable memory for the conversation thread',
default: true,
optional: true
},
{
label: 'Memory Type',
name: 'llmMemoryType',
type: 'options',
options: [
{
label: 'All Messages',
name: 'allMessages',
description: 'Retrieve all messages from the conversation'
},
{
label: 'Window Size',
name: 'windowSize',
description: 'Uses a fixed window size to surface the last N messages'
},
{
label: 'Conversation Summary',
name: 'conversationSummary',
description: 'Summarizes the whole conversation'
},
{
label: 'Conversation Summary Buffer',
name: 'conversationSummaryBuffer',
description: 'Summarize conversations once token limit is reached. Default to 2000'
}
],
optional: true,
default: 'allMessages',
show: {
llmEnableMemory: true
}
},
{
label: 'Window Size',
name: 'llmMemoryWindowSize',
type: 'number',
default: '20',
description: 'Uses a fixed window size to surface the last N messages',
show: {
llmMemoryType: 'windowSize'
}
},
{
label: 'Max Token Limit',
name: 'llmMemoryMaxTokenLimit',
type: 'number',
default: '2000',
description: 'Summarize conversations once token limit is reached. Default to 2000',
show: {
llmMemoryType: 'conversationSummaryBuffer'
}
},
{
label: 'Input Message',
name: 'llmUserMessage',
type: 'string',
description: 'Add an input message as user message at the end of the conversation',
rows: 4,
optional: true,
acceptVariable: true,
show: {
llmEnableMemory: true
}
},
{
label: 'Return Response As',
name: 'llmReturnResponseAs',
type: 'options',
options: [
{
label: 'User Message',
name: 'userMessage'
},
{
label: 'Assistant Message',
name: 'assistantMessage'
}
],
default: 'userMessage'
},
{
label: 'JSON Structured Output',
name: 'llmStructuredOutput',
description: 'Instruct the LLM to give output in a JSON structured schema',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'string'
},
{
label: 'Type',
name: 'type',
type: 'options',
options: [
{
label: 'String',
name: 'string'
},
{
label: 'String Array',
name: 'stringArray'
},
{
label: 'Number',
name: 'number'
},
{
label: 'Boolean',
name: 'boolean'
},
{
label: 'Enum',
name: 'enum'
},
{
label: 'JSON Array',
name: 'jsonArray'
}
]
},
{
label: 'Enum Values',
name: 'enumValues',
type: 'string',
placeholder: 'value1, value2, value3',
description: 'Enum values. Separated by comma',
optional: true,
show: {
'llmStructuredOutput[$index].type': 'enum'
}
},
{
label: 'JSON Schema',
name: 'jsonSchema',
type: 'code',
placeholder: `{
"answer": {
"type": "string",
"description": "Value of the answer"
},
"reason": {
"type": "string",
"description": "Reason for the answer"
},
"optional": {
"type": "boolean"
},
"count": {
"type": "number"
},
"children": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string",
"description": "Value of the children's answer"
}
}
}
}
}`,
description: 'JSON schema for the structured output',
optional: true,
show: {
'llmStructuredOutput[$index].type': 'jsonArray'
}
},
{
label: 'Description',
name: 'description',
type: 'string',
placeholder: 'Description of the key'
}
]
},
{
label: 'Update Flow State',
name: 'llmUpdateState',
description: 'Update runtime state during the execution of the workflow',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
name: 'value',
type: 'string',
acceptVariable: true,
acceptNodeOutputAsVariable: true
}
]
}
]
}
//@ts-ignore
loadMethods = {
async listModels(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const componentNodes = options.componentNodes as {
[key: string]: INode
}
const returnOptions: INodeOptionsValue[] = []
for (const nodeName in componentNodes) {
const componentNode = componentNodes[nodeName]
if (componentNode.category === 'Chat Models') {
if (componentNode.tags?.includes('LlamaIndex')) {
continue
}
returnOptions.push({
label: componentNode.label,
name: nodeName,
imageSrc: componentNode.icon
})
}
}
return returnOptions
},
async listRuntimeStateKeys(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const previousNodes = options.previousNodes as ICommonObject[]
const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow')
const state = startAgentflowNode?.inputs?.startState as ICommonObject[]
return state.map((item) => ({ label: item.key, name: item.key }))
}
}
async run(nodeData: INodeData, input: string | Record<string, any>, options: ICommonObject): Promise<any> {
let llmIds: ICommonObject | undefined
let analyticHandlers = options.analyticHandlers as AnalyticHandler
try {
const abortController = options.abortController as AbortController
// Extract input parameters
const model = nodeData.inputs?.llmModel as string
const modelConfig = nodeData.inputs?.llmModelConfig as ICommonObject
if (!model) {
throw new Error('Model is required')
}
// Extract memory and configuration options
const enableMemory = nodeData.inputs?.llmEnableMemory as boolean
const memoryType = nodeData.inputs?.llmMemoryType as string
const userMessage = nodeData.inputs?.llmUserMessage as string
const _llmUpdateState = nodeData.inputs?.llmUpdateState
const _llmStructuredOutput = nodeData.inputs?.llmStructuredOutput
const llmMessages = (nodeData.inputs?.llmMessages as unknown as ILLMMessage[]) ?? []
// Extract runtime state and history
const state = options.agentflowRuntime?.state as ICommonObject
const pastChatHistory = (options.pastChatHistory as BaseMessageLike[]) ?? []
const runtimeChatHistory = (options.agentflowRuntime?.chatHistory as BaseMessageLike[]) ?? []
const chatId = options.chatId as string
// Initialize the LLM model instance
const nodeInstanceFilePath = options.componentNodes[model].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newLLMNodeInstance = new nodeModule.nodeClass()
const newNodeData = {
...nodeData,
credential: modelConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...modelConfig
}
}
let llmNodeInstance = (await newLLMNodeInstance.init(newNodeData, '', options)) as BaseChatModel
// Prepare messages array
const messages: BaseMessageLike[] = []
// Use to store messages with image file references as we do not want to store the base64 data into database
let runtimeImageMessagesWithFileRef: BaseMessageLike[] = []
// Use to keep track of past messages with image file references
let pastImageMessagesWithFileRef: BaseMessageLike[] = []
for (const msg of llmMessages) {
const role = msg.role
const content = msg.content
if (role && content) {
messages.push({ role, content })
}
}
// Handle memory management if enabled
if (enableMemory) {
await this.handleMemory({
messages,
memoryType,
pastChatHistory,
runtimeChatHistory,
llmNodeInstance,
nodeData,
userMessage,
input,
abortController,
options,
modelConfig,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
})
} else if (!runtimeChatHistory.length) {
/*
* If this is the first node:
* - Add images to messages if exist
* - Add user message
*/
if (options.uploads) {
const imageContents = await getUniqueImageMessages(options, messages, modelConfig)
if (imageContents) {
const { imageMessageWithBase64, imageMessageWithFileRef } = imageContents
messages.push(imageMessageWithBase64)
runtimeImageMessagesWithFileRef.push(imageMessageWithFileRef)
}
}
if (input && typeof input === 'string') {
messages.push({
role: 'user',
content: input
})
}
}
delete nodeData.inputs?.llmMessages
// Configure structured output if specified
const isStructuredOutput = _llmStructuredOutput && Array.isArray(_llmStructuredOutput) && _llmStructuredOutput.length > 0
if (isStructuredOutput) {
llmNodeInstance = this.configureStructuredOutput(llmNodeInstance, _llmStructuredOutput)
}
// Initialize response and determine if streaming is possible
let response: AIMessageChunk = new AIMessageChunk('')
const isLastNode = options.isLastNode as boolean
const isStreamable = isLastNode && options.sseStreamer !== undefined && modelConfig?.streaming !== false && !isStructuredOutput
// Start analytics
if (analyticHandlers && options.parentTraceIds) {
const llmLabel = options?.componentNodes?.[model]?.label || model
llmIds = await analyticHandlers.onLLMStart(llmLabel, messages, options.parentTraceIds)
}
// Track execution time
const startTime = Date.now()
const sseStreamer: IServerSideEventStreamer | undefined = options.sseStreamer
if (isStreamable) {
response = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController)
} else {
response = await llmNodeInstance.invoke(messages, { signal: abortController?.signal })
// Stream whole response back to UI if this is the last node
if (isLastNode && options.sseStreamer) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
sseStreamer.streamTokenEvent(chatId, JSON.stringify(response, null, 2))
}
}
// Calculate execution time
const endTime = Date.now()
const timeDelta = endTime - startTime
// Update flow state if needed
let newState = { ...state }
if (_llmUpdateState && Array.isArray(_llmUpdateState) && _llmUpdateState.length > 0) {
newState = updateFlowState(state, _llmUpdateState)
}
// Clean up empty inputs
for (const key in nodeData.inputs) {
if (nodeData.inputs[key] === '') {
delete nodeData.inputs[key]
}
}
// Prepare final response and output object
const finalResponse = (response.content as string) ?? JSON.stringify(response, null, 2)
const output = this.prepareOutputObject(response, finalResponse, startTime, endTime, timeDelta)
// End analytics tracking
if (analyticHandlers && llmIds) {
await analyticHandlers.onLLMEnd(llmIds, finalResponse)
}
// Send additional streaming events if needed
if (isStreamable) {
this.sendStreamingEvents(options, chatId, response)
}
// Process template variables in state
if (newState && Object.keys(newState).length > 0) {
for (const key in newState) {
const stateValue = newState[key].toString()
if (stateValue.includes('{{ output')) {
// Handle simple output replacement
if (stateValue === '{{ output }}') {
newState[key] = finalResponse
continue
}
// Handle JSON path expressions like {{ output.item1 }}
// eslint-disable-next-line
const match = stateValue.match(/{{[\s]*output\.([\w\.]+)[\s]*}}/)
if (match) {
try {
// Parse the response if it's JSON
const jsonResponse = typeof finalResponse === 'string' ? JSON.parse(finalResponse) : finalResponse
// Get the value using lodash get
const path = match[1]
const value = get(jsonResponse, path)
newState[key] = value ?? stateValue // Fall back to original if path not found
} catch (e) {
// If JSON parsing fails, keep original template
console.warn(`Failed to parse JSON or find path in output: ${e}`)
newState[key] = stateValue
}
}
}
}
}
// Replace the actual messages array with one that includes the file references for images instead of base64 data
const messagesWithFileReferences = replaceBase64ImagesWithFileReferences(
messages,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
)
// Only add to runtime chat history if this is the first node
const inputMessages = []
if (!runtimeChatHistory.length) {
if (runtimeImageMessagesWithFileRef.length) {
inputMessages.push(...runtimeImageMessagesWithFileRef)
}
if (input && typeof input === 'string') {
inputMessages.push({ role: 'user', content: input })
}
}
const returnResponseAs = nodeData.inputs?.llmReturnResponseAs as string
let returnRole = 'user'
if (returnResponseAs === 'assistantMessage') {
returnRole = 'assistant'
}
// Prepare and return the final output
return {
id: nodeData.id,
name: this.name,
input: {
messages: messagesWithFileReferences,
...nodeData.inputs
},
output,
state: newState,
chatHistory: [
...inputMessages,
// LLM response
{
role: returnRole,
content: finalResponse,
name: nodeData?.label ? nodeData?.label.toLowerCase().replace(/\s/g, '_').trim() : nodeData?.id
}
]
}
} catch (error) {
if (options.analyticHandlers && llmIds) {
await options.analyticHandlers.onLLMError(llmIds, error instanceof Error ? error.message : String(error))
}
if (error instanceof Error && error.message === 'Aborted') {
throw error
}
throw new Error(`Error in LLM node: ${error instanceof Error ? error.message : String(error)}`)
}
}
/**
* Handles memory management based on the specified memory type
*/
private async handleMemory({
messages,
memoryType,
pastChatHistory,
runtimeChatHistory,
llmNodeInstance,
nodeData,
userMessage,
input,
abortController,
options,
modelConfig,
runtimeImageMessagesWithFileRef,
pastImageMessagesWithFileRef
}: {
messages: BaseMessageLike[]
memoryType: string
pastChatHistory: BaseMessageLike[]
runtimeChatHistory: BaseMessageLike[]
llmNodeInstance: BaseChatModel
nodeData: INodeData
userMessage: string
input: string | Record<string, any>
abortController: AbortController
options: ICommonObject
modelConfig: ICommonObject
runtimeImageMessagesWithFileRef: BaseMessageLike[]
pastImageMessagesWithFileRef: BaseMessageLike[]
}): Promise<void> {
const { updatedPastMessages, transformedPastMessages } = await getPastChatHistoryImageMessages(pastChatHistory, options)
pastChatHistory = updatedPastMessages
pastImageMessagesWithFileRef.push(...transformedPastMessages)
let pastMessages = [...pastChatHistory, ...runtimeChatHistory]
if (!runtimeChatHistory.length && input && typeof input === 'string') {
/*
* If this is the first node:
* - Add images to messages if exist
* - Add user message
*/
if (options.uploads) {
const imageContents = await getUniqueImageMessages(options, messages, modelConfig)
if (imageContents) {
const { imageMessageWithBase64, imageMessageWithFileRef } = imageContents
pastMessages.push(imageMessageWithBase64)
runtimeImageMessagesWithFileRef.push(imageMessageWithFileRef)
}
}
pastMessages.push({
role: 'user',
content: input
})
}
const { updatedMessages, transformedMessages } = await processMessagesWithImages(pastMessages, options)
pastMessages = updatedMessages
pastImageMessagesWithFileRef.push(...transformedMessages)
if (pastMessages.length > 0) {
if (memoryType === 'windowSize') {
// Window memory: Keep the last N messages
const windowSize = nodeData.inputs?.llmMemoryWindowSize as number
const windowedMessages = pastMessages.slice(-windowSize * 2)
messages.push(...windowedMessages)
} else if (memoryType === 'conversationSummary') {
// Summary memory: Summarize all past messages
const summary = await llmNodeInstance.invoke(
[
{
role: 'user',
content: DEFAULT_SUMMARIZER_TEMPLATE.replace(
'{conversation}',
pastMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
)
}
],
{ signal: abortController?.signal }
)
messages.push({ role: 'assistant', content: summary.content as string })
} else if (memoryType === 'conversationSummaryBuffer') {
// Summary buffer: Summarize messages that exceed token limit
await this.handleSummaryBuffer(messages, pastMessages, llmNodeInstance, nodeData, abortController)
} else {
// Default: Use all messages
messages.push(...pastMessages)
}
}
// Add user message
if (userMessage) {
messages.push({
role: 'user',
content: userMessage
})
}
}
/**
* Handles conversation summary buffer memory type
*/
private async handleSummaryBuffer(
messages: BaseMessageLike[],
pastMessages: BaseMessageLike[],
llmNodeInstance: BaseChatModel,
nodeData: INodeData,
abortController: AbortController
): Promise<void> {
const maxTokenLimit = (nodeData.inputs?.llmMemoryMaxTokenLimit as number) || 2000
// Convert past messages to a format suitable for token counting
const messagesString = pastMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
const tokenCount = await llmNodeInstance.getNumTokens(messagesString)
if (tokenCount > maxTokenLimit) {
// Calculate how many messages to summarize (messages that exceed the token limit)
let currBufferLength = tokenCount
const messagesToSummarize = []
const remainingMessages = [...pastMessages]
// Remove messages from the beginning until we're under the token limit
while (currBufferLength > maxTokenLimit && remainingMessages.length > 0) {
const poppedMessage = remainingMessages.shift()
if (poppedMessage) {
messagesToSummarize.push(poppedMessage)
// Recalculate token count for remaining messages
const remainingMessagesString = remainingMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
currBufferLength = await llmNodeInstance.getNumTokens(remainingMessagesString)
}
}
// Summarize the messages that were removed
const messagesToSummarizeString = messagesToSummarize.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n')
const summary = await llmNodeInstance.invoke(
[
{
role: 'user',
content: DEFAULT_SUMMARIZER_TEMPLATE.replace('{conversation}', messagesToSummarizeString)
}
],
{ signal: abortController?.signal }
)
// Add summary as a system message at the beginning, then add remaining messages
messages.push({ role: 'system', content: `Previous conversation summary: ${summary.content}` })
messages.push(...remainingMessages)
} else {
// If under token limit, use all messages
messages.push(...pastMessages)
}
}
/**
* Configures structured output for the LLM
*/
private configureStructuredOutput(llmNodeInstance: BaseChatModel, llmStructuredOutput: IStructuredOutput[]): BaseChatModel {
try {
const zodObj: ICommonObject = {}
for (const sch of llmStructuredOutput) {
if (sch.type === 'string') {
zodObj[sch.key] = z.string().describe(sch.description || '')
} else if (sch.type === 'stringArray') {
zodObj[sch.key] = z.array(z.string()).describe(sch.description || '')
} else if (sch.type === 'number') {
zodObj[sch.key] = z.number().describe(sch.description || '')
} else if (sch.type === 'boolean') {
zodObj[sch.key] = z.boolean().describe(sch.description || '')
} else if (sch.type === 'enum') {
const enumValues = sch.enumValues?.split(',').map((item: string) => item.trim()) || []
zodObj[sch.key] = z
.enum(enumValues.length ? (enumValues as [string, ...string[]]) : ['default'])
.describe(sch.description || '')
} else if (sch.type === 'jsonArray') {
const jsonSchema = sch.jsonSchema
if (jsonSchema) {
try {
// Parse the JSON schema
const schemaObj = JSON.parse(jsonSchema)
// Create a Zod schema from the JSON schema
const itemSchema = this.createZodSchemaFromJSON(schemaObj)
// Create an array schema of the item schema
zodObj[sch.key] = z.array(itemSchema).describe(sch.description || '')
} catch (err) {
console.error(`Error parsing JSON schema for ${sch.key}:`, err)
// Fallback to generic array of records
zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '')
}
} else {
// If no schema provided, use generic array of records
zodObj[sch.key] = z.array(z.record(z.any())).describe(sch.description || '')
}
}
}
const structuredOutput = z.object(zodObj)
// @ts-ignore
return llmNodeInstance.withStructuredOutput(structuredOutput)
} catch (exception) {
console.error(exception)
return llmNodeInstance
}
}
/**
* Handles streaming response from the LLM
*/
private async handleStreamingResponse(
sseStreamer: IServerSideEventStreamer | undefined,
llmNodeInstance: BaseChatModel,
messages: BaseMessageLike[],
chatId: string,
abortController: AbortController
): Promise<AIMessageChunk> {
let response = new AIMessageChunk('')
try {
for await (const chunk of await llmNodeInstance.stream(messages, { signal: abortController?.signal })) {
if (sseStreamer) {
let content = ''
if (Array.isArray(chunk.content) && chunk.content.length > 0) {
const contents = chunk.content as MessageContentText[]
content = contents.map((item) => item.text).join('')
} else {
content = chunk.content.toString()
}
sseStreamer.streamTokenEvent(chatId, content)
}
response = response.concat(chunk)
}
} catch (error) {
console.error('Error during streaming:', error)
throw error
}
if (Array.isArray(response.content) && response.content.length > 0) {
const responseContents = response.content as MessageContentText[]
response.content = responseContents.map((item) => item.text).join('')
}
return response
}
/**
* Prepares the output object with response and metadata
*/
private prepareOutputObject(
response: AIMessageChunk,
finalResponse: string,
startTime: number,
endTime: number,
timeDelta: number
): any {
const output: any = {
content: finalResponse,
timeMetadata: {
start: startTime,
end: endTime,
delta: timeDelta
}
}
if (response.tool_calls) {
output.calledTools = response.tool_calls
}
if (response.usage_metadata) {
output.usageMetadata = response.usage_metadata
}
return output
}
/**
* Sends additional streaming events for tool calls and metadata
*/
private sendStreamingEvents(options: ICommonObject, chatId: string, response: AIMessageChunk): void {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer
if (response.tool_calls) {
sseStreamer.streamCalledToolsEvent(chatId, response.tool_calls)
}
if (response.usage_metadata) {
sseStreamer.streamUsageMetadataEvent(chatId, response.usage_metadata)
}
sseStreamer.streamEndEvent(chatId)
}
/**
* Creates a Zod schema from a JSON schema object
* @param jsonSchema The JSON schema object
* @returns A Zod schema
*/
private createZodSchemaFromJSON(jsonSchema: any): z.ZodTypeAny {
// If the schema is an object with properties, create an object schema
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
const schemaObj: Record<string, z.ZodTypeAny> = {}
// Process each property in the schema
for (const [key, value] of Object.entries(jsonSchema)) {
if (value === null) {
// Handle null values
schemaObj[key] = z.null()
} else if (typeof value === 'object' && !Array.isArray(value)) {
// Check if the property has a type definition
if ('type' in value) {
const type = value.type as string
const description = ('description' in value ? (value.description as string) : '') || ''
// Create the appropriate Zod type based on the type property
if (type === 'string') {
schemaObj[key] = z.string().describe(description)
} else if (type === 'number') {
schemaObj[key] = z.number().describe(description)
} else if (type === 'boolean') {
schemaObj[key] = z.boolean().describe(description)
} else if (type === 'array') {
// If it's an array type, check if items is defined
if ('items' in value && value.items) {
const itemSchema = this.createZodSchemaFromJSON(value.items)
schemaObj[key] = z.array(itemSchema).describe(description)
} else {
// Default to array of any if items not specified
schemaObj[key] = z.array(z.any()).describe(description)
}
} else if (type === 'object') {
// If it's an object type, check if properties is defined
if ('properties' in value && value.properties) {
const nestedSchema = this.createZodSchemaFromJSON(value.properties)
schemaObj[key] = nestedSchema.describe(description)
} else {
// Default to record of any if properties not specified
schemaObj[key] = z.record(z.any()).describe(description)
}
} else {
// Default to any for unknown types
schemaObj[key] = z.any().describe(description)
}
// Check if the property is optional
if ('optional' in value && value.optional === true) {
schemaObj[key] = schemaObj[key].optional()
}
} else if (Array.isArray(value)) {
// Array values without a type property
if (value.length > 0) {
// If the array has items, recursively create a schema for the first item
const itemSchema = this.createZodSchemaFromJSON(value[0])
schemaObj[key] = z.array(itemSchema)
} else {
// Empty array, allow any array
schemaObj[key] = z.array(z.any())
}
} else {
// It's a nested object without a type property, recursively create schema
schemaObj[key] = this.createZodSchemaFromJSON(value)
}
} else if (Array.isArray(value)) {
// Array values
if (value.length > 0) {
// If the array has items, recursively create a schema for the first item
const itemSchema = this.createZodSchemaFromJSON(value[0])
schemaObj[key] = z.array(itemSchema)
} else {
// Empty array, allow any array
schemaObj[key] = z.array(z.any())
}
} else {
// For primitive values (which shouldn't be in the schema directly)
// Use the corresponding Zod type
if (typeof value === 'string') {
schemaObj[key] = z.string()
} else if (typeof value === 'number') {
schemaObj[key] = z.number()
} else if (typeof value === 'boolean') {
schemaObj[key] = z.boolean()
} else {
schemaObj[key] = z.any()
}
}
}
return z.object(schemaObj)
}
// Fallback to any for unknown types
return z.any()
}
}
module.exports = { nodeClass: LLM_Agentflow }
@@ -0,0 +1,94 @@
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface'
class Loop_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
hideOutput: boolean
hint: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Loop'
this.name = 'loopAgentflow'
this.version = 1.0
this.type = 'Loop'
this.category = 'Agent Flows'
this.description = 'Loop back to a previous node'
this.baseClasses = [this.type]
this.color = '#FFA07A'
this.hint = 'Make sure to have memory enabled in the LLM/Agent node to retain the chat history'
this.hideOutput = true
this.inputs = [
{
label: 'Loop Back To',
name: 'loopBackToNode',
type: 'asyncOptions',
loadMethod: 'listPreviousNodes',
freeSolo: true
},
{
label: 'Max Loop Count',
name: 'maxLoopCount',
type: 'number',
default: 5
}
]
}
//@ts-ignore
loadMethods = {
async listPreviousNodes(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const previousNodes = options.previousNodes as ICommonObject[]
const returnOptions: INodeOptionsValue[] = []
for (const node of previousNodes) {
returnOptions.push({
label: node.label,
name: `${node.id}-${node.label}`,
description: node.id
})
}
return returnOptions
}
}
async run(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const loopBackToNode = nodeData.inputs?.loopBackToNode as string
const _maxLoopCount = nodeData.inputs?.maxLoopCount as string
const state = options.agentflowRuntime?.state as ICommonObject
const loopBackToNodeId = loopBackToNode.split('-')[0]
const loopBackToNodeLabel = loopBackToNode.split('-')[1]
const data = {
nodeID: loopBackToNodeId,
maxLoopCount: _maxLoopCount ? parseInt(_maxLoopCount) : 5
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: data,
output: {
content: 'Loop back to ' + `${loopBackToNodeLabel} (${loopBackToNodeId})`,
nodeID: loopBackToNodeId,
maxLoopCount: _maxLoopCount ? parseInt(_maxLoopCount) : 5
},
state
}
return returnOutput
}
}
module.exports = { nodeClass: Loop_Agentflow }
@@ -0,0 +1,227 @@
import {
ICommonObject,
IDatabaseEntity,
INode,
INodeData,
INodeOptionsValue,
INodeParams,
IServerSideEventStreamer
} from '../../../src/Interface'
import { updateFlowState } from '../utils'
import { DataSource } from 'typeorm'
import { BaseRetriever } from '@langchain/core/retrievers'
import { Document } from '@langchain/core/documents'
interface IKnowledgeBase {
documentStore: string
}
class Retriever_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
hideOutput: boolean
hint: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Retriever'
this.name = 'retrieverAgentflow'
this.version = 1.0
this.type = 'Retriever'
this.category = 'Agent Flows'
this.description = 'Retrieve information from vector database'
this.baseClasses = [this.type]
this.color = '#b8bedd'
this.inputs = [
{
label: 'Knowledge (Document Stores)',
name: 'retrieverKnowledgeDocumentStores',
type: 'array',
description: 'Document stores to retrieve information from. Document stores must be upserted in advance.',
array: [
{
label: 'Document Store',
name: 'documentStore',
type: 'asyncOptions',
loadMethod: 'listStores'
}
]
},
{
label: 'Retriever Query',
name: 'retrieverQuery',
type: 'string',
placeholder: 'Enter your query here',
rows: 4,
acceptVariable: true
},
{
label: 'Output Format',
name: 'outputFormat',
type: 'options',
options: [
{ label: 'Text', name: 'text' },
{ label: 'Text with Metadata', name: 'textWithMetadata' }
],
default: 'text'
},
{
label: 'Update Flow State',
name: 'retrieverUpdateState',
description: 'Update runtime state during the execution of the workflow',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
name: 'value',
type: 'string',
acceptVariable: true,
acceptNodeOutputAsVariable: true
}
]
}
]
}
//@ts-ignore
loadMethods = {
async listRuntimeStateKeys(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const previousNodes = options.previousNodes as ICommonObject[]
const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow')
const state = startAgentflowNode?.inputs?.startState as ICommonObject[]
return state.map((item) => ({ label: item.key, name: item.key }))
},
async listStores(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const returnData: INodeOptionsValue[] = []
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
if (appDataSource === undefined || !appDataSource) {
return returnData
}
const stores = await appDataSource.getRepository(databaseEntities['DocumentStore']).find()
for (const store of stores) {
if (store.status === 'UPSERTED') {
const obj = {
name: `${store.id}:${store.name}`,
label: store.name,
description: store.description
}
returnData.push(obj)
}
}
return returnData
}
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const retrieverQuery = nodeData.inputs?.retrieverQuery as string
const outputFormat = nodeData.inputs?.outputFormat as string
const _retrieverUpdateState = nodeData.inputs?.retrieverUpdateState
const state = options.agentflowRuntime?.state as ICommonObject
const chatId = options.chatId as string
const isLastNode = options.isLastNode as boolean
const isStreamable = isLastNode && options.sseStreamer !== undefined
const abortController = options.abortController as AbortController
// Extract knowledge
let docs: Document[] = []
const knowledgeBases = nodeData.inputs?.retrieverKnowledgeDocumentStores as IKnowledgeBase[]
if (knowledgeBases && knowledgeBases.length > 0) {
for (const knowledgeBase of knowledgeBases) {
const [storeId, _] = knowledgeBase.documentStore.split(':')
const docStoreVectorInstanceFilePath = options.componentNodes['documentStoreVS'].filePath as string
const docStoreVectorModule = await import(docStoreVectorInstanceFilePath)
const newDocStoreVectorInstance = new docStoreVectorModule.nodeClass()
const docStoreVectorInstance = (await newDocStoreVectorInstance.init(
{
...nodeData,
inputs: {
...nodeData.inputs,
selectedStore: storeId
},
outputs: {
output: 'retriever'
}
},
'',
options
)) as BaseRetriever
docs = await docStoreVectorInstance.invoke(retrieverQuery || input, { signal: abortController?.signal })
}
}
const docsText = docs.map((doc) => doc.pageContent).join('\n')
// Update flow state if needed
let newState = { ...state }
if (_retrieverUpdateState && Array.isArray(_retrieverUpdateState) && _retrieverUpdateState.length > 0) {
newState = updateFlowState(state, _retrieverUpdateState)
}
try {
let finalOutput = ''
if (outputFormat === 'text') {
finalOutput = docsText
} else if (outputFormat === 'textWithMetadata') {
finalOutput = JSON.stringify(docs, null, 2)
}
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer
sseStreamer.streamTokenEvent(chatId, finalOutput)
}
// Process template variables in state
if (newState && Object.keys(newState).length > 0) {
for (const key in newState) {
if (newState[key].toString().includes('{{ output }}')) {
newState[key] = finalOutput
}
}
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {
question: retrieverQuery || input
},
output: {
content: finalOutput
},
state: newState
}
return returnOutput
} catch (e) {
throw new Error(e)
}
}
}
module.exports = { nodeClass: Retriever_Agentflow }
@@ -0,0 +1,217 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
class Start_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
hideInput: boolean
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Start'
this.name = 'startAgentflow'
this.version = 1.0
this.type = 'Start'
this.category = 'Agent Flows'
this.description = 'Starting point of the agentflow'
this.baseClasses = [this.type]
this.color = '#7EE787'
this.hideInput = true
this.inputs = [
{
label: 'Input Type',
name: 'startInputType',
type: 'options',
options: [
{
label: 'Chat Input',
name: 'chatInput',
description: 'Start the conversation with chat input'
},
{
label: 'Form Input',
name: 'formInput',
description: 'Start the workflow with form inputs'
}
],
default: 'chatInput'
},
{
label: 'Form Title',
name: 'formTitle',
type: 'string',
placeholder: 'Please Fill Out The Form',
show: {
startInputType: 'formInput'
}
},
{
label: 'Form Description',
name: 'formDescription',
type: 'string',
placeholder: 'Complete all fields below to continue',
show: {
startInputType: 'formInput'
}
},
{
label: 'Form Input Types',
name: 'formInputTypes',
description: 'Specify the type of form input',
type: 'array',
show: {
startInputType: 'formInput'
},
array: [
{
label: 'Type',
name: 'type',
type: 'options',
options: [
{
label: 'String',
name: 'string'
},
{
label: 'Number',
name: 'number'
},
{
label: 'Boolean',
name: 'boolean'
},
{
label: 'Options',
name: 'options'
}
],
default: 'string'
},
{
label: 'Label',
name: 'label',
type: 'string',
placeholder: 'Label for the input'
},
{
label: 'Variable Name',
name: 'name',
type: 'string',
placeholder: 'Variable name for the input (must be camel case)',
description: 'Variable name must be camel case. For example: firstName, lastName, etc.'
},
{
label: 'Add Options',
name: 'addOptions',
type: 'array',
show: {
'formInputTypes[$index].type': 'options'
},
array: [
{
label: 'Option',
name: 'option',
type: 'string'
}
]
}
]
},
{
label: 'Ephemeral Memory',
name: 'startEphemeralMemory',
type: 'boolean',
description: 'Start fresh for every execution without past chat history',
optional: true
},
{
label: 'Flow State',
name: 'startState',
description: 'Runtime state during the execution of the workflow',
type: 'array',
optional: true,
array: [
{
label: 'Key',
name: 'key',
type: 'string',
placeholder: 'Foo'
},
{
label: 'Value',
name: 'value',
type: 'string',
placeholder: 'Bar',
optional: true
}
]
}
]
}
async run(nodeData: INodeData, input: string | Record<string, any>, options: ICommonObject): Promise<any> {
const _flowState = nodeData.inputs?.startState as string
const startInputType = nodeData.inputs?.startInputType as string
const startEphemeralMemory = nodeData.inputs?.startEphemeralMemory as boolean
let flowStateArray = []
if (_flowState) {
try {
flowStateArray = typeof _flowState === 'string' ? JSON.parse(_flowState) : _flowState
} catch (error) {
throw new Error('Invalid Flow State')
}
}
let flowState: Record<string, any> = {}
for (const state of flowStateArray) {
flowState[state.key] = state.value
}
const inputData: ICommonObject = {}
const outputData: ICommonObject = {}
if (startInputType === 'chatInput') {
inputData.question = input
outputData.question = input
}
if (startInputType === 'formInput') {
inputData.form = {
title: nodeData.inputs?.formTitle,
description: nodeData.inputs?.formDescription,
inputs: nodeData.inputs?.formInputTypes
}
let form = input
if (options.agentflowRuntime?.form && Object.keys(options.agentflowRuntime.form).length) {
form = options.agentflowRuntime.form
}
outputData.form = form
}
if (startEphemeralMemory) {
outputData.ephemeralMemory = true
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: inputData,
output: outputData,
state: flowState
}
return returnOutput
}
}
module.exports = { nodeClass: Start_Agentflow }
@@ -0,0 +1,42 @@
import { INode, INodeParams } from '../../../src/Interface'
class StickyNote_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
tags: string[]
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Sticky Note'
this.name = 'stickyNoteAgentflow'
this.version = 1.0
this.type = 'StickyNote'
this.color = '#fee440'
this.category = 'Agent Flows'
this.description = 'Add notes to the agent flow'
this.inputs = [
{
label: '',
name: 'note',
type: 'string',
rows: 1,
placeholder: 'Type something here',
optional: true
}
]
this.baseClasses = [this.type]
}
async run(): Promise<any> {
return undefined
}
}
module.exports = { nodeClass: StickyNote_Agentflow }
@@ -0,0 +1,304 @@
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams, IServerSideEventStreamer } from '../../../src/Interface'
import { updateFlowState } from '../utils'
import { Tool } from '@langchain/core/tools'
import { ARTIFACTS_PREFIX } from '../../../src/agents'
import zodToJsonSchema from 'zod-to-json-schema'
interface IToolInputArgs {
inputArgName: string
inputArgValue: string
}
class Tool_Agentflow implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
color: string
hideOutput: boolean
hint: string
baseClasses: string[]
documentation?: string
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Tool'
this.name = 'toolAgentflow'
this.version = 1.0
this.type = 'Tool'
this.category = 'Agent Flows'
this.description = 'Tools allow LLM to interact with external systems'
this.baseClasses = [this.type]
this.color = '#d4a373'
this.inputs = [
{
label: 'Tool',
name: 'selectedTool',
type: 'asyncOptions',
loadMethod: 'listTools',
loadConfig: true
},
{
label: 'Tool Input Arguments',
name: 'toolInputArgs',
type: 'array',
acceptVariable: true,
refresh: true,
array: [
{
label: 'Input Argument Name',
name: 'inputArgName',
type: 'asyncOptions',
loadMethod: 'listToolInputArgs',
refresh: true
},
{
label: 'Input Argument Value',
name: 'inputArgValue',
type: 'string',
acceptVariable: true
}
],
show: {
selectedTool: '.+'
}
},
{
label: 'Update Flow State',
name: 'toolUpdateState',
description: 'Update runtime state during the execution of the workflow',
type: 'array',
optional: true,
acceptVariable: true,
array: [
{
label: 'Key',
name: 'key',
type: 'asyncOptions',
loadMethod: 'listRuntimeStateKeys',
freeSolo: true
},
{
label: 'Value',
name: 'value',
type: 'string',
acceptVariable: true,
acceptNodeOutputAsVariable: true
}
]
}
]
}
//@ts-ignore
loadMethods = {
async listTools(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const componentNodes = options.componentNodes as {
[key: string]: INode
}
const removeTools = ['chainTool', 'retrieverTool', 'webBrowser']
const returnOptions: INodeOptionsValue[] = []
for (const nodeName in componentNodes) {
const componentNode = componentNodes[nodeName]
if (componentNode.category === 'Tools' || componentNode.category === 'Tools (MCP)') {
if (componentNode.tags?.includes('LlamaIndex')) {
continue
}
if (removeTools.includes(nodeName)) {
continue
}
returnOptions.push({
label: componentNode.label,
name: nodeName,
imageSrc: componentNode.icon
})
}
}
return returnOptions
},
async listToolInputArgs(nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const currentNode = options.currentNode as ICommonObject
const selectedTool = currentNode?.inputs?.selectedTool as string
const selectedToolConfig = currentNode?.inputs?.selectedToolConfig as ICommonObject
const nodeInstanceFilePath = options.componentNodes[selectedTool].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newToolNodeInstance = new nodeModule.nodeClass()
const newNodeData = {
...nodeData,
credential: selectedToolConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...selectedToolConfig
}
}
try {
const toolInstance = (await newToolNodeInstance.init(newNodeData, '', options)) as Tool
let toolInputArgs: ICommonObject = {}
if (Array.isArray(toolInstance)) {
// Combine schemas from all tools in the array
const allProperties = toolInstance.reduce((acc, tool) => {
if (tool?.schema) {
const schema: Record<string, any> = zodToJsonSchema(tool.schema)
return { ...acc, ...(schema.properties || {}) }
}
return acc
}, {})
toolInputArgs = { properties: allProperties }
} else {
// Handle single tool instance
toolInputArgs = toolInstance.schema ? zodToJsonSchema(toolInstance.schema) : {}
}
if (toolInputArgs && Object.keys(toolInputArgs).length > 0) {
delete toolInputArgs.$schema
}
return Object.keys(toolInputArgs.properties || {}).map((item) => ({
label: item,
name: item,
description: toolInputArgs.properties[item].description
}))
} catch (e) {
return []
}
},
async listRuntimeStateKeys(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const previousNodes = options.previousNodes as ICommonObject[]
const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow')
const state = startAgentflowNode?.inputs?.startState as ICommonObject[]
return state.map((item) => ({ label: item.key, name: item.key }))
}
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const selectedTool = nodeData.inputs?.selectedTool as string
const selectedToolConfig = nodeData.inputs?.selectedToolConfig as ICommonObject
const toolInputArgs = nodeData.inputs?.toolInputArgs as IToolInputArgs[]
const _toolUpdateState = nodeData.inputs?.toolUpdateState
const state = options.agentflowRuntime?.state as ICommonObject
const chatId = options.chatId as string
const isLastNode = options.isLastNode as boolean
const isStreamable = isLastNode && options.sseStreamer !== undefined
const abortController = options.abortController as AbortController
// Update flow state if needed
let newState = { ...state }
if (_toolUpdateState && Array.isArray(_toolUpdateState) && _toolUpdateState.length > 0) {
newState = updateFlowState(state, _toolUpdateState)
}
if (!selectedTool) {
throw new Error('Tool not selected')
}
const nodeInstanceFilePath = options.componentNodes[selectedTool].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newToolNodeInstance = new nodeModule.nodeClass()
const newNodeData = {
...nodeData,
credential: selectedToolConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...selectedToolConfig
}
}
const toolInstance = (await newToolNodeInstance.init(newNodeData, '', options)) as Tool | Tool[]
let toolCallArgs: Record<string, any> = {}
for (const item of toolInputArgs) {
const variableName = item.inputArgName
const variableValue = item.inputArgValue
toolCallArgs[variableName] = variableValue
}
const flowConfig = {
sessionId: options.sessionId,
chatId: options.chatId,
input: input,
state: options.agentflowRuntime?.state
}
try {
let toolOutput: string
if (Array.isArray(toolInstance)) {
// Execute all tools and combine their outputs
const outputs = await Promise.all(
toolInstance.map((tool) =>
//@ts-ignore
tool.call(toolCallArgs, { signal: abortController?.signal }, undefined, flowConfig)
)
)
toolOutput = outputs.join('\n')
} else {
//@ts-ignore
toolOutput = await toolInstance.call(toolCallArgs, { signal: abortController?.signal }, undefined, flowConfig)
}
let parsedArtifacts
// Extract artifacts if present
if (typeof toolOutput === 'string' && toolOutput.includes(ARTIFACTS_PREFIX)) {
const [output, artifact] = toolOutput.split(ARTIFACTS_PREFIX)
toolOutput = output
try {
parsedArtifacts = JSON.parse(artifact)
} catch (e) {
console.error('Error parsing artifacts from tool:', e)
}
}
if (typeof toolOutput === 'object') {
toolOutput = JSON.stringify(toolOutput, null, 2)
}
if (isStreamable) {
const sseStreamer: IServerSideEventStreamer = options.sseStreamer
sseStreamer.streamTokenEvent(chatId, toolOutput)
}
// Process template variables in state
if (newState && Object.keys(newState).length > 0) {
for (const key in newState) {
if (newState[key].toString().includes('{{ output }}')) {
newState[key] = toolOutput
}
}
}
const returnOutput = {
id: nodeData.id,
name: this.name,
input: {
toolInputArgs: toolInputArgs,
selectedTool: selectedTool
},
output: {
content: toolOutput,
artifacts: parsedArtifacts
},
state: newState
}
return returnOutput
} catch (e) {
throw new Error(e)
}
}
}
module.exports = { nodeClass: Tool_Agentflow }
@@ -0,0 +1,75 @@
export const DEFAULT_SUMMARIZER_TEMPLATE = `Progressively summarize the conversation provided and return a new summary.
EXAMPLE:
Human: Why do you think artificial intelligence is a force for good?
AI: Because artificial intelligence will help humans reach their full potential.
New summary:
The human asks what the AI thinks of artificial intelligence. The AI thinks artificial intelligence is a force for good because it will help humans reach their full potential.
END OF EXAMPLE
Conversation:
{conversation}
New summary:`
export const DEFAULT_HUMAN_INPUT_DESCRIPTION = `Summarize the conversation between the user and the assistant, reiterate the last message from the assistant, and ask if user would like to proceed or if they have any feedback.
- Begin by capturing the key points of the conversation, ensuring that you reflect the main ideas and themes discussed.
- Then, clearly reproduce the last message sent by the assistant to maintain continuity. Make sure the whole message is reproduced.
- Finally, ask the user if they would like to proceed, or provide any feedback on the last assistant message
## Output Format The output should be structured in three parts in text:
- A summary of the conversation (1-3 sentences).
- The last assistant message (exactly as it appeared).
- Ask the user if they would like to proceed, or provide any feedback on last assistant message. No other explanation and elaboration is needed.
`
export const DEFAULT_HUMAN_INPUT_DESCRIPTION_HTML = `<p>Summarize the conversation between the user and the assistant, reiterate the last message from the assistant, and ask if user would like to proceed or if they have any feedback. </p>
<ul>
<li>Begin by capturing the key points of the conversation, ensuring that you reflect the main ideas and themes discussed.</li>
<li>Then, clearly reproduce the last message sent by the assistant to maintain continuity. Make sure the whole message is reproduced.</li>
<li>Finally, ask the user if they would like to proceed, or provide any feedback on the last assistant message</li>
</ul>
<h2 id="output-format-the-output-should-be-structured-in-three-parts-">Output Format The output should be structured in three parts in text:</h2>
<ul>
<li>A summary of the conversation (1-3 sentences).</li>
<li>The last assistant message (exactly as it appeared).</li>
<li>Ask the user if they would like to proceed, or provide any feedback on last assistant message. No other explanation and elaboration is needed.</li>
</ul>
`
export const CONDITION_AGENT_SYSTEM_PROMPT = `You are part of a multi-agent system designed to make agent coordination and execution easy. Your task is to analyze the given input and select one matching scenario from a provided set of scenarios. If none of the scenarios match the input, you should return "default."
- **Input**: A string representing the user's query or message.
- **Scenarios**: A list of predefined scenarios that relate to the input.
- **Instruction**: Determine if the input fits any of the scenarios.
## Steps
1. **Read the input string** and the list of scenarios.
2. **Analyze the content of the input** to identify its main topic or intention.
3. **Compare the input with each scenario**:
- If a scenario matches the main topic of the input, select that scenario.
- If no scenarios match, prepare to output "\`\`\`json\n{"output": "default"}\`\`\`"
4. **Output the result**: If a match is found, return the corresponding scenario in JSON; otherwise, return "\`\`\`json\n{"output": "default"}\`\`\`"
## Output Format
Output should be a JSON object that either names the matching scenario or returns "\`\`\`json\n{"output": "default"}\`\`\`" if no scenarios match. No explanation is needed.
## Examples
1. **Input**: {"input": "Hello", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}
**Output**: "\`\`\`json\n{"output": "default"}\`\`\`"
2. **Input**: {"input": "What is AIGC?", "scenarios": ["user is asking about AI", "default"], "instruction": "Your task is to check and see if user is asking topic about AI"}
**Output**: "\`\`\`json\n{"output": "user is asking about AI"}\`\`\`"
3. **Input**: {"input": "Can you explain deep learning?", "scenarios": ["user is interested in AI topics", "default"], "instruction": "Determine if the user is interested in learning about AI"}
**Output**: "\`\`\`json\n{"output": "user is interested in AI topics"}\`\`\`"
## Note
- Ensure that the input scenarios align well with potential user queries for accurate matching
- DO NOT include anything other than the JSON in your response.
`
@@ -0,0 +1,342 @@
import { BaseMessage, MessageContentImageUrl } from '@langchain/core/messages'
import { getImageUploads } from '../../src/multiModalUtils'
import { getFileFromStorage } from '../../src/storageUtils'
import { ICommonObject, IFileUpload } from '../../src/Interface'
import { BaseMessageLike } from '@langchain/core/messages'
import { IFlowState } from './Interface.Agentflow'
import { mapMimeTypeToInputField } from '../../src/utils'
export const addImagesToMessages = async (
options: ICommonObject,
allowImageUploads: boolean,
imageResolution?: 'auto' | 'low' | 'high'
): Promise<MessageContentImageUrl[]> => {
const imageContent: MessageContentImageUrl[] = []
if (allowImageUploads && options?.uploads && options?.uploads.length > 0) {
const imageUploads = getImageUploads(options.uploads)
for (const upload of imageUploads) {
let bf = upload.data
if (upload.type == 'stored-file') {
const contents = await getFileFromStorage(upload.name, options.chatflowid, options.chatId)
// as the image is stored in the server, read the file and convert it to base64
bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64')
imageContent.push({
type: 'image_url',
image_url: {
url: bf,
detail: imageResolution ?? 'low'
}
})
} else if (upload.type == 'url' && bf) {
imageContent.push({
type: 'image_url',
image_url: {
url: bf,
detail: imageResolution ?? 'low'
}
})
}
}
}
return imageContent
}
/**
* Process message array to replace stored file references with base64 image data
* @param messages Array of messages that may contain image references
* @param options Common options object containing chatflowid and chatId
* @returns Object containing updated messages array and transformed original messages
*/
export const processMessagesWithImages = async (
messages: BaseMessageLike[],
options: ICommonObject
): Promise<{
updatedMessages: BaseMessageLike[]
transformedMessages: BaseMessageLike[]
}> => {
if (!messages || !options.chatflowid || !options.chatId) {
return {
updatedMessages: messages,
transformedMessages: []
}
}
// Create a deep copy of the messages to avoid mutating the original
const updatedMessages = JSON.parse(JSON.stringify(messages))
// Track which messages were transformed
const transformedMessages: BaseMessageLike[] = []
// Scan through all messages looking for stored-file references
for (let i = 0; i < updatedMessages.length; i++) {
const message = updatedMessages[i]
// Skip non-user messages or messages without content
if (message.role !== 'user' || !message.content) {
continue
}
// Handle array content (typically containing file references)
if (Array.isArray(message.content)) {
const imageContents: MessageContentImageUrl[] = []
let hasImageReferences = false
// Process each content item
for (const item of message.content) {
// Look for stored-file type items
if (item.type === 'stored-file' && item.name && item.mime.startsWith('image/')) {
hasImageReferences = true
try {
// Get file contents from storage
const contents = await getFileFromStorage(item.name, options.chatflowid, options.chatId)
// Create base64 data URL
const base64Data = 'data:' + item.mime + ';base64,' + contents.toString('base64')
// Add to image content array
imageContents.push({
type: 'image_url',
image_url: {
url: base64Data,
detail: item.imageResolution ?? 'low'
}
})
} catch (error) {
console.error(`Failed to load image ${item.name}:`, error)
}
}
}
// Replace the content with the image content array
if (imageContents.length > 0) {
// Store the original message before modifying
if (hasImageReferences) {
transformedMessages.push(JSON.parse(JSON.stringify(messages[i])))
}
updatedMessages[i].content = imageContents
}
}
}
return {
updatedMessages,
transformedMessages
}
}
/**
* Replace base64 image data in messages with file references
* @param messages Array of messages that may contain base64 image data
* @param uniqueImageMessages Array of messages with file references for new images
* @param pastImageMessages Array of messages with file references for previous images
* @returns Updated messages array with file references instead of base64 data
*/
export const replaceBase64ImagesWithFileReferences = (
messages: BaseMessageLike[],
uniqueImageMessages: BaseMessageLike[] = [],
pastImageMessages: BaseMessageLike[] = []
): BaseMessageLike[] => {
// Create a deep copy to avoid mutating the original
const updatedMessages = JSON.parse(JSON.stringify(messages))
let imageMessagesIndex = 0
for (let i = 0; i < updatedMessages.length; i++) {
const message = updatedMessages[i]
if (message.content && Array.isArray(message.content)) {
for (let j = 0; j < message.content.length; j++) {
const item = message.content[j]
if (item.type === 'image_url') {
// Look for matching file reference in uniqueImageMessages or pastImageMessages
const imageMessage =
(uniqueImageMessages[imageMessagesIndex] as BaseMessage | undefined) ||
(pastImageMessages[imageMessagesIndex] as BaseMessage | undefined)
if (imageMessage && Array.isArray(imageMessage.content) && imageMessage.content[j]) {
const replaceContent = imageMessage.content[j]
message.content[j] = {
...replaceContent
}
imageMessagesIndex++
}
}
}
}
}
return updatedMessages
}
/**
* Get unique image messages from uploads
* @param options Common options object containing uploads
* @param messages Array of messages to check for existing images
* @param modelConfig Model configuration object containing allowImageUploads and imageResolution
* @returns Object containing imageMessageWithFileRef and imageMessageWithBase64
*/
export const getUniqueImageMessages = async (
options: ICommonObject,
messages: BaseMessageLike[],
modelConfig?: ICommonObject
): Promise<{ imageMessageWithFileRef: BaseMessageLike; imageMessageWithBase64: BaseMessageLike } | undefined> => {
if (!options.uploads) return undefined
// Get images from uploads
const images = await addImagesToMessages(options, modelConfig?.allowImageUploads, modelConfig?.imageResolution)
// Filter out images that are already in previous messages
const uniqueImages = images.filter((image) => {
// Check if this image is already in any existing message
return !messages.some((msg: any) => {
// For multimodal content (arrays with image objects)
if (Array.isArray(msg.content)) {
return msg.content.some(
(item: any) =>
// Compare by image URL/content for image objects
item.type === 'image_url' && image.type === 'image_url' && JSON.stringify(item) === JSON.stringify(image)
)
}
// For direct comparison of simple content
return JSON.stringify(msg.content) === JSON.stringify(image)
})
})
if (uniqueImages.length === 0) {
return undefined
}
// Create messages with the original file references for storage/display
const imageMessageWithFileRef = {
role: 'user',
content: options.uploads.map((upload: IFileUpload) => ({
type: upload.type,
name: upload.name,
mime: upload.mime,
imageResolution: modelConfig?.imageResolution
}))
}
// Create messages with base64 data for the LLM
const imageMessageWithBase64 = {
role: 'user',
content: uniqueImages
}
return {
imageMessageWithFileRef,
imageMessageWithBase64
}
}
/**
* Get past chat history image messages
* @param pastChatHistory Array of past chat history messages
* @param options Common options object
* @returns Object containing updatedPastMessages and transformedPastMessages
*/
export const getPastChatHistoryImageMessages = async (
pastChatHistory: BaseMessageLike[],
options: ICommonObject
): Promise<{ updatedPastMessages: BaseMessageLike[]; transformedPastMessages: BaseMessageLike[] }> => {
const chatHistory = []
const transformedPastMessages = []
for (let i = 0; i < pastChatHistory.length; i++) {
const message = pastChatHistory[i] as BaseMessage & { role: string }
const messageRole = message.role || 'user'
if (message.additional_kwargs && message.additional_kwargs.fileUploads) {
// example: [{"type":"stored-file","name":"0_DiXc4ZklSTo3M8J4.jpg","mime":"image/jpeg"}]
const fileUploads = message.additional_kwargs.fileUploads
try {
let messageWithFileUploads = ''
const uploads: IFileUpload[] = typeof fileUploads === 'string' ? JSON.parse(fileUploads) : fileUploads
const imageContents: MessageContentImageUrl[] = []
for (const upload of uploads) {
if (upload.type === 'stored-file' && upload.mime.startsWith('image/')) {
const fileData = await getFileFromStorage(upload.name, options.chatflowid, options.chatId)
// as the image is stored in the server, read the file and convert it to base64
const bf = 'data:' + upload.mime + ';base64,' + fileData.toString('base64')
imageContents.push({
type: 'image_url',
image_url: {
url: bf
}
})
} else if (upload.type === 'url' && upload.mime.startsWith('image') && upload.data) {
imageContents.push({
type: 'image_url',
image_url: {
url: upload.data
}
})
} else if (upload.type === 'stored-file:full') {
const fileLoaderNodeModule = await import('../../nodes/documentloaders/File/File')
// @ts-ignore
const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass()
const nodeOptions = {
retrieveAttachmentChatId: true,
chatflowid: options.chatflowid,
chatId: options.chatId
}
let fileInputFieldFromMimeType = 'txtFile'
fileInputFieldFromMimeType = mapMimeTypeToInputField(upload.mime)
const nodeData = {
inputs: {
[fileInputFieldFromMimeType]: `FILE-STORAGE::${JSON.stringify([upload.name])}`
}
}
const documents: string = await fileLoaderNodeInstance.init(nodeData, '', nodeOptions)
messageWithFileUploads += `<doc name='${upload.name}'>${documents}</doc>\n\n`
}
}
const messageContent = messageWithFileUploads ? `${messageWithFileUploads}\n\n${message.content}` : message.content
if (imageContents.length > 0) {
chatHistory.push({
role: messageRole,
content: imageContents
})
transformedPastMessages.push({
role: messageRole,
content: [...JSON.parse((pastChatHistory[i] as any).additional_kwargs.fileUploads)]
})
}
chatHistory.push({
role: messageRole,
content: messageContent
})
} catch (e) {
// failed to parse fileUploads, continue with text only
chatHistory.push({
role: messageRole,
content: message.content
})
}
} else {
chatHistory.push({
role: messageRole,
content: message.content
})
}
}
return {
updatedPastMessages: chatHistory,
transformedPastMessages
}
}
/**
* Updates the flow state with new values
*/
export const updateFlowState = (state: ICommonObject, llmUpdateState: IFlowState[]): ICommonObject => {
let newFlowState: Record<string, any> = {}
for (const state of llmUpdateState) {
newFlowState[state.key] = state.value
}
return {
...state,
...newFlowState
}
}
@@ -224,7 +224,7 @@ class OpenAIAssistant_Agents implements INode {
const openai = new OpenAI({ apiKey: openAIApiKey })
// Start analytics
const analyticHandlers = new AnalyticHandler(nodeData, options)
const analyticHandlers = AnalyticHandler.getInstance(nodeData, options)
await analyticHandlers.init()
const parentIds = await analyticHandlers.onChainStart('OpenAIAssistant', input)
@@ -743,7 +743,7 @@ class OpenAIAssistant_Agents implements INode {
state = await promise(threadId, newRunThread.id)
} else {
const errMsg = `Error processing thread: ${state}, Thread ID: ${threadId}`
await analyticHandlers.onChainError(parentIds, errMsg)
await analyticHandlers.onChainError(parentIds, errMsg, true)
throw new Error(errMsg)
}
}
@@ -62,7 +62,8 @@ class GoogleGenerativeAI_ChatModels implements INode {
type: 'string',
placeholder: 'gemini-1.5-pro-exp-0801',
description: 'Custom model name to use. If provided, it will override the model selected',
additionalParams: true
additionalParams: true,
optional: true
},
{
label: 'Temperature',
@@ -21,7 +21,7 @@ class ChatOpenAI_ChatModels implements INode {
constructor() {
this.label = 'ChatOpenAI'
this.name = 'chatOpenAI'
this.version = 8.1
this.version = 8.2
this.type = 'ChatOpenAI'
this.icon = 'openai.svg'
this.category = 'Chat Models'
@@ -172,7 +172,9 @@ class ChatOpenAI_ChatModels implements INode {
],
default: 'low',
optional: false,
additionalParams: true
show: {
allowImageUploads: true
}
},
{
label: 'Reasoning Effort',
@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="a" d="M5.5,22.9722h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,25.0278,42.5h0V22.9722Z"/><path class="a" d="M14.2361,14.2361h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556a8.7361,8.7361,0,0,0,8.7361,8.7361h0V14.2361Z"/><path class="a" d="M22.9722,5.5h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,42.5,25.0278h0V5.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 699 B

@@ -313,6 +313,7 @@ class ChatflowTool extends StructuredTool {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'flowise-tool': 'true',
...this.headers
},
body: JSON.stringify(body)
+3 -4
View File
@@ -6,7 +6,6 @@
"types": "dist/src/index.d.ts",
"scripts": {
"build": "tsc && gulp",
"dev:gulp": "gulp",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"clean": "rimraf dist",
"nuke": "rimraf dist node_modules .turbo"
@@ -26,7 +25,7 @@
"@aws-sdk/client-s3": "^3.427.0",
"@aws-sdk/client-secrets-manager": "^3.699.0",
"@datastax/astra-db-ts": "1.5.0",
"@dqbd/tiktoken": "^1.0.7",
"@dqbd/tiktoken": "^1.0.21",
"@e2b/code-interpreter": "^0.0.5",
"@elastic/elasticsearch": "^8.9.0",
"@flowiseai/nodevm": "^3.9.25",
@@ -52,7 +51,7 @@
"@langchain/mistralai": "^0.2.0",
"@langchain/mongodb": "^0.0.1",
"@langchain/ollama": "0.2.0",
"@langchain/openai": "0.4.4",
"@langchain/openai": "0.5.6",
"@langchain/pinecone": "^0.1.3",
"@langchain/qdrant": "^0.0.5",
"@langchain/weaviate": "^0.0.1",
@@ -122,7 +121,7 @@
"notion-to-md": "^3.1.1",
"object-hash": "^3.0.0",
"ollama": "^0.5.11",
"openai": "^4.82.0",
"openai": "^4.96.0",
"papaparse": "^5.4.1",
"pdf-parse": "^1.1.1",
"pdfjs-dist": "^3.7.107",
+34 -2
View File
@@ -8,6 +8,7 @@ import { Moderation } from '../nodes/moderation/Moderation'
export type NodeParamsType =
| 'asyncOptions'
| 'asyncMultiOptions'
| 'options'
| 'multiOptions'
| 'datagrid'
@@ -57,12 +58,13 @@ export interface INodeOptionsValue {
label: string
name: string
description?: string
imageSrc?: string
}
export interface INodeOutputsValue {
label: string
name: string
baseClasses: string[]
baseClasses?: string[]
description?: string
hidden?: boolean
isAnchor?: boolean
@@ -83,10 +85,12 @@ export interface INodeParams {
rows?: number
list?: boolean
acceptVariable?: boolean
acceptNodeOutputAsVariable?: boolean
placeholder?: string
fileType?: string
additionalParams?: boolean
loadMethod?: string
loadConfig?: boolean
hidden?: boolean
hideCodeExecute?: boolean
codeExample?: string
@@ -96,6 +100,11 @@ export interface INodeParams {
refresh?: boolean
freeSolo?: boolean
loadPreviousNodes?: boolean
array?: Array<INodeParams>
show?: INodeDisplay
hide?: INodeDisplay
generateDocStoreDescription?: boolean
generateInstruction?: boolean
}
export interface INodeExecutionData {
@@ -103,7 +112,7 @@ export interface INodeExecutionData {
}
export interface INodeDisplay {
[key: string]: string[] | string
[key: string]: string[] | string | boolean | number | ICommonObject
}
export interface INodeProperties {
@@ -120,11 +129,15 @@ export interface INodeProperties {
badge?: string
deprecateMessage?: string
hideOutput?: boolean
hideInput?: boolean
author?: string
documentation?: string
color?: string
hint?: string
}
export interface INode extends INodeProperties {
credential?: INodeParams
inputs?: INodeParams[]
output?: INodeOutputsValue[]
loadMethods?: {
@@ -412,14 +425,19 @@ export interface IServerSideEventStreamer {
streamCustomEvent(chatId: string, eventType: string, data: any): void
streamSourceDocumentsEvent(chatId: string, data: any): void
streamUsedToolsEvent(chatId: string, data: any): void
streamCalledToolsEvent(chatId: string, data: any): void
streamFileAnnotationsEvent(chatId: string, data: any): void
streamToolEvent(chatId: string, data: any): void
streamAgentReasoningEvent(chatId: string, data: any): void
streamAgentFlowExecutedDataEvent(chatId: string, data: any): void
streamAgentFlowEvent(chatId: string, data: any): void
streamNextAgentEvent(chatId: string, data: any): void
streamNextAgentFlowEvent(chatId: string, data: any): void
streamActionEvent(chatId: string, data: any): void
streamArtifactsEvent(chatId: string, data: any): void
streamAbortEvent(chatId: string): void
streamEndEvent(chatId: string): void
streamUsageMetadataEvent(chatId: string, data: any): void
}
export enum FollowUpPromptProvider {
@@ -446,3 +464,17 @@ export type FollowUpPromptConfig = {
status: boolean
selectedProvider: FollowUpPromptProvider
} & FollowUpPromptProviderConfig
export interface ICondition {
type: string
value1: CommonType
operation: string
value2: CommonType
isFulfilled?: boolean
}
export interface IHumanInput {
type: 'proceed' | 'reject'
startNodeId: string
feedback?: string
}
@@ -0,0 +1,655 @@
import { ICommonObject } from './Interface'
import { z } from 'zod'
import { StructuredOutputParser } from '@langchain/core/output_parsers'
import { isEqual, get, cloneDeep } from 'lodash'
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
const ToolType = z.array(z.string()).describe('List of tools')
// Define a more specific NodePosition schema
const NodePositionType = z.object({
x: z.number().describe('X coordinate of the node position'),
y: z.number().describe('Y coordinate of the node position')
})
// Define a more specific EdgeData schema
const EdgeDataType = z.object({
edgeLabel: z.string().optional().describe('Label for the edge')
})
// Define a basic NodeData schema to avoid using .passthrough() which might cause issues
const NodeDataType = z
.object({
label: z.string().optional().describe('Label for the node'),
name: z.string().optional().describe('Name of the node')
})
.optional()
const NodeType = z.object({
id: z.string().describe('Unique identifier for the node'),
type: z.enum(['agentFlow']).describe('Type of the node'),
position: NodePositionType.describe('Position of the node in the UI'),
width: z.number().describe('Width of the node'),
height: z.number().describe('Height of the node'),
selected: z.boolean().optional().describe('Whether the node is selected'),
positionAbsolute: NodePositionType.optional().describe('Absolute position of the node'),
data: NodeDataType
})
const EdgeType = z.object({
id: z.string().describe('Unique identifier for the edge'),
type: z.enum(['agentFlow']).describe('Type of the node'),
source: z.string().describe('ID of the source node'),
sourceHandle: z.string().describe('ID of the source handle'),
target: z.string().describe('ID of the target node'),
targetHandle: z.string().describe('ID of the target handle'),
data: EdgeDataType.optional().describe('Data associated with the edge')
})
const NodesEdgesType = z
.object({
description: z.string().optional().describe('Description of the workflow'),
usecases: z.array(z.string()).optional().describe('Use cases for this workflow'),
nodes: z.array(NodeType).describe('Array of nodes in the workflow'),
edges: z.array(EdgeType).describe('Array of edges connecting the nodes')
})
.describe('Generate Agentflowv2 nodes and edges')
interface NodePosition {
x: number
y: number
}
interface EdgeData {
edgeLabel?: string
sourceColor?: string
targetColor?: string
isHumanInput?: boolean
}
interface AgentToolConfig {
agentSelectedTool: string
agentSelectedToolConfig: {
agentSelectedTool: string
}
}
interface NodeInputs {
agentTools?: AgentToolConfig[]
selectedTool?: string
toolInputArgs?: Record<string, any>[]
selectedToolConfig?: {
selectedTool: string
}
[key: string]: any
}
interface NodeData {
label?: string
name?: string
id?: string
inputs?: NodeInputs
inputAnchors?: InputAnchor[]
inputParams?: InputParam[]
outputs?: Record<string, any>
outputAnchors?: OutputAnchor[]
credential?: string
color?: string
[key: string]: any
}
interface Node {
id: string
type: 'agentFlow' | 'iteration'
position: NodePosition
width: number
height: number
selected?: boolean
positionAbsolute?: NodePosition
data: NodeData
parentNode?: string
extent?: string
}
interface Edge {
id: string
type: 'agentFlow'
source: string
sourceHandle: string
target: string
targetHandle: string
data?: EdgeData
label?: string
}
interface InputAnchor {
id: string
label: string
name: string
type?: string
[key: string]: any
}
interface InputParam {
id: string
name: string
label?: string
type?: string
display?: boolean
show?: Record<string, any>
hide?: Record<string, any>
[key: string]: any
}
interface OutputAnchor {
id: string
label: string
name: string
}
export const generateAgentflowv2 = async (config: Record<string, any>, question: string, options: ICommonObject) => {
try {
const result = await generateNodesEdges(config, question, options)
const { nodes, edges } = generateNodesData(result, config)
const updatedNodes = await generateSelectedTools(nodes, config, question, options)
const updatedEdges = updateEdges(edges, nodes)
return { nodes: updatedNodes, edges: updatedEdges }
} catch (error) {
console.error('Error generating AgentflowV2:', error)
return { error: error.message || 'Unknown error occurred' }
}
}
const updateEdges = (edges: Edge[], nodes: Node[]): Edge[] => {
const isMultiOutput = (source: string) => {
return source.includes('conditionAgentflow') || source.includes('conditionAgentAgentflow') || source.includes('humanInputAgentflow')
}
const findNodeColor = (nodeId: string) => {
const node = nodes.find((node) => node.id === nodeId)
return node?.data?.color
}
// filter out edges that do not exist in nodes
edges = edges.filter((edge) => {
return nodes.some((node) => node.id === edge.source || node.id === edge.target)
})
// filter out the edge that has hideInput/hideOutput on the source/target node
const indexToDelete = []
for (let i = 0; i < edges.length; i += 1) {
const edge = edges[i]
const sourceNode = nodes.find((node) => node.id === edge.source)
if (sourceNode?.data?.hideOutput) {
indexToDelete.push(i)
}
const targetNode = nodes.find((node) => node.id === edge.target)
if (targetNode?.data?.hideInput) {
indexToDelete.push(i)
}
}
// delete the edges at the index in indexToDelete
for (let i = indexToDelete.length - 1; i >= 0; i -= 1) {
edges.splice(indexToDelete[i], 1)
}
const updatedEdges = edges.map((edge) => {
return {
...edge,
data: {
...edge.data,
sourceColor: findNodeColor(edge.source),
targetColor: findNodeColor(edge.target),
edgeLabel: isMultiOutput(edge.source) && edge.label && edge.label.trim() !== '' ? edge.label.trim() : undefined,
isHumanInput: edge.source.includes('humanInputAgentflow') ? true : false
},
type: 'agentFlow',
id: `${edge.source}-${edge.sourceHandle}-${edge.target}-${edge.targetHandle}`
}
}) as Edge[]
if (updatedEdges.length > 0) {
updatedEdges.forEach((edge) => {
if (isMultiOutput(edge.source)) {
if (edge.sourceHandle.includes('true')) {
edge.sourceHandle = edge.sourceHandle.replace('true', '0')
} else if (edge.sourceHandle.includes('false')) {
edge.sourceHandle = edge.sourceHandle.replace('false', '1')
}
}
})
}
return updatedEdges
}
const generateSelectedTools = async (nodes: Node[], config: Record<string, any>, question: string, options: ICommonObject) => {
const selectedTools: string[] = []
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]
if (!node.data.inputs) {
node.data.inputs = {}
}
if (node.data.name === 'agentAgentflow') {
const sysPrompt = `You are a workflow orchestrator that is designed to make agent coordination and execution easy. Your goal is to select the tools that are needed to achieve the given task.
Here are the tools to choose from:
${config.toolNodes}
Here's the selected tools:
${JSON.stringify(selectedTools, null, 2)}
Output Format should be a list of tool names:
For example:["googleCustomSearch", "slackMCP"]
Now, select the tools that are needed to achieve the given task. You must only select tools that are in the list of tools above. You must NOT select the tools that are already in the list of selected tools.
`
const tools = await _generateSelectedTools({ ...config, prompt: sysPrompt }, question, options)
if (Array.isArray(tools) && tools.length > 0) {
selectedTools.push(...tools)
const existingTools = node.data.inputs.agentTools || []
node.data.inputs.agentTools = [
...existingTools,
...tools.map((tool) => ({
agentSelectedTool: tool,
agentSelectedToolConfig: {
agentSelectedTool: tool
}
}))
]
}
} else if (node.data.name === 'toolAgentflow') {
const sysPrompt = `You are a workflow orchestrator that is designed to make agent coordination and execution easy. Your goal is to select ONE tool that is needed to achieve the given task.
Here are the tools to choose from:
${config.toolNodes}
Here's the selected tools:
${JSON.stringify(selectedTools, null, 2)}
Output Format should ONLY one tool name inside of a list:
For example:["googleCustomSearch"]
Now, select the ONLY tool that is needed to achieve the given task. You must only select tool that is in the list of tools above. You must NOT select the tool that is already in the list of selected tools.
`
const tools = await _generateSelectedTools({ ...config, prompt: sysPrompt }, question, options)
if (Array.isArray(tools) && tools.length > 0) {
selectedTools.push(...tools)
node.data.inputs.selectedTool = tools[0]
node.data.inputs.toolInputArgs = []
node.data.inputs.selectedToolConfig = {
selectedTool: tools[0]
}
}
}
}
return nodes
}
const _generateSelectedTools = async (config: Record<string, any>, question: string, options: ICommonObject) => {
try {
const chatModelComponent = config.componentNodes[config.selectedChatModel?.name]
if (!chatModelComponent) {
throw new Error('Chat model component not found')
}
const nodeInstanceFilePath = chatModelComponent.filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newToolNodeInstance = new nodeModule.nodeClass()
const model = (await newToolNodeInstance.init(config.selectedChatModel, '', options)) as BaseChatModel
// Create a parser to validate the output
const parser = StructuredOutputParser.fromZodSchema(ToolType)
// Generate JSON schema from our Zod schema
const formatInstructions = parser.getFormatInstructions()
// Full conversation with system prompt and instructions
const messages = [
{
role: 'system',
content: `${config.prompt}\n\n${formatInstructions}\n\nMake sure to follow the exact JSON schema structure.`
},
{
role: 'user',
content: question
}
]
// Standard completion without structured output
const response = await model.invoke(messages)
// Try to extract JSON from the response
const responseContent = response.content.toString()
const jsonMatch = responseContent.match(/```json\n([\s\S]*?)\n```/) || responseContent.match(/{[\s\S]*?}/)
if (jsonMatch) {
const jsonStr = jsonMatch[1] || jsonMatch[0]
try {
const parsedJSON = JSON.parse(jsonStr)
// Validate with our schema
return ToolType.parse(parsedJSON)
} catch (parseError) {
console.error('Error parsing JSON from response:', parseError)
return { error: 'Failed to parse JSON from response', content: responseContent }
}
} else {
console.error('No JSON found in response:', responseContent)
return { error: 'No JSON found in response', content: responseContent }
}
} catch (error) {
console.error('Error generating AgentflowV2:', error)
return { error: error.message || 'Unknown error occurred' }
}
}
const generateNodesEdges = async (config: Record<string, any>, question: string, options?: ICommonObject) => {
try {
const chatModelComponent = config.componentNodes[config.selectedChatModel?.name]
if (!chatModelComponent) {
throw new Error('Chat model component not found')
}
const nodeInstanceFilePath = chatModelComponent.filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newToolNodeInstance = new nodeModule.nodeClass()
const model = (await newToolNodeInstance.init(config.selectedChatModel, '', options)) as BaseChatModel
// Create a parser to validate the output
const parser = StructuredOutputParser.fromZodSchema(NodesEdgesType)
// Generate JSON schema from our Zod schema
const formatInstructions = parser.getFormatInstructions()
// Full conversation with system prompt and instructions
const messages = [
{
role: 'system',
content: `${config.prompt}\n\n${formatInstructions}\n\nMake sure to follow the exact JSON schema structure.`
},
{
role: 'user',
content: question
}
]
// Standard completion without structured output
const response = await model.invoke(messages)
// Try to extract JSON from the response
const responseContent = response.content.toString()
const jsonMatch = responseContent.match(/```json\n([\s\S]*?)\n```/) || responseContent.match(/{[\s\S]*?}/)
if (jsonMatch) {
const jsonStr = jsonMatch[1] || jsonMatch[0]
try {
const parsedJSON = JSON.parse(jsonStr)
// Validate with our schema
return NodesEdgesType.parse(parsedJSON)
} catch (parseError) {
console.error('Error parsing JSON from response:', parseError)
return { error: 'Failed to parse JSON from response', content: responseContent }
}
} else {
console.error('No JSON found in response:', responseContent)
return { error: 'No JSON found in response', content: responseContent }
}
} catch (error) {
console.error('Error generating AgentflowV2:', error)
return { error: error.message || 'Unknown error occurred' }
}
}
const generateNodesData = (result: Record<string, any>, config: Record<string, any>) => {
try {
if (result.error) {
return result
}
let nodes = result.nodes
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]
let nodeName = node.data.name
// If nodeName is not found in data.name, try extracting from node.id
if (!nodeName || !config.componentNodes[nodeName]) {
nodeName = node.id.split('_')[0]
}
const componentNode = config.componentNodes[nodeName]
if (!componentNode) {
continue
}
const initializedNodeData = initNode(cloneDeep(componentNode), node.id)
nodes[i].data = {
...initializedNodeData,
label: node.data?.label
}
if (nodes[i].data.name === 'iterationAgentflow') {
nodes[i].type = 'iteration'
}
if (nodes[i].parentNode) {
nodes[i].extent = 'parent'
}
}
return { nodes, edges: result.edges }
} catch (error) {
console.error('Error generating AgentflowV2:', error)
return { error: error.message || 'Unknown error occurred' }
}
}
const initNode = (nodeData: Record<string, any>, newNodeId: string): NodeData => {
const inputParams = []
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
// Inputs
for (let i = 0; i < incoming; i += 1) {
const newInput = {
...nodeData.inputs[i],
id: `${newNodeId}-input-${nodeData.inputs[i].name}-${nodeData.inputs[i].type}`
}
inputParams.push(newInput)
}
// Credential
if (nodeData.credential) {
const newInput = {
...nodeData.credential,
id: `${newNodeId}-input-${nodeData.credential.name}-${nodeData.credential.type}`
}
inputParams.unshift(newInput)
}
// Outputs
let outputAnchors = initializeOutputAnchors(nodeData, newNodeId)
/* Initial
inputs = [
{
label: 'field_label_1',
name: 'string'
},
{
label: 'field_label_2',
name: 'CustomType'
}
]
=> Convert to inputs, inputParams, inputAnchors
=> inputs = { 'field': 'defaultvalue' } // Turn into inputs object with default values
=> // For inputs that are part of whitelistTypes
inputParams = [
{
label: 'field_label_1',
name: 'string'
}
]
=> // For inputs that are not part of whitelistTypes
inputAnchors = [
{
label: 'field_label_2',
name: 'CustomType'
}
]
*/
// Inputs
if (nodeData.inputs) {
const defaultInputs = initializeDefaultNodeData(nodeData.inputs)
nodeData.inputAnchors = showHideInputAnchors({ ...nodeData, inputAnchors: [], inputs: defaultInputs })
nodeData.inputParams = showHideInputParams({ ...nodeData, inputParams, inputs: defaultInputs })
nodeData.inputs = defaultInputs
} else {
nodeData.inputAnchors = []
nodeData.inputParams = []
nodeData.inputs = {}
}
// Outputs
if (nodeData.outputs) {
nodeData.outputs = initializeDefaultNodeData(outputAnchors)
} else {
nodeData.outputs = {}
}
nodeData.outputAnchors = outputAnchors
// Credential
if (nodeData.credential) nodeData.credential = ''
nodeData.id = newNodeId
return nodeData
}
const initializeDefaultNodeData = (nodeParams: Record<string, any>[]) => {
const initialValues: Record<string, any> = {}
for (let i = 0; i < nodeParams.length; i += 1) {
const input = nodeParams[i]
initialValues[input.name] = input.default || ''
}
return initialValues
}
const createAgentFlowOutputs = (nodeData: Record<string, any>, newNodeId: string) => {
if (nodeData.hideOutput) return []
if (nodeData.outputs?.length) {
return nodeData.outputs.map((_: any, index: number) => ({
id: `${newNodeId}-output-${index}`,
label: nodeData.label,
name: nodeData.name
}))
}
return [
{
id: `${newNodeId}-output-${nodeData.name}`,
label: nodeData.label,
name: nodeData.name
}
]
}
const initializeOutputAnchors = (nodeData: Record<string, any>, newNodeId: string): OutputAnchor[] => {
return createAgentFlowOutputs(nodeData, newNodeId)
}
const _showHideOperation = (nodeData: Record<string, any>, inputParam: Record<string, any>, displayType: string, index?: number) => {
const displayOptions = inputParam[displayType]
/* For example:
show: {
enableMemory: true
}
*/
Object.keys(displayOptions).forEach((path) => {
const comparisonValue = displayOptions[path]
if (path.includes('$index') && index) {
path = path.replace('$index', index.toString())
}
const groundValue = get(nodeData.inputs, path, '')
if (Array.isArray(comparisonValue)) {
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'string') {
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'boolean') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'object') {
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
inputParam.display = false
}
} else if (typeof comparisonValue === 'number') {
if (displayType === 'show' && comparisonValue !== groundValue) {
inputParam.display = false
}
if (displayType === 'hide' && comparisonValue === groundValue) {
inputParam.display = false
}
}
})
}
const showHideInputs = (nodeData: Record<string, any>, inputType: string, overrideParams?: Record<string, any>, arrayIndex?: number) => {
const params = overrideParams ?? nodeData[inputType] ?? []
for (let i = 0; i < params.length; i += 1) {
const inputParam = params[i]
// Reset display flag to false for each inputParam
inputParam.display = true
if (inputParam.show) {
_showHideOperation(nodeData, inputParam, 'show', arrayIndex)
}
if (inputParam.hide) {
_showHideOperation(nodeData, inputParam, 'hide', arrayIndex)
}
}
return params
}
const showHideInputParams = (nodeData: Record<string, any>): InputParam[] => {
return showHideInputs(nodeData, 'inputParams')
}
const showHideInputAnchors = (nodeData: Record<string, any>): InputAnchor[] => {
return showHideInputs(nodeData, 'inputAnchors')
}
+177 -115
View File
@@ -29,7 +29,7 @@ import { ICommonObject, IDatabaseEntity, INodeData, IServerSideEventStreamer } f
import { LangWatch, LangWatchSpan, LangWatchTrace, autoconvertTypedValues } from 'langwatch'
import { DataSource } from 'typeorm'
import { ChatGenerationChunk } from '@langchain/core/outputs'
import { AIMessageChunk } from '@langchain/core/messages'
import { AIMessageChunk, BaseMessageLike } from '@langchain/core/messages'
import { Serialized } from '@langchain/core/load/serializable'
interface AgentRun extends Run {
@@ -635,137 +635,184 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO
}
export class AnalyticHandler {
nodeData: INodeData
options: ICommonObject = {}
handlers: ICommonObject = {}
private static instances: Map<string, AnalyticHandler> = new Map()
private nodeData: INodeData
private options: ICommonObject
private handlers: ICommonObject = {}
private initialized: boolean = false
private analyticsConfig: string | undefined
private chatId: string
private createdAt: number
constructor(nodeData: INodeData, options: ICommonObject) {
this.options = options
private constructor(nodeData: INodeData, options: ICommonObject) {
this.nodeData = nodeData
this.init()
this.options = options
this.analyticsConfig = options.analytic
this.chatId = options.chatId
this.createdAt = Date.now()
}
static getInstance(nodeData: INodeData, options: ICommonObject): AnalyticHandler {
const chatId = options.chatId
if (!chatId) throw new Error('ChatId is required for analytics')
// Reset instance if analytics config changed for this chat
const instance = AnalyticHandler.instances.get(chatId)
if (instance?.analyticsConfig !== options.analytic) {
AnalyticHandler.resetInstance(chatId)
}
if (!AnalyticHandler.instances.get(chatId)) {
AnalyticHandler.instances.set(chatId, new AnalyticHandler(nodeData, options))
}
return AnalyticHandler.instances.get(chatId)!
}
static resetInstance(chatId: string): void {
AnalyticHandler.instances.delete(chatId)
}
// Keep this as backup for orphaned instances
static cleanup(maxAge: number = 3600000): void {
const now = Date.now()
for (const [chatId, instance] of AnalyticHandler.instances) {
if (now - instance.createdAt > maxAge) {
AnalyticHandler.resetInstance(chatId)
}
}
}
async init() {
if (this.initialized) return
try {
if (!this.options.analytic) return
const analytic = JSON.parse(this.options.analytic)
for (const provider in analytic) {
const providerStatus = analytic[provider].status as boolean
if (providerStatus) {
const credentialId = analytic[provider].credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', this.options)
if (provider === 'langSmith') {
const langSmithProject = analytic[provider].projectName as string
const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData)
const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData)
const client = new LangsmithClient({
apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com',
apiKey: langSmithApiKey
})
this.handlers['langSmith'] = { client, langSmithProject }
} else if (provider === 'langFuse') {
const release = analytic[provider].release as string
const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData)
const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData)
const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData)
const langfuse = new Langfuse({
secretKey: langFuseSecretKey,
publicKey: langFusePublicKey,
baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com',
sdkIntegration: 'Flowise',
release
})
this.handlers['langFuse'] = { client: langfuse }
} else if (provider === 'lunary') {
const lunaryPublicKey = getCredentialParam('lunaryAppId', credentialData, this.nodeData)
const lunaryEndpoint = getCredentialParam('lunaryEndpoint', credentialData, this.nodeData)
lunary.init({
publicKey: lunaryPublicKey,
apiUrl: lunaryEndpoint,
runtime: 'flowise'
})
this.handlers['lunary'] = { client: lunary }
} else if (provider === 'langWatch') {
const langWatchApiKey = getCredentialParam('langWatchApiKey', credentialData, this.nodeData)
const langWatchEndpoint = getCredentialParam('langWatchEndpoint', credentialData, this.nodeData)
const langwatch = new LangWatch({
apiKey: langWatchApiKey,
endpoint: langWatchEndpoint
})
this.handlers['langWatch'] = { client: langwatch }
} else if (provider === 'arize') {
const arizeApiKey = getCredentialParam('arizeApiKey', credentialData, this.nodeData)
const arizeSpaceId = getCredentialParam('arizeSpaceId', credentialData, this.nodeData)
const arizeEndpoint = getCredentialParam('arizeEndpoint', credentialData, this.nodeData)
const arizeProject = analytic[provider].projectName as string
let arizeOptions: ArizeTracerOptions = {
apiKey: arizeApiKey,
spaceId: arizeSpaceId,
baseUrl: arizeEndpoint ?? 'https://otlp.arize.com',
projectName: arizeProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const arize: Tracer | undefined = getArizeTracer(arizeOptions)
const rootSpan: Span | undefined = undefined
this.handlers['arize'] = { client: arize, arizeProject, rootSpan }
} else if (provider === 'phoenix') {
const phoenixApiKey = getCredentialParam('phoenixApiKey', credentialData, this.nodeData)
const phoenixEndpoint = getCredentialParam('phoenixEndpoint', credentialData, this.nodeData)
const phoenixProject = analytic[provider].projectName as string
let phoenixOptions: PhoenixTracerOptions = {
apiKey: phoenixApiKey,
baseUrl: phoenixEndpoint ?? 'https://app.phoenix.arize.com',
projectName: phoenixProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const phoenix: Tracer | undefined = getPhoenixTracer(phoenixOptions)
const rootSpan: Span | undefined = undefined
this.handlers['phoenix'] = { client: phoenix, phoenixProject, rootSpan }
} else if (provider === 'opik') {
const opikApiKey = getCredentialParam('opikApiKey', credentialData, this.nodeData)
const opikEndpoint = getCredentialParam('opikUrl', credentialData, this.nodeData)
const opikWorkspace = getCredentialParam('opikWorkspace', credentialData, this.nodeData)
const opikProject = analytic[provider].opikProjectName as string
let opikOptions: OpikTracerOptions = {
apiKey: opikApiKey,
baseUrl: opikEndpoint ?? 'https://www.comet.com/opik/api',
projectName: opikProject ?? 'default',
workspace: opikWorkspace ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const opik: Tracer | undefined = getOpikTracer(opikOptions)
const rootSpan: Span | undefined = undefined
this.handlers['opik'] = { client: opik, opikProject, rootSpan }
}
await this.initializeProvider(provider, analytic[provider], credentialData)
}
}
this.initialized = true
} catch (e) {
throw new Error(e)
}
}
// Add getter for handlers (useful for debugging)
getHandlers(): ICommonObject {
return this.handlers
}
async initializeProvider(provider: string, providerConfig: any, credentialData: any) {
if (provider === 'langSmith') {
const langSmithProject = providerConfig.projectName as string
const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData)
const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData)
const client = new LangsmithClient({
apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com',
apiKey: langSmithApiKey
})
this.handlers['langSmith'] = { client, langSmithProject }
} else if (provider === 'langFuse') {
const release = providerConfig.release as string
const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData)
const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData)
const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData)
const langfuse = new Langfuse({
secretKey: langFuseSecretKey,
publicKey: langFusePublicKey,
baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com',
sdkIntegration: 'Flowise',
release
})
this.handlers['langFuse'] = { client: langfuse }
} else if (provider === 'lunary') {
const lunaryPublicKey = getCredentialParam('lunaryAppId', credentialData, this.nodeData)
const lunaryEndpoint = getCredentialParam('lunaryEndpoint', credentialData, this.nodeData)
lunary.init({
publicKey: lunaryPublicKey,
apiUrl: lunaryEndpoint,
runtime: 'flowise'
})
this.handlers['lunary'] = { client: lunary }
} else if (provider === 'langWatch') {
const langWatchApiKey = getCredentialParam('langWatchApiKey', credentialData, this.nodeData)
const langWatchEndpoint = getCredentialParam('langWatchEndpoint', credentialData, this.nodeData)
const langwatch = new LangWatch({
apiKey: langWatchApiKey,
endpoint: langWatchEndpoint
})
this.handlers['langWatch'] = { client: langwatch }
} else if (provider === 'arize') {
const arizeApiKey = getCredentialParam('arizeApiKey', credentialData, this.nodeData)
const arizeSpaceId = getCredentialParam('arizeSpaceId', credentialData, this.nodeData)
const arizeEndpoint = getCredentialParam('arizeEndpoint', credentialData, this.nodeData)
const arizeProject = providerConfig.projectName as string
let arizeOptions: ArizeTracerOptions = {
apiKey: arizeApiKey,
spaceId: arizeSpaceId,
baseUrl: arizeEndpoint ?? 'https://otlp.arize.com',
projectName: arizeProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const arize: Tracer | undefined = getArizeTracer(arizeOptions)
const rootSpan: Span | undefined = undefined
this.handlers['arize'] = { client: arize, arizeProject, rootSpan }
} else if (provider === 'phoenix') {
const phoenixApiKey = getCredentialParam('phoenixApiKey', credentialData, this.nodeData)
const phoenixEndpoint = getCredentialParam('phoenixEndpoint', credentialData, this.nodeData)
const phoenixProject = providerConfig.projectName as string
let phoenixOptions: PhoenixTracerOptions = {
apiKey: phoenixApiKey,
baseUrl: phoenixEndpoint ?? 'https://app.phoenix.arize.com',
projectName: phoenixProject ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const phoenix: Tracer | undefined = getPhoenixTracer(phoenixOptions)
const rootSpan: Span | undefined = undefined
this.handlers['phoenix'] = { client: phoenix, phoenixProject, rootSpan }
} else if (provider === 'opik') {
const opikApiKey = getCredentialParam('opikApiKey', credentialData, this.nodeData)
const opikEndpoint = getCredentialParam('opikUrl', credentialData, this.nodeData)
const opikWorkspace = getCredentialParam('opikWorkspace', credentialData, this.nodeData)
const opikProject = providerConfig.opikProjectName as string
let opikOptions: OpikTracerOptions = {
apiKey: opikApiKey,
baseUrl: opikEndpoint ?? 'https://www.comet.com/opik/api',
projectName: opikProject ?? 'default',
workspace: opikWorkspace ?? 'default',
sdkIntegration: 'Flowise',
enableCallback: false
}
const opik: Tracer | undefined = getOpikTracer(opikOptions)
const rootSpan: Span | undefined = undefined
this.handlers['opik'] = { client: opik, opikProject, rootSpan }
}
}
async onChainStart(name: string, input: string, parentIds?: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
@@ -1077,6 +1124,11 @@ export class AnalyticHandler {
chainSpan.end()
}
}
if (shutdown) {
// Cleanup this instance when chain ends
AnalyticHandler.resetInstance(this.chatId)
}
}
async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) {
@@ -1155,9 +1207,14 @@ export class AnalyticHandler {
chainSpan.end()
}
}
if (shutdown) {
// Cleanup this instance when chain ends
AnalyticHandler.resetInstance(this.chatId)
}
}
async onLLMStart(name: string, input: string, parentIds: ICommonObject) {
async onLLMStart(name: string, input: string | BaseMessageLike[], parentIds: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
langFuse: {},
@@ -1169,13 +1226,18 @@ export class AnalyticHandler {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun]
if (parentRun) {
const inputs: any = {}
if (Array.isArray(input)) {
inputs.messages = input
} else {
inputs.prompts = [input]
}
const childLLMRun = await parentRun.createChild({
name,
run_type: 'llm',
inputs: {
prompts: [input]
}
inputs
})
await childLLMRun.postRun()
this.handlers['langSmith'].llmRun = { [childLLMRun.id]: childLLMRun }
+1
View File
@@ -11,3 +11,4 @@ export * from './storageUtils'
export * from './handler'
export * from './followUpPrompts'
export * from './validator'
export * from './agentflowv2Generator'
+13 -7
View File
@@ -712,7 +712,7 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro
for (const message of chatmessages) {
if (message.role === 'apiMessage' || message.type === 'apiMessage') {
chatHistory.push(new AIMessage(message.content || ''))
} else if (message.role === 'userMessage' || message.role === 'userMessage') {
} else if (message.role === 'userMessage' || message.type === 'userMessage') {
// check for image/files uploads
if (message.fileUploads) {
// example: [{"type":"stored-file","name":"0_DiXc4ZklSTo3M8J4.jpg","mime":"image/jpeg"}]
@@ -788,17 +788,23 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro
* @param {IMessage[]} chatHistory
* @returns {string}
*/
export const convertChatHistoryToText = (chatHistory: IMessage[] = []): string => {
export const convertChatHistoryToText = (chatHistory: IMessage[] | { content: string; role: string }[] = []): string => {
return chatHistory
.map((chatMessage) => {
if (chatMessage.type === 'apiMessage') {
return `Assistant: ${chatMessage.message}`
} else if (chatMessage.type === 'userMessage') {
return `Human: ${chatMessage.message}`
if (!chatMessage) return ''
const messageContent = 'message' in chatMessage ? chatMessage.message : chatMessage.content
if (!messageContent || messageContent.trim() === '') return ''
const messageType = 'type' in chatMessage ? chatMessage.type : chatMessage.role
if (messageType === 'apiMessage' || messageType === 'assistant') {
return `Assistant: ${messageContent}`
} else if (messageType === 'userMessage' || messageType === 'user') {
return `Human: ${messageContent}`
} else {
return `${chatMessage.message}`
return `${messageContent}`
}
})
.filter((message) => message !== '') // Remove empty messages
.join('\n')
}