Files
2026-07-13 12:29:44 +08:00

378 lines
13 KiB
Python

import os
import sys
import json
import logging
import csv
from pdf import PDFHandler
from github import fetch_and_display_github_info
from models import JSONResume, EvaluationData
from typing import List, Optional, Dict
from evaluator import ResumeEvaluator
from pathlib import Path
from prompt import DEFAULT_MODEL, MODEL_PARAMETERS
from transform import (
transform_evaluation_response,
convert_json_resume_to_text,
convert_github_data_to_text,
convert_blog_data_to_text,
)
from config import DEVELOPMENT_MODE
logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)5s - %(lineno)5d - %(funcName)33s - %(levelname)5s - %(message)s",
)
def print_evaluation_results(
evaluation: EvaluationData, candidate_name: str = "Candidate"
):
"""Print evaluation results in a readable format."""
print("\n" + "=" * 80)
print(f"📊 RESUME EVALUATION RESULTS FOR: {candidate_name}")
print("=" * 80)
if not evaluation:
print("❌ No evaluation data available")
return
# Calculate overall score
total_score = 0
max_score = 0
if hasattr(evaluation, "scores") and evaluation.scores:
for category_name, category_data in evaluation.scores.model_dump().items():
category_score = min(category_data["score"], category_data["max"])
total_score += category_score
max_score += category_data["max"]
# Log warning if score was capped
if category_score < category_data["score"]:
print(
f"⚠️ Warning: {category_name} score capped from {category_data['score']} to {category_score} (max: {category_data['max']})"
)
# Add bonus points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
total_score += evaluation.bonus_points.total
# Subtract deductions
if hasattr(evaluation, "deductions") and evaluation.deductions:
total_score -= evaluation.deductions.total
# Ensure total score doesn't exceed maximum possible score
max_possible_score = max_score + 20 # 120 (100 categories + 20 bonus)
if total_score > max_possible_score:
total_score = max_possible_score
print(f"⚠️ Warning: Total score capped at maximum possible value")
# Overall Score
print(f"\n🎯 OVERALL SCORE: {total_score:.1f}/{max_score}")
# Detailed Scores
print("\n📈 DETAILED SCORES:")
print("-" * 60)
if hasattr(evaluation, "scores") and evaluation.scores:
# Define category maximums
category_maxes = {
"open_source": 35,
"self_projects": 30,
"production": 25,
"technical_skills": 10,
}
# Open Source
if hasattr(evaluation.scores, "open_source") and evaluation.scores.open_source:
os_score = evaluation.scores.open_source
capped_score = min(os_score.score, category_maxes["open_source"])
print(f"🌐 Open Source: {capped_score}/{os_score.max}")
print(f" Evidence: {os_score.evidence}")
print()
# Self Projects
if (
hasattr(evaluation.scores, "self_projects")
and evaluation.scores.self_projects
):
sp_score = evaluation.scores.self_projects
capped_score = min(sp_score.score, category_maxes["self_projects"])
print(f"🚀 Self Projects: {capped_score}/{sp_score.max}")
print(f" Evidence: {sp_score.evidence}")
print()
# Production Experience
if hasattr(evaluation.scores, "production") and evaluation.scores.production:
prod_score = evaluation.scores.production
capped_score = min(prod_score.score, category_maxes["production"])
print(f"🏢 Production Experience: {capped_score}/{prod_score.max}")
print(f" Evidence: {prod_score.evidence}")
print()
# Technical Skills
if (
hasattr(evaluation.scores, "technical_skills")
and evaluation.scores.technical_skills
):
tech_score = evaluation.scores.technical_skills
capped_score = min(tech_score.score, category_maxes["technical_skills"])
print(f"💻 Technical Skills: {capped_score}/{tech_score.max}")
print(f" Evidence: {tech_score.evidence}")
print()
# Bonus Points
if hasattr(evaluation, "bonus_points") and evaluation.bonus_points:
print(f"\n⭐ BONUS POINTS: {evaluation.bonus_points.total}")
print("-" * 30)
print(f" {evaluation.bonus_points.breakdown}")
# Deductions
if (
hasattr(evaluation, "deductions")
and evaluation.deductions
and evaluation.deductions.total > 0
):
print(f"\n⚠️ DEDUCTIONS: -{evaluation.deductions.total}")
print("-" * 30)
if evaluation.deductions.reasons:
print(f" {evaluation.deductions.reasons}")
# Key Strengths
if hasattr(evaluation, "key_strengths") and evaluation.key_strengths:
print(f"\n✅ KEY STRENGTHS:")
print("-" * 30)
for i, strength in enumerate(evaluation.key_strengths, 1):
print(f" {i}. {strength}")
# Areas for Improvement
if (
hasattr(evaluation, "areas_for_improvement")
and evaluation.areas_for_improvement
):
print(f"\n🔧 AREAS FOR IMPROVEMENT:")
print("-" * 30)
for i, area in enumerate(evaluation.areas_for_improvement, 1):
print(f" {i}. {area}")
print("\n" + "=" * 80)
def _evaluate_resume(
resume_data: JSONResume, github_data: dict = None, blog_data: dict = None
) -> Optional[EvaluationData]:
"""Evaluate the resume using AI and display results."""
model_params = MODEL_PARAMETERS.get(DEFAULT_MODEL)
evaluator = ResumeEvaluator(model_name=DEFAULT_MODEL, model_params=model_params)
# Convert JSON resume data to text
resume_text = convert_json_resume_to_text(resume_data)
# Add GitHub data if available
if github_data:
github_text = convert_github_data_to_text(github_data)
resume_text += github_text
# Add blog data if available
if blog_data:
blog_text = convert_blog_data_to_text(blog_data)
resume_text += blog_text
# Evaluate the enhanced resume
evaluation_result = evaluator.evaluate_resume(resume_text)
# print(evaluation_result)
return evaluation_result
def is_valid_resume_data(resume_data: JSONResume) -> bool:
"""Check if the resume data has at least some extracted core content."""
if not resume_data:
return False
core_sections = [
resume_data.basics,
resume_data.work,
resume_data.education,
resume_data.skills,
resume_data.projects,
]
return any(section is not None for section in core_sections)
def find_profile(profiles, network):
if not profiles:
return None
return next(
(p for p in profiles if p.network and p.network.lower() == network.lower()),
None,
)
def main(pdf_path):
# Create cache filename based on PDF path
cache_filename = (
f"cache/resumecache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
)
github_cache_filename = (
f"cache/githubcache_{os.path.basename(pdf_path).replace('.pdf', '')}.json"
)
resume_data = None
cache_loaded = False
# Check if cache exists and we're in development mode
if DEVELOPMENT_MODE and os.path.exists(cache_filename):
print(f"Loading cached data from {cache_filename}")
try:
cached_data = json.loads(Path(cache_filename).read_text(encoding="utf-8"))
loaded_resume = JSONResume(**cached_data)
if not is_valid_resume_data(loaded_resume):
raise ValueError("Cached resume data contains no core content")
resume_data = loaded_resume
cache_loaded = True
except Exception as e:
print(f"⚠️ Warning: Invalid cache file {cache_filename}: {e}")
print("Ignoring cache and reprocessing PDF...")
try:
os.remove(cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid cache file {cache_filename}: {delete_err}"
)
if not cache_loaded:
logger.debug(
f"Extracting data from PDF"
+ (" and caching to " + cache_filename if DEVELOPMENT_MODE else "")
)
pdf_handler = PDFHandler()
resume_data = pdf_handler.extract_json_from_pdf(pdf_path)
if resume_data == None:
return None
if DEVELOPMENT_MODE:
if is_valid_resume_data(resume_data):
os.makedirs(os.path.dirname(cache_filename), exist_ok=True)
Path(cache_filename).write_text(
json.dumps(resume_data.model_dump(), indent=2, ensure_ascii=False),
encoding="utf-8",
)
else:
logger.warning(
"Newly extracted resume data is empty/invalid. Skipping cache write."
)
# Check if cache exists and we're in development mode
github_data = {}
github_cache_loaded = False
if DEVELOPMENT_MODE and os.path.exists(github_cache_filename):
print(f"Loading cached data from {github_cache_filename}")
try:
loaded_github = json.loads(
Path(github_cache_filename).read_text(encoding="utf-8")
)
if (
not isinstance(loaded_github, dict)
or not loaded_github
or "profile" not in loaded_github
):
raise ValueError("Cached GitHub data is invalid or empty")
github_data = loaded_github
github_cache_loaded = True
except Exception as e:
print(f"⚠️ Warning: Invalid GitHub cache file {github_cache_filename}: {e}")
print("Ignoring GitHub cache and refetching...")
try:
os.remove(github_cache_filename)
except Exception as delete_err:
print(
f"Failed to delete invalid GitHub cache file {github_cache_filename}: {delete_err}"
)
if not github_cache_loaded:
# Add validation to handle None values
profiles = []
if resume_data and hasattr(resume_data, "basics") and resume_data.basics:
profiles = resume_data.basics.profiles or []
github_profile = find_profile(profiles, "Github")
if github_profile:
print(
f"Fetching GitHub data"
+ (
" and caching to " + github_cache_filename
if DEVELOPMENT_MODE
else ""
)
)
github_data = fetch_and_display_github_info(github_profile.url)
if (
DEVELOPMENT_MODE
and github_data
and isinstance(github_data, dict)
and "profile" in github_data
):
os.makedirs(os.path.dirname(github_cache_filename), exist_ok=True)
Path(github_cache_filename).write_text(
json.dumps(github_data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
score = _evaluate_resume(resume_data, github_data)
# Get candidate name for display
candidate_name = os.path.basename(pdf_path).replace(".pdf", "")
if (
resume_data
and hasattr(resume_data, "basics")
and resume_data.basics
and resume_data.basics.name
):
candidate_name = resume_data.basics.name
# Print evaluation results in readable format
print_evaluation_results(score, candidate_name)
if DEVELOPMENT_MODE:
csv_row = transform_evaluation_response(
file_name=os.path.basename(pdf_path),
evaluation=score,
resume_data=resume_data,
github_data=github_data,
)
# Write CSV row to file
csv_path = "resume_evaluations.csv"
file_exists = os.path.exists(csv_path)
with open(csv_path, "a", newline="", encoding="utf-8") as csvfile:
fieldnames = list(csv_row.keys())
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write headers if file doesn't exist
if not file_exists:
writer.writeheader()
# Write the row
writer.writerow(csv_row)
return score
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python score.py <pdf_path>")
exit(1)
pdf_path = sys.argv[1]
if not os.path.exists(pdf_path):
print(f"Error: File '{pdf_path}' does not exist.")
exit(1)
main(pdf_path)