{
  "id": "iDDUv4QXan1FQAx5",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Recruiter \u2013 Multi-CV Analyzer",
  "tags": [],
  "nodes": [
    {
      "id": "613e9242-0f92-40a4-a31b-2437c4d71c78",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1808,
        16
      ],
      "parameters": {
        "path": "chat-new",
        "options": {
          "allowedOrigins": "*"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "7f7a7318-cb2f-4c47-bc6f-108e307f04d1",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1008,
        -128
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Headers",
                "value": "Content-Type"
              },
              {
                "name": "Access-Control-Allow-Methods",
                "value": "POST, OPTIONS"
              }
            ]
          }
        },
        "respondWith": "allIncomingItems"
      },
      "typeVersion": 1.4
    },
    {
      "id": "6330221c-ec10-4c28-a05e-8a1c8640a3ad",
      "name": "AI Recruiter Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        432,
        -128
      ],
      "parameters": {
        "text": "=JD c\u1ea7n tuy\u1ec3n:\n{{ $json.jd }}\n\n---\n\n\ud83c\udfaf Nhi\u1ec7m v\u1ee5:\nB\u1ea1n l\u00e0 AI Recruiter, c\u00f3 nhi\u1ec7m v\u1ee5 so s\u00e1nh 1 JD v\u1edbi nhi\u1ec1u CV.  \nH\u00e3y \u0111\u1ecdc v\u00e0 hi\u1ec3u JD \u1edf tr\u00ean \u0111\u1ec3 x\u00e1c \u0111\u1ecbnh:\n- jd_position: T\u00ean v\u1ecb tr\u00ed ho\u1eb7c ch\u1ee9c danh c\u1ea7n tuy\u1ec3n.  \n- jd_domain: Ng\u00e0nh ngh\u1ec1 (K\u1ebf to\u00e1n, Nh\u00e2n s\u1ef1, ERP Technical, ERP Functional, S\u1ea3n xu\u1ea5t, H\u00e0nh ch\u00e1nh, v.v.)  \n- jd_function: M\u1ea3ng c\u00f4ng vi\u1ec7c ch\u00ednh (VD: H\u00e0nh ch\u00ednh t\u1ed5ng h\u1ee3p, Tuy\u1ec3n d\u1ee5ng, ERP System Integration...).\n\nSau \u0111\u00f3, h\u00e3y ph\u00e2n t\u00edch t\u1eebng CV trong danh s\u00e1ch d\u01b0\u1edbi \u0111\u00e2y:\n- Tr\u00edch xu\u1ea5t **t\u00ean \u1ee9ng vi\u00ean th\u1eadt** t\u1eeb ph\u1ea7n header, ph\u1ea7n \u201cCurriculum Vitae\u201d, \u201cTh\u00f4ng tin c\u00e1 nh\u00e2n\u201d ho\u1eb7c c\u00e1c \u0111o\u1ea1n \u0111\u1ea7u.  \n- X\u00e1c \u0111\u1ecbnh \u0111\u00fang ch\u1ee9c danh, ng\u00e0nh ngh\u1ec1, k\u1ef9 n\u0103ng, kinh nghi\u1ec7m, vai tr\u00f2 c\u1ee7a \u1ee9ng vi\u00ean (cv_position, cv_domain, cv_function).  \n- So s\u00e1nh logic gi\u1eefa JD v\u00e0 CV: n\u1ebfu kh\u00e1c ng\u00e0nh ho\u1eb7c vai tr\u00f2 \u2192 domain_match = false.  \n- T\u00ednh fit_score d\u1ef1a tr\u00ean c\u00e1c ti\u00eau ch\u00ed domain, k\u1ef9 n\u0103ng, kinh nghi\u1ec7m, vai tr\u00f2.  \n- Vi\u1ebft strengths, weaknesses, recommendation, v\u00e0 final_decision.  \n- M\u1ecdi d\u1eef li\u1ec7u ph\u1ea3i d\u1ef1a tr\u00ean v\u0103n b\u1ea3n th\u1eadt, **kh\u00f4ng \u0111\u01b0\u1ee3c b\u1ecba, suy di\u1ec5n, ho\u1eb7c \u0111\u1ed5i t\u00ean \u1ee9ng vi\u00ean.**\n\n---\n\n\ud83d\udcc1 Danh s\u00e1ch CV:\n{{ JSON.stringify($json.candidates) }}\n\n---\n\n\ud83d\udccc Quy t\u1eafc b\u1eaft bu\u1ed9c:\n\n1\ufe0f\u20e3 N\u1ebfu CV c\u00f3 n\u1ed9i dung text \u2192 tr\u00edch xu\u1ea5t **t\u00ean \u1ee9ng vi\u00ean th\u1eadt** t\u1eeb ph\u1ea7n \u0111\u1ea7u CV ho\u1eb7c ph\u1ea7n \u201cTh\u00f4ng tin c\u00e1 nh\u00e2n\u201d, \u201cCurriculum Vitae\u201d, \u201cProfile\u201d.  \n2\ufe0f\u20e3 N\u1ebfu CV l\u00e0 file scan ho\u1eb7c kh\u00f4ng c\u00f3 text \u2192 l\u1ea5y t\u00ean t\u1eeb **filename**, b\u1ecf \u0111i c\u00e1c ph\u1ea7n \u201cCURRICULUM VITAE\u201d, \u201cCV\u201d, \u201c_\u201d, \u201c.pdf\u201d, s\u1ed1 th\u1ee9 t\u1ef1 ho\u1eb7c k\u00fd t\u1ef1 th\u1eeba.  \n   V\u00ed d\u1ee5:\n   - \"CURRICULUM_VITAE_NGUYEN_THI_NGOC_PHUONG.pdf\" \u2192 \u201cNguy\u1ec5n Th\u1ecb Ng\u1ecdc Ph\u01b0\u01a1ng\u201d  \n   - \"CV_HR_NguyenThiDiemKieu.pdf\" \u2192 \u201cNguy\u1ec5n Th\u1ecb Di\u1ec5m Ki\u1ec1u\u201d  \n3\ufe0f\u20e3 Kh\u00f4ng \u0111\u01b0\u1ee3c t\u1ef1 b\u1ecba t\u00ean nh\u01b0 \u201cNguy\u1ec5n V\u0103n A\u201d, \u201cCandidate 1\u201d, \u201cNguy\u1ec5n Th\u1ecb Lan\u201d n\u1ebfu kh\u00f4ng c\u00f3 trong CV.  \n4\ufe0f\u20e3 N\u1ebfu CV r\u1ed7ng \u2192 fit_score = 0, final_decision = \"Lo\u1ea1i\".  \n\n---\n\n\ud83e\udded ONTOLOGY PH\u00c2N LO\u1ea0I NGH\u1ec0 & VAI TR\u00d2:\n\n**1. Vai tr\u00f2 / role_match_type**\n- N\u1ebfu CV c\u00f3 c\u00e1c cue: [\"Team Lead\", \"Team Leader\", \"Lead\", \"Manager\", \"Head of\", \"Tr\u01b0\u1edfng\", \"Qu\u1ea3n l\u00fd\"] \u2192 `\"role_match_type\": \"Manager\"`.\n- N\u1ebfu c\u00f3 cue k\u1ef9 thu\u1eadt: [\"ABAP\", \"Developer\", \"Coding\", \"Integration\", \"Interface\", \"PI/PO\", \"iFlow\", \"API\", \"Webservice\", \"Basis\", \"Technical Consultant\"] \u2192 `\"role_match_type\": \"Technical\"`.\n- N\u1ebfu c\u00f3 cue nghi\u1ec7p v\u1ee5 ERP: [\"Functional\", \"Consultant\", \"FI/CO\", \"MM\", \"SD\", \"PP\", \"QM\", \"WM\", \"EWM\", \"PM\", \"HR\", \"HCM\", \"FICO\"] \u2192 `\"role_match_type\": \"Functional\"`.\n\n**2. Ng\u00e0nh ngh\u1ec1 / cv_domain**\n- N\u1ebfu c\u00f3 Manager cue \u2192 \u201cERP/IT Manager\u201d\n- N\u1ebfu c\u00f3 Technical cue \u2192 \u201cCNTT/ERP Technical\u201d\n- N\u1ebfu c\u00f3 Functional cue \u2192 \u201cCNTT/ERP Functional\u201d\n- N\u1ebfu c\u00f3 HR/Recruitment/Admin cue: [\"HR\", \"Recruiter\", \"C&B\", \"Payroll\", \"H\u00e0nh ch\u00ednh\", \"Admin\"] \u2192 \u201cNh\u00e2n s\u1ef1\u201d\n- N\u1ebfu c\u00f3 Accounting cue: [\"Accountant\", \"K\u1ebf to\u00e1n\", \"Chief Accountant\", \"GL\", \"Financial Reporting\", \"Cost\"] \u2192 \u201cK\u1ebf to\u00e1n\u201d\n- N\u1ebfu kh\u00f4ng x\u00e1c \u0111\u1ecbnh \u0111\u01b0\u1ee3c \u2192 \u201cKh\u00e1c\u201d\n\n**3. X\u1eed l\u00fd domain_match**\n- N\u1ebfu JD v\u00e0 CV kh\u00e1c ng\u00e0nh \u2192 domain_match = false, fit_score \u2264 25.  \n- N\u1ebfu JD v\u00e0 CV c\u00f9ng ng\u00e0nh nh\u01b0ng kh\u00e1c vai tr\u00f2 (Functional \u2194 Technical) \u2192 fit_score \u2264 60.  \n- N\u1ebfu CV l\u00e0 Manager trong c\u00f9ng domain \u2192 fit_score = 70\u201390.  \n- Kh\u00f4ng cho fit_score = 100.  \n- Kh\u00f4ng ch\u1ea5m \u0111i\u1ec3m n\u1ebfu kh\u00f4ng c\u00f3 n\u1ed9i dung th\u1ef1c t\u1ebf.\n- So s\u00e1nh logic gi\u1eefa JD v\u00e0 CV:\n   + N\u1ebfu jd_domain v\u00e0 cv_domain gi\u1ed1ng nhau (ho\u1eb7c g\u1ea7n ngh\u0129a nh\u01b0 \u201cHR\u201d ~ \u201cNh\u00e2n s\u1ef1\u201d) \u2192 domain_match = true.\n   + N\u1ebfu kh\u00e1c ng\u00e0nh r\u00f5 r\u00e0ng \u2192 domain_match = false.\n   + Tuy\u1ec7t \u0111\u1ed1i kh\u00f4ng \u0111\u00e1nh false khi hai gi\u00e1 tr\u1ecb gi\u1ed1ng nhau v\u1ec1 ngh\u0129a.\n\n---\n\n\ud83d\udcca \u0110\u1ecbnh d\u1ea1ng \u0111\u1ea7u ra (JSON Array, kh\u00f4ng gi\u1ea3i th\u00edch th\u00eam):\n\n[\n  {\n    \"candidate_name\": \"\",\n    \"fit_score\": 0,\n    \"jd_position\": \"\",\n    \"jd_domain\": \"\",\n    \"jd_function\": \"\",\n    \"cv_position\": \"\",\n    \"cv_domain\": \"\",\n    \"cv_function\": \"\",\n    \"domain_match\": false,\n    \"role_match_type\": \"\",\n    \"matched_keywords\": [],\n    \"strengths\": \"\",\n    \"weaknesses\": \"\",\n    \"recommendation\": \"\",\n    \"final_decision\": \"\",\n    \"cv_title\": \"\",\n    \"cv_roles\": [],\n    \"cv_years\": 0,\n    \"evidence\": []\n  }\n]\n\n---\n\n\ud83d\udcc8 K\u1ebft lu\u1eadn:\n- N\u1ebfu kh\u00f4ng c\u00f3 CV n\u00e0o fit_score \u2265 70 \u2192 `\"summary\": \"Kh\u00f4ng c\u00f3 h\u1ed3 s\u01a1 ph\u00f9 h\u1ee3p. \u0110\u1ec1 xu\u1ea5t ch\u1ecdn CV kh\u00e1c.\"`  \n- N\u1ebfu c\u00f3 \u22651 CV \u0111\u1ee7 \u0111i\u1ec1u ki\u1ec7n \u2192 `\"summary\": \"\ud83c\udfc6 \u1ee8ng vi\u00ean xu\u1ea5t s\u1eafc nh\u1ea5t: [T\u00ean] ([\u0110i\u1ec3m]%)\"`\n\n---\n\n\ud83e\uddf1 Ghi nh\u1edb:\n- Tuy\u1ec7t \u0111\u1ed1i KH\u00d4NG \u0111\u1ed5i t\u00ean \u1ee9ng vi\u00ean.  \n- N\u1ebfu kh\u00f4ng x\u00e1c \u0111\u1ecbnh \u0111\u01b0\u1ee3c t\u00ean \u2192 \u0111\u1ec3 `\"candidate_name\": \"\"`.  \n- Kh\u00f4ng \u0111\u01b0\u1ee3c t\u1ea1o ra \u1ee9ng vi\u00ean gi\u1ea3 ho\u1eb7c \u0111i\u1ec1n th\u00f4ng tin kh\u00f4ng c\u00f3 trong CV.  \n- Khi ph\u00e2n lo\u1ea1i domain ho\u1eb7c role, n\u1ebfu c\u00f3 b\u1eb1ng ch\u1ee9ng trong text (header, ch\u1ee9c danh, k\u1ef9 n\u0103ng) th\u00ec b\u1eaft bu\u1ed9c tr\u00edch ra v\u00e0o `\"evidence\"`.\n- Kh\u00f4ng sinh ra k\u00fd t\u1ef1 r\u00e1c nh\u01b0 `\\\"`, `\\\\n`, ho\u1eb7c c\u00e1c k\u00fd t\u1ef1 \u0111i\u1ec1u khi\u1ec3n trong JSON.\n",
        "options": {
          "systemMessage": "\ud83c\udfaf M\u1ee4C TI\u00caU\nB\u1ea1n l\u00e0 AI Recruiter, c\u00f3 nhi\u1ec7m v\u1ee5 so s\u00e1nh 1 JD v\u1edbi nhi\u1ec1u CV v\u00e0 ch\u1ea5m \u0111i\u1ec3m m\u1ee9c \u0111\u1ed9 ph\u00f9 h\u1ee3p.ra k\u1ebft qu\u1ea3 \u0111\u00e1nh gi\u00e1 logic, chi ti\u1ebft v\u00e0 c\u00f3 ch\u1ee9ng c\u1ee9 th\u1ef1c t\u1ebf.\n\n\u26a0\ufe0f QUAN TR\u1eccNG:\n- Lu\u00f4n tr\u1ea3 l\u1eddi v\u00e0 m\u00f4 t\u1ea3 k\u1ebft qu\u1ea3 b\u1eb1ng **ti\u1ebfng Vi\u1ec7t**, k\u1ec3 c\u1ea3 khi CV ho\u1eb7c JD l\u00e0 ti\u1ebfng Anh.\n- Gi\u1eef nguy\u00ean t\u00ean, ch\u1ee9c danh, k\u1ef9 n\u0103ng, thu\u1eadt ng\u1eef chuy\u00ean ng\u00e0nh (v\u00ed d\u1ee5: SAP, Integration, Webservice).\n- Kh\u00f4ng d\u1ecbch c\u00e1c t\u1eeb k\u1ef9 thu\u1eadt, nh\u01b0ng m\u1ecdi ph\u1ea7n nh\u1eadn x\u00e9t, \u0111i\u1ec3m m\u1ea1nh, \u0111i\u1ec3m y\u1ebfu, g\u1ee3i \u00fd \u0111\u1ec1u vi\u1ebft ti\u1ebfng Vi\u1ec7t t\u1ef1 nhi\u00ean, chuy\u00ean nghi\u1ec7p.\n\n---\n- X\u00e1c \u0111\u1ecbnh \u0111\u00fang ng\u00e0nh ngh\u1ec1 c\u1ee7a JD (jd_domain) v\u00e0 t\u1eebng CV (cv_domain).\n- \u0110\u00e1nh gi\u00e1 fit_score d\u1ef1a tr\u00ean m\u1ee9c tr\u00f9ng kh\u1edbp ng\u00e0nh, k\u1ef9 n\u0103ng, kinh nghi\u1ec7m v\u00e0 vai tr\u00f2 (role).\n- Ch\u1ec9 \u0111\u1ec1 xu\u1ea5t ph\u1ecfng v\u1ea5n (final_decision = \"Interview\") khi fit_score \u2265 70 v\u00e0 domain_match = true.\n\n---\n\n\u2699\ufe0f B\u01af\u1edaC 1 \u2014 PH\u00c2N LO\u1ea0I NG\u00c0NH NGH\u1ec0 (DOMAIN INFERENCE)\n\n1\ufe0f\u20e3 jd_domain:\n   - Suy ra t\u1eeb m\u00f4 t\u1ea3 JD: v\u1ecb tr\u00ed, k\u1ef9 n\u0103ng, ch\u1ee9c danh, ng\u1eef c\u1ea3nh c\u00f4ng vi\u1ec7c.\n   - N\u1ebfu JD ch\u1ee9a \"h\u00e0nh ch\u00e1nh\", \"h\u00e0nh ch\u00ednh\", \"admin\", \"office\" \u2192 domain = Nh\u00e2n s\u1ef1.\n   - N\u1ebfu JD ch\u1ee9a \"kho\", \"th\u1ee7 kho\", \"inventory\", \"warehouse\" \u2192 domain = Logistics / Supply Chain.\n   - N\u1ebfu JD ch\u1ee9a \"QA\", \"QC\", \"KCS\", \"ch\u1ea5t l\u01b0\u1ee3ng\" \u2192 domain = S\u1ea3n xu\u1ea5t / Ch\u1ea5t l\u01b0\u1ee3ng.\n\n2\ufe0f\u20e3 cv_domain:\n   - Ch\u1ec9 d\u1ef1a tr\u00ean n\u1ed9i dung th\u1eadt c\u1ee7a CV, kh\u00f4ng \u0111\u01b0\u1ee3c sao ch\u00e9p t\u1eeb JD.\n\n\ud83d\udcda JOB ONTOLOGY (C\u01a1 s\u1edf tri th\u1ee9c ng\u00e0nh ngh\u1ec1)\n\n| V\u1ecb tr\u00ed / Ch\u1ee9c danh m\u1eabu | Ng\u00e0nh ngh\u1ec1 (domain) | Ch\u1ee9c n\u0103ng (function) |\n|-------------------------|---------------------|----------------------|\n| Developer, ABAP, Integration, Technical Consultant | CNTT / ERP Technical | Ph\u00e1t tri\u1ec3n, k\u1ef9 thu\u1eadt h\u1ec7 th\u1ed1ng |\n| FI/MM/SD Functional Consultant, ERP Business Analyst | CNTT / ERP Functional | Ph\u00e2n t\u00edch nghi\u1ec7p v\u1ee5 ERP |\n| ERP/IT Manager, Project Manager, Implementation Lead | ERP / IT Manager | Qu\u1ea3n l\u00fd d\u1ef1 \u00e1n ERP |\n| K\u1ebf to\u00e1n, Chief Accountant, Cost Controller | K\u1ebf to\u00e1n | T\u00e0i ch\u00ednh, ghi s\u1ed5, b\u00e1o c\u00e1o |\n| Nh\u00e2n s\u1ef1, HR Admin, C&B, Payroll | Nh\u00e2n s\u1ef1 | Tuy\u1ec3n d\u1ee5ng, h\u00e0nh ch\u00ednh, l\u01b0\u01a1ng th\u01b0\u1edfng |\n| Nh\u00e2n vi\u00ean h\u00e0nh ch\u00e1nh, Office Admin, Administrative Staff | Nh\u00e2n s\u1ef1 | H\u00e0nh ch\u00ednh t\u1ed5ng h\u1ee3p |\n| Nh\u00e2n vi\u00ean kho, Qu\u1ea3n l\u00fd kho, Th\u1ee7 kho | Logistics / Supply Chain | Qu\u1ea3n l\u00fd t\u1ed3n kho, v\u1eadn h\u00e0nh kho |\n| Nh\u00e2n vi\u00ean s\u1ea3n xu\u1ea5t, Qu\u1ea3n \u0111\u1ed1c, Gi\u00e1m s\u00e1t x\u01b0\u1edfng | S\u1ea3n xu\u1ea5t | Qu\u1ea3n l\u00fd s\u1ea3n xu\u1ea5t |\n| QA, QC, KCS, Ki\u1ec3m tra ch\u1ea5t l\u01b0\u1ee3ng | S\u1ea3n xu\u1ea5t / Ch\u1ea5t l\u01b0\u1ee3ng | \u0110\u1ea3m b\u1ea3o ch\u1ea5t l\u01b0\u1ee3ng |\n| Nh\u00e2n vi\u00ean kinh doanh, Sales Executive, Account Manager | Kinh doanh | B\u00e1n h\u00e0ng, kh\u00e1ch h\u00e0ng |\n| Marketing, Digital Marketing | Marketing | Th\u01b0\u01a1ng hi\u1ec7u, qu\u1ea3ng b\u00e1 |\n| Qu\u1ea3n l\u00fd d\u1ef1 \u00e1n, Director, General Manager | Qu\u1ea3n l\u00fd | L\u1eadp k\u1ebf ho\u1ea1ch, \u0111i\u1ec1u h\u00e0nh |\n| Kh\u00e1c | Kh\u00f4ng nh\u1eadn di\u1ec7n \u0111\u01b0\u1ee3c ng\u00e0nh ngh\u1ec1 | Kh\u00e1c (Other) |\n\n---\n\n\ud83e\udde0 B\u01af\u1edaC 2 \u2014 LOGIC DOMAIN MATCH\n\n| Tr\u01b0\u1eddng h\u1ee3p | domain_match | fit_score t\u1ed1i \u0111a | Ghi ch\u00fa |\n|-------------|--------------|------------------|----------|\n| JD = K\u1ebf to\u00e1n, CV = CNTT/ERP | \u274c | 25 | Sai ng\u00e0nh |\n| JD = Technical, CV = Functional (ho\u1eb7c ng\u01b0\u1ee3c l\u1ea1i) | \u2705 | 60 | C\u00f9ng ERP kh\u00e1c vai tr\u00f2 |\n| JD = ERP Technical, CV = ERP/IT Manager | \u2705 | 90 | Qu\u1ea3n l\u00fd c\u00f9ng ng\u00e0nh |\n| JD = Nh\u00e2n s\u1ef1, CV = H\u00e0nh ch\u00e1nh | \u2705 | 75 | G\u1ea7n ngh\u0129a |\n| JD = S\u1ea3n xu\u1ea5t, CV = QA/QC | \u2705 | 70 | C\u00f9ng kh\u1ed1i s\u1ea3n xu\u1ea5t |\n| Kh\u00f4ng r\u00f5 ng\u00e0nh | \u274c | 25 | Thi\u1ebfu d\u1eef li\u1ec7u |\n\n---\n\n\ud83d\udcca B\u01af\u1edaC 3 \u2014 QUY T\u1eaeC CH\u1ea4M \u0110I\u1ec2M (SCORING)\n\n1. fit_score = 0 (ban \u0111\u1ea7u)\n2. domain_match = true \u2192 +50\n3. matched_keywords t\u1eeb JD xu\u1ea5t hi\u1ec7n trong CV \u2192 +10\u201330\n4. C\u00f3 \u22653 n\u0103m kinh nghi\u1ec7m \u2192 +10\n5. Vai tr\u00f2 Manager/Lead c\u00f9ng ng\u00e0nh \u2192 +10\n6. domain_match = false \u2192 fit_score \u2264 25\n7. fit_score t\u1ed1i \u0111a 95 (kh\u00f4ng bao gi\u1edd 100)\n\n---\n\n\ud83d\udd0d B\u01af\u1edaC 4 \u2014 B\u1eb0NG CH\u1ee8NG (EVIDENCE)\n\nM\u1ed7i \u1ee9ng vi\u00ean ph\u1ea3i c\u00f3:\n{\n  \"cv_title\": \"ERP Manager\",\n  \"cv_roles\": [\"ERP Manager\", \"Technical Lead\"],\n  \"cv_years\": 8,\n  \"evidence\": [\n    \"ERP Manager t\u1ea1i c\u00f4ng ty ABC, qu\u1ea3n l\u00fd tri\u1ec3n khai SAP\",\n    \"Kinh nghi\u1ec7m 8 n\u0103m v\u1ec1 ABAP, Integration, Webservice\"\n  ]\n}\nN\u1ebfu kh\u00f4ng c\u00f3 ch\u1ee9ng c\u1ee9 r\u00f5 r\u00e0ng \u2192 domain_match = false, fit_score \u2264 25, final_decision = \"Lo\u1ea1i\".\n\n---\n\n\ud83e\uddf1 B\u01af\u1edaC 5 \u2014 CH\u1ed0NG NHI\u1ec4M JD (ANTI-LEAK)\n\n- Kh\u00f4ng d\u00f9ng t\u1eeb kh\u00f3a trong JD \u0111\u1ec3 x\u00e1c \u0111\u1ecbnh cv_domain, strengths ho\u1eb7c matched_keywords.  \n- N\u1ebfu c\u1ee5m ch\u1ec9 c\u00f3 trong JD m\u00e0 kh\u00f4ng c\u00f3 trong CV \u2192 KH\u00d4NG \u0111\u01b0\u1ee3c \u0111\u01b0a v\u00e0o matched_keywords.  \n- M\u1ecdi ph\u00e1n \u0111o\u00e1n ph\u1ea3i d\u1ef1a tr\u00ean **n\u1ed9i dung c\u00f3 th\u1eadt trong CV**.\n\n---\n\n\ud83e\udde9 B\u01af\u1edaC 6 \u2014 QUY T\u1eaeC CU\u1ed0I\n\n- fit_score kh\u00f4ng \u0111\u01b0\u1ee3c = 0 (tr\u1eeb CV tr\u1ed1ng).  \n- N\u1ebfu c\u00f9ng ng\u00e0nh kh\u00e1c vai tr\u00f2 \u2192 fit_score \u2264 60.  \n- Kh\u00f4ng cho fit_score = 100.  \n- role_match_type \u2208 {\"Functional\", \"Technical\", \"Manager\"}.\n\n---\n\n\ud83d\udccb B\u01af\u1edaC 7 \u2014 OUTPUT JSON\n\n{\n  \"candidate_name\": \"\",\n  \"fit_score\": 0,\n  \"jd_position\": \"\",\n  \"jd_domain\": \"\",\n  \"jd_function\": \"\",\n  \"cv_position\": \"\",\n  \"cv_domain\": \"\",\n  \"cv_function\": \"\",\n  \"domain_match\": false,\n  \"role_match_type\": \"\",\n  \"matched_keywords\": [],\n  \"strengths\": \"\",\n  \"weaknesses\": \"\",\n  \"recommendation\": \"\",\n  \"final_decision\": \"\",\n  \"cv_title\": \"\",\n  \"cv_roles\": [],\n  \"cv_years\": 0,\n  \"evidence\": []\n}\n\n---\n\n\ud83e\uddee B\u01af\u1edaC 8 \u2014 K\u1ebeT LU\u1eacN SAU X\u1eec L\u00dd\n\n- N\u1ebfu kh\u00f4ng c\u00f3 CV n\u00e0o fit_score \u2265 70 \u2192 \u201csummary\u201d: \u201cKh\u00f4ng c\u00f3 h\u1ed3 s\u01a1 ph\u00f9 h\u1ee3p. \u0110\u1ec1 xu\u1ea5t ch\u1ecdn CV kh\u00e1c.\u201d  \n- N\u1ebfu c\u00f3 \u22651 CV \u0111\u1ee7 \u0111i\u1ec1u ki\u1ec7n \u2192 \u201csummary\u201d: \u201c\ud83c\udfc6 \u1ee8ng vi\u00ean xu\u1ea5t s\u1eafc nh\u1ea5t: [T\u00ean] ([\u0110i\u1ec3m]%)\u201d.\n\n---\n\n\ud83e\udde0 M\u1ee4C TI\u00caU CU\u1ed0I  \n\u0110\u1ea3m b\u1ea3o m\u00f4 h\u00ecnh hi\u1ec3u v\u00e0 suy lu\u1eadn \u0111\u01b0\u1ee3c:\n- ERP Manager \u2260 K\u1ebf to\u00e1n  \n- Functional \u2260 Technical  \n- Nh\u00e2n s\u1ef1 \u2248 H\u00e0nh ch\u00e1nh  \n- QA/QC \u2248 S\u1ea3n xu\u1ea5t  \n- JD kh\u00f4ng lan sang CV  \n- Ph\u00e2n lo\u1ea1i ngh\u1ec1 nghi\u1ec7p ch\u00ednh x\u00e1c theo ontology tr\u00ean.\n"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "b417a559-5741-410e-ad42-89d88cd63f31",
      "name": "Parse Recruiter Output",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        -128
      ],
      "parameters": {
        "jsCode": "// Parse Recruiter Output - Tie-breaker only (no bonus to fit_score)\ntry {\n  let raw = $json.output || $json;\n\n  // N\u1ebfu AI tr\u1ea3 v\u1ec1 {output:\"[...json...]\"} th\u00ec l\u1ea5y ph\u1ea7n b\u00ean trong\n  if (typeof raw === 'object' && raw.output) raw = raw.output;\n\n  // L\u00e0m s\u1ea1ch chu\u1ed7i JSON n\u1ebfu c\u1ea7n\n  if (typeof raw === 'string') {\n    const match = raw.match(/\\[\\s*{[\\s\\S]*}\\s*\\]/);\n    if (match) raw = match[0];\n    raw = raw.trim().replace(/^[\\uFEFF\\x00-\\x1F]+/, '');\n  }\n\n  // Th\u1eed parse JSON (k\u1ec3 c\u1ea3 khi b\u1ecb double-encode)\n  let parsed;\n  try {\n    parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n  } catch (innerErr) {\n    parsed = JSON.parse(JSON.parse(raw));\n  }\n\n  // Chu\u1ea9n h\u00f3a th\u00e0nh m\u1ea3ng\n  const arr = Array.isArray(parsed) ? parsed : [parsed];\n  if (!arr.length) throw new Error(\"Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u \u1ee9ng vi\u00ean h\u1ee3p l\u1ec7\");\n\n  // Helpers\n  const norm = (s) => (s || '').toLowerCase().trim();\n  const domEq = (cv) => norm(cv.cv_domain) && norm(cv.jd_domain) && norm(cv.cv_domain) === norm(cv.jd_domain);\n  const domNear = (cv) => {\n    const a = norm(cv.cv_domain), b = norm(cv.jd_domain);\n    if (!a || !b) return false;\n    return a.includes(b) || b.includes(a);\n  };\n  const kwLen = (cv) => Array.isArray(cv.matched_keywords) ? cv.matched_keywords.length : 0;\n  const years = (cv) => Number(cv.cv_years || 0);\n\n  // \u0110\u1ed3ng b\u1ed9 nh\u00e3n hi\u1ec3n th\u1ecb v\u00e0 auto-fix domain_match n\u1ebfu AI sai\n  const candidates = arr.map(cv => {\n    const c = { ...cv };\n    // Role match label = \u0111\u00fang domain CV (kh\u00f4ng \u201cOther\u201d c\u1ee9ng)\n    c.role_match_label = (c.cv_domain || '').trim() || 'Kh\u00f4ng x\u00e1c \u0111\u1ecbnh';\n\n    // Auto-fix: n\u1ebfu domain b\u1eb1ng nhau m\u00e0 domain_match = false th\u00ec s\u1eeda th\u00e0nh true\n    if (domEq(c) && c.domain_match === false) c.domain_match = true;\n\n    return c;\n  });\n\n  // T\u1ed5ng v\u00e0 danh s\u00e1ch \u0111\u1ea1t chu\u1ea9n\n  const total = candidates.length;\n  const qualified = candidates.filter(cv => (cv.fit_score || 0) >= 70);\n\n  // Comparator ch\u1ecdn \u1ee9ng vi\u00ean t\u1ed1t nh\u1ea5t:\n  // 1) fit_score cao h\u01a1n th\u1eafng\n  // 2) N\u1ebfu b\u1eb1ng nhau: domain = JD th\u1eafng\n  // 3) N\u1ebfu v\u1eabn b\u1eb1ng nhau: domain g\u1ea7n JD th\u1eafng\n  // 4) N\u1ebfu v\u1eabn b\u1eb1ng nhau: nhi\u1ec1u matched_keywords h\u01a1n th\u1eafng\n  // 5) N\u1ebfu v\u1eabn b\u1eb1ng nhau: cv_years nhi\u1ec1u h\u01a1n th\u1eafng\n  const better = (a, b) => {\n    const fa = Number(a.fit_score || 0);\n    const fb = Number(b.fit_score || 0);\n    if (fa !== fb) return fa > fb ? a : b;\n\n    const aEq = domEq(a), bEq = domEq(b);\n    if (aEq !== bEq) return aEq ? a : b;\n\n    const aNear = domNear(a), bNear = domNear(b);\n    if (aNear !== bNear) return aNear ? a : b;\n\n    const aKw = kwLen(a), bKw = kwLen(b);\n    if (aKw !== bKw) return aKw > bKw ? a : b;\n\n    const aY = years(a), bY = years(b);\n    if (aY !== bY) return aY > bY ? a : b;\n\n    return a; // gi\u1eef nguy\u00ean th\u1ee9 t\u1ef1 n\u1ebfu v\u1eabn h\u00f2a\n  };\n\n  const best = candidates.reduce((acc, cv) => acc ? better(acc, cv) : cv, null);\n\n  // Summary lu\u00f4n d\u00f9ng fit_score \u201cg\u1ed1c\u201d (kh\u00f4ng c\u1ed9ng th\u01b0\u1edfng)\n  const summary_text =\n    best && (best.fit_score || 0) >= 70\n      ? `\ud83c\udfc6 \u1ee8ng vi\u00ean \u0111i\u1ec3m \u0111\u00e1nh gi\u00e1 cao nh\u1ea5t: ${best.candidate_name || \"(Kh\u00f4ng x\u00e1c \u0111\u1ecbnh)\"} (${best.fit_score}%)`\n      : `Kh\u00f4ng c\u00f3 h\u1ed3 s\u01a1 ph\u00f9 h\u1ee3p. \u0110\u1ec1 xu\u1ea5t t\u00ecm th\u00eam \u1ee9ng vi\u00ean kh\u00e1c.`;\n\n  return [{\n    json: {\n      total_candidates: total,\n      qualified_candidates: qualified.length,\n      best_candidate: best?.candidate_name || \"\",\n      best_score: best?.fit_score || 0,\n      summary_text,\n      candidates\n    }\n  }];\n\n} catch (err) {\n  return [{\n    json: {\n      error: \"JSON_PARSE_FAILED\",\n      message: err.message,\n      raw_snippet: String($json.output || '').slice(0, 500)\n    }\n  }];\n}\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "d87866a2-c309-45dc-9d6f-187eda5caac0",
      "name": "List_File",
      "type": "n8n-nodes-base.code",
      "position": [
        -1584,
        16
      ],
      "parameters": {
        "jsCode": "const body = $json.body || {};\nconst jd = body.message || \"JD ch\u01b0a x\u00e1c \u0111\u1ecbnh\";\nconst files = Array.isArray(body.files) ? body.files : [];\n\nreturn files.map((f, i) => ({\n  json: {\n    jd,\n    index: i + 1,\n    filename: f.name || `file_${i + 1}.pdf`,\n    base64: (f.data || \"\").split(\",\")[1] || \"\",\n  }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "32e65a79-c7e0-4f75-8f7b-72226f4f166e",
      "name": "Detect PDF Type",
      "type": "n8n-nodes-base.code",
      "position": [
        -1360,
        16
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Detect PDF type: text-based or scanned image\n\n// L\u1ea5y th\u00f4ng tin c\u01a1 b\u1ea3n\nconst jd = $json.jd || \"JD ch\u01b0a x\u00e1c \u0111\u1ecbnh\";\nconst index = $json.index || 1;\nconst filename = $json.filename || `cv_${index}.pdf`;\nconst base64 = $json.base64 || \"\";\n\n// N\u1ebfu kh\u00f4ng c\u00f3 base64 th\u00ec b\u1ecf qua\nif (!base64) {\n  return {\n    json: {\n      jd,\n      index,\n      filename,\n      base64: null,\n      pdf_type: \"unknown\",\n      note: \"\u26a0\ufe0f Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u base64 \u0111\u1ec3 ki\u1ec3m tra.\"\n    }\n  };\n}\n\ntry {\n  // Chuy\u1ec3n base64 \u2192 text \u0111\u1ec3 d\u00f2 n\u1ed9i dung readable\n  const pdfBuffer = Buffer.from(base64, \"base64\");\n  const text = pdfBuffer.toString(\"utf8\");\n\n  // Regex ki\u1ec3m tra xem c\u00f3 \u0111o\u1ea1n text \u0111\u1ecdc \u0111\u01b0\u1ee3c hay kh\u00f4ng\n  const hasReadableText = /[A-Za-z\u00c0-\u1ef90-9]{3,}/.test(text);\n  const pdf_type = hasReadableText ? \"text\" : \"scan\";\n\n  return {\n    json: {\n      jd,\n      index,\n      filename,\n      base64, // \ud83d\udc48 Gi\u1eef l\u1ea1i base64 \u0111\u1ec3 node sau c\u00f2n d\u00f9ng\n      pdf_type,\n      note: hasReadableText\n        ? \"\u2705 PDF c\u00f3 l\u1edbp text, \u0111\u1ecdc \u0111\u01b0\u1ee3c.\"\n        : \"\u26a0\ufe0f PDF d\u1ea1ng \u1ea3nh, kh\u00f4ng c\u00f3 l\u1edbp text.\"\n    }\n  };\n\n} catch (err) {\n  return {\n    json: {\n      jd,\n      index,\n      filename,\n      base64, // v\u1eabn gi\u1eef \u0111\u1ec3 debug khi l\u1ed7i\n      pdf_type: \"error\",\n      note: \"\u274c L\u1ed7i khi \u0111\u1ecdc base64: \" + err.message\n    }\n  };\n}\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4c49c7f6-7657-4453-985c-bc378849a09e",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        -1136,
        16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "603e402b-0f8c-4c51-a550-49f4564ad0cb",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": "=={{ $json[\"pdf_type\"] === \"text\" }}"
      },
      "typeVersion": 2.2,
      "alwaysOutputData": true
    },
    {
      "id": "64cdfed3-cba0-490b-8119-e90a3273632b",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        504,
        96
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "471ff4c6-bb44-45b8-bc9c-c8ef9e0aea45",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -464,
        -128
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "executeOnce": false,
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "508b522e-4eee-4104-9703-f3f4097d4ac9",
      "name": "Convert Base64 to Binary",
      "type": "n8n-nodes-base.code",
      "position": [
        -912,
        16
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Convert base64 \u2192 Binary cho t\u1eebng CV\n\nconst base64 = $json.base64 || \"\";\nconst jd = $json.jd || \"JD ch\u01b0a x\u00e1c \u0111\u1ecbnh\";\nconst index = $json.index || 1;\nconst filename = $json.filename || `cv_${index}.pdf`;\n\n// N\u1ebfu kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u base64 th\u00ec b\u1ecf qua\nif (!base64) {\n  return {\n    json: {\n      jd,\n      index,\n      filename,\n      note: \"\u26a0\ufe0f Kh\u00f4ng c\u00f3 d\u1eef li\u1ec7u base64 \u0111\u1ec3 convert.\"\n    }\n  };\n}\n\nreturn {\n  json: {\n    jd,\n    index,\n    filename\n  },\n  binary: {\n    data: {\n      data: Buffer.from(base64, \"base64\"),\n      mimeType: \"application/pdf\",\n      fileName: filename\n    }\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e9e7a983-9130-4f12-bfc5-c0d4163172c3",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -688,
        16
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "1f286db8-2495-4359-9db0-7d0a60c61201",
      "name": "Replace Me",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -464,
        64
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "4cbf8ff2-9884-46f9-b21d-207a880392f7",
      "name": "Reattach_Metadata_After_Extract",
      "type": "n8n-nodes-base.code",
      "position": [
        -240,
        -128
      ],
      "parameters": {
        "jsCode": "// Node: Reattach_Metadata_After_Extract (b\u1ea3n n\u00e2ng c\u1ea5p full logic)\n// T\u00e1c d\u1ee5ng: G\u1eafn l\u1ea1i jd, filename, text, v\u00e0 t\u1ef1 \u0111\u1ecdc candidate_name, candidate_position, role_match_type\n\nconst extracted = $input.all(); // K\u1ebft qu\u1ea3 t\u1eeb Extract From File\nconst srcItems = $items(\"Convert Base64 to Binary\", 0); // D\u1eef li\u1ec7u g\u1ed1c c\u00f3 jd, filename, index\n\n// ====== H\u00c0M TI\u1ec6N \u00cdCH ======\nconst normalize = (s) => (s || \"\").replace(/\\s+/g, \" \").trim();\n\nconst cleanName = (name) => {\n  return name\n    .replace(/[^\\p{L}\\s]/gu, \"\")\n    .replace(/\\s+/g, \" \")\n    .trim()\n    .replace(/\\b(\\p{L})/gu, (m) => m.toUpperCase());\n};\n\nreturn extracted.map((ex, i) => {\n  const src = (srcItems[i] && srcItems[i].json) ? srcItems[i].json : {};\n  const text = ex.json?.text || \"\";\n  const lines = text.split(/\\n/).map(l => l.trim()).filter(Boolean);\n  const header = lines.slice(0, 5).join(\" \");\n\n  let candidateName = \"\";\n  let candidatePosition = \"\";\n  let roleMatchType = \"Other\";\n\n  // ====== 1\ufe0f\u20e3 X\u00c1C \u0110\u1ecaNH T\u00caN \u1ee8NG VI\u00caN ======\n  // D\u1ea1ng \u0111\u1ea7y \u0111\u1ee7: \"Nguyen Thi Ngoc Phuong\"\n  const fullNameRegex = /\\b([A-Z\u0110][a-z\u00e0\u00e1\u1ea1\u1ea3\u00e3\u00e2\u1ea7\u1ea5\u1ead\u1ea9\u1eab\u0103\u1eb1\u1eaf\u1eb7\u1eb3\u1eb5\u00e8\u00e9\u1eb9\u1ebb\u1ebd\u00ea\u1ec1\u1ebf\u1ec7\u1ec3\u1ec5\u00ec\u00ed\u1ecb\u1ec9\u0129\u00f2\u00f3\u1ecd\u1ecf\u00f5\u00f4\u1ed3\u1ed1\u1ed9\u1ed5\u1ed7\u01a1\u1edd\u1edb\u1ee3\u1edf\u1ee1\u00f9\u00fa\u1ee5\u1ee7\u0169\u01b0\u1eeb\u1ee9\u1ef1\u1eed\u1eef\u1ef3\u00fd\u1ef5\u1ef7\u1ef9\u0111\u0110]{1,}\\s){1,4}[A-Z\u0110][a-z\u00e0\u00e1\u1ea1\u1ea3\u00e3\u00e2\u1ea7\u1ea5\u1ead\u1ea9\u1eab\u0103\u1eb1\u1eaf\u1eb7\u1eb3\u1eb5\u00e8\u00e9\u1eb9\u1ebb\u1ebd\u00ea\u1ec1\u1ebf\u1ec7\u1ec3\u1ec5\u00ec\u00ed\u1ecb\u1ec9\u0129\u00f2\u00f3\u1ecd\u1ecf\u00f5\u00f4\u1ed3\u1ed1\u1ed9\u1ed5\u1ed7\u01a1\u1edd\u1edb\u1ee3\u1edf\u1ee1\u00f9\u00fa\u1ee5\u1ee7\u0169\u01b0\u1eeb\u1ee9\u1ef1\u1eed\u1eef\u1ef3\u00fd\u1ef5\u1ef7\u1ef9\u0111\u0110]{2,}/gu;\n  const matchName = header.match(fullNameRegex);\n  if (matchName) candidateName = cleanName(matchName[0]);\n\n  // D\u1ea1ng vi\u1ebft t\u1eaft: N.T.K.Di\u1ec5m, L.T.M.Trinh\n  if (!candidateName) {\n    const abbr = header.match(/[A-Z]\\.[A-Z]\\.[A-Z]\\.[A-Za-z\u00c0-\u1ef9]{2,}/);\n    if (abbr) candidateName = abbr[0].replace(/\\./g, \" \").trim();\n  }\n\n  // T\u00ean n\u1eb1m trong \u201cTh\u00f4ng Tin C\u00e1 Nh\u00e2n\u201d ho\u1eb7c \u201cH\u1ecd v\u00e0 T\u00ean\u201d\n  if (!candidateName) {\n    const infoBlock = text.match(/(H\u1ecd\\s*v\u00e0\\s*t\u00ean|Th\u00f4ng\\s*Tin\\s*C\u00e1\\s*Nh\u00e2n)[\\s\\S]{0,150}/i);\n    if (infoBlock) {\n      const found = infoBlock[0].match(fullNameRegex);\n      if (found) candidateName = cleanName(found[0]);\n    }\n  }\n\n  // ====== 2\ufe0f\u20e3 X\u00c1C \u0110\u1ecaNH CH\u1ee8C DANH (V\u1eca TR\u00cd) ======\n  const roleRegex = /(Team\\s*Leader|Manager|Consultant|Specialist|Developer|Engineer|Officer|Supervisor|Coordinator|Executive|Director|Leader|Nh\u00e2n\\s*vi\u00ean\\s*[^\\n,;]+|Chuy\u00ean\\s*vi\u00ean\\s*[^\\n,;]+|Tr\u01b0\u1edfng\\s*[^\\n,;]+|Gi\u00e1m\\s*s\u00e1t\\s*[^\\n,;]+)/i;\n  const matchRole = text.match(roleRegex);\n  if (matchRole) candidatePosition = normalize(matchRole[0]);\n\n  // ====== 3\ufe0f\u20e3 PH\u00c2N LO\u1ea0I role_match_type ======\n  if (/Manager|Director|Tr\u01b0\u1edfng/i.test(candidatePosition)) roleMatchType = \"Manager\";\n  else if (/Consultant|Functional|Analyst|Specialist|Officer/i.test(candidatePosition)) roleMatchType = \"Functional\";\n  else if (/Developer|Technical|Engineer|Programmer|Basis/i.test(candidatePosition)) roleMatchType = \"Technical\";\n  else if (/Admin|H\u00e0nh\\s*ch\u00e1nh|Nh\u00e2n\\s*s\u1ef1/i.test(candidatePosition)) roleMatchType = \"Admin\";\n  else roleMatchType = \"Other\";\n\n  // ====== 4\ufe0f\u20e3 K\u1ebeT QU\u1ea2 ======\n  return {\n    json: {\n      jd: src.jd ?? null,\n      index: src.index ?? (i + 1),\n      filename: src.filename ?? ex?.binary?.data?.fileName ?? null,\n      text,\n      info: ex.json?.info ?? {},\n      candidate_name: candidateName || null,\n      candidate_position: candidatePosition || null,\n      role_match_type: roleMatchType\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c664f25b-3f3c-446e-832c-8a7df09334b4",
      "name": "Combine_Candidates_For_AI",
      "type": "n8n-nodes-base.code",
      "position": [
        -16,
        -128
      ],
      "parameters": {
        "jsCode": "// --- Combine_Candidates_For_AI ---\n// Gom t\u1ea5t c\u1ea3 c\u00e1c CV th\u00e0nh m\u1ea3ng candidates[] \u0111\u1ec3 AI Agent \u0111\u1ecdc\n// \u0110\u1ed3ng th\u1eddi t\u1ef1 \u0111\u1ed9ng l\u1ea5y d\u00f2ng t\u00ean \u0111\u1ea7u ti\u00ean trong CV (v\u00ed d\u1ee5: \"N.T.K.Di\u1ec5m\", \"L.T.M.Trinh\")\n\nfunction extractRawName(text = \"\") {\n  if (!text) return \"\";\n\n  const lines = text\n    .replace(/\\r/g, \"\")\n    .replace(/\\t/g, \" \")\n    .split(\"\\n\")\n    .map(l => l.trim())\n    .filter(l => l && l.length > 1)\n    .slice(0, 8); // ch\u1ec9 xem v\u00e0i d\u00f2ng \u0111\u1ea7u ti\u00ean\n\n  // B\u1ecf qua c\u00e1c d\u00f2ng ch\u1ee9a c\u1ee5m t\u1eeb \u201cTh\u00f4ng tin c\u00e1 nh\u00e2n\u201d, \u201cCV\u201d, \u201cS\u01a1 y\u1ebfu l\u00fd l\u1ecbch\u201d, \u201cCurriculum Vitae\u201d\n  const skip = /(th\u00f4ng tin|s\u01a1 y\u1ebfu|curriculum vitae|cv|profile)/i;\n  const filtered = lines.filter(l => !skip.test(l));\n\n  // L\u1ea5y d\u00f2ng \u0111\u1ea7u ti\u00ean h\u1ee3p l\u1ec7 (gi\u1eef nguy\u00ean format, kh\u00f4ng s\u1eeda g\u00ec)\n  return filtered.length ? filtered[0] : \"\";\n}\n\nreturn [\n  {\n    json: {\n      jd: $items(\"Reattach_Metadata_After_Extract\")[0].json.jd,\n      candidates: $items(\"Reattach_Metadata_After_Extract\").map(item => {\n        const text = item.json.text || \"\";\n        const nameFromText = extractRawName(text);\n\n        return {\n          filename: item.json.filename,\n          // \u01afu ti\u00ean t\u00ean tr\u00edch t\u1eeb text, n\u1ebfu r\u1ed7ng m\u1edbi l\u1ea5y t\u00ean c\u0169\n          candidate_name: nameFromText || item.json.candidate_name || \"\",\n          candidate_position: item.json.candidate_position || \"\",\n          role_match_type: item.json.role_match_type || \"Other\",\n          text\n        };\n      })\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f249f3d8-524e-4e46-97a9-5577fde7a46b",
      "name": "Preprocess_CV_Names",
      "type": "n8n-nodes-base.code",
      "position": [
        208,
        -128
      ],
      "parameters": {
        "jsCode": "// --- Node: Preprocess_CV_Names ---\n// M\u1ee5c ti\u00eau: l\u1ea5y \u0111\u00fang d\u00f2ng \u0111\u1ea7u ti\u00ean (t\u00ean th\u00f4 trong CV), b\u1ecf qua c\u00e1c d\u00f2ng nh\u01b0 \"Th\u00f4ng tin c\u00e1 nh\u00e2n\", \"CV\", \"S\u01a1 y\u1ebfu l\u00fd l\u1ecbch\".\n// Kh\u00f4ng \u0111\u1ed5i hoa/th\u01b0\u1eddng, kh\u00f4ng b\u1ecf d\u1ea5u, kh\u00f4ng b\u1ecf d\u1ea5u ch\u1ea5m.\n\nfunction extractRawName(text = \"\") {\n  if (!text) return \"\";\n\n  const lines = text\n    .replace(/\\r/g, \"\")\n    .replace(/\\t/g, \" \")\n    .split(\"\\n\")\n    .map(l => l.trim())\n    .filter(l => l && l.length > 1)\n    .slice(0, 8); // ch\u1ec9 qu\u00e9t v\u00e0i d\u00f2ng \u0111\u1ea7u CV\n\n  // B\u1ecf qua d\u00f2ng ch\u1ee9a \"Th\u00f4ng tin c\u00e1 nh\u00e2n\", \"CV\", \"Curriculum Vitae\"...\n  const skip = /(th\u00f4ng tin|s\u01a1 y\u1ebfu|curriculum vitae|cv|profile)/i;\n  const filtered = lines.filter(l => !skip.test(l));\n\n  // L\u1ea5y d\u00f2ng \u0111\u1ea7u ti\u00ean c\u00f2n l\u1ea1i (gi\u1eef nguy\u00ean xi)\n  return filtered.length ? filtered[0] : \"\";\n}\n\nreturn $input.all().map(item => {\n  const t = item.json.text || \"\";\n  const name = extractRawName(t);\n\n  // Debug xem \u0111\u1ecdc ra g\u00ec\n  console.log(\"\ud83d\udcc4 File:\", item.json.filename, \"\u2192 Name:\", name);\n\n  return {\n    json: {\n      ...item.json,\n      // \u00c9p c\u1eadp nh\u1eadt l\u1ea1i t\u00ean, KH\u00d4NG fallback\n      candidate_name: name,\n    },\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f8aff173-8caf-428a-8aae-e5ac8203edb5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1920,
        -368
      ],
      "parameters": {
        "height": 320,
        "content": "\ud83d\udfe9 Step 1: Upload Files (JD + CVs)\n- Webhook node receives one JD and multiple CV files.\n- List_File node extracts filenames and base64 data.\n- Detect PDF Type node determines whether each file is text-based or scanned.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c3c11827-e7f5-4753-914a-cf1a312fc5e3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -368
      ],
      "parameters": {
        "height": 304,
        "content": "\ud83d\udfe8 Step 2: Extract & Merge Text\n- Convert Base64 to Binary \u2192 prepares each CV for text extraction.\n- Extract From File \u2192 extracts readable text from PDF.\n- Reattach_Metadata_After_Extract \u2192 restores JD, filename, and candidate metadata.\n- Combine_Candidates_For_AI \u2192 aggregates all CVs into a single candidates[] array.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7e966941-fecf-4a79-ac74-104abe8cf9c5",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        -464
      ],
      "parameters": {
        "height": 288,
        "content": "\ud83d\udfe6 Step 3: AI Analysis & Output\n- AI Recruiter Agent compares JD vs. each CV and generates structured insights (fit_score, strengths, weaknesses, recommendation).\n- Parse Recruiter Output selects the top candidate and builds a summary.\n- Respond to Webhook returns the full JSON result to chat UI or external API.\n\n"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "95b2d7aa-89a3-4007-8960-a8f4dd2d158e",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Convert Base64 to Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "List_File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List_File": {
      "main": [
        [
          {
            "node": "Detect PDF Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Replace Me": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect PDF Type": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Replace Me",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "Reattach_Metadata_After_Extract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Recruiter Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "AI Recruiter Agent": {
      "main": [
        [
          {
            "node": "Parse Recruiter Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond to Webhook": {
      "main": [
        []
      ]
    },
    "Preprocess_CV_Names": {
      "main": [
        [
          {
            "node": "AI Recruiter Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Recruiter Output": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Base64 to Binary": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine_Candidates_For_AI": {
      "main": [
        [
          {
            "node": "Preprocess_CV_Names",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reattach_Metadata_After_Extract": {
      "main": [
        [
          {
            "node": "Combine_Candidates_For_AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}