Spaces:
Sleeping
Sleeping
| 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) | |
| } | |