import os import docx2txt import docx from docx import Document import openai from dotenv import load_dotenv import json import tempfile import re # Add these imports for PDF support import PyPDF2 import io # Load environment variables load_dotenv() # Set up OpenAI API key openai.api_key = os.getenv("OPENAI_API_KEY") # Default model DEFAULT_MODEL = "gpt-4o" current_model = DEFAULT_MODEL # Function to set OpenAI API key def set_openai_api_key(api_key=None): """ Set the OpenAI API key from the provided key or environment variable. Args: api_key: Optional API key to use. If None, will use the environment variable. Returns: bool: True if API key is set, False otherwise """ if api_key: openai.api_key = api_key return True else: env_api_key = os.getenv("OPENAI_API_KEY") if env_api_key: openai.api_key = env_api_key return True return False # Function to set OpenAI model def set_openai_model(model_name="gpt-4o"): """ Set the OpenAI model to use for API calls. Args: model_name: Name of the model to use (e.g., "gpt-4o", "gpt-4o-mini") Returns: str: The name of the model that was set """ global current_model current_model = model_name return current_model # Set up OpenAI API key from environment variable initially set_openai_api_key() def extract_text_from_document(file_path): """ Extract text from a document file (DOCX or PDF). Args: file_path: Path to the document file Returns: str: Extracted text from the document """ try: # Check file extension if file_path.lower().endswith('.docx'): # Extract text from DOCX return docx2txt.process(file_path) elif file_path.lower().endswith('.pdf'): # Extract text from PDF text = "" with open(file_path, 'rb') as file: pdf_reader = PyPDF2.PdfReader(file) for page_num in range(len(pdf_reader.pages)): page = pdf_reader.pages[page_num] text += page.extract_text() + "\n\n" return text else: raise ValueError(f"Unsupported file format: {os.path.splitext(file_path)[1]}") except Exception as e: print(f"Error extracting text from document: {e}") return None def parse_resume(file_path): """ Parse a resume file and extract its content. Args: file_path: Path to the resume file Returns: str: Extracted text from the resume """ try: return extract_text_from_document(file_path) except Exception as e: print(f"Error parsing resume: {e}") return None def analyze_resume_job_match(resume_text, job_description, creativity_level=30): """ Analyze the match between a resume and job description using GPT-4o. Args: resume_text: Raw text of the resume job_description: Job description text creativity_level: Level of creativity/modification allowed (0-100) Returns: dict: Analysis results including match percentage, gaps, and suggestions """ try: print(f"Analyzing resume match with creativity level: {creativity_level}") print(f"Resume text length: {len(resume_text)}") print(f"Job description length: {len(job_description)}") # Adjust system message based on creativity level if creativity_level < 20: system_message = "You are a conservative resume analyzer. Focus only on exact matches between the resume and job description. Be strict in your evaluation." elif creativity_level < 50: system_message = "You are a balanced resume analyzer. Evaluate the resume against the job description with a moderate level of flexibility, recognizing transferable skills." elif creativity_level < 80: system_message = "You are a creative resume analyzer. Be generous in your evaluation, recognizing potential and transferable skills even when not explicitly stated." else: system_message = "You are an optimistic resume analyzer. Focus on potential rather than exact matches. Be very generous in your evaluation and provide ambitious suggestions for improvement." prompt = f""" Analyze the following resume and job description: RESUME: {resume_text} JOB DESCRIPTION: {job_description} CREATIVITY LEVEL: {creativity_level}% (where 0% means strictly factual and 100% means highly creative) Provide a detailed analysis in JSON format with the following structure: 1. "match_percentage": A numerical percentage (0-100) representing how well the resume matches the job description. Use job skills keyword used in job description to match with the contents of the resume to come up with the match percentage. 2. "key_matches": List of skills and experiences in the resume that match the job requirements. 3. "gaps": List of skills or experiences mentioned in the job description that are missing from the resume. 4. "suggestions": Specific suggestions to improve the resume for this job. Ensure the suggestions are based on the job description and the resume and contain the exact keywords from the job description. 5. "summary": A brief summary of the overall match and main recommendations. 6. "skill_breakdown": An object containing categories of skills from the job description and how well the candidate matches each category: - "technical_skills": Assessment of technical skills match (percentage and comments) - "experience": Assessment of experience requirements match (percentage and comments) - "education": Assessment of education requirements match (percentage and comments) - "soft_skills": Assessment of soft skills/leadership match (percentage and comments) Adjust your analysis based on the creativity level. Higher creativity means being more generous with matches and more ambitious with suggestions. IMPORTANT: For each resume, provide a unique and accurate match percentage based on the actual content. Do not use the same percentage for different resumes. The excellent match resume should have a high percentage (80-95%), good match should be moderate-high (65-80%), average match should be moderate (40-65%), and poor match should be low (below 40%). Return ONLY the JSON object without any additional text. """ response = openai.chat.completions.create( model=current_model, messages=[ {"role": "system", "content": system_message}, {"role": "user", "content": prompt} ], response_format={"type": "json_object"} ) analysis = json.loads(response.choices[0].message.content) print(f"Analysis complete. Match percentage: {analysis.get('match_percentage', 0)}%") return analysis except Exception as e: print(f"Error analyzing resume: {e}") return { "match_percentage": 0, "key_matches": [], "gaps": ["Error analyzing resume"], "suggestions": ["Please try again"], "summary": f"Error: {str(e)}", "skill_breakdown": { "technical_skills": {"percentage": 0, "comments": "Error analyzing resume"}, "experience": {"percentage": 0, "comments": "Error analyzing resume"}, "education": {"percentage": 0, "comments": "Error analyzing resume"}, "soft_skills": {"percentage": 0, "comments": "Error analyzing resume"} } } def tailor_resume(resume_text, job_description, template_path=None, creativity_level=30, verbosity="elaborate"): """ Generate a tailored resume based on the job description using GPT-4o. Args: resume_text: Raw text of the original resume job_description: Job description text template_path: Optional path to a resume template creativity_level: Level of creativity/modification allowed (0-100) verbosity: Level of detail in the resume ('concise' or 'elaborate') Returns: str: Tailored resume content """ try: # If a template is provided, read its structure template_structure = "" template_sections = [] if template_path: # Extract the template structure and sections doc = Document(template_path) for para in doc.paragraphs: if para.text.strip(): template_structure += para.text + "\n" # Identify section headings (usually in all caps or with specific styles) if para.style.name.startswith('Heading') or para.text.isupper() or (para.runs and para.runs[0].bold): template_sections.append(para.text.strip()) # Adjust system message based on creativity level if creativity_level < 20: system_message = "You are a conservative resume editor. Only reorganize existing content to better match the job description. Do not add any new experiences or skills that aren't explicitly mentioned in the original resume." elif creativity_level < 50: system_message = "You are a balanced resume editor. Enhance existing content with better wording and highlight relevant skills. Make minor improvements but keep all content factual and based on the original resume." elif creativity_level < 80: system_message = "You are a creative resume editor. Significantly enhance the resume with improved wording and may suggest minor additions or extensions of existing experiences to better match the job description." else: system_message = "You are an aggressive resume optimizer. Optimize the resume to perfectly match the job description, including suggesting new skills and experiences that would make the candidate more competitive, while maintaining some connection to their actual background. Ensure exact keywords are included from the job description in the new resume. " # Adjust system message based on creativity level and verbosity if creativity_level < 20: base_message = "You are a conservative resume editor. Only reorganize existing content to better match the job description. Do not add any new experiences or skills that aren't explicitly mentioned in the original resume." elif creativity_level < 50: base_message = "You are a balanced resume editor. Enhance existing content with better wording and highlight relevant skills. Make minor improvements but keep all content factual and based on the original resume." elif creativity_level < 80: base_message = "You are a creative resume editor. Significantly enhance the resume with improved wording and may suggest minor additions or extensions of existing experiences to better match the job description." else: base_message = "You are an aggressive resume optimizer. Optimize the resume to perfectly match the job description, including suggesting new skills and experiences that would make the candidate more competitive, while maintaining some connection to their actual background. Ensure exact keywords are included from the job description in the new resume." # Add verbosity instructions to the system message if verbosity == "concise": system_message = base_message + " Create a concise resume with brief bullet points, focusing only on the most relevant information. Aim for a shorter resume that can be quickly scanned by recruiters." else: # elaborate system_message = base_message + " Create a detailed resume that thoroughly explains experiences and skills, providing context and specific achievements. Use comprehensive bullet points to showcase the candidate's qualifications." prompt = f""" Create a tailored version of this resume to better match the job description: ORIGINAL RESUME: {resume_text} JOB DESCRIPTION: {job_description} {("TEMPLATE STRUCTURE TO FOLLOW:" + chr(10) + template_structure) if template_path else ""} {("TEMPLATE SECTIONS TO INCLUDE:" + chr(10) + chr(10).join(template_sections)) if template_sections else ""} CREATIVITY LEVEL: {creativity_level}% (where 0% means strictly factual and 100% means highly creative) VERBOSITY: {verbosity.upper()} (CONCISE means brief and to-the-point, ELABORATE means detailed and comprehensive) Create a tailored resume that: 1. Highlights relevant skills and experiences that match the job description 2. Uses keywords from the job description 3. Quantifies achievements where possible 4. Removes or downplays irrelevant information 5. Adjusts content based on the specified creativity level IMPORTANT FORMATTING INSTRUCTIONS: - Format your response with clear section headings in ALL CAPS - Use bullet points (•) for listing items and achievements - If a template is provided, follow its exact section structure and organization - Maintain the same section headings as in the template when possible - For each section, provide content that matches the requested verbosity level: * CONCISE: Use 1-2 line bullet points, focus only on the most relevant achievements * ELABORATE: Use detailed bullet points with context and specific metrics Return the complete tailored resume content in a professional format. """ response = openai.chat.completions.create( model=current_model, messages=[ {"role": "system", "content": system_message}, {"role": "user", "content": prompt} ] ) tailored_resume = response.choices[0].message.content return tailored_resume except Exception as e: print(f"Error tailoring resume: {e}") return f"Error tailoring resume: {str(e)}" def create_word_document(content, output_path, template_path=None): """ Create a Word document with the given content, optionally using a template. Args: content: Text content for the document output_path: Path to save the document template_path: Optional path to a template document to use as a base Returns: bool: Success status """ try: # If a template is provided, use it as the base document if template_path and os.path.exists(template_path): # Create a new document instead of modifying the template directly doc = Document() original_template = Document(template_path) # Copy all styles from the template to the new document for style in original_template.styles: if style.name not in doc.styles: try: doc.styles.add_style(style.name, style.type) except: pass # Style might already exist or be built-in # Copy document properties and sections settings # We'll only add sections as needed, not at the beginning if len(original_template.sections) > 0: # Copy properties from the first section section = original_template.sections[0] # Use the existing first section in the new document new_section = doc.sections[0] new_section.page_height = section.page_height new_section.page_width = section.page_width new_section.left_margin = section.left_margin new_section.right_margin = section.right_margin new_section.top_margin = section.top_margin new_section.bottom_margin = section.bottom_margin new_section.header_distance = section.header_distance new_section.footer_distance = section.footer_distance # Copy additional sections if needed for i in range(1, len(original_template.sections)): section = original_template.sections[i] new_section = doc.add_section() new_section.page_height = section.page_height new_section.page_width = section.page_width new_section.left_margin = section.left_margin new_section.right_margin = section.right_margin new_section.top_margin = section.top_margin new_section.bottom_margin = section.bottom_margin new_section.header_distance = section.header_distance new_section.footer_distance = section.footer_distance # Copy complex elements like headers and footers copy_template_complex_elements(original_template, doc) # Split content by sections (using headings as delimiters) sections = [] current_section = [] lines = content.split('\n') for line in lines: line = line.strip() if not line: continue # Check if this is a heading (all caps or ends with a colon) if line.isupper() or (line.endswith(':') and len(line) < 50): if current_section: sections.append(current_section) current_section = [line] else: current_section.append(line) if current_section: sections.append(current_section) # Find template headings to match with our content sections template_headings = [] template_heading_styles = {} for para in original_template.paragraphs: if para.style.name.startswith('Heading') or para.text.isupper() or (para.runs and para.runs[0].bold): template_headings.append(para.text.strip()) template_heading_styles[para.text.strip()] = para.style.name # Add content to the document with appropriate formatting for section in sections: if not section: continue # First line of each section is treated as a heading heading = section[0] # Try to find a matching heading style from the template heading_style = 'Heading 1' # Default for template_heading in template_headings: if template_heading.upper() == heading.upper() or template_heading.upper() in heading.upper() or heading.upper() in template_heading.upper(): heading_style = template_heading_styles.get(template_heading, 'Heading 1') break # Add the heading with the appropriate style p = doc.add_paragraph() try: p.style = heading_style except: p.style = 'Heading 1' # Fallback run = p.add_run(heading) run.bold = True # Add the rest of the section content for line in section[1:]: if line.startswith('•') or line.startswith('-') or line.startswith('*'): # This is a bullet point p = doc.add_paragraph(line[1:].strip(), style='List Bullet') else: p = doc.add_paragraph(line) else: # Create a new document with basic formatting doc = Document() # Split content by lines and add to document with basic formatting paragraphs = content.split('\n') for para in paragraphs: para = para.strip() if not para: continue # Check if this is a heading (all caps or ends with a colon) if para.isupper() or (para.endswith(':') and len(para) < 50): p = doc.add_paragraph() p.style = 'Heading 1' run = p.add_run(para) run.bold = True elif para.startswith('•') or para.startswith('-') or para.startswith('*'): # This is a bullet point p = doc.add_paragraph(para[1:].strip(), style='List Bullet') else: doc.add_paragraph(para) doc.save(output_path) return True except Exception as e: print(f"Error creating Word document: {e}") return False def get_available_templates(): """ Get a list of available resume templates from the current working directory. Returns: list: List of template file paths """ # First, copy any templates from templates directory if they don't exist #copy_templates_to_current_directory() templates = [] # Check current directory for templates for file in os.listdir("."): if file.endswith("_Template.docx") or file.endswith("Template.docx"): templates.append(file) print(f"Found templates in current directory: {templates}") return templates def copy_templates_to_current_directory(): """ Copy templates from the templates directory to the current working directory if they don't already exist. Returns: list: List of copied template file paths """ copied_templates = [] templates_dir = "templates" # Check if templates directory exists if not os.path.exists(templates_dir): print(f"Templates directory '{templates_dir}' not found.") return copied_templates # Get list of template files in templates directory template_files = [f for f in os.listdir(templates_dir) if f.endswith(".docx")] # Copy each template file to current directory if it doesn't exist for template_file in template_files: source_path = os.path.join(templates_dir, template_file) dest_path = template_file if not os.path.exists(dest_path): try: import shutil shutil.copy2(source_path, dest_path) copied_templates.append(dest_path) print(f"Copied template '{template_file}' to current directory.") except Exception as e: print(f"Error copying template '{template_file}': {e}") else: print(f"Template '{template_file}' already exists in current directory.") return copied_templates def copy_template_complex_elements(source_doc, target_doc): """ Copy complex elements like headers and footers from source document to target document. Args: source_doc: Source Document object target_doc: Target Document object """ try: # Copy headers and footers for i, section in enumerate(target_doc.sections): # Skip if source doesn't have this many sections if i >= len(source_doc.sections): break # Copy header if section.header.is_linked_to_previous == False: # Check if there's at least one paragraph in the header if len(section.header.paragraphs) == 0: section.header.add_paragraph() # Copy text and style from source header paragraphs for j, para in enumerate(source_doc.sections[i].header.paragraphs): if j < len(section.header.paragraphs): section.header.paragraphs[j].text = para.text try: section.header.paragraphs[j].style = para.style except Exception: pass # Style might not be compatible else: new_para = section.header.add_paragraph(para.text) try: new_para.style = para.style except Exception: pass # Copy footer if section.footer.is_linked_to_previous == False: # Check if there's at least one paragraph in the footer if len(section.footer.paragraphs) == 0: section.footer.add_paragraph() # Copy text and style from source footer paragraphs for j, para in enumerate(source_doc.sections[i].footer.paragraphs): if j < len(section.footer.paragraphs): section.footer.paragraphs[j].text = para.text try: section.footer.paragraphs[j].style = para.style except Exception: pass # Style might not be compatible else: new_para = section.footer.add_paragraph(para.text) try: new_para.style = para.style except Exception: pass except Exception as e: print(f"Error copying complex elements: {e}") def create_tailored_resume_from_template(content, template_path, output_path): """ Create a tailored resume by directly modifying a template document. This preserves all formatting, tables, and styles from the original template. Args: content: Structured content for the resume (text) template_path: Path to the template document output_path: Path to save the output document Returns: bool: Success status """ try: if not os.path.exists(template_path): return False # Create a copy of the template doc = Document(template_path) # Parse the content into sections sections = {} current_section = None current_content = [] for line in content.split('\n'): line = line.strip() if not line: continue # Check if this is a heading (all caps or ends with a colon) if line.isupper() or (line.endswith(':') and len(line) < 50): # Save the previous section if current_section and current_content: sections[current_section] = current_content # Start a new section current_section = line current_content = [] else: if current_section: current_content.append(line) # Save the last section if current_section and current_content: sections[current_section] = current_content # Find all paragraphs in the template that are headings or potential section markers template_sections = {} for i, para in enumerate(doc.paragraphs): if para.style.name.startswith('Heading') or para.text.isupper() or para.runs and para.runs[0].bold: template_sections[para.text.strip()] = i # Create a new document to avoid duplicate content new_doc = Document() # Copy all styles from the template to the new document for style in doc.styles: if style.name not in new_doc.styles: try: new_doc.styles.add_style(style.name, style.type) except: pass # Style might already exist or be built-in # Copy document properties and sections settings # We'll only add sections as needed, not at the beginning if len(doc.sections) > 0: # Copy properties from the first section section = doc.sections[0] # Use the existing first section in the new document new_section = new_doc.sections[0] new_section.page_height = section.page_height new_section.page_width = section.page_width new_section.left_margin = section.left_margin new_section.right_margin = section.right_margin new_section.top_margin = section.top_margin new_section.bottom_margin = section.bottom_margin new_section.header_distance = section.header_distance new_section.footer_distance = section.footer_distance # Copy additional sections if needed for i in range(1, len(doc.sections)): section = doc.sections[i] new_section = new_doc.add_section() new_section.page_height = section.page_height new_section.page_width = section.page_width new_section.left_margin = section.left_margin new_section.right_margin = section.right_margin new_section.top_margin = section.top_margin new_section.bottom_margin = section.bottom_margin new_section.header_distance = section.header_distance new_section.footer_distance = section.footer_distance # Copy complex elements like headers and footers copy_template_complex_elements(doc, new_doc) # Replace content in the template with our tailored content # First, create a mapping between our sections and template sections section_mapping = {} for our_section in sections.keys(): best_match = None best_score = 0 for template_section in template_sections.keys(): # Calculate similarity between section headings if template_section.upper() == our_section.upper(): # Exact match best_match = template_section break elif template_section.upper() in our_section.upper() or our_section.upper() in template_section.upper(): # Partial match score = len(set(template_section.upper()) & set(our_section.upper())) / max(len(template_section), len(our_section)) if score > best_score: best_score = score best_match = template_section if best_match and best_score > 0.5: section_mapping[our_section] = best_match # Add content to the new document based on the template structure for our_section, content_lines in sections.items(): # Add section heading p = new_doc.add_paragraph() # Try to find a matching heading style from the template if our_section in section_mapping: template_section = section_mapping[our_section] section_index = template_sections[template_section] template_para = doc.paragraphs[section_index] try: p.style = template_para.style.name except: p.style = 'Heading 1' # Fallback # Copy formatting from template paragraph for run in template_para.runs: if run.text.strip(): p_run = p.add_run(our_section) p_run.bold = run.bold p_run.italic = run.italic p_run.underline = run.underline if run.font.name: p_run.font.name = run.font.name if run.font.size: p_run.font.size = run.font.size if run.font.color.rgb: p_run.font.color.rgb = run.font.color.rgb break else: # If no runs with text, add a default run p_run = p.add_run(our_section) p_run.bold = True else: # No matching template section, use default formatting p.style = 'Heading 1' p_run = p.add_run(our_section) p_run.bold = True # Add section content for line in content_lines: if line.startswith('•') or line.startswith('-') or line.startswith('*'): # This is a bullet point p = new_doc.add_paragraph(line[1:].strip(), style='List Bullet') else: p = new_doc.add_paragraph(line) new_doc.save(output_path) return True except Exception as e: print(f"Error creating tailored resume from template: {e}") return False def convert_text_to_word(text_file_path, output_docx_path): """ Convert a text file to a Word document. Args: text_file_path: Path to the text file output_docx_path: Path where the Word document will be saved Returns: bool: True if successful, False otherwise """ try: # Read the text file with open(text_file_path, 'r', encoding='utf-8') as file: content = file.read() # Create a new Word document doc = Document() # Split the content by double newlines to identify paragraphs paragraphs = content.split('\n\n') # Process each paragraph for i, para_text in enumerate(paragraphs): # Skip empty paragraphs if not para_text.strip(): continue # Check if this looks like a heading (all caps or ends with a colon) is_heading = para_text.isupper() or para_text.strip().endswith(':') # Add the paragraph to the document paragraph = doc.add_paragraph(para_text.strip()) # Apply formatting based on position and content if i == 0: # First paragraph is likely the name paragraph.style = 'Title' elif is_heading: paragraph.style = 'Heading 2' else: paragraph.style = 'Normal' # Save the document doc.save(output_docx_path) return True except Exception as e: print(f"Error converting text to Word: {e}") return False def convert_sample_resumes(): """ Convert all sample text resumes to Word documents. Returns: list: Paths to the created Word documents """ sample_files = [ "excellent_match_resume.docx.txt", "good_match_resume.docx.txt", "average_match_resume.docx.txt", "poor_match_resume.docx.txt" ] created_files = [] for text_file in sample_files: if os.path.exists(text_file): output_path = text_file.replace('.docx.txt', '.docx') print(f"Converting {text_file} to {output_path}...") if convert_text_to_word(text_file, output_path): created_files.append(output_path) print(f"Successfully created {output_path}") else: print(f"Failed to create {output_path}") else: print(f"Sample file not found: {text_file}") return created_files