MatchHive / app.py
ItqaAkhlaq's picture
Update app.py
f5dfcc0 verified
Raw
History Blame Contribute Delete
9.36 kB
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)