iris_backend / backend /src /services /ats_service.py
sameer2026's picture
fix: ats resume builder UI and backend extraction handling
9d6cc86
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)
}