🤖 Skills Gap Advisor
AI-Powered Resume Analysis & Career Guidance
Powered by configurable LLM providers • Available as MCP Tool
# app.py import os import re import json from dotenv import load_dotenv from llm_client import LLMClient from pypdf import PdfReader import gradio as gr # ─── Helpers ──────────────────────────────────────────────────────────────────── def extract_json(text: str) -> str: """Strip markdown fences so we can parse the JSON body.""" text = re.sub(r"```(?:json)?\s*", "", text) text = re.sub(r"\s*```", "", text) start, end = text.find("{"), text.rfind("}") return text[start:end+1] if (start != -1 and end != -1) else text def read_resume(file_obj): """Extract text from PDF or return raw UTF-8 text.""" name = file_obj.name.lower() if name.endswith(".pdf"): reader = PdfReader(file_obj.name) return "\n".join(page.extract_text() or "" for page in reader.pages) else: return file_obj.read().decode("utf-8") def analyze_resume(resume_content: str, job_title: str, job_description: str) -> dict: """Analyzes a candidate's resume against a specific job title and description. This tool evaluates the compatibility of a resume with a given job, providing a comprehensive assessment including an overall score, interview likelihood, matching skills, and areas for improvement. Args: resume_content (str): The full text content of the candidate's resume. job_title (str): The title of the job for which the resume is being evaluated. job_description (str): The detailed description of the job, including responsibilities and requirements. Returns: dict: A JSON object containing the analysis results with the following keys: - overall_score (int): A score from 1-10 indicating overall fit. - interview_likelihood (str): "High", "Medium", or "Low" likelihood of an interview. - matching_skills (list[str]): A list of skills from the resume that match the job description. - evaluation_summary (str): A concise summary of the resume's evaluation. - experience_match (str): Assessment of how well the candidate's experience matches the job. - education_match (str): Assessment of how well the candidate's education matches the job. - strengths (list[str]): Key strengths identified in the resume relevant to the job. - missing_skills (list[str]): Skills required by the job but missing from the resume. - improvement_areas (list[str]): Areas where the resume could be improved for better fit. - recommendations (list[str]): Actionable recommendations for the candidate. """ llm_provider = os.getenv("LLM_PROVIDER", "openai") llm_model = os.getenv("LLM_MODEL", "gpt-4o-mini") llm_client = LLMClient(provider=llm_provider, model=llm_model) prompt = { "role": "user", "content": ( "You are an expert career coach.\n\n" f"**Job Title:** {job_title}\n\n" "**Job Description (first 5k chars):**\n```\n" + job_description.strip()[:5000] + "\n```\n\n" "**Candidate's Resume** (first 5k chars):\n```\n" + resume_content[:5000] + "\n```\n\n" "Reply only in JSON with these keys:\n" " overall_score (int 1–10),\n" " interview_likelihood (\"High\"/\"Medium\"/\"Low\"),\n" " matching_skills […],\n" " evaluation_summary (str),\n" " experience_match (str),\n" " education_match (str),\n" " strengths […],\n" " missing_skills […],\n" " improvement_areas […],\n" " recommendations […]" ) } resp = llm_client.chat_completion( messages=[prompt] ) raw = resp.choices[0].message.content cleaned = extract_json(raw) try: return json.loads(cleaned) except json.JSONDecodeError: return {"error": "Failed to parse LLM output", "raw": raw} # ─── Formatters ───────────────────────────────────────────────────────────────── def format_summary(res): return ( f"### 🎯 Overall Score: **{res.get('overall_score','N/A')}/10**\n\n" f"### 🤝 Interview Likelihood: **{res.get('interview_likelihood','N/A')}**\n\n" f"### 🔍 Matching Skills: **{len(res.get('matching_skills',[]))} found**" ) def format_overview(res): return ( f"**Evaluation Summary:** \n{res.get('evaluation_summary','–')}\n\n" f"**Experience Match:** \n{res.get('experience_match','–')}\n\n" f"**Education Match:** \n{res.get('education_match','–')}" ) def format_strengths(res): ms = res.get("matching_skills", []) st = res.get("strengths", []) parts = ["**🔧 Matching Skills:**"] + [f"- {s}" for s in ms] or ["- None"] parts += ["\n**💪 Key Strengths:**"] + [f"- {s}" for s in st] or ["- None"] return "\n".join(parts) def format_gaps(res): mg = res.get("missing_skills", []) ia = res.get("improvement_areas", []) parts = ["**❌ Missing Skills:**"] + [f"- {s}" for s in mg] or ["- None"] parts += ["\n**🛠️ Areas for Improvement:**"] + [f"- {s}" for s in ia] or ["- None"] return "\n".join(parts) def format_recs(res): rc = res.get("recommendations", []) parts = ["**💡 Recommendations:**"] + [f"- {s}" for s in rc] or ["- None"] return "\n".join(parts) # ─── Main Logic & UI ───────────────────────────────────────────────────────────── load_dotenv() def run_agent(resume_file, job_title, job_desc): # Validate inputs if not (resume_file and job_title.strip() and job_desc.strip()): err = "**Error:** Please provide job title, job description, and resume." return err, err, err, err, err # Read and analyze resume = read_resume(resume_file) result = analyze_resume(resume, job_title, job_desc) # Handle parse errors if "error" in result: err_md = f"**Error:** {result['error']}\n\n```{result.get('raw','')}```" return err_md, err_md, err_md, err_md, err_md # Format each section summary_md = format_summary(result) overview_md = format_overview(result) strengths_md = format_strengths(result) gaps_md = format_gaps(result) recs_md = format_recs(result) return summary_md, overview_md, strengths_md, gaps_md, recs_md # Build the Gradio app app = gr.Blocks( title="Skills Gap Advisor", theme=gr.themes.Soft( primary_hue="indigo", secondary_hue="purple", neutral_hue="slate", font=gr.themes.GoogleFont("Inter") ), css=""" .gradio-container { max-width: 1200px !important; margin: 0 auto !important; background: #0f172a !important; color: #e2e8f0 !important; } .main-header { text-align: center; margin-bottom: 2rem; padding: 2rem 0; background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: #ffffff !important; border-radius: 12px; margin-bottom: 2rem; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); } .input-section { background: #1e293b !important; padding: 1.5rem; border-radius: 12px; border: 1px solid #334155; margin-bottom: 1rem; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .results-section { background: #1e293b !important; border-radius: 12px; border: 1px solid #334155; overflow: hidden; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .summary-card { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: #ffffff !important; padding: 1.5rem; border-radius: 12px; margin-bottom: 1rem; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); } .tabs { background: #1e293b !important; border-radius: 12px; padding: 1rem; } .tab-nav { border-bottom: 1px solid #334155 !important; } .tab-nav button { color: #94a3b8 !important; } .tab-nav button.selected { color: #4f46e5 !important; border-bottom: 2px solid #4f46e5 !important; } .tab-content { background: #1e293b !important; color: #e2e8f0 !important; padding: 1rem; } .markdown { color: #e2e8f0 !important; } .markdown h1, .markdown h2, .markdown h3 { color: #ffffff !important; } .markdown strong { color: #4f46e5 !important; } .markdown ul li { color: #e2e8f0 !important; } .accordion { background: #1e293b !important; border: 1px solid #334155 !important; } .accordion-title { color: #e2e8f0 !important; } .accordion-content { color: #94a3b8 !important; } .button-primary { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important; color: white !important; border: none !important; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1) !important; } .button-primary:hover { background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%) !important; transform: translateY(-1px); box-shadow: 0 6px 8px -1px rgb(0 0 0 / 0.1) !important; } """ ) with app: gr.Markdown( """
AI-Powered Resume Analysis & Career Guidance
Powered by configurable LLM providers • Available as MCP Tool