|
|
|
|
|
import os |
|
import re |
|
import json |
|
from dotenv import load_dotenv |
|
from llm_client import LLMClient |
|
from pypdf import PdfReader |
|
import gradio as gr |
|
|
|
|
|
|
|
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} |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
load_dotenv() |
|
|
|
def run_agent(resume_file, job_title, job_desc): |
|
|
|
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 |
|
|
|
|
|
resume = read_resume(resume_file) |
|
result = analyze_resume(resume, job_title, job_desc) |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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( |
|
""" |
|
<div class="main-header"> |
|
<h1>π€ Skills Gap Advisor</h1> |
|
<p>AI-Powered Resume Analysis & Career Guidance</p> |
|
<p><em>Powered by configurable LLM providers β’ Available as MCP Tool</em></p> |
|
</div> |
|
""", |
|
elem_classes=["main-header"] |
|
) |
|
|
|
with gr.Row(): |
|
|
|
with gr.Column(scale=1, elem_classes=["input-section"]): |
|
gr.Markdown("### π Job & Resume Information") |
|
title_input = gr.Textbox( |
|
label="π·οΈ Job Title", |
|
placeholder="e.g. Senior Data Scientist", |
|
info="Enter the exact job title you're applying for" |
|
) |
|
job_input = gr.Textbox( |
|
label="π Job Description", |
|
lines=6, |
|
placeholder="Copy-paste the complete job description here...", |
|
info="Include requirements, responsibilities, and qualifications" |
|
) |
|
resume_input = gr.File( |
|
label="π Resume Upload", |
|
file_types=[".pdf", ".txt"] |
|
) |
|
|
|
with gr.Accordion("βοΈ Advanced Settings", open=False): |
|
gr.Markdown("**LLM Configuration** (via environment variables)") |
|
gr.Markdown( |
|
""" |
|
- `LLM_PROVIDER`: openai, gemini, deepseek |
|
- `LLM_MODEL`: Model name (e.g., gpt-4o-mini, gemini-2.0-flash, deepseek-chat) |
|
- API keys: `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPSEEK_API_KEY` |
|
""" |
|
) |
|
|
|
run_btn = gr.Button( |
|
"π Analyze Resume", |
|
variant="primary", |
|
size="lg", |
|
scale=1, |
|
elem_classes=["button-primary"] |
|
) |
|
|
|
|
|
with gr.Column(scale=2, elem_classes=["results-section"]): |
|
gr.Markdown("### π Analysis Results") |
|
|
|
summary_display = gr.Markdown( |
|
elem_classes=["summary-card"], |
|
value="Upload a resume and job description to see the analysis results here." |
|
) |
|
|
|
with gr.Tabs(elem_classes=["tabs"]): |
|
with gr.TabItem("π Overview", elem_id="overview-tab"): |
|
overview_display = gr.Markdown( |
|
value="Detailed evaluation summary will appear here after analysis." |
|
) |
|
with gr.TabItem("πͺ Strengths", elem_id="strengths-tab"): |
|
strengths_display = gr.Markdown( |
|
value="Your matching skills and key strengths will be highlighted here." |
|
) |
|
with gr.TabItem("π― Gaps", elem_id="gaps-tab"): |
|
gaps_display = gr.Markdown( |
|
value="Missing skills and improvement areas will be identified here." |
|
) |
|
with gr.TabItem("π‘ Recommendations", elem_id="recommendations-tab"): |
|
recs_display = gr.Markdown( |
|
value="Personalized recommendations for improving your application will appear here." |
|
) |
|
|
|
|
|
run_btn.click( |
|
fn=run_agent, |
|
inputs=[resume_input, title_input, job_input], |
|
outputs=[ |
|
summary_display, |
|
overview_display, |
|
strengths_display, |
|
gaps_display, |
|
recs_display |
|
] |
|
) |
|
|
|
if __name__ == "__main__": |
|
app.queue() |
|
app.launch( |
|
server_name="0.0.0.0", |
|
server_port=7860, |
|
debug=True, |
|
mcp_server=True |
|
) |