michael-agent 1.0.4__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -267,12 +267,13 @@ def generate_jd():
267
267
  "errors": []
268
268
  }
269
269
 
270
- # Use the JD generator node to create the job description
270
+ # Use the JD generator node to create the job description and screening questions
271
271
  result_state = jd_generator.generate_job_description(state)
272
272
 
273
273
  return jsonify({
274
274
  'success': True,
275
- 'job_description_text': result_state.get('job_description_text', 'Error generating job description')
275
+ 'job_description_text': result_state.get('job_description_text', 'Error generating job description'),
276
+ 'screening_questions': result_state.get('screening_questions', [])
276
277
  })
277
278
  except Exception as e:
278
279
  logger.error(f"Error generating job description: {str(e)}")
@@ -295,7 +296,8 @@ def save_jd():
295
296
  "job_id": job_id,
296
297
  "timestamp": datetime.now().isoformat(),
297
298
  "job_data": data.get("metadata", {}),
298
- "job_description": data.get("content", "")
299
+ "job_description": data.get("content", ""),
300
+ "screening_questions": data.get("screening_questions", [])
299
301
  }
300
302
 
301
303
  # Save to job_descriptions directory
@@ -521,6 +523,9 @@ def get_job(job_id):
521
523
  required_skills = jd_data['job_data'].get('required_skills', [])
522
524
  preferred_skills = jd_data['job_data'].get('preferred_skills', [])
523
525
 
526
+ # Extract screening questions if available
527
+ screening_questions = jd_data.get('screening_questions', [])
528
+
524
529
  return jsonify({
525
530
  'id': job_id,
526
531
  'title': title,
@@ -530,7 +535,8 @@ def get_job(job_id):
530
535
  'employment_type': employment_type,
531
536
  'experience_level': experience_level,
532
537
  'required_skills': required_skills,
533
- 'preferred_skills': preferred_skills
538
+ 'preferred_skills': preferred_skills,
539
+ 'screening_questions': screening_questions
534
540
  })
535
541
  except Exception as e:
536
542
  logger.error(f"Error getting job details: {str(e)}")
@@ -1028,6 +1034,16 @@ def get_candidate(candidate_id):
1028
1034
  # Add more detailed debug logging
1029
1035
  logger.info(f"Returning candidate data for {candidate_id}, name={name}, has_experience={len(experience)}, has_education={len(education)}")
1030
1036
 
1037
+ # Extract screening questions and answers if available
1038
+ screening_responses = None
1039
+ if 'screening_questions' in candidate_data and 'screening_answers' in candidate_data:
1040
+ screening_responses = {
1041
+ 'questions': candidate_data['screening_questions'],
1042
+ 'answers': candidate_data['screening_answers']
1043
+ }
1044
+ elif 'resume_data' in candidate_data and 'screening_responses' in candidate_data['resume_data']:
1045
+ screening_responses = candidate_data['resume_data']['screening_responses']
1046
+
1031
1047
  # Return data with sensible defaults for all fields to prevent frontend errors
1032
1048
  response_data = {
1033
1049
  'id': candidate_id,
@@ -1046,6 +1062,7 @@ def get_candidate(candidate_id):
1046
1062
  'weaknesses': [],
1047
1063
  'overall': 'No analysis available'
1048
1064
  },
1065
+ 'screening_responses': screening_responses,
1049
1066
  'resume_path': resume_path or '',
1050
1067
  'status': candidate_data.get('status', 'new') or 'new'
1051
1068
  }
@@ -1170,6 +1187,7 @@ def apply_for_job():
1170
1187
  try:
1171
1188
  # Check if all required fields are present
1172
1189
  if 'resume_file' not in request.files:
1190
+ logger.error("No resume file provided in request")
1173
1191
  return jsonify({'success': False, 'error': 'No resume file provided'}), 400
1174
1192
 
1175
1193
  resume_file = request.files['resume_file']
@@ -1178,8 +1196,12 @@ def apply_for_job():
1178
1196
  email = request.form.get('email')
1179
1197
 
1180
1198
  if not resume_file or not job_id or not name or not email:
1199
+ logger.error(f"Missing required fields: resume_file={bool(resume_file)}, job_id={job_id}, name={name}, email={email}")
1181
1200
  return jsonify({'success': False, 'error': 'Missing required fields'}), 400
1182
1201
 
1202
+ # Log all form data for debugging
1203
+ logger.info(f"Received application form data: {request.form}")
1204
+
1183
1205
  # Check if directory exists and create if not
1184
1206
  import os
1185
1207
  from datetime import datetime
@@ -1190,13 +1212,44 @@ def apply_for_job():
1190
1212
 
1191
1213
  # Generate unique filename with timestamp
1192
1214
  timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
1193
- filename = f"{job_id}_{request.form.get('name').replace(' ', '_')}.{resume_file.filename.split('.')[-1]}"
1215
+ filename = f"{timestamp}_{job_id}_{request.form.get('name').replace(' ', '_')}.{resume_file.filename.split('.')[-1]}"
1194
1216
  filepath = os.path.join(incoming_dir, filename)
1195
1217
 
1196
1218
  # Save the file
1197
1219
  resume_file.save(filepath)
1198
1220
  logger.info(f"Saved resume file to {filepath}")
1199
1221
 
1222
+ # Get job screening questions
1223
+ screening_questions = []
1224
+ job_file = os.path.join(settings.JOB_DESCRIPTIONS_DIR, f'{job_id}.json')
1225
+ if os.path.exists(job_file):
1226
+ try:
1227
+ with open(job_file, 'r') as f:
1228
+ job_data = json.load(f)
1229
+ screening_questions = job_data.get('screening_questions', [])
1230
+ logger.info(f"Found {len(screening_questions)} screening questions for job {job_id}")
1231
+ except Exception as e:
1232
+ logger.error(f"Error reading job file for screening questions: {str(e)}")
1233
+
1234
+ # Extract screening answers from form data
1235
+ # Look for fields named screening_answer_0, screening_answer_1, etc.
1236
+ screening_responses = []
1237
+ for i, question in enumerate(screening_questions):
1238
+ answer_key = f'screening_answer_{i}'
1239
+ if answer_key in request.form:
1240
+ answer = request.form.get(answer_key)
1241
+ logger.info(f"Found screening answer {i}: {answer}")
1242
+ screening_responses.append({
1243
+ "question": question,
1244
+ "answer": answer
1245
+ })
1246
+ else:
1247
+ logger.warning(f"Missing screening answer for question {i}")
1248
+ screening_responses.append({
1249
+ "question": question,
1250
+ "answer": "No answer provided"
1251
+ })
1252
+
1200
1253
  # Create metadata file with applicant details
1201
1254
  metadata = {
1202
1255
  'job_id': job_id,
@@ -1204,41 +1257,28 @@ def apply_for_job():
1204
1257
  'email': email,
1205
1258
  'phone': request.form.get('phone', ''),
1206
1259
  'cover_letter': request.form.get('cover_letter', ''),
1260
+ 'screening_questions': screening_questions,
1261
+ 'screening_responses': screening_responses,
1207
1262
  'application_date': datetime.now().isoformat(),
1208
1263
  'status': 'new',
1209
1264
  'resume_path': filepath
1210
1265
  }
1211
1266
 
1212
- metadata_path = f"{filepath}.json"
1267
+ metadata_path = filepath.replace('.pdf', '.json')
1213
1268
  with open(metadata_path, 'w') as f:
1214
1269
  json.dump(metadata, f, indent=2)
1215
1270
 
1271
+ logger.info(f"Saved metadata with screening responses to {metadata_path}")
1272
+
1216
1273
  # In a real implementation, trigger the workflow to process the resume
1217
1274
  from langgraph_workflow.graph_builder import process_new_resume
1218
1275
 
