Resume-ATS / Process /utils.py
HARISH20205's picture
projects
979bdd1
import json
import concurrent.futures
import logging
import traceback
from typing import Dict, List, Optional, Union
from .response import get_response
# Set up logging
logger = logging.getLogger(__name__)
SYSTEM_INSTRUCTION = """
Provide responses in this exact JSON format:
{
"score": <number 0-10>,
"matching_elements": [<list of matching items>],
"missing_elements": [<list of recommended items>],
"explanation": "<explanation in 10-15 words>"
}
Ensure the score is always a number between 0-10.
"""
class ATSResumeParser:
def __init__(self):
logger.info("Initializing ATSResumeParser")
self.score_weights = {
"skills_match": 20,
"experience_relevance": 20,
"project_relevance": 15,
"education_relevance": 10,
"overall_formatting": 15,
"keyword_optimization": 10,
"extra_sections": 10,
}
self.total_weight = sum(self.score_weights.values())
logger.debug(f"Score weights configured with total weight: {self.total_weight}")
def _parse_gemini_response(self, response_text: str) -> Dict:
"""Parse the response from Gemini API with caching for better performance"""
try:
logger.debug("Parsing Gemini API response")
response = json.loads(response_text)
result = {
"score": float(response["score"]),
"matching": response.get("matching_elements", []),
"missing": response.get("missing_elements", []),
"explanation": response.get("explanation", ""),
}
logger.debug(f"Successfully parsed response with score: {result['score']}")
return result
except (json.JSONDecodeError, KeyError, ValueError) as e:
logger.error(f"Error parsing Gemini response: {e}")
logger.debug(f"Failed response content: {response_text}")
return {"score": 5.0, "matching": [], "missing": [], "explanation": ""}
except Exception as e:
logger.error(f"Unexpected error parsing Gemini response: {e}")
logger.debug(traceback.format_exc())
return {"score": 5.0, "matching": [], "missing": [], "explanation": ""}
def _score_skills(self, skills: List[str], job_description: Optional[str]) -> Dict:
"""Score skills with optimized processing"""
if not skills:
return {
"score": 0,
"matching": [],
"missing": [],
"explanation": "No skills provided",
}
base_score = 70
skills_length = len(skills)
if skills_length >= 5:
base_score += 10
if skills_length >= 10:
base_score += 10
if not job_description:
return {
"score": base_score,
"matching": skills,
"missing": [],
"explanation": "No job description provided",
}
prompt = f"Skills: {','.join(skills[:20])}. Job description: {job_description[:500]}. Rate match. in the missing section list only missing skills dont give paragraphs or any big content"
response = self._parse_gemini_response(get_response(prompt, SYSTEM_INSTRUCTION))
return {
"score": (base_score + (response["score"] * 10)) / 2,
"matching": response["matching"],
"missing": response["missing"],
"explanation": response["explanation"],
}
def _score_projects(
self, projects: List[Dict], job_description: Optional[str]
) -> Dict:
"""Score projects with optimized processing"""
print("567898765", projects)
if not projects:
return {
"score": 0,
"matching": [],
"missing": [],
"explanation": "No projects provided",
}
# Basic score based only on project count
base_score = 70
if not job_description:
return {
"score": base_score,
"matching": [p.get("title", "Untitled Project") for p in projects[:3]],
"missing": [],
"explanation": "No job description provided",
}
# Fix: Use 'name' instead of 'title' to match your data structure
simplified_projects = [
{"title": p.get("title", ""), "description": p.get("description", "")}
for p in projects[:3]
]
try:
prompt = f"""Projects: {json.dumps(simplified_projects)}. Job description: {job_description[:500]}.
Analyze how well these projects match the job requirements. In your response:
- Give specific matching elements from projects relevant to the job
- List missing project types or skills that would improve the match
- Keep lists concise with specific items, not paragraphs
- Provide a numerical score between 0-10 reflecting the overall match quality"""
response = self._parse_gemini_response(
get_response(prompt, SYSTEM_INSTRUCTION)
)
score = response.get("score", 5.0)
return {
"score": (base_score + (score * 10)) / 2,
"matching": response.get("matching", []),
"missing": response.get("missing", []),
"explanation": response.get(
"explanation", "Project assessment completed"
),
}
except Exception as e:
logger.error(f"Error in _score_projects: {e}")
logger.debug(traceback.format_exc())
return {
"score": base_score,
"matching": [p.get("name", "Untitled Project") for p in projects[:3]],
"missing": [],
"explanation": "Error analyzing project relevance",
}
def _score_experience(
self, experience: List[Dict], job_description: Optional[str]
) -> Dict:
"""Score experience with optimized processing"""
if not experience:
return {
"score": 0,
"matching": [],
"missing": [],
"explanation": "No experience provided",
}
base_score = 60
required_keys = {"title", "company", "description"}
improvement_keywords = {"increased", "decreased", "improved", "%", "reduced"}
for exp in experience:
if required_keys.issubset(exp.keys()):
base_score += 10
description = exp.get("description", "")
if description and any(
keyword in description for keyword in improvement_keywords
):
base_score += 5
if not job_description:
return {
"score": base_score,
"matching": [],
"missing": [],
"explanation": "No job description provided",
}
simplified_exp = [
{"title": e.get("title", ""), "description": e.get("description", "")[:100]}
for e in experience[:3]
]
prompt = f"Experience: {json.dumps(simplified_exp)}. Job description: {job_description[:500]}. Rate match."
response = self._parse_gemini_response(get_response(prompt, SYSTEM_INSTRUCTION))
return {
"score": (base_score + (response["score"] * 10)) / 2,
"matching": response["matching"],
"missing": response["missing"],
"explanation": response["explanation"],
}
def _score_education(self, education: List[Dict]) -> Dict:
"""Score education with optimized processing"""
if not education:
return {
"score": 0,
"matching": [],
"missing": [],
"explanation": "No education provided",
}
score = 70
matching = []
required_keys = {"institution", "degree", "start_date", "end_date"}
for edu in education:
gpa = edu.get("gpa")
if gpa and float(gpa) > 3.0:
score += 10
matching.append(f"Strong GPA: {gpa}")
if required_keys.issubset(edu.keys()):
score += 10
matching.append(
f"{edu.get('degree', '')} from {edu.get('institution', '')}"
)
return {
"score": min(100, score),
"matching": matching,
"missing": [],
"explanation": "Education assessment completed",
}
def _score_formatting(self, structured_data: Dict) -> Dict:
"""Score formatting with optimized processing"""
score = 100
contact_fields = ("name", "email", "phone")
essential_sections = ("skills", "experience", "education")
structured_keys = set(structured_data.keys())
missing_contacts = [
field for field in contact_fields if field not in structured_keys
]
if missing_contacts:
score -= 20
missing_sections = [
section for section in essential_sections if section not in structured_keys
]
missing_penalty = 15 * len(missing_sections)
if missing_sections:
score -= missing_penalty
return {
"score": max(0, score),
"matching": [field for field in contact_fields if field in structured_keys],
"missing": missing_contacts + missing_sections,
"explanation": "Format assessment completed",
}
def _score_extra(self, structured_data: Dict) -> Dict:
"""Score extra sections with optimized processing"""
extra_sections = {
"awards_and_achievements": 15,
"volunteer_experience": 10,
"hobbies_and_interests": 5,
"publications": 15,
"conferences_and_presentations": 10,
"patents": 15,
"professional_affiliations": 10,
"portfolio_links": 10,
"summary_or_objective": 10,
}
total_possible = sum(extra_sections.values())
structured_keys = set(structured_data.keys())
score = 0
matching = []
missing = []
for section, weight in extra_sections.items():
if section in structured_keys and structured_data.get(section):
score += weight
matching.append(section.replace("_", " ").title())
else:
missing.append(section.replace("_", " ").title())
normalized_score = (score * 100) // total_possible if total_possible > 0 else 0
return {
"score": normalized_score,
"matching": matching,
"missing": missing,
"explanation": "Additional sections assessment completed",
}
def parse_and_score(
self, structured_data: Dict, job_description: Optional[str] = None
) -> Dict:
"""Parse and score resume with parallel processing"""
scores = {}
feedback = {"strengths": [], "improvements": []}
detailed_feedback = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
tasks = {
"skills_match": executor.submit(
self._score_skills,
structured_data.get("skills", []),
job_description,
),
"experience_relevance": executor.submit(
self._score_experience,
structured_data.get("experience", []),
job_description,
),
"project_relevance": executor.submit(
self._score_projects,
structured_data.get("projects", []),
job_description,
),
"education_relevance": executor.submit(
self._score_education, structured_data.get("education", [])
),
"overall_formatting": executor.submit(
self._score_formatting, structured_data
),
"extra_sections": executor.submit(self._score_extra, structured_data),
}
total_score = 0
for category, future in tasks.items():
result = future.result()
scores[category] = result["score"]
weight = self.score_weights[category] / 100
total_score += result["score"] * weight
detailed_feedback[category] = {
"matching_elements": result["matching"],
"missing_elements": result["missing"],
"explanation": result["explanation"],
}
if result["score"] >= 80:
feedback["strengths"].append(f"Strong {category.replace('_', ' ')}")
elif result["score"] < 60:
feedback["improvements"].append(
f"Improve {category.replace('_', ' ')}"
)
return {
"total_score": round(total_score, 2),
"detailed_scores": scores,
"feedback": feedback,
"detailed_feedback": detailed_feedback,
}
def generate_ats_score(
structured_data: Union[Dict, str], job_des_text: Optional[str] = None
) -> Dict:
"""Generate ATS score with optimized processing"""
try:
logger.info("Starting ATS score generation")
if not structured_data:
return {"error": "No resume data provided"}
if isinstance(structured_data, str):
try:
structured_data = json.loads(structured_data)
except json.JSONDecodeError:
return {"error": "Invalid JSON format in resume data"}
parser = ATSResumeParser()
result = parser.parse_and_score(structured_data, job_des_text)
logger.info("ATS score generation completed successfully")
return {
"ats_score": result["total_score"],
"detailed_scores": result["detailed_scores"],
"feedback": result["feedback"],
"detailed_feedback": result["detailed_feedback"],
}
except Exception as e:
error_msg = f"Error generating ATS score: {e}"
logger.error(error_msg)
logger.debug(traceback.format_exc())
return {
"ats_score": 50.0,
"detailed_scores": {},
"feedback": {"error": error_msg},
}