diff --git a/scripts/n8n-kickstart.ts b/scripts/n8n-kickstart.ts index 942e794..39531c4 100644 --- a/scripts/n8n-kickstart.ts +++ b/scripts/n8n-kickstart.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as path from "path"; +import * as readline from "readline"; // Helper to manually load .env file function loadEnv() { @@ -23,7 +24,6 @@ loadEnv(); const N8N_HOST = process.env.N8N_HOST || "https://n8n.gaboggamer.online"; const N8N_API_KEY = process.env.N8N_API_KEY; -const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY; const EMAIL_USER = process.env.EMAIL_USER; const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD; const EMAIL_HOST = process.env.EMAIL_HOST; @@ -36,6 +36,21 @@ if (!N8N_API_KEY) { process.exit(1); } +// Interactive prompt helper +function askQuestion(query: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (ans) => { + rl.close(); + resolve(ans.trim()); + }) + ); +} + // n8n request helper async function n8nRequest(endpoint: string, method: string = "GET", body?: any) { const response = await fetch(`${N8N_HOST}${endpoint}`, { @@ -77,8 +92,140 @@ async function getOrCreateCredential(name: string, type: string, data: any) { } } +// LLM provider node configuration details +interface ProviderConfig { + nodeType: string; + credentialType: string; + credentialData: any; + nodeParameters: any; +} + +function getProviderConfig(provider: string, apiKey: string, modelName: string): ProviderConfig { + switch (provider) { + case "1": // Deepseek (Native) + return { + nodeType: "@n8n/n8n-nodes-langchain.lmChatDeepSeek", + credentialType: "deepSeekApi", + credentialData: { + apiKey, + allowedHttpRequestDomains: "none", + }, + nodeParameters: { + model: modelName || "deepseek-chat", + options: {}, + }, + }; + case "2": // OpenAI + return { + nodeType: "@n8n/n8n-nodes-langchain.lmChatOpenAi", + credentialType: "openAiApi", + credentialData: { + apiKey, + header: false, + allowedHttpRequestDomains: "none", + }, + nodeParameters: { + model: modelName || "gpt-4o-mini", + options: {}, + }, + }; + case "3": // Google Gemini + return { + nodeType: "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", + credentialType: "googlePalmApi", + credentialData: { + apiKey, + host: "https://generativelanguage.googleapis.com", + allowedHttpRequestDomains: "none", + }, + nodeParameters: { + model: modelName || "gemini-1.5-flash", + options: {}, + }, + }; + case "4": // Anthropic + return { + nodeType: "@n8n/n8n-nodes-langchain.lmChatAnthropic", + credentialType: "anthropicApi", + credentialData: { + apiKey, + allowedHttpRequestDomains: "none", + }, + nodeParameters: { + model: modelName || "claude-3-5-sonnet-latest", + options: {}, + }, + }; + default: + throw new Error("Invalid provider chosen"); + } +} + async function main() { - console.log("Kickstarting n8n configuration with native integrations..."); + console.log("\n=================================================="); + console.log("Welcome to n8n Kickstart Interactive Setup"); + console.log("=================================================="); + + // 1. Ask for Primary Provider + console.log("\nSelect Primary LLM Provider:"); + console.log("1. Deepseek (Native Node)"); + console.log("2. OpenAI (Standard)"); + console.log("3. Google Gemini"); + console.log("4. Anthropic"); + const primaryProviderChoice = await askQuestion("Enter choice (1-4) [default: 1]: ") || "1"; + + let defaultModel = "deepseek-chat"; + if (primaryProviderChoice === "2") defaultModel = "gpt-4o-mini"; + else if (primaryProviderChoice === "3") defaultModel = "gemini-1.5-flash"; + else if (primaryProviderChoice === "4") defaultModel = "claude-3-5-sonnet-latest"; + + const primaryModelName = await askQuestion(`Enter primary model name [default: ${defaultModel}]: `) || defaultModel; + + let defaultKey = ""; + if (primaryProviderChoice === "1") defaultKey = process.env.DEEPSEEK_API_KEY || ""; + else if (primaryProviderChoice === "3") defaultKey = process.env.GEMINI_API_KEY || ""; + + const primaryApiKey = await askQuestion(`Enter API key [default: ${defaultKey ? "Loaded from .env" : "None"}]: `) || defaultKey; + if (!primaryApiKey) { + console.error("API Key is required."); + process.exit(1); + } + + // 2. Ask for Fallback Provider + const configureFallback = (await askQuestion("\nDo you want to configure a Fallback LLM Model? (y/n) [default: n]: ") || "n").toLowerCase() === "y"; + let fallbackProviderChoice = ""; + let fallbackModelName = ""; + let fallbackApiKey = ""; + + if (configureFallback) { + console.log("\nSelect Fallback LLM Provider:"); + console.log("1. Deepseek (Native Node)"); + console.log("2. OpenAI (Standard)"); + console.log("3. Google Gemini"); + console.log("4. Anthropic"); + fallbackProviderChoice = await askQuestion("Enter choice (1-4) [default: 3]: ") || "3"; + + let defaultFallbackModel = "gemini-1.5-flash"; + if (fallbackProviderChoice === "1") defaultFallbackModel = "deepseek-chat"; + else if (fallbackProviderChoice === "2") defaultFallbackModel = "gpt-4o-mini"; + else if (fallbackProviderChoice === "4") defaultFallbackModel = "claude-3-5-sonnet-latest"; + + fallbackModelName = await askQuestion(`Enter fallback model name [default: ${defaultFallbackModel}]: `) || defaultFallbackModel; + + let defaultFallbackKey = ""; + if (fallbackProviderChoice === "1") defaultFallbackKey = process.env.DEEPSEEK_API_KEY || ""; + else if (fallbackProviderChoice === "3") defaultFallbackKey = process.env.GEMINI_API_KEY || ""; + + fallbackApiKey = await askQuestion(`Enter fallback API key [default: ${defaultFallbackKey ? "Loaded from .env" : "None"}]: `) || defaultFallbackKey; + if (!fallbackApiKey) { + console.error("Fallback API Key is required when fallback is enabled."); + process.exit(1); + } + } + + console.log("\n=================================================="); + console.log("Setting up credentials on n8n server..."); + console.log("=================================================="); // 1. Manage IMAP Credentials const imapCredId = await getOrCreateCredential("Semillero2_IMAP", "imap", { @@ -89,158 +236,369 @@ async function main() { secure: true, }); - // 2. Manage OpenAI/Deepseek Credentials with explicit required validation flags - const openAiCredId = await getOrCreateCredential("Semillero2_OpenAI", "openAiApi", { - apiKey: DEEPSEEK_API_KEY, - url: "https://api.deepseek.com/v1", - header: false, - allowedHttpRequestDomains: "none", - }); - - // 3. Manage Supabase Credentials using modern keys structure (sb_secret_...) + // 2. Manage Supabase Credentials using modern keys structure const supabaseCredId = await getOrCreateCredential("Semillero2_Supabase", "supabaseApi", { host: SUPABASE_URL, serviceRole: SUPABASE_SECRET_KEY, allowedHttpRequestDomains: "none", }); - // 4. Define Workflow 3: Core AI Parsing Sub-workflow - console.log("Deploying Workflow 3: Core AI Parsing Sub-workflow (Native AI & Supabase)..."); - const workflow3Definition = { - name: "Semillero2: Core AI Parsing Sub-workflow", - settings: {}, - nodes: [ - { - parameters: {}, - id: "w3-trigger-id", - name: "Execute Workflow Trigger", - type: "n8n-nodes-base.executeWorkflowTrigger", - typeVersion: 1, - position: [250, 300], + // 3. Manage Primary Model Credentials + const primaryConfig = getProviderConfig(primaryProviderChoice, primaryApiKey, primaryModelName); + const primaryCredId = await getOrCreateCredential("Semillero2_Primary_Model", primaryConfig.credentialType, primaryConfig.credentialData); + + // 4. Manage Fallback Model Credentials (if enabled) + let fallbackConfig: ProviderConfig | null = null; + let fallbackCredId = ""; + if (configureFallback) { + fallbackConfig = getProviderConfig(fallbackProviderChoice, fallbackApiKey, fallbackModelName); + fallbackCredId = await getOrCreateCredential("Semillero2_Fallback_Model", fallbackConfig.credentialType, fallbackConfig.credentialData); + } + + // Define nodes dynamically + console.log("\nBuilding workflow configurations..."); + + const parserNode = { + parameters: { + jsonSchema: "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": {\n \"type\": \"string\"\n },\n \"classification\": {\n \"type\": \"string\",\n \"enum\": [\"Qualified\", \"Unqualified\", \"Review\"]\n },\n \"suggestions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"riskLevel\": {\n \"type\": \"string\",\n \"enum\": [\"Low\", \"Medium\", \"High\"]\n }\n },\n \"required\": [\"summary\", \"classification\", \"suggestions\", \"riskLevel\"]\n}", + }, + id: "w3-parser-primary-id", + name: "Structured Output Parser", + type: "@n8n/n8n-nodes-langchain.outputParserStructured", + typeVersion: 1, + position: [680, 480], + }; + + const primaryChainNode = { + parameters: { + promptType: "Define below", // Fixed casing as required! + text: "={{ $json.text }}", + systemMessage: "You are an AI recruitment assistant. Analyze the candidate's CV text. You MUST respond with a raw JSON object containing exactly these four keys:\n- summary: a brief profile summary.\n- classification: 'Qualified', 'Unqualified', or 'Review'.\n- suggestions: an array of recommendations for next steps.\n- riskLevel: 'Low', 'Medium', or 'High'.\n\nDo not include markdown code blocks or any text outside the JSON.", + }, + id: "w3-chain-primary-id", + name: "Basic LLM Chain (Primary)", + type: "@n8n/n8n-nodes-langchain.chainLlm", + typeVersion: 1.4, + position: [480, 300], + continueOnFail: configureFallback, // enable continue on fail only if fallback exists + }; + + const primaryModelNode = { + parameters: primaryConfig.nodeParameters, + id: "w3-model-primary-id", + name: "Primary Chat Model", + type: primaryConfig.nodeType, + typeVersion: 1, + position: [480, 480], + credentials: { + [primaryConfig.credentialType]: { + id: primaryCredId, + name: "Semillero2_Primary_Model", }, - { - parameters: { - promptType: "defineBelow", - text: "={{ $json.text }}", - systemMessage: "You are an AI recruitment assistant. Analyze the candidate's CV text. You MUST respond with a raw JSON object containing exactly these four keys:\n- summary: a brief profile summary.\n- classification: 'Qualified', 'Unqualified', or 'Review'.\n- suggestions: an array of recommendations for next steps.\n- riskLevel: 'Low', 'Medium', or 'High'.\n\nDo not include markdown code blocks or any text outside the JSON.", - }, - id: "w3-chain-id", - name: "Basic LLM Chain", - type: "@n8n/n8n-nodes-langchain.chainLlm", - typeVersion: 1.4, - position: [480, 300], - }, - { - parameters: { - model: "deepseek-chat", - options: { - baseURL: "https://api.deepseek.com/v1", - }, - }, - id: "w3-model-id", - name: "OpenAI Chat Model", - type: "@n8n/n8n-nodes-langchain.lmChatOpenAi", - typeVersion: 1, - position: [480, 480], - credentials: { - openAiApi: { - id: openAiCredId, - name: "Semillero2_OpenAI", - }, - }, - }, - { - parameters: { - jsCode: `const content = $input.first().json.response.text; -const cleanedText = content.replace(/\`\`\`json\\n?|\\n?\`\`\`/g, "").trim(); -const evaluation = JSON.parse(cleanedText); -return [{ + }, + }; + + const parsePrimaryNode = { + parameters: { + jsCode: `return [{ json: { name: $('Execute Workflow Trigger').item.json.name || "Unknown Candidate", email: $('Execute Workflow Trigger').item.json.email || "unknown@example.com", - summary: evaluation.summary, - seniority: evaluation.classification + summary: $input.first().json.summary, + seniority: $input.first().json.classification } }];`, - }, - id: "w3-parse-id", - name: "Parse LLM Response", - type: "n8n-nodes-base.code", - typeVersion: 2, - position: [700, 300], - }, - { - parameters: { - operation: "insert", - table: "candidates", - options: {}, - }, - id: "w3-supabase-id", - name: "Insert Candidate to Supabase", - type: "n8n-nodes-base.supabase", - typeVersion: 1, - position: [920, 300], - credentials: { - supabaseApi: { - id: supabaseCredId, - name: "Semillero2_Supabase", - }, - }, - }, - ], - connections: { - "Execute Workflow Trigger": { - main: [ - [ - { - node: "Basic LLM Chain", - type: "main", - index: 0, - }, - ], - ], - }, - "OpenAI Chat Model": { - ai_languageModel: [ - [ - { - node: "Basic LLM Chain", - type: "ai_languageModel", - index: 0, - }, - ], - ], - }, - "Basic LLM Chain": { - main: [ - [ - { - node: "Parse LLM Response", - type: "main", - index: 0, - }, - ], - ], - }, - "Parse LLM Response": { - main: [ - [ - { - node: "Insert Candidate to Supabase", - type: "main", - index: 0, - }, - ], - ], + }, + id: "w3-parse-primary-id", + name: "Parse Primary Response", + type: "n8n-nodes-base.code", + typeVersion: 2, + position: [900, 200], + }; + + const supabaseInsertNode = { + parameters: { + operation: "insert", + table: "candidates", + options: {}, + }, + id: "w3-supabase-id", + name: "Insert Candidate to Supabase", + type: "n8n-nodes-base.supabase", + typeVersion: 1, + position: [1150, 300], + credentials: { + supabaseApi: { + id: supabaseCredId, + name: "Semillero2_Supabase", }, }, }; + // Build the complete list of nodes and connections for Workflow 3 + const w3Nodes: any[] = [ + { + parameters: {}, + id: "w3-trigger-id", + name: "Execute Workflow Trigger", + type: "n8n-nodes-base.executeWorkflowTrigger", + typeVersion: 1, + position: [250, 300], + }, + primaryChainNode, + primaryModelNode, + parserNode, + supabaseInsertNode, + ]; + + const w3Connections: any = { + "Execute Workflow Trigger": { + main: [ + [ + { + node: "Basic LLM Chain (Primary)", + type: "main", + index: 0, + }, + ], + ], + }, + "Primary Chat Model": { + ai_languageModel: [ + [ + { + node: "Basic LLM Chain (Primary)", + type: "ai_languageModel", + index: 0, + }, + ], + ], + }, + "Structured Output Parser": { + outputParser: [ + [ + { + node: "Basic LLM Chain (Primary)", + type: "outputParser", + index: 0, + }, + ], + ], + }, + }; + + if (configureFallback && fallbackConfig) { + console.log("Configuring error handling and fallback route..."); + + const checkErrorNode = { + parameters: { + conditions: { + boolean: [ + { + value1: "={{ $json.hasOwnProperty('error') }}", + value2: true, + }, + ], + }, + }, + id: "w3-check-error-id", + name: "Check Primary Error", + type: "n8n-nodes-base.if", + typeVersion: 2.2, + position: [680, 200], + }; + + const fallbackChainNode = { + parameters: { + promptType: "Define below", + text: "={{ $('Execute Workflow Trigger').item.json.text }}", + systemMessage: "You are an AI recruitment assistant. Analyze the candidate's CV text. You MUST respond with a raw JSON object containing exactly these four keys:\n- summary: a brief profile summary.\n- classification: 'Qualified', 'Unqualified', or 'Review'.\n- suggestions: an array of recommendations for next steps.\n- riskLevel: 'Low', 'Medium', or 'High'.\n\nDo not include markdown code blocks or any text outside the JSON.", + }, + id: "w3-chain-fallback-id", + name: "Basic LLM Chain (Fallback)", + type: "@n8n/n8n-nodes-langchain.chainLlm", + typeVersion: 1.4, + position: [900, 400], + }; + + const fallbackModelNode = { + parameters: fallbackConfig.nodeParameters, + id: "w3-model-fallback-id", + name: "Fallback Chat Model", + type: fallbackConfig.nodeType, + typeVersion: 1, + position: [900, 580], + credentials: { + [fallbackConfig.credentialType]: { + id: fallbackCredId, + name: "Semillero2_Fallback_Model", + }, + }, + }; + + const parserFallbackNode = { + parameters: { + jsonSchema: "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": {\n \"type\": \"string\"\n },\n \"classification\": {\n \"type\": \"string\",\n \"enum\": [\"Qualified\", \"Unqualified\", \"Review\"]\n },\n \"suggestions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"riskLevel\": {\n \"type\": \"string\",\n \"enum\": [\"Low\", \"Medium\", \"High\"]\n }\n },\n \"required\": [\"summary\", \"classification\", \"suggestions\", \"riskLevel\"]\n}", + }, + id: "w3-parser-fallback-id", + name: "Structured Output Parser (Fallback)", + type: "@n8n/n8n-nodes-langchain.outputParserStructured", + typeVersion: 1, + position: [1050, 580], + }; + + const parseFallbackNode = { + parameters: { + jsCode: `return [{ + json: { + name: $('Execute Workflow Trigger').item.json.name || "Unknown Candidate", + email: $('Execute Workflow Trigger').item.json.email || "unknown@example.com", + summary: $input.first().json.summary, + seniority: $input.first().json.classification + } +}];`, + }, + id: "w3-parse-fallback-id", + name: "Parse Fallback Response", + type: "n8n-nodes-base.code", + typeVersion: 2, + position: [1120, 400], + }; + + w3Nodes.push(checkErrorNode, parsePrimaryNode, fallbackChainNode, fallbackModelNode, parserFallbackNode, parseFallbackNode); + + // Wire fallback connections + w3Connections["Basic LLM Chain (Primary)"] = { + main: [ + [ + { + node: "Check Primary Error", + type: "main", + index: 0, + }, + ], + ], + }; + + w3Connections["Check Primary Error"] = { + main: [ + [ + { + node: "Basic LLM Chain (Fallback)", + type: "main", + index: 0, + }, + ], // Output 0 (true -> Error happened) + [ + { + node: "Parse Primary Response", + type: "main", + index: 0, + }, + ], // Output 1 (false -> Success) + ], + }; + + w3Connections["Fallback Chat Model"] = { + ai_languageModel: [ + [ + { + node: "Basic LLM Chain (Fallback)", + type: "ai_languageModel", + index: 0, + }, + ], + ], + }; + + w3Connections["Structured Output Parser (Fallback)"] = { + outputParser: [ + [ + { + node: "Basic LLM Chain (Fallback)", + type: "outputParser", + index: 0, + }, + ], + ], + }; + + w3Connections["Basic LLM Chain (Fallback)"] = { + main: [ + [ + { + node: "Parse Fallback Response", + type: "main", + index: 0, + }, + ], + ], + }; + + w3Connections["Parse Primary Response"] = { + main: [ + [ + { + node: "Insert Candidate to Supabase", + type: "main", + index: 0, + }, + ], + ], + }; + + w3Connections["Parse Fallback Response"] = { + main: [ + [ + { + node: "Insert Candidate to Supabase", + type: "main", + index: 0, + }, + ], + ], + }; + } else { + // If no fallback, wire directly Primary Chain -> Parse Primary Response -> Supabase + w3Nodes.push(parsePrimaryNode); + w3Connections["Basic LLM Chain (Primary)"] = { + main: [ + [ + { + node: "Parse Primary Response", + type: "main", + index: 0, + }, + ], + ], + }; + w3Connections["Parse Primary Response"] = { + main: [ + [ + { + node: "Insert Candidate to Supabase", + type: "main", + index: 0, + }, + ], + ], + }; + } + + // 5. Deploy Workflow 3 + console.log("\nDeploying Workflow 3: Core AI Parsing Sub-workflow..."); + const workflow3Definition = { + name: "Semillero2: Core AI Parsing Sub-workflow", + settings: {}, + nodes: w3Nodes, + connections: w3Connections, + }; + const w3Result = await n8nRequest("/api/v1/workflows", "POST", workflow3Definition); const w3Id = w3Result.id; console.log(`Workflow 3 deployed successfully (ID: ${w3Id})`); - // 5. Define Workflow 1: The API Gateway (Web UI Ingestion) + // 6. Define Workflow 1: The API Gateway (Web UI Ingestion) console.log("Deploying Workflow 1: The API Gateway..."); const workflow1Definition = { name: "Semillero2: API Gateway (Web UI Ingestion)", @@ -345,7 +703,7 @@ return [{ console.log(`Workflow 1 deployed successfully (ID: ${w1Id})`); console.log(`Webhook URL: ${webhookPath}`); - // 6. Define Workflow 2: Email Ingestion Listener + // 7. Define Workflow 2: Email Ingestion Listener console.log("Deploying Workflow 2: Email Ingestion Listener..."); const workflow2Definition = { name: "Semillero2: Email Ingestion Listener",