UNPKG

n8n-nodes-sap-ai-core

Version:

n8n nodes for SAP AI Core LLM and embeddings integration

475 lines 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getConnectedTools = void 0; exports.getMetadataFiltersValues = getMetadataFiltersValues; exports.isBaseChatMemory = isBaseChatMemory; exports.isBaseChatMessageHistory = isBaseChatMessageHistory; exports.isChatInstance = isChatInstance; exports.isToolsInstance = isToolsInstance; exports.getPromptInputByType = getPromptInputByType; exports.getSessionId = getSessionId; exports.logAiEvent = logAiEvent; exports.serializeChatHistory = serializeChatHistory; exports.escapeSingleCurlyBrackets = escapeSingleCurlyBrackets; exports.unwrapNestedOutput = unwrapNestedOutput; exports.hasLongSequentialRepeat = hasLongSequentialRepeat; exports.validateTool = validateTool; exports.getToolInfo = getToolInfo; exports.createSafeToolWrapper = createSafeToolWrapper; exports.filterToolsByCapability = filterToolsByCapability; exports.sortToolsByPriority = sortToolsByPriority; exports.getToolStatistics = getToolStatistics; const agents_1 = require("langchain/agents"); const n8n_workflow_1 = require("n8n-workflow"); const N8nTool_1 = require("./N8nTool"); /** * Check if an object has specific methods */ function hasMethods(obj, ...methodNames) { return methodNames.every((methodName) => typeof obj === "object" && obj !== null && methodName in obj && typeof obj[methodName] === "function"); } /** * Get metadata filters values from node parameters */ function getMetadataFiltersValues(ctx, itemIndex) { const options = ctx.getNodeParameter("options", itemIndex, {}); if (options.metadata && typeof options.metadata === 'object') { const metadata = options.metadata; const metadataValues = metadata.metadataValues; if (metadataValues && Array.isArray(metadataValues) && metadataValues.length > 0) { return metadataValues.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}); } } if (options.searchFilterJson) { return ctx.getNodeParameter("options.searchFilterJson", itemIndex, "", { ensureType: "object" }); } return undefined; } /** * Check if object is a base chat memory instance */ function isBaseChatMemory(obj) { return hasMethods(obj, "loadMemoryVariables", "saveContext"); } /** * Check if object is a base chat message history instance */ function isBaseChatMessageHistory(obj) { return hasMethods(obj, "getMessages", "addMessage"); } /** * Check if model is a chat instance */ function isChatInstance(model) { var _a; const namespace = (_a = model === null || model === void 0 ? void 0 : model.lc_namespace) !== null && _a !== void 0 ? _a : []; return namespace.includes("chat_models"); } /** * Check if model is a tools instance */ function isToolsInstance(model) { var _a; const namespace = (_a = model === null || model === void 0 ? void 0 : model.lc_namespace) !== null && _a !== void 0 ? _a : []; return namespace.includes("tools"); } /** * Get prompt input based on type configuration */ function getPromptInputByType(options) { const { ctx, i, promptTypeKey, inputKey } = options; const promptType = ctx.getNodeParameter(promptTypeKey, i, "define"); let input; if (promptType === "auto") { input = ctx.evaluateExpression('{{ $json["chatInput"] }}', i); } else { input = ctx.getNodeParameter(inputKey, i); } if (input === undefined) { throw new n8n_workflow_1.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; } /** * Get session ID from various sources */ function getSessionId(ctx, itemIndex, selectorKey = "sessionIdType", autoSelect = "fromInput", customKey = "sessionKey") { var _a; let sessionId = ""; const selectorType = ctx.getNodeParameter(selectorKey, itemIndex); if (selectorType === autoSelect) { if ("getBodyData" in ctx) { const bodyData = (_a = ctx.getBodyData()) !== null && _a !== void 0 ? _a : {}; sessionId = String(bodyData.sessionId || ""); } else { const evaluatedValue = ctx.evaluateExpression("{{ $json.sessionId }}", itemIndex); sessionId = String(evaluatedValue || ""); } if (sessionId === "" || sessionId === undefined) { throw new n8n_workflow_1.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 { const sessionKeyValue = ctx.getNodeParameter(customKey, itemIndex, ""); sessionId = sessionKeyValue ? String(sessionKeyValue) : ""; if (sessionId === "" || sessionId === "undefined") { throw new n8n_workflow_1.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; } /** * Log AI events with proper error handling */ function logAiEvent(executeFunctions, event, data) { try { if ('logAiEvent' in executeFunctions && typeof executeFunctions.logAiEvent === 'function') { // Type assertion for the logAiEvent method with proper data handling const logData = data ? (0, n8n_workflow_1.jsonStringify)(data) : undefined; executeFunctions.logAiEvent(event, logData); } else { // Fallback logging console.log(`[AI Event] ${event}:`, data); } } catch (error) { if (executeFunctions.logger) { executeFunctions.logger.debug(`Error logging AI event: ${event}`, { error, data }); } else { console.debug(`Error logging AI event: ${event}`, error); } } } /** * Serialize chat history to string format */ function serializeChatHistory(chatHistory) { 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"); } /** * Escape single curly brackets for template safety */ function escapeSingleCurlyBrackets(text) { if (text === undefined) return undefined; let result = text; result = result .replace(/(?<!{){{{(?!{)/g, "{{{{") .replace(/(?<!})}}}(?!})/g, "}}}}") .replace(/(?<!{){(?!{)/g, "{{") .replace(/(?<!})}(?!})/g, "}}"); return result; } /** * Enhanced getConnectedTools function with comprehensive tool discovery and validation * Based on OpenAI Assistant implementation with SAP AI Core adaptations */ const getConnectedTools = async (ctx, enforceUniqueNames, convertStructuredTool = true, escapeCurlyBrackets = false) => { var _a; try { // Get connected tools from the canvas const rawConnectedTools = await ctx.getInputConnectionData("ai_tool" /* NodeConnectionType.AiTool */, 0); // Handle null/undefined case if (!rawConnectedTools) { return []; } // Ensure it's an array and expand toolkits const toolsArray = Array.isArray(rawConnectedTools) ? rawConnectedTools : [rawConnectedTools]; const connectedTools = toolsArray.flatMap((toolOrToolkit) => { if (toolOrToolkit instanceof agents_1.Toolkit) { return toolOrToolkit.getTools(); } return toolOrToolkit; }); // If not enforcing unique names, return as-is if (!enforceUniqueNames) { return connectedTools; } // Validate and process tools const seenNames = new Set(); const finalTools = []; for (const tool of connectedTools) { try { // Validate tool has required properties if (!tool || typeof tool !== 'object') { console.warn('Skipping invalid tool:', tool); continue; } const { name } = tool; if (!name || typeof name !== 'string') { console.warn('Skipping tool without valid name:', tool); continue; } // Check for duplicate names if (seenNames.has(name)) { throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `You have multiple tools with the same name: '${name}', please rename them to avoid conflicts`); } seenNames.add(name); // Escape curly brackets in description if requested if (escapeCurlyBrackets && tool.description) { tool.description = (_a = escapeSingleCurlyBrackets(tool.description)) !== null && _a !== void 0 ? _a : tool.description; } // Convert N8nTool to DynamicTool if requested if (convertStructuredTool && tool instanceof N8nTool_1.N8nTool) { finalTools.push(tool.asDynamicTool()); } else { finalTools.push(tool); } } catch (toolError) { // Log tool processing error but continue with other tools console.error(`Error processing tool ${(tool === null || tool === void 0 ? void 0 : tool.name) || 'unknown'}:`, toolError); logAiEvent(ctx, 'tool-processing-error', { toolName: tool === null || tool === void 0 ? void 0 : tool.name, error: toolError instanceof Error ? toolError.message : String(toolError) }); // Re-throw if it's a duplicate name error (blocking) if (toolError instanceof n8n_workflow_1.NodeOperationError && toolError.message.includes('multiple tools with the same name')) { throw toolError; } } } // Log successful tool discovery logAiEvent(ctx, 'tools-connected', { toolCount: finalTools.length, toolNames: finalTools.map(t => t.name).filter(Boolean) }); return finalTools; } catch (error) { // Log connection error logAiEvent(ctx, 'tool-connection-error', { error: error instanceof Error ? error.message : String(error) }); // Re-throw NodeOperationErrors if (error instanceof n8n_workflow_1.NodeOperationError) { throw error; } // Wrap other errors throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Failed to get connected tools: ${error instanceof Error ? error.message : String(error)}`); } }; exports.getConnectedTools = getConnectedTools; /** * Unwrap nested output structures */ function unwrapNestedOutput(output) { if ("output" in output && Object.keys(output).length === 1 && typeof output.output === "object" && output.output !== null && "output" in output.output && Object.keys(output.output).length === 1) { return output.output; } return output; } /** * Check for long sequential character repeats (potential infinite loops) */ function hasLongSequentialRepeat(text, threshold = 1000) { try { if (text === null || typeof text !== "string" || text.length === 0 || threshold <= 0 || text.length < threshold) { return false; } const iterator = text[Symbol.iterator](); let prev = iterator.next(); if (prev.done) { return false; } let count = 1; for (const char of iterator) { if (char === prev.value) { count++; if (count >= threshold) { return true; } } else { count = 1; prev = { value: char, done: false }; } } return false; } catch (error) { return false; } } /** * Validate tool configuration */ function validateTool(tool) { const errors = []; if (!tool || typeof tool !== 'object') { errors.push('Tool must be an object'); return { isValid: false, errors }; } if (!tool.name || typeof tool.name !== 'string') { errors.push('Tool must have a valid name'); } if (!tool.description || typeof tool.description !== 'string') { errors.push('Tool must have a valid description'); } if (!tool.func && !tool._call && typeof tool.func !== 'function' && typeof tool._call !== 'function') { errors.push('Tool must have a callable function (func or _call method)'); } return { isValid: errors.length === 0, errors }; } /** * Get tool information for debugging */ function getToolInfo(tool) { var _a; if (!tool || typeof tool !== 'object') { return { error: 'Invalid tool' }; } return { name: tool.name, description: tool.description, hasFunc: typeof tool.func === 'function', hasCall: typeof tool._call === 'function', hasSchema: !!tool.schema, namespace: tool.lc_namespace, type: (_a = tool.constructor) === null || _a === void 0 ? void 0 : _a.name, metadata: tool.metadata }; } /** * Create a safe wrapper for tool execution */ function createSafeToolWrapper(tool, ctx) { const originalFunc = tool.func || tool._call; if (!originalFunc) { throw new Error(`Tool ${tool.name} has no executable function`); } const safeFunc = async (input) => { const startTime = Date.now(); try { // Log tool execution start logAiEvent(ctx, 'tool-execution-start', { toolName: tool.name, input: typeof input === 'string' ? input.substring(0, 200) : 'object' }); // Execute the tool const result = await originalFunc.call(tool, input); // Log successful execution const duration = Date.now() - startTime; logAiEvent(ctx, 'tool-execution-success', { toolName: tool.name, duration, resultType: typeof result }); return result; } catch (error) { // Log execution error const duration = Date.now() - startTime; logAiEvent(ctx, 'tool-execution-error', { toolName: tool.name, duration, error: error instanceof Error ? error.message : String(error) }); throw error; } }; return { ...tool, func: safeFunc, _call: safeFunc }; } /** * Filter tools by capabilities */ function filterToolsByCapability(tools, capability) { return tools.filter(tool => { if (tool.capabilities && Array.isArray(tool.capabilities)) { return tool.capabilities.includes(capability); } if (tool.metadata && tool.metadata.capabilities) { return tool.metadata.capabilities.includes(capability); } // Check if tool name or description contains capability const toolText = `${tool.name} ${tool.description}`.toLowerCase(); return toolText.includes(capability.toLowerCase()); }); } /** * Sort tools by priority */ function sortToolsByPriority(tools) { return [...tools].sort((a, b) => { var _a, _b; const aPriority = a.priority || ((_a = a.metadata) === null || _a === void 0 ? void 0 : _a.priority) || 0; const bPriority = b.priority || ((_b = b.metadata) === null || _b === void 0 ? void 0 : _b.priority) || 0; // Higher priority first return bPriority - aPriority; }); } /** * Get tool statistics */ function getToolStatistics(tools) { const stats = { total: tools.length, byType: {}, byNamespace: {}, withSchema: 0, withMetadata: 0 }; tools.forEach(tool => { var _a, _b; // Count by type const type = ((_a = tool.constructor) === null || _a === void 0 ? void 0 : _a.name) || 'Unknown'; stats.byType[type] = (stats.byType[type] || 0) + 1; // Count by namespace const namespace = ((_b = tool.lc_namespace) === null || _b === void 0 ? void 0 : _b[0]) || 'Unknown'; stats.byNamespace[namespace] = (stats.byNamespace[namespace] || 0) + 1; // Count tools with schema if (tool.schema) { stats.withSchema++; } // Count tools with metadata if (tool.metadata) { stats.withMetadata++; } }); return stats; } //# sourceMappingURL=helpers.js.map