mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +03:00
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:
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¤t_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)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user