1219
- # Get job description
1220
- job_file = os.path.join(settings.JOB_DESCRIPTIONS_DIR, f'{job_id}.json')
1221
- job_description = None
1222
-
1223
- if os.path.exists(job_file):
1224
- with open(job_file, 'r') as f:
1225
- job_data = json.load(f)
1226
-
1227
- # Extract title from job description using regex
1228
- import re
1229
- title_match = re.search(r'\*\*Job Title:\s*(.*?)\*\*', job_data.get('job_description', ''))
1230
- title = title_match.group(1) if title_match else "Untitled Job"
1231
-
1232
- job_description = {
1233
- 'id': job_id,
1234
- 'title': title,
1235
- 'content': job_data.get('job_description', ''),
1236
- 'metadata': job_data.get('job_data', {})
1237
- }
1238
-
1239
- # We only need to save the file and metadata - the file system watcher will handle the processing
1240
- # This prevents duplicate workflow execution
1241
- logger.info(f"Resume uploaded through career portal - file system watcher will process it automatically")
1276
+ # Start processing in a background thread to avoid blocking the response
1277
+ import threading
1278
+ thread = threading.Thread(target=process_new_resume, args=(filepath, job_id))
1279
+ thread.daemon = True
1280
+ thread.start()
1281
+ logger.info(f"Started background processing for resume: {filepath}")
1242
1282
 
1243
1283
  return jsonify({
1244
1284
  'success': True,
@@ -168,6 +168,12 @@
168
168
  <label for="cover-letter" class="form-label">Cover Letter</label>
169
169
  <textarea class="form-control" id="cover-letter" name="cover_letter" rows="4"></textarea>
170
170
  </div>
171
+
172
+ <!-- Screening Questions Section (will be populated dynamically) -->
173
+ <div id="screening-questions-container" class="mb-3 d-none">
174
+ <h5 class="mb-3">Screening Questions</h5>
175
+ <div id="screening-questions-list"></div>
176
+ </div>
171
177
  </form>
172
178
  </div>
173
179
  <div class="modal-footer">
@@ -222,20 +228,65 @@
222
228
 
223
229
  // Apply button in job modal
224
230
  document.getElementById('apply-button').addEventListener('click', function() {
225
- // Get job details from the current modal
226
231
  const jobId = this.getAttribute('data-job-id');
227
- const jobTitle = document.getElementById('modal-job-title').textContent;
228
-
229
- // Set values in application modal
232
+ if (!jobId) return;
233
+ // Set job ID in hidden field
230
234
  document.getElementById('application-job-id').value = jobId;
235
+
236
+ // Get job title
237
+ const jobTitle = document.getElementById('modal-job-title').textContent;
231
238
  document.getElementById('application-job-title').textContent = jobTitle;
232
239
 
240
+ console.log('Fetching job details for screening questions, job ID:', jobId);
241
+
242
+ // Fetch job details to get screening questions
243
+ fetch(`/api/jobs/${jobId}`)
244
+ .then(response => response.json())
245
+ .then(jobDetails => {
246
+ console.log('Job details received:', jobDetails);
247
+ // Show screening questions if available
248
+ if (jobDetails.screening_questions && jobDetails.screening_questions.length > 0) {
249
+ console.log('Screening questions found:', jobDetails.screening_questions);
250
+ const questionsContainer = document.getElementById('screening-questions-container');
251
+ const questionsList = document.getElementById('screening-questions-list');
252
+
253
+ // Show the container
254
+ questionsContainer.classList.remove('d-none');
255
+
256
+ // Clear previous questions
257
+ questionsList.innerHTML = '';
258
+
259
+ // Add each question
260
+ jobDetails.screening_questions.forEach((question, index) => {
261
+ const questionDiv = document.createElement('div');
262
+ questionDiv.className = 'mb-3';
263
+ questionDiv.innerHTML = `
264
+ <label class="form-label fw-bold">${index + 1}. ${question}</label>
265
+ <textarea class="form-control" name="screening_answer_${index}" rows="2" required></textarea>
266
+ `;
267
+ questionsList.appendChild(questionDiv);
268
+ });
269
+ } else {
270
+ console.log('No screening questions found for this job');
271
+ // Hide the container if no questions
272
+ document.getElementById('screening-questions-container').classList.add('d-none');
273
+ }
274
+ })
275
+ .catch(error => {
276
+ console.error('Error fetching job details:', error);
277
+ document.getElementById('screening-questions-container').classList.add('d-none');
278
+ });
279
+
233
280
  // Hide job details modal and show application modal
234
281
  const jobModal = bootstrap.Modal.getInstance(document.getElementById('job-details-modal'));
235
282
  jobModal.hide();
236
283
 
237
- const applicationModal = new bootstrap.Modal(document.getElementById('application-modal'));
238
- applicationModal.show();
284
+ // Show application modal after a short delay to ensure the previous modal is closed
285
+ setTimeout(() => {
286
+ const applicationModal = new bootstrap.Modal(document.getElementById('application-modal'));
287
+ applicationModal.show();
288
+ console.log('Application modal shown');
289
+ }, 300);
239
290
  });
240
291
 
241
292
  // Submit application
@@ -259,6 +310,24 @@
259
310
  }
260
311
  formData.append('resume_file', resumeFile);
261
312
 
313
+ // Collect screening question answers
314
+ const screeningAnswers = [];
315
+ document.querySelectorAll('[name^="screening_answer_"]').forEach(input => {
316
+ // Check if the answer field is empty
317
+ if (!input.value.trim() && input.required) {
318
+ alert('Please answer all screening questions');
319
+ valid = false;
320
+ return;
321
+ }
322
+ screeningAnswers.push(input.value);
323
+ });
324
+
325
+ // Add screening answers as JSON
326
+ if (screeningAnswers.length > 0) {
327
+ console.log('Adding screening answers to form data:', screeningAnswers);
328
+ formData.append('screening_answers', JSON.stringify(screeningAnswers));
329
+ }
330
+
262
331
  // Show spinner
263
332
  this.disabled = true;
264
333
  document.getElementById('submit-spinner').classList.remove('d-none');
@@ -281,6 +350,10 @@
281
350
 
282
351
  // Reset form
283
352
  form.reset();
353
+
354
+ // Clear screening questions
355
+ document.getElementById('screening-questions-container').classList.add('d-none');
356
+ document.getElementById('screening-questions-list').innerHTML = '';
284
357
  } else {
285
358
  alert('Error submitting application: ' + data.error);
286
359
  }
@@ -105,6 +105,11 @@
105
105
  <label for="application_deadline" class="form-label">Application Deadline (Optional)</label>
106
106
  <input type="date" class="form-control" id="application_deadline" name="application_deadline">
107
107
  </div>
108
+
109
+ <div class="alert alert-info mb-3">
110
+ <h5><i class="bi bi-info-circle"></i> Screening Questions</h5>
111
+ <p>When you generate a job description, 5 relevant screening questions will be automatically created based on the job details. These questions will be presented to candidates when they apply.</p>
112
+ </div>
108
113
 
109
114
  <div class="text-center">
110
115
  <button type="submit" class="btn btn-primary" id="generate-btn">
@@ -128,6 +133,15 @@
128
133
  <div id="jd-sections">
129
134
  <h3 id="jd-title" class="mb-3"></h3>
130
135
  <div id="jd-content"></div>
136
+
137
+ <hr class="my-4">
138
+ <div id="screening-questions-section" class="mt-4 p-4 bg-light border rounded">
139
+ <h4 class="mb-3"><i class="bi bi-question-circle"></i> Screening Questions</h4>
140
+ <div class="alert alert-info">
141
+ <strong>Important:</strong> These questions will be presented to candidates when they apply for this position.
142
+ </div>
143
+ <ol id="screening-questions-list" class="fw-bold"></ol>
144
+ </div>
131
145
  </div>
