Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -42,11 +42,10 @@ def load_models():
|
|
42 |
truncation=True
|
43 |
)
|
44 |
|
45 |
-
# Load model for evaluation
|
46 |
models['evaluator'] = pipeline(
|
47 |
-
"
|
48 |
-
model="
|
49 |
-
max_length=300
|
50 |
)
|
51 |
|
52 |
return models
|
@@ -412,7 +411,7 @@ def extract_job_requirements(job_description):
|
|
412 |
#####################################
|
413 |
def analyze_job_fit(resume_summary, job_description):
|
414 |
"""
|
415 |
-
Analyze how well the candidate fits the job requirements with
|
416 |
"""
|
417 |
start_time = time.time()
|
418 |
|
@@ -570,149 +569,67 @@ def analyze_job_fit(resume_summary, job_description):
|
|
570 |
# Apply final curve to keep scores in a realistic range
|
571 |
match_percentage = min(95, max(35, int(weighted_score * 100)))
|
572 |
|
573 |
-
#
|
574 |
-
|
|
|
|
|
|
|
575 |
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
for category, matches in found_skills.items():
|
580 |
-
if matches:
|
581 |
-
all_matching_skills.extend(matches)
|
582 |
-
|
583 |
-
top_skills = list(set(all_matching_skills))[:5] # Remove duplicates and take top 5
|
584 |
-
skills_text = ", ".join(top_skills) if top_skills else "limited relevant skills"
|
585 |
-
|
586 |
-
# Get strongest and weakest categories for more specific feedback
|
587 |
-
categories_sorted = sorted(
|
588 |
-
[(cat, category_details[cat]["adjusted_score"]) for cat in category_weights.keys() if cat in category_details],
|
589 |
-
key=lambda x: x[1],
|
590 |
-
reverse=True
|
591 |
-
)
|
592 |
-
|
593 |
-
top_category = category_weights[categories_sorted[0][0]]["label"] if categories_sorted else "Technical Skills"
|
594 |
-
weak_category = category_weights[categories_sorted[-1][0]]["label"] if categories_sorted else "Domain Knowledge"
|
595 |
-
|
596 |
-
# Create a prompt for the evaluation model
|
597 |
-
prompt = f"""
|
598 |
-
Generate a professional expert assessment for a job candidate applying for the position: {job_requirements['title']}.
|
599 |
-
Skills detected in candidate: {skills_text}.
|
600 |
-
Strongest area: {top_category} ({categories_sorted[0][1]}%).
|
601 |
-
Weakest area: {weak_category} ({categories_sorted[-1][1]}%).
|
602 |
-
Overall match: {match_percentage}%.
|
603 |
-
Fit status: {fit_status}
|
604 |
-
|
605 |
-
Write an evaluative assessment that analyzes the candidate's fit for this position.
|
606 |
-
Start with "{fit_status}: This candidate" and provide a professional evaluation of their fit.
|
607 |
-
|
608 |
-
{fit_status}: This candidate"""
|
609 |
-
|
610 |
-
try:
|
611 |
-
# Generate the assessment using the evaluation model
|
612 |
-
assessment_results = models['evaluator'](
|
613 |
-
prompt,
|
614 |
-
max_length=300,
|
615 |
-
do_sample=True,
|
616 |
-
temperature=0.75,
|
617 |
-
num_return_sequences=3
|
618 |
-
)
|
619 |
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
# Get the raw text
|
624 |
-
raw_text = result['generated_text'].strip()
|
625 |
-
|
626 |
-
# Extract just the part that starts with the fit status
|
627 |
-
if f"{fit_status}: This candidate" in raw_text:
|
628 |
-
# Find the start of the actual assessment
|
629 |
-
start_idx = raw_text.find(f"{fit_status}: This candidate")
|
630 |
-
text = raw_text[start_idx:]
|
631 |
-
|
632 |
-
# Check if it's actually an assessment (not just instructions)
|
633 |
-
if len(text) > 50 and not any(x in text.lower() for x in [
|
634 |
-
"actionable advice",
|
635 |
-
"include specific",
|
636 |
-
"make an assessment",
|
637 |
-
"evaluate their",
|
638 |
-
"assess their",
|
639 |
-
"provide specific areas"
|
640 |
-
]):
|
641 |
-
best_assessment = text
|
642 |
-
break
|
643 |
|
644 |
-
|
645 |
-
if best_assessment:
|
646 |
-
assessment = best_assessment
|
647 |
-
else:
|
648 |
-
# Generate a completely manual assessment
|
649 |
-
assessment = generate_fallback_assessment(
|
650 |
-
resume_summary,
|
651 |
-
job_requirements,
|
652 |
-
match_percentage,
|
653 |
-
top_skills,
|
654 |
-
top_category,
|
655 |
-
weak_category,
|
656 |
-
fit_status
|
657 |
-
)
|
658 |
-
|
659 |
-
except Exception as e:
|
660 |
-
# Fallback to a manual assessment
|
661 |
-
assessment = generate_fallback_assessment(
|
662 |
-
resume_summary,
|
663 |
-
job_requirements,
|
664 |
-
match_percentage,
|
665 |
-
top_skills,
|
666 |
-
top_category,
|
667 |
-
weak_category,
|
668 |
-
fit_status
|
669 |
-
)
|
670 |
-
|
671 |
-
# Final cleanup
|
672 |
-
assessment = re.sub(r'include specific actionable advice.*?improvement\.', '', assessment, flags=re.DOTALL|re.IGNORECASE)
|
673 |
-
assessment = re.sub(r'make an assessment.*?resume\.', '', assessment, flags=re.DOTALL|re.IGNORECASE)
|
674 |
-
assessment = re.sub(r'evaluate their technical skills.*?position\.', '', assessment, flags=re.DOTALL|re.IGNORECASE)
|
675 |
-
assessment = re.sub(r'assess their strengths.*?contributions', '', assessment, flags=re.DOTALL|re.IGNORECASE)
|
676 |
-
assessment = re.sub(r'provide specific areas.*?needed', '', assessment, flags=re.DOTALL|re.IGNORECASE)
|
677 |
-
assessment = re.sub(r'give an overall.*?position', '', assessment, flags=re.DOTALL|re.IGNORECASE)
|
678 |
-
|
679 |
-
# Clean up any double spaces, newlines, etc.
|
680 |
-
assessment = re.sub(r'\s+', ' ', assessment)
|
681 |
-
assessment = assessment.strip()
|
682 |
-
|
683 |
-
# If cleaning removed too much text, use the fallback
|
684 |
-
if len(assessment) < 50 or not assessment.startswith(f"{fit_status}: This candidate"):
|
685 |
-
assessment = generate_fallback_assessment(
|
686 |
-
resume_summary,
|
687 |
-
job_requirements,
|
688 |
-
match_percentage,
|
689 |
-
top_skills,
|
690 |
-
top_category,
|
691 |
-
weak_category,
|
692 |
-
fit_status
|
693 |
-
)
|
694 |
|
695 |
-
|
696 |
-
|
697 |
|
698 |
-
|
|
|
699 |
|
700 |
-
|
701 |
-
|
702 |
-
#
|
703 |
-
|
704 |
-
|
705 |
-
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
|
|
|
|
|
|
|
|
|
|
711 |
else:
|
712 |
-
|
713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
714 |
|
715 |
-
return assessment
|
716 |
|
717 |
#####################################
|
718 |
# Main Streamlit Interface
|
@@ -759,7 +676,7 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
|
|
759 |
|
760 |
# Step 3: Generate job fit assessment
|
761 |
status_text.text("Step 3/3: Evaluating job fit...")
|
762 |
-
assessment, match_percentage, category_details, job_requirements, assessment_time = analyze_job_fit(summary, job_description)
|
763 |
progress_bar.progress(100)
|
764 |
|
765 |
# Clear status messages
|
@@ -768,15 +685,29 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
|
|
768 |
# Display job fit results
|
769 |
st.subheader("Job Fit Assessment")
|
770 |
|
771 |
-
# Display
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
772 |
if match_percentage >= 85:
|
773 |
-
st.success(f"**
|
774 |
elif match_percentage >= 70:
|
775 |
-
st.success(f"**
|
776 |
elif match_percentage >= 50:
|
777 |
-
st.warning(f"**
|
778 |
else:
|
779 |
-
st.error(f"**
|
|
|
|
|
|
|
|
|
780 |
|
781 |
# Add detailed score breakdown
|
782 |
st.markdown("### Score Breakdown")
|
@@ -824,30 +755,26 @@ if uploaded_file is not None and job_description and st.button("Analyze Job Fit"
|
|
824 |
Scores are calculated based on keyword matches in your resume, with diminishing returns applied (first few skills matter more than later ones).
|
825 |
""")
|
826 |
|
827 |
-
# Display assessment
|
828 |
-
st.markdown("### Expert Assessment")
|
829 |
-
st.markdown(assessment)
|
830 |
-
|
831 |
st.info(f"Assessment completed in {assessment_time:.2f} seconds")
|
832 |
|
833 |
-
# Add potential next steps based on the
|
834 |
st.subheader("Recommended Next Steps")
|
835 |
|
836 |
-
if
|
837 |
st.markdown("""
|
838 |
- Consider applying for this position as you appear to be a strong match
|
839 |
- Prepare for technical interviews by focusing on your strongest skills
|
840 |
- Review the job description again to prepare for specific interview questions
|
841 |
""")
|
842 |
-
elif
|
843 |
st.markdown("""
|
844 |
-
- Focus on
|
845 |
-
-
|
846 |
-
-
|
847 |
""")
|
848 |
else:
|
849 |
st.markdown("""
|
850 |
- This position may not be the best fit for your current skills and experience
|
851 |
-
- Consider roles that better align with your strengths
|
852 |
- If you're set on this type of position, focus on developing skills in the areas mentioned in the job description
|
853 |
""")
|
|
|
42 |
truncation=True
|
43 |
)
|
44 |
|
45 |
+
# Load sentiment model for evaluation
|
46 |
models['evaluator'] = pipeline(
|
47 |
+
"sentiment-analysis",
|
48 |
+
model="distilbert/distilbert-base-uncased-finetuned-sst-2-english"
|
|
|
49 |
)
|
50 |
|
51 |
return models
|
|
|
411 |
#####################################
|
412 |
def analyze_job_fit(resume_summary, job_description):
|
413 |
"""
|
414 |
+
Analyze how well the candidate fits the job requirements with the DistilBERT sentiment model.
|
415 |
"""
|
416 |
start_time = time.time()
|
417 |
|
|
|
569 |
# Apply final curve to keep scores in a realistic range
|
570 |
match_percentage = min(95, max(35, int(weighted_score * 100)))
|
571 |
|
572 |
+
# Prepare input for sentiment analysis
|
573 |
+
# Create a structured summary of the match for sentiment model
|
574 |
+
match_summary = f"""
|
575 |
+
Job title: {job_requirements['title']}
|
576 |
+
Match percentage: {match_percentage}%
|
577 |
|
578 |
+
Technical skills match: {category_details['technical_skills']['adjusted_score']}%
|
579 |
+
Required technical skills: {', '.join(job_requirements['technical_skills'][:5])}
|
580 |
+
Candidate has: {', '.join(found_skills['technical_skills'][:5])}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
581 |
|
582 |
+
Experience match: {category_details['experience']['adjusted_score']}%
|
583 |
+
Required years: {job_requirements['years_experience']}
|
584 |
+
Candidate years: {experience_years}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
585 |
|
586 |
+
Education match: {category_details['education']['adjusted_score']}%
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
587 |
|
588 |
+
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.
|
589 |
+
"""
|
590 |
|
591 |
+
# Use the sentiment model to get a fit classification
|
592 |
+
sentiment_result = models['evaluator'](match_summary)
|
593 |
|
594 |
+
# Map sentiment analysis to our score:
|
595 |
+
# NEGATIVE = 0 (poor fit)
|
596 |
+
# POSITIVE = 1 (good fit)
|
597 |
+
score_mapping = {
|
598 |
+
"NEGATIVE": 0,
|
599 |
+
"POSITIVE": 1
|
600 |
+
}
|
601 |
+
|
602 |
+
# Get the sentiment score
|
603 |
+
sentiment_score = score_mapping.get(sentiment_result[0]['label'], 0)
|
604 |
+
|
605 |
+
# Adjust the score based on the match percentage to get our 0,1,2 scale
|
606 |
+
if sentiment_score == 1 and match_percentage >= 85:
|
607 |
+
final_score = 2 # Excellent fit
|
608 |
+
elif sentiment_score == 1:
|
609 |
+
final_score = 1 # Good fit
|
610 |
else:
|
611 |
+
final_score = 0 # Poor fit
|
612 |
+
|
613 |
+
# Map to fit status
|
614 |
+
fit_status_map = {
|
615 |
+
0: "NOT FIT",
|
616 |
+
1: "POTENTIAL FIT",
|
617 |
+
2: "STRONG FIT"
|
618 |
+
}
|
619 |
+
|
620 |
+
fit_status = fit_status_map[final_score]
|
621 |
+
|
622 |
+
# Generate assessment summary based on the score
|
623 |
+
if final_score == 2:
|
624 |
+
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})."
|
625 |
+
elif final_score == 1:
|
626 |
+
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'}."
|
627 |
+
else:
|
628 |
+
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})."
|
629 |
+
|
630 |
+
execution_time = time.time() - start_time
|
631 |
|
632 |
+
return assessment, final_score, match_percentage, category_details, job_requirements, execution_time
|
633 |
|
634 |
#####################################
|
635 |
# Main Streamlit Interface
|
|
|
676 |
|
677 |
# Step 3: Generate job fit assessment
|
678 |
status_text.text("Step 3/3: Evaluating job fit...")
|
679 |
+
assessment, fit_score, match_percentage, category_details, job_requirements, assessment_time = analyze_job_fit(summary, job_description)
|
680 |
progress_bar.progress(100)
|
681 |
|
682 |
# Clear status messages
|
|
|
685 |
# Display job fit results
|
686 |
st.subheader("Job Fit Assessment")
|
687 |
|
688 |
+
# Display fit score with label
|
689 |
+
fit_labels = {
|
690 |
+
0: "NOT FIT β",
|
691 |
+
1: "POTENTIAL FIT β οΈ",
|
692 |
+
2: "STRONG FIT β
"
|
693 |
+
}
|
694 |
+
|
695 |
+
# Show the score prominently
|
696 |
+
st.markdown(f"## Overall Result: {fit_labels[fit_score]}")
|
697 |
+
|
698 |
+
# Display match percentage
|
699 |
if match_percentage >= 85:
|
700 |
+
st.success(f"**Match Score:** {match_percentage}% π")
|
701 |
elif match_percentage >= 70:
|
702 |
+
st.success(f"**Match Score:** {match_percentage}% β
")
|
703 |
elif match_percentage >= 50:
|
704 |
+
st.warning(f"**Match Score:** {match_percentage}% β οΈ")
|
705 |
else:
|
706 |
+
st.error(f"**Match Score:** {match_percentage}% π")
|
707 |
+
|
708 |
+
# Display assessment
|
709 |
+
st.markdown("### Assessment")
|
710 |
+
st.markdown(assessment)
|
711 |
|
712 |
# Add detailed score breakdown
|
713 |
st.markdown("### Score Breakdown")
|
|
|
755 |
Scores are calculated based on keyword matches in your resume, with diminishing returns applied (first few skills matter more than later ones).
|
756 |
""")
|
757 |
|
|
|
|
|
|
|
|
|
758 |
st.info(f"Assessment completed in {assessment_time:.2f} seconds")
|
759 |
|
760 |
+
# Add potential next steps based on the fit score
|
761 |
st.subheader("Recommended Next Steps")
|
762 |
|
763 |
+
if fit_score == 2:
|
764 |
st.markdown("""
|
765 |
- Consider applying for this position as you appear to be a strong match
|
766 |
- Prepare for technical interviews by focusing on your strongest skills
|
767 |
- Review the job description again to prepare for specific interview questions
|
768 |
""")
|
769 |
+
elif fit_score == 1:
|
770 |
st.markdown("""
|
771 |
+
- Focus on highlighting your strongest matching skills in your application
|
772 |
+
- Consider addressing skill gaps in your cover letter by connecting your experience to the requirements
|
773 |
+
- Prepare to discuss how your transferable skills apply to this position
|
774 |
""")
|
775 |
else:
|
776 |
st.markdown("""
|
777 |
- This position may not be the best fit for your current skills and experience
|
778 |
+
- Consider roles that better align with your demonstrated strengths
|
779 |
- If you're set on this type of position, focus on developing skills in the areas mentioned in the job description
|
780 |
""")
|