Spaces:
Sleeping
Sleeping
| import os | |
| import pdfplumber | |
| import google.generativeai as genai | |
| from dotenv import load_dotenv | |
| import json | |
| import gradio as gr | |
| # --- NEW: "Dark Mode" Custom CSS --- | |
| custom_css = """ | |
| /* A modern, clean "Dark Mode" theme */ | |
| body { | |
| /* A dark gradient background */ | |
| background: linear-gradient(135deg, #1e1e1e 0%, #121212 100%); | |
| font-family: 'Inter', 'Segoe UI', 'Roboto', sans-serif; | |
| } | |
| /* Style the main app container with a "dark glass" effect */ | |
| .gradio-container { | |
| border: none !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3) !important; | |
| background-color: rgba(30, 30, 30, 0.85) !important; | |
| backdrop-filter: blur(10px) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.1) !important; | |
| } | |
| /* Style the primary 'generate' buttons (Blue stands out well on dark) */ | |
| button[data-testid="button-primary"] { | |
| background: linear-gradient(90deg, #3A7BD5 0%, #00D2FF 100%); | |
| color: white; | |
| border-radius: 8px; | |
| font-weight: bold; | |
| box-shadow: 0 4px 14px 0 rgba(0, 118, 255, 0.39); | |
| border: none !important; | |
| transition: all 0.3s ease; | |
| } | |
| button[data-testid="button-primary"]:hover { | |
| box-shadow: 0 6px 20px 0 rgba(0, 118, 255, 0.5); | |
| transform: translateY(-2px); | |
| } | |
| /* --- FIX FOR TAB TITLES --- */ | |
| [data-testid="tab-button"] { | |
| color: #a0a0a0 !important; /* Light grey for unselected tabs */ | |
| font-weight: 600 !important; | |
| } | |
| [data-testid="tab-button"].selected { | |
| color: #ffffff !important; /* White for selected tab */ | |
| border-bottom: 2px solid #00D2FF !important; /* Accent color for the underline */ | |
| } | |
| /* Style ALL markdown boxes (inputs and outputs) */ | |
| [data-testid="markdown"] { | |
| background-color: #2a2a2a !important; /* Dark grey for boxes */ | |
| border-radius: 8px !important; | |
| border: 1px solid #444 !important; | |
| padding: 20px !important; | |
| box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1); | |
| } | |
| /* Force all text within markdown components to be light */ | |
| [data-testid="markdown"] p, | |
| [data-testid="markdown"] h1, | |
| [data-testid="markdown"] h2, | |
| [data-testid="markdown"] h3, | |
| [data-testid="markdown"] li, | |
| [data-testid="markdown"] ol, | |
| [data-testid="markdown"] ul { | |
| color: #e0e0e0 !important; /* Light grey text */ | |
| } | |
| /* Style the input textboxes and file upload */ | |
| [data-testid="textbox"] textarea, .gradio-file { | |
| background-color: #2a2a2a !important; | |
| border-radius: 8px !important; | |
| border: 1px solid #444 !important; | |
| color: #e0e0e0 !important; /* Make typed text light */ | |
| } | |
| /* Style the labels for inputs (e.g., "Paste the Job Description Here") */ | |
| .gradio-form > * > label { | |
| color: #a0a0a0 !important; | |
| font-weight: 500 !important; | |
| } | |
| """ | |
| # --- (All 6 Agents and Helper Functions) --- | |
| # (No changes to the agent functions themselves) | |
| def setup_api_key(): | |
| """ | |
| Loads the Google API key from the .env file and configures the SDK. | |
| """ | |
| try: | |
| load_dotenv() # Loads environment variables from .env | |
| api_key = os.getenv("GOOGLE_API_KEY") | |
| if not api_key: | |
| print("Error: GOOGLE_API_KEY not found.") | |
| print("Please create a .env file in the project root and add:") | |
| print("GOOGLE_API_KEY=YOUR_API_KEY_HERE") | |
| return False | |
| genai.configure(api_key=api_key) | |
| print("API Key configured successfully.") | |
| return True | |
| except Exception as e: | |
| print(f"Error during API configuration: {e}") | |
| return False | |
| def extract_text_from_file(file_path): | |
| """ | |
| Extracts text from an uploaded file (.pdf or .txt). | |
| """ | |
| text = "" | |
| try: | |
| file_extension = os.path.splitext(file_path)[1].lower() | |
| if file_extension == '.pdf': | |
| print(f"Extracting text from PDF: {file_path}") | |
| with pdfplumber.open(file_path) as pdf: | |
| for i, page in enumerate(pdf.pages): | |
| page_text = page.extract_text() | |
| if page_text: | |
| text += page_text + "\n" | |
| print("PDF extraction complete.") | |
| elif file_extension == '.txt': | |
| print(f"Extracting text from TXT: {file_path}") | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| text = f.read() | |
| print("TXT extraction complete.") | |
| else: | |
| return "Unsupported file format. Please upload a .txt or .pdf file." | |
| if not text.strip(): | |
| return "Error: File is empty or text could not be extracted." | |
| return text | |
| except Exception as e: | |
| print(f"Error reading file {file_path}: {e}") | |
| return f"Error reading file. It may be corrupted or in an unsupported format." | |
| def analyze_job_description(jd_text): | |
| """ | |
| Agent 1: Analyzes the job description (JD). | |
| """ | |
| model = genai.GenerativeModel('models/gemini-flash-latest') | |
| prompt = f""" | |
| You are a Senior Technical Recruiter. Analyze the following job description (JD) | |
| and extract the most critical information. | |
| Return your analysis as a JSON object, and NOTHING ELSE. Do not add "```json". | |
| The JSON structure must be: | |
| {{ | |
| "job_title": "string", | |
| "company": "string", | |
| "key_responsibilities": ["list", "of", "strings"], | |
| "hard_skills_keywords": ["list", "of", "tech", "skills"], | |
| "soft_skills_keywords": ["list", "of", "interpersonal", "skills"], | |
| "company_tone": "e.g., 'Formal & Corporate', 'Fast-paced & Startup'" | |
| }} | |
| Job Description Text: | |
| --- | |
| {jd_text} | |
| --- | |
| """ | |
| try: | |
| print("\nSending JD to AI Recruiter Agent...") | |
| response = model.generate_content(prompt) | |
| cleaned_response_text = response.text.strip().replace("```json\n", "").replace("\n```", "").strip() | |
| print("AI analysis complete.") | |
| jd_analysis = json.loads(cleaned_response_text) | |
| return jd_analysis | |
| except Exception as e: | |
| print(f"Error during AI analysis (Recruiter Agent): {e}") | |
| return None | |
| def analyze_resume(resume_text, jd_analysis_json): | |
| """ | |
| Agent 2: Analyzes the resume against the JD's analysis. | |
| """ | |
| model = genai.GenerativeModel('models/gemini-flash-latest') | |
| jd_analysis_string = json.dumps(jd_analysis_json, indent=2) | |
| prompt = f""" | |
| You are an expert Career Coach. You are given a user's resume and a JSON analysis | |
| of their target job. Your task is to find all relevant experiences and identify gaps. | |
| Return your analysis as a JSON object, and NOTHING ELSE. Do not add "```json". | |
| The JSON structure must be: | |
| {{ | |
| "matching_experiences": ["list of text snippets from the resume that are highly relevant"], | |
| "quantifiable_achievements_found": ["list of bullet points from the resume that already have numbers"], | |
| "critical_gaps": ["list of key hard skills or responsibilities from the JD that are NOT mentioned in the resume"] | |
| }} | |
| --- | |
| Target Job Analysis (JSON): | |
| {jd_analysis_string} | |
| --- | |
| User's Resume Text: | |
| {resume_text} | |
| --- | |
| """ | |
| try: | |
| print("\nSending Resume and JD to AI Career Coach Agent...") | |
| response = model.generate_content(prompt) | |
| cleaned_response_text = response.text.strip().replace("```json\n", "").replace("\n```", "").strip() | |
| print("AI career coach analysis complete.") | |
| resume_analysis = json.loads(cleaned_response_text) | |
| return resume_analysis | |
| except Exception as e: | |
| print(f"Error during AI analysis (Career Coach Agent): {e}") | |
| return None | |
| def generate_tailored_resume(resume_text, jd_analysis_json, resume_analysis_json): | |
| """ | |
| Agent 3: The "Master Rewrite Agent." | |
| """ | |
| model = genai.GenerativeModel('models/gemini-flash-latest') | |
| jd_analysis_string = json.dumps(jd_analysis_json, indent=2) | |
| resume_analysis_string = json.dumps(resume_analysis_json, indent=2) | |
| prompt = f""" | |
| You are a world-class Professional Resume Writer. Your task is to rewrite the "Experience" | |
| section of the resume to align *perfectly* with a target job. | |
| **CRITICAL RULES:** | |
| 1. **Do NOT invent new experiences.** You must only rewrite and re-phrase the | |
| *existing* experience from the original resume. | |
| 2. Integrate the `hard_skills_keywords` from the Job Analysis naturally into the bullet points. | |
| 3. Rewrite weak bullet points to use the **STAR method** (Situation, Task, Action, Result) | |
| and quantify achievements where possible. | |
| 4. Reflect the `company_tone` from the Job Analysis in your writing style. | |
| 5. **Output *only* the new, fully rewritten 'Experience' section** in clean | |
| Markdown format. Do not add *any* other text, headings, or explanations. | |
| --- | |
| [Target Job Analysis (JSON)] | |
| {jd_analysis_string} | |
| --- | |
| [Career Coach's Gap Analysis (JSON)] | |
| {resume_analysis_string} | |
| --- | |
| [Original Resume Text] | |
| {resume_text} | |
| --- | |
| Provide the rewritten "Experience" section in Markdown: | |
| """ | |
| try: | |
| print("\nSending all data to Master Rewrite Agent...") | |
| response = model.generate_content(prompt) | |
| print("Master rewrite complete.") | |
| return response.text.strip() | |
| except Exception as e: | |
| print(f"Error during AI analysis (Rewrite Agent): {e}") | |
| return None | |
| def generate_ats_report(tailored_resume_text, jd_analysis_json): | |
| """ | |
| Agent 4: The "ATS Scorecard Agent." | |
| """ | |
| model = genai.GenerativeModel('models/gemini-flash-latest') | |
| jd_analysis_string = json.dumps(jd_analysis_json, indent=2) | |
| prompt = f""" | |
| You are an ATS (Applicant Tracking System) scanner. Your task is to generate a | |
| 'Tailor Report' in Markdown format that scores the "New Resume Section" against | |
| the "Target Job Analysis." | |
| **Output Format (Must be Markdown):** | |
| ### Your 'Tailor' Report | |
| **ATS Match Score:** 85% | |
| **Keywords Hit:** | |
| - Python | |
| - Django | |
| **Keywords Missing:** | |
| - Flask | |
| - Data Analytics | |
| **Suggestion:** Great job! Consider adding a project or skill that mentions 'Flask'. | |
| --- | |
| [Target Job Analysis (JSON)] | |
| {jd_analysis_string} | |
| --- | |
| [New Resume Section] | |
| {tailored_resume_text} | |
| --- | |
| Now, generate the 'Tailor Report' in Markdown, and NOTHING ELSE. | |
| """ | |
| try: | |
| print("\nSending data to ATS Scorecard Agent...") | |
| response = model.generate_content(prompt) | |
| print("ATS report complete.") | |
| return response.text.strip() | |
| except Exception as e: | |
| print(f"Error during AI analysis (ATS Agent): {e}") | |
| return None | |
| def generate_cover_letter(resume_text, jd_analysis_json, resume_analysis_json): | |
| """ | |
| Agent 5: The "Cover Letter Agent." | |
| """ | |
| model = genai.GenerativeModel('models/gemini-flash-latest') | |
| jd_analysis_string = json.dumps(jd_analysis_json, indent=2) | |
| resume_analysis_string = json.dumps(resume_analysis_json, indent=2) | |
| prompt = f""" | |
| You are a world-class Professional Resume Writer and Career Coach. | |
| Your task is to write a compelling, professional cover letter for a job applicant | |
| based on their resume and a target job description. | |
| **CRITICAL RULES:** | |
| 1. **Tone:** The tone must be professional, confident, and aligned with the `company_tone` | |
| from the Job Analysis. | |
| 2. **Structure:** Follow a standard cover letter format (Salutation, Introduction, Body, Conclusion, Sign-off). | |
| 3. **Body Paragraphs:** | |
| * In the first body paragraph, highlight the applicant's skills that | |
| match the `hard_skills_keywords` and `key_responsibilities`. | |
| * In the second body paragraph, use the `matching_experiences` to provide a | |
| specific example or story that proves their qualification. | |
| * Address any `critical_gaps` by framing them positively, e.g., "While my | |
| direct experience with 'Flask' is developing, my proven ability to master | |
| 'Django' and other Python frameworks demonstrates my capacity to learn quickly..." | |
| 4. **Do NOT invent new experiences.** You must only use information from the | |
| "Original Resume Text" and the analyses. | |
| 5. **Output *only* the cover letter** in clean Markdown format. Do not add | |
| any other text, headings, or explanations. | |
| --- | |
| [Target Job Analysis (JSON)] | |
| {jd_analysis_string} | |
| --- | |
| [Career Coach's Gap Analysis (JSON)] | |
| {resume_analysis_string} | |
| --- | |
| [Original Resume Text] | |
| {resume_text} | |
| --- | |
| Now, provide the complete, professional cover letter in Markdown: | |
| """ | |
| try: | |
| print("\nSending all data to Cover Letter Agent...") | |
| response = model.generate_content(prompt) | |
| print("Cover letter generation complete.") | |
| return response.text.strip() | |
| except Exception as e: | |
| print(f"Error during AI analysis (Cover Letter Agent): {e}") | |
| return None | |
| def generate_interview_prep(jd_analysis_json, resume_analysis_json): | |
| """ | |
| Agent 6: The "Hiring Manager Agent." | |
| Generates custom interview questions based on the job and the candidate's gaps. | |
| """ | |
| model = genai.GenerativeModel('models/gemini-flash-latest') | |
| jd_analysis_string = json.dumps(jd_analysis_json, indent=2) | |
| resume_analysis_string = json.dumps(resume_analysis_json, indent=2) | |
| prompt = f""" | |
| You are a senior Hiring Manager preparing to interview a candidate. You are given | |
| an analysis of the job and an analysis of the candidate's resume. | |
| Your task is to generate a custom "Interview Prep Sheet" in Markdown. | |
| **Instructions:** | |
| 1. Create 2-3 **Behavioral Questions** based on the `soft_skills_keywords`. | |
| 2. Create 2-3 **Technical Questions** based on the `hard_skills_keywords`. | |
| 3. Create 1-2 **Gap-Based Questions** based on the `critical_gaps`. These are the | |
| most important questions to ask. (e.g., "I see you have experience in X, but | |
| this role requires Y. Can you tell me how you'd bridge that gap?") | |
| 4. For *each* question, provide a brief **"Hint for a strong answer"** that tells | |
| the user what the interviewer is *really* looking for. | |
| **Output *only* the prep sheet** in clean Markdown. | |
| --- | |
| [Target Job Analysis (JSON)] | |
| {jd_analysis_string} | |
| --- | |
| [Career Coach's Gap Analysis (JSON)] | |
| {resume_analysis_string} | |
| --- | |
| Now, provide the complete "Interview Prep Sheet" in Markdown: | |
| """ | |
| try: | |
| print("\nSending all data to Hiring Manager Agent...") | |
| response = model.generate_content(prompt) | |
| print("Interview prep generation complete.") | |
| return response.text.strip() | |
| except Exception as e: | |
| print(f"Error during AI analysis (Hiring Manager Agent): {e}") | |
| return None | |
| # --- (Pipeline 1: Resume Tailor) --- | |
| def tailor_resume_pipeline(resume_file, job_description): | |
| """ | |
| Main controller for the "Resume Tailor" tab. | |
| Runs the full 4-agent pipeline (Agents 1, 2, 3, 4). | |
| """ | |
| if not setup_api_key(): | |
| return "Error: API Key is not configured. Please check your .env file.", "" | |
| print("--- RESUME TAILOR PIPELINE INITIATED ---") | |
| print("Step 1: Extracting text...") | |
| if resume_file is None: | |
| return "Error: Please upload a resume file.", "" | |
| resume_text = extract_text_from_file(resume_file.name) | |
| if "Error" in resume_text or not resume_text: | |
| return f"Could not process the resume file. Error: {resume_text}", "" | |
| if not job_description: | |
| return "Error: Please paste the job description.", "" | |
| print("Step 2: Analyzing job description...") | |
| jd_data = analyze_job_description(job_description) | |
| if not jd_data: | |
| return "The AI could not analyze the job description. Please try again.", "" | |
| print("Step 3: Analyzing resume...") | |
| resume_data = analyze_resume(resume_text, jd_data) | |
| if not resume_data: | |
| return "The AI could not analyze the resume. Please try again.", "" | |
| print("Step 4: Generating tailored resume...") | |
| tailored_resume = generate_tailored_resume(resume_text, jd_data, resume_data) | |
| if not tailored_resume: | |
| return "The AI failed to generate the final resume. Please try again.", "" | |
| print("Step 5: Generating ATS report...") | |
| ats_report = generate_ats_report(tailored_resume, jd_data) | |
| if not ats_report: | |
| ats_report = "Error: Could not generate ATS report." | |
| print("Resume pipeline complete! Returning 2 outputs.") | |
| return tailored_resume, ats_report | |
| # --- (Pipeline 2: Cover Letter) --- | |
| def generate_cover_letter_pipeline(resume_file, job_description): | |
| """ | |
| Main controller for the "Cover Letter" tab. | |
| Runs a 3-agent pipeline (Agents 1, 2, 5). | |
| """ | |
| if not setup_api_key(): | |
| return "Error: API Key is not configured. Please check your .env file." | |
| print("--- COVER LETTER PIPELINE INITIATED ---") | |
| print("Step 1: Extracting text...") | |
| if resume_file is None: | |
| return "Error: Please upload a resume file." | |
| resume_text = extract_text_from_file(resume_file.name) | |
| if "Error" in resume_text or not resume_text: | |
| return f"Could not process the resume file. Error: {resume_text}" | |
| if not job_description: | |
| return "Error: Please paste the job description." | |
| print("Step 2: Analyzing job description...") | |
| jd_data = analyze_job_description(job_description) | |
| if not jd_data: | |
| return "The AI could not analyze the job description. Please try again." | |
| print("Step 3: Analyzing resume...") | |
| resume_data = analyze_resume(resume_text, jd_data) | |
| if not resume_data: | |
| return "The AI could not analyze the resume. Please try again." | |
| print("Step 4: Generating cover letter...") | |
| cover_letter = generate_cover_letter(resume_text, jd_data, resume_data) | |
| if not cover_letter: | |
| return "The AI failed to generate the cover letter. Please try again." | |
| print("Cover letter pipeline complete! Returning 1 output.") | |
| return cover_letter | |
| # --- (Pipeline 3: Interview Prep) --- | |
| def generate_interview_prep_pipeline(resume_file, job_description): | |
| """ | |
| Main controller for the "Interview Prep" tab. | |
| Runs a 3-agent pipeline (Agents 1, 2, 6). | |
| """ | |
| if not setup_api_key(): | |
| return "Error: API Key is not configured. Please check your .env file." | |
| print("--- INTERVIEW PREP PIPELINE INITIATED ---") | |
| print("Step 1: Extracting text...") | |
| if resume_file is None: | |
| return "Error: Please upload a resume file." | |
| resume_text = extract_text_from_file(resume_file.name) | |
| if "Error" in resume_text or not resume_text: | |
| return f"Could not process the resume file. Error: {resume_text}" | |
| if not job_description: | |
| return "Error: Please paste the job description." | |
| print("Step 2: Analyzing job description...") | |
| jd_data = analyze_job_description(job_description) | |
| if not jd_data: | |
| return "The AI could not analyze the job description. Please try again." | |
| print("Step 3:Analyzing resume...") | |
| resume_data = analyze_resume(resume_text, jd_data) | |
| if not resume_data: | |
| return "The AI could not analyze the resume. Please. Please try again." | |
| print("Step 4: Generating interview prep...") | |
| interview_prep = generate_interview_prep(jd_data, resume_data) # Note: Doesn't need resume_text | |
| if not interview_prep: | |
| return "The AI failed to generate the interview prep. Please try again." | |
| print("Interview prep pipeline complete! Returning 1 output.") | |
| return interview_prep | |
| # --- FINAL Gradio Web Interface (with 3 Tabs + Custom UI) --- | |
| # We use gr.themes.Base() as our starting point and apply our CSS | |
| with gr.Blocks(theme=gr.themes.Base(), css=custom_css) as demo: | |
| gr.Markdown( | |
| """ | |
| <div style="text-align: center; padding: 20px 0;"> | |
| <h1 style="font-size: 2.5em; font-weight: 700; color: #ffffff;"> | |
| 🤖 AI-Powered Job Application Suite | |
| </h1> | |
| <p style="font-size: 1.2em; color: #a0a0a0;"> | |
| From Resume to Interview, get the AI-powered edge you need. | |
| </p> | |
| </div> | |
| """ | |
| ) | |
| with gr.Tabs(): | |
| # --- Tab 1: Resume Tailor --- | |
| with gr.TabItem("1. Resume Tailor"): | |
| gr.Markdown("Upload your resume and paste a job description to get a tailored 'Experience' section and an ATS report.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| resume_file_input_1 = gr.File(label="Upload Your Master Resume (.pdf or .txt)") | |
| job_description_input_1 = gr.Textbox(lines=15, label="Paste the Job Description Here") | |
| submit_button_1 = gr.Button("Tailor My Resume!", variant="primary") | |
| with gr.Column(scale=2): | |
| tailored_resume_output = gr.Markdown(label="Your New, Tailored Resume Section") | |
| ats_report_output = gr.Markdown(label="Your 'Tailor' Report") | |
| submit_button_1.click( | |
| fn=tailor_resume_pipeline, | |
| inputs=[resume_file_input_1, job_description_input_1], | |
| outputs=[tailored_resume_output, ats_report_output] | |
| ) | |
| # --- Tab 2: Cover Letter Generator --- | |
| with gr.TabItem("2. Cover Letter Generator"): | |
| gr.Markdown("Upload your resume and paste a job description to generate a custom cover letter in seconds.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| resume_file_input_2 = gr.File(label="Upload Your Master Resume (.pdf or .txt)") | |
| job_description_input_2 = gr.Textbox(lines=15, label="Paste the Job Description Here") | |
| submit_button_2 = gr.Button("Generate My Cover Letter!", variant="primary") | |
| with gr.Column(scale=2): | |
| cover_letter_output = gr.Markdown(label="Your New, Generated Cover Letter") | |
| submit_button_2.click( | |
| fn=generate_cover_letter_pipeline, | |
| inputs=[resume_file_input_2, job_description_input_2], | |
| outputs=[cover_letter_output] | |
| ) | |
| # --- Tab 3: Interview Prep --- | |
| with gr.TabItem("3. Interview Prep"): | |
| gr.Markdown("Upload your resume and paste a job description to get a custom list of interview questions and answer hints.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| resume_file_input_3 = gr.File(label="Upload Your Master Resume (.pdf or .txt)") | |
| job_description_input_3 = gr.Textbox(lines=15, label="Paste the Job Description Here") | |
| submit_button_3 = gr.Button("Generate My Prep Sheet!", variant="primary") | |
| with gr.Column(scale=2): | |
| interview_prep_output = gr.Markdown(label="Your Custom Interview Prep Sheet") | |
| submit_button_3.click( | |
| fn=generate_interview_prep_pipeline, | |
| inputs=[resume_file_input_3, job_description_input_3], | |
| outputs=[interview_prep_output] | |
| ) | |
| print("Launching the AI Job Application Suite (v6.0 DARK MODE)...") | |
| # Added share=True so you can easily open it on your phone or share it | |
| demo.launch(share=True) | |