import streamlit as st import fitz import os import requests import json import re st.set_page_config(page_title="AI Resume Screener", page_icon="๐Ÿ”", layout="wide") st.markdown(""" """, unsafe_allow_html=True) # โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def extract_pdf_text(pdf_bytes: bytes) -> str: doc = fitz.open(stream=pdf_bytes, filetype="pdf") text = "" for page in doc: text += page.get_text("text") + "\n" doc.close() return text.strip() def score_resume(jd_text: str, resume_text: str, candidate_name: str, api_key: str) -> dict: prompt = f"""You are an expert HR recruiter and talent evaluator. Analyze the candidate's resume against the job description and provide a detailed evaluation. Job Description: {jd_text[:2000]} Candidate Resume ({candidate_name}): {resume_text[:2500]} Respond ONLY with a valid JSON object in exactly this format: {{ "score": , "verdict": "", "summary": "<2-3 sentence overall assessment>", "strengths": ["", "", ""], "gaps": ["", ""], "recommendation": "", "experience_match": , "skills_match": , "education_match": }} Be objective and specific. Base scores purely on how well the resume matches the JD requirements.""" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = { "model": "llama-3.3-70b-versatile", "messages": [{"role": "user", "content": prompt}], "max_tokens": 800, "temperature": 0.1, } r = requests.post("https://api.groq.com/openai/v1/chat/completions", headers=headers, json=payload, timeout=30) r.raise_for_status() raw = r.json()["choices"][0]["message"]["content"] raw = re.sub(r"```json|```", "", raw).strip() return json.loads(raw) # โ”€โ”€โ”€ Sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with st.sidebar: st.markdown("## ๐Ÿ” Resume Screener") st.markdown("
Powered by Groq ยท Llama 3.3 70B
", unsafe_allow_html=True) st.markdown("---") env_key = os.environ.get("GROQ_API_KEY", "") api_key = env_key if env_key else st.text_input("๐Ÿ”‘ Groq API Key", type="password", placeholder="gsk_...") if not env_key and not api_key: st.caption("Free key โ†’ [console.groq.com](https://console.groq.com)") st.markdown("---") st.markdown("""
How it works
1. Paste the Job Description
2. Upload candidate resumes (PDF)
3. AI scores each resume 0โ€“100
4. Candidates ranked automatically

Scoring Dimensions
โ€ข Overall fit score
โ€ข Skills match %
โ€ข Experience match %
โ€ข Education match %
""", unsafe_allow_html=True) # โ”€โ”€โ”€ Main UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("""

๐Ÿ” AI Resume Screener

Upload a Job Description and multiple resumes โ€” AI scores, ranks, and explains each candidate automatically

""", unsafe_allow_html=True) col_jd, col_resumes = st.columns([1, 1], gap="large") with col_jd: st.markdown("", unsafe_allow_html=True) jd_input = st.text_area( "Job Description", placeholder="Paste the full job description here including role, responsibilities, required skills, and qualifications...", height=320, label_visibility="collapsed" ) with col_resumes: st.markdown("", unsafe_allow_html=True) uploaded_resumes = st.file_uploader( "Upload Resumes", type=["pdf"], accept_multiple_files=True, label_visibility="collapsed" ) if uploaded_resumes: for r in uploaded_resumes: st.markdown(f"
๐Ÿ“„ {r.name} ยท {round(r.size/1024,1)}KB
", unsafe_allow_html=True) st.markdown("") run_btn = st.button("๐Ÿš€ Screen All Candidates", type="primary", use_container_width=True, disabled=not (jd_input and uploaded_resumes and api_key)) if not api_key: st.warning("๐Ÿ‘ˆ Add your Groq API key to get started.") elif not jd_input: st.info("๐Ÿ“‹ Paste the job description on the left to begin.") elif not uploaded_resumes: st.info("๐Ÿ“‚ Upload at least one resume PDF to begin.") if run_btn and jd_input and uploaded_resumes and api_key: results = [] progress = st.progress(0, text="Screening candidates...") for i, resume_file in enumerate(uploaded_resumes): candidate_name = resume_file.name.replace(".pdf", "").replace("_", " ").replace("-", " ").title() progress.progress(i / len(uploaded_resumes), text=f"Analyzing {candidate_name}...") with st.spinner(f"Evaluating {candidate_name}..."): try: resume_text = extract_pdf_text(resume_file.read()) result = score_resume(jd_input, resume_text, candidate_name, api_key) result["name"] = candidate_name result["filename"] = resume_file.name results.append(result) except Exception as e: st.error(f"โŒ Error processing {candidate_name}: {str(e)}") progress.progress(1.0, text="Screening complete!") if results: # Sort by score results.sort(key=lambda x: x.get("score", 0), reverse=True) st.markdown("---") st.markdown("## ๐Ÿ“Š Screening Results") # Summary stats avg_score = round(sum(r.get("score", 0) for r in results) / len(results)) top_score = results[0].get("score", 0) strong = sum(1 for r in results if r.get("score", 0) >= 70) st.markdown(f"""
{len(results)}
Candidates Screened
{top_score}
Top Score
{avg_score}
Average Score
{strong}
Strong Matches
""", unsafe_allow_html=True) # Ranked results for rank, result in enumerate(results, start=1): score = result.get("score", 0) rank_class = f"rank-{rank}" if rank <= 3 else "rank-other" score_class = "score-high" if score >= 70 else "score-mid" if score >= 50 else "score-low" rank_emoji = "๐Ÿฅ‡" if rank == 1 else "๐Ÿฅˆ" if rank == 2 else "๐Ÿฅ‰" if rank == 3 else f"#{rank}" skills_w = result.get("skills_match", 0) exp_w = result.get("experience_match", 0) edu_w = result.get("education_match", 0) strengths_html = "".join([f"โœ“ {s}" for s in result.get("strengths", [])]) gaps_html = "".join([f"โœ— {g}" for g in result.get("gaps", [])]) with st.expander(f"{rank_emoji} {result['name']} โ€” {score}/100 ยท {result.get('verdict', '')}", expanded=(rank <= 3)): st.markdown(f"""
Rank #{rank}
{result['name']}
๐Ÿ“„ {result['filename']}
{score} / 100
{result.get("summary","")}
Skills Match
{skills_w}%
Experience Match
{exp_w}%
Education Match
{edu_w}%
โœ… Strengths
{strengths_html}
โš ๏ธ Gaps
{gaps_html}
๐Ÿ’ก Recommendation: {result.get("recommendation","")}
""", unsafe_allow_html=True)