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()