dtamind-components
Version:
Apps integration for Dtamind. Contain Nodes and Credentials.
1,049 lines • 73.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const messages_1 = require("@langchain/core/messages");
const prompt_1 = require("../prompt");
const agents_1 = require("../../../src/agents");
const lodash_1 = require("lodash");
const zod_to_json_schema_1 = __importDefault(require("zod-to-json-schema"));
const error_1 = require("../../../src/error");
const utils_1 = require("../utils");
class Agent_Agentflow {
constructor() {
//@ts-ignore
this.loadMethods = {
async listModels(_, options) {
const componentNodes = options.componentNodes;
const returnOptions = [];
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 listEmbeddings(_, options) {
const componentNodes = options.componentNodes;
const returnOptions = [];
for (const nodeName in componentNodes) {
const componentNode = componentNodes[nodeName];
if (componentNode.category === 'Embeddings') {
if (componentNode.tags?.includes('LlamaIndex')) {
continue;
}
returnOptions.push({
label: componentNode.label,
name: nodeName,
imageSrc: componentNode.icon
});
}
}
return returnOptions;
},
async listTools(_, options) {
const componentNodes = options.componentNodes;
const removeTools = ['chainTool', 'retrieverTool', 'webBrowser'];
const returnOptions = [];
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 listRuntimeStateKeys(_, options) {
const previousNodes = options.previousNodes;
const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow');
const state = startAgentflowNode?.inputs?.startState;
return state.map((item) => ({ label: item.key, name: item.key }));
},
async listStores(_, options) {
const returnData = [];
const appDataSource = options.appDataSource;
const databaseEntities = options.databaseEntities;
if (appDataSource === undefined || !appDataSource) {
return returnData;
}
const searchOptions = options.searchOptions || {};
const stores = await appDataSource.getRepository(databaseEntities['DocumentStore']).findBy(searchOptions);
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 listVectorStores(_, options) {
const componentNodes = options.componentNodes;
const returnOptions = [];
for (const nodeName in componentNodes) {
const componentNode = componentNodes[nodeName];
if (componentNode.category === 'Vector Stores') {
if (componentNode.tags?.includes('LlamaIndex')) {
continue;
}
returnOptions.push({
label: componentNode.label,
name: nodeName,
imageSrc: componentNode.icon
});
}
}
return returnOptions;
}
};
this.label = 'Agent';
this.name = 'agentAgentflow';
this.version = 1.0;
this.type = 'Agent';
this.category = 'Agent Flows';
this.description = 'Dynamically choose and utilize tools during runtime, enabling multi-step reasoning';
this.color = '#4DD0E1';
this.baseClasses = [this.type];
this.inputs = [
{
label: 'Model',
name: 'agentModel',
type: 'asyncOptions',
loadMethod: 'listModels',
loadConfig: true
},
{
label: 'Messages',
name: 'agentMessages',
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: 'Tools',
name: 'agentTools',
type: 'array',
optional: true,
array: [
{
label: 'Tool',
name: 'agentSelectedTool',
type: 'asyncOptions',
loadMethod: 'listTools',
loadConfig: true
},
{
label: 'Require Human Input',
name: 'agentSelectedToolRequiresHumanInput',
type: 'boolean',
optional: true
}
]
},
{
label: 'Knowledge (Document Stores)',
name: 'agentKnowledgeDocumentStores',
type: 'array',
description: 'Give your agent context about different document sources. Document stores must be upserted in advance.',
array: [
{
label: 'Document Store',
name: 'documentStore',
type: 'asyncOptions',
loadMethod: 'listStores'
},
{
label: 'Describe Knowledge',
name: 'docStoreDescription',
type: 'string',
generateDocStoreDescription: true,
placeholder: 'Describe what the knowledge base is about, this is useful for the AI to know when and how to search for correct information',
rows: 4
},
{
label: 'Return Source Documents',
name: 'returnSourceDocuments',
type: 'boolean',
optional: true
}
],
optional: true
},
{
label: 'Knowledge (Vector Embeddings)',
name: 'agentKnowledgeVSEmbeddings',
type: 'array',
description: 'Give your agent context about different document sources from existing vector stores and embeddings',
array: [
{
label: 'Vector Store',
name: 'vectorStore',
type: 'asyncOptions',
loadMethod: 'listVectorStores',
loadConfig: true
},
{
label: 'Embedding Model',
name: 'embeddingModel',
type: 'asyncOptions',
loadMethod: 'listEmbeddings',
loadConfig: true
},
{
label: 'Knowledge Name',
name: 'knowledgeName',
type: 'string',
placeholder: 'A short name for the knowledge base, this is useful for the AI to know when and how to search for correct information'
},
{
label: 'Describe Knowledge',
name: 'knowledgeDescription',
type: 'string',
placeholder: 'Describe what the knowledge base is about, this is useful for the AI to know when and how to search for correct information',
rows: 4
},
{
label: 'Return Source Documents',
name: 'returnSourceDocuments',
type: 'boolean',
optional: true
}
],
optional: true
},
{
label: 'Enable Memory',
name: 'agentEnableMemory',
type: 'boolean',
description: 'Enable memory for the conversation thread',
default: true,
optional: true
},
{
label: 'Memory Type',
name: 'agentMemoryType',
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: {
agentEnableMemory: true
}
},
{
label: 'Window Size',
name: 'agentMemoryWindowSize',
type: 'number',
default: '20',
description: 'Uses a fixed window size to surface the last N messages',
show: {
agentMemoryType: 'windowSize'
}
},
{
label: 'Max Token Limit',
name: 'agentMemoryMaxTokenLimit',
type: 'number',
default: '2000',
description: 'Summarize conversations once token limit is reached. Default to 2000',
show: {
agentMemoryType: 'conversationSummaryBuffer'
}
},
{
label: 'Input Message',
name: 'agentUserMessage',
type: 'string',
description: 'Add an input message as user message at the end of the conversation',
rows: 4,
optional: true,
acceptVariable: true,
show: {
agentEnableMemory: true
}
},
{
label: 'Return Response As',
name: 'agentReturnResponseAs',
type: 'options',
options: [
{
label: 'User Message',
name: 'userMessage'
},
{
label: 'Assistant Message',
name: 'assistantMessage'
}
],
default: 'userMessage'
},
{
label: 'Update Flow State',
name: 'agentUpdateState',
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
}
]
}
];
}
async run(nodeData, input, options) {
let llmIds;
let analyticHandlers = options.analyticHandlers;
try {
const abortController = options.abortController;
// Extract input parameters
const model = nodeData.inputs?.agentModel;
const modelConfig = nodeData.inputs?.agentModelConfig;
if (!model) {
throw new Error('Model is required');
}
// Extract tools
const tools = nodeData.inputs?.agentTools;
const toolsInstance = [];
for (const tool of tools) {
const toolConfig = tool.agentSelectedToolConfig;
const nodeInstanceFilePath = options.componentNodes[tool.agentSelectedTool].filePath;
const nodeModule = await Promise.resolve(`${nodeInstanceFilePath}`).then(s => __importStar(require(s)));
const newToolNodeInstance = new nodeModule.nodeClass();
const newNodeData = {
...nodeData,
credential: toolConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...toolConfig
}
};
const toolInstance = await newToolNodeInstance.init(newNodeData, '', options);
// toolInstance might returns a list of tools like MCP tools
if (Array.isArray(toolInstance)) {
for (const subTool of toolInstance) {
const subToolInstance = subTool;
subToolInstance.agentSelectedTool = tool.agentSelectedTool;
if (tool.agentSelectedToolRequiresHumanInput) {
;
subToolInstance.requiresHumanInput = true;
}
toolsInstance.push(subToolInstance);
}
}
else {
if (tool.agentSelectedToolRequiresHumanInput) {
toolInstance.requiresHumanInput = true;
}
toolsInstance.push(toolInstance);
}
}
const availableTools = toolsInstance.map((tool, index) => {
const originalTool = tools[index];
let agentSelectedTool = tool?.agentSelectedTool;
if (!agentSelectedTool) {
agentSelectedTool = originalTool?.agentSelectedTool;
}
const componentNode = options.componentNodes[agentSelectedTool];
const jsonSchema = (0, zod_to_json_schema_1.default)(tool.schema);
if (jsonSchema.$schema) {
delete jsonSchema.$schema;
}
return {
name: tool.name,
description: tool.description,
schema: jsonSchema,
toolNode: {
label: componentNode?.label || tool.name,
name: componentNode?.name || tool.name
}
};
});
// Extract knowledge
const knowledgeBases = nodeData.inputs?.agentKnowledgeDocumentStores;
if (knowledgeBases && knowledgeBases.length > 0) {
for (const knowledgeBase of knowledgeBases) {
const nodeInstanceFilePath = options.componentNodes['retrieverTool'].filePath;
const nodeModule = await Promise.resolve(`${nodeInstanceFilePath}`).then(s => __importStar(require(s)));
const newRetrieverToolNodeInstance = new nodeModule.nodeClass();
const [storeId, storeName] = knowledgeBase.documentStore.split(':');
const docStoreVectorInstanceFilePath = options.componentNodes['documentStoreVS'].filePath;
const docStoreVectorModule = await Promise.resolve(`${docStoreVectorInstanceFilePath}`).then(s => __importStar(require(s)));
const newDocStoreVectorInstance = new docStoreVectorModule.nodeClass();
const docStoreVectorInstance = await newDocStoreVectorInstance.init({
...nodeData,
inputs: {
...nodeData.inputs,
selectedStore: storeId
},
outputs: {
output: 'retriever'
}
}, '', options);
const newRetrieverToolNodeData = {
...nodeData,
inputs: {
...nodeData.inputs,
name: storeName
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, ''),
description: knowledgeBase.docStoreDescription,
retriever: docStoreVectorInstance,
returnSourceDocuments: knowledgeBase.returnSourceDocuments
}
};
const retrieverToolInstance = await newRetrieverToolNodeInstance.init(newRetrieverToolNodeData, '', options);
toolsInstance.push(retrieverToolInstance);
const jsonSchema = (0, zod_to_json_schema_1.default)(retrieverToolInstance.schema);
if (jsonSchema.$schema) {
delete jsonSchema.$schema;
}
const componentNode = options.componentNodes['retrieverTool'];
availableTools.push({
name: storeName
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, ''),
description: knowledgeBase.docStoreDescription,
schema: jsonSchema,
toolNode: {
label: componentNode?.label || retrieverToolInstance.name,
name: componentNode?.name || retrieverToolInstance.name
}
});
}
}
const knowledgeBasesForVSEmbeddings = nodeData.inputs?.agentKnowledgeVSEmbeddings;
if (knowledgeBasesForVSEmbeddings && knowledgeBasesForVSEmbeddings.length > 0) {
for (const knowledgeBase of knowledgeBasesForVSEmbeddings) {
const nodeInstanceFilePath = options.componentNodes['retrieverTool'].filePath;
const nodeModule = await Promise.resolve(`${nodeInstanceFilePath}`).then(s => __importStar(require(s)));
const newRetrieverToolNodeInstance = new nodeModule.nodeClass();
const selectedEmbeddingModel = knowledgeBase.embeddingModel;
const selectedEmbeddingModelConfig = knowledgeBase.embeddingModelConfig;
const embeddingInstanceFilePath = options.componentNodes[selectedEmbeddingModel].filePath;
const embeddingModule = await Promise.resolve(`${embeddingInstanceFilePath}`).then(s => __importStar(require(s)));
const newEmbeddingInstance = new embeddingModule.nodeClass();
const newEmbeddingNodeData = {
...nodeData,
credential: selectedEmbeddingModelConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...selectedEmbeddingModelConfig
}
};
const embeddingInstance = await newEmbeddingInstance.init(newEmbeddingNodeData, '', options);
const selectedVectorStore = knowledgeBase.vectorStore;
const selectedVectorStoreConfig = knowledgeBase.vectorStoreConfig;
const vectorStoreInstanceFilePath = options.componentNodes[selectedVectorStore].filePath;
const vectorStoreModule = await Promise.resolve(`${vectorStoreInstanceFilePath}`).then(s => __importStar(require(s)));
const newVectorStoreInstance = new vectorStoreModule.nodeClass();
const newVSNodeData = {
...nodeData,
credential: selectedVectorStoreConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...selectedVectorStoreConfig,
embeddings: embeddingInstance
},
outputs: {
output: 'retriever'
}
};
const vectorStoreInstance = await newVectorStoreInstance.init(newVSNodeData, '', options);
const knowledgeName = knowledgeBase.knowledgeName || '';
const newRetrieverToolNodeData = {
...nodeData,
inputs: {
...nodeData.inputs,
name: knowledgeName
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, ''),
description: knowledgeBase.knowledgeDescription,
retriever: vectorStoreInstance,
returnSourceDocuments: knowledgeBase.returnSourceDocuments
}
};
const retrieverToolInstance = await newRetrieverToolNodeInstance.init(newRetrieverToolNodeData, '', options);
toolsInstance.push(retrieverToolInstance);
const jsonSchema = (0, zod_to_json_schema_1.default)(retrieverToolInstance.schema);
if (jsonSchema.$schema) {
delete jsonSchema.$schema;
}
const componentNode = options.componentNodes['retrieverTool'];
availableTools.push({
name: knowledgeName
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, ''),
description: knowledgeBase.knowledgeDescription,
schema: jsonSchema,
toolNode: {
label: componentNode?.label || retrieverToolInstance.name,
name: componentNode?.name || retrieverToolInstance.name
}
});
}
}
// Extract memory and configuration options
const enableMemory = nodeData.inputs?.agentEnableMemory;
const memoryType = nodeData.inputs?.agentMemoryType;
const userMessage = nodeData.inputs?.agentUserMessage;
const _agentUpdateState = nodeData.inputs?.agentUpdateState;
const agentMessages = nodeData.inputs?.agentMessages ?? [];
// Extract runtime state and history
const state = options.agentflowRuntime?.state;
const pastChatHistory = options.pastChatHistory ?? [];
const runtimeChatHistory = options.agentflowRuntime?.chatHistory ?? [];
const prependedChatHistory = options.prependedChatHistory;
const chatId = options.chatId;
// Initialize the LLM model instance
const nodeInstanceFilePath = options.componentNodes[model].filePath;
const nodeModule = await Promise.resolve(`${nodeInstanceFilePath}`).then(s => __importStar(require(s)));
const newLLMNodeInstance = new nodeModule.nodeClass();
const newNodeData = {
...nodeData,
credential: modelConfig['FLOWISE_CREDENTIAL_ID'],
inputs: {
...nodeData.inputs,
...modelConfig
}
};
const llmWithoutToolsBind = (await newLLMNodeInstance.init(newNodeData, '', options));
let llmNodeInstance = llmWithoutToolsBind;
if (llmNodeInstance && toolsInstance.length > 0) {
if (llmNodeInstance.bindTools === undefined) {
throw new Error(`Agent needs to have a function calling capable models.`);
}
// @ts-ignore
llmNodeInstance = llmNodeInstance.bindTools(toolsInstance);
}
// Prepare messages array
const messages = [];
// Use to store messages with image file references as we do not want to store the base64 data into database
let runtimeImageMessagesWithFileRef = [];
// Use to keep track of past messages with image file references
let pastImageMessagesWithFileRef = [];
// Prepend history ONLY if it is the first node
if (prependedChatHistory.length > 0 && !runtimeChatHistory.length) {
for (const msg of prependedChatHistory) {
const role = msg.role === 'apiMessage' ? 'assistant' : 'user';
const content = msg.content ?? '';
messages.push({
role,
content
});
}
}
for (const msg of agentMessages) {
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 it does not exist in the agentMessages array
*/
if (options.uploads) {
const imageContents = await (0, utils_1.getUniqueImageMessages)(options, messages, modelConfig);
if (imageContents) {
const { imageMessageWithBase64, imageMessageWithFileRef } = imageContents;
messages.push(imageMessageWithBase64);
runtimeImageMessagesWithFileRef.push(imageMessageWithFileRef);
}
}
if (input && typeof input === 'string' && !agentMessages.some((msg) => msg.role === 'user')) {
messages.push({
role: 'user',
content: input
});
}
}
delete nodeData.inputs?.agentMessages;
// Initialize response and determine if streaming is possible
let response = new messages_1.AIMessageChunk('');
const isLastNode = options.isLastNode;
const isStreamable = isLastNode && options.sseStreamer !== undefined && modelConfig?.streaming !== false;
// 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();
// Get initial response from LLM
const sseStreamer = options.sseStreamer;
// Handle tool calls with support for recursion
let usedTools = [];
let sourceDocuments = [];
let artifacts = [];
let additionalTokens = 0;
let isWaitingForHumanInput = false;
// Store the current messages length to track which messages are added during tool calls
const messagesBeforeToolCalls = [...messages];
let _toolCallMessages = [];
// Check if this is hummanInput for tool calls
const _humanInput = nodeData.inputs?.humanInput;
const humanInput = typeof _humanInput === 'string' ? JSON.parse(_humanInput) : _humanInput;
const humanInputAction = options.humanInputAction;
const iterationContext = options.iterationContext;
if (humanInput) {
if (humanInput.type !== 'proceed' && humanInput.type !== 'reject') {
throw new Error(`Invalid human input type. Expected 'proceed' or 'reject', but got '${humanInput.type}'`);
}
const result = await this.handleResumedToolCalls({
humanInput,
humanInputAction,
messages,
toolsInstance,
sseStreamer,
chatId,
input,
options,
abortController,
llmWithoutToolsBind,
isStreamable,
isLastNode,
iterationContext
});
response = result.response;
usedTools = result.usedTools;
sourceDocuments = result.sourceDocuments;
artifacts = result.artifacts;
additionalTokens = result.totalTokens;
isWaitingForHumanInput = result.isWaitingForHumanInput || false;
// Calculate which messages were added during tool calls
_toolCallMessages = messages.slice(messagesBeforeToolCalls.length);
// Stream additional data if this is the last node
if (isLastNode && sseStreamer) {
if (usedTools.length > 0) {
sseStreamer.streamUsedToolsEvent(chatId, (0, lodash_1.flatten)(usedTools));
}
if (sourceDocuments.length > 0) {
sseStreamer.streamSourceDocumentsEvent(chatId, (0, lodash_1.flatten)(sourceDocuments));
}
if (artifacts.length > 0) {
sseStreamer.streamArtifactsEvent(chatId, (0, lodash_1.flatten)(artifacts));
}
}
}
else {
if (isStreamable) {
response = await this.handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController);
}
else {
response = await llmNodeInstance.invoke(messages, { signal: abortController?.signal });
}
}
if (!humanInput && response.tool_calls && response.tool_calls.length > 0) {
const result = await this.handleToolCalls({
response,
messages,
toolsInstance,
sseStreamer,
chatId,
input,
options,
abortController,
llmNodeInstance,
isStreamable,
isLastNode,
iterationContext
});
response = result.response;
usedTools = result.usedTools;
sourceDocuments = result.sourceDocuments;
artifacts = result.artifacts;
additionalTokens = result.totalTokens;
isWaitingForHumanInput = result.isWaitingForHumanInput || false;
// Calculate which messages were added during tool calls
_toolCallMessages = messages.slice(messagesBeforeToolCalls.length);
// Stream additional data if this is the last node
if (isLastNode && sseStreamer) {
if (usedTools.length > 0) {
sseStreamer.streamUsedToolsEvent(chatId, (0, lodash_1.flatten)(usedTools));
}
if (sourceDocuments.length > 0) {
sseStreamer.streamSourceDocumentsEvent(chatId, (0, lodash_1.flatten)(sourceDocuments));
}
if (artifacts.length > 0) {
sseStreamer.streamArtifactsEvent(chatId, (0, lodash_1.flatten)(artifacts));
}
}
}
else if (!humanInput && !isStreamable && isLastNode && sseStreamer) {
// Stream whole response back to UI if not streaming and no tool calls
let responseContent = JSON.stringify(response, null, 2);
if (typeof response.content === 'string') {
responseContent = response.content;
}
sseStreamer.streamTokenEvent(chatId, responseContent);
}
// Calculate execution time
const endTime = Date.now();
const timeDelta = endTime - startTime;
// Update flow state if needed
let newState = { ...state };
if (_agentUpdateState && Array.isArray(_agentUpdateState) && _agentUpdateState.length > 0) {
newState = (0, utils_1.updateFlowState)(state, _agentUpdateState);
}
// Clean up empty inputs
for (const key in nodeData.inputs) {
if (nodeData.inputs[key] === '') {
delete nodeData.inputs[key];
}
}
// Prepare final response and output object
let finalResponse = '';
if (response.content && Array.isArray(response.content)) {
finalResponse = response.content.map((item) => item.text).join('\n');
}
else if (response.content && typeof response.content === 'string') {
finalResponse = response.content;
}
else {
finalResponse = JSON.stringify(response, null, 2);
}
const output = this.prepareOutputObject(response, availableTools, finalResponse, startTime, endTime, timeDelta, usedTools, sourceDocuments, artifacts, additionalTokens, isWaitingForHumanInput);
// 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) {
if (newState[key].toString().includes('{{ output }}')) {
newState[key] = finalResponse;
}
}
}
// Replace the actual messages array with one that includes the file references for images instead of base64 data
const messagesWithFileReferences = (0, utils_1.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') {
if (!enableMemory) {
if (!agentMessages.some((msg) => msg.role === 'user')) {
inputMessages.push({ role: 'user', content: input });
}
else {
agentMessages.map((msg) => {
if (msg.role === 'user') {
inputMessages.push({ role: 'user', content: msg.content });
}
});
}
}
else {
inputMessages.push({ role: 'user', content: input });
}
}
}
const returnResponseAs = nodeData.inputs?.agentReturnResponseAs;
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,
// Add the messages that were specifically added during tool calls, this enable other nodes to see the full tool call history, temporaraily disabled
// ...toolCallMessages,
// End with the final assistant 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 Agent node: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Handles memory management based on the specified memory type
*/
async handleMemory({ messages, memoryType, pastChatHistory, runtimeChatHistory, llmNodeInstance, nodeData, userMessage, input, abortController, options, modelConfig, runtimeImageMessagesWithFileRef, pastImageMessagesWithFileRef }) {
const { updatedPastMessages, transformedPastMessages } = await (0, utils_1.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 (0, utils_1.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 (0, utils_1.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?.agentMemoryWindowSize;
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: prompt_1.DEFAULT_SUMMARIZER_TEMPLATE.replace('{conversation}', pastMessages.map((msg) => `${msg.role}: ${msg.content}`).join('\n'))
}
], { signal: abortController?.signal });
messages.push({ role: 'assistant', content: summary.content });
}
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
*/
async handleSummaryBuffer(messages, pastMessages, llmNodeInstance, nodeData, abortController) {
const maxTokenLimit = nodeData.inputs?.agentMemoryMaxTokenLimit || 2000;
// Convert past messages to a format suitable for token counting
const messagesString = pastMessages.map((msg) => `${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) => `${msg.role}: ${msg.content}`).join('\n');
currBufferLength = await llmNodeInstance.getNumTokens(remainingMessagesString);
}
}
// Summarize the messages that were removed
const messagesToSummarizeString = messagesToSummarize.map((msg) => `${msg.role}: ${msg.content}`).join('\n');
const summary = await llmNodeInstance.invoke([
{
role: 'user',
content: prompt_1.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);
}
}
/**
* Handles streaming response from the LLM
*/
async handleStreamingResponse(sseStreamer, llmNodeInstance, messages, chatId, abortController) {
let response = new messages_1.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;
content = contents.map((item) => item.text).join('');
}
else {
content = chunk.content.toString();
}
sseStreamer.streamTokenEvent(chatId, content);
}
response = response.concat(chunk);
}
}
catch (error)