| | import streamlit as st |
| | import pandas as pd |
| | import json |
| | import smtplib |
| | from email.message import EmailMessage |
| | from typing import Dict, List |
| |
|
| | from jobspy import scrape_jobs |
| | import groq |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | def remove_duplicates(df: pd.DataFrame) -> pd.DataFrame: |
| | df["__dedup__"] = ( |
| | df.get("title", "").astype(str) + "|" + |
| | df.get("company", "").astype(str) + "|" + |
| | df.get("location", "").astype(str) + "|" + |
| | df.get("job_url", "").astype(str) |
| | ) |
| | return df.drop_duplicates("__dedup__").drop(columns="__dedup__") |
| |
|
| |
|
| | def compute_keyword_score(text: str, keywords: List[str]) -> int: |
| | text_l = (text or "").lower() |
| | return sum(text_l.count(k.lower()) for k in keywords if k) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | def email_secrets_available() -> bool: |
| | required = [ |
| | "SMTP_SERVER", |
| | "SMTP_PORT", |
| | "SMTP_USER", |
| | "SMTP_PASSWORD", |
| | "EMAIL_FROM", |
| | ] |
| | return all(key in st.secrets for key in required) |
| |
|
| |
|
| | def send_email_with_csv(recipient_email: str, df: pd.DataFrame): |
| | smtp_server = st.secrets["SMTP_SERVER"] |
| | smtp_port = int(st.secrets["SMTP_PORT"]) |
| | smtp_user = st.secrets["SMTP_USER"] |
| | smtp_password = st.secrets["SMTP_PASSWORD"] |
| | email_from = st.secrets["EMAIL_FROM"] |
| |
|
| | msg = EmailMessage() |
| | msg["Subject"] = "Your Job Search Results" |
| | msg["From"] = email_from |
| | msg["To"] = recipient_email |
| | msg.set_content( |
| | "Hello,\n\nAttached is the CSV file containing your job search results.\n\nRegards,\nPrivate Job Search Tool" |
| | ) |
| |
|
| | csv_data = df.to_csv(index=False) |
| | msg.add_attachment(csv_data, subtype="csv", filename="job_results.csv") |
| |
|
| | with smtplib.SMTP(smtp_server, smtp_port) as server: |
| | server.starttls() |
| | server.login(smtp_user, smtp_password) |
| | server.send_message(msg) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | def extract_search_parameters(client, prompt: str) -> Dict[str, str]: |
| | system_prompt = """ |
| | Extract job search parameters. |
| | Return JSON ONLY: |
| | |
| | { |
| | "search_term": "<job title or keywords>", |
| | "location": "<city, province/state, or country>" |
| | } |
| | """ |
| |
|
| | response = client.chat.completions.create( |
| | model="meta-llama/llama-4-scout-17b-16e-instruct", |
| | temperature=0.2, |
| | max_tokens=200, |
| | messages=[ |
| | {"role": "system", "content": system_prompt}, |
| | {"role": "user", "content": prompt} |
| | ] |
| | ) |
| |
|
| | try: |
| | return json.loads(response.choices[0].message.content) |
| | except Exception: |
| | return {"search_term": prompt, "location": "Canada"} |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | @st.cache_data(ttl=3600) |
| | def get_indeed_jobs( |
| | search_term: str, |
| | location: str, |
| | radius_km: int, |
| | posted_within_days: int |
| | ) -> pd.DataFrame: |
| | try: |
| | jobs = scrape_jobs( |
| | site_name=["indeed"], |
| | search_term=search_term, |
| | location=location, |
| | results_wanted=100, |
| | hours_old=posted_within_days * 24, |
| | country_indeed="Canada", |
| | radius=radius_km |
| | ) |
| | return pd.DataFrame(jobs) |
| | except Exception: |
| | return pd.DataFrame() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | def main(): |
| | st.set_page_config(page_title="Private Job Search", layout="centered") |
| | st.title("📄 Private Job Search, Rank & Download") |
| |
|
| | |
| | |
| | |
| | job_prompt = st.text_area( |
| | "Describe the job you are looking for", |
| | placeholder="e.g. Civil Engineer, Water Resources, Transportation", |
| | height=120 |
| | ) |
| |
|
| | api_key = st.text_input("Groq API Key", type="password") |
| |
|
| | |
| | |
| | |
| | st.subheader("Location") |
| |
|
| | predefined_cities = [ |
| | "Use AI / Prompt Location", |
| | "Calgary, AB", |
| | "Edmonton, AB", |
| | "Toronto, ON", |
| | "Vancouver, BC", |
| | "Mississauga, ON", |
| | "Brampton, ON", |
| | "Ottawa, ON", |
| | "Hamilton, ON", |
| | "Custom city..." |
| | ] |
| |
|
| | selected_city = st.selectbox("Select city", predefined_cities) |
| |
|
| | custom_city = "" |
| | if selected_city == "Custom city...": |
| | custom_city = st.text_input( |
| | "Enter city (e.g., Red Deer, AB or Surrey, BC)" |
| | ) |
| |
|
| | |
| | |
| | |
| | st.subheader("Job Boards") |
| | use_indeed = st.checkbox("Indeed", value=True) |
| |
|
| | |
| | |
| | |
| | st.subheader("Filters") |
| |
|
| | posted_within_days = st.slider( |
| | "Posted within last (days)", |
| | min_value=1, |
| | max_value=30, |
| | value=7 |
| | ) |
| |
|
| | radius_km = st.slider( |
| | "Search radius (km)", |
| | min_value=5, |
| | max_value=100, |
| | value=25, |
| | step=5 |
| | ) |
| |
|
| | |
| | |
| | |
| | keywords_raw = st.text_input( |
| | "Keyword ranking (comma-separated)", |
| | placeholder="water, wastewater, stormwater, EPANET" |
| | ) |
| | keywords = [k.strip() for k in keywords_raw.split(",") if k.strip()] |
| |
|
| | |
| | |
| | |
| | send_email = st.checkbox("📧 Send results by email (optional)") |
| | email_address = st.text_input("Email address") if send_email else None |
| |
|
| | |
| | |
| | |
| | if st.button( |
| | "🔍 Search Jobs", |
| | disabled=not job_prompt or not api_key |
| | ): |
| | client = groq.Client(api_key=api_key) |
| |
|
| | with st.spinner("Understanding your request..."): |
| | params = extract_search_parameters(client, job_prompt) |
| |
|
| | |
| | if selected_city == "Use AI / Prompt Location": |
| | location = params.get("location", "Canada") |
| | elif selected_city == "Custom city...": |
| | location = custom_city if custom_city else params.get("location", "Canada") |
| | else: |
| | location = selected_city |
| |
|
| | if not use_indeed: |
| | st.warning("No job boards selected.") |
| | return |
| |
|
| | with st.spinner("Searching jobs..."): |
| | jobs_df = get_indeed_jobs( |
| | params["search_term"], |
| | location, |
| | radius_km, |
| | posted_within_days |
| | ) |
| |
|
| | if jobs_df.empty: |
| | st.warning("No jobs found.") |
| | return |
| |
|
| | jobs_df.fillna("", inplace=True) |
| | jobs_df = remove_duplicates(jobs_df) |
| |
|
| | |
| | jobs_df["keyword_score"] = jobs_df.apply( |
| | lambda r: compute_keyword_score( |
| | f"{r.get('title','')} {r.get('description','')}", |
| | keywords |
| | ), |
| | axis=1 |
| | ) |
| |
|
| | jobs_df = jobs_df.sort_values( |
| | by="keyword_score", |
| | ascending=False |
| | ) |
| |
|
| | st.success(f"✅ Found {len(jobs_df)} jobs for **{location}**") |
| |
|
| | |
| | |
| | |
| | csv_data = jobs_df.to_csv(index=False).encode("utf-8") |
| | st.download_button( |
| | label="⬇️ Download Jobs (CSV)", |
| | data=csv_data, |
| | file_name="job_results.csv", |
| | mime="text/csv" |
| | ) |
| |
|
| | |
| | |
| | |
| | if send_email: |
| | if not email_address: |
| | st.warning("Please enter an email address.") |
| | elif not email_secrets_available(): |
| | st.warning("Email not configured. Download is still available.") |
| | else: |
| | with st.spinner("Sending email..."): |
| | try: |
| | send_email_with_csv(email_address, jobs_df) |
| | st.success(f"📧 Results emailed to {email_address}") |
| | except Exception as e: |
| | st.error(f"Failed to send email: {e}") |
| |
|
| | |
| | |
| | |
| | st.subheader("Preview (Top 20 Results)") |
| | preview_cols = [ |
| | c for c in [ |
| | "title", "company", "location", |
| | "keyword_score", "date_posted", "job_url" |
| | ] if c in jobs_df.columns |
| | ] |
| | st.dataframe( |
| | jobs_df[preview_cols].head(20), |
| | use_container_width=True |
| | ) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | main() |
| |
|
| |
|
| |
|