{ "name": "CV Parser Completo - Experiencia + Estudios + Skills", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "upload-cv-completo", "responseMode": "lastNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1824, 0 ], "id": "a4f36329-15d7-42f8-855e-41aadc252dcc", "name": "Upload CV", "webhookId": "7563f4a0-2bd3-4d24-8647-d8da39ec086f" }, { "parameters": { "method": "POST", "url": "http://10.12.152.223:8000/extract-text/", "sendBody": true, "contentType": "multipart-form-data", "bodyParameters": { "parameters": [ { "parameterType": "formBinaryData", "name": "file", "inputDataFieldName": "data" } ] }, "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ -1552, 0 ], "id": "61c685c9-312f-4404-9dc3-9976f374ce07", "name": "Extract Text Python" }, { "parameters": { "modelId": { "__rl": true, "value": "gemma4:e4b", "mode": "list", "cachedResultName": "gemma4:e4b" }, "messages": { "values": [ { "content": "=Extrae la siguiente información del texto del CV y devuelve ÚNICAMENTE un JSON válido sin formato adicional.\n\nREGLAS ESTRICTAS:\n- NO uses ```, ```json, ni ningún bloque de código\n- NO agregues texto antes ni después\n- Responde únicamente con el JSON, nada más\n\nTexto:\n{{ $json.text }}\n\nEstructura EXACTA:\n{\n \"nombre_completo\": string | null,\n \"perfil\": string | null,\n \"teléfono\": string | null,\n \"email\": string | null,\n \"habilidades\": string | null,\n \"experiencia_laboral\": string | null,\n \"linkedln\": string | null,\n \"estudios\": string | null,\n \"nombres\": string | null,\n \"apellidos\": string | null\n}" } ] }, "options": { "system": "Eres un sistema que devuelve JSON válido.\n\nReglas OBLIGATORIAS:\n- Devuelve SOLO un objeto JSON\n- El JSON debe tener UN SOLO objeto raíz\n- NO uses markdown\n- NO expliques nada\n- NO agregues texto antes ni después" } }, "type": "@n8n/n8n-nodes-langchain.ollama", "typeVersion": 1, "position": [ -1360, 0 ], "id": "cadd3f39-a648-4b95-9575-df73b7d64dbf", "name": "Extraction Ollama", "credentials": { "ollamaApi": { "id": "sh3bLzH2GiM17GoH", "name": "Ollama account 2" } } }, { "parameters": { "jsCode": "try {\nlet raw = $input.first().json.content || '';\nif (typeof raw === 'object') {\n raw = JSON.stringify(raw);\n}\nraw = raw.trim();\nraw = raw.replace(/```json\\s*/g, '').replace(/```/g, '').trim();\nconst firstBrace = raw.indexOf('{');\nif (firstBrace === -1) throw new Error();\nlet depth = 0;\nlet inString = false;\nlet escaped = false;\nlet end = -1;\nfor (let i = firstBrace; i < raw.length; i++) {\n const c = raw[i];\n if (inString) {\n if (escaped) { escaped = false; }\n else if (c === '\\\\') { escaped = true; }\n else if (c === '\"') { inString = false; }\n } else {\n if (c === '\"') { inString = true; }\n else if (c === '{') { depth++; }\n else if (c === '}') {\n depth--;\n if (depth === 0) { end = i + 1; break; }\n }\n }\n}\nif (end === -1) {\n raw = raw.slice(firstBrace) + '}'.repeat(Math.max(0, depth));\n} else {\n raw = raw.slice(firstBrace, end);\n}\nconst parsed = JSON.parse(raw);\nreturn [{ json: { output: parsed } }];\n} catch (e) {\nreturn [{ json: { output: { nombre_completo: null, perfil: null, teléfono: null, email: null, habilidades: null, experiencia_laboral: null, linkedln: null, estudios: null, nombres: null, apellidos: null } } }];\n}" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1072, 0 ], "id": "2b10d2fd-6c72-48b7-89e2-dddc4b591af4", "name": "Clean JSON Output" }, { "parameters": { "modelId": { "__rl": true, "value": "gemma4:e4b", "mode": "list", "cachedResultName": "gemma4:e4b" }, "messages": { "values": [ { "content": "=Convierte la siguiente experiencia laboral en JSON estructurado.\n\nIMPORTANTE:\n- TODAS las experiencias deben ir DENTRO del array experiencia_laboral\n- NO devuelvas objetos fuera de ese array\n- Debe existir UN SOLO objeto raíz\n- NO inventes información\n- Si no existe un dato usa null\n- actividades debe ser un array de strings\n\nTexto:\n{{ $json.output.experiencia_laboral }}\n\nEstructura EXACTA:\n\n{\n \"experiencia_laboral\": [\n {\n \"empresa\": string,\n \"cargo\": string,\n \"fecha_inicio\": string,\n \"fecha_fin\": string,\n \"actividades\": [string]\n }\n ]\n}" } ] }, "options": { "system": "Eres un sistema que devuelve JSON válido.\n\nReglas OBLIGATORIAS:\n- Devuelve SOLO un objeto JSON\n- El JSON debe tener UN SOLO objeto raíz\n- NO uses markdown\n- NO expliques nada\n- NO agregues texto antes ni después" } }, "type": "@n8n/n8n-nodes-langchain.ollama", "typeVersion": 1, "position": [ -928, -128 ], "id": "14778ed3-2451-4cf6-91a2-3de749fed119", "name": "Experiencia LLM", "credentials": { "ollamaApi": { "id": "sh3bLzH2GiM17GoH", "name": "Ollama account 2" } } }, { "parameters": { "jsCode": "try {\nlet raw = $input.first().json.content || '';\nraw = raw.trim();\nraw = raw.replace(/```json/g, '').replace(/```/g, '').trim();\nconst firstBrace = raw.indexOf('{');\nif (firstBrace === -1) throw new Error();\nlet depth = 0;\nlet inString = false;\nlet escaped = false;\nlet end = -1;\nfor (let i = firstBrace; i < raw.length; i++) {\n const c = raw[i];\n if (inString) {\n if (escaped) { escaped = false; }\n else if (c === '\\\\') { escaped = true; }\n else if (c === '\"') { inString = false; }\n } else {\n if (c === '\"') { inString = true; }\n else if (c === '{') { depth++; }\n else if (c === '}') {\n depth--;\n if (depth === 0) { end = i + 1; break; }\n }\n }\n}\nif (end === -1) {\n raw = raw.slice(firstBrace) + '}'.repeat(Math.max(0, depth));\n} else {\n raw = raw.slice(firstBrace, end);\n}\nconst parsed = JSON.parse(raw);\nif (!Array.isArray(parsed.experiencia_laboral)) {\n parsed.experiencia_laboral = [];\n}\nreturn [{ json: parsed }];\n} catch (e) {\nreturn [{ json: { experiencia_laboral: [] } }];\n}" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -608, -128 ], "id": "75c2ef39-6af7-4a9b-a952-cbac82530889", "name": "Parse Experiencia" }, { "parameters": { "modelId": { "__rl": true, "value": "gemma4:e4b", "mode": "list", "cachedResultName": "gemma4:e4b" }, "messages": { "values": [ { "content": "=Convierte el siguiente texto en estudios estructurados.\n\nIMPORTANTE:\n- Debe existir UN SOLO objeto raíz\n- NO inventes información\n- Si no existe un dato usa null\n- TODOS los estudios deben ir dentro del array titulos\n\nTexto:\n{{ $json.output.estudios }}\n\nEstructura EXACTA:\n\n{\n \"titulos\": [\n {\n \"nivel_educativo\": string,\n \"titulo\": string,\n \"fecha_culminacion\": string\n }\n ]\n}" } ] }, "options": { "system": "Eres un sistema que devuelve JSON válido.\n\nReglas OBLIGATORIAS:\n- Devuelve SOLO un objeto JSON\n- El JSON debe tener UN SOLO objeto raíz\n- NO uses markdown\n- NO expliques nada\n- NO agregues texto antes ni después" } }, "type": "@n8n/n8n-nodes-langchain.ollama", "typeVersion": 1, "position": [ -928, 16 ], "id": "9727bce8-9e8c-4d9a-b4b8-2b07f0a0336d", "name": "Estudios LLM", "credentials": { "ollamaApi": { "id": "sh3bLzH2GiM17GoH", "name": "Ollama account 2" } } }, { "parameters": { "jsCode": "try {\nlet raw = $input.first().json.content || '';\nraw = raw.trim();\nraw = raw.replace(/```json/g, '').replace(/```/g, '').trim();\nconst firstBrace = raw.indexOf('{');\nif (firstBrace === -1) throw new Error();\nlet depth = 0;\nlet inString = false;\nlet escaped = false;\nlet end = -1;\nfor (let i = firstBrace; i < raw.length; i++) {\n const c = raw[i];\n if (inString) {\n if (escaped) { escaped = false; }\n else if (c === '\\\\') { escaped = true; }\n else if (c === '\"') { inString = false; }\n } else {\n if (c === '\"') { inString = true; }\n else if (c === '{') { depth++; }\n else if (c === '}') {\n depth--;\n if (depth === 0) { end = i + 1; break; }\n }\n }\n}\nif (end === -1) {\n raw = raw.slice(firstBrace) + '}'.repeat(Math.max(0, depth));\n} else {\n raw = raw.slice(firstBrace, end);\n}\nconst parsed = JSON.parse(raw);\nif (!Array.isArray(parsed.titulos)) {\n parsed.titulos = [];\n}\nreturn [{ json: parsed }];\n} catch (e) {\nreturn [{ json: { titulos: [] } }];\n}" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -608, 16 ], "id": "821e45af-1776-4ba0-bdd3-c1468bb83960", "name": "Parse Estudios" }, { "parameters": { "modelId": { "__rl": true, "value": "gemma4:e4b", "mode": "list", "cachedResultName": "gemma4:e4b" }, "messages": { "values": [ { "content": "=Extrae las habilidades del siguiente texto como un JSON estructurado.\n\nIMPORTANTE:\n- Debe existir UN SOLO objeto raíz\n- NO inventes información\n- Si no existen habilidades devuelve un array vacío\n- Todas las habilidades deben ir dentro del array skills\n\nTexto:\n{{ $json.output.habilidades }}\n\nEstructura EXACTA:\n\n{\n \"skills\": [string]\n}" } ] }, "options": { "system": "Eres un sistema que devuelve JSON válido.\n\nReglas OBLIGATORIAS:\n- Devuelve SOLO un objeto JSON\n- El JSON debe tener UN SOLO objeto raíz\n- NO uses markdown\n- NO expliques nada\n- NO agregues texto antes ni después" } }, "type": "@n8n/n8n-nodes-langchain.ollama", "typeVersion": 1, "position": [ -928, 160 ], "id": "cba8f163-a79a-4644-a149-9173a3314c81", "name": "Skills LLM", "credentials": { "ollamaApi": { "id": "sh3bLzH2GiM17GoH", "name": "Ollama account 2" } } }, { "parameters": { "jsCode": "try {\nlet raw = $input.first().json.content || '';\nraw = raw.trim();\nraw = raw.replace(/```json/g, '').replace(/```/g, '').trim();\nconst firstBrace = raw.indexOf('{');\nif (firstBrace === -1) throw new Error();\nlet depth = 0;\nlet inString = false;\nlet escaped = false;\nlet end = -1;\nfor (let i = firstBrace; i < raw.length; i++) {\n const c = raw[i];\n if (inString) {\n if (escaped) { escaped = false; }\n else if (c === '\\\\') { escaped = true; }\n else if (c === '\"') { inString = false; }\n } else {\n if (c === '\"') { inString = true; }\n else if (c === '{') { depth++; }\n else if (c === '}') {\n depth--;\n if (depth === 0) { end = i + 1; break; }\n }\n }\n}\nif (end === -1) {\n raw = raw.slice(firstBrace) + '}'.repeat(Math.max(0, depth));\n} else {\n raw = raw.slice(firstBrace, end);\n}\nconst parsed = JSON.parse(raw);\nif (!Array.isArray(parsed.skills)) {\n parsed.skills = [];\n}\nreturn [{ json: parsed }];\n} catch (e) {\nreturn [{ json: { skills: [] } }];\n}" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -608, 160 ], "id": "7b096606-2e1b-4ef0-8e47-525c68a2758c", "name": "Parse Skills" }, { "parameters": { "jsCode": "const base = $('Clean JSON Output').first().json.output;\n\nlet experiencia = {};\nlet estudios = {};\nlet skills = {};\nfor (const input of $input.all()) {\n const data = input.json;\n if (data.experiencia_laboral !== undefined) {\n experiencia = data;\n } else if (data.titulos !== undefined) {\n estudios = data;\n } else if (data.skills !== undefined) {\n skills = data;\n }\n}\n\nlet nombres = base.nombres || null;\nlet apellidos = base.apellidos || null;\n\nif ((!nombres || !apellidos) && base.nombre_completo) {\n const parts = base.nombre_completo.trim().split(/\\s+/);\n if (parts.length >= 2) {\n nombres = parts.slice(0, -2).join(' ') || parts[0] || null;\n apellidos = parts.slice(-2).join(' ') || null;\n } else {\n nombres = base.nombre_completo;\n apellidos = null;\n }\n}\n\nreturn [\n {\n json: {\n aspirante: {\n nombres,\n apellidos,\n email: base.email || null,\n celular: base['teléfono'] || null,\n linkedin: base.linkedln || null,\n perfil: base.perfil || null,\n nombre_completo: base.nombre_completo || null\n },\n titulos: estudios.titulos || [],\n experiencia_laboral: experiencia.experiencia_laboral || [],\n skills: skills.skills || []\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -256, 16 ], "id": "c53e8efa-2580-44e7-abce-af0637d01afc", "name": "Final JSON" } ], "pinData": {}, "connections": { "Upload CV": { "main": [ [ { "node": "Extract Text Python", "type": "main", "index": 0 } ] ] }, "Extract Text Python": { "main": [ [ { "node": "Extraction Ollama", "type": "main", "index": 0 } ] ] }, "Extraction Ollama": { "main": [ [ { "node": "Clean JSON Output", "type": "main", "index": 0 } ] ] }, "Clean JSON Output": { "main": [ [ { "node": "Experiencia LLM", "type": "main", "index": 0 }, { "node": "Estudios LLM", "type": "main", "index": 0 }, { "node": "Skills LLM", "type": "main", "index": 0 } ] ] }, "Experiencia LLM": { "main": [ [ { "node": "Parse Experiencia", "type": "main", "index": 0 } ] ] }, "Estudios LLM": { "main": [ [ { "node": "Parse Estudios", "type": "main", "index": 0 } ] ] }, "Skills LLM": { "main": [ [ { "node": "Parse Skills", "type": "main", "index": 0 } ] ] }, "Parse Experiencia": { "main": [ [ { "node": "Final JSON", "type": "main", "index": 0 } ] ] }, "Parse Estudios": { "main": [ [ { "node": "Final JSON", "type": "main", "index": 0 } ] ] }, "Parse Skills": { "main": [ [ { "node": "Final JSON", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1" }, "versionId": "b5101a38-bc7d-42a7-bd4a-ef76a8cff507", "meta": { "instanceId": "c033503d00370ddf30aad0c1195ba31fe9ff2453e6cd31f043f20e143092248d" }, "id": "g4rV6UMoouIUChNf", "tags": [] }