File size: 2,787 Bytes
d2b7a80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c72612
 
 
 
 
 
d2b7a80
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from utilities.keyword_match import final_ats_score, experience_level_penalty
from utilities.skills import (
    find_missing_skills,
    calculate_skill_overlap,
    extract_resume_skills,
    extract_required_skills_from_jd,
)
from services.feedback import generate_resume_feedback


# ---------------------------------------------------------------------------
# Gap analysis
# ---------------------------------------------------------------------------

def extract_gaps(resume_text: str, jd_text: str) -> dict:
    """
    Builds a structured gap report used both for the LLM feedback prompt
    and for any structured API response fields you add later.

    Fields
    ------
    missing_keywords        Top-10 skills in JD that are absent from resume.
    skill_overlap_pct       % of JD skills present in resume.
    matched_skills          Skills the candidate already has that JD wants.
    high_priority_missing   Missing skills that appear more than once in JD
                            (JD emphasises them → higher impact gaps).
    seniority_penalty       Penalty points from experience-level mismatch.
    """
    missing          = find_missing_skills(resume_text, jd_text)
    overlap          = calculate_skill_overlap(resume_text, jd_text)
    resume_skills    = extract_resume_skills(resume_text)
    jd_skills_freq   = extract_required_skills_from_jd(jd_text)

    matched = sorted(resume_skills & set(jd_skills_freq.keys()))

    # Skills the JD mentions more than once — candidate should prioritise these
    high_priority_missing = [
        skill for skill in missing
        if jd_skills_freq.get(skill, 0) > 1
    ]

    penalty = experience_level_penalty(resume_text, jd_text)

    return {
        "missing_keywords":       missing[:10],
        "skill_overlap_percentage": overlap,
        "matched_skills":         matched,
        "high_priority_missing":  high_priority_missing[:5],
        "seniority_penalty":      penalty,
    }


# ---------------------------------------------------------------------------
# Main scoring entry point
# ---------------------------------------------------------------------------

def resume_score(resume_text: str, jd_text: str) -> dict:
    """
    Orchestrates scoring → gap analysis → LLM feedback.

    Returns a dict matching ScoreResponse schema plus a 'summary' field.

    Note: text cleaning is intentionally left to each scoring/utility
    function so that sentence boundaries (newlines, punctuation) are
    preserved for semantic chunking before they are stripped.
    """
    scores = final_ats_score(resume_text, jd_text)
    gaps   = extract_gaps(resume_text, jd_text)

    feedback = generate_resume_feedback(scores, gaps)

    return {
        **scores,
        "summary": feedback,
    }