| 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() |
|
|
|
|
|
|