UNPKG

n8n

Version:

n8n Workflow Automation Tool

420 lines 20.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentsBuilderToolsService = void 0; exports.getAgentConfigHash = getAgentConfigHash; const agents_1 = require("@n8n/agents"); const api_types_1 = require("@n8n/api-types"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const node_crypto_1 = require("node:crypto"); const zod_1 = require("zod"); const agents_tools_service_1 = require("../agents-tools.service"); const agents_service_1 = require("../agents.service"); const agent_config_composition_1 = require("../json-config/agent-config-composition"); const agent_json_config_1 = require("../json-config/agent-json-config"); const agent_secure_runtime_1 = require("../runtime/agent-secure-runtime"); const builder_model_lookup_service_1 = require("./builder-model-lookup.service"); const interactive_1 = require("./interactive"); const builder_tool_names_1 = require("./builder-tool-names"); const EMPTY_INSTRUCTIONS_ERROR = { path: '/instructions', message: 'Refusing to write an agent with empty instructions. Ask the user what the agent should do before calling write_config or patch_config again.', }; const STALE_CONFIG_ERROR = { path: '(root)', message: 'Agent config changed since you last read it. Call read_config and retry with the returned configHash.', }; function rejectIfEmptyInstructions(config) { if (!config.instructions.trim()) { return { errors: [EMPTY_INSTRUCTIONS_ERROR] }; } return null; } function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } function canonicalizeJson(value) { if (Array.isArray(value)) { return value.map((item) => canonicalizeJson(item)); } if (!isRecord(value)) return value; const sorted = {}; for (const key of Object.keys(value).sort()) { sorted[key] = canonicalizeJson(value[key]); } return sorted; } function getAgentConfigHash(config) { if (!config) return null; return (0, node_crypto_1.createHash)('sha256') .update(JSON.stringify(canonicalizeJson(config))) .digest('hex'); } function snapshotFromConfig(config, updatedAt, versionId) { return { config, configHash: getAgentConfigHash(config), updatedAt, versionId, }; } let AgentsBuilderToolsService = class AgentsBuilderToolsService { constructor(agentsService, secureRuntime, workflowRepository, agentsToolsService, builderModelLookupService) { this.agentsService = agentsService; this.secureRuntime = secureRuntime; this.workflowRepository = workflowRepository; this.agentsToolsService = agentsToolsService; this.builderModelLookupService = builderModelLookupService; } getTools(agentId, projectId, credentialProvider, user) { return { json: this.getJsonTools(agentId, projectId, credentialProvider, user), shared: this.getSharedTools(agentId, projectId, credentialProvider), }; } getJsonTools(agentId, projectId, credentialProvider, user) { const readConfigTool = new agents_1.Tool(builder_tool_names_1.BUILDER_TOOLS.READ_CONFIG) .description('Read the latest persisted agent configuration and freshness metadata. ' + 'Returns { ok: true, config, configHash, updatedAt, versionId }. ' + 'Call this before every write_config or patch_config and use configHash as baseConfigHash.') .input(zod_1.z.object({})) .handler(async () => { try { return { ok: true, ...(await this.getConfigSnapshot(agentId, projectId)) }; } catch (e) { return { ok: false, errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }], }; } }) .build(); const writeConfigTool = new agents_1.Tool(builder_tool_names_1.BUILDER_TOOLS.WRITE_CONFIG) .description('Create or replace the agent configuration by writing a complete JSON string. ' + 'Requires baseConfigHash from the immediately preceding read_config result, or from a stale retry response. ' + 'Do not use a configHash copied from the prompt snapshot. ' + 'Returns { ok: true, config, configHash, updatedAt, versionId } on success or ' + '{ ok: false, stage, errors } with path, message, expected, received fields on failure.') .input(zod_1.z.object({ json: zod_1.z.string().describe('Complete agent configuration as a JSON string'), baseConfigHash: zod_1.z .string() .nullable() .describe('configHash from the immediately preceding read_config result; null only if no config exists'), })) .handler(async ({ json, baseConfigHash }) => { const parsed = (0, agent_json_config_1.tryParseConfigJson)(json); if (!parsed.ok) { return { ok: false, errors: parsed.errors }; } let snapshot; try { snapshot = await this.getConfigSnapshot(agentId, projectId); } catch (e) { return { ok: false, stage: 'stale', errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }], }; } if (baseConfigHash !== snapshot.configHash) { return { ok: false, stage: 'stale', errors: [STALE_CONFIG_ERROR], ...snapshot }; } const zodResult = agent_json_config_1.AgentJsonConfigSchema.safeParse(parsed.data); if (!zodResult.success) { return { ok: false, errors: (0, agent_json_config_1.formatZodErrors)(zodResult.error) }; } const emptyInstructions = rejectIfEmptyInstructions(zodResult.data); if (emptyInstructions) { return { ok: false, errors: emptyInstructions.errors }; } try { const result = await this.agentsService.updateConfig(agentId, projectId, zodResult.data); return { ok: true, ...snapshotFromConfig(result.config, result.updatedAt, result.versionId), }; } catch (e) { return { ok: false, stage: 'schema', errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }], }; } }) .build(); const patchConfigTool = new agents_1.Tool(builder_tool_names_1.BUILDER_TOOLS.PATCH_CONFIG) .description('Apply RFC 6902 JSON Patch operations to the current agent configuration. ' + 'Pass an array of patch operations as a JSON string. ' + 'Requires baseConfigHash from the immediately preceding read_config result, or from a stale retry response. ' + 'Do not use a configHash copied from the prompt snapshot. ' + 'Supported ops: add, remove, replace, move, copy, test. ' + 'Returns { ok: true, config, configHash, updatedAt, versionId } on success or ' + '{ ok: false, stage, errors } on failure. ' + 'stage is "parse", "stale", "patch", or "schema".') .input(zod_1.z.object({ operations: zod_1.z.string().describe('RFC 6902 JSON Patch operations array as a JSON string'), baseConfigHash: zod_1.z .string() .nullable() .describe('configHash from the immediately preceding read_config result; null only if no config exists'), })) .handler(async ({ operations, baseConfigHash, }) => { const parsedOps = (0, agent_json_config_1.tryParseConfigJson)(operations); if (!parsedOps.ok) { return { ok: false, stage: 'parse', errors: parsedOps.errors }; } let snapshot; try { snapshot = await this.getConfigSnapshot(agentId, projectId); } catch (e) { return { ok: false, stage: 'stale', errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }], }; } if (baseConfigHash !== snapshot.configHash) { return { ok: false, stage: 'stale', errors: [STALE_CONFIG_ERROR], ...snapshot }; } if (!snapshot.config) { return { ok: false, stage: 'patch', errors: [{ path: '(root)', message: 'Agent has no JSON config yet.' }], }; } const jsonpatch = (await Promise.resolve().then(() => __importStar(require('fast-json-patch')))).default; const ops = parsedOps.data; const patchError = jsonpatch.validate(ops, snapshot.config); if (patchError) { const opPath = patchError.operation?.path ?? '(root)'; return { ok: false, stage: 'patch', errors: [{ path: opPath, message: patchError.message ?? 'Invalid patch operation' }], }; } const patched = jsonpatch.applyPatch(jsonpatch.deepClone(snapshot.config), ops) .newDocument; const zodResult = agent_json_config_1.AgentJsonConfigSchema.safeParse(patched); if (!zodResult.success) { return { ok: false, stage: 'schema', errors: (0, agent_json_config_1.formatZodErrors)(zodResult.error) }; } const emptyInstructions = rejectIfEmptyInstructions(zodResult.data); if (emptyInstructions) { return { ok: false, stage: 'schema', errors: emptyInstructions.errors }; } try { const result = await this.agentsService.updateConfig(agentId, projectId, zodResult.data); return { ok: true, ...snapshotFromConfig(result.config, result.updatedAt, result.versionId), }; } catch (e) { return { ok: false, stage: 'schema', errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }], }; } }) .build(); const listIntegrationTypesTool = new agents_1.Tool(builder_tool_names_1.BUILDER_TOOLS.LIST_INTEGRATION_TYPES) .description("List trigger / integration types that can be added to the agent's `integrations` array. " + 'Returns the schedule trigger plus every connected chat platform with the list of ' + 'credential types it supports (`credentialTypes: string[]`). ' + 'Call this BEFORE asking the user for a credential. Then pick ONE entry from the ' + 'returned `credentialTypes` (prefer the OAuth variant if present, e.g. `slackOAuth2Api` ' + 'over `slackApi`) and pass it to `ask_credential` as the singular `credentialType` arg.') .input(zod_1.z.object({})) .handler(async () => { const chat = this.agentsService.listChatIntegrations(); return [ { type: 'schedule', label: 'Schedule', icon: 'clock', credentialTypes: [] }, ...chat, ]; }) .build(); const modelLookup = { list: async (credentialId, credentialType, lookup) => await this.builderModelLookupService.list(user, credentialId, credentialType, lookup), }; return [ readConfigTool, writeConfigTool, patchConfigTool, listIntegrationTypesTool, (0, interactive_1.buildResolveLlmTool)({ credentialProvider, modelLookup }), (0, interactive_1.buildAskCredentialTool)({ credentialProvider }), (0, interactive_1.buildAskLlmTool)(), (0, interactive_1.buildAskQuestionTool)(), ]; } getSharedTools(agentId, projectId, credentialProvider) { const buildCustomToolTool = new agents_1.Tool(builder_tool_names_1.BUILDER_TOOLS.BUILD_CUSTOM_TOOL) .description('Compile and store a custom tool. Pass the complete TypeScript source ' + 'using `export default new Tool(...)` builder chain. The code is validated in a ' + 'sandbox and saved against the agent, but this does NOT register the tool in the ' + 'agent config — follow up with patch_config (or write_config) to add a ' + '`{ type: "custom", id }` entry to `tools` so the agent actually uses it. ' + 'Returns { ok: true, id, descriptor } or { ok: false, errors }.') .input(zod_1.z.object({ code: zod_1.z .string() .describe('Complete TypeScript source using export default new Tool(...)'), })) .handler(async ({ code }) => { try { const descriptor = await this.secureRuntime.describeToolSecurely(code); const built = await this.agentsService.buildCustomTool(agentId, projectId, code, descriptor); return { ok: true, id: built.id, descriptor }; } catch (e) { return { ok: false, errors: [{ message: e instanceof Error ? e.message : String(e) }], }; } }) .build(); const createSkillTool = new agents_1.Tool(builder_tool_names_1.BUILDER_TOOLS.CREATE_SKILL) .description('Create and store an agent skill. Pass the skill name, a short description, and the full skill body. ' + 'The description should help the runtime decide when to load it. ' + 'The body is stored as the skill instructions, but this does NOT attach the skill to the agent config. ' + 'Follow up with read_config and patch_config (or write_config) to add a `{ type: "skill", id }` entry to `skills`. ' + 'Returns { ok: true, id, skill } or { ok: false, errors }.') .input(zod_1.z.object({ name: api_types_1.agentSkillSchema.shape.name.describe('Human-readable skill name'), description: api_types_1.agentSkillSchema.shape.description.describe('Short description of when to load the skill.'), body: api_types_1.agentSkillSchema.shape.instructions.describe('Full skill instructions/body'), })) .handler(async ({ name, description, body, }) => { const skill = { name, description, instructions: body }; const validation = api_types_1.agentSkillSchema.safeParse(skill); if (!validation.success) { return { ok: false, errors: (0, agent_json_config_1.formatZodErrors)(validation.error) }; } try { const created = await this.agentsService.createSkill(agentId, projectId, skill); return { ok: true, id: created.id, skill: created.skill }; } catch (e) { return { ok: false, errors: [{ message: e instanceof Error ? e.message : String(e) }], }; } }) .build(); const listWorkflowsTool = new agents_1.Tool('list_workflows') .description('List the n8n workflows that can be attached as tools via `type: "workflow"` in the agent config. ' + 'ALWAYS call this at the start — workflows are the preferred way to give agents real capabilities ' + '(sending emails, creating calendar events, querying databases, calling APIs, etc.). ' + 'Only returns workflows with supported trigger types.') .input(zod_1.z.object({})) .handler(async () => { const workflows = await this.workflowRepository.find({ select: ['id', 'name', 'nodes', 'active', 'updatedAt'], where: { shared: { projectId } }, relations: ['shared'], order: { updatedAt: 'DESC' }, take: 100, }); const SUPPORTED_TRIGGERS = { 'n8n-nodes-base.manualTrigger': 'manual', 'n8n-nodes-base.executeWorkflowTrigger': 'executeWorkflow', 'n8n-nodes-base.chatTrigger': 'chat', 'n8n-nodes-base.scheduleTrigger': 'schedule', 'n8n-nodes-base.formTrigger': 'form', }; const compatible = workflows .map((w) => { const triggerNode = (w.nodes ?? []).find((n) => SUPPORTED_TRIGGERS[n.type]); if (!triggerNode) return null; return { name: w.name, active: w.active, triggerType: SUPPORTED_TRIGGERS[triggerNode.type], }; }) .filter(Boolean); return { workflows: compatible }; }) .build(); return [ buildCustomToolTool, createSkillTool, listWorkflowsTool, ...this.agentsToolsService.getSharedTools(credentialProvider, 'Read-only inspection of available credentials. Use ask_credential to let the user ' + 'pick the credential to wire into a node tool — never copy ids from this list directly ' + 'into the config.'), ]; } async getConfigSnapshot(agentId, projectId) { const agent = await this.agentsService.findById(agentId, projectId); if (!agent) throw new Error('Agent not found'); const config = (0, agent_config_composition_1.composeJsonConfig)(agent); return snapshotFromConfig(config, agent.updatedAt.toISOString(), agent.versionId); } }; exports.AgentsBuilderToolsService = AgentsBuilderToolsService; exports.AgentsBuilderToolsService = AgentsBuilderToolsService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [agents_service_1.AgentsService, agent_secure_runtime_1.AgentSecureRuntime, db_1.WorkflowRepository, agents_tools_service_1.AgentsToolsService, builder_model_lookup_service_1.BuilderModelLookupService]) ], AgentsBuilderToolsService); //# sourceMappingURL=agents-builder-tools.service.js.map