n8n/packages/@n8n/nodes-langchain/utils/helpers.ts

224 lines
6.6 KiB
TypeScript

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<T>(obj: unknown, ...methodNames: Array<string | symbol>): obj is T {
return methodNames.every(
(methodName) =>
typeof obj === 'object' &&
obj !== null &&
methodName in obj &&
typeof (obj as Record<string | symbol, unknown>)[methodName] === 'function',
);
}
export function getMetadataFiltersValues(
ctx: IExecuteFunctions | ISupplyDataFunctions,
itemIndex: number,
): Record<string, never> | 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<string, never>;
}
return undefined;
}
export function isBaseChatMemory(obj: unknown) {
return hasMethods<BaseChatMemory>(obj, 'loadMemoryVariables', 'saveContext');
}
export function isBaseChatMessageHistory(obj: unknown) {
return hasMethods<BaseChatMessageHistory>(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(/(?<!{){{{(?!{)/g, '{{{{')
.replace(/(?<!})}}}(?!})/g, '}}}}')
// Then handle single brackets, but only if they're not part of double brackets
// Convert single { to {{ if it's not already part of {{ or {{{
.replace(/(?<!{){(?!{)/g, '{{')
// Convert single } to }} if it's not already part of }} or }}}
.replace(/(?<!})}(?!})/g, '}}');
return result;
}
export const getConnectedTools = async (
ctx: IExecuteFunctions,
enforceUniqueNames: boolean,
convertStructuredTool: boolean = true,
escapeCurlyBrackets: boolean = false,
) => {
const connectedTools =
((await ctx.getInputConnectionData(NodeConnectionType.AiTool, 0)) as Tool[]) || [];
if (!enforceUniqueNames) return connectedTools;
const seenNames = new Set<string>();
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;
};