import streamlit as st import requests import pdfplumber import docx import tempfile import numpy as np from sentence_transformers import SentenceTransformer, util from crewai import Agent, Task, Crew import os # ------------------------------------- # Setup # ------------------------------------- st.set_page_config(page_title="Job Matcher", layout="wide") @st.cache_resource def load_model(): return SentenceTransformer("all-MiniLM-L6-v2") model = load_model() SKILL_KEYWORDS = [ "python","django","flask","fastapi","react","javascript","node","aws","gcp","azure", "docker","kubernetes","sql","postgres","mysql","mongodb","nlp","computer vision", "pytorch","tensorflow","keras","ml","machine learning","data science","html","css" ] JOB_STORE = [] # in-memory jobs RESUME_TEXT = "" # global resume text GROQ_KEY = os.getenv("GROQ_API_KEY") # ------------------------------------- # Agent functions # ------------------------------------- def fetch_remoteok(): url = "https://remoteok.com/api" headers = {"User-Agent":"JobMatcher/1.0"} resp = requests.get(url, headers=headers, timeout=15) data = resp.json() jobs = [j for j in data if isinstance(j, dict) and j.get("id")] normalized = [] for j in jobs: normalized.append({ "source":"remoteok", "id": str(j.get("id")), "title": j.get("position") or j.get("title"), "company": j.get("company"), "description": j.get("description") or "", "url": j.get("url"), }) return normalized def fetch_remotive(): url = "https://remotive.com/api/remote-jobs" resp = requests.get(url, timeout=15) jobs = resp.json().get("jobs", []) normalized = [] for j in jobs: normalized.append({ "source":"remotive", "id": str(j.get("id")), "title": j.get("title"), "company": j.get("company_name"), "description": j.get("description") or "", "url": j.get("url"), }) return normalized def extract_text(path, filename): text = "" if filename.lower().endswith(".pdf"): with pdfplumber.open(path) as pdf: for page in pdf.pages: t = page.extract_text() if t: text += "\n" + t elif filename.lower().endswith(".docx"): doc = docx.Document(path) for p in doc.paragraphs: text += "\n" + p.text else: with open(path,"r",encoding="utf-8",errors="ignore") as f: text = f.read() return text.strip() def extract_skills(text): found = [] low = text.lower() for k in SKILL_KEYWORDS: if k in low: found.append(k) return sorted(set(found)) def match_resume(resume_text, jobs): emb = model.encode(resume_text) results = [] for j in jobs: text = f"{j['title']} {j['description']}" job_vec = model.encode(text) sim = util.cos_sim(emb, job_vec).item() semantic_norm = (sim + 1) / 2 resume_kw = set(extract_skills(resume_text)) job_kw = set(extract_skills(text)) keyword_score = len(resume_kw & job_kw) / len(job_kw) if job_kw else 0 score = 0.7*semantic_norm + 0.3*keyword_score results.append({ **j, "match_pct": round(score*100,2), "matched_keywords": list(resume_kw & job_kw) }) return sorted(results, key=lambda x: x["match_pct"], reverse=True) # ------------------------------------- # Groq resume & cover letter generation # ------------------------------------- def groq_generate(prompt): if not GROQ_KEY: return "❌ No GROQ_API_KEY found. Please set it in environment variables." url = "https://api.groq.com/openai/v1/chat/completions" headers = {"Authorization": f"Bearer {GROQ_KEY}", "Content-Type":"application/json"} payload = { "model": "groq/gemma-7b", "messages": [ {"role": "system", "content": "You are a helpful career assistant."}, {"role": "user", "content": prompt} ], "max_output_tokens": 800 } try: r = requests.post(url, headers=headers, json=payload, timeout=60) data = r.json() return data["choices"][0]["message"]["content"] except Exception as e: return f"❌ Groq API error: {e}" def generate_tailored_resume(resume_text, job): prompt = f"""Here is a candidate resume: {resume_text[:2000]} And here is a job description: Title: {job['title']} Company: {job['company']} Description: {job['description']} Rewrite the resume in a concise, professional way tailored for this job. Return only the resume text.""" return groq_generate(prompt) def generate_cover_letter(resume_text, job): prompt = f"""Candidate profile: {resume_text[:1000]} Job details: Title: {job['title']} Company: {job['company']} Description: {job['description']} Write a tailored cover letter (200-300 words) that highlights the candidate's strengths and fit for this role.""" return groq_generate(prompt) # ------------------------------------- # CrewAI setup # ------------------------------------- fetch_agent = Agent(role="Job Fetcher", goal="Fetch jobs", backstory="Fetches jobs from multiple job boards") parse_agent = Agent(role="Resume Parser", goal="Parse resumes", backstory="Extracts text and skills") match_agent = Agent(role="Matcher", goal="Match jobs", backstory="Finds best job matches for a resume") crew = Crew(agents=[fetch_agent, parse_agent, match_agent]) # ------------------------------------- # Streamlit UI # ------------------------------------- st.title("💼 Job Matcher with CrewAI + Groq") menu = st.sidebar.radio("Menu", ["Home","Fetch Jobs","Upload Resume","Match Jobs","Generate Resume & Cover Letter"]) global_resume = st.session_state.get("resume_text","") if menu == "Home": st.write("Demo: Multi-agent CrewAI app with Groq-powered resume & cover letter generation.") elif menu == "Fetch Jobs": st.subheader("Fetch jobs") if st.button("Fetch RemoteOK & Remotive"): t1 = Task(description="Fetch RemoteOK jobs", agent=fetch_agent, function=fetch_remoteok) t2 = Task(description="Fetch Remotive jobs", agent=fetch_agent, function=fetch_remotive) results = crew.kickoff([t1,t2]) JOB_STORE.clear() for r in results: JOB_STORE.extend(r) st.success(f"Fetched {len(JOB_STORE)} jobs.") if JOB_STORE: st.write("### Sample Jobs") for j in JOB_STORE[:5]: st.write(f"- {j['title']} at {j['company']} ({j['source']})") elif menu == "Upload Resume": st.subheader("Upload Resume") uploaded = st.file_uploader("Upload PDF or DOCX", type=["pdf","docx","txt"]) if uploaded: with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp.write(uploaded.read()) path = tmp.name t = Task(description="Parse resume", agent=parse_agent, function=lambda: extract_text(path, uploaded.name)) text = crew.kickoff([t])[0] skills = extract_skills(text) st.session_state["resume_text"] = text st.success("Resume parsed!") st.write("**Detected Skills:**", skills) st.text_area("Resume text", value=text[:2000], height=300) elif menu == "Match Jobs": st.subheader("Match resume text with jobs") resume_text = st.session_state.get("resume_text","") if not resume_text: resume_text = st.text_area("Paste resume text", height=200) if st.button("Match"): if not JOB_STORE: st.warning("Fetch jobs first.") elif not resume_text.strip(): st.warning("Provide resume text first.") else: t = Task(description="Match resume with jobs", agent=match_agent, function=lambda: match_resume(resume_text, JOB_STORE)) results = crew.kickoff([t])[0] for r in results[:10]: st.markdown(f"### {r['title']} at {r['company']} | {r['match_pct']}%") st.write("Matched keywords:", r["matched_keywords"]) st.write("URL:", r["url"]) st.write("---") st.session_state["match_results"] = results elif menu == "Generate Resume & Cover Letter": st.subheader("AI Resume & Cover Letter Generator (Groq)") resume_text = st.session_state.get("resume_text","") results = st.session_state.get("match_results", JOB_STORE) if not resume_text: st.warning("Upload or paste resume first.") elif not results: st.warning("Fetch and match jobs first.") else: job_options = [f"{j['title']} at {j['company']}" for j in results[:5]] choice = st.selectbox("Select a job", job_options) if st.button("Generate Tailored Resume & Cover Letter"): job = results[job_options.index(choice)] with st.spinner("Generating tailored resume..."): tailored_resume = generate_tailored_resume(resume_text, job) with st.spinner("Generating cover letter..."): cover_letter = generate_cover_letter(resume_text, job) st.subheader("📄 Tailored Resume") st.write(tailored_resume) st.subheader("✉️ Cover Letter") st.write(cover_letter)