Skip to content

Pendent-ai/ats-engine

Repository files navigation

@pendent/ats-engine

A comprehensive ATS (Applicant Tracking System) scoring engine with multi-LLM provider support.

Features

  • 5-Layer Output Structure: Machine-readable scores, recruiter explanations, audit evidence, workflow decisions, and candidate feedback
  • Multi-LLM Support: OpenAI, Anthropic, Azure OpenAI, Google Gemini, Groq, OpenRouter, and custom OpenAI-compatible APIs
  • LLM Optional: Works with rule-based scoring when no LLM is configured
  • Batch Processing: Process 1-1000 candidates per batch with concurrency control
  • Rule-Based Analysis: ATS parseability, formatting consistency, and structure analysis
  • LLM-Enhanced Scoring: Semantic matching, skills verification, and experience alignment
  • TypeScript First: Full type definitions included

Architecture Overview

flowchart TB
    subgraph Input["📥 Input Layer"]
        Single["Single Candidate<br/>scoreCandidate()"]
        Batch["Batch Processing<br/>scoreCandidateBatch()<br/>(1-1000 candidates)"]
    end

    subgraph Extraction["📄 Extraction Layer"]
        PDF["PDF Parser"]
        DOCX["DOCX Parser"]
        TXT["Text Extractor"]
        Normalize["Text Normalizer"]
    end

    subgraph RuleBased["⚙️ Rule-Based Analysis"]
        Parse["Parseability<br/>Analyzer"]
        Format["Formatting<br/>Analyzer"]
        Struct["Structure<br/>Analyzer"]
    end

    subgraph LLMLayer["🤖 LLM Analysis (Optional)"]
        LLMCheck{{"LLM Config<br/>Provided?"}}
        OpenAI["OpenAI<br/>GPT-4o"]
        Anthropic["Anthropic<br/>Claude"]
        Azure["Azure<br/>OpenAI"]
        Google["Google<br/>Gemini"]
        Groq["Groq<br/>Llama"]
        OpenRouter["OpenRouter"]
        Custom["Custom API"]
        Semantic["Semantic<br/>Analysis"]
    end

    subgraph Validation["✅ Validation Layer"]
        Validate["LLM Output<br/>Validator"]
        RuleOnly["Rule-Based<br/>Fallback"]
    end

    subgraph Output["📊 5-Layer Output"]
        Core["Layer 1: Core ATS<br/>(Machine-readable)"]
        Explain["Layer 2: Explainability<br/>(Recruiter-facing)"]
        Evidence["Layer 3: Evidence<br/>(Audit-grade)"]
        Decision["Layer 4: Decision<br/>(Workflow-level)"]
        Feedback["Layer 5: Feedback<br/>(Candidate-facing)"]
    end

    subgraph BatchOutput["📈 Batch Output"]
        Rankings["Ranked<br/>Candidates"]
        Stats["Statistics<br/>& Distribution"]
    end

    Single --> Extraction
    Batch --> Extraction
    
    Extraction --> PDF & DOCX & TXT
    PDF & DOCX & TXT --> Normalize
    
    Normalize --> RuleBased
    Parse & Format & Struct --> LLMCheck
    
    LLMCheck -->|"Yes"| OpenAI & Anthropic & Azure & Google & Groq & OpenRouter & Custom
    LLMCheck -->|"No"| RuleOnly
    
    OpenAI & Anthropic & Azure & Google & Groq & OpenRouter & Custom --> Semantic
    Semantic --> Validate
    Validate --> Core
    RuleOnly --> Core
    
    Core --> Explain --> Evidence --> Decision --> Feedback
    
    Feedback --> Rankings
    Rankings --> Stats
Loading

Installation

npm install @pendent/ats-engine

# Install peer dependencies based on your LLM provider
npm install openai          # For OpenAI/Azure/Google/Groq/OpenRouter
npm install @anthropic-ai/sdk  # For Anthropic

Quick Start

Full ATS Scoring with LLM

