feat: use integrated LLM chain and Supabase nodes with modern API key structure
This commit is contained in:
parent
248b16a66a
commit
c52417a113
3 changed files with 107 additions and 81 deletions
|
|
@ -11,3 +11,7 @@ ARCHITECTURAL RULE: Next.js is the presentation layer, authentication provider,
|
||||||
|
|
||||||
## Database Rule
|
## Database Rule
|
||||||
DATABASE RULE: All database schemas must be written for Supabase PostgreSQL and include pgvector.
|
DATABASE RULE: All database schemas must be written for Supabase PostgreSQL and include pgvector.
|
||||||
|
DATABASE API KEYS RULE: Use Supabase's modern 'Publishable and secrets API keys' structure (using prefix sb_publishable_ for public client-side access and sb_secret_ for backend service access) instead of legacy anon/service_role keys.
|
||||||
|
|
||||||
|
## Orchestration Rule
|
||||||
|
ORCHESTRATION INTEGRATIONS RULE: Always use n8n's native/integrated nodes for LLM Logic (Basic LLM Chain connected to OpenAI Chat Model) and database operations (Supabase node) rather than raw HTTP requests.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
|
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
SUPABASE_PUBLISHABLE_KEY=sb_publishable_your_publishable_key
|
||||||
|
SUPABASE_SECRET_KEY=sb_secret_your_secret_key
|
||||||
NEXT_PUBLIC_N8N_WEBHOOK_URL=your_n8n_webhook_url
|
NEXT_PUBLIC_N8N_WEBHOOK_URL=your_n8n_webhook_url
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD;
|
||||||
const EMAIL_HOST = process.env.EMAIL_HOST;
|
const EMAIL_HOST = process.env.EMAIL_HOST;
|
||||||
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT || "465", 10);
|
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT || "465", 10);
|
||||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
const SUPABASE_SECRET_KEY = process.env.SUPABASE_SECRET_KEY;
|
||||||
|
|
||||||
if (!N8N_API_KEY) {
|
if (!N8N_API_KEY) {
|
||||||
console.error("Error: N8N_API_KEY is not defined in .env file.");
|
console.error("Error: N8N_API_KEY is not defined in .env file.");
|
||||||
|
|
@ -55,40 +55,57 @@ async function n8nRequest(endpoint: string, method: string = "GET", body?: any)
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function getOrCreateCredential(name: string, type: string, data: any) {
|
||||||
console.log("Kickstarting n8n configuration...");
|
|
||||||
|
|
||||||
// 1. Manage IMAP Credentials
|
|
||||||
console.log("Setting up IMAP credentials...");
|
|
||||||
let credentialId = "";
|
|
||||||
try {
|
try {
|
||||||
const credsList = await n8nRequest("/api/v1/credentials");
|
const credsList = await n8nRequest("/api/v1/credentials");
|
||||||
const existingCred = credsList.data.find((c: any) => c.name === "Semillero2_IMAP" && c.type === "imap");
|
const existingCred = credsList.data.find((c: any) => c.name === name && c.type === type);
|
||||||
if (existingCred) {
|
if (existingCred) {
|
||||||
credentialId = existingCred.id;
|
console.log(`Reusing existing credential: ${name} (ID: ${existingCred.id})`);
|
||||||
console.log(`Reusing existing IMAP credentials (ID: ${credentialId})`);
|
return existingCred.id;
|
||||||
} else {
|
} else {
|
||||||
const newCred = await n8nRequest("/api/v1/credentials", "POST", {
|
const newCred = await n8nRequest("/api/v1/credentials", "POST", {
|
||||||
name: "Semillero2_IMAP",
|
name,
|
||||||
type: "imap",
|
type,
|
||||||
data: {
|
data,
|
||||||
host: EMAIL_HOST,
|
|
||||||
port: EMAIL_PORT,
|
|
||||||
user: EMAIL_USER,
|
|
||||||
password: EMAIL_PASSWORD,
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
credentialId = newCred.id;
|
console.log(`Created new credential: ${name} (ID: ${newCred.id})`);
|
||||||
console.log(`Created new IMAP credentials (ID: ${credentialId})`);
|
return newCred.id;
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Error setting up IMAP credentials:", err.message);
|
console.error(`Error setting up credential ${name}:`, err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Define Workflow 3: Core AI Parsing Sub-workflow
|
async function main() {
|
||||||
console.log("Deploying Workflow 3: Core AI Parsing Sub-workflow...");
|
console.log("Kickstarting n8n configuration with native integrations...");
|
||||||
|
|
||||||
|
// 1. Manage IMAP Credentials
|
||||||
|
const imapCredId = await getOrCreateCredential("Semillero2_IMAP", "imap", {
|
||||||
|
host: EMAIL_HOST,
|
||||||
|
port: EMAIL_PORT,
|
||||||
|
user: EMAIL_USER,
|
||||||
|
password: EMAIL_PASSWORD,
|
||||||
|
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_...)
|
||||||
|
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 = {
|
const workflow3Definition = {
|
||||||
name: "Semillero2: Core AI Parsing Sub-workflow",
|
name: "Semillero2: Core AI Parsing Sub-workflow",
|
||||||
settings: {},
|
settings: {},
|
||||||
|
|
@ -103,79 +120,72 @@ async function main() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
parameters: {
|
parameters: {
|
||||||
method: "POST",
|
promptType: "defineBelow",
|
||||||
url: "https://api.deepseek.com/chat/completions",
|
text: "={{ $json.text }}",
|
||||||
sendHeaders: true,
|
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.",
|
||||||
headerParameters: {
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "Authorization",
|
|
||||||
value: `Bearer ${DEEPSEEK_API_KEY}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
sendBody: true,
|
|
||||||
specifyBody: "json",
|
|
||||||
jsonBody: "={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"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.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $json.text }}\"\n }\n ],\n \"response_format\": { \"type\": \"json_object\" }\n}",
|
|
||||||
},
|
},
|
||||||
id: "w3-deepseek-id",
|
id: "w3-chain-id",
|
||||||
name: "Deepseek LLM Parsing",
|
name: "Basic LLM Chain",
|
||||||
type: "n8n-nodes-base.httpRequest",
|
type: "@n8n/n8n-nodes-langchain.chainLlm",
|
||||||
typeVersion: 4.2,
|
typeVersion: 1.4,
|
||||||
position: [480, 300],
|
position: [480, 300],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
parameters: {
|
parameters: {
|
||||||
jsCode: `const content = $input.first().json.choices[0].message.content;
|
model: "deepseek-chat",
|
||||||
const evaluation = JSON.parse(content);
|
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 [{
|
return [{
|
||||||
json: {
|
json: {
|
||||||
name: $('Execute Workflow Trigger').item.json.name || "Unknown Candidate",
|
name: $('Execute Workflow Trigger').item.json.name || "Unknown Candidate",
|
||||||
email: $('Execute Workflow Trigger').item.json.email || "unknown@example.com",
|
email: $('Execute Workflow Trigger').item.json.email || "unknown@example.com",
|
||||||
summary: evaluation.summary,
|
summary: evaluation.summary,
|
||||||
classification: evaluation.classification,
|
seniority: evaluation.classification
|
||||||
suggestions: evaluation.suggestions,
|
|
||||||
riskLevel: evaluation.riskLevel,
|
|
||||||
evaluation: evaluation
|
|
||||||
}
|
}
|
||||||
}];`,
|
}];`,
|
||||||
},
|
},
|
||||||
id: "w3-parse-id",
|
id: "w3-parse-id",
|
||||||
name: "Parse Deepseek Response",
|
name: "Parse LLM Response",
|
||||||
type: "n8n-nodes-base.code",
|
type: "n8n-nodes-base.code",
|
||||||
typeVersion: 2,
|
typeVersion: 2,
|
||||||
position: [700, 300],
|
position: [700, 300],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
parameters: {
|
parameters: {
|
||||||
method: "POST",
|
operation: "insert",
|
||||||
url: `${SUPABASE_URL}/rest/v1/candidates`,
|
table: "candidates",
|
||||||
sendHeaders: true,
|
options: {},
|
||||||
headerParameters: {
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "apikey",
|
|
||||||
value: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Authorization",
|
|
||||||
value: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Prefer",
|
|
||||||
value: "return=representation",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
sendBody: true,
|
|
||||||
specifyBody: "json",
|
|
||||||
jsonBody: "={\n \"name\": \"{{ $json.name }}\",\n \"email\": \"{{ $json.email }}\",\n \"summary\": \"{{ $json.summary }}\",\n \"seniority\": \"{{ $json.classification }}\"\n}",
|
|
||||||
},
|
},
|
||||||
id: "w3-supabase-id",
|
id: "w3-supabase-id",
|
||||||
name: "Insert Candidate to Supabase",
|
name: "Insert Candidate to Supabase",
|
||||||
type: "n8n-nodes-base.httpRequest",
|
type: "n8n-nodes-base.supabase",
|
||||||
typeVersion: 4.2,
|
typeVersion: 1,
|
||||||
position: [920, 300],
|
position: [920, 300],
|
||||||
|
credentials: {
|
||||||
|
supabaseApi: {
|
||||||
|
id: supabaseCredId,
|
||||||
|
name: "Semillero2_Supabase",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
connections: {
|
connections: {
|
||||||
|
|
@ -183,25 +193,36 @@ return [{
|
||||||
main: [
|
main: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
node: "Deepseek LLM Parsing",
|
node: "Basic LLM Chain",
|
||||||
type: "main",
|
type: "main",
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"Deepseek LLM Parsing": {
|
"OpenAI Chat Model": {
|
||||||
|
ai_languageModel: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: "Basic LLM Chain",
|
||||||
|
type: "ai_languageModel",
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Basic LLM Chain": {
|
||||||
main: [
|
main: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
node: "Parse Deepseek Response",
|
node: "Parse LLM Response",
|
||||||
type: "main",
|
type: "main",
|
||||||
index: 0,
|
index: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"Parse Deepseek Response": {
|
"Parse LLM Response": {
|
||||||
main: [
|
main: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -219,7 +240,7 @@ return [{
|
||||||
const w3Id = w3Result.id;
|
const w3Id = w3Result.id;
|
||||||
console.log(`Workflow 3 deployed successfully (ID: ${w3Id})`);
|
console.log(`Workflow 3 deployed successfully (ID: ${w3Id})`);
|
||||||
|
|
||||||
// 3. Define Workflow 1: The API Gateway (Web UI Ingestion)
|
// 5. Define Workflow 1: The API Gateway (Web UI Ingestion)
|
||||||
console.log("Deploying Workflow 1: The API Gateway...");
|
console.log("Deploying Workflow 1: The API Gateway...");
|
||||||
const workflow1Definition = {
|
const workflow1Definition = {
|
||||||
name: "Semillero2: API Gateway (Web UI Ingestion)",
|
name: "Semillero2: API Gateway (Web UI Ingestion)",
|
||||||
|
|
@ -324,7 +345,7 @@ return [{
|
||||||
console.log(`Workflow 1 deployed successfully (ID: ${w1Id})`);
|
console.log(`Workflow 1 deployed successfully (ID: ${w1Id})`);
|
||||||
console.log(`Webhook URL: ${webhookPath}`);
|
console.log(`Webhook URL: ${webhookPath}`);
|
||||||
|
|
||||||
// 4. Define Workflow 2: Email Ingestion Listener
|
// 6. Define Workflow 2: Email Ingestion Listener
|
||||||
console.log("Deploying Workflow 2: Email Ingestion Listener...");
|
console.log("Deploying Workflow 2: Email Ingestion Listener...");
|
||||||
const workflow2Definition = {
|
const workflow2Definition = {
|
||||||
name: "Semillero2: Email Ingestion Listener",
|
name: "Semillero2: Email Ingestion Listener",
|
||||||
|
|
@ -346,7 +367,7 @@ return [{
|
||||||
position: [100, 300],
|
position: [100, 300],
|
||||||
credentials: {
|
credentials: {
|
||||||
imap: {
|
imap: {
|
||||||
id: credentialId,
|
id: imapCredId,
|
||||||
name: "Semillero2_IMAP",
|
name: "Semillero2_IMAP",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue