import os import json import logging import requests from PyPDF2 import PdfReader from datetime import datetime from .utils import create_zoom_meeting from recruitment.models import Candidate from .models import ScheduledInterview, ZoomMeeting, Candidate, JobPosting, InterviewSchedule logger = logging.getLogger(__name__) OPENROUTER_API_KEY ='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' OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' if not OPENROUTER_API_KEY: logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.") def extract_text_from_pdf(file_path): print("text extraction") text = "" try: with open(file_path, "rb") as f: reader = PdfReader(f) for page in reader.pages: text += (page.extract_text() or "") except Exception as e: logger.error(f"PDF extraction failed: {e}") raise return text.strip() def ai_handler(prompt): print("model call") response = requests.post( url="https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json", }, data=json.dumps({ "model": OPENROUTER_MODEL, "messages": [{"role": "user", "content": prompt}], }, ) ) res = {} print(response.status_code) if response.status_code == 200: res = response.json() content = res["choices"][0]['message']['content'] try: content = content.replace("```json","").replace("```","") res = json.loads(content) except Exception as e: print(e) # res = raw_output["choices"][0]["message"]["content"] else: print("error response") return res def handle_reume_parsing_and_scoring(pk): logger.info(f"Scoring resume for candidate {pk}") try: instance = Candidate.objects.get(pk=pk) file_path = instance.resume.path if not os.path.exists(file_path): logger.warning(f"Resume file not found: {file_path}") return resume_text = extract_text_from_pdf(file_path) job_detail= f"{instance.job.description} {instance.job.qualifications}" resume_parser_prompt = f""" You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object: full_name: Full name of the candidate current_title: Most recent or current job title location: City and state (or country if outside the U.S.) contact: Phone number and email (as a single string or separate fields) linkedin: LinkedIn profile URL (if present) github: GitHub or portfolio URL (if present) summary: Brief professional profile or summary (1–2 sentences) education: List of degrees, each with: institution degree year gpa (if provided) relevant_courses (as a list, if mentioned) skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools experience: List of roles, each with: company job_title location start_date and end_date (or "Present" if applicable) key_achievements (as a list of concise bullet points) projects: List of notable projects (if clearly labeled), each with: name year technologies_used brief_description Instructions: Be concise but preserve key details. Normalize formatting (e.g., “Jun. 2014” → “2014-06”). Omit redundant or promotional language. If a section is missing, omit the key or set it to null/empty list as appropriate. Output only valid JSON—no markdown, no extra text. Now, process the following resume text: {resume_text} """ resume_parser_result = ai_handler(resume_parser_prompt) resume_scoring_prompt = f""" You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria. **Job Criteria:** {job_detail} **Candidate's Extracted Resume Json:** \"\"\" {resume_parser_result} \"\"\" **Your Task:** Provide a response in strict JSON format with the following keys: 1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role. 2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria. 3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing. 4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}). Only output valid JSON. Do not include any other text. """ resume_scoring_result = ai_handler(resume_scoring_prompt) instance.parsed_summary = str(resume_parser_result) # Update candidate with scoring results instance.match_score = resume_scoring_result.get('match_score') instance.strengths = resume_scoring_result.get('strengths', '') instance.weaknesses = resume_scoring_result.get('weaknesses', '') instance.criteria_checklist = resume_scoring_result.get('criteria_checklist', {}) instance.is_resume_parsed = True # Save only scoring-related fields to avoid recursion instance.save(update_fields=[ 'match_score', 'strengths', 'weaknesses', 'criteria_checklist','parsed_summary', 'is_resume_parsed' ]) logger.info(f"Successfully scored resume for candidate {instance.id}") except Exception as e: logger.error(f"Failed to score resume for candidate {instance.id}: {e}") def create_interview_and_meeting( candidate_id, job_id, schedule_id, slot_date, slot_time, duration ): """ Synchronous task for a single interview slot, dispatched by django-q. """ try: candidate = Candidate.objects.get(pk=candidate_id) job = JobPosting.objects.get(pk=job_id) schedule = InterviewSchedule.objects.get(pk=schedule_id) interview_datetime = datetime.combine(slot_date, slot_time) meeting_topic = f"Interview for {job.title} - {candidate.name}" # 1. External API Call (Slow) result = create_zoom_meeting(meeting_topic, interview_datetime, duration) if result["status"] == "success": # 2. Database Writes (Slow) zoom_meeting = ZoomMeeting.objects.create( topic=meeting_topic, start_time=interview_datetime, duration=duration, meeting_id=result["meeting_details"]["meeting_id"], join_url=result["meeting_details"]["join_url"], zoom_gateway_response=result["zoom_gateway_response"], ) ScheduledInterview.objects.create( candidate=candidate, job=job, zoom_meeting=zoom_meeting, schedule=schedule, interview_date=slot_date, interview_time=slot_time ) # Log success or use Django-Q result system for monitoring logger.info(f"Successfully scheduled interview for {candidate.name}") return True # Task succeeded else: # Handle Zoom API failure (e.g., log it or notify administrator) logger.error(f"Zoom API failed for {candidate.name}: {result['message']}") return False # Task failed except Exception as e: # Catch any unexpected errors during database lookups or processing logger.error(f"Critical error scheduling interview: {e}") return False # Task failed def handle_zoom_webhook_event(payload): """ Background task to process a Zoom webhook event and update the local ZoomMeeting status. It handles: created, updated, started, ended, and deleted events. """ event_type = payload.get('event') object_data = payload['payload']['object'] # Zoom often uses a long 'id' for the scheduled meeting and sometimes a 'uuid'. # We rely on the unique 'id' that maps to your ZoomMeeting.meeting_id field. meeting_id_zoom = str(object_data.get('id')) print(meeting_id_zoom) if not meeting_id_zoom: logger.warning(f"Webhook received without a valid Meeting ID: {event_type}") return False try: # Use filter().first() to avoid exceptions if the meeting doesn't exist yet, # and to simplify the logic flow. meeting_instance = ZoomMeeting.objects.filter(meeting_id=meeting_id_zoom).first() print(meeting_instance) # --- 1. Creation and Update Events --- if event_type == 'meeting.updated': if meeting_instance: # Update key fields from the webhook payload meeting_instance.topic = object_data.get('topic', meeting_instance.topic) # Check for and update status and time details # if event_type == 'meeting.created': # meeting_instance.status = 'scheduled' # elif event_type == 'meeting.updated': # Only update time fields if they are in the payload print(object_data) meeting_instance.start_time = object_data.get('start_time', meeting_instance.start_time) meeting_instance.duration = object_data.get('duration', meeting_instance.duration) meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone) # Also update join_url, password, etc., if needed based on the payload structure meeting_instance.status = 'scheduled' meeting_instance.save(update_fields=['topic', 'start_time', 'duration', 'timezone', 'status']) # --- 2. Status Change Events (Start/End) --- elif event_type == 'meeting.started': if meeting_instance: meeting_instance.status = 'started' meeting_instance.save(update_fields=['status']) elif event_type == 'meeting.ended': if meeting_instance: meeting_instance.status = 'ended' meeting_instance.save(update_fields=['status']) # --- 3. Deletion Event (User Action) --- elif event_type == 'meeting.deleted': if meeting_instance: # Mark as cancelled/deleted instead of physically deleting for audit trail meeting_instance.status = 'cancelled' meeting_instance.save(update_fields=['status']) return True except Exception as e: logger.error(f"Failed to process Zoom webhook for {event_type} (ID: {meeting_id_zoom}): {e}", exc_info=True) return False