import type { BaseChatMessageHistory } from '@langchain/core/chat_history'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseLLM } from '@langchain/core/language_models/llms'; import type { BaseMessage } from '@langchain/core/messages'; import type { Tool } from '@langchain/core/tools'; import type { BaseChatMemory } from 'langchain/memory'; import { NodeConnectionType, NodeOperationError, jsonStringify } from 'n8n-workflow'; import type { AiEvent, IDataObject, IExecuteFunctions, ISupplyDataFunctions, IWebhookFunctions, } from 'n8n-workflow'; import { N8nTool } from './N8nTool'; function hasMethods(obj: unknown, ...methodNames: Array): obj is T { return methodNames.every( (methodName) => typeof obj === 'object' && obj !== null && methodName in obj && typeof (obj as Record)[methodName] === 'function', ); } export function getMetadataFiltersValues( ctx: IExecuteFunctions | ISupplyDataFunctions, itemIndex: number, ): Record | undefined { const options = ctx.getNodeParameter('options', itemIndex, {}); if (options.metadata) { const { metadataValues: metadata } = options.metadata as { metadataValues: Array<{ name: string; value: string; }>; }; if (metadata.length > 0) { return metadata.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}); } } if (options.searchFilterJson) { return ctx.getNodeParameter('options.searchFilterJson', itemIndex, '', { ensureType: 'object', }) as Record; } return undefined; } export function isBaseChatMemory(obj: unknown) { return hasMethods(obj, 'loadMemoryVariables', 'saveContext'); } export function isBaseChatMessageHistory(obj: unknown) { return hasMethods(obj, 'getMessages', 'addMessage'); } export function isChatInstance(model: unknown): model is BaseChatModel { const namespace = (model as BaseLLM)?.lc_namespace ?? []; return namespace.includes('chat_models'); } export function isToolsInstance(model: unknown): model is Tool { const namespace = (model as Tool)?.lc_namespace ?? []; return namespace.includes('tools'); } export function getPromptInputByType(options: { ctx: IExecuteFunctions; i: number; promptTypeKey: string; inputKey: string; }) { const { ctx, i, promptTypeKey, inputKey } = options; const prompt = ctx.getNodeParameter(promptTypeKey, i) as string; let input; if (prompt === 'auto') { input = ctx.evaluateExpression('{{ $json["chatInput"] }}', i) as string; } else { input = ctx.getNodeParameter(inputKey, i) as string; } if (input === undefined) { throw new NodeOperationError(ctx.getNode(), 'No prompt specified', { description: "Expected to find the prompt in an input field called 'chatInput' (this is what the chat trigger node outputs). To use something else, change the 'Prompt' parameter", }); } return input; } export function getSessionId( ctx: ISupplyDataFunctions | IWebhookFunctions, itemIndex: number, selectorKey = 'sessionIdType', autoSelect = 'fromInput', customKey = 'sessionKey', ) { let sessionId = ''; const selectorType = ctx.getNodeParameter(selectorKey, itemIndex) as string; if (selectorType === autoSelect) { // If memory node is used in webhook like node(like chat trigger node), it doesn't have access to evaluateExpression // so we try to extract sessionId from the bodyData if ('getBodyData' in ctx) { const bodyData = ctx.getBodyData() ?? {}; sessionId = bodyData.sessionId as string; } else { sessionId = ctx.evaluateExpression('{{ $json.sessionId }}', itemIndex) as string; } if (sessionId === '' || sessionId === undefined) { throw new NodeOperationError(ctx.getNode(), 'No session ID found', { description: "Expected to find the session ID in an input field called 'sessionId' (this is what the chat trigger node outputs). To use something else, change the 'Session ID' parameter", itemIndex, }); } } else { sessionId = ctx.getNodeParameter(customKey, itemIndex, '') as string; if (sessionId === '' || sessionId === undefined) { throw new NodeOperationError(ctx.getNode(), 'Key parameter is empty', { description: "Provide a key to use as session ID in the 'Key' parameter or use the 'Connected Chat Trigger Node' option to use the session ID from your Chat Trigger", itemIndex, }); } } return sessionId; } export function logAiEvent( executeFunctions: IExecuteFunctions | ISupplyDataFunctions, event: AiEvent, data?: IDataObject, ) { try { executeFunctions.logAiEvent(event, data ? jsonStringify(data) : undefined); } catch (error) { executeFunctions.logger.debug(`Error logging AI event: ${event}`); } } export function serializeChatHistory(chatHistory: BaseMessage[]): string { return chatHistory .map((chatMessage) => { if (chatMessage._getType() === 'human') { return `Human: ${chatMessage.content}`; } else if (chatMessage._getType() === 'ai') { return `Assistant: ${chatMessage.content}`; } else { return `${chatMessage.content}`; } }) .join('\n'); } export function escapeSingleCurlyBrackets(text?: string): string | undefined { if (text === undefined) return undefined; let result = text; result = result // First handle triple brackets to avoid interference with double brackets .replace(/(? { const connectedTools = ((await ctx.getInputConnectionData(NodeConnectionType.AiTool, 0)) as Tool[]) || []; if (!enforceUniqueNames) return connectedTools; const seenNames = new Set(); const finalTools = []; for (const tool of connectedTools) { const { name } = tool; if (seenNames.has(name)) { throw new NodeOperationError( ctx.getNode(), `You have multiple tools with the same name: '${name}', please rename them to avoid conflicts`, ); } seenNames.add(name); if (escapeCurlyBrackets) { tool.description = escapeSingleCurlyBrackets(tool.description) ?? tool.description; } if (convertStructuredTool && tool instanceof N8nTool) { finalTools.push(tool.asDynamicTool()); } else { finalTools.push(tool); } } return finalTools; };