Spaces:
Sleeping
Sleeping
import os | |
import io | |
import streamlit as st | |
import docx | |
import docx2txt | |
import tempfile | |
import time | |
import re | |
import math | |
import concurrent.futures | |
import pandas as pd | |
from functools import lru_cache | |
from transformers import pipeline | |
# Set page title and hide sidebar | |
st.set_page_config( | |
page_title="Resume-Job Fit Analyzer", | |
initial_sidebar_state="collapsed" | |
) | |
# Hide sidebar completely with custom CSS | |
st.markdown(""" | |
<style> | |
[data-testid="collapsedControl"] {display: none;} | |
section[data-testid="stSidebar"] {display: none;} | |
</style> | |
""", unsafe_allow_html=True) | |
##################################### | |
# Preload Models | |
##################################### | |
def load_models(): | |
"""Load models at startup""" | |
with st.spinner("Loading AI models... This may take a minute on first run."): | |
models = {} | |
# Use bart-base for summarization | |
models['summarizer'] = pipeline( | |
"summarization", | |
model="facebook/bart-base", | |
max_length=100, | |
truncation=True | |
) | |
# Load sentiment model for evaluation | |
models['evaluator'] = pipeline( | |
"sentiment-analysis", | |
model="distilbert/distilbert-base-uncased-finetuned-sst-2-english" | |
) | |
return models | |
# Preload models immediately when app starts | |
models = load_models() | |
##################################### | |
# Function: Extract Text from File | |
##################################### | |
def extract_text_from_file(file_obj): | |
""" | |
Extract text from .docx and .doc files. | |
Returns the extracted text or an error message if extraction fails. | |
""" | |
filename = file_obj.name | |
ext = os.path.splitext(filename)[1].lower() | |
text = "" | |
if ext == ".docx": | |
try: | |
document = docx.Document(file_obj) | |
text = "\n".join(para.text for para in document.paragraphs if para.text.strip()) | |
except Exception as e: | |
text = f"Error processing DOCX file: {e}" | |
elif ext == ".doc": | |
try: | |
# For .doc files, we need to save to a temp file | |
with tempfile.NamedTemporaryFile(delete=False, suffix='.doc') as temp_file: | |
temp_file.write(file_obj.getvalue()) | |
temp_path = temp_file.name | |
# Use docx2txt which is generally faster | |
try: | |
text = docx2txt.process(temp_path) | |
except Exception: | |
text = "Could not process .doc file. Please convert to .docx format." | |
# Clean up temp file | |
os.unlink(temp_path) | |
except Exception as e: | |
text = f"Error processing DOC file: {e}" | |
elif ext == ".txt": | |
try: | |
text = file_obj.getvalue().decode("utf-8") | |
except Exception as e: | |
text = f"Error processing TXT file: {e}" | |
else: | |
text = "Unsupported file type. Please upload a .docx, .doc, or .txt file." | |
# Limit text size for faster processing | |
return text[:15000] if text else text | |
##################################### | |
# Functions for Information Extraction | |
##################################### | |
# Cache the extraction functions to avoid reprocessing | |
def extract_name(text_start): | |
"""Extract candidate name from the beginning of resume text""" | |
# Only use the first 500 characters to speed up processing | |
lines = text_start.split('\n') | |
# Check first few non-empty lines for potential names | |
potential_name_lines = [line.strip() for line in lines[:5] if line.strip()] | |
if potential_name_lines: | |
# First line is often the name if it's short and doesn't contain common headers | |
first_line = potential_name_lines[0] | |
if 5 <= len(first_line) <= 40 and not any(x in first_line.lower() for x in ["resume", "cv", "curriculum", "vitae", "profile"]): | |
return first_line | |
# Look for lines that might contain a name | |
for line in potential_name_lines[:3]: | |
if len(line.split()) <= 4 and not any(x in line.lower() for x in ["address", "phone", "email", "resume", "cv"]): | |
return line | |
return "Unknown (please extract from resume)" | |
def extract_age(text): | |
"""Extract candidate age from resume text""" | |
# Simplified: just check a few common patterns | |
age_patterns = [ | |
r'age:?\s*(\d{1,2})', | |
r'(\d{1,2})\s*years\s*old', | |
] | |
text_lower = text.lower() | |
for pattern in age_patterns: | |
matches = re.search(pattern, text_lower) | |
if matches: | |
return matches.group(1) | |
return "Not specified" | |
def extract_industry(text, base_summary): | |
"""Extract expected job industry from resume""" | |
# Simplified industry keywords focused on the most common ones | |
industry_keywords = { | |
"technology": ["software", "programming", "developer", "IT", "tech", "computer"], | |
"finance": ["banking", "financial", "accounting", "finance", "analyst"], | |
"healthcare": ["medical", "health", "hospital", "clinical", "nurse", "doctor"], | |
"education": ["teaching", "teacher", "professor", "education", "university"], | |
"marketing": ["marketing", "advertising", "digital marketing", "social media"], | |
"engineering": ["engineer", "engineering"], | |
"data science": ["data science", "machine learning", "AI", "analytics"], | |
"information systems": ["information systems", "ERP", "systems management"] | |
} | |
# Count occurrences of industry keywords - using the summary to speed up | |
combined_text = base_summary.lower() | |
counts = {} | |
for industry, keywords in industry_keywords.items(): | |
counts[industry] = sum(combined_text.count(keyword.lower()) for keyword in keywords) | |
# Get the industry with the highest count | |
if counts: | |
likely_industry = max(counts.items(), key=lambda x: x[1]) | |
if likely_industry[1] > 0: | |
return likely_industry[0].capitalize() | |
# Check for educational background that might indicate industry | |
degrees = ["computer science", "business", "engineering", "medicine", "education", "finance", "marketing"] | |
for degree in degrees: | |
if degree in combined_text: | |
return f"{degree.capitalize()}-related field" | |
return "Not clearly specified" | |
def extract_skills_and_work(text): | |
"""Extract both skills and work experience at once to save processing time""" | |
# Common skill categories - reduced keyword list for speed | |
skill_categories = { | |
"Programming": ["Python", "Java", "JavaScript", "HTML", "CSS", "SQL", "C++", "C#", "Go"], | |
"Data Science": ["Machine Learning", "Data Analysis", "Statistics", "TensorFlow", "PyTorch", "AI", "Algorithms"], | |
"Database": ["SQL", "MySQL", "MongoDB", "Database", "NoSQL", "PostgreSQL"], | |
"Web Development": ["React", "Angular", "Node.js", "Frontend", "Backend", "Full-Stack"], | |
"Software Development": ["Agile", "Scrum", "Git", "DevOps", "Docker", "System Design"], | |
"Cloud": ["AWS", "Azure", "Google Cloud", "Cloud Computing"], | |
"Security": ["Cybersecurity", "Network Security", "Encryption", "Security"], | |
"Business": ["Project Management", "Business Analysis", "Leadership", "Teamwork"], | |
"Design": ["UX/UI", "User Experience", "Design Thinking", "Adobe"] | |
} | |
# Work experience extraction | |
work_headers = [ | |
"work experience", "professional experience", "employment history", | |
"work history", "experience" | |
] | |
next_section_headers = [ | |
"education", "skills", "certifications", "projects", "achievements" | |
] | |
# Process everything at once | |
lines = text.split('\n') | |
text_lower = text.lower() | |
# Skills extraction | |
found_skills = [] | |
for category, skills in skill_categories.items(): | |
category_skills = [] | |
for skill in skills: | |
if skill.lower() in text_lower: | |
category_skills.append(skill) | |
if category_skills: | |
found_skills.append(f"{category}: {', '.join(category_skills)}") | |
# Work experience extraction - simplified approach | |
work_section = [] | |
in_work_section = False | |
for idx, line in enumerate(lines): | |
line_lower = line.lower().strip() | |
# Start of work section | |
if not in_work_section: | |
if any(header in line_lower for header in work_headers): | |
in_work_section = True | |
continue | |
# End of work section | |
elif in_work_section: | |
if any(header in line_lower for header in next_section_headers): | |
break | |
if line.strip(): | |
work_section.append(line.strip()) | |
# Simplified work formatting | |
if not work_section: | |
work_experience = "Work experience not clearly identified" | |
else: | |
# Just take the first 5-7 lines of the work section as a summary | |
work_lines = [] | |
company_count = 0 | |
current_company = "" | |
for line in work_section: | |
# New company entry often has a date | |
if re.search(r'(19|20)\d{2}', line): | |
company_count += 1 | |
if company_count <= 3: # Limit to 3 most recent positions | |
current_company = line | |
work_lines.append(f"**{line}**") | |
else: | |
break | |
elif company_count <= 3 and len(work_lines) < 10: # Limit total lines | |
work_lines.append(line) | |
work_experience = "\nβ’ " + "\nβ’ ".join(work_lines[:7]) if work_lines else "Work experience not clearly structured" | |
skills_formatted = "\nβ’ " + "\nβ’ ".join(found_skills) if found_skills else "No specific technical skills clearly identified" | |
return skills_formatted, work_experience | |
##################################### | |
# Function: Summarize Resume Text | |
##################################### | |
def summarize_resume_text(resume_text): | |
""" | |
Generates a structured summary of the resume text | |
""" | |
start_time = time.time() | |
# First, generate a quick summary using pre-loaded model | |
max_input_length = 1024 # Model limit | |
# Only summarize the first portion of text for speed | |
text_to_summarize = resume_text[:min(len(resume_text), max_input_length)] | |
base_summary = models['summarizer'](text_to_summarize)[0]['summary_text'] | |
# Extract information in parallel where possible | |
with concurrent.futures.ThreadPoolExecutor() as executor: | |
# These can run in parallel | |
name_future = executor.submit(extract_name, resume_text[:500]) # Only use start of text | |
age_future = executor.submit(extract_age, resume_text) | |
industry_future = executor.submit(extract_industry, resume_text, base_summary) | |
skills_work_future = executor.submit(extract_skills_and_work, resume_text) | |
# Get results | |
name = name_future.result() | |
age = age_future.result() | |
industry = industry_future.result() | |
skills, work_experience = skills_work_future.result() | |
# Format the structured summary | |
formatted_summary = f"Name: {name}\n" | |
formatted_summary += f"Age: {age}\n" | |
formatted_summary += f"Expected Job Industry: {industry}\n\n" | |
formatted_summary += f"Previous Work Experience: {work_experience}\n\n" | |
formatted_summary += f"Skills: {skills}" | |
execution_time = time.time() - start_time | |
return formatted_summary, execution_time | |
##################################### | |
# Function: Extract Job Requirements | |
##################################### | |
def extract_job_requirements(job_description): | |
""" | |
Extract key requirements and skills from a job description | |
""" | |
# Common technical skill categories to look for | |
tech_skill_categories = { | |
"programming_languages": ["Python", "Java", "C++", "JavaScript", "TypeScript", "Go", "Rust", "SQL", "Ruby", "PHP", "Swift", "Kotlin"], | |
"web_technologies": ["React", "Angular", "Vue", "Node.js", "HTML", "CSS", "Django", "Flask", "Spring", "REST API", "GraphQL"], | |
"data_tech": ["Machine Learning", "TensorFlow", "PyTorch", "Data Science", "AI", "Big Data", "Deep Learning", "NLP", "Computer Vision"], | |
"cloud_devops": ["AWS", "Azure", "GCP", "Docker", "Kubernetes", "CI/CD", "Jenkins", "GitHub Actions", "Terraform", "Serverless"], | |
"database": ["SQL", "MySQL", "PostgreSQL", "MongoDB", "Redis", "Elasticsearch", "DynamoDB", "Cassandra"], | |
} | |
# Common soft skills to look for | |
soft_skills = ["Communication", "Leadership", "Teamwork", "Problem-solving", "Critical thinking", "Adaptability", "Creativity", "Time management"] | |
# Clean the text for processing | |
clean_job_text = job_description.lower() | |
# Extract job title | |
title_patterns = [ | |
r'^([^:.\n]+?)(position|role|job|opening|vacancy)', | |
r'^([^:.\n]+?)\n', | |
r'(hiring|looking for(?: a| an)?|recruiting)(?: a| an)? ([^:.\n]+?)(:-|[.:]|\n|$)' | |
] | |
job_title = "Not specified" | |
for pattern in title_patterns: | |
title_match = re.search(pattern, clean_job_text, re.IGNORECASE) | |
if title_match: | |
potential_title = title_match.group(1).strip() if len(title_match.groups()) >= 1 else title_match.group(2).strip() | |
if 3 <= len(potential_title) <= 50: # Reasonable title length | |
job_title = potential_title.capitalize() | |
break | |
# Extract years of experience | |
exp_patterns = [ | |
r'(\d+)(?:\+)?\s*(?:years|yrs)(?:\s*of)?\s*(?:experience|exp)', | |
r'experience\s*(?:of)?\s*(\d+)(?:\+)?\s*(?:years|yrs)' | |
] | |
years_required = 0 | |
for pattern in exp_patterns: | |
exp_match = re.search(pattern, clean_job_text, re.IGNORECASE) | |
if exp_match: | |
try: | |
years_required = int(exp_match.group(1)) | |
break | |
except: | |
pass | |
# Extract technical skills | |
found_tech_skills = {} | |
all_tech_skills = [] | |
for category, skills in tech_skill_categories.items(): | |
category_skills = [] | |
for skill in skills: | |
if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text): | |
category_skills.append(skill) | |
all_tech_skills.append(skill) | |
if category_skills: | |
found_tech_skills[category] = category_skills | |
# Extract soft skills | |
found_soft_skills = [] | |
for skill in soft_skills: | |
if re.search(r'\b' + re.escape(skill.lower()) + r'\b', clean_job_text): | |
found_soft_skills.append(skill) | |
# Extract educational requirements | |
edu_patterns = [ | |
r"bachelor'?s degree|bs|b\.s\.", | |
r"master'?s degree|ms|m\.s\.", | |
r"phd|ph\.d\.|doctorate", | |
r"mba|m\.b\.a\." | |
] | |
education_required = [] | |
for pattern in edu_patterns: | |
if re.search(pattern, clean_job_text, re.IGNORECASE): | |
edu_match = re.search(pattern, clean_job_text, re.IGNORECASE).group(0) | |
education_required.append(edu_match.capitalize()) | |
# Format the job requirements | |
job_requirements = { | |
"title": job_title, | |
"years_experience": years_required, | |
"technical_skills": all_tech_skills, | |
"soft_skills": found_soft_skills, | |
"education": education_required, | |
} | |
return job_requirements | |
##################################### | |
# Function: Analyze Job Fit | |
##################################### | |
def analyze_job_fit(resume_summary, job_description): | |
""" | |
Analyze how well the candidate fits the job requirements with the DistilBERT sentiment model. | |
""" | |
start_time = time.time() | |
# Extract job requirements | |
job_requirements = extract_job_requirements(job_description) | |
# Define skill categories to evaluate against | |
resume_lower = resume_summary.lower() | |
job_lower = job_description.lower() | |
# Define keyword categories based on the job description | |
# We'll dynamically build these based on the job requirements | |
skill_keywords = { | |
"technical_skills": job_requirements["technical_skills"], | |
"soft_skills": job_requirements["soft_skills"], | |
"education": job_requirements["education"], | |
} | |
# Add additional keywords from the job description for comprehensive analysis | |
additional_keywords = { | |
"problem_solving": ["problem solving", "analytical", "critical thinking", "troubleshooting", "debugging", | |
"optimization", "solution", "resolve", "analyze"], | |
"domain_knowledge": ["industry", "experience", "expertise", "knowledge", "familiar with", "understanding of"], | |
"collaboration": ["team", "collaborate", "cooperation", "cross-functional", "communication", "stakeholder"] | |
} | |
# Merge the keywords | |
skill_keywords.update(additional_keywords) | |
# Category weights with descriptive labels | |
category_weights = { | |
"technical_skills": {"weight": 0.40, "label": "Technical Skills"}, | |
"soft_skills": {"weight": 0.15, "label": "Soft Skills"}, | |
"education": {"weight": 0.10, "label": "Education"}, | |
"problem_solving": {"weight": 0.15, "label": "Problem Solving"}, | |
"domain_knowledge": {"weight": 0.10, "label": "Domain Knowledge"}, | |
"collaboration": {"weight": 0.10, "label": "Collaboration"} | |
} | |
# Calculate category scores and store detailed information | |
category_scores = {} | |
category_details = {} | |
found_skills = {} | |
for category, keywords in skill_keywords.items(): | |
if not keywords: # Skip empty categories | |
category_scores[category] = 0.0 | |
category_details[category] = { | |
"raw_percentage": 0, | |
"adjusted_score": 0, | |
"matching_keywords": [], | |
"total_keywords": 0, | |
"matches": 0 | |
} | |
found_skills[category] = [] | |
continue | |
# Find the specific matching keywords for feedback | |
category_matches = [] | |
for keyword in keywords: | |
if keyword.lower() in resume_lower: | |
category_matches.append(keyword) | |
found_skills[category] = category_matches | |
# Count matches but cap at a reasonable level | |
matches = len(category_matches) | |
total_keywords = len(keywords) | |
# Calculate raw percentage for this category | |
raw_percentage = int((matches / max(1, total_keywords)) * 100) | |
# Apply logarithmic scaling for more realistic scores | |
if matches == 0: | |
adjusted_score = 0.0 | |
else: | |
# Logarithmic scaling to prevent perfect scores | |
adjusted_score = min(0.95, (math.log(matches + 1) / math.log(min(total_keywords, 8) + 1))) | |
# Store both raw and adjusted scores for feedback | |
category_scores[category] = adjusted_score | |
category_details[category] = { | |
"raw_percentage": raw_percentage, | |
"adjusted_score": int(adjusted_score * 100), | |
"matching_keywords": category_matches, | |
"total_keywords": total_keywords, | |
"matches": matches | |
} | |
# Check for years of experience match | |
years_required = job_requirements["years_experience"] | |
# Extract years of experience from resume | |
experience_years = 0 | |
year_patterns = [ | |
r'(\d+)\s*(?:\+)?\s*years?\s*(?:of)?\s*experience', | |
r'experience\s*(?:of)?\s*(\d+)\s*(?:\+)?\s*years?' | |
] | |
for pattern in year_patterns: | |
exp_match = re.search(pattern, resume_lower) | |
if exp_match: | |
try: | |
experience_years = int(exp_match.group(1)) | |
break | |
except: | |
pass | |
# If we couldn't find explicit years, try to count based on work history | |
if experience_years == 0: | |
# Try to extract from work experience section | |
work_exp_match = re.search(r'work experience:(.*?)(?=\n\n|$)', resume_summary, re.IGNORECASE | re.DOTALL) | |
if work_exp_match: | |
work_text = work_exp_match.group(1).lower() | |
years = re.findall(r'(\d{4})\s*-\s*(\d{4}|present|current)', work_text) | |
total_years = 0 | |
for year_range in years: | |
start_year = int(year_range[0]) | |
if year_range[1].isdigit(): | |
end_year = int(year_range[1]) | |
else: | |
end_year = 2025 # Assume "present" is current year | |
total_years += (end_year - start_year) | |
experience_years = total_years | |
# Calculate experience match score | |
if years_required > 0: | |
if experience_years >= years_required: | |
exp_score = 1.0 | |
else: | |
exp_score = experience_years / years_required | |
else: | |
exp_score = 1.0 # If no specific years required, assume full match | |
category_scores["experience"] = exp_score | |
category_details["experience"] = { | |
"raw_percentage": int(exp_score * 100), | |
"adjusted_score": int(exp_score * 100), | |
"candidate_years": experience_years, | |
"required_years": years_required | |
} | |
# Calculate weighted score | |
weighted_score = 0 | |
for category, score in category_scores.items(): | |
if category in category_weights: | |
weighted_score += score * category_weights[category]["weight"] | |
# Add experience separately (not in the original weights) | |
weighted_score = (weighted_score * 0.8) + (category_scores["experience"] * 0.2) | |
# Apply final curve to keep scores in a realistic range | |
match_percentage = min(95, max(35, int(weighted_score * 100))) | |
# Prepare input for sentiment analysis | |
# Create a structured summary of the match for sentiment model | |
match_summary = f""" | |
Job title: {job_requirements['title']} | |
Match percentage: {match_percentage}% | |
Technical skills match: {category_details['technical_skills']['adjusted_score']}% | |
Required technical skills: {', '.join(job_requirements['technical_skills'][:5])} | |
Candidate has: {', '.join(found_skills['technical_skills'][:5])} | |
Experience match: {category_details['experience']['adjusted_score']}% | |
Required years: {job_requirements['years_experience']} | |
Candidate years: {experience_years} | |
Education match: {category_details['education']['adjusted_score']}% | |
Overall profile match: The candidate's skills and experience appear to {match_percentage >= 70 and "match well with" or "partially match with"} the job requirements. | |
""" | |
# Use the sentiment model to get a fit classification | |
sentiment_result = models['evaluator'](match_summary) | |
# Map sentiment analysis to our score: | |
# NEGATIVE = 0 (poor fit) | |
# POSITIVE = 1 (good fit) | |
score_mapping = { | |
"NEGATIVE": 0, | |
"POSITIVE": 1 | |
} | |
# Get the sentiment score | |
sentiment_score = score_mapping.get(sentiment_result[0]['label'], 0) | |
# Adjust the score based on the match percentage to get our 0,1,2 scale | |
if sentiment_score == 1 and match_percentage >= 85: | |
final_score = 2 # Excellent fit | |
elif sentiment_score == 1: | |
final_score = 1 # Good fit | |
else: | |
final_score = 0 # Poor fit | |
# Map to fit status | |
fit_status_map = { | |
0: "NOT FIT", | |
1: "POTENTIAL FIT", | |
2: "STRONG FIT" | |
} | |
fit_status = fit_status_map[final_score] | |
# Generate assessment summary based on the score | |
if final_score == 2: | |
assessment = f"{final_score}: The candidate is a strong match for this {job_requirements['title']} position, with excellent alignment in technical skills and experience. Their background demonstrates the required expertise in key areas such as {', '.join(found_skills['technical_skills'][:3]) if found_skills['technical_skills'] else 'relevant technical domains'}, and they possess the necessary {experience_years} years of experience (required: {years_required})." | |
elif final_score == 1: | |
assessment = f"{final_score}: The candidate shows potential for this {job_requirements['title']} position, with some good matches in required skills. They demonstrate experience with {', '.join(found_skills['technical_skills'][:2]) if found_skills['technical_skills'] else 'some relevant technologies'}, but may need development in areas like {', '.join(set(job_requirements['technical_skills']) - set(found_skills['technical_skills']))[:2] if set(job_requirements['technical_skills']) - set(found_skills['technical_skills']) else 'specific technical requirements'}." | |
else: | |
assessment = f"{final_score}: The candidate does not appear to be a strong match for this {job_requirements['title']} position. Their profile shows limited alignment with key requirements, particularly in {', '.join(set(job_requirements['technical_skills']) - set(found_skills['technical_skills']))[:3] if set(job_requirements['technical_skills']) - set(found_skills['technical_skills']) else 'required technical skills'}, and they have {experience_years} years of experience (required: {years_required})." | |
execution_time = time.time() - start_time | |
return assessment, final_score, match_percentage, category_details, job_requirements, execution_time | |
##################################### | |
# Main Streamlit Interface | |
##################################### | |
st.title("Resume-Job Fit Analyzer") | |
st.markdown( | |
""" | |
Upload your resume file in **.docx**, **.doc**, or **.txt** format and enter a job description to see how well you match with the job requirements. The app performs the following tasks: | |
1. Extracts text from your resume. | |
2. Uses AI to generate a structured candidate summary. | |
3. Analyzes how well your profile fits the specific job requirements. | |
""" | |
) | |
# Resume upload | |
uploaded_file = st.file_uploader("Upload your resume (.docx, .doc, or .txt)", type=["docx", "doc", "txt"]) | |
# Job description input | |
job_description = st.text_area("Enter Job Description", height=200, placeholder="Paste the job description here...") | |
# Process button with optimized flow | |
if uploaded_file is not None and job_description and st.button("Analyze Job Fit"): | |
# Create a placeholder for the progress bar | |
progress_bar = st.progress(0) | |
status_text = st.empty() | |
# Step 1: Extract text | |
status_text.text("Step 1/3: Extracting text from resume...") | |
resume_text = extract_text_from_file(uploaded_file) | |
progress_bar.progress(25) | |
if resume_text.startswith("Error") or resume_text == "Unsupported file type. Please upload a .docx, .doc, or .txt file.": | |
st.error(resume_text) | |
else: | |
# Step 2: Generate summary | |
status_text.text("Step 2/3: Analyzing resume and generating summary...") | |
summary, summarization_time = summarize_resume_text(resume_text) | |
progress_bar.progress(50) | |
# Display summary | |
st.subheader("Your Resume Summary") | |
st.markdown(summary) | |
st.info(f"Summary generated in {summarization_time:.2f} seconds") | |
# Step 3: Generate job fit assessment | |
status_text.text("Step 3/3: Evaluating job fit...") | |
assessment, fit_score, match_percentage, category_details, job_requirements, assessment_time = analyze_job_fit(summary, job_description) | |
progress_bar.progress(100) | |
# Clear status messages | |
status_text.empty() | |
# Display job fit results | |
st.subheader("Job Fit Assessment") | |
# Display fit score with label | |
fit_labels = { | |
0: "NOT FIT β", | |
1: "POTENTIAL FIT β οΈ", | |
2: "STRONG FIT β " | |
} | |
# Show the score prominently | |
st.markdown(f"## Overall Result: {fit_labels[fit_score]}") | |
# Display match percentage | |
if match_percentage >= 85: | |
st.success(f"**Match Score:** {match_percentage}% π") | |
elif match_percentage >= 70: | |
st.success(f"**Match Score:** {match_percentage}% β ") | |
elif match_percentage >= 50: | |
st.warning(f"**Match Score:** {match_percentage}% β οΈ") | |
else: | |
st.error(f"**Match Score:** {match_percentage}% π") | |
# Display assessment | |
st.markdown("### Assessment") | |
st.markdown(assessment) | |
# Add detailed score breakdown | |
st.markdown("### Score Breakdown") | |
# Create a neat table with category scores | |
breakdown_data = [] | |
for category, details in category_details.items(): | |
if category == "experience": | |
label = "Experience" | |
matching_info = f"{details['candidate_years']} years (Required: {details['required_years']} years)" | |
else: | |
# Get the nice label for the category | |
label = {"technical_skills": "Technical Skills", | |
"soft_skills": "Soft Skills", | |
"education": "Education", | |
"problem_solving": "Problem Solving", | |
"domain_knowledge": "Domain Knowledge", | |
"collaboration": "Collaboration"}[category] | |
matching_info = ", ".join(details["matching_keywords"][:3]) if details.get("matching_keywords") else "None detected" | |
# Add formatted breakdown row | |
breakdown_data.append({ | |
"Category": label, | |
"Score": f"{details['adjusted_score']}%", | |
"Matching Items": matching_info | |
}) | |
# Convert to DataFrame and display | |
breakdown_df = pd.DataFrame(breakdown_data) | |
# Remove the index column entirely | |
st.table(breakdown_df.set_index('Category').reset_index()) # This removes the numerical index | |
# Show a note about how scores are calculated | |
with st.expander("How are these scores calculated?"): | |
st.markdown(""" | |
- **Technical Skills** (40% of total): Evaluates programming languages, software tools, and technical requirements | |
- **Soft Skills** (15% of total): Assesses communication, teamwork, and interpersonal abilities | |
- **Education** (10% of total): Compares educational requirements with candidate's background | |
- **Problem Solving** (15% of total): Measures analytical thinking and approach to challenges | |
- **Domain Knowledge** (10% of total): Evaluates industry-specific experience and knowledge | |
- **Collaboration** (10% of total): Assesses team skills and cross-functional collaboration | |
- **Experience** (20% overall modifier): Years of relevant experience compared to job requirements | |
Scores are calculated based on keyword matches in your resume, with diminishing returns applied (first few skills matter more than later ones). | |
""") | |
st.info(f"Assessment completed in {assessment_time:.2f} seconds") | |
# Add potential next steps based on the fit score | |
st.subheader("Recommended Next Steps") | |
if fit_score == 2: | |
st.markdown(""" | |
- Consider applying for this position as you appear to be a strong match | |
- Prepare for technical interviews by focusing on your strongest skills | |
- Review the job description again to prepare for specific interview questions | |
""") | |
elif fit_score == 1: | |
st.markdown(""" | |
- Focus on highlighting your strongest matching skills in your application | |
- Consider addressing skill gaps in your cover letter by connecting your experience to the requirements | |
- Prepare to discuss how your transferable skills apply to this position | |
""") | |
else: | |
st.markdown(""" | |
- This position may not be the best fit for your current skills and experience | |
- Consider roles that better align with your demonstrated strengths | |
- If you're set on this type of position, focus on developing skills in the areas mentioned in the job description | |
""") |