@kaia-team/n8n-nodes-kaia
Version:
n8n node to interact with Kaia API for signal capture and security gating
461 lines (460 loc) • 20.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SaveSignal = void 0;
const crypto_1 = require("crypto");
class SaveSignal {
constructor() {
this.description = {
displayName: 'KAIA Save Signal',
name: 'saveSignal',
icon: 'file:kaia.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Capture a signal for knowledge graph processing',
defaults: {
name: 'KAIA Save Signal',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'kaiaApi',
required: false,
},
],
properties: [
{
displayName: 'Content',
name: 'content',
type: 'string',
typeOptions: {
rows: 4,
},
default: '={{JSON.stringify($json)}}',
required: true,
description: 'Signal content',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Signal Type',
name: 'signalType',
type: 'options',
options: [
{
name: 'API Call',
value: 'api_call',
},
{
name: 'Custom',
value: 'custom',
},
{
name: 'Data Sync',
value: 'data_sync',
},
{
name: 'Decision Made',
value: 'decision_made',
},
{
name: 'Department Data',
value: 'department_data',
},
{
name: 'Document Processed',
value: 'document_processed',
},
{
name: 'Email Sent',
value: 'email_sent',
},
{
name: 'LLM Input',
value: 'llm_input',
},
{
name: 'LLM Output',
value: 'llm_output',
},
{
name: 'Product Data',
value: 'product_data',
},
{
name: 'User Data',
value: 'user_data',
},
{
name: 'Workflow Execution',
value: 'workflow_execution',
},
],
default: 'custom',
description: 'Type of signal being captured',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Signal title',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
description: 'User identifier',
},
{
displayName: 'User Email',
name: 'userEmail',
type: 'string',
default: '',
description: 'User email address',
},
{
displayName: 'User Roles',
name: 'userRoles',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
description: 'Array of user roles',
options: [
{
displayName: 'Role',
name: 'role',
values: [
{
displayName: 'Role',
name: 'value',
type: 'string',
default: '',
description: 'User role',
},
],
},
],
},
{
displayName: 'Department',
name: 'department',
type: 'options',
options: [
{
name: 'Engineering',
value: 'Engineering',
},
{
name: 'General',
value: 'General',
},
{
name: 'HR',
value: 'HR',
},
{
name: 'Legal',
value: 'Legal',
},
{
name: 'Sales',
value: 'Sales',
},
],
default: 'General',
description: 'User department',
},
{
displayName: 'Workflow ID',
name: 'workflowId',
type: 'string',
default: '={{$workflow.id}}',
description: 'Workflow identifier',
},
{
displayName: 'Workflow Name',
name: 'workflowName',
type: 'string',
default: '={{$workflow.name}}',
description: 'Human-readable workflow name',
},
{
displayName: 'Execution ID',
name: 'executionId',
type: 'string',
default: '={{$execution.id}}',
description: 'Execution identifier',
},
{
displayName: 'Node ID',
name: 'nodeId',
type: 'string',
default: '={{$prevNode.name}}-{{$prevNode.outputIndex}}-{{$prevNode.runIndex}}',
description: 'Node identifier within the workflow',
},
{
displayName: 'Node Name',
name: 'nodeName',
type: 'string',
default: '={{$prevNode.name}}',
description: 'Human-readable node name',
},
{
displayName: 'Priority',
name: 'priority',
type: 'options',
options: [
{
name: 'Low',
value: 'low',
},
{
name: 'Medium',
value: 'medium',
},
{
name: 'High',
value: 'high',
},
{
name: 'Critical',
value: 'critical',
},
],
default: 'medium',
description: 'Signal priority level',
},
{
displayName: 'Context Level',
name: 'context_level',
type: 'options',
options: [
{
name: 'Raw',
value: 'raw',
},
{
name: 'Baked',
value: 'baked',
},
{
name: 'Cluster',
value: 'cluster',
},
],
default: 'raw',
description: 'Context processing level',
},
{
displayName: 'Category',
name: 'category',
type: 'string',
default: '',
description: 'Signal category',
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
description: 'Comma-separated tags, or use expressions for array',
hint: 'Use expressions like =["tag1", "tag2"] for array format',
},
{
displayName: 'LLM Context',
name: 'llmContext',
type: 'json',
default: '',
description: 'LLM context information as JSON object',
},
{
displayName: 'Knowledge Graph',
name: 'knowledge_graph',
type: 'json',
default: '',
description: 'Knowledge graph structure as JSON object',
},
],
},
],
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
for (let i = 0; i < items.length; i++) {
try {
// Get credentials
let credentials;
let baseUrl = '';
let token = '';
try {
credentials = (await this.getCredentials('kaiaApi'));
baseUrl = credentials?.baseUrl || '';
token = credentials?.token || '';
}
catch {
// Credentials not configured - will throw error below
}
// Get parameters
const content = this.getNodeParameter('content', i);
const additionalFields = this.getNodeParameter('additionalFields', i);
// Get optional fields from additionalFields
const userId = additionalFields.userId;
const userEmail = additionalFields.userEmail;
const userRolesRaw = additionalFields.userRoles;
const signalType = additionalFields.signalType;
const title = additionalFields.title;
const department = additionalFields.department;
// Get workflow context from additionalFields or use defaults
// Check if fields exist in additionalFields, otherwise use defaults
const workflow = this.getWorkflow();
const currentNode = this.getNode();
// Get workflow context - use additionalFields if provided, otherwise use defaults
const workflowId = additionalFields.workflowId || workflow.id?.toString() || '';
const workflowName = additionalFields.workflowName || workflow.name || '';
const executionId = additionalFields.executionId || this.getExecutionId() || '';
// For nodeId and nodeName, try to get from previous node if available
const currentItem = items[i];
const prevNodeName = currentItem?.json?.name;
const nodeId = additionalFields.nodeId ||
(prevNodeName ? `${prevNodeName}-${currentItem?.json?.outputIndex || ''}-${currentItem?.json?.runIndex || ''}` : currentNode.id || '');
const nodeName = additionalFields.nodeName || prevNodeName || currentNode.name || '';
// Auto-generate content hash
const contentHash = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
// Parse userRoles from fixedCollection
let userRoles = [];
if (userRolesRaw && typeof userRolesRaw === 'object') {
// Handle fixedCollection format: { role: [{ value: 'role1' }, { value: 'role2' }] }
if (userRolesRaw.role && Array.isArray(userRolesRaw.role)) {
userRoles = userRolesRaw.role
.map((item) => item.value)
.filter((r) => r);
}
}
else if (userRolesRaw && Array.isArray(userRolesRaw)) {
// Handle case where userRoles is already an array
userRoles = userRolesRaw.map((r) => String(r)).filter((r) => r);
}
// Build request body
const body = {
workflowId,
workflowName,
executionId,
nodeId,
nodeName,
content,
};
// Only include userId, userEmail, userRoles if they have values
if (userId && userId.trim()) {
body.userId = userId;
}
if (userEmail && userEmail.trim()) {
body.userEmail = userEmail;
}
if (userRoles && userRoles.length > 0) {
body.userRoles = userRoles;
}
// Only include signalType, title, and department if they are defined by the user
// Server will set defaults if not provided
if (signalType && signalType.trim()) {
body.signalType = signalType;
}
if (title && title.trim()) {
body.title = title;
}
if (department && department.trim()) {
body.department = department;
}
// Always include contentHash (auto-generated)
body.contentHash = contentHash;
// Add additional fields
if (additionalFields.priority) {
body.priority = additionalFields.priority;
}
if (additionalFields.context_level) {
body.context_level = additionalFields.context_level;
}
if (additionalFields.category) {
body.category = additionalFields.category;
}
if (additionalFields.tags) {
let tags = [];
if (typeof additionalFields.tags === 'string') {
try {
tags = JSON.parse(additionalFields.tags);
}
catch {
tags = additionalFields.tags.split(',').map((t) => t.trim()).filter((t) => t);
}
}
else if (Array.isArray(additionalFields.tags)) {
tags = additionalFields.tags.map((tag) => String(tag));
}
if (tags.length > 0) {
body.tags = tags;
}
}
if (additionalFields.llmContext) {
try {
const llmContext = typeof additionalFields.llmContext === 'string'
? JSON.parse(additionalFields.llmContext)
: additionalFields.llmContext;
body.llmContext = llmContext;
}
catch (error) {
throw new Error(`Invalid LLM Context JSON: ${error}`);
}
}
if (additionalFields.knowledge_graph) {
try {
const knowledgeGraph = typeof additionalFields.knowledge_graph === 'string'
? JSON.parse(additionalFields.knowledge_graph)
: additionalFields.knowledge_graph;
body.knowledge_graph = knowledgeGraph;
}
catch (error) {
throw new Error(`Invalid Knowledge Graph JSON: ${error}`);
}
}
// Make API request
if (!baseUrl || !token) {
throw new Error('Kaia API credentials are required. Please configure the credentials.');
}
const requestUrl = `${baseUrl}/api/signals/remember`;
const options = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
method: 'POST',
body,
url: requestUrl,
json: true,
};
const response = await this.helpers.httpRequest(options);
returnData.push(response);
}
catch (error) {
if (this.continueOnFail()) {
const errorMessage = error instanceof Error ? error.message : String(error);
returnData.push({ error: errorMessage });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
exports.SaveSignal = SaveSignal;