import { scoreCandidate, createLLMConfig, createJobInput } from '@pendent/ats-engine';
import { readFileSync } from 'fs';

const result = await scoreCandidate({
  candidate: {
    name: 'John Doe',
    email: 'john@example.com',
    phone: '+1-555-1234',
    linkedinUrl: 'https://linkedin.com/in/johndoe',
    githubUrl: 'https://github.com/johndoe', // optional
    portfolioUrl: 'https://johndoe.dev', // optional
    resumeBuffer: readFileSync('resume.pdf'),
    resumeMimeType: 'application/pdf',
  },
  job: createJobInput(
    'job-123',
    'Senior Software Engineer',
    'Looking for a senior engineer with React and Node.js experience...',
    {
      mustHave: ['React', 'Node.js', 'TypeScript'],
      niceToHave: ['AWS', 'Docker', 'Kubernetes'],
    },
    {
      requiredExperienceYears: 5,
      experienceLevel: 'SENIOR',
    }
  ),
  llmConfig: createLLMConfig('openai', process.env.OPENAI_API_KEY!),
  options: {
    includeEvidence: true,
    includeCandidateFeedback: true,
  },
});

// Access the 5-layer output
console.log(result.core);              // Layer 1: Machine-readable scores
console.log(result.explanation);       // Layer 2: Recruiter-facing summary
console.log(result.evidence);          // Layer 3: Audit-grade evidence
console.log(result.decision);          // Layer 4: Workflow decision
console.log(result.candidateFeedback); // Layer 5: Candidate feedback

Quick Rule-Based Check (No LLM)

import { quickScore, createJobInput } from '@pendent/ats-engine';
import { readFileSync } from 'fs';

const result = await quickScore(
  readFileSync('resume.pdf'),
  'application/pdf',
  createJobInput('job-123', 'Developer', 'Description here')
);

console.log(`Score: ${result.score}/100 (${result.grade})`);
console.log(`Decision: ${result.decisionBand}`);
console.log('Issues:', result.issues);

Batch Processing (1-1000 Candidates)

Process multiple candidates efficiently with concurrency control and progress tracking:

import { scoreCandidateBatch, createLLMConfig, createJobInput } from '@pendent/ats-engine';

// Prepare candidates (add candidateId to each)
const candidates = yourCandidates.map((c, i) => ({
  ...c,
  candidateId: `candidate-${i}`,
}));

// Process batch with optional LLM
const result = await scoreCandidateBatch({
  candidates,
  job: createJobInput('job-123', 'Senior Engineer', 'Job description...', {
    mustHave: ['React', 'Node.js'],
    niceToHave: ['AWS', 'Docker'],
  }),
  // LLM is optional - omit for rule-based only scoring
  llmConfig: createLLMConfig('openai', process.env.OPENAI_API_KEY!),
  batchOptions: {
    concurrency: 10,        // Process 10 at a time (max 50)
    continueOnError: true,  // Don't stop on individual failures
    onProgress: (progress) => {
      console.log(`Progress: ${progress.percentage}%`);
      console.log(`${progress.succeeded}/${progress.total} successful`);
      console.log(`ETA: ${progress.estimatedTimeRemaining}ms`);
    },
  },
});

// Access batch results
console.log('Summary:', result.summary);
// {
//   totalCandidates: 500,
//   successfulScores: 498,
//   failedScores: 2,
//   totalProcessingTimeMs: 45000,
//   averageProcessingTimeMs: 90,
//   llmUsed: true
// }

console.log('Top Candidates:', result.rankedCandidates.slice(0, 10));
// Sorted by score (highest first)

console.log('Statistics:', result.statistics);
// {
//   averageScore: 68,
//   medianScore: 70,
//   highestScore: 94,
//   lowestScore: 23,
//   scoreDistribution: {
//     strongMatch: 45,
//     goodMatch: 120,
//     moderateMatch: 180,
//     weakMatch: 100,
//     noMatch: 53
//   }
// }

