michael-agent 1.0.3__tar.gz → 1.0.5__tar.gz

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.
Files changed (47) hide show
  1. michael_agent-1.0.5/MANIFEST.in +4 -0
  2. {michael_agent-1.0.3 → michael_agent-1.0.5}/PKG-INFO +1 -1
  3. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/app.py +69 -29
  4. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/templates/career_portal.html +79 -6
  5. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/templates/jd_creation.html +44 -1
  6. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/templates/resume_scoring.html +44 -0
  7. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/graph_builder.py +23 -1
  8. michael_agent-1.0.5/michael_agent/langgraph_workflow/nodes/jd_generator.py +226 -0
  9. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/question_generator.py +31 -9
  10. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/resume_analyzer.py +183 -5
  11. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/sentiment_analysis.py +29 -5
  12. michael_agent-1.0.5/michael_agent/main.py +159 -0
  13. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent.egg-info/PKG-INFO +1 -1
  14. {michael_agent-1.0.3 → michael_agent-1.0.5}/setup.py +1 -1
  15. michael_agent-1.0.3/MANIFEST.in +0 -3
  16. michael_agent-1.0.3/michael_agent/langgraph_workflow/nodes/jd_generator.py +0 -139
  17. michael_agent-1.0.3/michael_agent/main.py +0 -103
  18. {michael_agent-1.0.3 → michael_agent-1.0.5}/README.md +0 -0
  19. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/__init__.py +0 -0
  20. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/config/__init__.py +0 -0
  21. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/config/settings.py +0 -0
  22. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/__init__.py +0 -0
  23. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/static/__init__.py +0 -0
  24. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/static/styles.css +0 -0
  25. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/templates/__init__.py +0 -0
  26. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/templates/dashboard.html +0 -0
  27. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/dashboard/templates/upload_resume.html +0 -0
  28. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/__init__.py +0 -0
  29. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/__init__.py +0 -0
  30. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/assessment_handler.py +0 -0
  31. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/jd_poster.py +0 -0
  32. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/recruiter_notifier.py +0 -0
  33. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/langgraph_workflow/nodes/resume_ingestor.py +0 -0
  34. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/monitor.py +0 -0
  35. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/__init__.py +0 -0
  36. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/email_utils.py +0 -0
  37. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/id_mapper.py +0 -0
  38. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/jd_utils.py +0 -0
  39. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/lms_api.py +0 -0
  40. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/logging_utils.py +0 -0
  41. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/monitor_utils.py +0 -0
  42. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent/utils/node_tracer.py +0 -0
  43. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent.egg-info/SOURCES.txt +0 -0
  44. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent.egg-info/dependency_links.txt +0 -0
  45. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent.egg-info/requires.txt +0 -0
  46. {michael_agent-1.0.3 → michael_agent-1.0.5}/michael_agent.egg-info/top_level.txt +0 -0
  47. {michael_agent-1.0.3 → michael_agent-1.0.5}/setup.cfg +0 -0
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ recursive-include michael_agent/dashboard/templates *
3
+ recursive-include michael_agent/dashboard/static *
4
+ recursive-include michael_agent/requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: michael_agent
3
- Version: 1.0.3
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
@@ -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}")