feat: add EducationEntry model, extend ResumePayload with education/achievements/career_summary
- Add EducationEntry Pydantic model (institution, degree, field, start_date, end_date) - Extend ResumePayload with career_summary str, education List[EducationEntry], achievements List[str] - Rewrite _normalize_experience to pass through Vue-native format (period/responsibilities keys) unchanged; AIHawk format (key_responsibilities/employment_period) still converted - Extend GET /api/settings/resume to fall back to user.yaml for legacy career_summary when resume YAML is missing or the field is empty
This commit is contained in:
parent
53bfe6b326
commit
eef6c33d94
1 changed files with 54 additions and 21 deletions
75
dev-api.py
75
dev-api.py
|
|
@ -2691,10 +2691,17 @@ class WorkEntry(BaseModel):
|
||||||
title: str = ""; company: str = ""; period: str = ""; location: str = ""
|
title: str = ""; company: str = ""; period: str = ""; location: str = ""
|
||||||
industry: str = ""; responsibilities: str = ""; skills: List[str] = []
|
industry: str = ""; responsibilities: str = ""; skills: List[str] = []
|
||||||
|
|
||||||
|
class EducationEntry(BaseModel):
|
||||||
|
institution: str = ""; degree: str = ""; field: str = ""
|
||||||
|
start_date: str = ""; end_date: str = ""
|
||||||
|
|
||||||
class ResumePayload(BaseModel):
|
class ResumePayload(BaseModel):
|
||||||
name: str = ""; email: str = ""; phone: str = ""; linkedin_url: str = ""
|
name: str = ""; email: str = ""; phone: str = ""; linkedin_url: str = ""
|
||||||
surname: str = ""; address: str = ""; city: str = ""; zip_code: str = ""; date_of_birth: str = ""
|
surname: str = ""; address: str = ""; city: str = ""; zip_code: str = ""; date_of_birth: str = ""
|
||||||
|
career_summary: str = ""
|
||||||
experience: List[WorkEntry] = []
|
experience: List[WorkEntry] = []
|
||||||
|
education: List[EducationEntry] = []
|
||||||
|
achievements: List[str] = []
|
||||||
salary_min: int = 0; salary_max: int = 0; notice_period: str = ""
|
salary_min: int = 0; salary_max: int = 0; notice_period: str = ""
|
||||||
remote: bool = False; relocation: bool = False
|
remote: bool = False; relocation: bool = False
|
||||||
assessment: bool = False; background_check: bool = False
|
assessment: bool = False; background_check: bool = False
|
||||||
|
|
@ -2722,32 +2729,46 @@ def _tokens_path() -> Path:
|
||||||
def _normalize_experience(raw: list) -> list:
|
def _normalize_experience(raw: list) -> list:
|
||||||
"""Normalize AIHawk-style experience entries to the Vue WorkEntry schema.
|
"""Normalize AIHawk-style experience entries to the Vue WorkEntry schema.
|
||||||
|
|
||||||
Parser / AIHawk stores: bullets (list[str]), start_date, end_date
|
AIHawk stores: key_responsibilities (numbered dicts), employment_period, skills_acquired
|
||||||
Vue WorkEntry expects: responsibilities (str), period (str)
|
Vue WorkEntry: responsibilities (str), period (str), skills (list)
|
||||||
|
If already in Vue format (has 'period' key or 'responsibilities' key), pass through unchanged.
|
||||||
"""
|
"""
|
||||||
out = []
|
out = []
|
||||||
for e in raw:
|
for e in raw:
|
||||||
if not isinstance(e, dict):
|
if not isinstance(e, dict):
|
||||||
continue
|
continue
|
||||||
entry = dict(e)
|
# Already in Vue WorkEntry format — pass through
|
||||||
# bullets → responsibilities
|
if "period" in e or "responsibilities" in e:
|
||||||
if "responsibilities" not in entry or not entry["responsibilities"]:
|
out.append({
|
||||||
bullets = entry.pop("bullets", None) or []
|
"title": e.get("title", ""),
|
||||||
if isinstance(bullets, list):
|
"company": e.get("company", ""),
|
||||||
entry["responsibilities"] = "\n".join(b for b in bullets if b)
|
"period": e.get("period", ""),
|
||||||
elif isinstance(bullets, str):
|
"location": e.get("location", ""),
|
||||||
entry["responsibilities"] = bullets
|
"industry": e.get("industry", ""),
|
||||||
|
"responsibilities": e.get("responsibilities", ""),
|
||||||
|
"skills": e.get("skills") or [],
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
# AIHawk format
|
||||||
|
resps = e.get("key_responsibilities", {})
|
||||||
|
if isinstance(resps, dict):
|
||||||
|
resp_text = "\n".join(v for v in resps.values() if isinstance(v, str))
|
||||||
|
elif isinstance(resps, list):
|
||||||
|
resp_text = "\n".join(str(r) for r in resps)
|
||||||
else:
|
else:
|
||||||
entry.pop("bullets", None)
|
resp_text = str(resps)
|
||||||
# start_date + end_date → period
|
period = e.get("employment_period", "")
|
||||||
if "period" not in entry or not entry["period"]:
|
skills_raw = e.get("skills_acquired", [])
|
||||||
start = entry.pop("start_date", "") or ""
|
skills = skills_raw if isinstance(skills_raw, list) else []
|
||||||
end = entry.pop("end_date", "") or ""
|
out.append({
|
||||||
entry["period"] = f"{start} – {end}".strip(" –") if (start or end) else ""
|
"title": e.get("position", ""),
|
||||||
else:
|
"company": e.get("company", ""),
|
||||||
entry.pop("start_date", None)
|
"period": period,
|
||||||
entry.pop("end_date", None)
|
"location": e.get("location", ""),
|
||||||
out.append(entry)
|
"industry": e.get("industry", ""),
|
||||||
|
"responsibilities": resp_text,
|
||||||
|
"skills": skills,
|
||||||
|
})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2756,12 +2777,24 @@ def get_resume():
|
||||||
try:
|
try:
|
||||||
resume_path = _resume_path()
|
resume_path = _resume_path()
|
||||||
if not resume_path.exists():
|
if not resume_path.exists():
|
||||||
|
# Backward compat: check user.yaml for career_summary
|
||||||
|
_uy = Path(_user_yaml_path())
|
||||||
|
if _uy.exists():
|
||||||
|
uy = yaml.safe_load(_uy.read_text(encoding="utf-8")) or {}
|
||||||
|
if uy.get("career_summary"):
|
||||||
|
return {"exists": False, "legacy_career_summary": uy["career_summary"]}
|
||||||
return {"exists": False}
|
return {"exists": False}
|
||||||
with open(resume_path) as f:
|
with open(resume_path, encoding="utf-8") as f:
|
||||||
data = yaml.safe_load(f) or {}
|
data = yaml.safe_load(f) or {}
|
||||||
data["exists"] = True
|
data["exists"] = True
|
||||||
if "experience" in data and isinstance(data["experience"], list):
|
if "experience" in data and isinstance(data["experience"], list):
|
||||||
data["experience"] = _normalize_experience(data["experience"])
|
data["experience"] = _normalize_experience(data["experience"])
|
||||||
|
# Backward compat: if career_summary missing from YAML, try user.yaml
|
||||||
|
if not data.get("career_summary"):
|
||||||
|
_uy = Path(_user_yaml_path())
|
||||||
|
if _uy.exists():
|
||||||
|
uy = yaml.safe_load(_uy.read_text(encoding="utf-8")) or {}
|
||||||
|
data["career_summary"] = uy.get("career_summary", "")
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue