import gradio as gr import fitz import joblib import numpy as np import openai import pandas as pd from fpdf import FPDF import tempfile import os openai.api_key=os.getenv("OPENAI_API_KEY") linkedin_rag_df=pd.read_csv("final_linkedin_post_ideas.csv") # Load model + vectorizer model = joblib.load("xgb_resume_model.pkl") vectorizer = joblib.load("tfidf_vectorizer.pkl") # Thresholds from earlier training q_low = 0.5166355336146575 q_high = 2.831921823997124 # Weighted scoring dict (add yours here) weighted_keywords = { # πŸ”Ή Advanced AI / Technical 'llm': 3.5, 'langchain': 3.5, 'rag': 3.5, 'rag pipeline': 3.5, 'vector db': 3.5, 'weaviate': 3, 'chromadb': 3, 'pinecone': 3, 'agent': 3, 'langchain agents': 3.5, 'autonomous agent': 3, 'fine-tuning': 3, 'embedding': 3, 'semantic search': 3, 'transformers': 3, 'huggingface': 3, 'openai': 3, 'streamlit': 2.5, 'flask': 2.5, 'gradio': 2.5, 'pytorch': 2.5, 'tensorflow': 2.5, 'sql': 2, 'power bi': 2, 'pandas': 2, 'numpy': 2, 'data analysis': 2, # πŸ”Έ Business / Management 'project management': 3.5, 'agile': 3, 'stakeholder': 2.5, 'scrum': 3, 'planning': 2, 'budgeting': 2, 'strategic partnerships': 3, 'gtm': 2.5, 'account planning': 2.5, 'market share': 2.5, 'revenue growth': 3, 'client relationships': 2.5, # 🟒 Sales / CRM 'crm': 3, 'channel sales': 3, 'business development': 3, 'partner engagement': 3, 'sales forecasting': 2.5, 'campaign': 2.5, 'salesforce': 2.5, 'leads': 2, 'market research': 2.5, 'negotiation': 2, 'presentation': 2, # 🟠 Education / Teaching 'curriculum': 3, 'lesson planning': 2.5, 'teaching': 3, 'student engagement': 2.5, 'learning outcomes': 2.5, 'training': 2.5, 'academic institutions': 2.5, 'ministry of education': 3, 'educational partnerships': 2.5, 'demo': 2, 'workshop': 2, # πŸ”΅ HR / Support 'recruitment': 3, 'employee engagement': 2.5, 'onboarding': 2.5, 'conflict resolution': 2, 'policy': 2, 'human resources': 3, # πŸ”΄ Security / Law Enforcement 'security guard': 3, 'loss prevention': 3, 'cctv': 2.5, 'access control': 2.5, 'conflict de-escalation': 2, 'law enforcement': 2.5, 'threat assessment': 2, 'certified protection': 3, 'crisis intervention': 2, 'surveillance': 2 } # Score function def weighted_score(text): text = text.lower() return sum(weight for kw, weight in weighted_keywords.items() if kw in text) # GPT Job Role Prediction roles = ["AI Engineer", "Data Scientist", "Project Manager", "Sales Executive", "Teacher", "HR Specialist", "Security Officer"] def gpt_predict_role(resume_text): prompt = f""" You are a job role classification expert. You will be given a resume summary and skills. From the list below, identify the **single most appropriate job role** this candidate fits into. Do not guess or create new titles. Choose **only from the list**. Roles: {', '.join(roles)} Resume: {resume_text} Answer only with one of the roles from the list. """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0 ) return response.choices[0].message.content.strip() except Exception as e: return f"❌ Error: {str(e)}" # GPT Resume Feedback def gpt_resume_feedback(resume_text): prompt = f""" You are an expert resume reviewer. Analyze the resume text below and provide **structured, clear, and reader-friendly** markdown feedback under these sections: ## πŸ”§ Resume Improvement Suggestions **Clarity & Formatting** - (Short bullet points) **Missing Sections** - (Mention if Skills, Certifications, Projects, etc. are missing) **Projects** - (How to describe them better or where to move them) --- ## 🧠 Missing Keywords List any specific tools, technologies, or keywords that are missing for the predicted role. --- ## πŸš€ Quick Wins **Certifications** - Recommend top 2 certifications to improve resume strength **Free Courses** - Suggest 1-2 free courses from platforms like Coursera, edX, or YouTube **Small Edits** - Easy improvements: better formatting, quantifiable achievements, etc. Be professional, clean, encouraging and use proper markdown formatting: - Use **bold** for subheadings - Use `-` for bullet points - If suggesting any certifications or courses, format them as **clickable links** using markdown like: `[Course Title](https://example.com)` Resume: {resume_text} """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.3 ) return response.choices[0].message.content.strip() except Exception as e: return f"❌ Error: {str(e)}" # Main app logic def process_resume(file): doc = fitz.open(file.name) resume_text = " ".join([page.get_text() for page in doc]).strip() # ML prediction X_input = vectorizer.transform([resume_text]) predicted_strength = model.predict(X_input)[0] # Hybrid logic resume_score = weighted_score(resume_text) normalized_score = resume_score / np.log(len(resume_text.split()) + 1) if predicted_strength == 'Average' and normalized_score >= q_high: predicted_strength = 'Strong' elif predicted_strength == 'Average' and normalized_score < q_low: predicted_strength = 'Weak' # GPT feedback + role role = gpt_predict_role(resume_text) tips = gpt_resume_feedback(resume_text) return predicted_strength, role, tips #Linkedin Enhancement def generate_linkedin_feedback(about_text, file, role): try: doc = fitz.open(file.name) resume_text = " ".join([page.get_text() for page in doc]).strip() except: resume_text = "" # Get RAG tips # Check if the role exists in the dataframe before accessing .values if role in linkedin_rag_df['role'].values: rag_tip = linkedin_rag_df[linkedin_rag_df['role'] == role]['tips'].values rag_tip_text = rag_tip[0] if len(rag_tip) > 0 else "" else: rag_tip_text = "" # Provide a default or empty tip if role not found prompt = f""" You are a career branding expert helping people improve their LinkedIn. Based on the resume and predicted role, generate structured LinkedIn content guidance. 1. πŸ”§ **Improve "About Me"** If About Me is provided: suggest improvements. If no About Me is given, generate a new one in a confident, first-person tone β€” as if the user is speaking directly to their network without mentioning that nothing was provided. Avoid formal third-person voice. Use warm, natural language suitable for LinkedIn. 2. ✍ **Suggest 3 LinkedIn post ideas** Inspire posts relevant to their role. Include tips from this RAG input:\n{rag_tip_text} 3. πŸ”– **Offer engagement tips** How to grow visibility (e.g., comment, hashtag use, follow-up posts) Format your reply with markdown bullets and emojis. Be concise and encouraging. Resume: {resume_text} About Section: {about_text} """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.5 ) return response.choices[0].message.content.strip() except Exception as e: return f"❌ Error: {str(e)}" #Job Match def match_resume_with_jd(resume_file, jd_text): try: doc = fitz.open(resume_file.name) resume_text = " ".join([page.get_text() for page in doc]).strip() except: return "❌ Unable to read resume." prompt = f""" You are a helpful and ethical career assistant. Compare the candidate's resume and the job description below. Do these 3 things: 1. **Match Score**: Estimate how well the resume matches the JD (0–100%) with clear reasoning. 2. **Missing Keywords**: Identify only the important keywords or skills that are *actually not found* in the resume. 3. **Suggestions to Improve**: Based ONLY on the content present in the resume, suggest realistic ways the candidate can: - Rephrase existing experience to better match the job - Emphasize transferrable skills (like mentoring, public speaking, teamwork) - Avoid fabricating roles or experiences not present Never invent teaching experience, tools, or certifications that are not mentioned. Resume: {resume_text} Job Description: {jd_text} Respond in markdown format with bold section headings. """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.3 ) return response.choices[0].message.content.strip() except Exception as e: return f"❌ GPT Error: {str(e)}" # Job Explorer def generate_job_explorer_output(resume_file): import fitz from urllib.parse import quote # Step 1: Extract text from PDF try: doc = fitz.open(resume_file.name) resume_text = " ".join([page.get_text() for page in doc]).strip() except: return "❌ Unable to read resume. Please upload a valid PDF." # Step 2: Use GPT to detect experience level + suggest roles prompt = f""" You are an AI career coach. Read the resume below and do the following: 1. Predict the user's experience level: Entry / Mid / Senior - Consider total years of work **and** how recent their last full-time job was. - If they had a long break or are doing a training/residency now, treat them as Entry-Level. 2. Suggest 3–4 job roles the candidate is likely to be a good fit for (avoid duplicates) Respond in this markdown format: **Experience Level**: Entry **Suggested Roles**: - Data Analyst - Junior BI Developer - Reporting Analyst Resume: {resume_text} """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.5 ) result = response.choices[0].message.content.strip() except Exception as e: return f"❌ Error from GPT: {str(e)}" # Step 3: Generate Indeed links based on experience level final_output = result + "\n\n**πŸ”— Explore Jobs on Indeed UAE:**\n" # Extract experience level experience_level = "entry" for line in result.splitlines(): if "Experience Level" in line: experience_level = line.split(":")[-1].strip().lower() # Experience level filters for Indeed experience_filters = { "entry": "&explvl=entry_level", "mid": "&explvl=mid_level", "senior": "&explvl=senior_level" } exp_filter = experience_filters.get(experience_level, "") # Create links for each suggested role for line in result.splitlines(): if "- " in line and "Suggested Roles" not in line: role = line.strip("- ").strip() query = quote(role) indeed_url = f"https://ae.indeed.com/jobs?q={query}&l=United+Arab+Emirates{exp_filter}" final_output += f"- [{role} Jobs in UAE]({indeed_url})\n" # Final tip final_output += "\nπŸ’‘ _Tip: You can also search the same job titles on LinkedIn or Bayt for more options._" return final_output # 🧠 Conversational career agent def chat_with_career_agent(history, user_message, resume_file): try: doc = fitz.open(resume_file.name) resume_text = " ".join([page.get_text() for page in doc]).strip() except: return history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": "❌ Unable to read resume."}] prompt = f""" You are a warm and friendly AI career coach. ONLY answer questions related to: - Resume review, improvement - LinkedIn profile enhancement - Role suitability or job matching - Career growth plans (e.g., certifications, skill roadmaps) - Interview tips, career clarity Ignore personal, unrelated questions (like recipes, coding help, travel). Resume: {resume_text} User asked: {user_message} If the query is valid (even if slightly unclear), ask a clarifying question and help warmly. If it's off-topic (not career/job related), reply: "I'm here to support your career and resume journey only 😊" """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[ {"role": "system", "content": "You are a career guidance assistant."}, {"role": "user", "content": prompt} ], temperature=0.5 ) reply = response.choices[0].message.content.strip() except Exception as e: reply = f"❌ Error: {str(e)}" return history + [[user_message, reply]] #Download PDF #for main tab def rewrite_resume_main(resume_file, strength, role, tips): resume_text = extract_resume_text(resume_file) prompt = f""" You are a professional resume rewriter. Rewrite the following resume to improve its strength, based on: - Strength: {strength} - Predicted Role: {role} - AI Feedback: {tips} Generate a clean, ATS-friendly version with proper formatting in sections like: 1. **Summary** 2. **Skills** 3. **Experience** 4. **Projects** 5. **Certifications** Keep the language warm, confident, and professional. Do NOT mention the words 'suggestion' or 'AI'. Resume to rewrite: {resume_text} """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.4 ) rewritten = response.choices[0].message.content.strip() except Exception as e: return None, f"❌ GPT Error: {str(e)}" pdf_path = generate_pdf(rewritten) print("βœ… PDF saved at:", pdf_path) return pdf_path, "βœ… Resume rewritten successfully!" #for jdmatch tab def rewrite_resume_for_jd(resume_file, jd_text): resume_text = extract_resume_text(resume_file) prompt = f""" You are an AI resume enhancer. Rewrite this resume to best match the following job description (JD) while being honest and using only real information found in the resume. Resume: {resume_text} JD: {jd_text} Structure it with proper headings: Summary, Skills, Experience, Projects, and Certifications. Do not add false experiences. Use persuasive language to reframe existing experience in a way that aligns with the JD. """ try: response = openai.ChatCompletion.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.4 ) rewritten = response.choices[0].message.content.strip() except Exception as e: return None, f"❌ GPT Error: {str(e)}" pdf_path = generate_pdf(rewritten) return pdf_path, "βœ… Resume rewritten for JD match!" def generate_pdf(resume_text): pdf = FPDF() pdf.add_page() pdf.set_auto_page_break(auto=True, margin=15) pdf.set_font("Arial", size=12) for line in resume_text.split("\n"): if line.strip() == "": pdf.ln() else: pdf.multi_cell(0, 10, line.encode("latin-1", "replace").decode("latin-1")) temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") pdf.output(temp_file.name) return temp_file.name def clear_fields(): return None, "", "","","","",None,"","" def show_loading_linkedin(): return "⏳ Generating your LinkedIn suggestions... Please wait." def hide_loading_linkedin(): return "" def show_main_loading(): return "⏳ Preparing your resume... Please wait." def show_main_file(file_path): return gr.update(value=file_path, visible=True) def hide_main_loading(): return "" # Clears the status message def show_loading_jd(): return "⏳ Matching in progress..." def hide_loading_jd(): return "" def show_loading(): return "⏳ Looking for jobs based on your resume…" def hide_loading(): return " " # Clears the status message def clear_jd_fields(): # Clears the JD Tab fields return None, "", ""," ",None # Corresponds to shared_resume_file, jd_text_input, jd_output def clear_explore_fields(): # Clears the Job Explorer Tab fields return None, "" , " "# Corresponds to shared_resume_file, explore_output def extract_resume_text(resume_file): try: doc = fitz.open(resume_file.name) return " ".join([page.get_text() for page in doc]).strip() except: return "" def show_chat_ui(): return gr.update(visible=True) with gr.Blocks(css=""" /* ✨ Updated Clean UI */ body { background-color: #f0f4f8 !important; } .gradio-container { font-family: 'Segoe UI', sans-serif; max-width: 960px; margin: auto; padding: 30px; background-color: #ffffff; border-radius: 16px; box-shadow: 0 6px 18px rgba(0,0,0,0.08); } h1, h2, h3 { color: #2a4365; } .card { background-color: #ffffff; border: 1px solid #e2e8f0; padding: 20px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 2px 6px rgba(0,0,0,0.05); } /* Upload Box Styling */ .gr-file-upload, .gr-file, div[data-testid="file"] { background-color: #f7fafc !important; border: 2px dashed #90cdf4 !important; color: #2d3748 !important; font-size: 14px !important; border-radius: 10px !important; min-height: 30px !important; } .gr-file .file-preview { max-height: 18px !important; font-size: 12px !important; padding: 2px !important; } button { background-color: #3182ce !important; color: white !important; font-weight: bold; border-radius: 8px !important; padding: 10px 16px !important; } button.secondary { background-color: #e2e8f0 !important; color: #2d3748 !important; } .gr-button { width: 100% !important; margin-top: 8px; } /* βœ… Compact Upload Size */ .compact-upload { min-height: 80px !important; height: 80px !important; padding: 8px !important; } /* βœ… Markdown Enhancements */ .gr-markdown { font-size: 16px; color: #1a202c; } /* βœ… Labels */ label { font-weight: 500; color: #374151; } /* βœ… Resume Output Boxes */ textarea[aria-label*="Resume Strength"], textarea[aria-label*="Predicted Job Role"] { background-color: #fff !important; border: 1px solid #ddd !important; color: #111; } """) as demo: gr.Markdown("## πŸš€ PATHFORGE AI") gr.Markdown("Empower your job search with AI resume analysis, job fit, and LinkedIn guidance.") with gr.Tabs(): with gr.Tab("🏠 Main Resume Coach"): with gr.Column(elem_classes="card"): resume_file = gr.File(label="πŸ“„ Upload Resume", file_types=[".pdf"]) with gr.Row(): submit_btn = gr.Button("πŸ” Analyze Resume", interactive=False) clear_btn = gr.Button("πŸ—‘οΈ Clear", variant="secondary") download_main_btn = gr.Button("πŸ“₯ Download AI Resume") status_text_main = gr.Markdown(" ") # βœ… Define missing output file main_pdf_file = gr.File(visible=False, elem_classes="compact-upload") with gr.Accordion("πŸ’ͺ Resume Insights", open=True): with gr.Row(): with gr.Column(): # Added Column strength_output = gr.Textbox(label="Resume Strength", interactive=False) with gr.Column(): # Added Column role_output = gr.Textbox(label="Predicted Role", interactive=False) with gr.Accordion("πŸ› οΈ AI Feedback", open=False): tips_output = gr.Markdown("") with gr.Column(elem_classes="card"): about_input = gr.Textbox(label="LinkedIn About(Can PAste Your About me or it will generate from the resume) (Optional)", lines=3) linkedin_btn = gr.Button("✨ Enhance LinkedIn") linkedin_output = gr.Markdown("") status_text = gr.Markdown(" ") with gr.Tab("🎯 JD Match + Explorer"): with gr.Column(elem_classes="card"): shared_resume_file = gr.File(label="πŸ“„ Upload Resume Again", file_types=[".pdf"]) with gr.Column(elem_classes="card"): jd_text_input = gr.Textbox(label="πŸ“‹ Paste Job Description") with gr.Row(): jd_match_btn = gr.Button("πŸ” Match with JD",visible=False,interactive=False) jd_clear_btn = gr.Button("🧹 Clear", variant="secondary") download_jd_btn = gr.Button("πŸ“₯ Enhanced Resume ",visible=False,interactive=False) jd_status = gr.Markdown(" ") jd_output = gr.Markdown("") # βœ… Define missing output file jd_pdf_file = gr.File(visible=True, elem_classes="compact-upload") with gr.Column(elem_classes="card"): explore_btn = gr.Button("🌍 Suggest Jobs") clear_explore_btn = gr.Button("🧹 Clear Explorer", variant="secondary") explore_status = gr.Markdown(" ") explore_output = gr.Markdown("") with gr.Column(elem_classes="card"): career_chat_btn = gr.Button("πŸ’¬ Talk to Career Coach") chat_section = gr.Column(visible=False) with chat_section: career_chatbot = gr.Chatbot() text_input = gr.Textbox(label="πŸ’¬ Ask your question") send_btn = gr.Button("🧠 Send") # Define button clicks *inside* the gr.Blocks context submit_btn.click( fn=process_resume, inputs=resume_file, outputs=[strength_output, role_output, tips_output] ) # Dynamically enable button + add green border on upload resume_file.change( lambda file: ( gr.update(interactive=True), # submit_btn gr.update(interactive=True), # download_main_btn gr.update(visible=True if file else False), # main_pdf_file gr.update(elem_classes="compact-upload uploaded") if file else gr.update(elem_classes="compact-upload") # resume_file css ), inputs=[resume_file], outputs=[submit_btn, download_main_btn, main_pdf_file, resume_file] ) clear_btn.click( fn=clear_fields, inputs=[], outputs=[resume_file, strength_output, role_output, tips_output, about_input, linkedin_output,main_pdf_file, status_text_main, status_text] ) linkedin_btn.click( fn=show_loading_linkedin, inputs=[], outputs=[status_text] ).then( fn=generate_linkedin_feedback, inputs=[about_input, resume_file, role_output], # Use resume_file from Tab 1 outputs=[linkedin_output] ).then( fn=hide_loading_linkedin, inputs=[], outputs=[status_text] ) def rewrite_main_flow(resume_file, strength, role, tips): path, msg = rewrite_resume_main(resume_file, strength, role, tips) print("βœ… PDF Path:", path) if path: return gr.update(value=path, visible=True, interactive=True), msg else: return gr.update(visible=False), msg # Handle error case download_main_btn.click( fn=show_main_loading, inputs=[], outputs=[status_text_main] ).then( fn=rewrite_main_flow, inputs=[resume_file, strength_output, role_output, tips_output], outputs=[main_pdf_file, status_text_main] ).then( fn=hide_main_loading, inputs=[], outputs=[status_text_main] ) # JD Match Clicks jd_match_btn.click( fn=show_loading_jd, inputs=[], outputs=[jd_status] # Update JD status ).then( fn=match_resume_with_jd, inputs=[shared_resume_file, jd_text_input], # Use shared_resume_file from Tab 2 outputs=[jd_output] ).then( fn=hide_loading_jd, # hide status inputs=[], outputs=[jd_status] # Update JD status ) jd_clear_btn.click( fn=clear_jd_fields, inputs=[], outputs=[shared_resume_file, jd_text_input, jd_output, jd_status, jd_pdf_file] ) jd_text_input.change( lambda jd: gr.update(visible=bool(jd.strip()), interactive=bool(jd.strip())), inputs=[jd_text_input], outputs=[jd_match_btn] ) jd_match_btn.click( fn=lambda: gr.update(visible=True, interactive=True), inputs=[], outputs=[download_jd_btn] ) # Processing flow explore_btn.click( fn=show_loading, inputs=[], outputs=[explore_status] ).then( fn=generate_job_explorer_output, inputs=[shared_resume_file], outputs=[explore_output] ).then( fn=hide_loading, inputs=[], outputs=[explore_status] ) # Added clear_explore_btn click event clear_explore_btn.click( fn=clear_explore_fields, inputs=[], outputs=[shared_resume_file, explore_output, explore_status] ) def rewrite_jd_flow(resume_file, jd_text_input): path, msg = rewrite_resume_for_jd(resume_file, jd_text_input) print("βœ… JD PDF Path:", path) if path: return gr.update(value=path, visible=True, interactive=True), msg else: return gr.update(visible=False), "❌ Failed to generate resume." download_jd_btn.click( fn=lambda: "⏳ Preparing tailored resume for JD... please wait.", inputs=[], outputs=[jd_status] ).then( fn=rewrite_jd_flow, inputs=[shared_resume_file, jd_text_input], outputs=[jd_pdf_file, jd_status] ).then( fn=lambda: "", # hide status inputs=[], outputs=[jd_status] ) career_chat_btn.click( fn=show_chat_ui, inputs=[], outputs=[chat_section] ) send_btn.click( fn=chat_with_career_agent, inputs=[career_chatbot, text_input, shared_resume_file], # shared_resume_file from JD tab outputs=career_chatbot ).then( fn=lambda: "", # clear the text input inputs=[], outputs=[text_input] ) gr.Markdown( "

✨ Built with πŸ’» ML + GPT | Made for the AI Challenge by Kiruthika Ramalingam

") demo.launch() if __name__ == "__main__": demo.launch()