Spaces:
No application file
No application file
| import eventlet | |
| eventlet.monkey_patch() | |
| import os | |
| import logging | |
| from dotenv import load_dotenv | |
| from flask import Flask, render_template, request, jsonify # Removed Flask session import | |
| from flask_socketio import SocketIO, emit, disconnect # Removed join/leave room imports | |
| import uuid # For temporary file names | |
| import base64 # Needed for decoding file data | |
| import sys # For potential exit on critical error | |
| # Import from your refactored logic | |
| from interview_logic import ( | |
| initialize_llm, | |
| parse_resume, | |
| ResumeData, # ResumeData needed? Only for type hint maybe | |
| InterviewSession | |
| ) | |
| # Load environment variables (especially GOOGLE_API_KEY) | |
| load_dotenv() | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| # --- Flask App Setup --- | |
| app = Flask(__name__) | |
| # SECRET_KEY is needed for SocketIO even if Flask sessions aren't used explicitly | |
| app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', os.urandom(24)) | |
| app.config['UPLOAD_FOLDER'] = 'uploads' | |
| # Ensure upload folder exists | |
| if not os.path.exists(app.config['UPLOAD_FOLDER']): | |
| try: | |
| os.makedirs(app.config['UPLOAD_FOLDER']) | |
| logging.info(f"Created upload folder: {app.config['UPLOAD_FOLDER']}") | |
| except OSError as e: | |
| logging.error(f"Could not create upload folder {app.config['UPLOAD_FOLDER']}: {e}") | |
| # Depending on severity, you might want to exit | |
| # sys.exit(f"Fatal Error: Cannot create upload directory {app.config['UPLOAD_FOLDER']}") | |
| # Use eventlet or gevent for better concurrency with WebSockets | |
| async_mode = None | |
| try: | |
| import eventlet | |
| async_mode = 'eventlet' | |
| eventlet.monkey_patch() | |
| logging.info("Using eventlet for async mode.") | |
| except ImportError: | |
| logging.warning("eventlet not found, WebSocket performance might be limited.") | |
| pass | |
| socketio = SocketIO(app, async_mode=async_mode, cors_allowed_origins="*") # Allow all origins for simplicity | |
| # --- Global Variables / State Management --- | |
| # WARNING: In-memory storage is lost on server restart and doesn't scale. | |
| # Use Redis, a database, etc., for production. | |
| active_sessions = {} # Store InterviewSession instances mapped by sid | |
| llm = None # Initialize LLM globally once | |
| # --- Helper Functions --- | |
| def get_llm(): | |
| """Gets the initialized LLM instance, initializing if needed.""" | |
| global llm | |
| if llm is None: | |
| api_key = os.getenv("GOOGLE_API_KEY") | |
| if not api_key or api_key == "YOUR_ACTUAL_GOOGLE_API_KEY_HERE": | |
| logging.error("GOOGLE_API_KEY not found or not set in .env file.") | |
| # This is a critical configuration error | |
| raise ValueError("Server configuration error: Google API Key is missing.") | |
| try: | |
| llm = initialize_llm(api_key) | |
| except Exception as e: | |
| logging.error(f"LLM Initialization failed: {e}") | |
| # Raise the error to prevent the server from starting incorrectly | |
| raise RuntimeError(f"Fatal Error: Could not initialize LLM - {e}") | |
| return llm | |
| # --- Flask Routes --- | |
| def index(): | |
| """Serves the main HTML page.""" | |
| return render_template('index.html') | |
| # --- SocketIO Event Handlers --- | |
| def handle_connect(): | |
| """Handles new client connections.""" | |
| sid = request.sid | |
| logging.info(f"Client connected: {sid}") | |
| emit('status_update', {'message': 'Connected to interview server.'}) | |
| def handle_disconnect(): | |
| """Handles client disconnections and cleans up resources.""" | |
| sid = request.sid | |
| logging.info(f"Client disconnected: {sid}") | |
| # Clean up the session for this user | |
| session_data = active_sessions.pop(sid, None) | |
| if session_data: | |
| logging.info(f"Cleaned up session for {sid}") | |
| # Clean up temporary PDF file if it exists | |
| pdf_path = session_data.get('pdf_path') | |
| if pdf_path and os.path.exists(pdf_path): | |
| try: | |
| os.remove(pdf_path) | |
| logging.info(f"Removed temporary PDF: {pdf_path}") | |
| except OSError as e: | |
| logging.error(f"Error removing temporary PDF {pdf_path}: {e}") | |
| def handle_resume_upload(data): | |
| """Handles PDF upload, parsing, and starting the interview.""" | |
| sid = request.sid | |
| logging.info(f"Received resume upload request from {sid}") | |
| pdf_path = None # Initialize pdf_path for potential cleanup in error handlers | |
| # Ensure LLM is ready (called here to handle potential init issues per request) | |
| try: | |
| current_llm = get_llm() | |
| if not current_llm: # Should not happen if get_llm raises error, but check anyway | |
| emit('error_message', {'message': 'Server error: AI model not available.'}, room=sid) | |
| return | |
| except (ValueError, RuntimeError) as llm_error: | |
| emit('error_message', {'message': f'{llm_error}'}, room=sid) | |
| return | |
| # Clean up any existing session for this connection ID | |
| if sid in active_sessions: | |
| logging.warning(f"Session already exists for {sid}. Cleaning up old session first.") | |
| handle_disconnect() # Reuse disconnect logic for cleanup | |
| pdf_data = data.get('file_data') # Expecting base64 encoded data URI | |
| file_name = data.get('file_name', 'uploaded_resume.pdf') | |
| if not pdf_data: | |
| emit('error_message', {'message': 'No file data received.'}, room=sid) | |
| return | |
| try: | |
| # Decode base64 data URI | |
| try: | |
| header, encoded = pdf_data.split(',', 1) | |
| pdf_bytes = base64.b64decode(encoded) | |
| except (ValueError, base64.binascii.Error) as decode_error: | |
| logging.error(f"Invalid base64 data received from {sid}: {decode_error}") | |
| emit('error_message', {'message': 'Invalid file data format received.'}, room=sid) | |
| return | |
| # Sanitize filename slightly (prevent directory traversal) | |
| safe_filename = os.path.basename(file_name) | |
| temp_filename = f"{uuid.uuid4()}_{safe_filename}" | |
| pdf_path = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename) | |
| # Ensure upload folder still exists (paranoid check) | |
| if not os.path.exists(app.config['UPLOAD_FOLDER']): | |
| os.makedirs(app.config['UPLOAD_FOLDER']) | |
| with open(pdf_path, 'wb') as f: | |
| f.write(pdf_bytes) | |
| logging.info(f"Temporary PDF saved to: {pdf_path}") | |
| # --- Start Parsing --- | |
| emit('status_update', {'message': f"Analyzing resume: {safe_filename}..."}, room=sid) | |
| resume_data = parse_resume(pdf_path, current_llm) | |
| # --- Initialize Interview Session --- | |
| interview_session = InterviewSession(resume_data, current_llm) | |
| active_sessions[sid] = { | |
| 'interview_session': interview_session, | |
| 'pdf_path': pdf_path # Store path for cleanup | |
| } | |
| logging.info(f"Interview session created for {sid} with candidate {interview_session.candidate_name}") | |
| # --- Send Welcome and First Question --- | |
| welcome_message = f"Okay {interview_session.candidate_name}, I've analyzed your resume. Let's start the interview simulation." | |
| emit('agent_message', {'text': welcome_message}, room=sid) | |
| socketio.sleep(1.5) # Pause slightly after welcome | |
| first_question = interview_session.get_next_question() | |
| if first_question and not first_question.startswith("[Error"): | |
| emit('agent_message', {'text': first_question, 'is_question': True}, room=sid) # Mark as question | |
| else: | |
| failed_question_msg = first_question if first_question else "I couldn't generate the first question." | |
| emit('agent_message', {'text': f"{failed_question_msg} Please try uploading the resume again."}, room=sid) | |
| logging.error(f"Failed to get a valid first question for {sid}.") | |
| # Clean up immediately if first question fails critically | |
| handle_disconnect() | |
| except (FileNotFoundError, ValueError, RuntimeError) as e: | |
| # These are expected errors during processing | |
| logging.error(f"Error processing resume for {sid}: {e}") | |
| emit('error_message', {'message': f"Error processing resume: {e}"}, room=sid) | |
| if pdf_path and os.path.exists(pdf_path): | |
| try: os.remove(pdf_path) | |
| except OSError: pass | |
| active_sessions.pop(sid, None) | |
| except Exception as e: | |
| # Catch unexpected errors | |
| logging.exception(f"Unexpected error during resume upload/parse for {sid}: {e}") # Log full traceback | |
| emit('error_message', {'message': 'An unexpected server error occurred during upload.'}, room=sid) | |
| if pdf_path and os.path.exists(pdf_path): | |
| try: os.remove(pdf_path) | |
| except OSError: pass | |
| active_sessions.pop(sid, None) | |
| def handle_user_response(data): | |
| """Handles the transcribed text from the user.""" | |
| sid = request.sid | |
| logging.info(f"Received user response from {sid}") | |
| session_data = active_sessions.get(sid) | |
| if not session_data or 'interview_session' not in session_data: | |
| logging.warning(f"Received response from {sid}, but no active session found.") | |
| emit('error_message', {'message': 'Your session seems to have expired. Please upload the resume again.'}, room=sid) | |
| return | |
| interview_session = session_data['interview_session'] | |
| answer_text = data.get('text', '[No Answer Provided]') | |
| try: | |
| # --- Evaluate Answer --- | |
| emit('status_update', {'message': 'Evaluating your answer...'}, room=sid) | |
| feedback = interview_session.record_answer_and_evaluate(answer_text) | |
| if feedback: | |
| emit('agent_message', {'text': feedback}, room=sid) | |
| socketio.sleep(1.5) # Pause after feedback | |
| # --- Get Next Question or Final Feedback --- | |
| next_question = interview_session.get_next_question() | |
| if next_question and not next_question.startswith("[Error"): | |
| emit('agent_message', {'text': next_question, 'is_question': True}, room=sid) # Mark as question | |
| else: | |
| # Handle case where next question generation fails OR interview is done | |
| if next_question and next_question.startswith("[Error"): | |
| logging.error(f"Failed to generate next question for {sid}. Proceeding to final feedback.") | |
| emit('agent_message', {'text': "I had trouble generating the next question. Let's move to the final feedback."}, room=sid) | |
| socketio.sleep(1.5) | |
| # Interview questions finished OR failed to get next question, generate final feedback | |
| emit('status_update', {'message': 'Generating final feedback...'}, room=sid) | |
| final_feedback = interview_session.generate_final_feedback() | |
| emit('agent_message', {'text': final_feedback}, room=sid) | |
| socketio.sleep(1) # Short pause | |
| emit('interview_finished', {'message': 'Interview simulation complete. Thank you!'}, room=sid) | |
| # Clean up the session after completion | |
| handle_disconnect() # Reuse disconnect logic | |
| except Exception as e: | |
| logging.exception(f"Unexpected error during user response processing for {sid}: {e}") | |
| emit('error_message', {'message': 'An unexpected server error occurred while processing your answer.'}, room=sid) | |
| # Consider ending the interview gracefully | |
| handle_disconnect() | |
| # --- Main Execution --- | |
| if __name__ == '__main__': | |
| print("Starting Flask-SocketIO server...") | |
| # Attempt to initialize LLM on startup to catch config errors early | |
| llm_init_error = False | |
| try: | |
| get_llm() | |
| print("AI Model initialized successfully.") | |
| except (ValueError, RuntimeError) as e: | |
| print(f"FATAL STARTUP ERROR: {e}") | |
| print("Please check your .env file and API key permissions.") | |
| llm_init_error = True | |
| # Exit if LLM is critical for the app to function | |
| sys.exit(f"Exiting due to LLM initialization failure: {e}") | |
| except Exception as e: | |
| # Catch other potential init errors | |
| print(f"WARNING: Unexpected error during LLM pre-initialization: {e}") | |
| llm_init_error = True # Treat as potential issue | |
| if llm_init_error: | |
| # This part might not be reached if sys.exit() was called | |
| print("There was an issue initializing the AI model on startup.") | |
| print("The application server might run, but API calls will likely fail.") | |
| print(f"Server starting on http://127.0.0.1:5000 (or your local IP on port 5000)") | |
| # Use socketio.run for development. For production, use Gunicorn/Waitress. | |
| # Example: gunicorn --worker-class eventlet -w 1 app:app | |
| socketio.run(app, debug=True, host='0.0.0.0', port=5000, use_reloader=False) |