autoapply-ai / utils.py
Faraz618's picture
Create utils.py
b15b3e9 verified
Raw
History Blame Contribute Delete
5.14 kB
import re
from datetime import datetime
# ── Resume Scoring ────────────────────────────────────
def score_resume_vs_job(resume_text: str, job: dict) -> int:
"""
Score how well a resume matches a job.
Returns integer 0–100.
Pure keyword + heuristic scoring (no LLM cost).
"""
if not resume_text or not job:
return 0
resume_lower = resume_text.lower()
score = 0
max_score = 0
# 1. Job title words in resume (up to 30 pts)
title_words = _tokenize(job.get("title", ""))
title_hits = sum(1 for w in title_words if w in resume_lower)
title_score = int((title_hits / max(len(title_words), 1)) * 30)
score += title_score
max_score += 30
# 2. Tags / tech stack match (up to 40 pts)
tags = [t.lower() for t in job.get("tags", [])]
tag_hits = sum(1 for t in tags if t in resume_lower)
tag_score = int((tag_hits / max(len(tags), 1)) * 40)
score += tag_score
max_score += 40
# 3. Description keywords in resume (up to 20 pts)
desc_words = _tokenize(job.get("description", ""))
important = _extract_important_words(desc_words)
desc_hits = sum(1 for w in important if w in resume_lower)
desc_score = int((desc_hits / max(len(important), 1)) * 20)
score += desc_score
max_score += 20
# 4. Resume length bonus (up to 10 pts)
word_count = len(resume_text.split())
if word_count > 300:
score += 10
elif word_count > 150:
score += 5
max_score += 10
# Normalize to 100
final = int((score / max_score) * 100) if max_score > 0 else 0
return min(final, 100)
def rank_jobs(resume_text: str, jobs: list) -> list:
"""
Return jobs sorted by match score (highest first).
Adds 'match_score' key to each job dict.
"""
scored = []
for job in jobs:
job_copy = dict(job)
job_copy["match_score"] = score_resume_vs_job(resume_text, job)
scored.append(job_copy)
scored.sort(key=lambda x: x["match_score"], reverse=True)
return scored
# ── Text Helpers ──────────────────────────────────────
def _tokenize(text: str) -> list:
"""Split text into lowercase word tokens, remove noise."""
words = re.findall(r'\b[a-zA-Z]{3,}\b', text.lower())
stopwords = {
"the", "and", "for", "with", "this", "that", "are", "you",
"will", "have", "our", "your", "from", "not", "but", "can",
"all", "work", "team", "role", "join", "looking", "able",
"must", "help", "well", "also", "more", "who", "what",
"how", "when", "they", "their", "about", "into"
}
return [w for w in words if w not in stopwords]
def _extract_important_words(words: list) -> list:
"""Return the most meaningful/unique words for matching."""
# Prioritize longer words (usually technical terms)
return list(set([w for w in words if len(w) > 4]))[:40]
def format_match_score(score: int) -> tuple:
"""Return (emoji, label, color) for a score."""
if score >= 80:
return "🟒", "Excellent Match", "green"
elif score >= 60:
return "🟑", "Good Match", "orange"
elif score >= 40:
return "🟠", "Fair Match", "orange"
else:
return "πŸ”΄", "Low Match", "red"
# ── Date Helpers ──────────────────────────────────────
def format_date(iso_string: str) -> str:
"""Convert ISO timestamp to human-readable."""
if not iso_string:
return "Unknown"
try:
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
return dt.strftime("%b %d, %Y at %I:%M %p")
except Exception:
return iso_string[:10]
def days_ago(iso_string: str) -> str:
"""Return '3 days ago' style string."""
if not iso_string:
return ""
try:
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
delta = datetime.now(dt.tzinfo) - dt
days = delta.days
if days == 0:
return "Today"
elif days == 1:
return "Yesterday"
else:
return f"{days} days ago"
except Exception:
return ""
# ── Resume Helpers ────────────────────────────────────
def extract_name_from_resume(resume_text: str) -> str:
"""Best-effort extract name from first line of resume."""
if not resume_text:
return "Candidate"
first_line = resume_text.strip().split("\n")[0].strip()
if len(first_line) < 40:
return first_line
return "Candidate"
def truncate(text: str, max_chars: int = 300) -> str:
"""Truncate text with ellipsis."""
if not text:
return ""
if len(text) <= max_chars:
return text
return text[:max_chars].rstrip() + "..."
def sanitize_for_json(text: str) -> str:
"""Remove characters that break JSON storage."""
return text.replace("\x00", "").strip()