132
146
  </div>
133
147
  </div>
@@ -240,6 +254,25 @@
240
254
  // Display the generated JD
241
255
  document.getElementById('jd-title').textContent = jobData.position;
242
256
  document.getElementById('jd-content').innerHTML = data.job_description_text.replace(/\n/g, '<br>');
257
+
258
+ // Display screening questions
259
+ const screeningQuestionsList = document.getElementById('screening-questions-list');
260
+ screeningQuestionsList.innerHTML = ''; // Clear previous questions
261
+
262
+ if (data.screening_questions && data.screening_questions.length > 0) {
263
+ data.screening_questions.forEach(question => {
264
+ const li = document.createElement('li');
265
+ li.className = 'mb-2';
266
+ li.textContent = question;
267
+ screeningQuestionsList.appendChild(li);
268
+ });
269
+ // Store screening questions for saving
270
+ window.screeningQuestions = data.screening_questions;
271
+ } else {
272
+ screeningQuestionsList.innerHTML = '<div class="alert alert-warning">No screening questions were generated.</div>';
273
+ window.screeningQuestions = [];
274
+ }
275
+
243
276
  document.getElementById('result-card').style.display = 'block';
244
277
 
245
278
  // Scroll to result
@@ -264,7 +297,16 @@
264
297
  const jdText = document.getElementById('jd-content').innerText;
265
298
  const title = document.getElementById('jd-title').innerText;
266
299
 
