UNPKG

@gotohuman/n8n-nodes-gotohuman

Version:

n8n node to request human reviews in AI workflows with gotoHuman

851 lines 40.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GotoHuman = void 0; const n8n_workflow_1 = require("n8n-workflow"); const helpers_1 = require("./helpers"); class GotoHuman { constructor() { this.description = { displayName: 'gotoHuman', name: 'gotoHuman', icon: 'file:gotohuman.svg', group: ['transform'], version: [1, 2], features: { sendRawReviewData: { '@version': [{ _cnd: { gte: 2 } }] }, }, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Request human reviews with gotoHuman', defaults: { name: 'gotoHuman', }, usableAsTool: true, inputs: [n8n_workflow_1.NodeConnectionTypes.Main], outputs: [n8n_workflow_1.NodeConnectionTypes.Main], credentials: [ { name: 'gotoHumanApi', required: true, }, ], webhooks: [ { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', responseData: '', path: '={{ $nodeId }}', restartWebhook: true, isFullPath: true, } ], properties: [ { displayName: 'Resource', name: 'resource', type: 'options', noDataExpression: true, options: [ { name: 'Review Request', value: 'reviewRequest', description: 'Request for Human Review', }, { name: 'Message', value: 'message', description: 'Send a message to a human', }, ], default: 'reviewRequest', required: true, }, { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Send', value: 'send', action: 'Send review request', description: 'Request a human review', }, { name: 'Send and Wait for Response', value: 'sendAndWait', action: 'Send review request and wait for response', description: 'Request a human review and wait for the response', }, { name: 'Delete', value: 'delete', action: 'Delete review request', description: 'Delete an existing review request', }, ], default: 'sendAndWait', displayOptions: { show: { resource: [ 'reviewRequest', ], }, }, }, { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Send Message to Human', value: 'sendMessage', action: 'Send message to human', description: 'Send a message to a human', }, ], default: 'sendMessage', displayOptions: { show: { resource: ['message'], }, }, }, { displayName: 'Agent', name: 'agentID', type: 'resourceLocator', description: 'Choose the agent you are acting as from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', default: { mode: 'list', value: '' }, modes: [ { displayName: 'From List', name: 'list', type: 'list', placeholder: 'Select the agent you are requesting as...', typeOptions: { searchListMethod: 'searchAgents', searchable: true, }, }, { displayName: 'ID', name: 'id', type: 'string', hint: 'Enter an ID', placeholder: 'e.g. Y5XrpfmvpKcdJzKYLX2M', } ], displayOptions: { show: { resource: [ 'reviewRequest', ], operation: [ 'send', 'sendAndWait', ], }, }, }, { displayName: 'Agent', name: 'agentID', type: 'resourceLocator', required: true, description: 'Choose the agent you are messaging as from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', default: { mode: 'list', value: '' }, modes: [ { displayName: 'From List', name: 'list', type: 'list', placeholder: 'Select the agent you are messaging as...', typeOptions: { searchListMethod: 'searchAgents', searchable: true, }, }, { displayName: 'ID', name: 'id', type: 'string', hint: 'Enter an ID', placeholder: 'e.g. Y5XrpfmvpKcdJzKYLX2M', } ], displayOptions: { show: { resource: ['message'], operation: ['sendMessage'], }, }, }, { displayName: 'Session ID', name: 'sessionId', type: 'string', default: '', description: 'The session ID to associate the message with', displayOptions: { show: { resource: ['message'], operation: ['sendMessage'], }, }, }, { displayName: 'Message', name: 'message', type: 'string', required: true, default: '', description: 'The message to send to the human', displayOptions: { show: { resource: ['message'], operation: ['sendMessage'], }, }, }, { displayName: 'Review Template', name: 'reviewTemplateID', type: 'resourceLocator', required: true, description: 'Choose a review template from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', default: { mode: 'list', value: '' }, modes: [ { displayName: 'From List', name: 'list', type: 'list', placeholder: 'Select a Review Template...', typeOptions: { searchListMethod: 'searchReviewTemplates', searchable: true, }, }, { displayName: 'ID', name: 'id', type: 'string', hint: 'Enter an ID', placeholder: 'e.g. FjbxGtNfPIuDdRm55eqK', } ], displayOptions: { show: { resource: [ 'reviewRequest', ], operation: [ 'send', 'sendAndWait', ], }, }, }, { displayName: 'Fields', name: 'fields', type: 'resourceMapper', noDataExpression: true, default: { mappingMode: 'defineBelow', value: null, }, required: true, typeOptions: { loadOptionsDependsOn: ['reviewTemplateID.value'], resourceMapper: { resourceMapperMethod: 'getMappingFields', mode: 'add', valuesLabel: 'Fields', fieldWords: { singular: 'field', plural: 'fields', }, supportAutoMap: false, }, }, displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], '@feature': [{ _cnd: { not: 'sendRawReviewData' } }], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Review Data', name: 'reviewData', type: 'json', required: true, default: '', placeholder: 'e.g. {"property1": "value1"}', description: 'The data to populate the review template', displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], '@feature': ['sendRawReviewData'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Review Config', name: 'reviewConfig', type: 'json', default: '{}', placeholder: 'e.g. {"fields": { "myField1": { ... } }}', description: "Can optionally be used to dynamically configure your review and its fields", displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], '@feature': ['sendRawReviewData'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Session ID', name: 'sessionId', type: 'string', default: '', description: 'Send to connect various events and reviews of the same agent session/execution', displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], }, }, }, { displayName: 'Meta Data', name: 'metaSelect', description: 'Select if you want to add meta data that you want to receive back in the response webhook', required: true, type: 'options', options: [ { name: 'No Meta Data', value: 'no', }, { name: 'Add as JSON', value: 'json', }, { name: 'Add as Key-Value Pairs', value: 'keyValue', }, ], default: 'no', displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Meta Data JSON', name: 'metaJson', type: 'json', default: '', placeholder: 'e.g. {"myKey": "myValue"}', displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], metaSelect: ['json'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Meta Data Values', name: 'metaKeyValues', type: 'fixedCollection', placeholder: 'Add Key-Value Pair', default: {}, typeOptions: { multipleValues: true, }, options: [ { name: 'metaArray', displayName: 'Metadata', values: [ { displayName: 'Key', name: 'key', type: 'string', placeholder: 'myKey', default: '', description: 'Name of the metadata key to add', }, { displayName: 'Value', name: 'value', type: 'string', placeholder: 'myValue', default: '', description: 'Value to set for the metadata key', }, ], }, ], displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], metaSelect: ['keyValue'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Assigned Users', name: 'assignToSelect', description: 'Change if you want to assign the review to a specific user', required: true, type: 'options', options: [ { name: 'Default / All', value: 'all', }, { name: 'Only Selected Users', value: 'selectByEmail', }, ], default: 'all', displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Selected Users', name: 'assignTo', description: 'List the email addresses of the users you want to assign the review to', type: 'fixedCollection', placeholder: 'Add User', typeOptions: { multipleValues: true, }, default: [], options: [ { displayName: 'Values', name: 'values', values: [ { displayName: 'Email Address', name: 'email', description: 'The email address the user used when signing up for gotoHuman', type: 'string', required: true, placeholder: 'e.g. nathan@example.com', default: '', hint: 'Only emails from registered users will be accepted', }, ], }, ], displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], assignToSelect: ['selectByEmail'], }, hide: { reviewTemplateID: [''] }, }, }, { displayName: 'Review ID', name: 'reviewId', type: 'string', required: true, default: '', placeholder: 'e.g. 123456', description: 'The ID of the review request to delete', displayOptions: { show: { resource: ['reviewRequest'], operation: ['delete'], }, }, }, { displayName: 'Additional Fields', name: 'additionalFields', type: 'collection', placeholder: 'Add Field', default: {}, displayOptions: { show: { resource: ['reviewRequest'], operation: ['send', 'sendAndWait'], }, hide: { reviewTemplateID: [''] }, }, options: [ { displayName: 'Title', name: 'title', type: 'string', default: '', description: 'Set a title for this review request. Read more about it <a href="https://docs.gotohuman.com/send-requests">here</a>.', }, { displayName: 'Auto Approve', name: 'autoApprove', type: 'boolean', default: false, description: 'Whether to automatically approve this request. Read more about it <a href="https://docs.gotohuman.com/send-requests">here</a>.', }, { displayName: 'Workflow Info (Legacy)', name: 'workflow', type: 'json', default: '', placeholder: '{"runId": "123456", "runName": "My Workflow", "prevSteps": ["1234567890"]}', description: 'Send to connect multiple review steps. Read more about it <a href="https://docs.gotohuman.com/send-requests#workflow-metadata">here</a>.', }, { displayName: 'Update for Review ID (Legacy)', name: 'updateForReviewId', type: 'string', default: '', description: 'To update a specific review, enter the review ID here. Read more about it <a href="https://docs.gotohuman.com/retries">here</a>.', }, ], } ] }; this.methods = { listSearch: { async searchAgents(filter) { const options = { method: 'GET', url: `${helpers_1.BASE_URL}/fetchN8nAgents`, json: true, }; const agentsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options); if (agentsResponse === undefined) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No agents found. Please create one first in our web app.'); } return { results: ((agentsResponse === null || agentsResponse === void 0 ? void 0 : agentsResponse.agents) || []) .filter((agent) => !filter || agent.label.toLowerCase().includes(filter.toLowerCase())) .map((agent) => ({ name: agent.label, value: agent.value, })), }; }, async searchReviewTemplates(filter) { const options = { method: 'GET', url: `${helpers_1.BASE_URL}/fetchN8nForms`, json: true, }; const reviewTemplates = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options); if (reviewTemplates === undefined) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No review templates found. Please create one first in our web app.'); } return { results: ((reviewTemplates === null || reviewTemplates === void 0 ? void 0 : reviewTemplates.forms) || []) .filter((template) => !filter || template.label.toLowerCase().includes(filter.toLowerCase())) .map((template) => ({ name: template.label, value: template.value, })), }; }, }, resourceMapping: { async getMappingFields() { const reviewTemplateObj = this.getNodeParameter('reviewTemplateID', 0); if (!reviewTemplateObj) return { fields: [] }; const { value: reviewTemplateID } = reviewTemplateObj; const options = { method: 'GET', url: `${helpers_1.BASE_URL}/fetchN8nFields`, qs: { formId: reviewTemplateID, }, json: true, }; const templateFieldsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options); if ((templateFieldsResponse === null || templateFieldsResponse === void 0 ? void 0 : templateFieldsResponse.fields) === undefined) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No fields found for review template. Please add some fields in our web editor first.'); } const fieldSpecs = templateFieldsResponse.fields; return { fields: fieldSpecs }; } } }; this.webhookMethods = helpers_1.gotoHumanWebhookMethods; this.webhook = async function () { const bodyData = this.getBodyData(); return { workflowData: [this.helpers.returnJsonArray(bodyData)], }; }; } async execute() { var _a, _b; const items = this.getInputData(); const returnData = []; const resource = this.getNodeParameter('resource', 0); const operation = this.getNodeParameter('operation', 0); let waitMillis = 14 * 24 * 60 * 60 * 1000; for (let i = 0; i < items.length; i++) { try { if (resource === 'reviewRequest' && (operation === 'sendAndWait' || operation === 'send')) { const agentID = this.getNodeParameter('agentID', i); const reviewTemplateID = this.getNodeParameter('reviewTemplateID', i); const useSendRawReviewData = this.isNodeFeatureEnabled('sendRawReviewData'); let parsedFields; let parsedConfig; if (useSendRawReviewData) { const reviewDataRaw = this.getNodeParameter('reviewData', i); if (typeof reviewDataRaw === 'string') { try { parsedFields = (0, n8n_workflow_1.jsonParse)(reviewDataRaw); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'reviewData is not valid JSON', error); } } else { parsedFields = reviewDataRaw; } const reviewConfigRaw = this.getNodeParameter('reviewConfig', i); if (reviewConfigRaw) { if (typeof reviewConfigRaw === 'string') { try { parsedConfig = (0, n8n_workflow_1.jsonParse)(reviewConfigRaw); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'reviewConfig is not valid JSON', error); } } else { parsedConfig = reviewConfigRaw; } } } else { const fieldsParam = this.getNodeParameter('fields', i); parsedFields = { ...(fieldsParam.value || {}) }; const schemaList = fieldsParam.schema; if (fieldsParam.value && schemaList) { for (const field of schemaList) { const val = fieldsParam.value[field.id]; if ((field.type === 'array' || field.type === 'object') && typeof val === 'string') { try { parsedFields[field.id] = (0, n8n_workflow_1.jsonParse)(val); } catch { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Could not parse field '${field.id}' as JSON: ${val}`, { itemIndex: i }); } } } } } const metaSelect = this.getNodeParameter('metaSelect', i); let meta; if (metaSelect === 'json') { const metaJson = this.getNodeParameter('metaJson', i); if (metaJson) { try { meta = (0, n8n_workflow_1.jsonParse)(metaJson); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Not a valid JSON object', error); } } } else if (metaSelect === 'keyValue') { const metaKeyValues = this.getNodeParameter('metaKeyValues', i); if (metaKeyValues && Array.isArray(metaKeyValues.metaArray)) { meta = {}; for (const pair of metaKeyValues.metaArray) { meta[pair.key] = pair.value; } } } const assignToSelect = this.getNodeParameter('assignToSelect', i); let assignTo; if (assignToSelect === 'selectByEmail') { const assignToParam = this.getNodeParameter('assignTo', i); if (assignToParam && Array.isArray(assignToParam.values)) { assignTo = assignToParam.values.map((v) => v.email); } } const additionalFields = this.getNodeParameter('additionalFields', i); const body = { formId: reviewTemplateID.value, fields: parsedFields, origin: 'n8n', originV: this.getNode().typeVersion, }; if (parsedConfig !== undefined) body.config = parsedConfig; if (agentID === null || agentID === void 0 ? void 0 : agentID.value) body.agentId = agentID.value; if (meta !== undefined) body.meta = meta; if (assignTo !== undefined) body.assignTo = assignTo; if (additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.title) body.title = additionalFields.title; if ((additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.autoApprove) !== undefined) body.autoApprove = additionalFields.autoApprove; if (additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.workflow) { try { body.workflow = (0, n8n_workflow_1.jsonParse)(additionalFields.workflow); } catch { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'workflow field is not valid JSON', { itemIndex: i }); } } if (additionalFields === null || additionalFields === void 0 ? void 0 : additionalFields.updateForReviewId) { body.updateForReviewId = additionalFields.updateForReviewId; } const sessionId = this.getNodeParameter('sessionId', i); if (sessionId) body.sessionId = sessionId; if (operation === 'sendAndWait') { const resumeUrl = this.evaluateExpression('{{ $execution?.resumeUrl }}', i); const nodeId = this.evaluateExpression('{{ $nodeId }}', i); body.webhookUrl = `${resumeUrl}/${nodeId}`; } const options = { method: 'POST', url: `${helpers_1.BASE_URL}/requestReview`, body, json: true, returnFullResponse: true, ignoreHttpStatusErrors: true, }; let responseData; try { responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options); } catch (error) { throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { itemIndex: i }); } const statusCode = (responseData === null || responseData === void 0 ? void 0 : responseData.statusCode) || 200; if (String(statusCode).startsWith('4') || String(statusCode).startsWith('5')) { throw new n8n_workflow_1.NodeApiError(this.getNode(), responseData, { message: responseData.body ? responseData.body : JSON.stringify(responseData), httpCode: String(statusCode), itemIndex: i, }); } if (((_a = responseData === null || responseData === void 0 ? void 0 : responseData.body) === null || _a === void 0 ? void 0 : _a.timeoutInMillis) && typeof responseData.body.timeoutInMillis === 'number' && responseData.body.timeoutInMillis > 0) { waitMillis = responseData.body.timeoutInMillis; } returnData.push({ json: responseData, pairedItem: { item: i } }); } else if (resource === 'reviewRequest' && operation === 'delete') { const reviewId = this.getNodeParameter('reviewId', i); const options = { method: 'DELETE', url: `${helpers_1.BASE_URL}/deleteReview`, body: { reviewId }, json: true, }; const responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options); returnData.push({ json: responseData !== null && responseData !== void 0 ? responseData : { success: true }, pairedItem: { item: i } }); } else if (resource === 'message' && operation === 'sendMessage') { const agentID = this.getNodeParameter('agentID', i); const sessionId = this.getNodeParameter('sessionId', i); const message = this.getNodeParameter('message', i); const body = { sessionId, message, origin: 'n8n', originV: this.getNode().typeVersion, }; if (agentID === null || agentID === void 0 ? void 0 : agentID.value) body.agentId = agentID.value; const options = { method: 'POST', url: `${helpers_1.BASE_URL}/sendMessageToHuman`, body, json: true, returnFullResponse: true, ignoreHttpStatusErrors: true, }; let responseData; try { responseData = await this.helpers.httpRequestWithAuthentication.call(this, 'gotoHumanApi', options); } catch (error) { throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { itemIndex: i }); } const statusCode = (responseData === null || responseData === void 0 ? void 0 : responseData.statusCode) || 200; if (String(statusCode).startsWith('4') || String(statusCode).startsWith('5')) { throw new n8n_workflow_1.NodeApiError(this.getNode(), responseData, { message: responseData.body ? responseData.body : JSON.stringify(responseData), httpCode: String(statusCode), itemIndex: i, }); } returnData.push({ json: responseData, pairedItem: { item: i } }); } } catch (err) { if (this.continueOnFail()) { const errorMessage = err instanceof Error ? err.message : String(err); returnData.push({ json: { error: errorMessage }, error: err, pairedItem: { item: i }, }); continue; } if (err instanceof n8n_workflow_1.NodeApiError) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: err.message }, { message: err.message, httpCode: (_b = err.httpCode) !== null && _b !== void 0 ? _b : undefined, itemIndex: i, }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), err.message || 'An unknown error occurred', { itemIndex: i }); } } } if (operation === 'sendAndWait') { const waitTill = new Date(new Date().getTime() + waitMillis); await this.putExecutionToWait(waitTill); return [this.getInputData()]; } return [returnData]; } } exports.GotoHuman = GotoHuman; //# sourceMappingURL=GotoHuman.node.js.map