Spaces:
Sleeping
Sleeping
File size: 10,209 Bytes
0e70529 689bd71 0e70529 689bd71 0e70529 47b43eb 0e70529 47b43eb 0e70529 689bd71 0e70529 47b43eb 0e70529 47b43eb 0e70529 689bd71 0e70529 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | """
Canonical Resume data model β the SINGLE source of truth that drives both
the LLM tailoring contract (v4) and the canonical renderer.
Design principles:
- Flat list of bullets per role (no sub-sections, no Β§Β§HEADERΒ§Β§ markers)
- 5-7 bullets per role max β the LLM selects the best per JD
- No Skills/Competencies section (per project policy R6)
- Keywords live in `summary` + per-bullet text only
- One canonical visual format applied to every tailored resume
Phase 4 β adopted after the user approved Option A on 2026-06-16.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field, asdict
from typing import Optional
@dataclass
class Contact:
"""Candidate's contact line β rendered as one centered row under the name."""
phone: str = ""
email: str = ""
linkedin: str = ""
website: str = ""
location: str = "" # City, State, Country β needed for ATS "address" checks
def render_line(self) -> str:
"""Format as a single dot-separated line for the resume header."""
parts = []
if self.phone:
parts.append(self.phone)
if self.email:
parts.append(self.email)
if self.linkedin:
# Strip protocol for cleanness; the link layer adds it back
ln = self.linkedin.replace("https://", "").replace("http://", "")
parts.append(ln)
if self.website:
ws = self.website.replace("https://", "").replace("http://", "")
parts.append(ws)
if self.location:
parts.append(self.location)
return " Β· ".join(parts)
@dataclass
class Role:
"""One employment entry."""
title: str
company: str
location: str = ""
dates: str = "" # e.g. "Jan 2023 β Present"
bullets: list[str] = field(default_factory=list)
@dataclass
class Education:
"""One education entry."""
degree: str # e.g. "Diploma β Product & Brand Management"
institution: str # e.g. "IIM Rohtak"
dates: str = "" # e.g. "Mar 2023 β Sep 2023"
@dataclass
class Resume:
"""The complete canonical resume."""
name: str
contact: Contact = field(default_factory=Contact)
summary: str = "" # 4-6 sentences, opens with recruiter pitch
skills: list[str] = field(default_factory=list) # flat skill list; renderer groups into a categorized SKILLS section
roles: list[Role] = field(default_factory=list)
achievements: list[str] = field(default_factory=list)
education: list[Education] = field(default_factory=list)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Serialization (LLM round-trip)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def to_dict(self) -> dict:
return {
"name": self.name,
"contact": asdict(self.contact),
"summary": self.summary,
"skills": list(self.skills),
"roles": [asdict(r) for r in self.roles],
"achievements": list(self.achievements),
"education": [asdict(e) for e in self.education],
}
def to_json(self, indent: int = 2) -> str:
return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
@classmethod
def from_dict(cls, data: dict) -> "Resume":
c = data.get("contact") or {}
return cls(
name=data.get("name", "Your Name"),
contact=Contact(
phone=c.get("phone", ""),
email=c.get("email", ""),
linkedin=c.get("linkedin", ""),
website=c.get("website", ""),
location=c.get("location", ""),
),
summary=data.get("summary", ""),
skills=[str(s) for s in (data.get("skills") or []) if str(s).strip()],
roles=[
Role(
title=r.get("title", ""),
company=r.get("company", ""),
location=r.get("location", ""),
dates=r.get("dates", ""),
bullets=[str(b) for b in (r.get("bullets") or []) if str(b).strip()],
)
for r in (data.get("roles") or [])
],
achievements=[str(a) for a in (data.get("achievements") or []) if str(a).strip()],
education=[
Education(
degree=e.get("degree", ""),
institution=e.get("institution", ""),
dates=e.get("dates", ""),
)
for e in (data.get("education") or [])
],
)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Flat-text view (used by the ATS scorer)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def to_flat_text(self) -> str:
"""Plain-text rendering for keyword matching / ATS scoring."""
parts = [self.name]
cl = self.contact.render_line()
if cl:
parts.append(cl)
if self.summary:
parts.append("PROFESSIONAL SUMMARY")
parts.append(self.summary)
if self.skills:
parts.append("SKILLS")
# Emit in chunks of 8 (mirrors the renderer's categorized lines) so
# no single line trips the scorer's anti-spam strip (15+ separators).
for i in range(0, len(self.skills), 8):
parts.append(", ".join(self.skills[i:i + 8]))
if self.roles:
parts.append("PROFESSIONAL EXPERIENCE")
for r in self.roles:
parts.append(f"{r.title}")
meta = " Β· ".join(filter(None, [r.company, r.location]))
if meta:
parts.append(meta)
if r.dates:
parts.append(r.dates)
for b in r.bullets:
parts.append(f"β’ {b}")
if self.achievements:
parts.append("KEY ACHIEVEMENTS")
for a in self.achievements:
parts.append(f"β’ {a}")
if self.education:
parts.append("EDUCATION")
for e in self.education:
parts.append(e.degree)
meta = " Β· ".join(filter(None, [e.institution, e.dates]))
if meta:
parts.append(meta)
return "\n".join(parts)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Validation helpers
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def validate_resume(resume: Resume, strict: bool = False) -> list[str]:
"""
Return a list of validation warnings. Empty list = resume is well-formed.
If `strict=True`, also enforces canonical format limits (5-7 bullets/role,
name/contact/summary present, etc.). Used in the renderer to flag issues
before writing.
"""
issues: list[str] = []
if not resume.name or resume.name == "Your Name":
issues.append("name is missing or placeholder")
if not resume.contact.email and not resume.contact.phone:
issues.append("contact has no email or phone")
if not resume.summary or len(resume.summary) < 100:
issues.append(f"summary is too short ({len(resume.summary)} chars; min 100)")
if not resume.roles:
issues.append("no roles in experience section")
if strict:
for i, role in enumerate(resume.roles):
n = len(role.bullets)
if n < 2:
issues.append(f"role {i} ({role.title}) has only {n} bullets β recommend 3+")
elif n > 8:
issues.append(f"role {i} ({role.title}) has {n} bullets β recommend 5-7")
if not resume.education:
issues.append("no education entries (recommended)")
return issues
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# JSON Schema (for LLM prompt embedding)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RESUME_JSON_SCHEMA_DESCRIPTION = """\
{
"name": "<full name>",
"contact": {
"phone": "<phone>",
"email": "<email>",
"linkedin": "<linkedin URL>",
"website": "<optional personal site>",
"location": "<city, state, country β keep the candidate's existing value verbatim>"
},
"summary": "<4-6 sentence summary. Opens with: 'Strong-fit candidate for [role] at [company]: ...' Weave 8+ JD keywords naturally.>",
"roles": [
{
"title": "<job title>",
"company": "<company name>",
"location": "<city, country>",
"dates": "<e.g. Jan 2023 β Present>",
"bullets": [
"<Action-verb-start bullet with quantified impact and JD keywords>",
"<5-7 bullets per role; pick the best across all original sub-projects>"
]
}
],
"achievements": [
"<3-5 top quantified highlights cutting across roles>"
],
"education": [
{"degree": "<Degree β Specialization>", "institution": "<School>", "dates": "<date range>"}
]
}
"""
|