by Ravi Yeluru
pip install langchain langchain-openai openai PyPDF2
pip install streamlit==1.28.0
export OPENAI_API_KEY=your_key_here
import os
os.environ["OPENAI_API_KEY"] = "your_key_here"
resume_analyzer.py
is the heart of our AI agent. It takes in a job description and a candidate's resume, extracts the relevant text, and uses a GPT-powered language model to evaluate the match between them. Unlike traditional resume screeners that rely on rigid keyword matching, this script generates intelligent, structured feedback based on how well the candidate meets the job’s technical, experiential, educational, and soft skill requirements. It produces a numerical match score along with human-readable justifications—explaining strengths, weaknesses, and reasoning behind the score. The agent behaves like a smart assistant, capable of understanding context and drawing insights from resumes the way a human recruiter would. This is not just automation; it’s intelligent evaluation powered by LLM reasoning.# resume_analyzer.py
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.output_parsers import PydanticOutputParser
from PyPDF2 import PdfReader
from pydantic import BaseModel, Field
import json
import sys
from typing import List
from pathlib import Path
class ResumeEvaluation(BaseModel):
score: int = Field(description="Overall match score from 0-100")
strengths: List[str] = Field(description="List of candidate strengths")
weaknesses: List[str] = Field(description="List of missing skills or experiences")
explanation: str = Field(description="Detailed explanation of the evaluation")
parser = PydanticOutputParser(pydantic_object=ResumeEvaluation)
def read_pdf(path):
reader = PdfReader(path)
text = ""
for page in reader.pages:
text += page.extract_text() or ""
return text
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")
template = PromptTemplate.from_template("""
You are an AI hiring assistant for a software engineering position. Your job is to review a resume and compare it to the job description.
Job Description: {job}
Candidate Resume: {resume}
First, identify the key requirements from the job description (skills, experience, education).
Then, find evidence in the resume for each requirement.
Be specific about years of experience and project details.
Evaluate the match on a scale of 0 to 100 using these criteria:
- Technical skills match (weight: 40%)
- Years of experience (weight: 30%)
- Education requirements (weight: 10%)
- Soft skills and cultural fit indicators (weight: 20%)
{format_instructions}
""")
prompt = template.partial(format_instructions=parser.get_format_instructions())
def evaluate_resume(job_path, resume_path):
try:
with open(job_path, "r") as file:
job_description = file.read()
if resume_path.endswith(".pdf"):
resume_text = read_pdf(resume_path)
else:
with open(resume_path, "r") as file:
resume_text = file.read()
formatted_prompt = prompt.format(job=job_description, resume=resume_text)
response = llm.invoke(formatted_prompt).content
return parser.parse(response)
except Exception as e:
return f"Error: {str(e)}"
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python resume_analyzer.py ")
sys.exit(1)
job_path = str(Path(sys.argv[1]).expanduser())
resume_path = str(Path(sys.argv[2]).expanduser())
result = evaluate_resume(job_path, resume_path)
if isinstance(result, str):
print(result)
else:
print(json.dumps(result.model_dump(), indent=2))
python resume_analyzer.py job_description.txt resumes/John_Doe.pdf
{
"score": 78,
"strengths": [
"5 years of Java development (2 years more than required)",
"Strong Spring Boot experience with 3 production applications",
"Computer Science degree from accredited university",
"Experience with microservices architecture"
],
"weaknesses": [
"No Kubernetes experience mentioned (required in job description)",
"Limited AWS exposure (only mentions S3, job requires EC2 and Lambda)",
"No mention of CI/CD pipeline experience"
],
"explanation": "This candidate demonstrates strong backend development skills with Java and Spring Boot, exceeding the required experience. Their CS degree fulfills the educational requirement. However, they lack specific DevOps skills mentioned in the job description, particularly Kubernetes and comprehensive AWS experience. The candidate would likely require some training in these areas but has a solid foundation to build upon."
}
batch_resume_analyzer.py
extends the resume analyzer into a scalable tool that can process an entire folder of resumes in parallel. It evaluates each resume against the same job description using the AI agent logic, collects the results, and outputs both a detailed JSON report and a Markdown summary highlighting the top five candidates. Each result includes a match score, key strengths, areas for improvement, and a natural-language explanation generated by the language model. This script transforms the one-on-one evaluation into a high-efficiency screening pipeline—ideal for real-world hiring scenarios where dozens or even hundreds of resumes need to be reviewed intelligently, quickly, and consistently.
# batch_resume_analyzer.py import os import json from concurrent.futures import ThreadPoolExecutor from tqdm import tqdm from pathlib import Path from resume_analyzer import evaluate_resume # Make sure evaluate_resume is importable folder = "resumes" results = {} def process_file(filename): if filename.endswith((".pdf", ".txt", ".docx")): path = os.path.join(folder, filename) try: result = evaluate_resume("job_description.txt", path) return filename, result except Exception as e: return filename, f"Error: {str(e)}" return None, None files = [f for f in os.listdir(folder) if f.endswith((".pdf", ".txt", ".docx"))] with ThreadPoolExecutor(max_workers=5) as executor: for filename, result in tqdm(executor.map(process_file, files), total=len(files)): if filename: if hasattr(result, "model_dump"): results[filename] = result.model_dump() else: results[filename] = result with open("match_report.json", "w") as f: json.dump(results, f, indent=2) ranked_candidates = sorted( [(name, data) for name, data in results.items() if isinstance(data, dict)], key=lambda x: x[1].get("score", 0), reverse=True ) with open("match_report.md", "w") as f: f.write("# Resume Matching Results\n\n") f.write("## Top Candidates\n\n") for i, (name, data) in enumerate(ranked_candidates[:5], 1): f.write(f"### {i}. {name} - Score: {data['score']}/100\n\n") f.write("**Strengths:**\n") for s in data["strengths"]: f.write(f"- {s}\n") f.write("\n**Areas for Improvement:**\n") for w in data["weaknesses"]: f.write(f"- {w}\n") f.write(f"\n**Summary:** {data['explanation']}\n\n")
python batch_resume_analyzer.py
app.py
brings the resume evaluation agent into an interactive, user-friendly interface using Streamlit. It allows users to upload a job description and multiple resumes directly from their browser, select their preferred AI model, and set a minimum score threshold. Once triggered, the app processes each resume using the same intelligent agent logic and displays ranked results in real time. Each candidate's evaluation includes a match score, strengths, weaknesses, and a detailed analysis. This turns the backend intelligence of the agent into a practical tool for non-technical users, making AI-powered hiring accessible and intuitive.# app.py
import streamlit as st
import tempfile
import os
import json
from resume_analyzer import evaluate_resume
from langchain_openai import ChatOpenAI
st.set_page_config(page_title="AI Resume Reviewer", layout="wide")
st.title("AI Resume Reviewer")
st.write("Upload a job description and resumes to find the best matches")
with st.sidebar:
st.header("Settings")
model_choice = st.selectbox(
"Select AI Model",
["gpt-3.5-turbo ($0.002/1K tokens)", "gpt-4 ($0.06/1K tokens)"]
)
threshold = st.slider(
"Minimum Matching Score",
min_value=0,
max_value=100,
value=60,
help="Only show candidates above this score"
)
col1, col2 = st.columns(2)
job_path = None
resume_paths = []
with col1:
st.subheader("Job Description")
uploaded_job = st.file_uploader("Upload Job Description", type=["txt", "pdf"])
if uploaded_job:
with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as tmp:
tmp.write(uploaded_job.getvalue())
job_path = tmp.name
with col2:
st.subheader("Resumes")
uploaded_resumes = st.file_uploader(
"Upload Resumes",
type=["pdf", "txt", "docx"],
accept_multiple_files=True
)
if uploaded_job and uploaded_resumes and st.button("Analyze Resumes"):
progress_bar = st.progress(0)
results = []
model_name = "gpt-3.5-turbo" if "3.5" in model_choice else "gpt-4"
for i, resume_file in enumerate(uploaded_resumes):
with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{resume_file.name.split(".")[-1]}') as tmp:
tmp.write(resume_file.getvalue())
resume_path = tmp.name
resume_paths.append(resume_path)
progress = (i + 1) / len(uploaded_resumes)
progress_bar.progress(progress)
st.write(f"Processing: {resume_file.name}")
result = evaluate_resume(job_path, resume_path)
if hasattr(result, 'model_dump'):
results.append((resume_file.name, result.model_dump()))
st.success("Analysis complete!")
sorted_results = sorted(results, key=lambda x: x[1]['score'], reverse=True)
filtered_results = [r for r in sorted_results if r[1]['score'] >= threshold]
if not filtered_results:
st.warning(f"No candidates scored above the {threshold}% threshold")
for name, result in filtered_results:
with st.expander(f"{name} - Match Score: {result['score']}%"):
col1, col2 = st.columns(2)
with col1:
st.subheader("Strengths")
for strength in result['strengths']:
st.write(f"- {strength}")
with col2:
st.subheader("Areas for Improvement")
for weakness in result['weaknesses']:
st.write(f"- {weakness}")
st.subheader("Detailed Analysis")
st.write(result['explanation'])
try:
os.unlink(job_path)
for path in resume_paths:
os.unlink(path)
except:
pass
streamlit run app.py