from __future__ import annotations from typing import List, Tuple, Optional import textwrap import re from .text import normalize_whitespace ACTION_VERBS = [ "Led", "Built", "Improved", "Optimized", "Delivered", "Designed", "Implemented", "Automated", "Reduced", "Increased", "Analyzed", "Developed", "Launched", "Managed", "Resolved", "Created", # Reed-recommended action words "Achieved", "Formulated", "Planned", "Generated", "Represented", "Completed", ] # Weak openers to avoid on bullets and suggested stronger replacements _WEAK_TO_STRONG = [ (re.compile(r"^\s*-\s*responsible for\s+", re.IGNORECASE), "- Led "), (re.compile(r"^\s*-\s*tasked with\s+", re.IGNORECASE), "- Executed "), (re.compile(r"^\s*-\s*worked on\s+", re.IGNORECASE), "- Delivered "), (re.compile(r"^\s*-\s*helped\s+", re.IGNORECASE), "- Supported "), (re.compile(r"^\s*-\s*assisted with\s+", re.IGNORECASE), "- Supported "), (re.compile(r"^\s*-\s*handled\s+", re.IGNORECASE), "- Managed "), ] def strengthen_action_verbs(text: str) -> str: """Promote weak bullet openers to stronger action verbs (The Muse guidance).""" if not text: return text lines = text.splitlines() out: List[str] = [] for line in lines: new_line = line for pattern, repl in _WEAK_TO_STRONG: if pattern.search(new_line): new_line = pattern.sub(repl, new_line) break out.append(new_line) return "\n".join(out) def make_bullets(lines: List[str]) -> str: clean_lines = [f"- {normalize_whitespace(l)}" for l in lines if l and l.strip()] return "\n".join(clean_lines) def ensure_keywords(text: str, keywords: List[str], max_new: int = 30, allowed_keywords: Optional[set] = None) -> Tuple[str, List[str]]: used = [] missing = [] lower_text = text.lower() for k in keywords: if k.lower() in lower_text: used.append(k) else: missing.append(k) if missing: additions = [] actually_added = [] for k in missing: if len(actually_added) >= max_new: break if allowed_keywords is not None and k.lower() not in allowed_keywords: continue additions.append(f"Experience with {k}.") actually_added.append(k) if additions: text = text.rstrip() + "\n\nKeywords: " + ", ".join(actually_added) + "\n" + make_bullets(additions) used.extend(actually_added) return text, used def format_resume_header(full_name: str, headline: str, email: str | None, phone: str | None, location: str | None, links: dict) -> str: contact_parts = [p for p in [email, phone, location] if p] links_str = " | ".join([f"{k}: {v}" for k, v in links.items()]) if links else "" top_line = f"{full_name} — {headline}" if headline else full_name contact_line = " | ".join(filter(None, [" | ".join(contact_parts), links_str])) return "\n".join([top_line, contact_line]).strip() + "\n" def format_experience_section(experiences: List[dict]) -> str: sections: List[str] = [] for exp in experiences: header = f"{exp.get('title','')} — {exp.get('company','')} ({exp.get('start_date','')} – {exp.get('end_date','Present')})" bullets = exp.get("achievements") or [] if not bullets: bullets = [ f"{ACTION_VERBS[0]} key outcomes relevant to the role.", "Collaborated cross-functionally to deliver results.", "Drove measurable impact with data-informed decisions.", ] sections.append("\n".join([header, make_bullets(bullets)])) return "\n\n".join(sections) def format_skills_section(skills: List[str]) -> str: if not skills: return "" return "Skills: " + ", ".join(skills) def basic_resume_template(header: str, summary: str | None, skills: str, experience: str, education: str | None) -> str: parts = [header] if summary: parts.append("\nSummary\n" + textwrap.fill(summary, width=100)) if skills: parts.append("\n" + skills) if experience: parts.append("\n\nExperience\n" + experience) if education: parts.append("\n\nEducation\n" + education) return "\n".join(parts).strip() + "\n" def basic_cover_letter_template(greeting: str, body_paragraphs: List[str], closing: str, signature: str) -> str: body = "\n\n".join(textwrap.fill(p, width=100) for p in body_paragraphs) return "\n".join([greeting, "", body, "", closing, "", signature]).strip() + "\n"