267
- navigator.clipboard.writeText(title + '\n\n' + jdText)
300
+ // Prepare screening questions text
301
+ let screeningQuestionsText = '';
302
+ if (window.screeningQuestions && window.screeningQuestions.length > 0) {
303
+ screeningQuestionsText = '\n\nScreening Questions:\n';
304
+ window.screeningQuestions.forEach((question, index) => {
305
+ screeningQuestionsText += `${index + 1}. ${question}\n`;
306
+ });
307
+ }
308
+
309
+ navigator.clipboard.writeText(title + '\n\n' + jdText + screeningQuestionsText)
268
310
  .then(() => {
269
311
  const originalText = this.textContent;
270
312
  this.textContent = 'Copied!';
@@ -291,6 +333,7 @@
291
333
  body: JSON.stringify({
292
334
  title: title,
293
335
  content: jdText,
336
+ screening_questions: window.screeningQuestions || [],
294
337
  metadata: {
295
338
  position: document.getElementById('position').value,
296
339
  location: document.getElementById('location').value,
@@ -177,6 +177,15 @@
177
177
  </div>
178
178
  </div>
179
179
 
180
+ <div class="card border-primary mb-3">
181
+ <div class="card-header bg-light">Screening Responses</div>
182
+ <div class="card-body">
183
+ <div id="screening-responses-container">
184
+ <p class="text-muted">No screening responses available</p>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
180
189
  <div class="card border-success">
181
190
  <div class="card-header bg-light">Actions</div>
182
191
  <div class="card-body">
@@ -482,6 +491,9 @@
482
491
 
483
492
  // AI Analysis - make sure we don't pass undefined
484
493
  renderAIAnalysis(candidate.ai_analysis || {});
494
+
495
+ // Screening Responses - make sure we don't pass undefined
496
+ renderScreeningResponses(candidate.screening_responses || {});
485
497
  } catch (error) {
486
498
  console.error('Error processing candidate data:', error);
487
499
  document.getElementById('resume-details-card').style.display = 'block';
@@ -906,6 +918,38 @@
906
918
  }
907
919
  }
908
920
 
921
+ // Render screening questions and answers
922
+ function renderScreeningResponses(screeningData) {
923
+ const container = document.getElementById('screening-responses-container');
924
+ container.innerHTML = '';
925
+
926
+ if (!screeningData || !screeningData.questions || !screeningData.answers ||
927
+ screeningData.questions.length === 0 || screeningData.answers.length === 0) {
928
+ container.innerHTML = '<p class="text-muted">No screening responses available</p>';
929
+ return;
930
+ }
931
+
932
+ let html = '<div class="list-group">';
933
+
934
+ // Loop through questions and answers
935
+ for (let i = 0; i < Math.min(screeningData.questions.length, screeningData.answers.length); i++) {
936
+ const question = screeningData.questions[i];
937
+ const answer = screeningData.answers[i];
938
+
939
+ html += `
940
+ <div class="list-group-item">
941
+ <div class="d-flex w-100 justify-content-between">
942
+ <h6 class="mb-2">Q${i+1}: ${question}</h6>
943
+ </div>
944
+ <p class="mb-1"><strong>Answer:</strong> ${answer}</p>
945
+ </div>
946
+ `;
947
+ }
948
+
949
+ html += '</div>';
950
+ container.innerHTML = html;
951
+ }
952
+
909
953
  // Update statistics
910
954
  function updateStatistics(stats) {
911
955
  document.getElementById('total-resumes').textContent = stats.total;
@@ -269,7 +269,7 @@ def start_workflow():
269
269
  logger.error(traceback.format_exc())
270
270
  raise
271
271
 
272
- def process_new_resume(resume_path: str, job_description: Dict[str, Any] = None):
272
+ def process_new_resume(resume_path, job_id=None):
273
273
  """Process a new resume through the workflow"""
274
274
  try:
275
275
  # Extract job ID from filename (e.g., 20250626225207_Michael_Jone.pdf)
@@ -280,6 +280,28 @@ def process_new_resume(resume_path: str, job_description: Dict[str, Any] = None)
280
280
  # Create initial state with extracted job ID
281
281
  initial_state = create_initial_state(resume_path=resume_path, job_id=job_id)
282
282
 
283
+ # Check for metadata file with screening questions/answers
284
+ metadata_path = f"{resume_path}.json"
285
+ if os.path.exists(metadata_path):
286
+ try:
287
+ with open(metadata_path, 'r') as f:
288
+ metadata = json.load(f)
289
+
290
+ # Extract screening questions and answers
291
+ if 'screening_questions' in metadata and 'screening_answers' in metadata:
292
+ logger.info(f"Found screening questions/answers for resume {resume_path}")
293
+ initial_state['screening_questions'] = metadata['screening_questions']
294
+ initial_state['screening_answers'] = metadata['screening_answers']
295
+
296
+ # Also extract candidate name and email if available
297
+ if 'name' in metadata:
298
+ initial_state['candidate_name'] = metadata['name']
299
+ if 'email' in metadata:
300
+ initial_state['candidate_email'] = metadata['email']
301
+
302
+ except Exception as e:
303
+ logger.error(f"Error loading metadata file: {str(e)}")
304
+
283
305
  # Start workflow with initial state
284
306
  workflow = build_workflow()
285
307
  logger.info(f"[Job {job_id}] Starting workflow processing with job ID {job_id}")
@@ -70,6 +70,35 @@ def get_jd_prompt(job_data: Dict[str, Any]) -> str:
70
70
  proposition for potential candidates.
71
71
  """
72
72
 
73
+ def get_screening_questions_prompt(job_data: Dict[str, Any]) -> str:
74
+ """Generate the prompt for screening questions creation"""
75
+ position = job_data.get('position', 'Software Engineer')
76
+ return f"""
77
+ You are an HR specialist creating initial screening questions for job applicants.
78
+ Create 5 simple screening questions for a {position} role.
79
+
80
+ These questions should:
81
+ 1. Be simple and quick to answer (yes/no or short responses)
82
+ 2. Relate directly to the role's basic requirements
83
+ 3. Help filter candidates early in the process
84
+ 4. Be easy to understand without technical jargon
85
+ 5. Assess key role-related experience or skills
86
+
87
+ Job details:
88
+ Position: {job_data.get('position', 'Software Engineer')}
89
+ Required skills: {', '.join(job_data.get('required_skills', []))}
90
+ Experience level: {job_data.get('experience_level', 'Not specified')}
91
+
92
+ Format your response as a JSON array with exactly 5 questions:
93
+ [
94
+ "Question 1",
95
+ "Question 2",
96
+ "Question 3",
97
+ "Question 4",
98
+ "Question 5"
99
+ ]
100
+ """
101
+
73
102
  def generate_job_description(state: Dict[str, Any]) -> Dict[str, Any]:
74
103
  """
75
104
  LangGraph node to generate a job description
@@ -83,8 +112,8 @@ def generate_job_description(state: Dict[str, Any]) -> Dict[str, Any]:
83
112
  logger.info("Starting job description generation")
84
113
 
85
114
  # Check if job description already exists in state
86
- if state.get("job_description") and state.get("job_description_text"):
87
- logger.info("Job description already exists, skipping generation")
115
+ if state.get("job_description") and state.get("job_description_text") and state.get("screening_questions"):
116
+ logger.info("Job description and screening questions already exist, skipping generation")
88
117
  return state
89
118
 
90
119
  try:
@@ -108,25 +137,73 @@ def generate_job_description(state: Dict[str, Any]) -> Dict[str, Any]:
108
137
  "preferred_skills": ["Azure", "CI/CD", "TypeScript"]
109
138
  }
110
139
 
111
- # Generate the prompt
112
- prompt = get_jd_prompt(job_data)
140
+ # Generate the prompt for job description
141
+ jd_prompt = get_jd_prompt(job_data)
142
+
143
+ # Generate the prompt for screening questions
144
+ sq_prompt = get_screening_questions_prompt(job_data)
145
+
146
+ # Invoke the language model for job description
147
+ jd_response = llm.invoke(jd_prompt)
148
+ generated_jd_text = jd_response.content
149
+
150
+ # Invoke the language model for screening questions
151
+ sq_response = llm.invoke(sq_prompt)
152
+ generated_sq_text = sq_response.content
153
+
154
+ # Extract questions from the response (handling JSON format)
155
+ screening_questions = []
156
+ try:
157
+ import json
158
+ import re
159
+
160
+ # Try to parse JSON directly
161
+ try:
162
+ screening_questions = json.loads(generated_sq_text)
163
+ except json.JSONDecodeError:
164
+ # Extract JSON array using regex if direct parsing fails
165
+ json_match = re.search(r'\[\s*".*"\s*\]', generated_sq_text, re.DOTALL)
166
+ if json_match:
167
+ screening_questions = json.loads(json_match.group(0))
168
+ else:
169
+ # Simple line-by-line extraction as fallback
170
+ lines = generated_sq_text.strip().split('\n')
171
+ for line in lines:
172
+ question = line.strip().strip('"[]').strip('"').strip()
173
+ if question and not question.startswith('#') and not question.startswith('//'):
174
+ screening_questions.append(question)
175
+ except Exception as e:
176
+ logger.error(f"Error parsing screening questions: {str(e)}")
177
+ # Fallback questions
178
+ screening_questions = [
179
+ f"Do you have experience as a {job_data.get('position', 'professional')}?",
180
+ f"Are you familiar with {', '.join(job_data.get('required_skills', ['the required skills']))}?",
181
+ f"Can you work in {job_data.get('location', 'this location')}?",
182
+ f"Are you available for {job_data.get('employment_type', 'this position type')}?",
183
+ "How many years of relevant experience do you have?"
184
+ ]
113
185
 
114
- # Invoke the language model
115
- response = llm.invoke(prompt)
116
- generated_text = response.content
186
+ # Ensure we have exactly 5 questions
187
+ if len(screening_questions) > 5:
188
+ screening_questions = screening_questions[:5]
189
+ while len(screening_questions) < 5:
190
+ screening_questions.append(f"Do you have experience with {job_data.get('preferred_skills', ['the preferred skills'])[0] if job_data.get('preferred_skills') else 'the relevant technologies'}?")
117
191
 
118
- # Update the state with the generated job description
192
+ # Update the state with the generated job description and screening questions
119
193
  state["job_description"] = job_data
120
- state["job_description_text"] = generated_text
194
+ state["job_description_text"] = generated_jd_text
195
+ state["screening_questions"] = screening_questions
121
196
  state["status"] = "jd_generated"
122
197
 
123
- logger.info("Job description generated successfully")
198
+ logger.info("Job description and screening questions generated successfully")
124
199
 
125
200
  except Exception as e:
126
201
  error_message = f"Error generating job description: {str(e)}"
127
202
  logger.error(error_message)
128
203
 
129
204
  # Add error to state
205
+ if "errors" not in state:
206
+ state["errors"] = []
130
207
  state["errors"].append({
131
208
  "step": "jd_generator",
132
209
  "error": error_message
@@ -135,5 +212,15 @@ def generate_job_description(state: Dict[str, Any]) -> Dict[str, Any]:
135
212
  # Set fallback job description text if needed
136
213
  if not state.get("job_description_text"):
137
214
  state["job_description_text"] = "Default job description text for fallback purposes."
215
+
216
+ # Set fallback screening questions if needed
217
+ if not state.get("screening_questions"):
218
+ state["screening_questions"] = [
219
+ "Do you have experience with this role?",
220
+ "Are you familiar with the required skills?",
221
+ "Can you work in the specified location?",
222
+ "Are you available for the listed employment type?",
223
+ "How many years of relevant experience do you have?"
224
+ ]
138
225
 
139
226
  return state
@@ -27,9 +27,9 @@ def create_llm():
27
27
  logger.error(f"Error initializing Azure OpenAI: {str(e)}")
28
28
  return None
29
29
 
30
- def get_question_generation_prompt(resume_text: str, jd_text: str, relevance_score: float) -> str:
30
+ def get_question_generation_prompt(resume_text: str, jd_text: str, relevance_score: float, screening_data=None) -> str:
31
31
  """Generate the prompt for interview question generation"""
32
- return f"""
32
+ base_prompt = f"""
33
33
  You are an expert technical interviewer and recruiter.
34
34
 
35
35
  Generate a set of interview questions for a candidate based on their resume and the job description.
@@ -42,34 +42,47 @@ def get_question_generation_prompt(resume_text: str, jd_text: str, relevance_sco
42
42
  {jd_text[:1000]}
43
43
 
44
44
  The candidate's resume relevance score is {relevance_score:.2f} out of 1.0.
45
+ """
45
46
 
47
+ # Add screening questions and answers if available
48
+ if screening_data and 'questions' in screening_data and 'answers' in screening_data:
49
+ screening_prompt = "\nThe candidate has already answered these initial screening questions:\n"
50
+ for i, (q, a) in enumerate(zip(screening_data['questions'], screening_data['answers'])):
51
+ screening_prompt += f"Question {i+1}: {q}\nAnswer {i+1}: {a}\n\n"
52
+ base_prompt += screening_prompt
53
+
54
+ # Add format instructions for the response
55
+ result_prompt = base_prompt + """
46
56
  Generate 10 questions in the following JSON format:
47
57
  ```json
48
- {{
58
+ {
49
59
  "technical_questions": [
50
- {{
60
+ {
51
61
  "question": "string",
52
62
  "difficulty": "easy|medium|hard",
53
63
  "category": "technical_skill|domain_knowledge|problem_solving",
54
64
  "purpose": "brief explanation of what this question assesses"
55
- }},
65
+ },
56
66
  // 4 more technical questions...
57
67
  ],
58
68
  "behavioral_questions": [
59
- {{
69
+ {
60
70
  "question": "string",
61
71
  "category": "teamwork|leadership|conflict_resolution|problem_solving|adaptability",
62
72
  "purpose": "brief explanation of what this question assesses"
63
- }},
73
+ },
64
74
  // 4 more behavioral questions...
65
75
  ],
66
76
  "follow_up_areas": ["Area 1", "Area 2", "Area 3"] // Important areas to explore further based on resume gaps
67
- }}
77
+ }
68
78
  ```
69
79
 
70
80
  Focus on questions that will reveal the candidate's true abilities related to the job requirements.
71
81
  Include questions that address any potential gaps between the resume and job description.
82
+ Also, do not repeat questions that were already asked in the screening process.
72
83
  """
84
+
85
+ return result_prompt
73
86
 
74
87
  def generate_interview_questions(state: Dict[str, Any]) -> Dict[str, Any]:
75
88
  """Generate interview questions based on resume and job description."""
@@ -162,8 +175,17 @@ def generate_interview_questions(state: Dict[str, Any]) -> Dict[str, Any]:
162
175
  # Get resume score from state (default to 0.5 if missing)
163
176
  relevance_score = state.get("relevance_score", 0.5)
164
177
 
178
+ # Prepare screening data if available
179
+ screening_data = None
180
+ if state.get("screening_questions") and state.get("screening_answers"):
181
+ screening_data = {
182
+ "questions": state.get("screening_questions"),
183
+ "answers": state.get("screening_answers")
184
+ }
185
+ logger.info("Including screening questions and answers in question generation")
186
+
165
187
  # Generate the prompt
166
- prompt = get_question_generation_prompt(resume_text, jd_text, relevance_score)
188
+ prompt = get_question_generation_prompt(resume_text, jd_text, relevance_score, screening_data)
167
189
  print(f"[QUESTION_GENERATOR] Created prompt with resume length {len(resume_text)} and JD length {len(jd_text)}")
168
190
 
169
191
  # Invoke the language model
@@ -316,15 +316,15 @@ def extract_jd_sections(jd_text: str) -> Dict[str, str]:
316
316
 
317
317
  return sections
318
318
 
319
- def compute_similarity_score_llm(client, resume_text: str, jd_text: str) -> float:
319
+ def compute_similarity_score_llm(client, resume_text: str, jd_text: str, screening_data: Dict[str, Any] = None) -> float:
320
320
  """Compute similarity score between resume and job description using LLM"""
321
321
  try:
322
322
  # Truncate texts if they are too long for the LLM context window
323
323
  resume_truncated = resume_text[:2000] # Adjust based on your LLM's limits
324
324
  jd_truncated = jd_text[:2000] # Adjust based on your LLM's limits
325
325
 
326
- # Create the prompt
327
- prompt = f"""
326
+ # Start building the prompt
327
+ base_prompt = f"""
328
328
  Your task is to compare a resume and job description to determine how well the candidate matches the job requirements.
329
329
 
330
330
  Job Description:
@@ -332,7 +332,17 @@ def compute_similarity_score_llm(client, resume_text: str, jd_text: str) -> floa
332
332
 
333
333
  Resume:
334
334
  {resume_truncated}
335
+ """
335
336
 
337
+ # Add screening questions and answers if available
338
+ if screening_data and 'questions' in screening_data and 'answers' in screening_data:
339
+ screening_context = "\nCandidate's screening question responses:\n"
340
+ for i, (q, a) in enumerate(zip(screening_data['questions'], screening_data['answers'])):
341
+ screening_context += f"Question {i+1}: {q}\nAnswer {i+1}: {a}\n\n"
342
+ base_prompt += screening_context
343
+
344
+ # Complete the prompt with instructions
345
+ prompt = base_prompt + """
336
346
  Based on the match between the resume and job description, provide a relevance score between 0.0 and 1.0,
337
347
  where 1.0 means perfect match and 0.0 means no match at all.
338
348
 
@@ -475,11 +485,21 @@ def analyze_resume(state: Dict[str, Any]) -> Dict[str, Any]:
475
485
  resume_sections = extract_resume_sections(resume_text)
476
486
  jd_sections = extract_jd_sections(jd_text)
477
487
 
488
+ # Prepare screening data if available
489
+ screening_data = None
490
+ if state.get("screening_questions") and state.get("screening_answers"):
491
+ screening_data = {
492
+ "questions": state.get("screening_questions"),
493
+ "answers": state.get("screening_answers")
494
+ }
495
+ logger.info("Including screening questions and answers in similarity scoring")
496
+
478
497
  # Compute overall similarity score using LLM
479
498
  overall_score = compute_similarity_score_llm(
480
499
  chat_client,
481
500
  resume_sections["full"],
482
- jd_sections["full"]
501
+ jd_sections["full"],
502
+ screening_data
483
503
  )
484
504
 
485
505
  # Store the score in state
@@ -596,6 +616,13 @@ def save_snapshot(state: Dict[str, Any]) -> None:
596
616
  if "job_description" in state:
597
617
  save_state["job_description"] = state["job_description"]
598
618
 
619
+ # Add screening questions and answers if available
620
+ if "screening_questions" in state:
621
+ save_state["screening_questions"] = state["screening_questions"]
622
+
623
+ if "screening_answers" in state:
624
+ save_state["screening_answers"] = state["screening_answers"]
625
+
599
626
  # Add relevance score if available
600
627
  if "relevance_score" in state:
601
628
  save_state["relevance_score"] = state["relevance_score"]
@@ -628,4 +655,155 @@ def save_snapshot(state: Dict[str, Any]) -> None:
628
655
 
629
656
  except Exception as e:
630
657
  logger.error(f"Error saving state snapshot: {str(e)}")
631
- logger.error(traceback.format_exc())
658
+ logger.error(traceback.format_exc())
659
+
660
+ def analyze_resume_with_llm(resume_text: str, jd_text: str, llm, screening_data: Dict[str, Any] = None) -> Dict[str, Any]:
661
+ """
662
+ Analyze the resume using Azure OpenAI, comparing to job description
663
+
664
+ Args:
665
+ resume_text: The text of the resume
666
+ jd_text: The text of the job description
667
+ llm: The LLM client to use
668
+ screening_data: Dictionary containing screening questions and answers
669
+
670
+ Returns:
671
+ Analysis results
672
+ """
673
+
674
+ # Create the system prompt
675
+ system_prompt = f"""
676
+ You are an expert resume analyzer and HR professional.
677
+ Your task is to analyze a resume against a job description and provide
678
+ a relevance score (0.0-1.0) and detailed feedback.
679
+
680
+ Resume text:
681
+ {resume_text[:3000] if resume_text else "No resume text provided"}
682
+
683
+ Job Description:
684
+ {jd_text[:1500] if jd_text else "No job description provided"}
685
+ """
686
+
687
+ # Add screening questions and answers if available
688
+ if screening_data and 'questions' in screening_data and 'answers' in screening_data:
689
+ screening_context = "\n\nCandidate's screening question responses:\n"
690
+ for i, (q, a) in enumerate(zip(screening_data['questions'], screening_data['answers'])):
691
+ screening_context += f"Question {i+1}: {q}\nAnswer {i+1}: {a}\n\n"
692
+ system_prompt += screening_context
693
+
694
+ # Complete the prompt with instructions
695
+ system_prompt += """
696
+ Analyze the resume against the job description and provide your assessment in the following JSON format:
697
+ {
698
+ "relevance_score": 0.85, // A float from 0.0 to 1.0 indicating overall fit
699
+ "skill_match": {
700
+ "matched_skills": ["skill1", "skill2"], // Skills found in both resume and JD
701
+ "missing_skills": ["skill3", "skill4"] // Important JD skills not found in resume
702
+ },
703
+ "experience_match": {
704
+ "score": 0.7, // 0.0 to 1.0
705
+ "feedback": "Candidate has relevant experience in..."
706
+ },
707
+ "education_match": {
708
+ "score": 0.8,
709
+ "feedback": "Candidate has appropriate education..."
710
+ },
711
+ "overall_feedback": "The candidate is a strong fit because..."
712
+ }
713
+
714
+ Please ensure the JSON is valid and properly formatted.
715
+ """
716
+
717
+ try:
718
+ # Truncate text if it's too long to avoid token limits
719
+ truncated_resume = resume_text[:3000] if resume_text and len(resume_text) > 3000 else resume_text
720
+ truncated_jd = jd_text[:1500] if jd_text and len(jd_text) > 1500 else jd_text
721
+
722
+ # Create the user message
723
+ user_message = f"""
724
+ Resume: {truncated_resume}
725
+
726
+ Job Description: {truncated_jd}
727
+
728
+ Provide a detailed analysis of this resume against the job description in the JSON format specified.
729
+ """
730
+
731
+ # Get the response
732
+ response = llm.invoke([
733
+ {"role": "system", "content": system_prompt},
734
+ {"role": "user", "content": user_message}
735
+ ])
736
+
737
+ # Extract and parse the JSON response
738
+ import json
739
+ import re
740
+
741
+ content = response.content
742
+
743
+ # Try to find a JSON object in the response
744
+ json_match = re.search(r'\{[\s\S]*\}', content)
745
+ if json_match:
746
+ try:
747
+ return json.loads(json_match.group(0))
748
+ except json.JSONDecodeError:
749
+ return parse_analysis_response(content)
750
+ else:
751
+ return parse_analysis_response(content)
752
+ except Exception as e:
753
+ logger.error(f"Error analyzing resume with LLM: {str(e)}")
754
+ return {
755
+ "relevance_score": 0.5,
756
+ "skill_match": {"matched_skills": [], "missing_skills": []},
757
+ "experience_match": {"score": 0.5, "feedback": "Error analyzing experience"},
758
+ "education_match": {"score": 0.5, "feedback": "Error analyzing education"},
759
+ "overall_feedback": f"Error analyzing resume: {str(e)}"
760
+ }
761
+
762
+ def parse_analysis_response(content: str) -> Dict[str, Any]:
763
+ """
764
+ Parse the LLM's response to extract analysis information when JSON parsing fails
765
+
766
+ Args:
767
+ content: The text response from the LLM
768
+
769
+ Returns:
770
+ Dictionary with analysis results extracted from the text
771
+ """
772
+ # Default values
773
+ result = {
774
+ "relevance_score": 0.5,
775
+ "skill_match": {"matched_skills": [], "missing_skills": []},
776
+ "experience_match": {"score": 0.5, "feedback": "Unable to extract from response"},
777
+ "education_match": {"score": 0.5, "feedback": "Unable to extract from response"},
778
+ "overall_feedback": "Analysis completed with limited structure"
779
+ }
780
+
781
+ # Extract relevance score using regex
782
+ import re
783
+ score_match = re.search(r'relevance_score["\s:]+([0-9\.]+)', content)
784
+ if score_match:
785
+ try:
786
+ result["relevance_score"] = float(score_match.group(1))
787
+ except ValueError:
788
+ pass
789
+
790
+ # Extract matched skills
791
+ matched_skills_match = re.search(r'matched_skills["\s:]+\[(.*?)\]', content, re.DOTALL)
792
+ if matched_skills_match:
793
+ skills_text = matched_skills_match.group(1)
794
+ skills = [s.strip().strip('"\'').strip() for s in skills_text.split(',')]
795
+ result["skill_match"]["matched_skills"] = [s for s in skills if s]
796
+
797
+ # Extract missing skills
798
+ missing_skills_match = re.search(r'missing_skills["\s:]+\[(.*?)\]', content, re.DOTALL)
799
+ if missing_skills_match:
800
+ skills_text = missing_skills_match.group(1)
801
+ skills = [s.strip().strip('"\'').strip() for s in skills_text.split(',')]
802
+ result["skill_match"]["missing_skills"] = [s for s in skills if s]
803
+
804
+ # Extract overall feedback
805
+ feedback_match = re.search(r'overall_feedback["\s:]+["\'](.*?)["\']', content, re.DOTALL)
806
+ if feedback_match:
807
+ result["overall_feedback"] = feedback_match.group(1).strip()
808
+
809
+ return result
@@ -74,19 +74,33 @@ def extract_cover_letter(resume_text: str) -> Optional[str]:
74
74
 
75
75
  return cover_letter_text
76
76
 
77
- def analyze_text_sentiment_openai_chat(client, text: str) -> Dict[str, Any]:
77
+ def analyze_text_sentiment_openai_chat(client, text: str, screening_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
78
78
  """
79
79
  Analyze text sentiment using Azure OpenAI Chat
80
80
  Args:
81
81
  client: Azure OpenAI client
82
82
  text: Text to analyze
83
+ screening_data: Dictionary containing screening questions and answers for additional context
83
84
  Returns:
84
85
  Sentiment analysis result
85
86
  """
86
87
  try:
87
- # Create the prompt
88
- prompt = f"""
89
- Analyze the sentiment of the following text. Respond with a JSON object containing:
88
+ # Create the base prompt
89
+ base_prompt = f"""
90
+ Analyze the sentiment of the following text.
91
+ """
92
+
93
+ # Add screening questions and answers context if available
94
+ screening_context = ""
95
+ if screening_data and 'questions' in screening_data and 'answers' in screening_data:
96
+ screening_context = "\n\nCandidate's screening question responses:\n"
97
+ for i, (q, a) in enumerate(zip(screening_data['questions'], screening_data['answers'])):
98
+ screening_context += f"Question {i+1}: {q}\nAnswer {i+1}: {a}\n\n"
99
+ base_prompt += f"\nConsider the following screening question responses as additional context for sentiment analysis:{screening_context}"
100
+
101
+ # Complete the prompt
102
+ prompt = base_prompt + f"""
103
+ Respond with a JSON object containing:
90
104
  - sentiment: either "positive", "neutral", or "negative"
91
105
  - positive_score: a float between 0 and 1
92
106
  - neutral_score: a float between 0 and 1
@@ -256,6 +270,15 @@ def analyze_sentiment(state: Dict[str, Any]) -> Dict[str, Any]:
256
270
  # If no cover letter found, use the first 1000 characters of resume as a sample
257
271
  sample_text = cover_letter if cover_letter else resume_text[:1000]
258
272
 
273
+ # Prepare screening data if available
274
+ screening_data = None
275
+ if state.get("screening_questions") and state.get("screening_answers"):
276
+ screening_data = {
277
+ "questions": state.get("screening_questions"),
278
+ "answers": state.get("screening_answers")
279
+ }
280
+ logger.info(f"Screening data prepared: {screening_data}")
281
+
259
282
  # Initialize clients and results
260
283
  sentiment_result = None
261
284
  chat_client = None
@@ -268,7 +291,7 @@ def analyze_sentiment(state: Dict[str, Any]) -> Dict[str, Any]:
268
291
  # Only use chat-based or basic sentiment analysis
269
292
  if chat_client:
270
293
  # Method 1: Use Azure OpenAI Chat for sentiment analysis
271
- sentiment_result = analyze_text_sentiment_openai_chat(chat_client, sample_text)
294
+ sentiment_result = analyze_text_sentiment_openai_chat(chat_client, sample_text, screening_data)
272
295
  logger.info("Used Azure OpenAI Chat for sentiment analysis")
273
296
  else:
274
297
  # Fallback: Basic sentiment analysis
@@ -279,6 +302,7 @@ def analyze_sentiment(state: Dict[str, Any]) -> Dict[str, Any]:
279
302
  sentiment_result["cover_letter_available"] = True if cover_letter else False
280
303
  sentiment_result["text_used"] = "cover_letter" if cover_letter else "resume_sample"
281
304
  sentiment_result["available"] = True
305
+ sentiment_result["screening_data_used"] = screening_data is not None
282
306
 
283
307
  # Update state
284
308
  state["sentiment_score"] = sentiment_result
michael_agent/main.py CHANGED
@@ -16,39 +16,41 @@ from dotenv import load_dotenv
16
16
  load_dotenv()
17
17
 
18
18
  # Then import settings which may depend on environment variables
19
- from .config import settings
19
+ from config import settings
20
20
 
21
21
  # Import our logging utilities early to set up logging
22
- from .utils.logging_utils import workflow_logger as logger
23
-
24
- def ensure_directories():
25
- """Ensure all required directories exist"""
26
- required_directories = [
27
- settings.RESUME_WATCH_DIR,
28
- settings.OUTPUT_DIR,
29
- settings.LOG_DIR,
30
- os.path.join(settings.LOG_DIR, "snapshots"),
31
- settings.LANGGRAPH_CHECKPOINT_DIR,
32
- settings.JOB_DESCRIPTIONS_DIR
33
- ]
34
-
35
- for directory in required_directories:
36
- os.makedirs(directory, exist_ok=True)
37
- logger.info(f"Ensured directory exists: {directory}")
38
-
39
- # Import node tracer after directories are created
40
- from .utils import node_tracer
22
+ from utils.logging_utils import workflow_logger as logger
23
+
24
+ # Ensure all required directories exist
25
+ required_directories = [
26
+ settings.RESUME_WATCH_DIR,
27
+ settings.OUTPUT_DIR,
28
+ settings.LOG_DIR,
29
+ os.path.join(settings.LOG_DIR, "snapshots"),
30
+ settings.LANGGRAPH_CHECKPOINT_DIR,
31
+ settings.JOB_DESCRIPTIONS_DIR
32
+ ]
33
+
34
+ for directory in required_directories:
35
+ os.makedirs(directory, exist_ok=True)
36
+ logger.info(f"Ensured directory exists: {directory}")
37
+
38
+ # Import our node tracer to apply logging to all workflow nodes
39
+ # This must be imported after directories are created but before the workflow starts
40
+ import utils.node_tracer
41
+ from utils.id_mapper import get_timestamp_id, get_all_ids_for_timestamp
41
42
 
42
43
  def start_langgraph():
43
44
  """Start the LangGraph workflow in a separate thread"""
45
+ global langgraph_app # Add a global reference to store the app
44
46
  try:
45
- from .langgraph_workflow.graph_builder import start_workflow
47
+ from langgraph_workflow.graph_builder import start_workflow
46
48
  logger.info("Starting LangGraph workflow engine...")
47
- langgraph_app = start_workflow()
49
+ langgraph_app = start_workflow() # Store the returned app
48
50
 
49
51
  # Keep the thread alive with a simple loop
50
52
  while True:
51
- time.sleep(10)
53
+ time.sleep(10) # Sleep to avoid high CPU usage
52
54
  except Exception as e:
53
55
  logger.error(f"Error starting LangGraph workflow: {str(e)}")
54
56
  logger.error(traceback.format_exc())
@@ -56,14 +58,22 @@ def start_langgraph():
56
58
  def start_flask_dashboard():
57
59
  """Start the Flask dashboard in a separate thread"""
58
60
  try:
59
- from .dashboard.app import app, socketio
60
- port = int(os.getenv("FLASK_PORT", 5000))
61
- logger.info(f"Starting Flask dashboard on port {port}...")
62
- socketio.run(app, host='0.0.0.0', port=port, debug=False, allow_unsafe_werkzeug=True)
61
+ from dashboard.app import app, socketio
62
+ # Use threading instead of eventlet for the background tasks
63
+ import threading
64
+
65
+ # Configure Socket.IO to not use eventlet
66
+ socketio.init_app(app, async_mode='threading')
67
+
68
+ port = int(os.getenv("FLASK_PORT", 8080))
69
+ logger.info(f"Starting Flask dashboard on http://localhost:{port}")
70
+ # Start the Flask app with threading
71
+ socketio.run(app, host='0.0.0.0', port=port, debug=os.getenv("FLASK_ENV") == "development", use_reloader=False, allow_unsafe_werkzeug=True)
63
72
  except Exception as e:
64
73
  logger.error(f"Error starting Flask dashboard: {str(e)}")
65
74
  logger.error(traceback.format_exc())
66
75
 
76
+
67
77
  def main():
68
78
  """Main entry point for the application"""
69
79
  logger.info("=" * 50)
@@ -71,9 +81,7 @@ def main():
71
81
  logger.info(f"Date/Time: {datetime.now().isoformat()}")
72
82
  logger.info("=" * 50)
73
83
 
74
- ensure_directories()
75
-
76
- port = int(os.getenv("FLASK_PORT", 5000))
84
+ port = int(os.getenv("FLASK_PORT", 8080))
77
85
  logger.info(f"Dashboard will be available at: http://localhost:{port}")
78
86
 
79
87
  try:
@@ -84,10 +92,15 @@ def main():
84
92
 
85
93
  # Give LangGraph more time to initialize
86
94
  logger.info("Waiting for LangGraph to initialize...")
87
- time.sleep(5)
95
+ time.sleep(5) # Increased from 2 to 5 seconds
88
96
 
89
97
  logger.info("LangGraph initialization complete")
90
98
 
99
+ # Verify job description directory exists and is accessible
100
+ job_description_dir = "job_descriptions"
101
+ os.makedirs(job_description_dir, exist_ok=True)
102
+ logger.info(f"Job description directory verified: {job_description_dir}")
103
+
91
104
  # Start Flask dashboard in the main thread
92
105
  logger.info("Starting Flask dashboard in main thread...")
93
106
  start_flask_dashboard()
@@ -98,6 +111,49 @@ def main():
98
111
  logger.error(traceback.format_exc())
99
112
  finally:
100
113
  logger.info("SmartRecruitAgent shutting down...")
101
-
114
+
102
115
  if __name__ == "__main__":
103
116
  main()
117
+
118
+ def on_created(self, event):
119
+ """Process newly created resume files"""
120
+ # Skip temporary files and directories
121
+ if event.is_directory or event.src_path.endswith('.tmp'):
122
+ return
123
+
124
+ # Check if the file matches resume extensions
125
+ file_ext = os.path.splitext(event.src_path)[1].lower()
126
+ if file_ext not in self.valid_extensions:
127
+ return
128
+
129
+ try:
130
+ # Create timestamped filename if needed
131
+ filename = os.path.basename(event.src_path)
132
+ if not self.has_timestamp_prefix(filename):
133
+ # Add timestamp as job ID prefix
134
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
135
+ new_filename = f"{timestamp}_{filename}"
136
+ new_path = os.path.join(os.path.dirname(event.src_path), new_filename)
137
+ os.rename(event.src_path, new_path)
138
+ file_path = new_path
139
+ else:
140
+ file_path = event.src_path
141
+
142
+ logger.info(f"New resume detected: {file_path}")
143
+
144
+ # Process the resume through the workflow
145
+ self.processor_func(file_path)
146
+ except Exception as e:
147
+ logger.error(f"Error processing new resume {event.src_path}: {str(e)}")
148
+ logger.error(traceback.format_exc())
149
+
150
+ def has_timestamp_prefix(self, filename):
151
+ """Check if filename has a timestamp prefix in format YYYYMMDDHHMMSS_"""
152
+ # Extract the first part before underscore
153
+ parts = filename.split('_', 1)
154
+ if len(parts) < 2:
155
+ return False
156
+
157
+ prefix = parts[0]
158
+ # Check if prefix is a 14-digit number (YYYYMMDDHHMMSS)
159
+ return len(prefix) == 14 and prefix.isdigit()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: michael_agent
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: SmartRecruitAgent - A recruitment automation library
5
5
  Home-page: https://github.com/yourusername/agent
6
6
  Author: Michael Jone
@@ -1,29 +1,29 @@
1
1
  michael_agent/__init__.py,sha256=prEeI3mdpO8R5QTpniRR_Tl21uqF7pGJHwBidQ9JIKQ,179
2
- michael_agent/main.py,sha256=j5BXOxg_8YE-bnu3cKrylQCHCssZ6PP4UbGorRVJMd4,3385
2
+ michael_agent/main.py,sha256=a_sUmlS2fSZ0dfU0f0ukjhr36cmWESfr_7DG6lot0-s,5790
3
3
  michael_agent/monitor.py,sha256=RThpdPW7lf5zI3ilMShVeDf4Vao5Yq0E4Rao9uuS9XY,2473
4
4
  michael_agent/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  michael_agent/config/settings.py,sha256=_4uvWQnMscK01Sd0zT5wesVW5uN0njtKYRMsjMQXEOY,3180
6
6
  michael_agent/dashboard/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- michael_agent/dashboard/app.py,sha256=UtMswD7TGGJBY9cMeuFQPJAtgaRiXFso6PsTHtvPGN8,61963
7
+ michael_agent/dashboard/app.py,sha256=kArwbsiYR1FzAcxOA_Xzf3uDlFuXj4uDFGvMN9QWuuw,64161
8
8
  michael_agent/dashboard/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  michael_agent/dashboard/static/styles.css,sha256=vSG1saAHIYxw_7sH5z9NipzvkRx3x4kMEjhgx0aUKow,6030
10
10
  michael_agent/dashboard/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- michael_agent/dashboard/templates/career_portal.html,sha256=ImVi7f2lvbv-xgQE7AtRpbrWDA_CbeQj02xGwOtS7-U,24128
11
+ michael_agent/dashboard/templates/career_portal.html,sha256=7hUcKQvAh_U8S0hdnJdzKDzZ-a3vuz3T-hJdp6CqvIU,28419
12
12
  michael_agent/dashboard/templates/dashboard.html,sha256=VepZKTvsaPQaCVR-Nxw00gx3ueiBZMxlIIIxr__6nKU,39877
13
- michael_agent/dashboard/templates/jd_creation.html,sha256=5YqYLFhfQaE8GogWXuZeRQ24IHg7M_J9O3ai5ojhwhE,15931
14
- michael_agent/dashboard/templates/resume_scoring.html,sha256=ylvM9OVGqi-T1XaAR49OYbuV_3KiKa2SswWGe2O5yOc,54843
13
+ michael_agent/dashboard/templates/jd_creation.html,sha256=mACZFlDmPZVvDCibV4xhnlLXMSkNjdFnrAyRLPFKEmk,18725
14
+ michael_agent/dashboard/templates/resume_scoring.html,sha256=m7piNDOs_tKuH9BSGl3DZpsaN8wGVAfnLaUTsd4ByBE,57187
15
15
  michael_agent/dashboard/templates/upload_resume.html,sha256=mG-92r1L3A4N-tEdaSRd35ez92fx6Wg7y9gnvhObc_w,19995
16
16
  michael_agent/langgraph_workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- michael_agent/langgraph_workflow/graph_builder.py,sha256=xdsZ_lVWFn5B8xNXg_L49H-Jwfj-p7nxPVOwtc9Rf2U,14487
17
+ michael_agent/langgraph_workflow/graph_builder.py,sha256=G7bhOxi6QyCCGkF-8D2Q2TkAidT0hoMKGN52lUM8whg,15658
18
18
  michael_agent/langgraph_workflow/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  michael_agent/langgraph_workflow/nodes/assessment_handler.py,sha256=qgMB8fJdyA2CP8VWY2m-7-418LcY302S-SR_JHamSTE,6401
20
- michael_agent/langgraph_workflow/nodes/jd_generator.py,sha256=G8cM3NkGqd44iUAbJJDed9kFjJ-F02_FXB6I_7AE_kA,5105
20
+ michael_agent/langgraph_workflow/nodes/jd_generator.py,sha256=h1seH2JHgvDEJA03oTzzBs7fC63CVuavLuDwFYv3k9M,9364
21
21
  michael_agent/langgraph_workflow/nodes/jd_poster.py,sha256=6F1jQRG_IoiopIOpIDjSpuCE3I6_A7-ZEMkV8FtKXQs,4550
22
- michael_agent/langgraph_workflow/nodes/question_generator.py,sha256=XgDc5f7-ifsJ3UdzB22NjKMqjUcG2_elTZ5LOPGVkt8,11670
22
+ michael_agent/langgraph_workflow/nodes/question_generator.py,sha256=bXCrq6lxeabimp7uAL9ndiLVS3t8pzwVXcaz6lepqqI,12813
23
23
  michael_agent/langgraph_workflow/nodes/recruiter_notifier.py,sha256=xLVhRP1I-QIcO_b0lYLuMnMTpGHAFakG-luPJrhkN6Y,8522
24
- michael_agent/langgraph_workflow/nodes/resume_analyzer.py,sha256=XG4MksqSqhhNwGSfDauIbEpPmxogDJ6skJgR-xpeY0g,24027
24
+ michael_agent/langgraph_workflow/nodes/resume_analyzer.py,sha256=z5X5Mo2dAT1F33dFY_JN_PSKpQYrb0dgV4jOeUMCYD0,31403
25
25
  michael_agent/langgraph_workflow/nodes/resume_ingestor.py,sha256=h14J4AcFk22BWoFHCPRkK3HpzY8RvwGW6_jjqBxLXNU,9279
26
- michael_agent/langgraph_workflow/nodes/sentiment_analysis.py,sha256=H-geV4AbFbt1EpiLKnpaXdvrrjjXMN-Dzzg4sZOjhdM,11657
26
+ michael_agent/langgraph_workflow/nodes/sentiment_analysis.py,sha256=St2opmz3naiCw9Z0b4CVMWXOdYCDpdV55LMaigZOJj4,13017
27
27
  michael_agent/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  michael_agent/utils/email_utils.py,sha256=PsL3QTQuV_iVou_2Y3o_Dohz7tN9YNp9FPxsTKkDRv0,4989
29
29
  michael_agent/utils/id_mapper.py,sha256=GzYRuAhGWf2BUAb9hVMS3KR8bmYExnmXRWkQ_j-kWaw,397
@@ -32,7 +32,7 @@ michael_agent/utils/lms_api.py,sha256=tmntU6tjyAdMLak_vfoxBkWNIPUKvejeEwb2t6yQBU
32
32
  michael_agent/utils/logging_utils.py,sha256=Ld7fs2uuCOM0bx-totxHzKzKHl5lfAe3TXeH1QYJBjw,7179
33
33
  michael_agent/utils/monitor_utils.py,sha256=1Ig6C79bQ_OOLKhgFNmm0ybntQavqzyJ3zsxD0iZxxw,11069
34
34
  michael_agent/utils/node_tracer.py,sha256=N1MWly4qfzh87Fo1xRS5hpefoAvfSyZIPvMOegPrtBY,3411
35
- michael_agent-1.0.4.dist-info/METADATA,sha256=sqpBqSp6klOnKtRHLKwTKWswuhQwEo7P6fv-QW3Y33s,1340
36
- michael_agent-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- michael_agent-1.0.4.dist-info/top_level.txt,sha256=-r35JOIHnK3RsMhJ77tDKfWtmfGDr_iT2642k-suUDo,14
38
- michael_agent-1.0.4.dist-info/RECORD,,
35
+ michael_agent-1.0.5.dist-info/METADATA,sha256=hUavXJcuPMKXoDvQ6PBuXgNgcW2p21Lkjf_Xts1SrzY,1340
36
+ michael_agent-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ michael_agent-1.0.5.dist-info/top_level.txt,sha256=-r35JOIHnK3RsMhJ77tDKfWtmfGDr_iT2642k-suUDo,14
38
+ michael_agent-1.0.5.dist-info/RECORD,,