// Access individual results
for (const candidate of result.results) {
  if (candidate.success) {
    console.log(`${candidate.candidateName}: ${candidate.output.core.overallScore}`);
  } else {
    console.log(`${candidate.candidateName}: FAILED - ${candidate.error}`);
  }
}

Rule-Based Only (No LLM Required)

Score candidates without any LLM configuration:

import { scoreCandidate, createJobInput } from '@pendent/ats-engine';

// No llmConfig = automatic rule-based scoring
const result = await scoreCandidate({
  candidate: { /* ... */ },
  job: createJobInput('job-123', 'Developer', 'Description'),
  // llmConfig is omitted - uses rule-based scoring only
});

console.log(result.core.overallScore); // Rule-based score
console.log(result.metadata.llmUsed);   // false

5-Layer Output Structure

Layer 1: Core ATS Output (Machine-readable)

{
  overallScore: 72.4,
  decisionBand: "GOOD_MATCH",
  confidence: 0.91,
  componentScores: {
    skillsMatch: 82,
    experienceRelevance: 68,
    educationFit: 60,
    keywordCoverage: 74,
    seniorityFit: 70,
    parseability: 88,
    formatting: 90
  },
  missingCriticalSkills: ["Kafka", "System Design"],
  matchedSkills: {
    exact: ["Node.js", "PostgreSQL", "AWS"],
    semantic: ["REST APIs", "Distributed systems"]
  }
}

Layer 2: Explainability Output (Recruiter-facing)

{
  summary: "Strong backend profile with good cloud exposure but missing streaming skills.",
  strengths: [
    "5+ years backend experience",
    "Strong Node.js and database usage",
    "AWS production exposure"
  ],
  gaps: [
    "No Kafka or event-driven systems",
    "Limited system design examples"
  ],
  riskFlags: [
    "Single company for 6 years",
    "No leadership experience mentioned"
  ],
  keyHighlights: {
    yearsOfExperience: 5,
    topSkills: ["Node.js", "PostgreSQL", "AWS"],
    educationLevel: "bachelors"
  }
}

Layer 3: Evidence Mapping (Audit-grade)

{
  skillEvidence: {
    "Node.js": [
      "Built REST APIs using Node.js and Express",
      "Microservices written in Node.js"
    ],
    "AWS": [
      "Deployed services on EC2 and RDS"
    ]
  },
  experienceEvidence: [
    {
      claim: "Node.js",
      evidence: "...built REST APIs using Node.js...",
      confidence: 0.95
    }
  ]
}

Layer 4: ATS Decision Output (Workflow-level)

{
  recommendedAction: "REVIEW",
  autoReject: false,
  humanReviewRequired: true,
  reasonCodes: ["MISSING_NON_CRITICAL_SKILLS"],
  queuePriority: 2,
  suggestedNextStep: "Verify missing skills with candidate",
  autoRejectReasons: [],     // Reasons if auto-rejected
  failedFilters: []          // Filters that failed
}

Layer 5: Candidate Feedback (Optional)

{
  readinessLevel: "NEAR_READY",
  positives: ["Strong JavaScript skills", "Good experience"],
  improvementSuggestions: [
    "Add examples of system design decisions",
    "Mention experience with event-driven architectures"
  ],
  skillsToHighlight: ["Kafka", "System Design"],
  formatSuggestions: ["Add clear section headers"],
  potentialScoreImprovement: 15
}

Configuration

The ATS engine supports extensive customization via the ATSConfig interface. See Configuration Documentation for full details.

Quick Configuration Example

import { scoreCandidate } from '@pendent/ats-engine';

const result = await scoreCandidate({
  candidate: { /* ... */ },
  job: { /* ... */ },
  options: {
    config: {
      // Decision thresholds
      passThreshold: 65,
      passReviewThreshold: 80,
      
      // Custom scoring weights (must sum to 1.0)
      scoringWeights: {
        skillsWeight: 0.35,
        experienceWeight: 0.25,
        domainWeight: 0.10,
        semanticWeight: 0.10,
        educationWeight: 0.05,
        resumeQualityWeight: 0.15,
      },
      
      // Penalty settings
      penalties: {
        missingCriticalSkillPenalty: 8,
        experienceGapPenaltyPerYear: 3,
        maxTotalPenalty: 40,
      },
      
      // Auto-reject rules
      autoRejectRules: {
        employmentGapEnabled: true,
        employmentGapMonths: 12,
        jobHoppingEnabled: true,
        jobHoppingCount: 4,
        jobHoppingYears: 3,
      },
      
      // Advanced filters
      advancedFilters: {
        skillMatchThreshold: 60,
        experienceTolerance: 2,
        minEducationLevel: "bachelors",
        educationEquivalence: true,
      },
    },
  },
});

// Check auto-reject and filter results
console.log(result.decision.autoReject);       // boolean
console.log(result.decision.autoRejectReasons); // string[]
console.log(result.decision.failedFilters);    // string[]
console.log(result.core.appliedPenalties);     // string[]

LLM Providers

OpenAI

const config = createLLMConfig('openai', 'sk-...', {
  model: 'gpt-4o-mini',
  temperature: 0.1,
});

Anthropic

const config = createLLMConfig('anthropic', 'sk-ant-...', {
  model: 'claude-sonnet-4-20250514',
});

Azure OpenAI

const config = createLLMConfig('azure', process.env.AZURE_API_KEY!, {
  model: 'gpt-4o-mini',
  azureEndpoint: 'https://your-resource.openai.azure.com',
  azureDeployment: 'your-deployment-name',
});

Google Gemini

const config = createLLMConfig('google', process.env.GOOGLE_API_KEY!, {
  model: 'gemini-1.5-pro', // or gemini-1.5-flash
});

Groq

const config = createLLMConfig('groq', process.env.GROQ_API_KEY!, {
  model: 'llama-3.1-70b-versatile',
});

OpenRouter

const config = createLLMConfig('openrouter', process.env.OPENROUTER_API_KEY!, {
  model: 'anthropic/claude-3.5-sonnet',
});

Custom OpenAI-Compatible API

const config = createLLMConfig('custom', 'your-api-key', {
  baseURL: 'https://api.your-provider.com/v1',
  model: 'your-model',
  headers: { 'X-Custom-Header': 'value' },
});

Input Types

CandidateInput

interface CandidateInput {
  // Required
  name: string;
  email: string;
  phone: string;
  linkedinUrl: string;
  resumeBuffer: Buffer;
  resumeMimeType: string;
  
  // Optional
  githubUrl?: string;
  portfolioUrl?: string;
  coverLetter?: string;
  resumeFileName?: string;
}

JobInput

interface JobInput {
  // Required
  id: string;
  title: string;
  description: string;
  
  // Optional
  category?: string;
  department?: string;
  location?: string;
  locationType?: 'REMOTE' | 'HYBRID' | 'ONSITE';
  employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERNSHIP' | 'FREELANCE';
  experienceLevel?: 'FRESHER' | 'ENTRY' | 'MID' | 'SENIOR' | 'LEAD' | 'EXECUTIVE';
  
  // Skills
  mustHaveSkills?: string[];
  niceToHaveSkills?: string[];
  
  // Experience
  requiredExperienceYears?: number;
  preferredIndustries?: string[];
}

Scoring Breakdown

Component Max Score Description
Parseability 25 ATS-friendly layout
Formatting 20 Consistent formatting
Structure 10 Required sections present
Semantic 30 Keyword coverage (LLM)
Skills 10 Required skills verified (LLM)
Experience 10 Experience alignment (LLM)
Education 10 Education fit (LLM)
Total 115 (normalized to 100)

Grade Scale

Grade Score Decision Band
A 85+ STRONG_MATCH
B 70-84 GOOD_MATCH
C 55-69 MODERATE_MATCH
D 40-54 WEAK_MATCH
F <40 NO_MATCH

Supported File Types

  • PDF (.pdf)
  • Word Documents (.docx)
  • Plain Text (.txt)

License

Private - All rights reserved.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages