Spaces:
Sleeping
Sleeping
| 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") | |
| 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) | |