ApplyAI / app.py
NandanV's picture
Update app.py
14ebaf4 verified
Raw
History Blame Contribute Delete
15.1 kB
import streamlit as st
import pdfplumber
import docx
import re
import requests
import mimetypes
import base64
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Attachment, FileContent, FileName, FileType, Disposition
from openai import OpenAI
from collections import Counter
import mimetypes
import time
# ---------------- Page Config ----------------
st.set_page_config(page_title="ApplyAi", layout="wide")
st.sidebar.image("logo.png", use_container_width=True)
# ---------------- Helper Functions ---------------
def parse_resume(file):
if file.name.endswith(".pdf"):
with pdfplumber.open(file) as pdf:
return "\n".join(page.extract_text() or "" for page in pdf.pages)
elif file.name.endswith(".docx"):
doc = docx.Document(file)
return "\n".join([para.text for para in doc.paragraphs])
return "Unsupported file format"
def extract_skills(text):
keywords = [
# Programming Languages
"python", "java", "javascript", "typescript", "c++", "c#", "go", "ruby", "kotlin", "swift",
# Data & AI
"machine learning", "deep learning", "artificial intelligence", "nlp", "computer vision",
"pandas", "numpy", "scikit-learn", "tensorflow", "pytorch", "keras", "matplotlib", "seaborn",
# Data Analysis & BI
"sql", "excel", "power bi", "tableau", "looker", "data analysis", "data visualization",
"data wrangling", "data engineering", "etl", "snowflake", "bigquery", "redshift",
# Cloud & DevOps
"aws", "azure", "gcp", "docker", "kubernetes", "git", "github", "gitlab",
"ci/cd", "jenkins", "terraform", "linux", "bash", "shell scripting",
# Web & App Development
"html", "css", "react", "angular", "vue", "next.js", "node.js", "express", "flask", "django",
"rest api", "graphql", "firebase",
# Tools & Soft Skills
"jira", "confluence", "notion", "agile", "scrum", "teamwork", "communication", "problem solving",
"critical thinking", "leadership", "project management", "unit testing", "integration testing"
]
text = text.lower()
found = [kw for kw in keywords if kw in text]
return [skill for skill, _ in Counter(found).most_common()]
def extract_entities(text):
emails = re.findall(r'\S+@\S+', text)
phones = re.findall(r'\+?\d[\d\s()-]{7,}\d', text)
return {"emails": list(set(emails)), "phones": list(set(phones))}
def analyze_resume(text):
return {
"skills": extract_skills(text),
"entities": extract_entities(text),
}
def fetch_jobs(query, location="Remote", num_pages=1):
url = "https://jsearch.p.rapidapi.com/search"
headers = {
"X-RapidAPI-Key": st.secrets["api"],
"X-RapidAPI-Host": "jsearch.p.rapidapi.com"
}
params = {
"query": f"{query} in {location}",
"page": 1,
"num_pages": num_pages
}
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
return response.json().get("data", [])
else:
st.error(f"Error: {response.status_code} - {response.text}")
return []
# ---------------- Main App ----------------
st.title("ApplyAi β€” Job search, simplified.")
st.markdown("Upload your resume and find matching jobs based on your skills.")
# Upload Resume
st.sidebar.header("πŸ“„ Upload Resume")
uploaded_file = st.sidebar.file_uploader("Upload PDF or DOCX", type=["pdf", "docx"])
# Initialize lists to avoid undefined errors
jobs = []
recommended_jobs = []
if uploaded_file:
resume_text = parse_resume(uploaded_file)
analysis = analyze_resume(resume_text)
skills = analysis["skills"]
entities = analysis["entities"]
st.success("βœ… Resume processed successfully!")
# ---------------- Resume Analysis ----------------
with st.expander("πŸ” Resume Analysis", expanded=True):
st.subheader("πŸ“Œ Extracted Skills")
if skills:
skill_tags = "".join(
[f"<span style='background-color:#262730;padding:6px 12px;border-radius:12px;margin:4px;display:inline-block;font-size:16px;'>{s.title()}</span>" for s in skills]
)
st.markdown(f"<div style='line-height:2;flex-wrap:wrap'>{skill_tags}</div>", unsafe_allow_html=True)
else:
st.write("No relevant skills found.")
st.subheader("πŸ“§ Contact Info")
if entities["emails"]:
st.write("**Emails:**", ", ".join(entities["emails"]))
if entities["phones"]:
st.write("**Phone Numbers:**", ", ".join(entities["phones"]))
if not (entities["emails"] or entities["phones"]):
st.write("No contact info found.")
st.subheader("πŸ“ Raw Resume Text")
st.text_area("Resume Text", resume_text, height=300)
st.subheader("πŸ“Š AI-Powered Resume Score")
with st.spinner("Scoring your resume with AI..."):
prompt = f"""
You're a professional career advisor. Based on the resume text below, do the following:
1. Give a score out of 100 reflecting the overall quality, formatting, skill diversity, and clarity.
2. Briefly mention strengths.
3. Suggest 2–3 improvements.
Only include the score once. Avoid markdown formatting.
Resume:
\"\"\"{resume_text[:3000]}\"\"\"
"""
client = OpenAI(api_key=st.secrets["openai_api_key"])
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
ai_response = response.choices[0].message.content.strip()
# Extract score
match = re.search(r"\d{1,3}", ai_response)
score = int(match.group()) if match else 0
score = min(score, 100)
# Display visual progress bar
st.progress(score, text=f"Resume Score: {score}%")
# Show motivational feedback
if score >= 80:
st.success("πŸš€ Great resume! Ready to apply.")
elif score >= 60:
st.info("πŸ’‘ Good start. A few tweaks can help.")
else:
st.warning("⚠️ Needs improvement before applying.")
# Clean and extract suggestions from AI response
improvements = []
for line in ai_response.splitlines():
line_clean = line.strip("β€’- ").strip()
if any(kw in line_clean.lower() for kw in ["improve", "suggest", "consider", "could", "recommend"]):
# Skip repeated headings like "Improvements:"
if "improvement" in line_clean.lower() and len(line_clean) < 30:
continue
# Remove numbered prefixes
line_clean = re.sub(r"^\d+[\.\)]\s*", "", line_clean)
improvements.append(line_clean)
# Display clean suggestions
if improvements:
st.markdown("#### πŸ› οΈ Suggested Improvements:")
for tip in improvements:
st.markdown(f"- {tip}")
# ---------------- Job Recommendations ---------------
with st.expander("πŸ’Ό Job Recommendations", expanded=True):
auto_keyword = skills[0] if skills else "Data Scientist"
st.markdown("### 🧠 Resume-based suggestion")
st.markdown(f"Top skill detected: <code>{auto_keyword}</code>", unsafe_allow_html=True)
recommended_jobs = fetch_jobs(auto_keyword, "Remote", num_pages=1)
if recommended_jobs:
best_job = recommended_jobs[0]
st.markdown("#### βœ… Recommended for You")
st.markdown(f"**[{best_job.get('job_title')}]({best_job.get('job_apply_link', '#')})** at *{best_job.get('employer_name')}*")
st.markdown(f"πŸ“ {best_job.get('job_city', 'Remote')}, {best_job.get('job_country', '')}")
st.markdown(f"πŸ“ {best_job.get('job_description', '')[:300]}... [Apply here]({best_job.get('job_apply_link', '#')})", unsafe_allow_html=True)
st.markdown("---")
else:
st.info("No auto-suggestions available. Try manual search below.")
st.markdown("### ✏️ Or enter your own search")
custom_term = st.text_input("Job Title / Keywords", value=auto_keyword)
custom_location = st.text_input("Location", value="Calgary")
if st.button("πŸ”Ž Find Jobs"):
jobs = fetch_jobs(custom_term, custom_location, num_pages=2)
if jobs:
st.subheader("πŸ“‹ Job Listings")
for job in jobs:
link = job.get("job_apply_link") or "#"
st.markdown(f"### [{job.get('job_title')}]({link})")
st.write(f"**Company:** {job.get('employer_name')}")
st.write(f"**Location:** {job.get('job_city', 'Remote')}, {job.get('job_country')}")
st.write(f"πŸ“ {job.get('job_description', '')[:300]}...")
st.markdown(f"[Apply here]({link})", unsafe_allow_html=True)
st.markdown("---")
else:
st.warning("No jobs found. Try different search terms.")
# ---------------- Email Section ----------------
all_jobs = []
if recommended_jobs:
all_jobs.extend(recommended_jobs)
if jobs:
all_jobs.extend(jobs)
st.markdown("## πŸ“§ Compose and Send Email")
if not all_jobs:
st.info("No jobs found yet. Try uploading a resume or running a job search first.")
else:
job_titles = [f"{job.get('job_title')} at {job.get('employer_name', '')}" for job in all_jobs]
selected_title = st.selectbox("Select a job to apply for", job_titles)
selected_job = all_jobs[job_titles.index(selected_title)]
job_desc = selected_job.get("job_description", "")
email_matches = re.findall(r'[\w\.-]+@[\w\.-]+\.\w+', job_desc)
auto_email = email_matches[0] if email_matches else ""
smart_subject = f"Job Application: {selected_job.get('job_title')} at {selected_job.get('employer_name')}"
smart_body = f"""Dear Hiring Team,
I hope this message finds you well. I recently came across your job listing for the position of {selected_job.get('job_title')} at {selected_job.get('employer_name')}, and I am writing to express my strong interest in this opportunity.
With a background in {', '.join(skills[:3])}, I believe I bring the technical expertise and enthusiasm required to make a meaningful impact in this role. My experience includes developing scalable applications, collaborating on cross-functional teams, and continuously learning new tools to stay at the forefront of the industry.
What excites me about this opportunity at {selected_job.get('employer_name')} is not only the alignment with my skillset but also the chance to contribute to an organization that values innovation and growth.
Please find my resume attached for your review. I would welcome the opportunity to discuss how my background and passion align with your team's goals. Thank you for considering my application.
Warm regards,
Sri Nandan
"""
with st.form("email_form"):
to_email = st.text_input("Recipient Email", value=auto_email)
subject = st.text_input("Subject", value=smart_subject)
body = st.text_area("Email Body", value=smart_body, height=180)
if uploaded_file:
st.markdown("**πŸ“Ž Your resume will be attached to the email**")
else:
st.warning("Please upload a resume before sending.")
submitted = st.form_submit_button("πŸ“¨ Send Email")
if submitted:
if to_email and uploaded_file:
# Guess the MIME type based on the file name
mime_type, _ = mimetypes.guess_type(uploaded_file.name)
maintype, subtype = mime_type.split("/") if mime_type else ("application", "octet-stream")
uploaded_file.seek(0)
file_bytes = uploaded_file.read()
encoded = base64.b64encode(file_bytes).decode()
message = Mail(
from_email=st.secrets["email_user"],
to_emails=to_email,
subject=subject,
plain_text_content=body
)
attachment = Attachment(
FileContent(encoded),
FileName(uploaded_file.name),
FileType(mime_type or "application/octet-stream"),
Disposition("attachment")
)
message.attachment = attachment
progress = st.progress(0, text="πŸ“¨ Sending email...")
try:
for percent in range(0, 101, 20):
time.sleep(0.2)
progress.progress(percent, text="πŸ“¨ Sending email...")
sg = SendGridAPIClient(st.secrets["sendgrid_api_key"])
sg.send(message)
progress.empty()
st.success("βœ… Email sent successfully!")
except Exception as e:
progress.empty()
st.error(f"❌ SendGrid failed: {e}")
else:
st.error("Missing recipient email or resume.")
client = OpenAI(api_key=st.secrets["openai_api_key"])
# Initialize memory
if "chat_history" not in st.session_state:
st.session_state.chat_history = [
{"role": "system", "content": (
"You are ApplyAi, an AI assistant that only answers questions about job search, resumes, interviews, and career advice. "
"If a user asks about something unrelated, politely ask them to stay on topic."
)}
]
# Input box ABOVE the expander
st.markdown("## πŸ’¬ Chat with ApplyAi")
user_input = st.text_input("Ask a job-related question:", key="job_chat_input")
if user_input:
st.session_state.chat_history.append({"role": "user", "content": user_input})
with st.spinner("Thinking..."):
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=st.session_state.chat_history
)
reply = response.choices[0].message.content
st.session_state.chat_history.append({"role": "assistant", "content": reply})
# Collapsible chat history
with st.expander("πŸ—‚οΈ Chat History", expanded=False):
for msg in st.session_state.chat_history[1:]: # skip system prompt
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
else:
st.info("πŸ‘ˆ Upload your resume to get started.")