UNPKG

@kaia-team/n8n-nodes-kaia

Version:

n8n node to interact with Kaia API for signal capture and security gating

372 lines (371 loc) 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModelGate = void 0; const n8n_workflow_1 = require("n8n-workflow"); const crypto_1 = require("crypto"); class ModelGate { constructor() { this.description = { displayName: 'KAIA Model Gate', name: 'modelGate', icon: 'file:kaia.svg', group: ['transform'], version: 1, subtitle: '={{$parameter["model"]}}', description: 'Intercept LLM input and output to record all interactions for AI Act compliance', defaults: { name: 'KAIA Model Gate', }, // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node inputs: [ { displayName: 'Model', type: n8n_workflow_1.NodeConnectionTypes.AiLanguageModel, required: true, maxConnections: 1, }, ], outputs: [ { displayName: 'Model', type: n8n_workflow_1.NodeConnectionTypes.AiLanguageModel, }, ], credentials: [ { name: 'kaiaApi', required: true, }, ], properties: [ { displayName: 'Connect a language model on the canvas to intercept its calls', name: 'modelNotice', type: 'notice', default: '', description: 'This node requires a connected LangChain model to intercept its input and output', }, ], }; } async supplyData(itemIndex) { // Get credentials - required const credentials = await this.getCredentials('kaiaApi'); const baseUrl = credentials?.baseUrl || ''; const token = credentials?.token || ''; if (!baseUrl || !token) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Kaia API credentials are required'); } // Get the connected model from the input connection const model = await this.getInputConnectionData(n8n_workflow_1.NodeConnectionTypes.AiLanguageModel, itemIndex); if (!model) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No model connected. Please connect a language model to the Model input.'); } // Get workflow context for signal metadata const workflow = this.getWorkflow(); const currentNode = this.getNode(); const workflowId = workflow.id?.toString() || ''; const workflowName = workflow.name || ''; const executionId = this.getExecutionId() || ''; const nodeId = currentNode.id || ''; const nodeName = currentNode.name || ''; // Create a callback handler to intercept LLM calls // Capture context variables in closure const helpers = this.helpers; const requestUrl = `${baseUrl}/api/signals/remember`; const authHeader = `Bearer ${token}`; // Create a simple callback handler object that LangChain can use const callbackHandler = { name: 'KaiaModelGate', startTime: 0, inputPrompt: '', modelName: 'unknown', provider: 'unknown', async handleLLMStart(llm, prompts, runId) { this.startTime = Date.now(); this.inputPrompt = JSON.stringify(prompts, null, 2); // Extract model metadata if (llm) { this.modelName = llm.modelName || llm.model || llm.id || 'unknown'; this.provider = llm.constructor?.name || 'unknown'; } // Send input signal asynchronously (non-blocking) this.sendInputSignal(helpers, requestUrl, authHeader, workflowId, workflowName, executionId, nodeId, nodeName).catch(() => { // Silently fail - don't block model execution }); }, async handleLLMEnd(output, runId) { const endTime = Date.now(); const latencyMs = endTime - this.startTime; // Extract token information if available const usage = output.llmOutput?.tokenUsage || output.usage || {}; const promptTokens = usage.promptTokens || 0; const completionTokens = usage.completionTokens || 0; const totalTokens = usage.totalTokens || (promptTokens + completionTokens); // Extract response content const responseContent = output.generations?.[0]?.[0]?.text || output.text || JSON.stringify(output, null, 2); // Send output signal asynchronously (non-blocking) this.sendOutputSignal(helpers, requestUrl, authHeader, workflowId, workflowName, executionId, nodeId, nodeName, responseContent, { provider: this.provider, model: this.modelName, promptTokens, completionTokens, totalTokens, latencyMs, }).catch(() => { // Silently fail - don't block model execution }); }, async sendInputSignal(helpers, requestUrl, authHeader, workflowId, workflowName, executionId, nodeId, nodeName) { const inputContentHash = (0, crypto_1.createHash)('sha256').update(this.inputPrompt).digest('hex'); const inputSignalBody = { workflowId, workflowName, executionId, nodeId, nodeName, signalType: 'llm_input', content: this.inputPrompt, contentHash: inputContentHash, llmContext: { provider: this.provider, model: this.modelName, }, priority: 'high', context_level: 'raw', }; try { await helpers.httpRequest({ headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, method: 'POST', url: requestUrl, body: inputSignalBody, json: true, }); } catch { // Non-blocking - errors are silently ignored } }, async sendOutputSignal(helpers, requestUrl, authHeader, workflowId, workflowName, executionId, nodeId, nodeName, outputContent, llmContext) { const outputContentHash = (0, crypto_1.createHash)('sha256').update(outputContent).digest('hex'); const outputSignalBody = { workflowId, workflowName, executionId, nodeId, nodeName, signalType: 'llm_output', content: outputContent, contentHash: outputContentHash, llmContext, priority: 'high', context_level: 'raw', }; try { await helpers.httpRequest({ headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, method: 'POST', url: requestUrl, body: outputSignalBody, json: true, }); } catch { // Non-blocking - errors are silently ignored } }, }; // Intercept model calls WITHOUT wrapping the instance // This preserves the exact original model instance so n8n's type checks pass // We'll wrap the model's invoke/generate methods to intercept calls const originalInvoke = model.invoke; const originalGenerate = model.generate; const originalCall = model.call; // Wrap invoke method if (typeof originalInvoke === 'function') { model.invoke = async function (...args) { const startTime = Date.now(); const prompts = args[0] || {}; // Extract model metadata const modelName = this.modelName || this.model || this.id || 'unknown'; const provider = this.constructor?.name || 'unknown'; // Send input signal const inputContent = JSON.stringify(prompts, null, 2); const inputContentHash = (0, crypto_1.createHash)('sha256').update(inputContent).digest('hex'); const inputSignalBody = { workflowId, workflowName, executionId, nodeId, nodeName, signalType: 'llm_input', content: inputContent, contentHash: inputContentHash, llmContext: { provider, model: modelName, }, priority: 'high', context_level: 'raw', }; // Send input signal asynchronously (non-blocking) helpers.httpRequest({ headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, method: 'POST', url: requestUrl, body: inputSignalBody, json: true, }).catch(() => { // Silently fail }); // Call original method with callbacks const callbacks = this.callbacks || []; const allCallbacks = Array.isArray(callbacks) ? [...callbacks, callbackHandler] : [callbacks, callbackHandler]; const result = await originalInvoke.apply(this, args); // Send output signal const endTime = Date.now(); const latencyMs = endTime - startTime; const outputContent = JSON.stringify(result, null, 2); const outputContentHash = (0, crypto_1.createHash)('sha256').update(outputContent).digest('hex'); // Extract token usage if available const usage = result.usage || result.llmOutput?.tokenUsage || {}; const promptTokens = usage.promptTokens || 0; const completionTokens = usage.completionTokens || 0; const totalTokens = usage.totalTokens || (promptTokens + completionTokens); const outputSignalBody = { workflowId, workflowName, executionId, nodeId, nodeName, signalType: 'llm_output', content: outputContent, contentHash: outputContentHash, llmContext: { provider, model: modelName, promptTokens, completionTokens, totalTokens, latencyMs, }, priority: 'high', context_level: 'raw', }; // Send output signal asynchronously (non-blocking) helpers.httpRequest({ headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, method: 'POST', url: requestUrl, body: outputSignalBody, json: true, }).catch(() => { // Silently fail }); return result; }; } // Also wrap generate if it exists if (typeof originalGenerate === 'function') { model.generate = async function (...args) { const startTime = Date.now(); const prompts = args[0] || []; // Extract model metadata const modelName = this.modelName || this.model || this.id || 'unknown'; const provider = this.constructor?.name || 'unknown'; // Send input signal const inputContent = JSON.stringify(prompts, null, 2); const inputContentHash = (0, crypto_1.createHash)('sha256').update(inputContent).digest('hex'); const inputSignalBody = { workflowId, workflowName, executionId, nodeId, nodeName, signalType: 'llm_input', content: inputContent, contentHash: inputContentHash, llmContext: { provider, model: modelName, }, priority: 'high', context_level: 'raw', }; helpers.httpRequest({ headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, method: 'POST', url: requestUrl, body: inputSignalBody, json: true, }).catch(() => { }); const result = await originalGenerate.apply(this, args); // Send output signal const endTime = Date.now(); const latencyMs = endTime - startTime; const outputContent = JSON.stringify(result, null, 2); const outputContentHash = (0, crypto_1.createHash)('sha256').update(outputContent).digest('hex'); const usage = result.llmOutput?.tokenUsage || result.usage || {}; const promptTokens = usage.promptTokens || 0; const completionTokens = usage.completionTokens || 0; const totalTokens = usage.totalTokens || (promptTokens + completionTokens); const outputSignalBody = { workflowId, workflowName, executionId, nodeId, nodeName, signalType: 'llm_output', content: outputContent, contentHash: outputContentHash, llmContext: { provider, model: modelName, promptTokens, completionTokens, totalTokens, latencyMs, }, priority: 'high', context_level: 'raw', }; helpers.httpRequest({ headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, method: 'POST', url: requestUrl, body: outputSignalBody, json: true, }).catch(() => { }); return result; }; } // Return the ORIGINAL model instance - no wrapping, preserves all type checks const wrappedModel = model; // Return the wrapped model so it can be passed through to the AI Agent return { response: wrappedModel, }; } } exports.ModelGate = ModelGate;