CareerForge / app.py
arundh987's picture
Update app.py
1defc09 verified
raw
history blame
9.86 kB
import os
import re
from typing import Tuple, List
import gradio as gr
# =============================
# Config
# =============================
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
USE_OPENAI = bool(OPENAI_API_KEY)
# Prefer the most reliable model first, with fallbacks
PREFERRED_MODELS = ["gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"]
oai_client = None
if USE_OPENAI:
try:
from openai import OpenAI
oai_client = OpenAI(api_key=OPENAI_API_KEY)
except Exception:
USE_OPENAI = False
# =============================
# Helpers
# =============================
def normalize_text(t: str) -> str:
return re.sub(r"\s+", " ", (t or "").strip())
def keyword_match_score(resume: str, jd: str) -> Tuple[float, List[str]]:
"""Simple keyword overlap metric for demo."""
def toks(s):
return {w.lower() for w in re.findall(r"[A-Za-z][A-Za-z\-]{3,}", s)}
r_set = toks(resume)
j_set = toks(jd)
if not r_set or not j_set:
return 0.0, []
overlap = sorted(list((r_set & j_set)))
score = 100.0 * len(overlap) / max(1, len(j_set))
return round(score, 1), overlap[:30]
def call_openai_with_fallback(system_prompt: str, user_prompt: str) -> str:
"""Try several models; return first successful response or empty string."""
if not (USE_OPENAI and oai_client):
return ""
for model in PREFERRED_MODELS:
try:
resp = oai_client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0.5,
)
content = resp.choices[0].message.content
if content:
return content.strip()
except Exception:
# Try next model in the list
continue
return ""
# =============================
# Prompts
# =============================
SYS_BASE = (
"You are CareerForge, a concise, supportive career assistant. "
"Prioritize clarity, actionability, fairness, and transparency. "
"Never fabricate facts about the candidate."
)
def prompt_resume_improvements(resume: str, jd: str) -> str:
return (
f"Job Description:\n{jd}\n\n"
f"Resume:\n{resume}\n\n"
"Task:\n"
"1) Give a brief fit summary (2–3 sentences).\n"
"2) List 6–8 bullet-point improvements grouped under headings: "
"'Impact Metrics', 'Skills & Keywords', 'Experience Rewording', 'ATS Hygiene'.\n"
"3) Provide an optimized 3–5 line professional summary."
)
def prompt_cover_letter(resume: str, jd: str) -> str:
return (
f"Job Description:\n{jd}\n\n"
f"Resume:\n{resume}\n\n"
"Generate a 3-paragraph cover letter draft (150–220 words): "
"(1) hook + role match, (2) 2–3 aligned achievements, (3) brief close."
)
def prompt_mock_interview(resume: str, jd: str, style: str) -> str:
return (
f"Style: {style}. Ask 5 questions tailored to the job description and resume.\n\n"
f"Job Description:\n{jd}\n\n"
f"Resume:\n{resume}\n\n"
"For each question, provide:\n"
"- The QUESTION\n"
"- A STRONG SAMPLE ANSWER (6–8 sentences)\n"
"- A 1–2 sentence FEEDBACK TIP"
)
BIAS_REFLECTION = (
"### Bias & Fairness Reflection\n\n"
"Risks:\n"
"• Over-weighting prestigious schools or employers\n"
"• Penalizing career gaps\n"
"• Favoring culture-specific language\n"
"• Keyword stuffing bias\n\n"
"Mitigations:\n"
"• Neutral, skill-first phrasing\n"
"• Prompts discourage demographic inferences\n"
"• Encourage transferable skills\n"
"• Transparent keyword match score\n\n"
"Limitations: Human review is always essential."
)
# =============================
# Core Functions
# =============================
def ingest_inputs(jd_text: str, resume_text: str):
jd_clean = normalize_text(jd_text)
resume_clean = normalize_text(resume_text)
score, matched = keyword_match_score(resume_clean, jd_clean)
visual = f"Match Score: {score}/100\nTop Overlaps: {', '.join(matched) if matched else '—'}"
return jd_clean, resume_clean, visual
def generate_improvements(jd_clean: str, resume_clean: str):
if not jd_clean or not resume_clean:
return "Please provide both Job Description and Resume."
out = call_openai_with_fallback(SYS_BASE, prompt_resume_improvements(resume_clean, jd_clean))
if out:
return out
# Mock fallback
score, matched = keyword_match_score(resume_clean, jd_clean)
return (
f"(Mock Output)\nResume aligns moderately (score {score}).\n\n"
"Improvements:\n"
"• Impact Metrics: add measurable outcomes (e.g., “reduced processing time by 18%”).\n"
"• Skills & Keywords: surface real skills from the JD: " + ", ".join(matched[:8]) + "\n"
"• Experience Rewording: lead with strong verbs; include tools.\n"
"• ATS Hygiene: standard headers; avoid images/tables.\n\n"
"Optimized Summary: Results-driven candidate aligning skills and impact with the posted role."
)
def generate_cover_letter(jd_clean: str, resume_clean: str):
if not jd_clean or not resume_clean:
return "Please provide both Job Description and Resume."
out = call_openai_with_fallback(SYS_BASE, prompt_cover_letter(resume_clean, jd_clean))
if out:
return out
return (
"(Mock Cover Letter)\n"
"Dear Hiring Manager,\n\n"
"I’m excited to apply for this role. My background in process improvement and collaboration aligns with your needs. "
"Recently, I contributed to a workflow update that improved turnaround time and reporting clarity.\n\n"
"I enjoy translating goals into actionable plans and using data to measure outcomes. "
"I’m eager to support your team’s objectives and would welcome the chance to discuss how I can help.\n\n"
"Sincerely,\nCandidate"
)
def generate_mock_interview(jd_clean: str, resume_clean: str, style: str):
if not jd_clean or not resume_clean:
return "Please provide both Job Description and Resume."
out = call_openai_with_fallback(SYS_BASE, prompt_mock_interview(resume_clean, jd_clean, style))
if out:
return out
return (
f"(Mock {style} Interview)\n\n"
"Q1: Tell me about a project relevant to this role.\n"
"A: I mapped requirements, built a quick prototype, iterated with feedback, and reduced cycle time.\n"
"Tip: Add concrete metrics and tools.\n\n"
"Q2: How do you prioritize conflicting tasks?\n"
"A: Clarify goals, assess impact/urgency, time-box experiments, and communicate trade-offs early.\n"
"Tip: Give a brief example with outcome.\n\n"
"Q3: Describe a time you handled ambiguity.\n"
"A: I proposed a small experiment, gathered data, and adjusted scope based on results.\n"
"Tip: Share what you learned.\n\n"
"Q4: How do you measure success?\n"
"A: Define indicators upfront and review them with stakeholders.\n"
"Tip: Tie metrics to the JD.\n\n"
"Q5: Why this role/company?\n"
"A: The mission, product focus, and my experience align with the team’s goals.\n"
"Tip: Reference 1–2 specifics from the JD."
)
# =============================
# Demo Defaults
# =============================
DEMO_JD = "Responsibilities: Collaborate, improve processes, analyze metrics. Preferred: Python, SQL."
DEMO_RESUME = "Experience: Assisted in process improvement; created reports. Skills: Python, SQL, Excel."
# =============================
# UI
# =============================
with gr.Blocks(title="CareerForge — Resume & Interview Coach") as demo:
gr.Markdown("# CareerForge — Resume & Interview Coach")
gr.Markdown(
"Paste a **Job Description** and **Resume**, then generate suggestions, a cover letter, and a mock interview.\n"
f"{'🔐 OpenAI mode enabled.' if USE_OPENAI else '⚠️ Mock Mode (no OPENAI_API_KEY set).'}"
)
with gr.Tab("1) Inputs"):
with gr.Row():
jd_text = gr.Textbox(label="Job Description (paste)", value=DEMO_JD, lines=10)
resume_text = gr.Textbox(label="Resume (paste)", value=DEMO_RESUME, lines=10)
process_btn = gr.Button("Ingest & Score")
jd_out = gr.Textbox(label="Job Description (processed)", interactive=False)
resume_out = gr.Textbox(label="Resume (processed)", interactive=False)
match_vis = gr.Textbox(label="Keyword Match Score", interactive=False)
with gr.Tab("2) Resume Improvements"):
improve_btn = gr.Button("Generate Suggestions")
improve_out = gr.Markdown()
with gr.Tab("3) Cover Letter"):
cover_btn = gr.Button("Generate Cover Letter")
cover_out = gr.Markdown()
with gr.Tab("4) Mock Interview"):
style = gr.Dropdown(choices=["Behavioral", "Technical", "Product/Program"], value="Behavioral", label="Interview Style")
interview_btn = gr.Button("Start Mock Interview")
interview_out = gr.Markdown()
with gr.Tab("5) Bias & Fairness"):
gr.Markdown(BIAS_REFLECTION)
# Wiring
process_btn.click(fn=ingest_inputs, inputs=[jd_text, resume_text], outputs=[jd_out, resume_out, match_vis])
improve_btn.click(fn=generate_improvements, inputs=[jd_out, resume_out], outputs=[improve_out])
cover_btn.click(fn=generate_cover_letter, inputs=[jd_out, resume_out], outputs=[cover_out])
interview_btn.click(fn=generate_mock_interview, inputs=[jd_out, resume_out, style], outputs=[interview_out])
# Local dev
if __name__ == "__main__":
demo.launch()