| 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 |
|
|
| |
| st.set_page_config(page_title="ApplyAi", layout="wide") |
| st.sidebar.image("logo.png", use_container_width=True) |
| |
|
|
| 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 = [ |
| |
| "python", "java", "javascript", "typescript", "c++", "c#", "go", "ruby", "kotlin", "swift", |
| |
| |
| "machine learning", "deep learning", "artificial intelligence", "nlp", "computer vision", |
| "pandas", "numpy", "scikit-learn", "tensorflow", "pytorch", "keras", "matplotlib", "seaborn", |
|
|
| |
| "sql", "excel", "power bi", "tableau", "looker", "data analysis", "data visualization", |
| "data wrangling", "data engineering", "etl", "snowflake", "bigquery", "redshift", |
|
|
| |
| "aws", "azure", "gcp", "docker", "kubernetes", "git", "github", "gitlab", |
| "ci/cd", "jenkins", "terraform", "linux", "bash", "shell scripting", |
|
|
| |
| "html", "css", "react", "angular", "vue", "next.js", "node.js", "express", "flask", "django", |
| "rest api", "graphql", "firebase", |
|
|
| |
| "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 [] |
|
|
| |
|
|
| st.title("ApplyAi β Job search, simplified.") |
| st.markdown("Upload your resume and find matching jobs based on your skills.") |
|
|
| |
| st.sidebar.header("π Upload Resume") |
| uploaded_file = st.sidebar.file_uploader("Upload PDF or DOCX", type=["pdf", "docx"]) |
|
|
| |
| 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!") |
|
|
| |
| 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() |
| |
| |
| match = re.search(r"\d{1,3}", ai_response) |
| score = int(match.group()) if match else 0 |
| score = min(score, 100) |
| |
| |
| st.progress(score, text=f"Resume Score: {score}%") |
| |
| |
| 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.") |
| |
| |
| 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"]): |
| |
| if "improvement" in line_clean.lower() and len(line_clean) < 30: |
| continue |
| |
| line_clean = re.sub(r"^\d+[\.\)]\s*", "", line_clean) |
| improvements.append(line_clean) |
| |
| |
| if improvements: |
| st.markdown("#### π οΈ Suggested Improvements:") |
| for tip in improvements: |
| st.markdown(f"- {tip}") |
|
|
| |
| |
| 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.") |
|
|
| |
| 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: |
| |
| 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"]) |
|
|
| |
| 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." |
| )} |
| ] |
| |
| |
| 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}) |
| |
| |
| with st.expander("ποΈ Chat History", expanded=False): |
| for msg in st.session_state.chat_history[1:]: |
| with st.chat_message(msg["role"]): |
| st.markdown(msg["content"]) |
|
|
|
|
|
|
| |
| else: |
| st.info("π Upload your resume to get started.") |
|
|