michael-agent 1.0.1__py3-none-any.whl → 1.0.3__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.
- michael_agent/dashboard/static/__init__.py +0 -0
- michael_agent/dashboard/static/styles.css +311 -0
- michael_agent/dashboard/templates/__init__.py +0 -0
- michael_agent/dashboard/templates/career_portal.html +482 -0
- michael_agent/dashboard/templates/dashboard.html +807 -0
- michael_agent/dashboard/templates/jd_creation.html +318 -0
- michael_agent/dashboard/templates/resume_scoring.html +1032 -0
- michael_agent/dashboard/templates/upload_resume.html +411 -0
- michael_agent/langgraph_workflow/nodes/__init__.py +0 -0
- michael_agent/langgraph_workflow/nodes/assessment_handler.py +177 -0
- michael_agent/langgraph_workflow/nodes/jd_generator.py +139 -0
- michael_agent/langgraph_workflow/nodes/jd_poster.py +156 -0
- michael_agent/langgraph_workflow/nodes/question_generator.py +295 -0
- michael_agent/langgraph_workflow/nodes/recruiter_notifier.py +224 -0
- michael_agent/langgraph_workflow/nodes/resume_analyzer.py +631 -0
- michael_agent/langgraph_workflow/nodes/resume_ingestor.py +225 -0
- michael_agent/langgraph_workflow/nodes/sentiment_analysis.py +309 -0
- {michael_agent-1.0.1.dist-info → michael_agent-1.0.3.dist-info}/METADATA +1 -1
- michael_agent-1.0.3.dist-info/RECORD +38 -0
- michael_agent-1.0.1.dist-info/RECORD +0 -21
- {michael_agent-1.0.1.dist-info → michael_agent-1.0.3.dist-info}/WHEEL +0 -0
- {michael_agent-1.0.1.dist-info → michael_agent-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,411 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Upload Resumes - SmartRecruitAgent</title>
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
9
|
+
<link rel="stylesheet" href="/static/styles.css">
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
<header class="bg-dark text-white p-3">
|
13
|
+
<div class="container">
|
14
|
+
<div class="d-flex justify-content-between align-items-center">
|
15
|
+
<h1 class="h3 mb-0">SmartRecruitAgent - Resume Upload</h1>
|
16
|
+
<div>
|
17
|
+
<a href="/career-portal" class="btn btn-outline-light me-2">Career Portal</a>
|
18
|
+
<a href="/" class="btn btn-outline-light">Dashboard</a>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</header>
|
23
|
+
|
24
|
+
<div class="container mt-4">
|
25
|
+
<div class="row">
|
26
|
+
<div class="col-md-8 mx-auto">
|
27
|
+
<div class="card mb-4">
|
28
|
+
<div class="card-header bg-primary text-white">
|
29
|
+
Upload Resumes
|
30
|
+
</div>
|
31
|
+
<div class="card-body">
|
32
|
+
<form id="resume-upload-form">
|
33
|
+
<div class="mb-4">
|
34
|
+
<label for="job-select" class="form-label">Select Target Job Position*</label>
|
35
|
+
<select class="form-select" id="job-select" name="job_id" required>
|
36
|
+
<option value="" selected>Loading jobs...</option>
|
37
|
+
</select>
|
38
|
+
<div class="form-text">Choose the job position these resumes are for</div>
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div class="mb-4">
|
42
|
+
<label class="form-label">Upload Resume Files*</label>
|
43
|
+
<div class="upload-area p-4 border rounded text-center" id="drop-area">
|
44
|
+
<i class="bi bi-cloud-arrow-up display-4 text-primary"></i>
|
45
|
+
<h5 class="mt-3">Drag & Drop Files Here</h5>
|
46
|
+
<p class="text-muted">Or click to browse files</p>
|
47
|
+
<input type="file" id="resume-files" name="resume_files" class="d-none" multiple accept=".pdf,.doc,.docx">
|
48
|
+
<button type="button" class="btn btn-outline-primary mt-2" id="browse-files">Browse Files</button>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<div id="file-list-container" class="mb-4" style="display: none;">
|
53
|
+
<label class="form-label">Selected Files</label>
|
54
|
+
<div class="list-group" id="file-list">
|
55
|
+
<!-- Files will be listed here -->
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<div class="mb-4">
|
60
|
+
<div class="form-check">
|
61
|
+
<input class="form-check-input" type="checkbox" id="auto-process" name="auto_process" checked>
|
62
|
+
<label class="form-check-label" for="auto-process">
|
63
|
+
Automatically process resumes after upload
|
64
|
+
</label>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
|
68
|
+
<div class="text-center">
|
69
|
+
<button type="submit" class="btn btn-primary" id="upload-btn">
|
70
|
+
<span class="spinner-border spinner-border-sm d-none" id="upload-spinner" role="status" aria-hidden="true"></span>
|
71
|
+
Upload Resumes
|
72
|
+
</button>
|
73
|
+
</div>
|
74
|
+
</form>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
|
78
|
+
<div class="card mb-4" id="upload-progress-card" style="display: none;">
|
79
|
+
<div class="card-header bg-info text-white">
|
80
|
+
Upload Progress
|
81
|
+
</div>
|
82
|
+
<div class="card-body">
|
83
|
+
<div class="d-flex justify-content-between mb-2">
|
84
|
+
<span id="progress-text">Uploading files...</span>
|
85
|
+
<span id="progress-percent">0%</span>
|
86
|
+
</div>
|
87
|
+
<div class="progress">
|
88
|
+
<div class="progress-bar progress-bar-striped progress-bar-animated" id="upload-progress-bar" role="progressbar" style="width: 0%"></div>
|
89
|
+
</div>
|
90
|
+
<div class="mt-3 small" id="file-status-container">
|
91
|
+
<!-- Individual file statuses will be shown here -->
|
92
|
+
</div>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
|
96
|
+
<div class="card" id="upload-result-card" style="display: none;">
|
97
|
+
<div class="card-header bg-success text-white">
|
98
|
+
Upload Complete
|
99
|
+
</div>
|
100
|
+
<div class="card-body">
|
101
|
+
<div class="text-center mb-4">
|
102
|
+
<i class="bi bi-check-circle-fill text-success display-1"></i>
|
103
|
+
<h4 class="mt-3">Resumes Uploaded Successfully</h4>
|
104
|
+
<p id="result-summary">0 files have been uploaded and queued for processing.</p>
|
105
|
+
</div>
|
106
|
+
<div class="d-flex justify-content-center gap-3">
|
107
|
+
<a href="/resume-scoring" class="btn btn-primary">View Processing Results</a>
|
108
|
+
<button type="button" class="btn btn-outline-secondary" id="upload-more-btn">Upload More Resumes</button>
|
109
|
+
</div>
|
110
|
+
</div>
|
111
|
+
</div>
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
</div>
|
115
|
+
|
116
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
117
|
+
<script>
|
118
|
+
document.addEventListener('DOMContentLoaded', function() {
|
119
|
+
// File list for tracking uploaded files
|
120
|
+
let selectedFiles = [];
|
121
|
+
|
122
|
+
// Load job listings for the dropdown
|
123
|
+
fetch('/api/jobs')
|
124
|
+
.then(response => response.json())
|
125
|
+
.then(data => {
|
126
|
+
const jobSelect = document.getElementById('job-select');
|
127
|
+
jobSelect.innerHTML = '';
|
128
|
+
|
129
|
+
if (data.jobs.length === 0) {
|
130
|
+
const option = document.createElement('option');
|
131
|
+
option.value = '';
|
132
|
+
option.textContent = 'No jobs available - create a job description first';
|
133
|
+
jobSelect.appendChild(option);
|
134
|
+
document.getElementById('upload-btn').disabled = true;
|
135
|
+
} else {
|
136
|
+
const defaultOption = document.createElement('option');
|
137
|
+
defaultOption.value = '';
|
138
|
+
defaultOption.textContent = 'Select a job position';
|
139
|
+
jobSelect.appendChild(defaultOption);
|
140
|
+
|
141
|
+
data.jobs.forEach(job => {
|
142
|
+
const option = document.createElement('option');
|
143
|
+
option.value = job.id;
|
144
|
+
option.textContent = job.title;
|
145
|
+
jobSelect.appendChild(option);
|
146
|
+
});
|
147
|
+
}
|
148
|
+
})
|
149
|
+
.catch(error => {
|
150
|
+
console.error('Error loading jobs:', error);
|
151
|
+
document.getElementById('job-select').innerHTML =
|
152
|
+
'<option value="">Error loading jobs</option>';
|
153
|
+
});
|
154
|
+
|
155
|
+
// File input handling
|
156
|
+
document.getElementById('browse-files').addEventListener('click', function() {
|
157
|
+
document.getElementById('resume-files').click();
|
158
|
+
});
|
159
|
+
|
160
|
+
document.getElementById('resume-files').addEventListener('change', function(e) {
|
161
|
+
handleFileSelection(this.files);
|
162
|
+
});
|
163
|
+
|
164
|
+
// Drag and drop handling
|
165
|
+
const dropArea = document.getElementById('drop-area');
|
166
|
+
|
167
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
168
|
+
dropArea.addEventListener(eventName, preventDefaults, false);
|
169
|
+
});
|
170
|
+
|
171
|
+
function preventDefaults(e) {
|
172
|
+
e.preventDefault();
|
173
|
+
e.stopPropagation();
|
174
|
+
}
|
175
|
+
|
176
|
+
['dragenter', 'dragover'].forEach(eventName => {
|
177
|
+
dropArea.addEventListener(eventName, highlight, false);
|
178
|
+
});
|
179
|
+
|
180
|
+
['dragleave', 'drop'].forEach(eventName => {
|
181
|
+
dropArea.addEventListener(eventName, unhighlight, false);
|
182
|
+
});
|
183
|
+
|
184
|
+
function highlight() {
|
185
|
+
dropArea.classList.add('border-primary');
|
186
|
+
}
|
187
|
+
|
188
|
+
function unhighlight() {
|
189
|
+
dropArea.classList.remove('border-primary');
|
190
|
+
}
|
191
|
+
|
192
|
+
dropArea.addEventListener('drop', function(e) {
|
193
|
+
const dt = e.dataTransfer;
|
194
|
+
const files = dt.files;
|
195
|
+
handleFileSelection(files);
|
196
|
+
});
|
197
|
+
|
198
|
+
// Handle selected files
|
199
|
+
function handleFileSelection(files) {
|
200
|
+
if (files.length === 0) return;
|
201
|
+
|
202
|
+
// Filter only valid file types
|
203
|
+
const validFiles = Array.from(files).filter(file => {
|
204
|
+
const fileType = file.type.toLowerCase();
|
205
|
+
return fileType === 'application/pdf' ||
|
206
|
+
fileType === 'application/msword' ||
|
207
|
+
fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
208
|
+
});
|
209
|
+
|
210
|
+
if (validFiles.length === 0) {
|
211
|
+
alert('Please select valid resume files (PDF, DOC, DOCX)');
|
212
|
+
return;
|
213
|
+
}
|
214
|
+
|
215
|
+
// Add to selected files array
|
216
|
+
selectedFiles = [...selectedFiles, ...validFiles];
|
217
|
+
|
218
|
+
// Display file list
|
219
|
+
updateFileList();
|
220
|
+
}
|
221
|
+
|
222
|
+
function updateFileList() {
|
223
|
+
const fileListContainer = document.getElementById('file-list-container');
|
224
|
+
const fileList = document.getElementById('file-list');
|
225
|
+
|
226
|
+
if (selectedFiles.length === 0) {
|
227
|
+
fileListContainer.style.display = 'none';
|
228
|
+
return;
|
229
|
+
}
|
230
|
+
|
231
|
+
fileListContainer.style.display = 'block';
|
232
|
+
fileList.innerHTML = '';
|
233
|
+
|
234
|
+
selectedFiles.forEach((file, index) => {
|
235
|
+
const item = document.createElement('div');
|
236
|
+
item.className = 'list-group-item d-flex justify-content-between align-items-center';
|
237
|
+
|
238
|
+
const icon = file.type.includes('pdf') ? 'bi-file-earmark-pdf' : 'bi-file-earmark-word';
|
239
|
+
|
240
|
+
item.innerHTML = `
|
241
|
+
<div>
|
242
|
+
<i class="bi ${icon} me-2"></i>
|
243
|
+
<span>${file.name}</span>
|
244
|
+
<small class="text-muted ms-2">${formatFileSize(file.size)}</small>
|
245
|
+
</div>
|
246
|
+
<button type="button" class="btn btn-sm btn-outline-danger remove-file" data-index="${index}">
|
247
|
+
<i class="bi bi-trash"></i>
|
248
|
+
</button>
|
249
|
+
`;
|
250
|
+
|
251
|
+
fileList.appendChild(item);
|
252
|
+
});
|
253
|
+
|
254
|
+
// Add remove handlers
|
255
|
+
document.querySelectorAll('.remove-file').forEach(btn => {
|
256
|
+
btn.addEventListener('click', function() {
|
257
|
+
const index = parseInt(this.getAttribute('data-index'));
|
258
|
+
selectedFiles.splice(index, 1);
|
259
|
+
updateFileList();
|
260
|
+
});
|
261
|
+
});
|
262
|
+
}
|
263
|
+
|
264
|
+
function formatFileSize(bytes) {
|
265
|
+
if (bytes === 0) return '0 Bytes';
|
266
|
+
|
267
|
+
const k = 1024;
|
268
|
+
const sizes = ['Bytes', 'KB', 'MB'];
|
269
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
270
|
+
|
271
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
272
|
+
}
|
273
|
+
|
274
|
+
// Form submission
|
275
|
+
document.getElementById('resume-upload-form').addEventListener('submit', function(e) {
|
276
|
+
e.preventDefault();
|
277
|
+
|
278
|
+
const jobId = document.getElementById('job-select').value;
|
279
|
+
if (!jobId) {
|
280
|
+
alert('Please select a job position');
|
281
|
+
return;
|
282
|
+
}
|
283
|
+
|
284
|
+
if (selectedFiles.length === 0) {
|
285
|
+
alert('Please select at least one resume file');
|
286
|
+
return;
|
287
|
+
}
|
288
|
+
|
289
|
+
// Show upload progress card
|
290
|
+
document.getElementById('upload-progress-card').style.display = 'block';
|
291
|
+
document.getElementById('upload-progress-card').scrollIntoView({ behavior: 'smooth' });
|
292
|
+
|
293
|
+
// Disable submit button and show spinner
|
294
|
+
const uploadBtn = document.getElementById('upload-btn');
|
295
|
+
const uploadSpinner = document.getElementById('upload-spinner');
|
296
|
+
uploadBtn.disabled = true;
|
297
|
+
uploadSpinner.classList.remove('d-none');
|
298
|
+
|
299
|
+
// Create FormData for file upload
|
300
|
+
const formData = new FormData();
|
301
|
+
formData.append('job_id', jobId);
|
302
|
+
formData.append('auto_process', document.getElementById('auto-process').checked);
|
303
|
+
|
304
|
+
// Add all files
|
305
|
+
selectedFiles.forEach((file, index) => {
|
306
|
+
formData.append('resume_files', file);
|
307
|
+
});
|
308
|
+
|
309
|
+
// Simulate progress for demo
|
310
|
+
let progress = 0;
|
311
|
+
const progressBar = document.getElementById('upload-progress-bar');
|
312
|
+
const progressText = document.getElementById('progress-text');
|
313
|
+
const progressPercent = document.getElementById('progress-percent');
|
314
|
+
const fileStatusContainer = document.getElementById('file-status-container');
|
315
|
+
|
316
|
+
// Clear previous file statuses
|
317
|
+
fileStatusContainer.innerHTML = '';
|
318
|
+
|
319
|
+
// Add file status indicators
|
320
|
+
selectedFiles.forEach(file => {
|
321
|
+
const statusDiv = document.createElement('div');
|
322
|
+
statusDiv.className = 'd-flex justify-content-between mb-2';
|
323
|
+
statusDiv.innerHTML = `
|
324
|
+
<span>${file.name}</span>
|
325
|
+
<span class="badge bg-secondary">Pending</span>
|
326
|
+
`;
|
327
|
+
fileStatusContainer.appendChild(statusDiv);
|
328
|
+
});
|
329
|
+
|
330
|
+
// For demonstration, let's simulate the upload with progress
|
331
|
+
const progressInterval = setInterval(() => {
|
332
|
+
if (progress >= 100) {
|
333
|
+
clearInterval(progressInterval);
|
334
|
+
showUploadResults(selectedFiles.length);
|
335
|
+
return;
|
336
|
+
}
|
337
|
+
|
338
|
+
progress += 5;
|
339
|
+
progressBar.style.width = `${progress}%`;
|
340
|
+
progressPercent.textContent = `${progress}%`;
|
341
|
+
|
342
|
+
// Update file status randomly as we progress
|
343
|
+
if (progress > 30) {
|
344
|
+
const statuses = fileStatusContainer.querySelectorAll('.badge');
|
345
|
+
const randomIndex = Math.floor(Math.random() * statuses.length);
|
346
|
+
|
347
|
+
if (statuses[randomIndex].textContent === 'Pending') {
|
348
|
+
statuses[randomIndex].textContent = 'Uploading';
|
349
|
+
statuses[randomIndex].className = 'badge bg-info';
|
350
|
+
} else if (statuses[randomIndex].textContent === 'Uploading' && progress > 70) {
|
351
|
+
statuses[randomIndex].textContent = 'Complete';
|
352
|
+
statuses[randomIndex].className = 'badge bg-success';
|
353
|
+
}
|
354
|
+
}
|
355
|
+
|
356
|
+
// When progress is complete, update all remaining badges
|
357
|
+
if (progress === 100) {
|
358
|
+
const pendingStatuses = fileStatusContainer.querySelectorAll('.badge:not(.bg-success)');
|
359
|
+
pendingStatuses.forEach(badge => {
|
360
|
+
badge.textContent = 'Complete';
|
361
|
+
badge.className = 'badge bg-success';
|
362
|
+
});
|
363
|
+
progressText.textContent = 'Upload complete!';
|
364
|
+
}
|
365
|
+
}, 200);
|
366
|
+
|
367
|
+
// Remove simulation code and uncomment the actual implementation
|
368
|
+
fetch('/api/upload-resumes', {
|
369
|
+
method: 'POST',
|
370
|
+
body: formData
|
371
|
+
})
|
372
|
+
.then(response => response.json())
|
373
|
+
.then(data => {
|
374
|
+
if (data.success) {
|
375
|
+
showUploadResults(data.uploaded_count);
|
376
|
+
} else {
|
377
|
+
alert('Error uploading resumes: ' + data.error);
|
378
|
+
}
|
379
|
+
})
|
380
|
+
.catch(error => {
|
381
|
+
console.error('Error:', error);
|
382
|
+
alert('Error uploading resumes');
|
383
|
+
})
|
384
|
+
.finally(() => {
|
385
|
+
uploadBtn.disabled = false;
|
386
|
+
uploadSpinner.classList.add('d-none');
|
387
|
+
});
|
388
|
+
});
|
389
|
+
|
390
|
+
function showUploadResults(count) {
|
391
|
+
document.getElementById('result-summary').textContent =
|
392
|
+
`${count} files have been uploaded and queued for processing.`;
|
393
|
+
|
394
|
+
document.getElementById('upload-result-card').style.display = 'block';
|
395
|
+
document.getElementById('upload-result-card').scrollIntoView({ behavior: 'smooth' });
|
396
|
+
|
397
|
+
// Reset form for new uploads
|
398
|
+
document.getElementById('upload-more-btn').addEventListener('click', function() {
|
399
|
+
selectedFiles = [];
|
400
|
+
updateFileList();
|
401
|
+
document.getElementById('resume-upload-form').reset();
|
402
|
+
document.getElementById('upload-progress-card').style.display = 'none';
|
403
|
+
document.getElementById('upload-result-card').style.display = 'none';
|
404
|
+
document.getElementById('upload-btn').disabled = false;
|
405
|
+
document.getElementById('upload-spinner').classList.add('d-none');
|
406
|
+
});
|
407
|
+
}
|
408
|
+
});
|
409
|
+
</script>
|
410
|
+
</body>
|
411
|
+
</html>
|
File without changes
|
@@ -0,0 +1,177 @@
|
|
1
|
+
"""
|
2
|
+
Assessment Handler Node
|
3
|
+
Sends assessments to candidates via email or LMS integration
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Dict, Any, List
|
8
|
+
|
9
|
+
from utils.email_utils import send_email
|
10
|
+
from utils.lms_api import get_lms_client
|
11
|
+
|
12
|
+
from config import settings
|
13
|
+
|
14
|
+
# Configure logging
|
15
|
+
logging.basicConfig(level=logging.INFO)
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
def create_assessment_email(candidate_name: str, position_name: str, test_link: str) -> Dict[str, str]:
|
19
|
+
"""Create email content for assessment"""
|
20
|
+
subject = f"Assessment for {position_name} Position"
|
21
|
+
|
22
|
+
plain_text = f"""
|
23
|
+
Hello {candidate_name},
|
24
|
+
|
25
|
+
Thank you for your interest in the {position_name} position. As part of our evaluation process,
|
26
|
+
we'd like you to complete an assessment.
|
27
|
+
|
28
|
+
Please click the link below to start the assessment:
|
29
|
+
{test_link}
|
30
|
+
|
31
|
+
The assessment should take approximately 60 minutes to complete.
|
32
|
+
|
33
|
+
Best regards,
|
34
|
+
Recruitment Team
|
35
|
+
"""
|
36
|
+
|
37
|
+
html_content = f"""
|
38
|
+
<html>
|
39
|
+
<body>
|
40
|
+
<p>Hello {candidate_name},</p>
|
41
|
+
<p>Thank you for your interest in the <strong>{position_name}</strong> position. As part of our evaluation process,
|
42
|
+
we'd like you to complete an assessment.</p>
|
43
|
+
<p>Please click the link below to start the assessment:</p>
|
44
|
+
<p><a href="{test_link}">{test_link}</a></p>
|
45
|
+
<p>The assessment should take approximately 60 minutes to complete.</p>
|
46
|
+
<p>Best regards,<br>Recruitment Team</p>
|
47
|
+
</body>
|
48
|
+
</html>
|
49
|
+
"""
|
50
|
+
|
51
|
+
return {
|
52
|
+
"subject": subject,
|
53
|
+
"plain_text": plain_text,
|
54
|
+
"html_content": html_content
|
55
|
+
}
|
56
|
+
|
57
|
+
def handle_assessment(state: Dict[str, Any]) -> Dict[str, Any]:
|
58
|
+
"""
|
59
|
+
LangGraph node to handle sending assessments to candidates
|
60
|
+
|
61
|
+
Args:
|
62
|
+
state: The current workflow state
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Updated workflow state with assessment status
|
66
|
+
"""
|
67
|
+
logger.info("Starting assessment handler")
|
68
|
+
|
69
|
+
# Check if candidate email exists in state
|
70
|
+
candidate_email = state.get("candidate_email")
|
71
|
+
candidate_name = state.get("candidate_name", "Candidate")
|
72
|
+
|
73
|
+
if not candidate_email:
|
74
|
+
error_message = "Missing candidate email for assessment"
|
75
|
+
logger.error(error_message)
|
76
|
+
state["errors"].append({
|
77
|
+
"step": "assessment_handler",
|
78
|
+
"error": error_message
|
79
|
+
})
|
80
|
+
state["assessment"] = {"status": "failed", "reason": "missing_email"}
|
81
|
+
return state
|
82
|
+
|
83
|
+
# Get job position name
|
84
|
+
job_data = state.get("job_description", {})
|
85
|
+
position_name = job_data.get("position", "open position")
|
86
|
+
|
87
|
+
try:
|
88
|
+
# Check if we should use LMS integration
|
89
|
+
if settings.LMS_API_URL and settings.LMS_API_KEY:
|
90
|
+
# Send assessment via LMS
|
91
|
+
lms_client = get_lms_client()
|
92
|
+
lms_result = lms_client.send_assessment(
|
93
|
+
candidate_email=candidate_email,
|
94
|
+
candidate_name=candidate_name,
|
95
|
+
position_name=position_name
|
96
|
+
)
|
97
|
+
|
98
|
+
if lms_result.get("success"):
|
99
|
+
state["assessment"] = {
|
100
|
+
"status": "sent",
|
101
|
+
"method": "lms",
|
102
|
+
"lms_type": settings.LMS_TYPE,
|
103
|
+
"assessment_id": lms_result.get("assessment_id"),
|
104
|
+
"invitation_id": lms_result.get("invitation_id")
|
105
|
+
}
|
106
|
+
logger.info(f"Assessment sent to {candidate_email} via LMS")
|
107
|
+
else:
|
108
|
+
# LMS failed, fall back to email
|
109
|
+
logger.warning(f"LMS assessment failed: {lms_result.get('error')}, falling back to email")
|
110
|
+
assessment_link = f"https://example.com/assessment?id={state['job_id']}"
|
111
|
+
email_content = create_assessment_email(candidate_name, position_name, assessment_link)
|
112
|
+
|
113
|
+
email_sent = send_email(
|
114
|
+
recipient_email=candidate_email,
|
115
|
+
subject=email_content["subject"],
|
116
|
+
body=email_content["plain_text"],
|
117
|
+
html_content=email_content["html_content"]
|
118
|
+
)
|
119
|
+
|
120
|
+
state["assessment"] = {
|
121
|
+
"status": "sent" if email_sent else "failed",
|
122
|
+
"method": "email",
|
123
|
+
"assessment_link": assessment_link
|
124
|
+
}
|
125
|
+
|
126
|
+
if email_sent:
|
127
|
+
logger.info(f"Assessment email sent to {candidate_email}")
|
128
|
+
else:
|
129
|
+
logger.error(f"Failed to send assessment email to {candidate_email}")
|
130
|
+
state["errors"].append({
|
131
|
+
"step": "assessment_handler",
|
132
|
+
"error": "Failed to send assessment email"
|
133
|
+
})
|
134
|
+
else:
|
135
|
+
# No LMS configured, send via email
|
136
|
+
assessment_link = f"https://example.com/assessment?id={state['job_id']}"
|
137
|
+
email_content = create_assessment_email(candidate_name, position_name, assessment_link)
|
138
|
+
|
139
|
+
email_sent = send_email(
|
140
|
+
recipient_email=candidate_email,
|
141
|
+
subject=email_content["subject"],
|
142
|
+
body=email_content["plain_text"],
|
143
|
+
html_content=email_content["html_content"]
|
144
|
+
)
|
145
|
+
|
146
|
+
state["assessment"] = {
|
147
|
+
"status": "sent" if email_sent else "failed",
|
148
|
+
"method": "email",
|
149
|
+
"assessment_link": assessment_link
|
150
|
+
}
|
151
|
+
|
152
|
+
if email_sent:
|
153
|
+
logger.info(f"Assessment email sent to {candidate_email}")
|
154
|
+
else:
|
155
|
+
logger.error(f"Failed to send assessment email to {candidate_email}")
|
156
|
+
state["errors"].append({
|
157
|
+
"step": "assessment_handler",
|
158
|
+
"error": "Failed to send assessment email"
|
159
|
+
})
|
160
|
+
|
161
|
+
state["status"] = "assessment_handled"
|
162
|
+
|
163
|
+
except Exception as e:
|
164
|
+
error_message = f"Error sending assessment: {str(e)}"
|
165
|
+
logger.error(error_message)
|
166
|
+
|
167
|
+
state["errors"].append({
|
168
|
+
"step": "assessment_handler",
|
169
|
+
"error": error_message
|
170
|
+
})
|
171
|
+
|
172
|
+
state["assessment"] = {
|
173
|
+
"status": "failed",
|
174
|
+
"reason": str(e)
|
175
|
+
}
|
176
|
+
|
177
|
+
return state
|