import { tool } from "@langchain/core/tools";
// tool from "@langchain/core/tools" — correct 2025 import location
import { z } from "zod";
// ─────────────────────────────────────────
// TOOL 1 — Extract Skills from Resume
// Parses resume text and pulls out skills
// ─────────────────────────────────────────
export const extractResumeSkilllsTool = tool(
async ({ resumeText }) => {
// resumeText = the full resume as a plain text string
// This tool does text processing to extract skills
// In this implementation we do basic keyword extraction
// In production you could use a dedicated NLP model
const skillKeywords = [
// Programming Languages
"javascript", "typescript", "python", "java", "golang", "rust", "c++", "php", "ruby",
// Frontend
"react", "vue", "angular", "next.js", "svelte", "html", "css", "tailwind",
// Backend
"node.js", "express", "fastapi", "django", "spring boot", "laravel",
// Databases
"mongodb", "postgresql", "mysql", "redis", "elasticsearch", "sqlite",
// Cloud
"aws", "gcp", "azure", "docker", "kubernetes", "terraform",
// Tools
"git", "github", "ci/cd", "github actions", "jenkins", "agile", "scrum",
// Other
"rest api", "graphql", "microservices", "socket.io", "jwt", "oauth",
];
const resumeLower = resumeText.toLowerCase();
// convert to lowercase for case-insensitive matching
// "React" and "react" should both match
const foundSkills = skillKeywords.filter(skill =>
resumeLower.includes(skill)
);
// filter = keep only skills that appear in the resume text
// example foundSkills: ["javascript", "typescript", "react", "node.js", ...]
// Extract years of experience from resume text
const experienceMatch = resumeText.match(/(\d+)\s*(?:year|yr)/gi);
// regex matches patterns like "3 years" or "5yr" or "2 Years"
// example match: ["2022 - Present", "2021 - 2022"]
const yearsMatch = resumeText.match(/\((\d{4})\s*-\s*(?:Present|\d{4})\)/gi);
// matches date ranges like "(2022 - Present)" or "(2021 - 2022)"
let totalExperience = 0;
if (yearsMatch) {
yearsMatch.forEach(match => {
const years = match.match(/\d{4}/g);
// extract 4-digit years from each match
if (years) {
const startYear = parseInt(years[0]);
const endYear = years[1] === "Present" ? 2025 : parseInt(years[1]);
totalExperience += endYear - startYear;
// add duration of this job to total experience
}
});
}
return JSON.stringify({
skills: foundSkills,
// array of detected skills
// example: ["javascript", "typescript", "react", "node.js", "mongodb"]
skillCount: foundSkills.length,
// total number of skills found
// example: 18
estimatedYearsExperience: Math.min(totalExperience, 15),
// total years across all jobs (capped at 15)
// example: 4 (2 years at TechStartup + 1 year freelance + rounding)
rawSkillsText: resumeText.substring(0, 500),
// first 500 chars for additional context
});
// returns JSON string — LLM parses this to understand the resume
},
{
name: "extract_resume_skills",
description: `Extracts and analyzes skills, technologies, and experience from a resume.
Use this as the FIRST step when analyzing a resume.
Returns a structured list of detected skills and estimated years of experience.`,
schema: z.object({
resumeText: z.string().describe("the complete resume text to analyze"),
}),
}
);
// ─────────────────────────────────────────
// TOOL 2 — Extract Requirements from Job Description
// Parses JD and pulls out what the company wants
// ─────────────────────────────────────────
export const extractJobRequirementsTool = tool(
async ({ jobText }) => {
// jobText = the full job description as plain text
const requirementKeywords = [
"javascript", "typescript", "python", "java", "react", "node.js", "vue", "angular",
"mongodb", "postgresql", "mysql", "redis", "aws", "gcp", "azure",
"docker", "kubernetes", "microservices", "rest api", "graphql",
"ci/cd", "git", "agile", "system design", "next.js", "express",
];
const jobLower = jobText.toLowerCase();
// Separate required vs nice-to-have skills
const requiredSection = extractSection(jobText, ["REQUIREMENTS", "REQUIRED", "MUST HAVE"]);
const preferredSection = extractSection(jobText, ["NICE TO HAVE", "PREFERRED", "BONUS"]);
// extractSection finds the relevant section of the JD
const requiredSkills = requirementKeywords.filter(skill =>
requiredSection.toLowerCase().includes(skill)
);
// skills mentioned in the REQUIREMENTS section
// example: ["typescript", "react", "node.js", "postgresql"]
const preferredSkills = requirementKeywords.filter(skill =>
preferredSection.toLowerCase().includes(skill) &&
!requiredSkills.includes(skill)
// exclude skills already in required list
);
// skills mentioned in NICE TO HAVE section
// example: ["next.js", "graphql", "kubernetes"]
// Extract minimum years of experience
const yearsMatch = jobText.match(/(\d+)\+?\s*years?\s*(?:of\s*)?experience/gi);
const minYears = yearsMatch
? Math.max(...yearsMatch.map(m => parseInt(m)))
: 0;
// take the maximum years mentioned — that's likely the senior requirement
// example: "3+ years" → 3
return JSON.stringify({
requiredSkills,
// skills that are REQUIRED (must have)
// example: ["typescript", "mysql", "aws", "docker"]
preferredSkills,
// skills that are NICE TO HAVE (bonus)
// example: ["next.js", "graphql", "kubernetes"]
minimumYearsRequired: minYears,
// minimum years of experience required
// example: 3
totalRequirements: requiredSkills.length + preferredSkills.length,
// total number of requirements found
});
},
{
name: "extract_job_requirements",
description: `Extracts required skills, preferred skills, and experience requirements from a job description.
Use this as the SECOND step — after extracting resume skills.
Returns required vs preferred skills separately.`,
schema: z.object({
jobText: z.string().describe("the complete job description text to analyze"),
}),
}
);
function extractSection(text, sectionNames) {
// Helper to find a specific section in the job description text
// sectionNames = array of possible headings to look for
for (const name of sectionNames) {
const index = text.toUpperCase().indexOf(name);
// find where this section heading appears
if (index !== -1) {
const afterHeading = text.substring(index);
// text from this heading onwards
const nextSectionMatch = afterHeading.slice(name.length).match(/\n[A-Z\s]{3,}:/);
// find the next section heading (all caps text followed by colon)
if (nextSectionMatch) {
return afterHeading.slice(name.length, name.length + nextSectionMatch.index);
// return text between this heading and the next
}
return afterHeading.slice(name.length, name.length + 500);
// if no next section, return next 500 chars
}
}
return text;
// if section not found, return full text
}
// ─────────────────────────────────────────
// TOOL 3 — Match Skills and Calculate Score
// Compares resume skills vs job requirements
// ─────────────────────────────────────────
export const calculateMatchScoreTool = tool(
async ({ resumeSkillsJson, jobRequirementsJson }) => {
// resumeSkillsJson = JSON string from extract_resume_skills tool
// jobRequirementsJson = JSON string from extract_job_requirements tool
const resumeData = JSON.parse(resumeSkillsJson);
const jobData = JSON.parse(jobRequirementsJson);
// parse both JSON strings back into JavaScript objects
const resumeSkills = resumeData.skills || [];
const requiredSkills = jobData.requiredSkills || [];
const preferredSkills = jobData.preferredSkills || [];
const minYears = jobData.minimumYearsRequired || 0;
const candidateYears = resumeData.estimatedYearsExperience || 0;
// Find matched and missing required skills
const matchedRequired = requiredSkills.filter(skill =>
resumeSkills.includes(skill)
);
// skills that are required AND present in resume
// example: ["typescript", "mysql", "aws"]
const missingRequired = requiredSkills.filter(skill =>
!resumeSkills.includes(skill)
);
// required skills that are NOT in the resume
// example: ["docker", "microservices"]
// Find matched preferred skills (bonus points)
const matchedPreferred = preferredSkills.filter(skill =>
resumeSkills.includes(skill)
);
// example: ["next.js"]
// Calculate score components
const requiredScore = requiredSkills.length > 0
? (matchedRequired.length / requiredSkills.length) * 70
: 70;
// required skills worth 70% of total score
// example: 4/5 required matched = 0.8 × 70 = 56 points
const preferredScore = preferredSkills.length > 0
? (matchedPreferred.length / preferredSkills.length) * 20
: 20;
// preferred skills worth 20% of total score
// example: 1/3 preferred matched = 0.33 × 20 = 6.6 points
const experienceScore = candidateYears >= minYears ? 10 : (candidateYears / minYears) * 10;
// experience worth 10% of total score
// example: 4 years vs 3 required = full 10 points
const totalScore = Math.round(requiredScore + preferredScore + experienceScore);
// final score out of 100
// example: 56 + 6.6 + 10 = 72.6 → 73
const verdict =
totalScore >= 80 ? "STRONG MATCH — Highly recommended to apply" :
totalScore >= 60 ? "GOOD MATCH — Apply with confidence" :
totalScore >= 40 ? "PARTIAL MATCH — Apply but address skill gaps" :
"WEAK MATCH — Significant skill development needed first";
return JSON.stringify({
totalScore,
// overall match score 0-100
// example: 73
requiredMatchPercent: requiredSkills.length > 0
? Math.round((matchedRequired.length / requiredSkills.length) * 100)
: 100,
// what % of required skills are matched
// example: 80 (means 80% of required skills are in resume)
matchedRequired,
// example: ["typescript", "mysql", "aws", "rest api"]
missingRequired,
// example: ["docker", "microservices"]
matchedPreferred,
// example: ["next.js"]
experienceMatch: {
candidateYears,
required: minYears,
meets: candidateYears >= minYears,
// example: { candidateYears: 4, required: 3, meets: true }
},
verdict,
// human readable overall assessment
});
},
{
name: "calculate_match_score",
description: `Calculates how well a candidate's skills match the job requirements.
Use this THIRD — after extracting both resume skills and job requirements.
Returns a detailed score breakdown with matched and missing skills.`,
schema: z.object({
resumeSkillsJson: z.string().describe("JSON output from extract_resume_skills tool"),
jobRequirementsJson: z.string().describe("JSON output from extract_job_requirements tool"),
}),
}
);
// ─────────────────────────────────────────
// TOOL 4 — Generate Improvement Suggestions
// Gives specific, actionable advice
// ─────────────────────────────────────────
export const generateSuggestionsTool = tool(
async ({ missingSkills, candidateBackground, targetRole }) => {
// missingSkills = skills the candidate is missing (comma-separated string)
// candidateBackground = brief description of current background
// targetRole = job title they are targeting
const missing = missingSkills.split(",").map(s => s.trim()).filter(Boolean);
// convert comma-separated string to array and clean whitespace
// example: "docker, microservices, kubernetes" → ["docker", "microservices", "kubernetes"]
// Learning path suggestions per skill
const learningPaths = {
"docker": {
timeToLearn: "2-3 weeks",
resources: ["Docker official docs", "Docker for Developers course on Udemy", "Play with Docker (free sandbox)"],
project: "Containerize your existing MERN application",
},
"kubernetes": {
timeToLearn: "4-6 weeks",
resources: ["Kubernetes official docs", "CKA exam prep course", "Killercoda free labs"],
project: "Deploy your Dockerized app on a local K8s cluster using minikube",
},
"microservices": {
timeToLearn: "3-4 weeks",
resources: ["Microservices.io patterns site", "Building Microservices book by Sam Newman", "YouTube: TechWorld with Nana"],
project: "Split your monolithic app into 2-3 microservices communicating via REST",
},
"graphql": {
timeToLearn: "1-2 weeks",
resources: ["GraphQL official docs", "How to GraphQL tutorial", "Apollo GraphQL docs"],
project: "Add a GraphQL API alongside your existing REST API",
},
"postgresql": {
timeToLearn: "1-2 weeks",
resources: ["PostgreSQL Tutorial website", "pgexercises.com for practice", "Supabase docs"],
project: "Migrate one of your MongoDB collections to PostgreSQL",
},
"system design": {
timeToLearn: "4-8 weeks",
resources: ["System Design Primer on GitHub", "Designing Data-Intensive Applications book", "ByteByteGo newsletter"],
project: "Design and document the architecture of your most complex project",
},
};
const suggestions = missing.map(skill => {
const path = learningPaths[skill.toLowerCase()];
if (path) {
return `${skill.toUpperCase()}:
Time to learn: ${path.timeToLearn}
Resources: ${path.resources.join(", ")}
Hands-on project: ${path.project}`;
}
return `${skill.toUpperCase()}: Start with official documentation and build a small demo project to practice.`;
});
const priorityOrder = missing.slice(0, 3);
// top 3 missing skills — focus on these first
return `IMPROVEMENT PLAN FOR: ${targetRole}
Background: ${candidateBackground}
PRIORITY SKILLS TO LEARN (top 3 missing required skills):
${priorityOrder.map((s, i) => `${i + 1}. ${s}`).join("\n")}
DETAILED LEARNING PATH:
${suggestions.join("\n\n")}
GENERAL ADVICE:
1. Focus on the top 3 priority skills before applying
2. Build projects that use these new skills — employers verify skills through code
3. Update your GitHub with these new projects before applying
4. Estimated time to be competitive: ${missing.length <= 2 ? "2-4 weeks" : missing.length <= 4 ? "4-8 weeks" : "2-3 months"}`;
},
{
name: "generate_suggestions",
description: `Generates a detailed, actionable improvement plan based on skill gaps.
Use this LAST — after calculating the match score.
Returns specific learning resources and project ideas for each missing skill.`,
schema: z.object({
missingSkills: z.string().describe("comma-separated list of missing skills"),
candidateBackground: z.string().describe("brief description of candidate's current background"),
targetRole: z.string().describe("the job title the candidate is applying for"),
}),
}
);