"""LLM-powered resume parser — extracts a structured candidate profile.""" from __future__ import annotations import json import re _SYSTEM_PROMPT = ( "You are a professional resume analyst. Your job is to extract a structured " "profile from the raw resume text provided by the user. " "Return ONLY valid JSON — no markdown fences, no commentary, no preamble." ) _USER_TEMPLATE = """\ Here is the raw resume text: {resume_raw_text} Return a JSON object matching this exact schema: {{ "full_name": "string", "seniority_level": "intern | junior | mid | senior | staff | principal | executive", "primary_domain": "string (e.g. Backend Engineering, Product Management)", "secondary_domains": ["string (up to 3)"], "years_of_experience": "number", "technical_skills": ["string (tools, languages, frameworks)"], "soft_skills": ["string (leadership, communication, etc.)"], "industries": ["string (sectors worked in)"], "key_achievements": ["string (3–5 bullet-ready achievement statements)"], "notable_companies": ["string"], "education_summary": "string (one-line summary)", "professional_summary": "string (3–4 sentence narrative for interview context)", "contact": {{ "email": "string | null", "phone": "string | null", "location": "string | null", "linkedin": "string | null", "github": "string | null", "portfolio": "string | null" }}, "work_experience": [ {{ "company": "string", "title": "string", "start_date": "string | null (e.g. 'Jan 2021')", "end_date": "string | null (e.g. 'Present')", "highlights": ["string (key responsibilities / achievements)"] }} ], "education": [ {{ "institution": "string", "degree": "string", "field": "string | null", "graduation_year": "string | null" }} ], "certifications": ["string"] }} Do not invent information. If a field cannot be determined, use null (or [] for lists). """ def parse_resume(raw_text: str, llm) -> dict: """Call the LLM to extract a structured profile from raw resume text. Args: raw_text: Plain text extracted from the uploaded resume document. llm: A LangChain chat model instance (e.g. ChatGoogleGenerativeAI). Returns: Parsed dict matching the resume schema. Fields that cannot be determined are set to null / None. Raises: ValueError: If the LLM response cannot be parsed as JSON. """ from langchain_core.messages import HumanMessage, SystemMessage messages = [ SystemMessage(content=_SYSTEM_PROMPT), HumanMessage(content=_USER_TEMPLATE.format(resume_raw_text=raw_text)), ] from src.services.llm import _extract_text response = llm.invoke(messages) content = _extract_text(response.content if hasattr(response, "content") else response) # Strip markdown fences if the model ignores our instructions content = re.sub(r"^```(?:json)?\s*", "", content.strip()) content = re.sub(r"\s*```$", "", content.strip()) try: return json.loads(content) except json.JSONDecodeError as exc: raise ValueError(f"LLM returned invalid JSON: {exc}\n\nRaw output:\n{content}") from exc def build_gap_map(candidate_profile: dict, jd_text: str, llm) -> dict: """Compute skill overlap between the candidate profile and a job description. Returns a gap_map with three keys: - strong_match: skills present in both profile and JD - partial_match: adjacent / related skills - gaps: JD requires, candidate hasn't claimed """ from langchain_core.messages import HumanMessage, SystemMessage candidate_skills = candidate_profile.get("technical_skills") or [] soft_skills = candidate_profile.get("soft_skills") or [] all_skills = candidate_skills + soft_skills system = ( "You are a talent gap analyst. Given a candidate's skills and a job description, " "identify skill overlap and gaps. Return ONLY valid JSON — no markdown, no preamble." ) user = ( f"Candidate skills: {json.dumps(all_skills)}\n\n" f"Job description:\n{jd_text[:3000]}\n\n" "Return a JSON object:\n" "{\n" ' "strong_match": ["skills present in both profile and JD"],\n' ' "partial_match": ["adjacent or related skills"],\n' ' "gaps": ["JD requires but candidate has not claimed"]\n' "}\n" "Use only skills/requirements explicitly mentioned. Do not invent." ) from src.services.llm import _extract_text messages = [SystemMessage(content=system), HumanMessage(content=user)] response = llm.invoke(messages) content = _extract_text(response.content if hasattr(response, "content") else response) content = re.sub(r"^```(?:json)?\s*", "", content.strip()) content = re.sub(r"\s*```$", "", content.strip()) try: return json.loads(content) except json.JSONDecodeError: return {"strong_match": [], "partial_match": [], "gaps": []}