File size: 5,814 Bytes
ea9ca44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d6cc86
 
 
 
 
 
 
 
 
 
 
 
 
ea9ca44
 
 
9d6cc86
 
ea9ca44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e5bfbf
ea9ca44
 
 
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

import os
import shutil
import tempfile
from pathlib import Path
from fastapi import UploadFile
from src.ingestion.parser import parse_file
from src.extraction.person_details_extraction_gemini import extract_resume_entities_gemini
from src.extraction.job_extractor import extract_job_entities_gemini

async def analyze_ats_compatibility(resume_file: UploadFile, job_description: str):
    """
    Orchestrates the ATS analysis:
    1. Saves uploaded resume to temp file.
    2. Parses text from resume.
    3. Extracts entities from resume (using Gemini).
    4. Extracts entities from JD (using Gemini).
    5. Compares and calculates score.
    """
    temp_file_path = None
    try:
        # 1. Save UploadFile to a temporary file
        suffix = Path(resume_file.filename).suffix
        with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
            shutil.copyfileobj(resume_file.file, tmp)
            temp_file_path = tmp.name

        # 2. Parse Text
        resume_text = parse_file(temp_file_path)

        # 3. Extract Resume Entities
        # We assume extract_resume_entities_gemini takes raw text
        resume_data = extract_resume_entities_gemini(resume_text)

        # 4. Extract Job Entities
        job_data = extract_job_entities_gemini(job_description)

        # 5. Compare and Score
        analysis_result = calculate_ats_score(resume_data, job_data)

        return analysis_result

    finally:
        # Cleanup
        if temp_file_path and os.path.exists(temp_file_path):
            os.remove(temp_file_path)


def calculate_ats_score(resume_data: dict, job_data: dict) -> dict:
    """
    Compares resume entities with job requirements to generate a score and insights.
    """
    score = 0
    max_score = 100
    matches = []
    recommendations = []

    # --- 1. Skill Matching (Weight: 60%) ---
    # Merge all job skills into a set for easier lookup
    job_skills = set()
    
    def safe_add_skills(target_set, data_dict, key):
        if not data_dict: return
        items = data_dict.get(key)
        if items:
            if isinstance(items, list):
                for s in items:
                    if isinstance(s, str): target_set.add(s.lower())
            elif isinstance(items, str):
                target_set.add(items.lower())

    safe_add_skills(job_skills, job_data, "technical_skills")
    safe_add_skills(job_skills, job_data, "skills")
    
    # Merge all resume skills
    resume_skills = set()
    safe_add_skills(resume_skills, resume_data, "technical_skills")
    safe_add_skills(resume_skills, resume_data, "skills")

    # Calculate overlaps
    found_skills = job_skills.intersection(resume_skills)
    missing_skills = job_skills - resume_skills
    
    # Keyword Match Logic
    total_job_skills = len(job_skills)
    skill_score = 0
    
    if total_job_skills > 0:
        match_percentage = len(found_skills) / total_job_skills
        skill_score = match_percentage * 60  # Max 60 points
    else:
        # If no skills extracted from JD, give full marks for this section (benefit of doubt) or 0? 
        # Let's give neutral 30
        skill_score = 30

    # formatting matches for frontend
    # Priority: High (Technical), Medium (General) - Simplified for now
    for skill in found_skills:
        matches.append({"keyword": skill.title(), "found": True, "importance": "High"})
    for skill in missing_skills:
        matches.append({"keyword": skill.title(), "found": False, "importance": "High"})

    # --- 2. Experience Matching (Weight: 20%) ---
    # This is harder to match exactly without more complex logic, 
    # so we'll do a basic check if "experience" or "years" is mentioned in JD and Resume.
    # For now, we'll give partial credit just for having an experience section.
    experience_score = 0
    if resume_data.get("work_experience") and len(resume_data["work_experience"]) > 0:
        experience_score = 20
    else:
        recommendations.append("Add a 'Work Experience' section with detailed roles and achievements.")

    # --- 3. Education Matching (Weight: 10%) ---
    education_score = 0
    if resume_data.get("education") and len(resume_data["education"]) > 0:
        education_score = 10
    else:
         recommendations.append("Include an 'Education' section listing your degrees and institutions.")

    # --- 4. Formatting/Completeness (Weight: 10%) ---
    format_score = 0
    if resume_data.get("email") or resume_data.get("phone"):
        format_score += 5
    else:
        recommendations.append("Ensure your contact information (Email/Phone) is clearly visible.")
        
    if resume_data.get("summary"):
        format_score += 5
    else:
        recommendations.append("Add a professional summary at the top of your resume.")

    # --- Final Calculation ---
    total_score = int(skill_score + experience_score + education_score + format_score)
    
    # Cap at 100
    total_score = min(100, max(0, total_score))

    # Generate Summary
    summary = ""
    if total_score >= 80:
        summary = "Excellent match! Your resume is well-optimized for this role."
    elif total_score >= 60:
        summary = "Good match, but there are some missing key skills."
    else:
        summary = "Low match. Consider tailoring your resume specifically for this job description."

    if missing_skills:
        recommendations.insert(0, f"Add missing keywords: {', '.join(list(missing_skills)[:5])}...")

    return {
        "score": total_score,
        "matches": matches,
        "summary": summary,
        "recommendations": recommendations,
        "resume_data": resume_data, # Added to pre-fill the Resume Builder
        "debug_resume_skills": list(resume_skills), # Helpful for debugging
        "debug_job_skills": list(job_skills)
    }