pascalx/careerplus
0
1import os2import google.generativeai as genai3import google.ai.generativelanguage as glm # Added for Tool definition4from flask import Flask, render_template, request, jsonify, send_file5from dotenv import load_dotenv6import PyPDF2 # Added for PDF processing7import io # Added to handle file stream8import traceback # For detailed error logging9from weasyprint import HTML # Added for PDF generation10# Remove web scraping imports, no longer needed for search11# import requests 12# from bs4 import BeautifulSoup13from tavily import TavilyClient # Added for Tavily Search API14import json15import uuid16import re17import time18from datetime import datetime19 20# TODO: Add PDF processing library (e.g., PyPDF2 or pdfminer.six)21# TODO: Add Web scraping library (e.g., requests, beautifulsoup4) if needed for LinkedIn22 23load_dotenv() # Load environment variables from .env file24 25app = Flask(__name__)26app.config['SECRET_KEY'] = os.urandom(24) # For session management, flash messages etc.27app.config['UPLOAD_FOLDER'] = 'uploads' # Optional: Define a folder to save uploads28 29# --- Initialize Analytics Storage --- 30# Simple in-memory storage for analytics (would use a database in production)31analytics = {32 "requests": 0,33 "successful_analyses": 0,34 "errors": 0,35 "quota_errors": 0,36 "last_quota_error_time": None,37 "fallback_mode": False,38 "industries": {},39 "job_titles": {},40 "recent_searches": [] # Limited list of recent searches41}42 43# --- Tavily API Client Initialization --- 44tavily_api_key = os.getenv("TAVILY_API_KEY")45if not tavily_api_key:46 print("Warning: TAVILY_API_KEY not found in environment variables. Web search tool will not function.")47 tavily_client = None48else:49 try:50 tavily_client = TavilyClient(api_key=tavily_api_key)51 print("Tavily client initialized successfully.")52 except Exception as e:53 print(f"Error initializing Tavily client: {e}")54 tavily_client = None55 56# --- Tool Definitions & Implementations --- 57 58def perform_web_search(query: str):59 """Performs a web search using the Tavily API and returns a concise summary of results."""60 if not tavily_client:61 return "Error: Tavily API client is not configured. Cannot perform web search."62 63 print(f"--- Performing Tavily web search for: {query} ---")64 try:65 # Use Tavily's search method66 # Options: search_depth="advanced" for more in-depth results (consumes more credits)67 # include_answer=True to potentially get a direct answer summarized by Tavily68 response = tavily_client.search(query=query, search_depth="basic", max_results=5)69 70 # Extract and format results for the LLM71 # response['results'] is a list of dictionaries, each with 'title', 'url', 'content'72 if not response or 'results' not in response or not response['results']:73 print("Tavily search returned no results.")74 return "Web search returned no results."75 76 formatted_results = []77 for result in response['results']:78 formatted_results.append(f"Title: {result.get('title', 'N/A')}\nURL: {result.get('url', 'N/A')}\nSnippet: {result.get('content', 'N/A')}")79 80 summary = "\n\n".join(formatted_results)81 print(f"--- Tavily search summary: ---\n{summary[:300]}...\n------------------------")82 return summary83 84 except Exception as e:85 print(f"Error during Tavily API search: {e}")86 traceback.print_exc()87 # Attempt to provide a more specific error if possible88 error_message = str(e)89 if "API key" in error_message:90 return "Error performing web search: Invalid Tavily API key."91 # Add more specific error checks if needed based on Tavily's potential exceptions92 return f"Error performing web search: {error_message}"93 94def retrieve_company_info(company_name: str):95 """Retrieves company information to provide context for job application."""96 if not tavily_client:97 return "Error: Tavily API client is not configured. Cannot retrieve company information."98 99 # Handle case where company_name is a MapComposite object100 if hasattr(company_name, '__dict__'):101 try:102 # Try to extract the company name from the MapComposite object103 company_name = str(company_name)104 print(f"Converted MapComposite to string: {company_name}")105 except Exception as e:106 print(f"Error converting MapComposite to string: {e}")107 return "Error: Invalid company name format provided."108 109 # Ensure company_name is a string110 company_name = str(company_name).strip()111 if not company_name:112 return "Error: Empty company name provided."113 114 print(f"--- Retrieving information for company: {company_name} ---")115 try:116 # First search for company culture and values117 culture_query = f"{company_name} company culture values mission statement work environment employee experience"118 culture_response = tavily_client.search(119 query=culture_query,120 search_depth="advanced",121 max_results=3122 )123 124 # Second search for company's tech stack and innovation125 tech_query = f"{company_name} technology stack innovation products services development"126 tech_response = tavily_client.search(127 query=tech_query,128 search_depth="advanced",129 max_results=2130 )131 132 # Initialize company info sections133 company_info = f"# Company Analysis: {company_name}\n\n"134 135 # Process culture and values information136 if culture_response and 'results' in culture_response and culture_response['results']:137 company_info += "## Company Culture & Values\n\n"138 for result in culture_response['results']:139 # Filter out irrelevant or low-quality results140 content = result.get('content', '')141 if len(content) < 50: # Skip very short snippets142 continue143 144 # Clean and format the content145 content = content.replace('\n', ' ').strip()146 company_info += f"* {content}\n"147 company_info += f" Source: {result.get('url', 'N/A')}\n\n"148 149 # Process technology and innovation information150 if tech_response and 'results' in tech_response and tech_response['results']:151 company_info += "## Technology & Innovation\n\n"152 for result in tech_response['results']:153 # Filter out irrelevant or low-quality results154 content = result.get('content', '')155 if len(content) < 50: # Skip very short snippets156 continue157 158 # Clean and format the content159 content = content.replace('\n', ' ').strip()160 company_info += f"* {content}\n"161 company_info += f" Source: {result.get('url', 'N/A')}\n\n"162 163 # Add a note if no relevant information was found164 if len(company_info.split('\n')) <= 3: # Only header and no content165 company_info += "Note: Limited information available about the company. Consider checking their official website or LinkedIn page for more details.\n"166 167 return company_info168 169 except Exception as e:170 print(f"Error retrieving company information: {e}")171 traceback.print_exc()172 return f"Error retrieving company information: {str(e)}"173 174def analyze_linkedin_profile(linkedin_url: str):175 """Analyzes a LinkedIn profile URL to extract relevant information."""176 if not linkedin_url:177 return "Error: No LinkedIn URL provided for analysis."178 179 print(f"--- Analyzing LinkedIn profile: {linkedin_url} ---")180 try:181 # Use Tavily to search for information about the LinkedIn profile182 # Note: This is a workaround since we can't directly access LinkedIn's API183 search_query = f"LinkedIn profile information for {linkedin_url}"184 response = tavily_client.search(185 query=search_query,186 search_depth="basic",187 max_results=3188 )189 190 if not response or 'results' not in response or not response['results']:191 return f"Could not find detailed information about the LinkedIn profile at {linkedin_url}."192 193 # Format results194 profile_info = "LinkedIn Profile Analysis:\n\n"195 for result in response['results']:196 profile_info += f"Title: {result.get('title', 'N/A')}\n"197 profile_info += f"Source: {result.get('url', 'N/A')}\n"198 profile_info += f"Information: {result.get('content', 'N/A')}\n\n"199 200 return profile_info201 202 except Exception as e:203 print(f"Error analyzing LinkedIn profile: {e}")204 traceback.print_exc()205 return f"Error analyzing LinkedIn profile: {str(e)}"206 207def analyze_skill_relevance(skills: list, job_description: str, company_name: str):208 """Analyzes the relevance of skills to the job and company."""209 if not skills or not job_description:210 return "Error: Insufficient information for skill relevance analysis."211 212 print(f"--- Analyzing skill relevance for {company_name if company_name else 'the position'} ---")213 try:214 # Create a search query for skill relevance215 skills_str = ", ".join(skills[:5]) # Limit to first 5 skills to avoid too long queries216 search_query = f"skill relevance {skills_str} for {job_description[:50]} at {company_name if company_name else 'companies'}"217 218 response = tavily_client.search(219 query=search_query,220 search_depth="basic",221 max_results=3222 )223 224 if not response or 'results' not in response or not response['results']:225 return f"Could not find detailed information about skill relevance for the position."226 227 # Format results228 relevance_info = "Skill Relevance Analysis:\n\n"229 for result in response['results']:230 relevance_info += f"Title: {result.get('title', 'N/A')}\n"231 relevance_info += f"Source: {result.get('url', 'N/A')}\n"232 relevance_info += f"Information: {result.get('content', 'N/A')}\n\n"233 234 return relevance_info235 236 except Exception as e:237 print(f"Error analyzing skill relevance: {e}")238 traceback.print_exc()239 return f"Error analyzing skill relevance: {str(e)}"240 241# Define tools for the Gemini API242web_search_tool = glm.Tool(243 function_declarations=[244 glm.FunctionDeclaration(245 name='perform_web_search',246 description="Performs a web search using the Tavily API to find relevant, up-to-date information about companies, job roles, industries, or specific skills. Use this if the provided context (resume, job description) is insufficient or potentially outdated.",247 parameters=glm.Schema(248 type=glm.Type.OBJECT,249 properties={250 'query': glm.Schema(type=glm.Type.STRING, description="The specific search query.")251 },252 required=['query']253 )254 )255 ]256)257 258company_info_tool = glm.Tool(259 function_declarations=[260 glm.FunctionDeclaration(261 name='retrieve_company_info',262 description="Retrieves detailed information about a company including its culture, values, work environment, and more. Use this when you need to understand the company better to provide tailored advice for the job application.",263 parameters=glm.Schema(264 type=glm.Type.OBJECT,265 properties={266 'company_name': glm.Schema(type=glm.Type.STRING, description="The name of the company to research.")267 },268 required=['company_name']269 )270 )271 ]272)273 274linkedin_analysis_tool = glm.Tool(275 function_declarations=[276 glm.FunctionDeclaration(277 name='analyze_linkedin_profile',278 description="Analyzes a LinkedIn profile URL to extract relevant information about the candidate's experience, skills, and background. Use this to get additional context about the candidate beyond their resume.",279 parameters=glm.Schema(280 type=glm.Type.OBJECT,281 properties={282 'linkedin_url': glm.Schema(type=glm.Type.STRING, description="The LinkedIn profile URL to analyze.")283 },284 required=['linkedin_url']285 )286 )287 ]288)289 290skill_relevance_tool = glm.Tool(291 function_declarations=[292 glm.FunctionDeclaration(293 name='analyze_skill_relevance',294 description="Analyzes the relevance of specific skills to the job description and company. Use this to provide insights on which skills are most valuable and which might need improvement.",295 parameters=glm.Schema(296 type=glm.Type.OBJECT,297 properties={298 'skills': glm.Schema(299 type=glm.Type.ARRAY, 300 description="List of skills to analyze for relevance.",301 items=glm.Schema(type=glm.Type.STRING)302 ),303 'job_description': glm.Schema(type=glm.Type.STRING, description="The job description to compare skills against."),304 'company_name': glm.Schema(type=glm.Type.STRING, description="The name of the company to consider in the analysis.")305 },306 required=['skills', 'job_description']307 )308 )309 ]310)311 312# --- Gemini API Configuration --- 313 314try:315 gemini_api_key = os.getenv("GOOGLE_API_KEY")316 if not gemini_api_key:317 raise ValueError("GOOGLE_API_KEY not found in environment variables.")318 319 # Configure Gemini API320 genai.configure(api_key=gemini_api_key)321 322 # List available models for debugging323 print("--- Available Gemini Models ---")324 available_models = [m.name for m in genai.list_models()]325 print("\n".join(available_models))326 print("-----------------------------")327 328 # Try newer model name first, then fallback to original if needed329 model_name = "gemini-2.0-flash" # Try the newer model name format330 if f"models/{model_name}" not in available_models:331 model_name = "gemini-pro" # Fallback to original name332 if f"models/{model_name}" not in available_models:333 # Find any gemini model that supports generateContent334 gemini_models = [m for m in available_models if "gemini" in m.lower()]335 if gemini_models:336 model_name = gemini_models[0].replace("models/", "")337 print(f"Falling back to available Gemini model: {model_name}")338 else:339 raise ValueError("No suitable Gemini models found. Please check API access.")340 341 print(f"Using Gemini model: {model_name}")342 343 # Initialize the model344 model = genai.GenerativeModel(model_name)345 print("Gemini API configured successfully.")346except Exception as e:347 print(f"Error configuring Gemini API: {e}")348 traceback.print_exc()349 model = None350 # Ensure tools are not used if model fails351 web_search_tool = None352 company_info_tool = None353 linkedin_analysis_tool = None354 skill_relevance_tool = None355 356# Ensure upload folder exists if you plan to save files357# if not os.path.exists(app.config['UPLOAD_FOLDER']):358# os.makedirs(app.config['UPLOAD_FOLDER'])359 360# Helper function for PDF extraction361def extract_text_from_pdf(pdf_stream):362 """Extracts text from a PDF file stream."""363 try:364 pdf_reader = PyPDF2.PdfReader(pdf_stream)365 text = ""366 for page in pdf_reader.pages:367 page_text = page.extract_text()368 if page_text:369 text += page_text + "\n" # Add newline between pages370 if not text:371 print("Warning: PyPDF2 extracted no text from the PDF.")372 # Consider fallback or logging detailed info about the PDF373 return text.strip()374 except PyPDF2.errors.PdfReadError as e:375 print(f"Error reading PDF: {e}")376 raise ValueError("Invalid or corrupted PDF file.") from e377 except Exception as e:378 print(f"An unexpected error occurred during PDF parsing: {e}")379 traceback.print_exc()380 raise ValueError("Could not process PDF file.") from e381 382# --- Resume Keywords Extraction ---383def extract_resume_keywords(resume_text, job_description):384 """Extracts and counts important keywords from the resume that match the job description."""385 # Basic implementation - in production, would use more sophisticated NLP386 if not model:387 return {}388 389 try:390 # Use Gemini to extract keywords391 keyword_chat = model.start_chat()392 keyword_prompt = f"""393 Extract the top 10-15 most important keywords or skills from this resume, focusing on those that would be relevant for job applications. Order them by likely relevance to the job description.394 395 Resume:396 ```397 {resume_text[:3000]} # Limit length for API constraints398 ```399 400 Job Description:401 ```402 {job_description[:1000]}403 ```404 405 Return ONLY the keywords as a simple comma-separated list.406 """407 408 response = keyword_chat.send_message(keyword_prompt)409 keywords_text = response.text.strip()410 411 # Format into a list412 keywords = [kw.strip() for kw in re.split(r',|\n', keywords_text) if kw.strip()]413 414 return keywords415 except Exception as e:416 print(f"Error extracting resume keywords: {e}")417 return []418 419def get_fallback_analysis(resume_text, job_description, job_title, company_name):420 """Provides a basic analysis when the API quota is exceeded."""421 try:422 # Extract basic information from resume and job description423 resume_lines = resume_text.split('\n')424 job_lines = job_description.split('\n')425 426 # Basic keyword extraction with improved filtering427 resume_keywords = set()428 job_keywords = set()429 430 # Common words to filter out431 common_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'}432 433 # Extract keywords from resume (improved approach)434 for line in resume_lines:435 # Split on common delimiters436 words = re.split(r'[,;:\s]+', line.lower())437 # Filter out common words and short terms438 words = {w for w in words if w not in common_words and len(w) > 2}439 resume_keywords.update(words)440 441 # Extract keywords from job description442 for line in job_lines:443 words = re.split(r'[,;:\s]+', line.lower())444 words = {w for w in words if w not in common_words and len(w) > 2}445 job_keywords.update(words)446 447 # Find matching and missing keywords448 matching_keywords = resume_keywords.intersection(job_keywords)449 missing_keywords = job_keywords - resume_keywords450 451 # Extract potential skills and experience452 skills_pattern = r'(?i)(skills|expertise|proficient|experienced|knowledge|abilities)'453 experience_pattern = r'(?i)(experience|work|employment|position|role)'454 455 skills_section = []456 experience_section = []457 458 for line in resume_lines:459 if re.search(skills_pattern, line):460 skills_section.append(line.strip())461 if re.search(experience_pattern, line):462 experience_section.append(line.strip())463 464 # Generate enhanced analysis465 analysis = f"""466# Basic Resume Analysis for {job_title}467 468## Overview469This is a basic analysis provided due to API limitations. For a more detailed analysis, please try again later.470 471## Company Information472- **Company:** {company_name if company_name else 'Not Provided'}473- **Position:** {job_title}474 475## Basic Keyword Analysis476- **Matching Keywords:** {', '.join(list(matching_keywords)[:15])}477- **Missing Keywords:** {', '.join(list(missing_keywords)[:15])}478 479## Skills Section480{chr(10).join(f"- {skill}" for skill in skills_section[:5]) if skills_section else "- No clear skills section identified"}481 482## Experience Highlights483{chr(10).join(f"- {exp}" for exp in experience_section[:3]) if experience_section else "- No clear experience section identified"}484 485## Basic Recommendations4861. Review your resume for the missing keywords identified above4872. Ensure your experience aligns with the job requirements4883. Consider adding specific examples that demonstrate required skills4894. Proofread your resume for any errors or inconsistencies4905. Consider reorganizing your resume to highlight relevant experience first491 492## Action Items4931. Add any missing keywords naturally into your experience descriptions4942. Quantify your achievements where possible (e.g., "increased productivity by 25%")4953. Ensure your most relevant experience is listed first4964. Review the job description for any specific requirements you haven't addressed497 498## Note499This is a simplified analysis. For a more comprehensive review, please try again in a few minutes.500"""501 return analysis502 except Exception as e:503 print(f"Error in fallback analysis: {e}")504 return "Error generating fallback analysis. Please try again later."505 506def should_use_fallback():507 """Determines if we should use fallback mode based on quota errors and time."""508 if analytics["quota_errors"] >= 3: # Reduced from 5 to 3509 return True510 511 # If we had a quota error in the last 5 minutes, use fallback512 if analytics["last_quota_error_time"]:513 time_since_last_error = (datetime.now() - analytics["last_quota_error_time"]).total_seconds()514 if time_since_last_error < 300: # 5 minutes515 return True516 517 return False518 519@app.route('/')520def index():521 """Renders the main page with the form."""522 return render_template('index.html')523 524@app.route('/process', methods=['POST'])525def process_data():526 """Handles the form submission, extracts data, calls the AI model with tools."""527 # Track analytics528 analytics["requests"] += 1529 request_id = str(uuid.uuid4())530 start_time = time.time()531 532 # Check if AI model is configured533 if not model:534 analytics["errors"] += 1535 return jsonify({"error": "AI Model not configured. Check Google API Key."}), 500536 537 # Check if we should use fallback mode538 if should_use_fallback():539 try:540 # Extract data from form541 resume_file = request.files.get('resume')542 job_description = request.form.get('job_description', '').strip()543 job_title = request.form.get('job_title', '').strip()544 company_name = request.form.get('company_name', '').strip()545 546 if not resume_file or not job_description or not job_title:547 return jsonify({"error": "Missing required fields"}), 400548 549 # Process resume550 pdf_stream = io.BytesIO(resume_file.read())551 resume_text = extract_text_from_pdf(pdf_stream)552 553 if not resume_text:554 return jsonify({"error": "Could not extract text from PDF"}), 400555 556 # Get fallback analysis557 fallback_analysis = get_fallback_analysis(resume_text, job_description, job_title, company_name)558 559 return jsonify({560 "analysis_result": fallback_analysis,561 "request_id": request_id,562 "processing_time": f"{time.time() - start_time:.2f}s",563 "fallback_mode": True564 })565 566 except Exception as e:567 return jsonify({"error": f"Error in fallback mode: {str(e)}"}), 500568 569 # Continue with normal processing if not in fallback mode570 try:571 # Prepare tools based on configuration572 active_tools = []573 if web_search_tool and tavily_client:574 active_tools.append(web_search_tool)575 if company_info_tool and tavily_client:576 active_tools.append(company_info_tool)577 if linkedin_analysis_tool and tavily_client:578 active_tools.append(linkedin_analysis_tool)579 if skill_relevance_tool and tavily_client:580 active_tools.append(skill_relevance_tool)581 582 if not active_tools:583 print("Warning: No tools are active due to configuration issues.")584 585 # --- 1. Extract Data from Form --- 586 resume_file = request.files.get('resume')587 job_description = request.form.get('job_description', '').strip()588 linkedin_url = request.form.get('linkedin_url', '').strip()589 company_name = request.form.get('company_name', '').strip()590 job_title = request.form.get('job_title', '').strip()591 industry = request.form.get('industry', '').strip()592 593 # Track analytics594 if industry:595 analytics["industries"][industry] = analytics["industries"].get(industry, 0) + 1596 if job_title:597 analytics["job_titles"][job_title] = analytics["job_titles"].get(job_title, 0) + 1598 599 # Add to recent searches (limited list)600 analytics["recent_searches"] = [601 {"job_title": job_title, "company": company_name, "timestamp": datetime.now().isoformat()} 602 ] + analytics["recent_searches"][:9] # Keep only 10 most recent603 604 # Basic validation605 if not resume_file or not job_description or not job_title:606 analytics["errors"] += 1607 return jsonify({"error": "Missing required fields (Resume, Job Description, Job Title)."}), 400608 609 if resume_file.filename == '' or not resume_file.filename.lower().endswith('.pdf'):610 analytics["errors"] += 1611 return jsonify({"error": "Invalid resume file. Please upload a PDF."}), 400612 613 # --- 2. Process Resume PDF --- 614 print(f"Processing resume: {resume_file.filename}")615 try:616 pdf_stream = io.BytesIO(resume_file.read())617 resume_text = extract_text_from_pdf(pdf_stream)618 if not resume_text:619 analytics["errors"] += 1620 return jsonify({"error": "Could not extract text from the provided PDF. It might be image-based or empty."}), 400621 print(f"Successfully extracted text from {resume_file.filename}")622 except ValueError as e:623 analytics["errors"] += 1624 return jsonify({"error": str(e)}), 400625 except Exception as e:626 analytics["errors"] += 1627 print(f"Error reading resume file stream: {e}")628 traceback.print_exc()629 return jsonify({"error": "Failed to read the resume file."}), 500630 631 # --- 3. Prepare for LLM Interaction ---632 linkedin_data = f"LinkedIn URL: {linkedin_url}" if linkedin_url else "LinkedIn URL Not Provided"633 634 # Extract skills from resume for skill relevance analysis635 resume_skills = extract_resume_keywords(resume_text, job_description)636 637 # Construct the enhanced prompt for the chat638 initial_prompt = f"""639 # Resume and Job Application Analysis for {job_title}640 641 You are CareerPulse AI, a specialized AI career coach with expertise in resume optimization, job market analysis, and interview preparation. Your task is to thoroughly analyze the provided resume against the job description and provide detailed, actionable recommendations to help the candidate significantly increase their chances of success.642 643 ## Candidate Information:644 645 * **Resume Text:**646 ```647 {resume_text}648 ```649 * **LinkedIn Profile URL:** {linkedin_data}650 * **Applying for Job Title:** {job_title}651 * **Company:** {company_name if company_name else 'Not Provided'}652 * **Industry:** {industry if industry else 'Not Provided'}653 654 ## Job Description:655 ```656 {job_description}657 ```658 659 ## Analysis Instructions:660 661 Provide a comprehensive, objective, and firm analysis divided into these clear sections:662 663 ### 1. Resume Polishing Suggestions664 665 - Critically evaluate how well the resume aligns with the job description666 - Identify and list key skills and keywords from the job description that should be incorporated667 - Suggest specific, concrete improvements (rephrasing, reorganizing, adding/removing content)668 - Point out missing keywords from the job description that should be incorporated669 - Identify irrelevant or potentially negative content that should be removed670 - If applicable, suggest structural improvements for better readability and impact671 672 ### 2. Skill Gap Analysis673 674 - Identify specific skills/qualifications in the job description that appear to be missing from the resume675 - For each missing skill, suggest how the candidate might address it:676 * Is there related experience that could be reframed?677 * Could they quickly acquire this skill?678 * Is this a critical requirement or a "nice-to-have"?679 - If the LinkedIn URL was provided, suggest checking if any of these skills might be evidenced there680 681 ### 3. Company Fit Analysis682 683 - Research the company to understand their culture, values, and work environment684 - Evaluate how well the candidate's background and skills align with the company's needs685 - Identify specific aspects of the company that the candidate should highlight in their application686 - Suggest ways to tailor the application to better match the company's expectations687 688 ### 4. Potential Interview Questions689 690 - Create 5-7 highly specific interview questions that are likely for this position691 - Include a mix of behavioral, technical, and role-specific questions692 - If the company name was provided, tailor questions to that company's known culture and values693 - For each question, provide a brief note on what the interviewer is looking for694 695 ### 5. Overall Feedback696 697 - Provide an honest, objective assessment of the candidate's apparent fit for this role698 - Highlight 2-3 strongest selling points based on the resume and job description699 - Identify 2-3 critical areas for improvement700 - Give a firm, actionable recommendation about how to proceed with the application701 702 ## Tool Usage:703 704 Use the available tools when appropriate:705 706 - Use `perform_web_search` to find current information about the industry, job role requirements, or specific technologies707 - Use `retrieve_company_info` to research the company's culture, values, and work environment if the company name is provided708 - Use `analyze_linkedin_profile` to get additional information about the candidate's background if a LinkedIn URL is provided709 - Use `analyze_skill_relevance` to evaluate how well the candidate's skills match the job requirements710 711 Be thoughtful about when to use tools. Only use them when the information would significantly enhance your analysis.712 713 ## Output Format:714 715 Format your response using Markdown with clear headings and subheadings. Use bullet points for lists and bold text for emphasis. Ensure the output is well-structured for easy reading. Be direct, objective, and firm in your recommendations.716 """717 718 # --- 4. Call Gemini API with Tool Integration --- 719 print("\n--- Starting Chat with Gemini (with tools) --- \n")720 721 # Create a new model instance with tools722 model_with_tools = genai.GenerativeModel(723 model_name=model.model_name,724 tools=active_tools,725 generation_config={726 "temperature": 0.7,727 "top_p": 0.8,728 "top_k": 40,729 "max_output_tokens": 2048,730 }731 )732 733 # Start a chat session with the model that has tools734 chat = model_with_tools.start_chat(history=[])735 736 # Initialize ai_feedback variable737 ai_feedback = None738 739 try:740 # Send initial prompt741 print("Sending initial prompt...")742 response = chat.send_message(initial_prompt)743 print("Received initial response from Gemini.")744 745 # Handle potential function calls with a counter to prevent infinite loops746 call_count = 0747 max_calls = 3 # Reduced from 5 to 3 to minimize API usage748 749 while call_count < max_calls:750 if hasattr(response, 'candidates') and response.candidates:751 candidate = response.candidates[0]752 if hasattr(candidate, 'content') and candidate.content:753 content = candidate.content754 755 # Check for function calls756 if hasattr(content, 'parts'):757 for part in content.parts:758 if hasattr(part, 'function_call'):759 call_count += 1760 761 # Debug information762 print(f"Function call object: {part.function_call}")763 print(f"Function call attributes: {dir(part.function_call)}")764 765 # Extract function name - handle different possible structures766 function_name = None767 if hasattr(part.function_call, 'name') and part.function_call.name:768 function_name = part.function_call.name769 elif hasattr(part.function_call, 'function_name') and part.function_call.function_name:770 function_name = part.function_call.function_name771 elif hasattr(part.function_call, 'name') and isinstance(part.function_call.name, dict):772 # Handle case where name might be a dictionary773 function_name = part.function_call.name.get('name', None)774 775 if not function_name:776 print(f"Warning: Function call {call_count} has no name, skipping")777 continue778 779 print(f"Gemini requested function call ({call_count}/{max_calls}): {function_name}")780 781 # Execute the appropriate function based on the call782 if function_name == "perform_web_search":783 # Convert function arguments to a dictionary784 args = {}785 if hasattr(part.function_call, 'args'):786 # Handle different types of args787 if isinstance(part.function_call.args, str):788 try:789 args = json.loads(part.function_call.args)790 except json.JSONDecodeError:791 print("Warning: Could not parse function args as JSON")792 args = {"query": part.function_call.args}793 elif isinstance(part.function_call.args, dict):794 args = part.function_call.args795 else:796 args = {"query": str(part.function_call.args)}797 798 query = args.get('query', '')799 if not query:800 print("Warning: No query provided in function call")801 continue802 803 result = perform_web_search(query)804 print("Web search completed successfully.")805 elif function_name == "retrieve_company_info":806 # Convert function arguments to a dictionary807 args = {}808 if hasattr(part.function_call, 'args'):809 # Handle different types of args810 if isinstance(part.function_call.args, str):811 try:812 args = json.loads(part.function_call.args)813 except json.JSONDecodeError:814 print("Warning: Could not parse function args as JSON")815 args = {"company_name": part.function_call.args}816 elif isinstance(part.function_call.args, dict):817 args = part.function_call.args818 else:819 # Handle MapComposite objects820 try:821 # Try to convert to dict if possible822 if hasattr(part.function_call.args, 'items'):823 args = dict(part.function_call.args)824 else:825 # Otherwise use the string representation826 args = {"company_name": str(part.function_call.args)}827 except Exception as e:828 print(f"Warning: Error handling function args: {e}")829 args = {"company_name": str(part.function_call.args)}830 831 company_name = args.get('company_name', '')832 if not company_name:833 print("Warning: No company name provided in function call")834 continue835 836 result = retrieve_company_info(company_name)837 print("Company info retrieval completed successfully.")838 elif function_name == "analyze_linkedin_profile":839 # Convert function arguments to a dictionary840 args = {}841 if hasattr(part.function_call, 'args'):842 # Handle different types of args843 if isinstance(part.function_call.args, str):844 try:845 args = json.loads(part.function_call.args)846 except json.JSONDecodeError:847 print("Warning: Could not parse function args as JSON")848 args = {"linkedin_url": part.function_call.args}849 elif isinstance(part.function_call.args, dict):850 args = part.function_call.args851 else:852 args = {"linkedin_url": str(part.function_call.args)}853 854 linkedin_url = args.get('linkedin_url', '')855 if not linkedin_url:856 print("Warning: No LinkedIn URL provided in function call")857 continue858 859 result = analyze_linkedin_profile(linkedin_url)860 print("LinkedIn profile analysis completed successfully.")861 elif function_name == "analyze_skill_relevance":862 # Convert function arguments to a dictionary863 args = {}864 if hasattr(part.function_call, 'args'):865 # Handle different types of args866 if isinstance(part.function_call.args, str):867 try:868 args = json.loads(part.function_call.args)869 except json.JSONDecodeError:870 print("Warning: Could not parse function args as JSON")871 args = {"skills": resume_skills, "job_description": job_description, "company_name": company_name}872 elif isinstance(part.function_call.args, dict):873 args = part.function_call.args874 else:875 args = {"skills": resume_skills, "job_description": job_description, "company_name": company_name}876 877 skills = args.get('skills', resume_skills)878 job_desc = args.get('job_description', job_description)879 company = args.get('company_name', company_name)880 881 if not skills or not job_desc:882 print("Warning: Missing required arguments for skill relevance analysis")883 continue884 885 result = analyze_skill_relevance(skills, job_desc, company)886 print("Skill relevance analysis completed successfully.")887 else:888 print(f"Warning: Received unexpected function call request: {function_name}")889 continue890 891 # Send the result back to Gemini892 print(f"Sending {function_name} result back to Gemini...")893 response = chat.send_message(894 f"Function {function_name} returned: {result}"895 )896 print(f"Received response after function call {call_count}")897 else:898 # No more function calls, print the final response899 if hasattr(part, 'text'):900 print("\nFinal response from Gemini:")901 ai_feedback = part.text902 break903 else:904 # No function calls in this response905 if hasattr(response, 'text'):906 print("\nFinal response from Gemini:")907 ai_feedback = response.text908 break909 910 # If we've reached the maximum number of calls or there are no more function calls911 if call_count >= max_calls or not hasattr(response, 'candidates') or not response.candidates or not any(hasattr(p, 'function_call') for p in response.candidates[0].content.parts):912 break913 914 # --- Explicitly request final analysis after tool calls ---915 print("\nTool calls complete. Requesting final analysis from Gemini...")916 try:917 # Send a message asking the model to synthesize the final result918 final_request_prompt = "Please provide the complete, final analysis based on our conversation and the tool results, following all the instructions in the initial prompt."919 final_response = chat.send_message(final_request_prompt)920 921 if hasattr(final_response, 'text') and final_response.text:922 ai_feedback = final_response.text923 print("Received final analysis after explicit request.")924 else:925 print("Warning: Final request did not yield text response. Attempting to use last known response.")926 # Fallback to trying the last response from the loop if explicit request failed927 if not ai_feedback and hasattr(response, 'text') and response.text:928 ai_feedback = response.text 929 elif not ai_feedback and hasattr(response, 'candidates') and response.candidates and hasattr(response.candidates[0].content, 'parts'):930 # Try to extract from last candidate parts if available931 for part in response.candidates[0].content.parts:932 if hasattr(part, 'text'):933 ai_feedback = part.text934 break935 except Exception as final_request_error:936 print(f"Error requesting final analysis: {final_request_error}")937 # Keep existing ai_feedback if any, otherwise proceed to fallback938 939 # Fallback if no feedback captured940 if not ai_feedback:941 # If all else fails, use a simple fallback response942 ai_feedback = f"""943# Resume Analysis for {job_title}944 945## Overview946I've analyzed your resume against the job description for the {job_title} position at {company_name if company_name else 'the company'}.947 948## Resume Polishing Suggestions949- Ensure your resume highlights skills that match the job requirements950- Consider reorganizing your experience to emphasize relevant achievements951- Add specific metrics and results where possible952 953## Skill Gap Analysis954- Review the job description for any required skills not present in your resume955- Consider how your existing experience might relate to these requirements956 957## Potential Interview Questions9581. Can you describe your experience with [relevant skill]?9592. How have you handled [specific situation] in your previous roles?9603. What interests you about this position at {company_name if company_name else 'our company'}?961 962## Overall Feedback963Based on the information provided, I recommend focusing on aligning your resume more closely with the job requirements and preparing specific examples that demonstrate your relevant experience.964 965*Note: This is a simplified analysis due to technical limitations or failure to retrieve the full AI response. Please try again later.*966"""967 print("Using fallback response due to no valid response from model after tool calls and final request.")968 969 print("--- Received Final Text Response from Gemini ---")970 971 # --- DEBUG: Log the final AI feedback before sending --- 972 print("\n===== Final AI Feedback being sent to frontend: ====")973 print(ai_feedback)974 print("=====================================================")975 976 # Analytics tracking for success977 analytics["successful_analyses"] += 1978 processing_time = time.time() - start_time979 print(f"Request {request_id} completed in {processing_time:.2f} seconds")980 981 # Return the analysis result AND the original resume text for editing982 return jsonify({983 "analysis_result": ai_feedback,984 "resume_text": resume_text,985 "request_id": request_id,986 "processing_time": f"{processing_time:.2f}s"987 })988 989 except Exception as e:990 analytics["errors"] += 1991 print(f"Error during Gemini chat interaction: {e}")992 traceback.print_exc()993 error_details = getattr(e, 'details', str(e))994 995 # Ensure error_details is treated as a string996 error_details_str = str(error_details)997 998 # Handle different error types999 error_type = type(e).__name__1000 error_msg = str(e)1001 print(f"Caught generic exception: Type={error_type}, Msg={error_msg}, Details={error_details_str}")1002 1003 if "API key not valid" in error_details_str:1004 return jsonify({"error": "Invalid or missing Google API Key. Please check your configuration."}), 5001005 elif "quota" in error_details_str.lower():1006 print("API quota exceeded. Adding to analytics.")1007 # Track quota errors specifically1008 analytics["quota_errors"] += 11009 analytics["last_quota_error_time"] = datetime.now()1010 analytics["fallback_mode"] = True1011 # Return a more detailed error with HTTP 429 (Too Many Requests) status code1012 return jsonify({1013 "error": "API quota exceeded. Switching to fallback mode. Please try again.",1014 "error_type": "quota_exceeded",1015 "retry_after": "300", # Suggest retry after 5 minutes1016 "fallback_mode": True1017 }), 4291018 elif "Function" in error_details_str and "not found" in error_details_str:1019 return jsonify({"error": f"Internal Error: Gemini tried to call an undefined function. Check tool definitions."}), 5001020 else:1021 return jsonify({1022 "error": f"An error occurred while communicating with the AI model. Type: {error_type}, Message: {error_msg}, Details={error_details_str}"1023 }), 5001024 1025 except Exception as e:1026 # Catch-all for any other unexpected errors1027 analytics["errors"] += 11028 print(f"An unexpected error occurred in /process: {e}")1029 traceback.print_exc()1030 return jsonify({"error": f"An internal server error occurred: {str(e)}"}), 5001031 1032# --- PDF Download Endpoint ---1033@app.route('/download-pdf', methods=['POST'])1034def download_pdf():1035 """Generates a PDF from edited resume text and sends it to the user."""1036 try:1037 data = request.get_json()1038 resume_text = data.get('resume_text')1039 1040 if not resume_text:1041 return jsonify({"error": "No resume text provided."}), 4001042 1043 # Render HTML template with the resume text1044 rendered_html = render_template('resume_pdf.html', resume_text=resume_text)1045 1046 # Generate PDF in memory1047 pdf_bytes = HTML(string=rendered_html).write_pdf()1048 1049 # Send the PDF as a file1050 return send_file(1051 io.BytesIO(pdf_bytes),1052 mimetype='application/pdf',1053 as_attachment=True,1054 download_name='Improved_Resume.pdf'1055 )1056 except Exception as e:1057 print(f"Error generating PDF: {e}")1058 traceback.print_exc()1059 return jsonify({"error": f"An internal server error occurred during PDF generation."}), 5001060 1061# --- Analytics Endpoint ---1062@app.route('/api/analytics', methods=['GET'])1063def get_analytics():1064 """Returns anonymized analytics about application usage."""1065 # This would normally be protected with authentication1066 return jsonify({1067 "total_requests": analytics["requests"],1068 "successful_analyses": analytics["successful_analyses"],1069 "error_rate": f"{(analytics['errors'] / analytics['requests'] * 100) if analytics['requests'] > 0 else 0:.1f}%",1070 "top_industries": dict(sorted(analytics["industries"].items(), key=lambda x: x[1], reverse=True)[:5]),1071 "top_job_titles": dict(sorted(analytics["job_titles"].items(), key=lambda x: x[1], reverse=True)[:5]),1072 "recent_searches_count": len(analytics["recent_searches"])1073 })1074 1075if __name__ == '__main__':1076 port = int(os.environ.get('PORT', 7860)) # Using Hugging Face Spaces default port1077 app.run(host='0.0.0.0', port=port, debug=False)