""" Resume Customizer Application. This application customizes resumes based on job descriptions. """ import os import gradio as gr import subprocess from openai import OpenAI import dotenv # Load environment variables dotenv.load_dotenv() # Constants DEFAULT_TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates", "Arbab - Resume.tex") OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs") DEFAULT_JOBS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "jobs-applied-for.txt") # Ensure output directory exists os.makedirs(OUTPUT_DIR, exist_ok=True) def get_openai_client(api_key=None): """Get the OpenAI client with the provided API key or from environment variables.""" if api_key: return OpenAI(api_key=api_key) api_key = os.environ.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("No OpenAI API key provided") return OpenAI(api_key=api_key) def customize_resume_with_llm(job_description, api_key=None): """Customize the entire resume based on the job description using OpenAI's LLM.""" client = get_openai_client(api_key) # Read the original resume with open(DEFAULT_TEMPLATE_PATH, "r") as f: resume_content = f.read() # Find the preamble end and content beginning begin_document_index = resume_content.find("\\begin{document}") if begin_document_index == -1: raise ValueError("Could not find \\begin{document} in the resume template") # Split the resume into preamble and content preamble = resume_content[:begin_document_index + len("\\begin{document}")] content = resume_content[begin_document_index + len("\\begin{document}"):] # Find the end of the document end_document_index = content.find("\\end{document}") if end_document_index == -1: raise ValueError("Could not find \\end{document} in the resume template") # Extract just the content (without \end{document}) content = content[:end_document_index].strip() # Create the prompt for OpenAI prompt = f""" You will rewrite a resume to tailor it for a specific job description. Job Description: {job_description} Original Resume Content: {content} Instructions: 1. Keep the SAME STRUCTURE and SECTION HEADINGS as the original resume. 2. Add a "Why Consider me for [mention Job Title or Company Name if could be inferred from the providedjob description]" section at the beginning, after the header/contact info but before Experience. Maximum 4 lines. Write genuinely and not like a robot/AI. Be subtly grateful but only genuinely. 3. Customize/reword the Technical Skills, Experience, Projects, and other sections to emphasize relevant skills and experience. 4. Keep the same formatting, but highlight achievements that match the job description. 5. Do not invent new experiences or skills not in the original resume. 6. Maintain all section markers (like %-----------EXPERIENCE-----------------). 7. Return ONLY THE CONTENT between \begin{{document}} and \end{{document}} (do not include these tags). 8. Keep the header with name and contact information exactly the same. whatever is related to the job description that you think should really catch the eye of the recruiter or the person who is going to look at the updated resume that is the hiring person , You should underline it in the LaTeX format (at a time dont underline more than 5 words, and total underline instances in overall resume should be less than 3, thats all. not more). Remember not to overdo it and specifically do it with the terms which are exact match from the job description. Basically we are preparing this resume for the person who will be hiring. So the final should be crisp and it should not be too divergent from the original resume and do not include additional information which does not already exist in my resume. So do not make stuff up. But you can make reasonable adjustments, for example if the job description mentions generative ai and my resume mentions large language models then you can also say generative ai in my resume. this is just one of the examples but keep it on the conservative side. And do not include the name of the company in the why consider me section beecause this will make it it look like this is written by an AI. So, the reader instead should think that its organically written by me and I am meant for this job. And you can give headline of 'Summary' to this section. This could be imperfect but should look out as genuine. If the role is more applied and industry focused and does not mention about publications, you can skip publications and instead focus in why hire me section that i have multiple industry internships And most of my PhD projects have applied applications and give short and concise and crucial justification for that (you can remove the publications section). But if the role is more research focused you can focus on the research publication and mention that I have published in NeurIPS and WACV and keep the final publication section as well . Actually, keep all the projects and only update the phrasing if need be. Be strategic about where to reduce text from. Do not just text a little little from all the places. Instead, be strategic and cut brutally from some places which are not contributing as much to the final resume for this job. Keep most of the things. FORMAT YOUR RESPONSE AS CLEAN LATEX CODE WITH NO EXPLANATIONS OR CODE BLOCKS. Keep all the projects in the projects section, do not remove any of them. Keeping all the above constraints in mind, the goal is to optimize for Applicant Tracking Systems (ATS). """ try: response = client.responses.create( model="gpt-5.2-chat-latest", reasoning={"effort": "medium"}, instructions="You are a professional resume writer who tailors resumes for job applications, while strictly maintaining the original LaTeX formatting and structure.", input=prompt ) # Extract the generated content customized_content = response.output_text.strip() print(customized_content) # Clean any code block markers customized_content = customized_content.replace("```latex", "").replace("```", "").strip() # Reconstruct the full LaTeX document customized_resume = f"{preamble}\n\n{customized_content}\n\n\\end{{document}}" return customized_resume except Exception as e: print(f"Error customizing resume: {str(e)}") # Return error message instead of original content raise ValueError(f"Error occurred: {str(e)}") def find_relevant_jobs(api_key, job_description, jobs_list=None): """Find the most relevant jobs from a list based on the job description.""" if not api_key.strip(): return "Please provide an OpenAI API key" try: client = get_openai_client(api_key) # Use default jobs list if none provided if not jobs_list or jobs_list.strip() == "": try: with open(DEFAULT_JOBS_PATH, "r") as f: jobs_list = f.read() except Exception as e: return f"Error reading default jobs list: {str(e)}" # LinkedIn jobs link to include in the search linkedin_jobs_link = "https://www.linkedin.com/jobs/search/?currentJobId=4212868961&f_E=2%2C3%2C4&f_TPR=r86400&geoId=102095887&keywords=(machine%20learning)%20OR%20(software%20engineer)&origin=JOB_SEARCH_PAGE_JOB_FILTER&refresh=true&sortBy=R&spellCorrectionEnabled=true" # Create the prompt for OpenAI prompt = f""" Given the following job description and a list of job positions, identify the top 5 most relevant jobs from the list. Job Description: {job_description} Available Jobs List: {jobs_list} Additionally, consider checking recent job postings from LinkedIn: {linkedin_jobs_link} Return only the top 5 most relevant jobs in this format: 1. [Company Name] - [Position Title] (Relevance Score: X/10) 2. [Company Name] - [Position Title] (Relevance Score: X/10) 3. [Company Name] - [Position Title] (Relevance Score: X/10) 4. [Company Name] - [Position Title] (Relevance Score: X/10) 5. [Company Name] - [Position Title] (Relevance Score: X/10) For each job, briefly explain in 1-2 sentences why it's relevant to the job description. """ response = client.responses.create( model="gpt-5.2-pro", instructions="You are a job matching assistant that identifies the most relevant jobs based on a job description.", input=prompt ) # Return the response return response.output_text.strip() except Exception as e: return f"Error finding relevant jobs: {str(e)}" def create_customized_resume(api_key, job_description): """Create a customized resume based on the job description.""" if not api_key.strip(): return None, "Please provide an OpenAI API key" try: # Generate customized resume content customized_resume = customize_resume_with_llm(job_description, api_key) # Save to file output_tex_path = os.path.join(OUTPUT_DIR, "customized_resume.tex") with open(output_tex_path, "w") as f: f.write(customized_resume) # Convert to PDF pdf_path = convert_to_pdf(output_tex_path) if pdf_path: return pdf_path, "Resume successfully customized! Click to download." else: return None, "Failed to generate PDF. Check the logs for details." except Exception as e: return None, f"Error: {str(e)}" def convert_to_pdf(tex_path): """Convert a LaTeX file to PDF using pdflatex.""" tex_dir = os.path.dirname(tex_path) tex_filename = os.path.basename(tex_path) original_dir = os.getcwd() os.chdir(tex_dir) try: # Run pdflatex twice for i in range(2): result = subprocess.run( ['pdflatex', '-interaction=nonstopmode', tex_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode != 0: print(f"pdflatex error: {result.stderr}") # Check output pdf_path = os.path.join(tex_dir, tex_filename.replace('.tex', '.pdf')) return os.path.abspath(pdf_path) if os.path.exists(pdf_path) else None except Exception as e: print(f"PDF conversion error: {str(e)}") return None finally: os.chdir(original_dir) # Define the Gradio interface def create_interface(): with gr.Blocks(title="Resume Customizer") as app: with gr.Tabs(): with gr.Tab("Resume Customizer"): gr.Markdown("# Resume Customizer") gr.Markdown("Enter a job description to generate a fully customized resume tailored to the position.") with gr.Row(): with gr.Column(): api_key_resume = gr.Textbox( label="OpenAI API Key", placeholder="Enter your OpenAI API key", type="password" ) job_description_resume = gr.Textbox( label="Job Description", placeholder="Paste the job description here...", lines=10 ) customize_btn = gr.Button("Customize Resume") with gr.Column(): pdf_output = gr.File(label="Download Resume") status_text = gr.Textbox(label="Status", interactive=False) customize_btn.click( fn=create_customized_resume, inputs=[api_key_resume, job_description_resume], outputs=[pdf_output, status_text] ) gr.Markdown(""" ## How to Use 1. Enter your OpenAI API key 2. Paste a job description in the text area 3. Click "Customize Resume" 4. Wait for the AI to tailor your entire resume to match the job requirements 5. Download the customized resume PDF """) with gr.Tab("Job Finder"): gr.Markdown("# Job Finder") gr.Markdown("Enter a job description to find the top 5 most relevant jobs from your list and LinkedIn recommendations.") with gr.Row(): with gr.Column(): api_key_job = gr.Textbox( label="OpenAI API Key", placeholder="Enter your OpenAI API key", type="password" ) job_description_job = gr.Textbox( label="Job Description", placeholder="Paste the job description here...", lines=10 ) jobs_list = gr.Textbox( label="Jobs List (Optional)", placeholder="Paste your list of jobs here or leave empty to use the default list...", lines=10 ) find_jobs_btn = gr.Button("Find Relevant Jobs") with gr.Column(): relevant_jobs_output = gr.Textbox( label="Top 5 Most Relevant Jobs", interactive=False, lines=15 ) find_jobs_btn.click( fn=find_relevant_jobs, inputs=[api_key_job, job_description_job, jobs_list], outputs=relevant_jobs_output ) gr.Markdown(""" ## How to Use 1. Enter your OpenAI API key 2. Paste a job description in the text area 3. Optionally, paste your own list of jobs (or leave empty to use the default list) 4. Click "Find Relevant Jobs" 5. View the top 5 most relevant jobs based on the job description (includes LinkedIn recommendations) """) return app # Create and launch the app app = create_interface() if __name__ == "__main__": app.launch()