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) }