Om-Shandilya's picture
Add RESTful API backend and decoupled frontend
a9988a0
raw
history blame
8.23 kB
import streamlit as st
import os
import sys
import tempfile
import pandas as pd
import shutil
import altair as alt
import requests # Import for making API requests
# Ensure the parent directory is in sys.path for imports
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
if PROJECT_ROOT not in sys.path:
sys.path.append(PROJECT_ROOT)
from src.utils.bulk_loading import bulk_load_raw_resume_files
from src.utils.file_reader import extract_text_from_file
# Configuring the backend API URL
API_URL = "http://127.0.0.1:8000"
# Configuring the Streamlit app
st.set_page_config(
page_title="Resume-Job Matcher",
page_icon="👨‍💼",
layout="wide"
)
# Main app title and description
st.title("🎯 AI-Powered Resume-Job Matcher")
st.write("---")
# Creating sidebar for controls
with st.sidebar:
st.header("Controls")
app_mode = st.radio(
"Choose your view",
("Applicant", "Recruiter"),
help="Select 'Applicant' to match your resume to jobs. Select 'Recruiter' to rank resumes for a job."
)
model_choice = st.selectbox(
"Choose the AI Model",
("TF-IDF", "BERT"),
help="TF-IDF is baseline. BERT is more accurate and semantic."
)
st.write("---")
show_all = st.checkbox("Show all matches", value=False)
if show_all:
top_k = None
st.slider(
"Number of matches to show",
min_value=1, max_value=50, value=5, step=1,
disabled=True
)
st.info("Showing all ranked results.")
else:
top_k = st.slider(
"Number of matches to show",
min_value=1, max_value=50, value=5, step=1,
disabled=False
)
# Applicant view of the app
if app_mode == "Applicant":
st.header("Applicant: Match Your Resume to a Job")
resume_file = st.file_uploader(
"Upload your resume",
type=['pdf', 'docx', 'txt'],
help="Please upload your resume in PDF, DOCX, or TXT format."
)
if resume_file:
st.success(f"✅ Successfully uploaded `{resume_file.name}`")
if st.button("Find Top Job Matches", type="primary", width='stretch'):
with st.spinner(f"Sending your resume to the AI backend for matching..."):
tmp_file_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(resume_file.name)[1]) as tmp_file:
tmp_file.write(resume_file.getvalue())
tmp_file_path = tmp_file.name
raw_resume_text = extract_text_from_file(tmp_file_path)
endpoint = f"{API_URL}/applicant/match/{model_choice.lower()}"
payload = {"raw_text": raw_resume_text, "top_k": top_k}
response = requests.post(endpoint, json=payload, timeout=180) # 3-minute timeout
response.raise_for_status() # Raises HTTPError for bad responses eg. 4xx, 5xx
api_data = response.json()
matches = api_data.get("matches", [])
message = api_data.get("message", "No message from server.")
if not matches:
st.warning("⚠️ No suitable job matches found.")
else:
st.info(message)
st.subheader(f"Top {len(matches)} Job Matches:")
df = pd.DataFrame(matches) # Pandas handles list of dicts perfectly
df = df.sort_values(by="match_score", ascending=False).reset_index(drop=True)
chart = alt.Chart(df).mark_bar().encode(
y=alt.Y('job_title', sort='-x', title=None, axis=alt.Axis(labelLimit=400)),
x=alt.X('match_score', axis=None, scale=alt.Scale(domainMin=0)),
tooltip=['job_title', alt.Tooltip('match_score', format='.3f')]
).properties(title="Relative Job Match Scores").interactive()
st.altair_chart(chart, use_container_width=True)
except requests.exceptions.RequestException as e:
st.error(f"API Error: Could not connect to the backend. Please ensure the backend server is running. Details: {e}")
except Exception as e:
st.error(f"An error occurred: {e}")
finally:
if tmp_file_path and os.path.exists(tmp_file_path):
os.unlink(tmp_file_path)
# Recruiter view of the app
if app_mode == "Recruiter":
st.header("Recruiter: Rank Resumes for a Job Description")
job_desc_file = st.file_uploader("Upload the job description", type=['pdf', 'docx', 'txt'])
resume_files = st.file_uploader("Upload candidate resumes", type=['pdf', 'docx', 'txt'], accept_multiple_files=True)
if job_desc_file and resume_files:
st.success(f"✅ Successfully uploaded job description and {len(resume_files)} resumes.")
if st.button("Rank Resumes", type="primary", width='stretch'):
with st.spinner(f"Sending files to the AI backend for ranking..."):
temp_dir = None
job_desc_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(job_desc_file.name)[1]) as tmp_file:
tmp_file.write(job_desc_file.getvalue())
job_desc_path = tmp_file.name
raw_job_text = extract_text_from_file(job_desc_path)
temp_dir = tempfile.mkdtemp()
for resume_file in resume_files:
with open(os.path.join(temp_dir, resume_file.name), "wb") as f:
f.write(resume_file.getbuffer())
raw_resume_texts = bulk_load_raw_resume_files(temp_dir)
endpoint = f"{API_URL}/recruiter/rank/{model_choice.lower()}"
payload = {
"raw_job_text": raw_job_text,
"raw_resume_texts": raw_resume_texts,
"top_k": top_k
}
response = requests.post(endpoint, json=payload, timeout=300) # 5-minute timeout
response.raise_for_status() # Raises HTTPError for bad responses eg. 4xx, 5xx
api_data = response.json()
ranked_resumes = api_data.get("matches", [])
message = api_data.get("message", "No message from server.")
if not ranked_resumes:
st.warning("⚠️ Could not rank resumes. Please check the files.")
else:
st.info(message)
st.subheader(f"Top {len(ranked_resumes)} Ranked Resumes:")
df = pd.DataFrame(ranked_resumes)
df["match_score"] = df["match_score"].apply(lambda x: min(1.0, x))
st.dataframe(
df,
column_config={
"resume_filename": st.column_config.TextColumn("Resume"),
"match_score": st.column_config.ProgressColumn(
"Match Score", format="%.2f", min_value=0, max_value=1
),
},
hide_index=True,
)
except requests.exceptions.RequestException as e:
st.error(f"API Error: Could not connect to the backend. Please ensure the backend server is running. Details: {e}")
except Exception as e:
st.error(f"An error occurred: {e}")
finally:
if job_desc_path and os.path.exists(job_desc_path):
os.unlink(job_desc_path)
if temp_dir and os.path.exists(temp_dir):
shutil.rmtree(temp_dir)