CoolFace
Apppublic

pascalx/careerplus

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py1077 linesDownload Raw Back to root
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)