Anupam007 commited on
Commit
00314eb
·
verified ·
1 Parent(s): 18fc9d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +678 -7
app.py CHANGED
@@ -1,4 +1,3 @@
1
- # app.py
2
  import os
3
  import re
4
  import json
@@ -21,9 +20,18 @@ from email.mime.application import MIMEApplication
21
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
22
  logging.getLogger().addHandler(logging.FileHandler("application_log.txt"))
23
 
 
 
 
 
 
24
  # Set up GPU if available
25
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
26
- logging.info(f"Using device: {device}")
 
 
 
 
27
 
28
  # Initialize the sentence transformer model
29
  @torch.no_grad()
@@ -38,18 +46,681 @@ def initialize_model():
38
 
39
  model = initialize_model()
40
 
41
- # [Include your functions here: extract_resume_text, parse_resume, authenticate_job_board, search_jobs, calculate_match_score, generate_cover_letter, generate_job_form, save_job_form, test_smtp_login, send_application, predict_interview_likelihood, schedule_interviews, process_application, format_results]
42
- # For brevity, copy the functions from your original code, excluding Colab-specific parts like files.download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  # Gradio interface
45
  def gradio_interface(resume_file, job_title, location, user_email, user_password, num_applications):
46
  logging.info("Starting Gradio interface processing")
47
  try:
48
  num_applications = int(num_applications) if num_applications else 5
49
- # Save the uploaded resume file
50
  resume_path = "resume.pdf"
 
 
51
  with open(resume_path, "wb") as f:
52
- f.write(resume_file.read())
53
  results = process_application(resume_path, job_title, location, user_email, user_password, num_applications)
54
  return format_results(results)
55
  except ValueError:
 
 
1
  import os
2
  import re
3
  import json
 
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
  logging.getLogger().addHandler(logging.FileHandler("application_log.txt"))
22
 
23
+ # Set up authentication keys as environment variables
24
+ os.environ['CLIENT_ID'] = '78iccqej5ala77'
25
+ os.environ['CLIENT_SECRET'] = 'WPL_AP1.TQCswIWpXAXUOKeQ.8EwVvA==' # Replace with actual 32-character secret
26
+ logging.info("Authentication keys set as environment variables")
27
+
28
  # Set up GPU if available
29
+ if torch.cuda.is_available():
30
+ device = torch.device("cuda")
31
+ logging.info(f"Using GPU: {torch.cuda.get_device_name(0)}")
32
+ else:
33
+ device = torch.device("cpu")
34
+ logging.info("GPU not available, using CPU instead")
35
 
36
  # Initialize the sentence transformer model
37
  @torch.no_grad()
 
46
 
47
  model = initialize_model()
48
 
