UNPKG

n8n

Version:

n8n Workflow Automation Tool

232 lines (224 loc) 9.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createLlmMockHandler = createLlmMockHandler; const backend_common_1 = require("@n8n/backend-common"); const di_1 = require("@n8n/di"); const n8n_workflow_1 = require("n8n-workflow"); const instance_ai_1 = require("@n8n/instance-ai"); const api_docs_1 = require("./api-docs"); const node_config_1 = require("./node-config"); const request_sanitizer_1 = require("./request-sanitizer"); const MOCK_SYSTEM_PROMPT = `You generate realistic HTTP responses for one specific request, mocking an API in n8n workflow evaluation. You get everything you need in the user message: the request (service, method, URL, body, query), API docs for the endpoint, the n8n node's parameters, and optional context (globalContext, nodeHint, scenarioHints). Generate the response directly — do NOT call any tools. Response SHAPE comes from the API docs; DATA VALUES come from the node config. Use names/IDs from the config exactly (case-sensitive). Node-config patterns to know: - "__rl" object: "value" is the selected resource id - "schema" array: each entry's "id" is the response field name (NOT "displayName"). e.g. {id:"timestamp",displayName:"Timestamp"} → response uses "timestamp" - Strings starting with "=" are expressions (ignore) Match THIS request only (URL + method): a node may make multiple sequential calls; reply to the specific one shown. Echo identifiers, placeholders, and reference values from the request back into the response. No pagination — always indicate end of results. ## Output format Return ONLY a JSON object, no prose, no markdown: { "type": "json", "body": { ... } } { "type": "binary", "contentType": "application/pdf", "filename": "doc.pdf" } { "type": "error", "statusCode": 404, "body": { ... } } For APIs that return empty responses on success (204/202), use { "type": "json", "body": {} }.`; const DEFAULT_MAX_RETRIES = 1; function createLlmMockHandler(options) { const nodeConfigCache = new Map(); return async (requestOptions, node) => { if (!nodeConfigCache.has(node.name)) { nodeConfigCache.set(node.name, (0, node_config_1.extractNodeConfig)(node)); } return await generateMockResponse(requestOptions, node, { scenarioHints: options?.scenarioHints, globalContext: options?.globalContext, nodeHint: options?.nodeHints?.[node.name], nodeConfig: nodeConfigCache.get(node.name) ?? '', maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES, }); }; } async function generateMockResponse(request, node, context) { const serviceName = extractServiceName(request.url); const endpoint = extractEndpoint(request.url); const sections = [ '## Request', `Service: ${serviceName}`, `Node: "${node.name}" (${node.type})`, (request.method ?? 'GET') + ' ' + endpoint, 'Generate the response for this EXACT endpoint and method.', ]; if (request.body) { const sanitized = (0, request_sanitizer_1.redactSecretKeys)(request.body); const serialized = (0, request_sanitizer_1.truncateForLlm)(JSON.stringify(sanitized)); sections.push(`Body: ${serialized}`); } if (request.qs && Object.keys(request.qs).length > 0) { const sanitizedQs = (0, request_sanitizer_1.redactSecretKeys)(request.qs); sections.push(`Query: ${JSON.stringify(sanitizedQs)}`); } const isGraphQL = endpoint.includes('/graphql') || (typeof request.body === 'object' && request.body !== null && 'query' in request.body); if (isGraphQL) { sections.push('', '## GraphQL format requirement'); sections.push('This is a GraphQL endpoint. ALL responses MUST use GraphQL response format:', '- Success: { "data": { ...fields matching the query... } }', '- Error: { "errors": [{ "message": "...", "extensions": { "code": "..." } }], "data": null }', 'Never return flat REST-style error objects.'); } const apiDocs = await (0, api_docs_1.fetchApiDocs)(serviceName, `${request.method ?? 'GET'} ${endpoint} response format`); sections.push('', '## API documentation', apiDocs); if (context.nodeConfig) { sections.push('', '## Node Configuration', context.nodeConfig); } if (context.globalContext || context.nodeHint || context.scenarioHints) { sections.push('', '## Context'); if (context.globalContext) sections.push(`Data: ${context.globalContext}`); if (context.nodeHint) sections.push(`Hint: ${context.nodeHint}`); if (context.scenarioHints) { sections.push(`Scenario: ${context.scenarioHints}`); sections.push(isGraphQL ? '(For error scenarios, use GraphQL error format with "data": null. Don\'t use "type": "error" wrapper.)' : '(Use "error" type with appropriate statusCode for error scenarios.)'); } } const userPrompt = sections.join('\n'); const safeUrl = extractEndpoint(request.url); let lastError = ''; for (let attempt = 0; attempt <= context.maxRetries; attempt++) { try { const spec = await callLlm(userPrompt); return materializeSpec(spec); } catch (error) { lastError = error instanceof Error ? error.message : String(error); if (attempt < context.maxRetries) { di_1.Container.get(backend_common_1.Logger).warn(`[EvalMock] Mock generation failed for ${request.method ?? 'GET'} ${safeUrl}, retrying (${attempt + 1}/${context.maxRetries}): ${lastError}`); } } } di_1.Container.get(backend_common_1.Logger).error(`[EvalMock] Mock generation failed for ${request.method ?? 'GET'} ${safeUrl} after ${context.maxRetries + 1} attempts: ${lastError}`); return { body: { _evalMockError: true, message: `Mock generation failed: ${lastError}` }, headers: { 'content-type': 'application/json' }, statusCode: 200, }; } async function callLlm(userPrompt) { const agent = (0, instance_ai_1.createEvalAgent)('eval-mock-responder', { instructions: MOCK_SYSTEM_PROMPT, }); const result = await agent.generate(userPrompt); const text = (0, instance_ai_1.extractText)(result); return parseResponseText(text); } function parseResponseText(raw) { let text = raw.trim(); const fencedMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/i); if (fencedMatch) { text = fencedMatch[1].trim(); } text = text .replace(/^```(?:json)?\s*\n?/i, '') .replace(/\n?\s*```\s*$/i, '') .trim(); if (text.length > 0 && !text.startsWith('{') && !text.startsWith('[')) { const extracted = extractJsonObject(text); if (extracted) { text = extracted; } } const parsed = (0, n8n_workflow_1.jsonParse)(text); if (!parsed.type || !['json', 'binary', 'error'].includes(parsed.type)) { return { type: 'json', body: parsed }; } return parsed; } function materializeSpec(spec) { switch (spec.type) { case 'json': return { body: spec.body ?? { ok: true }, headers: { 'content-type': 'application/json' }, statusCode: 200, }; case 'binary': { const filename = spec.filename ?? 'mock-file.dat'; const contentType = spec.contentType ?? 'application/octet-stream'; const content = `[eval-mock] Synthetic file: ${filename} (${contentType})`; return { body: Buffer.from(content), headers: { 'content-type': contentType }, statusCode: 200, }; } case 'error': return { body: spec.body ?? { error: 'Mock error' }, headers: { 'content-type': 'application/json' }, statusCode: spec.statusCode ?? 500, }; default: return { body: spec.body ?? { ok: true }, headers: { 'content-type': 'application/json' }, statusCode: 200, }; } } function extractJsonObject(text) { const start = text.indexOf('{'); if (start < 0) return undefined; let depth = 0; let inString = false; let escape = false; for (let i = start; i < text.length; i++) { const ch = text[i]; if (escape) { escape = false; continue; } if (ch === '\\' && inString) { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === '{') depth++; if (ch === '}') { depth--; if (depth === 0) { return text.slice(start, i + 1); } } } return text.slice(start); } function extractServiceName(url) { try { const hostname = new URL(url).hostname; const parts = hostname .replace(/^api\./, '') .replace(/^www\./, '') .split('.'); return parts[0].charAt(0).toUpperCase() + parts[0].slice(1); } catch { return 'Unknown'; } } function extractEndpoint(url) { try { const parsed = new URL(url); return parsed.pathname + (parsed.search ? parsed.search : ''); } catch { return url; } } //# sourceMappingURL=mock-handler.js.map