Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import json | |
| import random | |
| import smtplib | |
| import requests | |
| import logging | |
| import gradio as gr | |
| from datetime import datetime, timedelta | |
| from PyPDF2 import PdfReader | |
| from bs4 import BeautifulSoup | |
| from sentence_transformers import SentenceTransformer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| import torch | |
| from email.mime.text import MIMEText | |
| from email.mime.multipart import MIMEMultipart | |
| from email.mime.application import MIMEApplication | |
| # Set up logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logging.getLogger().addHandler(logging.FileHandler("application_log.txt")) | |
| # Set up authentication keys as environment variables | |
| os.environ['CLIENT_ID'] = '78iccqej5ala77' | |
| os.environ['CLIENT_SECRET'] = 'WPL_AP1.TQCswIWpXAXUOKeQ.8EwVvA==' # Replace with actual 32-character secret | |
| logging.info("Authentication keys set as environment variables") | |
| # Set up GPU if available | |
| if torch.cuda.is_available(): | |
| device = torch.device("cuda") | |
| logging.info(f"Using GPU: {torch.cuda.get_device_name(0)}") | |
| else: | |
| device = torch.device("cpu") | |
| logging.info("GPU not available, using CPU instead") | |
| # Initialize the sentence transformer model | |
| def initialize_model(): | |
| logging.info("Initializing sentence transformer model") | |
| try: | |
| model = SentenceTransformer('paraphrase-MiniLM-L6-v2', device=device) | |
| return model | |
| except Exception as e: | |
| logging.error(f"Failed to initialize model: {str(e)}") | |
| raise | |
| model = initialize_model() | |
| # Function to extract text from a PDF resume | |
| def extract_resume_text(pdf_file_path): | |
| logging.info("Extracting resume text") | |
| try: | |
| with open(pdf_file_path, 'rb') as f: | |
| pdf_reader = PdfReader(f) | |
| text = "" | |
| for page in pdf_reader.pages: | |
| extracted = page.extract_text() | |
| if extracted: | |
| text += extracted | |
| if not text.strip(): | |
| raise Exception("No text extracted from PDF. Ensure the PDF is not image-based.") | |
| logging.info(f"Extracted resume text (first 200 chars): {text[:200]}") | |
| return text | |
| except Exception as e: | |
| logging.error(f"Error extracting text from PDF: {str(e)}") | |
| raise Exception(f"Error extracting text from PDF: {str(e)}") | |
| # Function to parse resume and extract key information | |
| def parse_resume(resume_text): | |
| logging.info("Parsing resume") | |
| parsed_info = { | |
| "skills": [], | |
| "education": [], | |
| "experience": [], | |
| "personal_info": {}, | |
| "react_experience": "0", | |
| "redux_experience": "0", | |
| "javascript_experience": "0", | |
| "education_details": [], | |
| "work_history": [] | |
| } | |
| # Split resume into sections based on candidate headers | |
| candidate_pattern = r'(IM A\. SAMPLE [IVX]+)\s*' | |
| candidate_sections = re.split(candidate_pattern, resume_text, flags=re.IGNORECASE) | |
| candidates = [] | |
| for i in range(1, len(candidate_sections), 2): | |
| candidates.append((candidate_sections[i], candidate_sections[i+1])) | |
| if not candidates: | |
| candidates = [("Unknown Candidate", resume_text)] | |
| candidate_name, candidate_text = candidates[0] | |
| parsed_info["personal_info"]["name"] = candidate_name.strip() | |
| logging.info(f"Parsed candidate name: {candidate_name}") | |
| # Extract email | |
| email_pattern = r'[\w\.-]+@[\w\.-]+\.\w+' | |
| email_matches = re.findall(email_pattern, candidate_text, re.IGNORECASE) | |
| if email_matches: | |
| parsed_info["personal_info"]["email"] = email_matches[0] | |
| else: | |
| logging.warning("No email found in resume") | |
| # Extract phone number | |
| phone_pattern = r'\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}' | |
| phone_matches = re.findall(phone_pattern, candidate_text) | |
| if phone_matches: | |
| parsed_info["personal_info"]["phone"] = phone_matches[0] | |
| else: | |
| logging.warning("No phone number found in resume") | |
| # Extract address | |
| address_pattern = r'(\d+\s+[A-Za-z\s]+,\s*[A-Za-z\s]+,\s*[A-Z]{2}\s*\d{5})' | |
| address_matches = re.findall(address_pattern, candidate_text, re.IGNORECASE) | |
| if address_matches: | |
| parsed_info["personal_info"]["address"] = address_matches[0] | |
| else: | |
| parsed_info["personal_info"]["address"] = "Not found" | |
| logging.warning("No address found in resume") | |
| # Expanded skill keywords for various fields | |
| skill_keywords = [ | |
| "python", "java", "javascript", "html", "css", "sql", "react", "node", "aws", "azure", | |
| "docker", "git", "c++", "visual basic", "perl", "asp", "php", "cobol", "xml", "asp.net", | |
| "quickbooks", "ms office", "ms access", "spss", "typescript", "angular", "vue", "mysql", | |
| "mongodb", "linux", "bash", "kubernetes", "jenkins", | |
| "marketing", "digital marketing", "seo", "content creation", "social media", "branding", | |
| "finance", "accounting", "financial analysis", "bookkeeping", "tax preparation", | |
| "nursing", "patient care", "medical coding", "pharmacy", "clinical research", | |
| "project management", "agile", "scrum", "leadership", "team management", | |
| "graphic design", "ui/ux", "adobe photoshop", "illustrator", "canva", | |
| "teaching", "curriculum development", "classroom management", | |
| "sales", "customer service", "crm", "business development", | |
| "writing", "editing", "technical writing", "grant writing" | |
| ] | |
| resume_lower = candidate_text.lower() | |
| for skill in skill_keywords: | |
| if skill.lower() in resume_lower or f"{skill.lower()} " in resume_lower: | |
| parsed_info["skills"].append(skill) | |
| if not parsed_info["skills"]: | |
| logging.warning("No skills extracted from resume") | |
| # Extract specific experience (technical fields only for now) | |
| patterns = { | |
| "react_experience": r'(\d+)[\s\+]*(years?|yrs?)[\s\+]*(?:of)?[\s\+]*(?:experience)?[\s\+]*(?:with|in)?[\s\+]*React', | |
| "redux_experience": r'(\d+)[\s\+]*(years?|yrs?)[\s\+]*(?:of)?[\s\+]*(?:experience)?[\s\+]*(?:with|in)?[\s\+]*Redux', | |
| "javascript_experience": r'(\d+)[\s\+]*(years?|yrs?)[\s\+]*(?:of)?[\s\+]*(?:experience)?[\s\+]*(?:with|in)?[\s\+]*(?:JavaScript|JS)' | |
| } | |
| for key, pattern in patterns.items(): | |
| matches = re.findall(pattern, candidate_text, re.IGNORECASE) | |
| if matches: | |
| parsed_info[key] = matches[0][0] | |
| else: | |
| logging.debug(f"No {key} found in resume") | |
| # Extract education | |
| education_pattern = r'(?i)(bachelor|master|phd|b\.s\.|m\.s\.|b\.a\.|m\.a\.|mba|associate|certificate)\s*[\'’]?\s*[so]?\s*[A-Za-z\s,]+?(?:(?:\(|,|\n)((?:19|20)\d{2}|Expected[^\n]*|June|Jan|Summer|Fall|Spring))' | |
| education_matches = re.findall(education_pattern, candidate_text) | |
| parsed_info["education_details"] = [ | |
| {"degree": deg, "institution": inst.strip(), "year": year.strip()} | |
| for deg, inst, year in education_matches | |
| ] | |
| parsed_info["education"] = [f"{edu['degree']} from {edu['institution']} ({edu['year']})" for edu in parsed_info["education_details"]] | |
| if not parsed_info["education"]: | |
| logging.warning("No education details extracted from resume") | |
| # Extract experience periods | |
| experience_pattern = r'(?i)(\d{4})\s*(?:-|to)\s*(present|\d{4})' | |
| experience_matches = re.findall(experience_pattern, candidate_text) | |
| parsed_info["experience"] = [f"{start}-{end}" for start, end in experience_matches] | |
| if not parsed_info["experience"]: | |
| logging.warning("No experience periods extracted from resume") | |
| # Extract work history details | |
| work_history_pattern = r'(?i)([A-Za-z\s\/-]+),\s*([A-Za-z\s]+),\s*([A-Za-z\s]+)\s*\(([\d\s-]+|present|Summer|Fall|Spring|Jan|June)\)' | |
| work_history_matches = re.findall(work_history_pattern, candidate_text) | |
| parsed_info["work_history"] = [ | |
| {"role": role.strip(), "company": company.strip(), "location": location.strip(), "years": years.strip()} | |
| for role, company, location, years in work_history_matches | |
| ] | |
| if not parsed_info["work_history"]: | |
| logging.warning("No work history extracted from resume") | |
| logging.info(f"Parsed resume info: {json.dumps(parsed_info, indent=2)}") | |
| return parsed_info | |
| # Function to authenticate with job board API | |
| def authenticate_job_board(): | |
| logging.info("Authenticating with job board API") | |
| try: | |
| client_id = os.environ.get('CLIENT_ID') | |
| client_secret = os.environ.get('CLIENT_SECRET') | |
| if not client_id or not client_secret: | |
| logging.error("Missing Client ID or Client Secret") | |
| raise Exception("Authentication failed: Missing Client ID or Client Secret") | |
| auth_url = "https://api.jobboard.example.com/oauth/token" # Replace with actual API | |
| payload = { | |
| "client_id": client_id, | |
| "client_secret": client_secret, | |
| "grant_type": "client_credentials" | |
| } | |
| response = requests.post(auth_url, data=payload, timeout=5) | |
| if response.status_code == 200: | |
| access_token = response.json().get("access_token") | |
| logging.info("API authentication successful") | |
| return access_token | |
| else: | |
| logging.error(f"API authentication failed: HTTP {response.status_code}") | |
| raise Exception(f"API authentication failed: HTTP {response.status_code}") | |
| except Exception as e: | |
| logging.error(f"Error during API authentication: {str(e)}") | |
| return None | |
| # Function to scrape LinkedIn jobs or use job board API | |
| def search_jobs(job_title, location, num_jobs=5, skills=[]): | |
| logging.info(f"Searching jobs for {job_title} in {location}") | |
| try: | |
| access_token = authenticate_job_board() | |
| if access_token: | |
| job_api_url = f"https://api.jobboard.example.com/jobs?query={job_title}&location={location}&limit={num_jobs}" | |
| headers = { | |
| "Authorization": f"Bearer {access_token}", | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124" | |
| } | |
| response = requests.get(job_api_url, headers=headers, timeout=5) | |
| if response.status_code == 200: | |
| jobs = [] | |
| api_jobs = response.json().get("jobs", []) | |
| for i, job_data in enumerate(api_jobs[:num_jobs]): | |
| job = { | |
| "id": f"api_job_{i}", | |
| "title": job_data.get("title", f"{job_title} - Entry"), | |
| "company": job_data.get("company", f"Company {i+1}"), | |
| "location": job_data.get("location", location), | |
| "description": job_data.get("description", f"Entry-level position for {job_title}. Requirements: {', '.join(skills[:2] if skills else ['Relevant skills'])}."), | |
| "posting_date": job_data.get("posted_date", datetime.now().strftime("%Y-%m-%d")), | |
| "salary_range": job_data.get("salary", "$40,000 - $60,000"), | |
| "application_url": job_data.get("apply_url", f"https://jobboard.example.com/jobs/{i}"), | |
| "email": f"careers@{job_data.get('company', 'company').lower().replace(' ', '')}.com", | |
| "requires_form": random.choice([True, False]) | |
| } | |
| jobs.append(job) | |
| if jobs: | |
| logging.info(f"Retrieved {len(jobs)} jobs from API") | |
| return jobs[:num_jobs] | |
| job_title_encoded = job_title.replace(" ", "%20") | |
| location_encoded = location.replace(" ", "%20") | |
| url = f"https://www.linkedin.com/jobs/search/?keywords={job_title_encoded}&location={location_encoded}&f_E=2" | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" | |
| } | |
| response = requests.get(url, headers=headers, timeout=5) | |
| if response.status_code != 200: | |
| logging.error(f"LinkedIn request failed with status {response.status_code}") | |
| raise Exception(f"HTTP {response.status_code}") | |
| soup = BeautifulSoup(response.text, 'html.parser') | |
| job_cards = soup.find_all('div', class_='base-card')[:num_jobs] | |
| jobs = [] | |
| for i, card in enumerate(job_cards): | |
| title = card.find('h3', class_='base-search-card__title') | |
| company = card.find('h4', class_='base-search-card__subtitle') | |
| job_location = card.find('span', class_='job-search-card__location') | |
| description = card.find('div', class_='show-more-less-html__markup') or card.find('p') | |
| title_text = title.get_text(strip=True) if title else f"{job_title} - Entry" | |
| company_text = company.get_text(strip=True) if company else f"Company {i+1}" | |
| location_text = job_location.get_text(strip=True) if job_location else location | |
| description_text = description.get_text(strip=True)[:500] if description else f"Entry-level position for {job_title}. Requirements: {', '.join(skills[:2] if skills else ['Relevant skills'])}." | |
| email = f"careers@{company_text.lower().replace(' ', '').replace('&', '')}.com" | |
| job = { | |
| "id": f"linkedin_job_{i}", | |
| "title": title_text, | |
| "company": company_text, | |
| "location": location_text, | |
| "description": description_text, | |
| "posting_date": datetime.now().strftime("%Y-%m-%d"), | |
| "salary_range": "$40,000 - $60,000", | |
| "application_url": card.find('a', class_='base-card__full-link')['href'] if card.find('a') else f"https://linkedin.com/jobs/{i}", | |
| "email": email, | |
| "requires_form": random.choice([True, False]) | |
| } | |
| jobs.append(job) | |
| if not jobs: | |
| logging.warning("No jobs found on LinkedIn, falling back to mock data") | |
| raise Exception("No jobs found") | |
| logging.info(f"Scraped {len(jobs)} LinkedIn jobs") | |
| return jobs[:num_jobs] | |
| except Exception as e: | |
| logging.error(f"Error in job search: {str(e)}") | |
| mock_jobs = [] | |
| companies = [ | |
| "TechCorp", "DataSys", "InnoTech", "FutureSoft", "CodeWizards", | |
| "MarketTrend", "GrowEasy", "BrandBoost", | |
| "HealthCarePlus", "MediCare", "WellnessHub", | |
| "FinancePro", "WealthCore", "MoneyWise", | |
| "EduLearn", "SkillAcademy" | |
| ] | |
| job_descriptions = { | |
| "software engineer": f"Seeking an entry-level {job_title} to join our team. Learn and grow with hands-on projects under mentorship.", | |
| "marketing": f"Looking for a creative {job_title} to develop campaigns and engage audiences.", | |
| "nurse": f"Entry-level {job_title} to provide compassionate patient care in a supportive environment.", | |
| "financial analyst": f"Join our team as a {job_title} to analyze financial data and support strategic decisions.", | |
| "teacher": f"Seeking a dedicated {job_title} to inspire students and develop engaging curricula.", | |
| "default": f"Entry-level position for {job_title}. Learn and grow in a dynamic team." | |
| } | |
| field_keywords = { | |
| "software engineer": ["Java", "Python", "JavaScript", "SQL", "HTML", "CSS", "Git"], | |
| "frontend developer": ["JavaScript", "HTML", "CSS", "React"], | |
| "data analyst": ["Python", "SQL", "Excel", "SPSS"], | |
| "systems analyst": ["SQL", "Visual Basic", "Database Management"], | |
| "marketing": ["SEO", "Content Creation", "Social Media", "Branding"], | |
| "nurse": ["Patient Care", "Medical Coding", "Clinical Skills"], | |
| "financial analyst": ["Financial Analysis", "Excel", "Accounting"], | |
| "teacher": ["Curriculum Development", "Classroom Management", "Pedagogy"], | |
| "sales": ["CRM", "Customer Service", "Business Development"], | |
| "graphic designer": ["Adobe Photoshop", "Illustrator", "UI/UX"] | |
| } | |
| job_title_lower = job_title.lower() | |
| relevant_keywords = next( | |
| (v for k, v in field_keywords.items() if k in job_title_lower), | |
| skills[:3] if skills else ["Relevant skills"] | |
| ) | |
| description_template = next( | |
| (v for k, v in job_descriptions.items() if k in job_title_lower), | |
| job_descriptions["default"] | |
| ) | |
| for i in range(num_jobs): | |
| company = random.choice(companies) | |
| job_desc = description_template.format(job_title=job_title) | |
| selected_keywords = random.sample(relevant_keywords, min(2, len(relevant_keywords))) | |
| requirements = f"Requirements: {', '.join(selected_keywords)}." | |
| job = { | |
| "id": f"mock_job_{i}", | |
| "title": f"{job_title} - Entry", | |
| "company": company, | |
| "location": location, | |
| "description": f"{job_desc} {requirements}", | |
| "posting_date": (datetime.now() - timedelta(days=random.randint(1, 7))).strftime("%Y-%m-%d"), | |
| "salary_range": "$40,000 - $60,000", | |
| "application_url": f"https://example.com/jobs/{i}", | |
| "email": f"careers@{company.lower().replace(' ', '')}.com", | |
| "requires_form": random.choice([True, False]) | |
| } | |
| mock_jobs.append(job) | |
| logging.info(f"Fell back to {len(mock_jobs)} mock jobs") | |
| return mock_jobs | |
| # Function to calculate match score | |
| def calculate_match_score(resume_text, job_description): | |
| logging.info("Calculating match score") | |
| try: | |
| resume_lines = resume_text.lower().split('\n') | |
| skills_section = ' '.join([line for line in resume_lines if any(skill in line.lower() for skill in [ | |
| 'java', 'sql', 'javascript', 'python', 'html', 'css', 'react', 'node', 'aws', 'azure', 'docker', 'git', | |
| 'marketing', 'seo', 'finance', 'nursing', 'patient care', 'project management', 'graphic design', 'teaching', 'sales' | |
| ])]) | |
| if not skills_section: | |
| skills_section = resume_text.lower() | |
| logging.warning("No specific skills section found, using full resume text for matching") | |
| resume_embedding = model.encode(skills_section, convert_to_tensor=True) | |
| job_embedding = model.encode(job_description, convert_to_tensor=True) | |
| similarity = cosine_similarity(resume_embedding.cpu().numpy().reshape(1, -1), job_embedding.cpu().numpy().reshape(1, -1))[0][0] | |
| score = similarity * 100 | |
| logging.info(f"Match score calculated: {score}%") | |
| return score | |
| except Exception as e: | |
| logging.error(f"Error calculating match score: {str(e)}") | |
| return 0.0 | |
| # Function to generate entry-level cover letter | |
| def generate_cover_letter(resume_info, job_info): | |
| logging.info(f"Generating cover letter for {job_info['title']}") | |
| company_name = job_info["company"] | |
| job_title = job_info["title"] | |
| skills_text = ", ".join(resume_info["skills"][:2]) if resume_info["skills"] else "relevant skills" | |
| name = resume_info.get('personal_info', {}).get('name', 'Your Name') | |
| templates = [ | |
| f"""Dear Hiring Manager at {company_name}, | |
| I am excited to apply for the {job_title} position. With skills in {skills_text}, I am eager to contribute to your team and grow in a dynamic environment. | |
| {company_name}'s mission inspires me, and I am committed to delivering value in an entry-level role. | |
| Thank you for considering my application. I look forward to discussing how I can contribute. | |
| Sincerely, | |
| {name}""" | |
| ] | |
| return random.choice(templates) | |
| # Function to generate job application form | |
| def generate_job_form(resume_info, job_info): | |
| logging.info(f"Generating job form for {job_info['id']}") | |
| personal_info = resume_info.get("personal_info", {}) | |
| address = personal_info.get("address", "") | |
| city_state_zip = address.split(",")[-1].strip() if address else "" | |
| city = city_state_zip.split()[:-2] if city_state_zip else [] | |
| state_zip = city_state_zip.split()[-2:] if city_state_zip else ["", ""] | |
| state = state_zip[0] if state_zip else "" | |
| zip_code = state_zip[1] if len(state_zip) > 1 else "" | |
| return { | |
| "job_title": job_info["title"], | |
| "company": job_info["company"], | |
| "application_date": datetime.now().strftime("%Y-%m-%d"), | |
| "personal_info": { | |
| "name": personal_info.get("name", ""), | |
| "email": personal_info.get("email", ""), | |
| "phone": personal_info.get("phone", ""), | |
| "address": address.split(",")[0] if address else "", | |
| "city": " ".join(city) if city else "", | |
| "state": state, | |
| "zip": zip_code, | |
| "country": "USA" | |
| }, | |
| "experience": { | |
| "react_js": resume_info.get("react_experience", "0"), | |
| "redux_js": resume_info.get("redux_experience", "0"), | |
| "javascript": resume_info.get("javascript_experience", "0") | |
| }, | |
| "preferences": { | |
| "onsite_work": "Yes", | |
| "commuting": "Yes", | |
| "relocation": "Yes", | |
| "remote_work": "Yes" | |
| }, | |
| "education": resume_info.get("education", []), | |
| "skills": resume_info.get("skills", []), | |
| "work_history": resume_info.get("work_history", []) | |
| } | |
| # Function to save job application form | |
| def save_job_form(form_data, job_id): | |
| logging.info(f"Saving job form for {job_id}") | |
| filename = f"job_application_form_{job_id}.json" | |
| try: | |
| with open(filename, "w") as f: | |
| json.dump(form_data, f, indent=2) | |
| return filename | |
| except Exception as e: | |
| logging.error(f"Error saving form: {str(e)}") | |
| return None | |
| # Function to test SMTP login | |
| def test_smtp_login(user_email, user_password): | |
| logging.info(f"Testing SMTP login for {user_email}") | |
| user_password = user_password.strip() | |
| if len(user_password) != 16: | |
| logging.error(f"Invalid app-specific password length: {len(user_password)} characters") | |
| return False, "SMTP login failed: App-specific password must be exactly 16 characters. Generate a new one at https://myaccount.google.com/security > App passwords > Select app: Mail > Generate." | |
| if not re.match(r'^[a-zA-Z0-9]+$', user_password): | |
| logging.error("Invalid app-specific password format: contains invalid characters") | |
| return False, "SMTP login failed: App-specific password contains invalid characters. Use only letters and numbers." | |
| try: | |
| with smtplib.SMTP('smtp.gmail.com', 587, timeout=5) as server: | |
| server.starttls() | |
| server.login(user_email, user_password) | |
| logging.info("SMTP login successful") | |
| return True, "SMTP login successful" | |
| except smtplib.SMTPAuthenticationError: | |
| logging.error("SMTP authentication failed: Invalid email or password") | |
| return False, "SMTP login failed: Invalid email or app-specific password. Ensure 2-Factor Authentication is enabled (https://myaccount.google.com/security > 2-Step Verification) and use a new app-specific password." | |
| except Exception as e: | |
| logging.error(f"SMTP login failed: {str(e)}") | |
| return False, f"SMTP login failed: {str(e)}. Check network connection or try again later." | |
| # Function to send application email | |
| def send_application(resume_file_path, cover_letter, job_info, user_email, user_password, form_data=None): | |
| logging.info(f"Sending application to {job_info['email']}") | |
| try: | |
| msg = MIMEMultipart() | |
| msg['From'] = user_email | |
| msg['To'] = job_info['email'] | |
| msg['Subject'] = f"Application for {job_info['title']} - {resume_info['personal_info']['name']}" | |
| msg.attach(MIMEText(cover_letter, 'plain')) | |
| with open(resume_file_path, 'rb') as f: | |
| resume_attachment = MIMEApplication(f.read(), _subtype='pdf') | |
| resume_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(resume_file_path)) | |
| msg.attach(resume_attachment) | |
| if form_data: | |
| form_filename = save_job_form(form_data, job_info['id']) | |
| if form_filename: | |
| with open(form_filename, 'rb') as f: | |
| form_attachment = MIMEApplication(f.read(), _subtype='json') | |
| form_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(form_filename)) | |
| msg.attach(form_attachment) | |
| with smtplib.SMTP('smtp.gmail.com', 587, timeout=5) as server: | |
| server.starttls() | |
| server.login(user_email, user_password.strip()) | |
| server.sendmail(user_email, job_info['email'], msg.as_string()) | |
| logging.info(f"Application sent successfully to {job_info['email']}") | |
| return { | |
| "status": "success", | |
| "message": "Application sent successfully", | |
| "to": job_info["email"], | |
| "from": user_email, | |
| "subject": msg['Subject'], | |
| "body": cover_letter, | |
| "resume_attached": True, | |
| "form_attached": form_data is not None, | |
| "sent_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| except Exception as e: | |
| logging.error(f"Error sending email: {str(e)}") | |
| return { | |
| "status": "error", | |
| "message": f"Failed to send email: {str(e)}", | |
| "to": job_info["email"], | |
| "from": user_email, | |
| "subject": f"Application for {job_info['title']}", | |
| "body": cover_letter, | |
| "resume_attached": True, | |
| "form_attached": form_data is not None, | |
| "sent_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| # Function to predict interview likelihood | |
| def predict_interview_likelihood(match_score): | |
| if match_score > 85: | |
| return "Very High" | |
| elif match_score > 70: | |
| return "High" | |
| elif match_score > 50: | |
| return "Medium" | |
| else: | |
| return "Low" | |
| # Function to simulate interview scheduling | |
| def schedule_interviews(applications, min_interviews=5): | |
| logging.info("Scheduling mock interviews") | |
| interview_candidates = random.sample(applications, min(max(min_interviews, int(len(applications) * 0.2)), len(applications))) | |
| interview_schedule = [] | |
| start_date = datetime.now() + timedelta(days=1) | |
| time_slots = [ | |
| "09:00 AM", "10:00 AM", "11:00 AM", "01:00 PM", "02:00 PM", "03:00 PM" | |
| ] | |
| for i, app in enumerate(interview_candidates): | |
| job = app["job"] | |
| interview_date = (start_date + timedelta(days=i // len(time_slots))).strftime("%Y-%m-%d") | |
| interview_schedule.append({ | |
| "company": job["company"], | |
| "job_title": job["title"], | |
| "date": interview_date, | |
| "time": time_slots[i % len(time_slots)], | |
| "email": job["email"], | |
| "status": "Scheduled (Mock)" | |
| }) | |
| logging.info(f"Scheduled {len(interview_schedule)} mock interviews") | |
| return interview_schedule | |
| # Main application processing function | |
| def process_application(resume_file, job_title, location, user_email, user_password, num_applications=5, progress=gr.Progress()): | |
| global resume_info | |
| progress(0, desc="Starting processing...") | |
| try: | |
| progress(0.1, desc="Validating inputs...") | |
| if not all([resume_file, job_title, location, user_email, user_password]): | |
| return {"error": "All fields are required"} | |
| if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", user_email): | |
| return {"error": "Invalid email format"} | |
| if not isinstance(num_applications, int) or num_applications < 1 or num_applications > 50: | |
| return {"error": "Number of applications must be between 1 and 50"} | |
| if not resume_file or not isinstance(resume_file, str) or not resume_file.lower().endswith('.pdf'): | |
| return {"error": "Resume must be a valid PDF file path"} | |
| progress(0.2, desc="Testing SMTP login...") | |
| smtp_success, smtp_message = test_smtp_login(user_email, user_password) | |
| if not smtp_success: | |
| return {"error": smtp_message} | |
| progress(0.3, desc="Processing resume...") | |
| resume_text = extract_resume_text(resume_file) | |
| resume_info = parse_resume(resume_text) | |
| progress(0.4, desc="Searching jobs...") | |
| jobs = search_jobs(job_title, location, num_applications, resume_info["skills"]) | |
| results = [] | |
| for i, job in enumerate(jobs): | |
| progress(0.5 + (i / len(jobs)) * 0.4, desc=f"Processing application {i+1}/{len(jobs)}...") | |
| match_score = calculate_match_score(resume_text, job["description"]) | |
| cover_letter = generate_cover_letter(resume_info, job) | |
| form_data = generate_job_form(resume_info, job) if job.get("requires_form", False) else None | |
| if form_data: | |
| form_filename = save_job_form(form_data, job["id"]) | |
| job["form_filename"] = form_filename | |
| application_result = send_application(resume_file, cover_letter, job, user_email, user_password, form_data) | |
| results.append({ | |
| "job": job, | |
| "match_score": round(match_score, 2), | |
| "interview_likelihood": predict_interview_likelihood(match_score), | |
| "application_status": application_result["status"], | |
| "application_message": application_result.get("message", ""), | |
| "form_data": form_data | |
| }) | |
| progress(0.9, desc="Scheduling interviews...") | |
| results.sort(key=lambda x: x["match_score"], reverse=True) | |
| interview_schedule = schedule_interviews(results) | |
| progress(1.0, desc="Finalizing results...") | |
| return { | |
| "resume_info": resume_info, | |
| "results": results, | |
| "interview_schedule": interview_schedule, | |
| "total_applications": len(results), | |
| "successful_applications": sum(1 for r in results if r["application_status"] == "success"), | |
| "failed_applications": sum(1 for r in results if r["application_status"] == "error"), | |
| "top_match_score": results[0]["match_score"] if results else 0, | |
| "forms_generated": sum(1 for r in results if r.get("form_data") is not None) | |
| } | |
| except Exception as e: | |
| logging.error(f"Error processing application: {str(e)}") | |
| return { | |
| "error": str(e), | |
| "resume_info": None, | |
| "results": [], | |
| "interview_schedule": [], | |
| "total_applications": 0, | |
| "successful_applications": 0, | |
| "failed_applications": 0, | |
| "top_match_score": 0, | |
| "forms_generated": 0 | |
| } | |
| # Function to format results | |
| def format_results(results): | |
| logging.info("Formatting results") | |
| if "error" in results and results["error"]: | |
| return f"Error: {results['error']}\n\n**Troubleshooting**:\n- **SMTP Error**: Follow these steps:\n 1. Enable 2-Factor Authentication: https://myaccount.google.com/security > 2-Step Verification.\n 2. Generate an app-specific password: https://myaccount.google.com/security > App passwords > Select app: Mail > Generate.\n 3. Enter the 16-character password without spaces.\n- **No Jobs Found**: Job board API or LinkedIn may have blocked the request. Try reducing the number of applications or wait 5 minutes." | |
| resume_info = results["resume_info"] | |
| application_results = results["results"] | |
| interview_schedule = results["interview_schedule"] | |
| output = "## Resume Analysis\n" | |
| output += f"- Name: {resume_info.get('personal_info', {}).get('name', 'Not found')}\n" | |
| output += f"- Email: {resume_info.get('personal_info', {}).get('email', 'Not found')}\n" | |
| output += f"- Phone: {resume_info.get('personal_info', {}).get('phone', 'Not found')}\n" | |
| output += f"- Address: {resume_info.get('personal_info', {}).get('address', 'Not found')}\n" | |
| output += f"- Skills: {', '.join(resume_info['skills']) or 'None'}\n" | |
| output += f"- Education: {', '.join(resume_info['education']) or 'None'}\n" | |
| output += f"- Experience: {', '.join(resume_info['experience']) or 'None'}\n" | |
| output += "\n## Application Results\n" | |
| output += f"- Total Applications: {results['total_applications']}\n" | |
| output += f"- Successful: {results['successful_applications']}\n" | |
| output += f"- Failed: {results['failed_applications']}\n" | |
| output += f"- Top Match Score: {results['top_match_score']}%\n" | |
| output += f"- Forms Generated: {results['forms_generated']}\n" | |
| output += f"- Scheduled Interviews: {len(interview_schedule)} (Note: These are mock schedules pending real company responses)\n\n" | |
| output += "## Interview Schedule\n" | |
| for i, interview in enumerate(interview_schedule, 1): | |
| output += f"### {i}. {interview['job_title']} at {interview['company']}\n" | |
| output += f"- Date: {interview['date']}\n" | |
| output += f"- Time: {interview['time']}\n" | |
| output += f"- Email: {interview['email']}\n" | |
| output += f"- Status: {interview['status']}\n\n" | |
| output += "## Job Matches\n" | |
| for i, result in enumerate(application_results, 1): | |
| job = result["job"] | |
| output += f"### {i}. {job['title']} at {job['company']}\n" | |
| output += f"- Location: {job['location']}\n" | |
| output += f"- Match Score: {result['match_score']}%\n" | |
| output += f"- Interview Likelihood: {result['interview_likelihood']}\n" | |
| output += f"- Status: {result['application_status'].upper()}\n" | |
| if job.get("requires_form", False): | |
| output += f"- Form: {job.get('form_filename', 'Generated')}\n" | |
| if result["application_status"] == "error": | |
| output += f"- Error: {result['application_message']}\n" | |
| output += f"- Email: {job['email']}\n" | |
| output += f"- Description: {job['description']}\n" | |
| output += f"- Applied: {datetime.now().strftime('%Y-%m-%d')}\n\n" | |
| output += "## Download Generated Files\n" | |
| form_files = [f for f in os.listdir('.') if f.startswith("job_application_form_") and f.endswith(".json")] | |
| for form_file in form_files: | |
| output += f"- [{form_file}](./{form_file})\n" | |
| if os.path.exists("application_log.txt"): | |
| output += f"- [Application Log](./application_log.txt)\n" | |
| logging.info("Results formatted") | |
| return output | |
| # Gradio interface | |
| def gradio_interface(resume_file, job_title, location, user_email, user_password, num_applications): | |
| logging.info("Starting Gradio interface processing") | |
| try: | |
| num_applications = int(num_applications) if num_applications else 5 | |
| resume_path = "resume.pdf" | |
| if resume_file is None: | |
| return "Error: No resume file uploaded. Please upload a PDF file." | |
| with open(resume_path, "wb") as f: | |
| f.write(resume_file.data) | |
| results = process_application(resume_path, job_title, location, user_email, user_password, num_applications) | |
| return format_results(results) | |
| except ValueError: | |
| logging.error("Invalid number of applications") | |
| return "Error: Number of applications must be an integer between 1 and 50." | |
| except Exception as e: | |
| logging.error(f"Gradio interface error: {str(e)}") | |
| return f"Error: {str(e)}" | |
| # Launch Gradio interface | |
| iface = gr.Interface( | |
| fn=gradio_interface, | |
| inputs=[ | |
| gr.File(label="Upload Resume (PDF)", file_types=[".pdf"]), | |
| gr.Textbox(label="Job Title (e.g., Software Engineer, Marketing Coordinator, Nurse)", placeholder="Enter any job title"), | |
| gr.Textbox(label="Location (e.g., India, New York, NY)", placeholder="India"), | |
| gr.Textbox(label="Your Gmail Address", placeholder="example@gmail.com"), | |
| gr.Textbox(label="Your Gmail App-Specific Password (16 characters, no spaces)", type="password"), | |
| gr.Number(label="Number of Applications (default 5)", value=5, minimum=1, maximum=50) | |
| ], | |
| outputs=gr.Markdown(label="Results"), | |
| title="Job Application Automator", | |
| description="Upload your resume and apply to entry-level jobs in any field. **Important**: To generate a Gmail app-specific password:\n1. Enable 2-Factor Authentication: https://myaccount.google.com/security > 2-Step Verification.\n2. Generate an app-specific password: https://myaccount.google.com/security > App passwords > Select app: Mail > Select device: Other > Generate.\n3. Use the 16-character password without spaces." | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() |