49
+ # Function to extract text from a PDF resume
50
+ def extract_resume_text(pdf_file_path):
51
+ logging.info("Extracting resume text")
52
+ try:
53
+ with open(pdf_file_path, 'rb') as f:
54
+ pdf_reader = PdfReader(f)
55
+ text = ""
56
+ for page in pdf_reader.pages:
57
+ extracted = page.extract_text()
58
+ if extracted:
59
+ text += extracted
60
+ if not text.strip():
61
+ raise Exception("No text extracted from PDF. Ensure the PDF is not image-based.")
62
+ logging.info(f"Extracted resume text (first 200 chars): {text[:200]}")
63
+ return text
64
+ except Exception as e:
65
+ logging.error(f"Error extracting text from PDF: {str(e)}")
66
+ raise Exception(f"Error extracting text from PDF: {str(e)}")
67
+
68
+ # Function to parse resume and extract key information
69
+ def parse_resume(resume_text):
70
+ logging.info("Parsing resume")
71
+ parsed_info = {
72
+ "skills": [],
73
+ "education": [],
74
+ "experience": [],
75
+ "personal_info": {},
76
+ "react_experience": "0",
77
+ "redux_experience": "0",
78
+ "javascript_experience": "0",
79
+ "education_details": [],
80
+ "work_history": []
81
+ }
82
+
83
+ # Split resume into sections based on candidate headers
84
+ candidate_pattern = r'(IM A\. SAMPLE [IVX]+)\s*'
85
+ candidate_sections = re.split(candidate_pattern, resume_text, flags=re.IGNORECASE)
86
+ candidates = []
87
+ for i in range(1, len(candidate_sections), 2):
88
+ candidates.append((candidate_sections[i], candidate_sections[i+1]))
89
+
90
+ if not candidates:
91
+ candidates = [("Unknown Candidate", resume_text)]
92
+
93
+ candidate_name, candidate_text = candidates[0]
94
+ parsed_info["personal_info"]["name"] = candidate_name.strip()
95
+ logging.info(f"Parsed candidate name: {candidate_name}")
96
+
97
+ # Extract email
98
+ email_pattern = r'[\w\.-]+@[\w\.-]+\.\w+'
99
+ email_matches = re.findall(email_pattern, candidate_text, re.IGNORECASE)
100
+ if email_matches:
101
+ parsed_info["personal_info"]["email"] = email_matches[0]
102
+ else:
103
+ logging.warning("No email found in resume")
104
+
105
+ # Extract phone number
106
+ phone_pattern = r'\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}'
107
+ phone_matches = re.findall(phone_pattern, candidate_text)
108
+ if phone_matches:
109
+ parsed_info["personal_info"]["phone"] = phone_matches[0]
110
+ else:
111
+ logging.warning("No phone number found in resume")
112
+
113
+ # Extract address
114
+ address_pattern = r'(\d+\s+[A-Za-z\s]+,\s*[A-Za-z\s]+,\s*[A-Z]{2}\s*\d{5})'
115
+ address_matches = re.findall(address_pattern, candidate_text, re.IGNORECASE)
116
+ if address_matches:
117
+ parsed_info["personal_info"]["address"] = address_matches[0]
118
+ else:
119
+ parsed_info["personal_info"]["address"] = "Not found"
120
+ logging.warning("No address found in resume")
121
+
122
+ # Expanded skill keywords for various fields
123
+ skill_keywords = [
124
+ "python", "java", "javascript", "html", "css", "sql", "react", "node", "aws", "azure",
125
+ "docker", "git", "c++", "visual basic", "perl", "asp", "php", "cobol", "xml", "asp.net",
126
+ "quickbooks", "ms office", "ms access", "spss", "typescript", "angular", "vue", "mysql",
127
+ "mongodb", "linux", "bash", "kubernetes", "jenkins",
128
+ "marketing", "digital marketing", "seo", "content creation", "social media", "branding",
129
+ "finance", "accounting", "financial analysis", "bookkeeping", "tax preparation",
130
+ "nursing", "patient care", "medical coding", "pharmacy", "clinical research",
131
+ "project management", "agile", "scrum", "leadership", "team management",
132
+ "graphic design", "ui/ux", "adobe photoshop", "illustrator", "canva",
133
+ "teaching", "curriculum development", "classroom management",
134
+ "sales", "customer service", "crm", "business development",
135
+ "writing", "editing", "technical writing", "grant writing"
136
+ ]
137
+ resume_lower = candidate_text.lower()
138
+ for skill in skill_keywords:
139
+ if skill.lower() in resume_lower or f"{skill.lower()} " in resume_lower:
140
+ parsed_info["skills"].append(skill)
141
+ if not parsed_info["skills"]:
142
+ logging.warning("No skills extracted from resume")
143
+
144
+ # Extract specific experience (technical fields only for now)
145
+ patterns = {
146
+ "react_experience": r'(\d+)[\s\+]*(years?|yrs?)[\s\+]*(?:of)?[\s\+]*(?:experience)?[\s\+]*(?:with|in)?[\s\+]*React',
147
+ "redux_experience": r'(\d+)[\s\+]*(years?|yrs?)[\s\+]*(?:of)?[\s\+]*(?:experience)?[\s\+]*(?:with|in)?[\s\+]*Redux',
148
+ "javascript_experience": r'(\d+)[\s\+]*(years?|yrs?)[\s\+]*(?:of)?[\s\+]*(?:experience)?[\s\+]*(?:with|in)?[\s\+]*(?:JavaScript|JS)'
149
+ }
150
+
151
+ for key, pattern in patterns.items():
152
+ matches = re.findall(pattern, candidate_text, re.IGNORECASE)
153
+ if matches:
154
+ parsed_info[key] = matches[0][0]
155
+ else:
156
+ logging.debug(f"No {key} found in resume")
157
+
158
+ # Extract education
159
+ 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))'
160
+ education_matches = re.findall(education_pattern, candidate_text)
161
+ parsed_info["education_details"] = [
162
+ {"degree": deg, "institution": inst.strip(), "year": year.strip()}
163
+ for deg, inst, year in education_matches
164
+ ]
165
+ parsed_info["education"] = [f"{edu['degree']} from {edu['institution']} ({edu['year']})" for edu in parsed_info["education_details"]]
166
+ if not parsed_info["education"]:
167
+ logging.warning("No education details extracted from resume")
168
+
169
+ # Extract experience periods
170
+ experience_pattern = r'(?i)(\d{4})\s*(?:-|to)\s*(present|\d{4})'
171
+ experience_matches = re.findall(experience_pattern, candidate_text)
172
+ parsed_info["experience"] = [f"{start}-{end}" for start, end in experience_matches]
173
+ if not parsed_info["experience"]:
174
+ logging.warning("No experience periods extracted from resume")
175
+
176
+ # Extract work history details
177
+ 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)\)'
178
+ work_history_matches = re.findall(work_history_pattern, candidate_text)
179
+ parsed_info["work_history"] = [
180
+ {"role": role.strip(), "company": company.strip(), "location": location.strip(), "years": years.strip()}
181
+ for role, company, location, years in work_history_matches
182
+ ]
183
+ if not parsed_info["work_history"]:
184
+ logging.warning("No work history extracted from resume")
185
+
186
+ logging.info(f"Parsed resume info: {json.dumps(parsed_info, indent=2)}")
187
+ return parsed_info
188
+
189
+ # Function to authenticate with job board API
190
+ def authenticate_job_board():
191
+ logging.info("Authenticating with job board API")
192
+ try:
193
+ client_id = os.environ.get('CLIENT_ID')
194
+ client_secret = os.environ.get('CLIENT_SECRET')
195
+ if not client_id or not client_secret:
196
+ logging.error("Missing Client ID or Client Secret")
197
+ raise Exception("Authentication failed: Missing Client ID or Client Secret")
198
+
199
+ auth_url = "https://api.jobboard.example.com/oauth/token" # Replace with actual API
200
+ payload = {
201
+ "client_id": client_id,
202
+ "client_secret": client_secret,
203
+ "grant_type": "client_credentials"
204
+ }
205
+ response = requests.post(auth_url, data=payload, timeout=5)
206
+ if response.status_code == 200:
207
+ access_token = response.json().get("access_token")
208
+ logging.info("API authentication successful")
209
+ return access_token
210
+ else:
211
+ logging.error(f"API authentication failed: HTTP {response.status_code}")
212
+ raise Exception(f"API authentication failed: HTTP {response.status_code}")
213
+ except Exception as e:
214
+ logging.error(f"Error during API authentication: {str(e)}")
215
+ return None
216
+
217
+ # Function to scrape LinkedIn jobs or use job board API
218
+ def search_jobs(job_title, location, num_jobs=5, skills=[]):
219
+ logging.info(f"Searching jobs for {job_title} in {location}")
220
+ try:
221
+ access_token = authenticate_job_board()
222
+ if access_token:
223
+ job_api_url = f"https://api.jobboard.example.com/jobs?query={job_title}&location={location}&limit={num_jobs}"
224
+ headers = {
225
+ "Authorization": f"Bearer {access_token}",
226
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124"
227
+ }
228
+ response = requests.get(job_api_url, headers=headers, timeout=5)
229
+ if response.status_code == 200:
230
+ jobs = []
231
+ api_jobs = response.json().get("jobs", [])
232
+ for i, job_data in enumerate(api_jobs[:num_jobs]):
233
+ job = {
234
+ "id": f"api_job_{i}",
235
+ "title": job_data.get("title", f"{job_title} - Entry"),
236
+ "company": job_data.get("company", f"Company {i+1}"),
237
+ "location": job_data.get("location", location),
238
+ "description": job_data.get("description", f"Entry-level position for {job_title}. Requirements: {', '.join(skills[:2] if skills else ['Relevant skills'])}."),
239
+ "posting_date": job_data.get("posted_date", datetime.now().strftime("%Y-%m-%d")),
240
+ "salary_range": job_data.get("salary", "$40,000 - $60,000"),
241
+ "application_url": job_data.get("apply_url", f"https://jobboard.example.com/jobs/{i}"),
242
+ "email": f"careers@{job_data.get('company', 'company').lower().replace(' ', '')}.com",
243
+ "requires_form": random.choice([True, False])
244
+ }
245
+ jobs.append(job)
246
+ if jobs:
247
+ logging.info(f"Retrieved {len(jobs)} jobs from API")
248
+ return jobs[:num_jobs]
249
+
250
+ job_title_encoded = job_title.replace(" ", "%20")
251
+ location_encoded = location.replace(" ", "%20")
252
+ url = f"https://www.linkedin.com/jobs/search/?keywords={job_title_encoded}&location={location_encoded}&f_E=2"
253
+ headers = {
254
+ "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"
255
+ }
256
+
257
+ response = requests.get(url, headers=headers, timeout=5)
258
+ if response.status_code != 200:
259
+ logging.error(f"LinkedIn request failed with status {response.status_code}")
260
+ raise Exception(f"HTTP {response.status_code}")
261
+
262
+ soup = BeautifulSoup(response.text, 'html.parser')
263
+ job_cards = soup.find_all('div', class_='base-card')[:num_jobs]
264
+ jobs = []
265
+
266
+ for i, card in enumerate(job_cards):
267
+ title = card.find('h3', class_='base-search-card__title')
268
+ company = card.find('h4', class_='base-search-card__subtitle')
269
+ job_location = card.find('span', class_='job-search-card__location')
270
+ description = card.find('div', class_='show-more-less-html__markup') or card.find('p')
271
+
272
+ title_text = title.get_text(strip=True) if title else f"{job_title} - Entry"
273
+ company_text = company.get_text(strip=True) if company else f"Company {i+1}"
274
+ location_text = job_location.get_text(strip=True) if job_location else location
275
+ 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'])}."
276
+
277
+ email = f"careers@{company_text.lower().replace(' ', '').replace('&', '')}.com"
278
+
279
+ job = {
280
+ "id": f"linkedin_job_{i}",
281
+ "title": title_text,
282
+ "company": company_text,
283
+ "location": location_text,
284
+ "description": description_text,
285
+ "posting_date": datetime.now().strftime("%Y-%m-%d"),
286
+ "salary_range": "$40,000 - $60,000",
287
+ "application_url": card.find('a', class_='base-card__full-link')['href'] if card.find('a') else f"https://linkedin.com/jobs/{i}",
288
+ "email": email,
289
+ "requires_form": random.choice([True, False])
290
+ }
291
+ jobs.append(job)
292
+
293
+ if not jobs:
294
+ logging.warning("No jobs found on LinkedIn, falling back to mock data")
295
+ raise Exception("No jobs found")
296
+
297
+ logging.info(f"Scraped {len(jobs)} LinkedIn jobs")
298
+ return jobs[:num_jobs]
299
+ except Exception as e:
300
+ logging.error(f"Error in job search: {str(e)}")
301
+ mock_jobs = []
302
+ companies = [
303
+ "TechCorp", "DataSys", "InnoTech", "FutureSoft", "CodeWizards",
304
+ "MarketTrend", "GrowEasy", "BrandBoost",
305
+ "HealthCarePlus", "MediCare", "WellnessHub",
306
+ "FinancePro", "WealthCore", "MoneyWise",
307
+ "EduLearn", "SkillAcademy"
308
+ ]
309
+ job_descriptions = {
310
+ "software engineer": f"Seeking an entry-level {job_title} to join our team. Learn and grow with hands-on projects under mentorship.",
311
+ "marketing": f"Looking for a creative {job_title} to develop campaigns and engage audiences.",
312
+ "nurse": f"Entry-level {job_title} to provide compassionate patient care in a supportive environment.",
313
+ "financial analyst": f"Join our team as a {job_title} to analyze financial data and support strategic decisions.",
314
+ "teacher": f"Seeking a dedicated {job_title} to inspire students and develop engaging curricula.",
315
+ "default": f"Entry-level position for {job_title}. Learn and grow in a dynamic team."
316
+ }
317
+ field_keywords = {
318
+ "software engineer": ["Java", "Python", "JavaScript", "SQL", "HTML", "CSS", "Git"],
319
+ "frontend developer": ["JavaScript", "HTML", "CSS", "React"],
320
+ "data analyst": ["Python", "SQL", "Excel", "SPSS"],
321
+ "systems analyst": ["SQL", "Visual Basic", "Database Management"],
322
+ "marketing": ["SEO", "Content Creation", "Social Media", "Branding"],
323
+ "nurse": ["Patient Care", "Medical Coding", "Clinical Skills"],
324
+ "financial analyst": ["Financial Analysis", "Excel", "Accounting"],
325
+ "teacher": ["Curriculum Development", "Classroom Management", "Pedagogy"],
326
+ "sales": ["CRM", "Customer Service", "Business Development"],
327
+ "graphic designer": ["Adobe Photoshop", "Illustrator", "UI/UX"]
328
+ }
329
+
330
+ job_title_lower = job_title.lower()
331
+ relevant_keywords = next(
332
+ (v for k, v in field_keywords.items() if k in job_title_lower),
333
+ skills[:3] if skills else ["Relevant skills"]
334
+ )
335
+ description_template = next(
336
+ (v for k, v in job_descriptions.items() if k in job_title_lower),
337
+ job_descriptions["default"]
338
+ )
339
+
340
+ for i in range(num_jobs):
341
+ company = random.choice(companies)
342
+ job_desc = description_template.format(job_title=job_title)
343
+ selected_keywords = random.sample(relevant_keywords, min(2, len(relevant_keywords)))
344
+ requirements = f"Requirements: {', '.join(selected_keywords)}."
345
+
346
+ job = {
347
+ "id": f"mock_job_{i}",
348
+ "title": f"{job_title} - Entry",
349
+ "company": company,
350
+ "location": location,
351
+ "description": f"{job_desc} {requirements}",
352
+ "posting_date": (datetime.now() - timedelta(days=random.randint(1, 7))).strftime("%Y-%m-%d"),
353
+ "salary_range": "$40,000 - $60,000",
354
+ "application_url": f"https://example.com/jobs/{i}",
355
+ "email": f"careers@{company.lower().replace(' ', '')}.com",
356
+ "requires_form": random.choice([True, False])
357
+ }
358
+ mock_jobs.append(job)
359
+ logging.info(f"Fell back to {len(mock_jobs)} mock jobs")
360
+ return mock_jobs
361
+
362
+ # Function to calculate match score
363
+ def calculate_match_score(resume_text, job_description):
364
+ logging.info("Calculating match score")
365
+ try:
366
+ resume_lines = resume_text.lower().split('\n')
367
+ skills_section = ' '.join([line for line in resume_lines if any(skill in line.lower() for skill in [
368
+ 'java', 'sql', 'javascript', 'python', 'html', 'css', 'react', 'node', 'aws', 'azure', 'docker', 'git',
369
+ 'marketing', 'seo', 'finance', 'nursing', 'patient care', 'project management', 'graphic design', 'teaching', 'sales'
370
+ ])])
371
+ if not skills_section:
372
+ skills_section = resume_text.lower()
373
+ logging.warning("No specific skills section found, using full resume text for matching")
374
+
375
+ resume_embedding = model.encode(skills_section, convert_to_tensor=True)
376
+ job_embedding = model.encode(job_description, convert_to_tensor=True)
377
+ similarity = cosine_similarity(resume_embedding.cpu().numpy().reshape(1, -1), job_embedding.cpu().numpy().reshape(1, -1))[0][0]
378
+ score = similarity * 100
379
+ logging.info(f"Match score calculated: {score}%")
380
+ return score
381
+ except Exception as e:
382
+ logging.error(f"Error calculating match score: {str(e)}")
383
+ return 0.0
384
+
385
+ # Function to generate entry-level cover letter
386
+ def generate_cover_letter(resume_info, job_info):
387
+ logging.info(f"Generating cover letter for {job_info['title']}")
388
+ company_name = job_info["company"]
389
+ job_title = job_info["title"]
390
+ skills_text = ", ".join(resume_info["skills"][:2]) if resume_info["skills"] else "relevant skills"
391
+ name = resume_info.get('personal_info', {}).get('name', 'Your Name')
392
+
393
+ templates = [
394
+ f"""Dear Hiring Manager at {company_name},
395
+
396
+ 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.
397
+
398
+ {company_name}'s mission inspires me, and I am committed to delivering value in an entry-level role.
399
+
400
+ Thank you for considering my application. I look forward to discussing how I can contribute.
401
+
402
+ Sincerely,
403
+ {name}"""
404
+ ]
405
+ return random.choice(templates)
406
+
407
+ # Function to generate job application form
408
+ def generate_job_form(resume_info, job_info):
409
+ logging.info(f"Generating job form for {job_info['id']}")
410
+ personal_info = resume_info.get("personal_info", {})
411
+ address = personal_info.get("address", "")
412
+ city_state_zip = address.split(",")[-1].strip() if address else ""
413
+ city = city_state_zip.split()[:-2] if city_state_zip else []
414
+ state_zip = city_state_zip.split()[-2:] if city_state_zip else ["", ""]
415
+ state = state_zip[0] if state_zip else ""
416
+ zip_code = state_zip[1] if len(state_zip) > 1 else ""
417
+
418
+ return {
419
+ "job_title": job_info["title"],
420
+ "company": job_info["company"],
421
+ "application_date": datetime.now().strftime("%Y-%m-%d"),
422
+ "personal_info": {
423
+ "name": personal_info.get("name", ""),
424
+ "email": personal_info.get("email", ""),
425
+ "phone": personal_info.get("phone", ""),
426
+ "address": address.split(",")[0] if address else "",
427
+ "city": " ".join(city) if city else "",
428
+ "state": state,
429
+ "zip": zip_code,
430
+ "country": "USA"
431
+ },
432
+ "experience": {
433
+ "react_js": resume_info.get("react_experience", "0"),
434
+ "redux_js": resume_info.get("redux_experience", "0"),
435
+ "javascript": resume_info.get("javascript_experience", "0")
436
+ },
437
+ "preferences": {
438
+ "onsite_work": "Yes",
439
+ "commuting": "Yes",
440
+ "relocation": "Yes",
441
+ "remote_work": "Yes"
442
+ },
443
+ "education": resume_info.get("education", []),
444
+ "skills": resume_info.get("skills", []),
445
+ "work_history": resume_info.get("work_history", [])
446
+ }
447
+
448
+ # Function to save job application form
449
+ def save_job_form(form_data, job_id):
450
+ logging.info(f"Saving job form for {job_id}")
451
+ filename = f"job_application_form_{job_id}.json"
452
+ try:
453
+ with open(filename, "w") as f:
454
+ json.dump(form_data, f, indent=2)
455
+ return filename
456
+ except Exception as e:
457
+ logging.error(f"Error saving form: {str(e)}")
458
+ return None
459
+
460
+ # Function to test SMTP login
461
+ def test_smtp_login(user_email, user_password):
462
+ logging.info(f"Testing SMTP login for {user_email}")
463
+ user_password = user_password.strip()
464
+ if len(user_password) != 16:
465
+ logging.error(f"Invalid app-specific password length: {len(user_password)} characters")
466
+ 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."
467
+ if not re.match(r'^[a-zA-Z0-9]+$', user_password):
468
+ logging.error("Invalid app-specific password format: contains invalid characters")
469
+ return False, "SMTP login failed: App-specific password contains invalid characters. Use only letters and numbers."
470
+ try:
471
+ with smtplib.SMTP('smtp.gmail.com', 587, timeout=5) as server:
472
+ server.starttls()
473
+ server.login(user_email, user_password)
474
+ logging.info("SMTP login successful")
475
+ return True, "SMTP login successful"
476
+ except smtplib.SMTPAuthenticationError:
477
+ logging.error("SMTP authentication failed: Invalid email or password")
478
+ 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."
479
+ except Exception as e:
480
+ logging.error(f"SMTP login failed: {str(e)}")
481
+ return False, f"SMTP login failed: {str(e)}. Check network connection or try again later."
482
+
483
+ # Function to send application email
484
+ def send_application(resume_file_path, cover_letter, job_info, user_email, user_password, form_data=None):
485
+ logging.info(f"Sending application to {job_info['email']}")
486
+ try:
487
+ msg = MIMEMultipart()
488
+ msg['From'] = user_email
489
+ msg['To'] = job_info['email']
490
+ msg['Subject'] = f"Application for {job_info['title']} - {resume_info['personal_info']['name']}"
491
+
492
+ msg.attach(MIMEText(cover_letter, 'plain'))
493
+
494
+ with open(resume_file_path, 'rb') as f:
495
+ resume_attachment = MIMEApplication(f.read(), _subtype='pdf')
496
+ resume_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(resume_file_path))
497
+ msg.attach(resume_attachment)
498
+
499
+ if form_data:
500
+ form_filename = save_job_form(form_data, job_info['id'])
501
+ if form_filename:
502
+ with open(form_filename, 'rb') as f:
503
+ form_attachment = MIMEApplication(f.read(), _subtype='json')
504
+ form_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(form_filename))
505
+ msg.attach(form_attachment)
506
+
507
+ with smtplib.SMTP('smtp.gmail.com', 587, timeout=5) as server:
508
+ server.starttls()
509
+ server.login(user_email, user_password.strip())
510
+ server.sendmail(user_email, job_info['email'], msg.as_string())
511
+
512
+ logging.info(f"Application sent successfully to {job_info['email']}")
513
+ return {
514
+ "status": "success",
515
+ "message": "Application sent successfully",
516
+ "to": job_info["email"],
517
+ "from": user_email,
518
+ "subject": msg['Subject'],
519
+ "body": cover_letter,
520
+ "resume_attached": True,
521
+ "form_attached": form_data is not None,
522
+ "sent_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
523
+ }
524
+ except Exception as e:
525
+ logging.error(f"Error sending email: {str(e)}")
526
+ return {
527
+ "status": "error",
528
+ "message": f"Failed to send email: {str(e)}",
529
+ "to": job_info["email"],
530
+ "from": user_email,
531
+ "subject": f"Application for {job_info['title']}",
532
+ "body": cover_letter,
533
+ "resume_attached": True,
534
+ "form_attached": form_data is not None,
535
+ "sent_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
536
+ }
537
+
538
+ # Function to predict interview likelihood
539
+ def predict_interview_likelihood(match_score):
540
+ if match_score > 85:
541
+ return "Very High"
542
+ elif match_score > 70:
543
+ return "High"
544
+ elif match_score > 50:
545
+ return "Medium"
546
+ else:
547
+ return "Low"
548
+
549
+ # Function to simulate interview scheduling
550
+ def schedule_interviews(applications, min_interviews=5):
551
+ logging.info("Scheduling mock interviews")
552
+ interview_candidates = random.sample(applications, min(max(min_interviews, int(len(applications) * 0.2)), len(applications)))
553
+ interview_schedule = []
554
+
555
+ start_date = datetime.now() + timedelta(days=1)
556
+ time_slots = [
557
+ "09:00 AM", "10:00 AM", "11:00 AM", "01:00 PM", "02:00 PM", "03:00 PM"
558
+ ]
559
+
560
+ for i, app in enumerate(interview_candidates):
561
+ job = app["job"]
562
+ interview_date = (start_date + timedelta(days=i // len(time_slots))).strftime("%Y-%m-%d")
563
+ interview_schedule.append({
564
+ "company": job["company"],
565
+ "job_title": job["title"],
566
+ "date": interview_date,
567
+ "time": time_slots[i % len(time_slots)],
568
+ "email": job["email"],
569
+ "status": "Scheduled (Mock)"
570
+ })
571
+
572
+ logging.info(f"Scheduled {len(interview_schedule)} mock interviews")
573
+ return interview_schedule
574
+
575
+ # Main application processing function
576
+ def process_application(resume_file, job_title, location, user_email, user_password, num_applications=5, progress=gr.Progress()):
577
+ global resume_info
578
+ progress(0, desc="Starting processing...")
579
+ try:
580
+ progress(0.1, desc="Validating inputs...")
581
+ if not all([resume_file, job_title, location, user_email, user_password]):
582
+ return {"error": "All fields are required"}
583
+ if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", user_email):
584
+ return {"error": "Invalid email format"}
585
+ if not isinstance(num_applications, int) or num_applications < 1 or num_applications > 50:
586
+ return {"error": "Number of applications must be between 1 and 50"}
587
+ if not resume_file or not isinstance(resume_file, str) or not resume_file.lower().endswith('.pdf'):
588
+ return {"error": "Resume must be a valid PDF file path"}
589
+
590
+ progress(0.2, desc="Testing SMTP login...")
591
+ smtp_success, smtp_message = test_smtp_login(user_email, user_password)
592
+ if not smtp_success:
593
+ return {"error": smtp_message}
594
+
595
+ progress(0.3, desc="Processing resume...")
596
+ resume_text = extract_resume_text(resume_file)
597
+ resume_info = parse_resume(resume_text)
598
+
599
+ progress(0.4, desc="Searching jobs...")
600
+ jobs = search_jobs(job_title, location, num_applications, resume_info["skills"])
601
+
602
+ results = []
603
+ for i, job in enumerate(jobs):
604
+ progress(0.5 + (i / len(jobs)) * 0.4, desc=f"Processing application {i+1}/{len(jobs)}...")
605
+ match_score = calculate_match_score(resume_text, job["description"])
606
+ cover_letter = generate_cover_letter(resume_info, job)
607
+ form_data = generate_job_form(resume_info, job) if job.get("requires_form", False) else None
608
+
609
+ if form_data:
610
+ form_filename = save_job_form(form_data, job["id"])
611
+ job["form_filename"] = form_filename
612
+
613
+ application_result = send_application(resume_file, cover_letter, job, user_email, user_password, form_data)
614
+
615
+ results.append({
616
+ "job": job,
617
+ "match_score": round(match_score, 2),
618
+ "interview_likelihood": predict_interview_likelihood(match_score),
619
+ "application_status": application_result["status"],
620
+ "application_message": application_result.get("message", ""),
621
+ "form_data": form_data
622
+ })
623
+
624
+ progress(0.9, desc="Scheduling interviews...")
625
+ results.sort(key=lambda x: x["match_score"], reverse=True)
626
+ interview_schedule = schedule_interviews(results)
627
+
628
+ progress(1.0, desc="Finalizing results...")
629
+ return {
630
+ "resume_info": resume_info,
631
+ "results": results,
632
+ "interview_schedule": interview_schedule,
633
+ "total_applications": len(results),
634
+ "successful_applications": sum(1 for r in results if r["application_status"] == "success"),
635
+ "failed_applications": sum(1 for r in results if r["application_status"] == "error"),
636
+ "top_match_score": results[0]["match_score"] if results else 0,
637
+ "forms_generated": sum(1 for r in results if r.get("form_data") is not None)
638
+ }
639
+ except Exception as e:
640
+ logging.error(f"Error processing application: {str(e)}")
641
+ return {
642
+ "error": str(e),
643
+ "resume_info": None,
644
+ "results": [],
645
+ "interview_schedule": [],
646
+ "total_applications": 0,
647
+ "successful_applications": 0,
648
+ "failed_applications": 0,
649
+ "top_match_score": 0,
650
+ "forms_generated": 0
651
+ }
652
+
653
+ # Function to format results
654
+ def format_results(results):
655
+ logging.info("Formatting results")
656
+ if "error" in results and results["error"]:
657
+ 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."
658
+
659
+ resume_info = results["resume_info"]
660
+ application_results = results["results"]
661
+ interview_schedule = results["interview_schedule"]
662
+
663
+ output = "## Resume Analysis\n"
664
+ output += f"- Name: {resume_info.get('personal_info', {}).get('name', 'Not found')}\n"
665
+ output += f"- Email: {resume_info.get('personal_info', {}).get('email', 'Not found')}\n"
666
+ output += f"- Phone: {resume_info.get('personal_info', {}).get('phone', 'Not found')}\n"
667
+ output += f"- Address: {resume_info.get('personal_info', {}).get('address', 'Not found')}\n"
668
+ output += f"- Skills: {', '.join(resume_info['skills']) or 'None'}\n"
669
+ output += f"- Education: {', '.join(resume_info['education']) or 'None'}\n"
670
+ output += f"- Experience: {', '.join(resume_info['experience']) or 'None'}\n"
671
+
672
+ output += "\n## Application Results\n"
673
+ output += f"- Total Applications: {results['total_applications']}\n"
674
+ output += f"- Successful: {results['successful_applications']}\n"
675
+ output += f"- Failed: {results['failed_applications']}\n"
676
+ output += f"- Top Match Score: {results['top_match_score']}%\n"
677
+ output += f"- Forms Generated: {results['forms_generated']}\n"
678
+ output += f"- Scheduled Interviews: {len(interview_schedule)} (Note: These are mock schedules pending real company responses)\n\n"
679
+
680
+ output += "## Interview Schedule\n"
681
+ for i, interview in enumerate(interview_schedule, 1):
682
+ output += f"### {i}. {interview['job_title']} at {interview['company']}\n"
683
+ output += f"- Date: {interview['date']}\n"
684
+ output += f"- Time: {interview['time']}\n"
685
+ output += f"- Email: {interview['email']}\n"
686
+ output += f"- Status: {interview['status']}\n\n"
687
+
688
+ output += "## Job Matches\n"
689
+ for i, result in enumerate(application_results, 1):
690
+ job = result["job"]
691
+ output += f"### {i}. {job['title']} at {job['company']}\n"
692
+ output += f"- Location: {job['location']}\n"
693
+ output += f"- Match Score: {result['match_score']}%\n"
694
+ output += f"- Interview Likelihood: {result['interview_likelihood']}\n"
695
+ output += f"- Status: {result['application_status'].upper()}\n"
696
+ if job.get("requires_form", False):
697
+ output += f"- Form: {job.get('form_filename', 'Generated')}\n"
698
+ if result["application_status"] == "error":
699
+ output += f"- Error: {result['application_message']}\n"
700
+ output += f"- Email: {job['email']}\n"
701
+ output += f"- Description: {job['description']}\n"
702
+ output += f"- Applied: {datetime.now().strftime('%Y-%m-%d')}\n\n"
703
+
704
+ output += "## Download Generated Files\n"
705
+ form_files = [f for f in os.listdir('.') if f.startswith("job_application_form_") and f.endswith(".json")]
706
+ for form_file in form_files:
707
+ output += f"- [{form_file}](./{form_file})\n"
708
+ if os.path.exists("application_log.txt"):
709
+ output += f"- [Application Log](./application_log.txt)\n"
710
+
711
+ logging.info("Results formatted")
712
+ return output
713
 
714
  # Gradio interface
715
  def gradio_interface(resume_file, job_title, location, user_email, user_password, num_applications):
716
  logging.info("Starting Gradio interface processing")
717
  try:
718
  num_applications = int(num_applications) if num_applications else 5
 
719
  resume_path = "resume.pdf"
720
+ if resume_file is None:
721
+ return "Error: No resume file uploaded. Please upload a PDF file."
722
  with open(resume_path, "wb") as f:
723
+ f.write(resume_file.data)
724
  results = process_application(resume_path, job_title, location, user_email, user_password, num_applications)
725
  return format_results(results)
726
  except ValueError: