n8n-nodes-sap-ai-core
Version:
n8n nodes for SAP AI Core LLM and embeddings integration
475 lines • 17.5 kB
JavaScript
;
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