UNPKG

n8n-nodes-velatir

Version:

n8n community node for Velatir - Human-in-the-loop AI function approval

333 lines (332 loc) 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VelatirV2 = void 0; const n8n_workflow_1 = require("n8n-workflow"); const versionDescription = { displayName: 'Velatir', name: 'velatir', icon: 'file:velatir.svg', group: ['transform'], version: 2, subtitle: 'Send and Wait for Response', description: 'AI compliance and approval workflows', defaults: { name: 'Velatir', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [ { name: 'velatirApi', required: true, }, ], webhooks: [ { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', responseData: '', path: '={{ $nodeId }}', restartWebhook: true, isFullPath: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Send and Wait for Response', value: 'sendAndWait', action: 'Send trace and wait for response', description: 'Create a trace for assessment and wait for the result', }, ], default: 'sendAndWait', }, { displayName: 'Tool Calls', name: 'toolCalls', type: 'string', default: '', description: 'Comma-separated list of tool names being called', placeholder: 'gmail_send, hubspot_update_contact', }, { displayName: 'Arguments', name: 'args', type: 'json', default: '={{ $json }}', description: 'Data to assess (JSON object)', }, { displayName: 'Session ID', name: 'sessionId', type: 'string', default: '', description: 'ID to group related traces together. Defaults to n8n execution ID if not provided.', placeholder: 'session_abc123', }, { displayName: 'Direction', name: 'direction', type: 'options', options: [ { name: 'Inlet', value: 'inlet', description: 'Incoming request/data', }, { name: 'Response', value: 'response', description: 'Outgoing response', }, { name: 'Signal', value: 'signal', description: 'Internal signal/event', }, ], default: 'inlet', description: 'Direction of the trace', }, { displayName: 'Title', name: 'title', type: 'string', default: '', description: 'Custom title for the trace', placeholder: 'Customer onboarding approval', }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, options: [ { displayName: 'Metadata', name: 'metadata', type: 'json', default: '{}', description: 'Additional metadata to include', }, { displayName: 'Timeout (Minutes)', name: 'timeoutMinutes', type: 'number', default: 20160, description: 'Maximum time to wait for response (default: 14 days)', typeOptions: { minValue: 1, }, }, ], }, ], }; class VelatirV2 { constructor(baseDescription) { this.webhook = async function () { var _a; const bodyData = this.getBodyData(); const state = ((_a = bodyData.state) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || 'approved'; // approved = true only for "approved" state // changerequested and rejected both mean not approved const approved = state === 'approved'; return { workflowData: [this.helpers.returnJsonArray({ approved, state, traceId: bodyData.traceId, reviewTaskId: bodyData.reviewTaskId, requestedChange: bodyData.requestedChange, })], }; }; this.description = { ...baseDescription, ...versionDescription, }; } async execute() { var _a; const items = this.getInputData(); const returnData = []; let requiresWaiting = false; // Check if we're resuming from a webhook callback // When resuming, input data will have approved field from the webhook response const firstItem = (_a = items[0]) === null || _a === void 0 ? void 0 : _a.json; if ((firstItem === null || firstItem === void 0 ? void 0 : firstItem.approved) !== undefined) { // This is a resume from webhook - just pass through the data return [items]; } // Default timeout: 14 days let waitMillis = 14 * 24 * 60 * 60 * 1000; for (let i = 0; i < items.length; i++) { try { const credentials = await this.getCredentials('velatirApi'); // Get parameters const toolCallsStr = this.getNodeParameter('toolCalls', i); const args = this.getNodeParameter('args', i); const sessionId = this.getNodeParameter('sessionId', i); const direction = this.getNodeParameter('direction', i); const title = this.getNodeParameter('title', i); const additionalFields = this.getNodeParameter('additionalFields', i); // Parse tool calls from comma-separated string const toolCalls = toolCallsStr ? toolCallsStr.split(',').map((s) => s.trim()).filter(Boolean) : undefined; // Parse user metadata let userMetadata = {}; if (additionalFields.metadata) { try { userMetadata = typeof additionalFields.metadata === 'string' ? JSON.parse(additionalFields.metadata) : additionalFields.metadata; } catch { // Ignore parse errors, use empty object } } // Build request body const requestBody = { functionName: this.getNode().name, args, metadata: { n8n: { workflowId: this.getWorkflow().id, executionId: this.getExecutionId(), nodeId: this.getNode().id, itemIndex: i, }, ...userMetadata, }, }; // Add optional fields only if provided if (toolCalls && toolCalls.length > 0) { requestBody.toolCalls = toolCalls; } // Use provided sessionId, or default to execution ID to group traces from same workflow run requestBody.sessionId = sessionId || this.getExecutionId(); if (direction && direction !== 'inlet') { requestBody.direction = direction; } if (title) { requestBody.title = title; } // Add webhook URL for callback (used if human intervention needed) const resumeUrl = this.evaluateExpression('{{ $execution?.resumeUrl }}', i); const nodeId = this.evaluateExpression('{{ $nodeId }}', i); requestBody.webhookUrl = `${resumeUrl}/${nodeId}`; // Set timeout from additional fields if (additionalFields.timeoutMinutes) { waitMillis = additionalFields.timeoutMinutes * 60 * 1000; } // Create the trace const response = await this.helpers.httpRequest({ method: 'POST', url: `${credentials.domain}/api/v1/trace`, body: requestBody, json: true, headers: { 'X-API-Key': credentials.apiKey, 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); // Validate response if (!response || !response.traceId) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid API response: ${JSON.stringify(response)}`, { itemIndex: i }); } const { traceId, status, processedAsync, reviewTaskId } = response; const normalizedStatus = status === null || status === void 0 ? void 0 : status.toLowerCase(); // Case 1: No workflows configured - queued for async assessment, proceed immediately if (processedAsync === true) { returnData.push({ json: { approved: true, state: 'approved', traceId, processedAsync: true, }, pairedItem: { item: i }, }); continue; } // Case 2: Completed - approved by workflow if (normalizedStatus === 'completed') { returnData.push({ json: { approved: true, state: 'approved', traceId, }, pairedItem: { item: i }, }); continue; } // Case 3: Rejected - blocked by workflow if (normalizedStatus === 'rejected') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Request was rejected by policy', { itemIndex: i }); } // Case 4: Assessed with reviewTaskId - requires human intervention, wait for webhook if (normalizedStatus === 'assessed' && reviewTaskId) { requiresWaiting = true; returnData.push({ json: { state: 'pending', traceId, reviewTaskId, }, pairedItem: { item: i }, }); continue; } // Fallback: Unknown state, treat as needing to wait if reviewTaskId present if (reviewTaskId) { requiresWaiting = true; } const fallbackApproved = !reviewTaskId; returnData.push({ json: { approved: fallbackApproved, state: reviewTaskId ? 'pending' : 'approved', traceId, reviewTaskId, }, pairedItem: { item: i }, }); } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { approved: false, state: 'error', error: error instanceof Error ? error.message : 'Unknown error', }, pairedItem: { item: i }, }); continue; } throw error; } } // If any item requires waiting for human intervention, pause execution if (requiresWaiting) { const waitTill = new Date(Date.now() + waitMillis); await this.putExecutionToWait(waitTill); return [this.getInputData()]; } // All items resolved immediately, return results return [returnData]; } } exports.VelatirV2 = VelatirV2;