Spaces:
Sleeping
Sleeping
| 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() | |