import streamlit as st import requests import pdfplumber import docx from sentence_transformers import SentenceTransformer import faiss from groq import Groq from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, ListFlowable, ListItem import io # ----------------------------- # CONFIG # ----------------------------- REMOTEOK_URL = "https://remoteok.com/api" EMBED_MODEL = "BAAI/bge-small-en-v1.5" AI_MODEL = "openai/gpt-oss-120b" # Groq model # Load embedding model @st.cache_resource def load_model(): return SentenceTransformer(EMBED_MODEL) model = load_model() # Initialize Groq client groq_client = Groq(api_key=st.secrets.get("GROQ_API_KEY", None)) # ----------------------------- # FUNCTIONS # ----------------------------- def extract_text_from_resume(file): """Extract text from PDF or DOCX file""" if file.name.endswith(".pdf"): text = "" with pdfplumber.open(file) as pdf: for page in pdf.pages: text += page.extract_text() or "" return text elif file.name.endswith(".docx"): doc = docx.Document(file) text = "\n".join([p.text for p in doc.paragraphs]) return text else: st.error("Unsupported file type. Please upload PDF or DOCX.") return "" def fetch_jobs(): resp = requests.get(REMOTEOK_URL) if resp.status_code == 200: jobs = resp.json()[1:] # skip metadata return jobs return [] def embed_texts(texts): return model.encode(texts, convert_to_numpy=True, normalize_embeddings=True) def match_jobs(resume_text, jobs, top_k=5): job_texts = [f"{job.get('position','')} {job.get('company','')} {job.get('description','')}" for job in jobs] resume_vec = embed_texts([resume_text]) job_vecs = embed_texts(job_texts) dim = job_vecs.shape[1] index = faiss.IndexFlatIP(dim) index.add(job_vecs) scores, idx = index.search(resume_vec, top_k) results = [] for i, score in zip(idx[0], scores[0]): results.append((jobs[i], float(score))) return results def generate_resume(resume_text, job): prompt = f""" You are an AI career assistant. Given this resume:\n{resume_text}\n and this job description:\n{job['description']}\n Generate a structured resume in this format: Summary ----------------- [2-3 line summary tailored for the job] Skills ----------------- - Skill 1 - Skill 2 - Skill 3 Experience ----------------- Job Title | Company | Dates • Achievement 1 • Achievement 2 Education ----------------- Degree | Institution | Year """ chat_completion = groq_client.chat.completions.create( model=AI_MODEL, messages=[{"role": "user", "content": prompt}], temperature=0.7, ) return chat_completion.choices[0].message.content def generate_cover_letter(resume_text, job, name, email, phone): prompt = f""" You are an AI career assistant. Given this resume:\n{resume_text}\n and this job description:\n{job['description']}\n Generate a professional, one-page cover letter tailored to this role. Format it like this: Dear Hiring Manager, [Intro paragraph: Show enthusiasm and alignment with company/role] [Body paragraph: Highlight 2-3 most relevant skills/experiences from resume] [Closing paragraph: Express eagerness and thank them] Sincerely, {name} {email} | {phone} """ chat_completion = groq_client.chat.completions.create( model=AI_MODEL, messages=[{"role": "user", "content": prompt}], temperature=0.7, ) return chat_completion.choices[0].message.content def build_pdf(content, title="Resume", name="John Doe", email="john.doe@email.com", phone="+1 234 567 890"): buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=A4, leftMargin=40, rightMargin=40, topMargin=40, bottomMargin=40) styles = getSampleStyleSheet() # Custom styles header_style = ParagraphStyle("Header", parent=styles["Heading1"], fontSize=18, spaceAfter=6, textColor=colors.HexColor("#2C3E50"), alignment=1) contact_style = ParagraphStyle("Contact", parent=styles["Normal"], fontSize=11, textColor=colors.HexColor("#566573"), alignment=1) section_style = ParagraphStyle("Section", parent=styles["Heading2"], fontSize=13, spaceBefore=15, spaceAfter=8, textColor=colors.HexColor("#1B2631")) normal_style = ParagraphStyle("Normal", parent=styles["Normal"], fontSize=11, leading=15) bullet_style = ParagraphStyle("Bullet", parent=styles["Normal"], fontSize=11, leading=15, leftIndent=20) story = [] # ---- HEADER ---- story.append(Paragraph(name, header_style)) story.append(Paragraph(f"{email} | {phone}", contact_style)) story.append(Spacer(1, 12)) # ---- BODY ---- if title == "Resume": sections = content.split("**") for sec in sections: sec = sec.strip() if not sec: continue if sec.lower().startswith("summary"): story.append(Paragraph("Summary", section_style)) elif sec.lower().startswith("skills"): story.append(Paragraph("Skills", section_style)) elif sec.lower().startswith("experience"): story.append(Paragraph("Experience", section_style)) elif sec.lower().startswith("education"): story.append(Paragraph("Education", section_style)) else: if sec.startswith("- "): bullets = [s.strip("- ").strip() for s in sec.split("\n") if s.strip()] bullet_list = ListFlowable([ListItem(Paragraph(b, bullet_style)) for b in bullets], bulletType="bullet") story.append(bullet_list) else: story.append(Paragraph(sec, normal_style)) story.append(Spacer(1, 8)) else: # Treat as cover letter: keep paragraphs for line in content.split("\n"): if line.strip(): story.append(Paragraph(line.strip(), normal_style)) story.append(Spacer(1, 10)) doc.build(story) buffer.seek(0) return buffer # ----------------------------- # STREAMLIT UI # ----------------------------- st.title("MATCHHIVE - AI Job Matcher") # Upload resume resume_file = st.file_uploader("Upload your resume (PDF or DOCX)", type=["pdf", "docx"]) if resume_file: resume_text = extract_text_from_resume(resume_file) if resume_text.strip(): st.subheader("Contact Information") name = st.text_input("Full Name", "John Doe") email = st.text_input("Email", "john.doe@email.com") phone = st.text_input("Phone", "+1 234 567 890") st.subheader("Fetching jobs...") jobs = fetch_jobs() st.subheader("Best Matches") matches = match_jobs(resume_text, jobs, top_k=5) for job, score in matches: st.markdown(f"**{job['position']}** at *{job['company']}* \n" f"[View Job Posting]({job['url']}) \n" f"**Match Score:** {score:.2f}") col1, col2 = st.columns(2) with col1: if st.button(f"Generate Resume for {job['position']}", key=f"resume_{job['id']}"): tailored_resume = generate_resume(resume_text, job) edited_resume = st.text_area("Tailored Resume", tailored_resume, height=300) pdf_buffer = build_pdf(edited_resume, title="Resume", name=name, email=email, phone=phone) st.download_button( label="📥 Download Resume (PDF)", data=pdf_buffer, file_name="tailored_resume.pdf", mime="application/pdf", ) with col2: if st.button(f"Generate Cover Letter for {job['position']}", key=f"cl_{job['id']}"): tailored_cl = generate_cover_letter(resume_text, job, name, email, phone) edited_cl = st.text_area("Cover Letter", tailored_cl, height=300) pdf_buffer = build_pdf(edited_cl, title="Cover Letter", name=name, email=email, phone=phone) st.download_button( label="📥 Download Cover Letter (PDF)", data=pdf_buffer, file_name="cover_letter.pdf", mime="application/pdf", )