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,224 @@
|
|
1
|
+
"""
|
2
|
+
Recruiter Notifier Node
|
3
|
+
Composes and sends emails to recruiters with candidate summary
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Dict, Any, List
|
8
|
+
|
9
|
+
from utils.email_utils import send_email
|
10
|
+
from config import settings
|
11
|
+
|
12
|
+
# Configure logging
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
def create_recruiter_email_content(state: Dict[str, Any]) -> Dict[str, str]:
|
17
|
+
"""Create email content for recruiter notification"""
|
18
|
+
# Get data from state
|
19
|
+
candidate_name = state.get("candidate_name", "Unknown Candidate")
|
20
|
+
job_data = state.get("job_description", {})
|
21
|
+
position_name = job_data.get("position", "Unspecified Position")
|
22
|
+
relevance_score = state.get("relevance_score", 0)
|
23
|
+
relevance_percentage = int(relevance_score * 100)
|
24
|
+
sentiment_data = state.get("sentiment_score", {})
|
25
|
+
sentiment = sentiment_data.get("sentiment", "neutral")
|
26
|
+
resume_data = state.get("resume_data", {})
|
27
|
+
|
28
|
+
# Format interview questions
|
29
|
+
interview_questions = state.get("interview_questions", {})
|
30
|
+
tech_questions = interview_questions.get("technical_questions", [])
|
31
|
+
behavioral_questions = interview_questions.get("behavioral_questions", [])
|
32
|
+
|
33
|
+
# Create email subject
|
34
|
+
subject = f"Candidate Assessment: {candidate_name} for {position_name} ({relevance_percentage}% Match)"
|
35
|
+
|
36
|
+
# Create plain text email
|
37
|
+
plain_text = f"""
|
38
|
+
Candidate Assessment Report
|
39
|
+
|
40
|
+
Candidate: {candidate_name}
|
41
|
+
Position: {position_name}
|
42
|
+
Match Score: {relevance_percentage}%
|
43
|
+
Sentiment Analysis: {sentiment.capitalize()}
|
44
|
+
|
45
|
+
Resume Summary:
|
46
|
+
- Email: {resume_data.get('email', 'Not provided')}
|
47
|
+
- Phone: {resume_data.get('phone', 'Not provided')}
|
48
|
+
|
49
|
+
Assessment Status: {state.get('assessment', {}).get('status', 'Not sent')}
|
50
|
+
|
51
|
+
Recommended Technical Interview Questions:
|
52
|
+
{_format_questions_text(tech_questions[:3])}
|
53
|
+
|
54
|
+
Recommended Behavioral Questions:
|
55
|
+
{_format_questions_text(behavioral_questions[:3])}
|
56
|
+
|
57
|
+
View the full candidate profile in the dashboard.
|
58
|
+
"""
|
59
|
+
|
60
|
+
# Create HTML email content
|
61
|
+
sentiment_color = {
|
62
|
+
"positive": "green",
|
63
|
+
"neutral": "gray",
|
64
|
+
"negative": "orange"
|
65
|
+
}.get(sentiment, "gray")
|
66
|
+
|
67
|
+
html_content = f"""
|
68
|
+
<html>
|
69
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
70
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
71
|
+
<h1 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">Candidate Assessment Report</h1>
|
72
|
+
|
73
|
+
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
|
74
|
+
<tr>
|
75
|
+
<td style="padding: 8px; width: 30%;"><strong>Candidate:</strong></td>
|
76
|
+
<td style="padding: 8px;">{candidate_name}</td>
|
77
|
+
</tr>
|
78
|
+
<tr>
|
79
|
+
<td style="padding: 8px;"><strong>Position:</strong></td>
|
80
|
+
<td style="padding: 8px;">{position_name}</td>
|
81
|
+
</tr>
|
82
|
+
<tr>
|
83
|
+
<td style="padding: 8px;"><strong>Match Score:</strong></td>
|
84
|
+
<td style="padding: 8px;">
|
85
|
+
<div style="background-color: #f0f0f0; border-radius: 10px; height: 20px; width: 200px;">
|
86
|
+
<div style="background-color: #3498db; border-radius: 10px; height: 20px; width: {relevance_percentage*2}px;"></div>
|
87
|
+
</div>
|
88
|
+
<span style="margin-left: 10px;">{relevance_percentage}%</span>
|
89
|
+
</td>
|
90
|
+
</tr>
|
91
|
+
<tr>
|
92
|
+
<td style="padding: 8px;"><strong>Sentiment:</strong></td>
|
93
|
+
<td style="padding: 8px; color: {sentiment_color};">{sentiment.capitalize()}</td>
|
94
|
+
</tr>
|
95
|
+
</table>
|
96
|
+
|
97
|
+
<h2 style="color: #2c3e50;">Contact Information</h2>
|
98
|
+
<p>
|
99
|
+
<strong>Email:</strong> {resume_data.get('email', 'Not provided')}<br>
|
100
|
+
<strong>Phone:</strong> {resume_data.get('phone', 'Not provided')}
|
101
|
+
</p>
|
102
|
+
|
103
|
+
<h2 style="color: #2c3e50;">Assessment Status</h2>
|
104
|
+
<p>{state.get('assessment', {}).get('status', 'Not sent').capitalize()}</p>
|
105
|
+
|
106
|
+
<h2 style="color: #2c3e50;">Recommended Interview Questions</h2>
|
107
|
+
|
108
|
+
<h3>Technical Questions</h3>
|
109
|
+
{_format_questions_html(tech_questions[:3])}
|
110
|
+
|
111
|
+
<h3>Behavioral Questions</h3>
|
112
|
+
{_format_questions_html(behavioral_questions[:3])}
|
113
|
+
|
114
|
+
<p style="text-align: center; margin-top: 30px;">
|
115
|
+
<a href="http://localhost:5000/dashboard"
|
116
|
+
style="background-color: #3498db; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
|
117
|
+
View Full Profile in Dashboard
|
118
|
+
</a>
|
119
|
+
</p>
|
120
|
+
</div>
|
121
|
+
</body>
|
122
|
+
</html>
|
123
|
+
"""
|
124
|
+
|
125
|
+
return {
|
126
|
+
"subject": subject,
|
127
|
+
"plain_text": plain_text,
|
128
|
+
"html_content": html_content
|
129
|
+
}
|
130
|
+
|
131
|
+
def _format_questions_text(questions: List[Dict[str, str]]) -> str:
|
132
|
+
"""Format questions for plain text email"""
|
133
|
+
result = ""
|
134
|
+
for i, q in enumerate(questions, 1):
|
135
|
+
result += f"{i}. {q.get('question', '')}\n"
|
136
|
+
return result
|
137
|
+
|
138
|
+
def _format_questions_html(questions: List[Dict[str, str]]) -> str:
|
139
|
+
"""Format questions for HTML email"""
|
140
|
+
result = "<ol>"
|
141
|
+
for q in questions:
|
142
|
+
purpose = q.get('purpose', '')
|
143
|
+
difficulty = q.get('difficulty', '')
|
144
|
+
difficulty_span = f"<span style='color: {'green' if difficulty == 'easy' else 'orange' if difficulty == 'medium' else 'red'}'>({difficulty})</span>" if difficulty else ""
|
145
|
+
|
146
|
+
result += f"<li><strong>{q.get('question', '')}</strong> {difficulty_span}<br>"
|
147
|
+
if purpose:
|
148
|
+
result += f"<em>Purpose: {purpose}</em></li>"
|
149
|
+
result += "</ol>"
|
150
|
+
return result
|
151
|
+
|
152
|
+
def notify_recruiter(state: Dict[str, Any]) -> Dict[str, Any]:
|
153
|
+
"""
|
154
|
+
LangGraph node to notify recruiters about candidates
|
155
|
+
|
156
|
+
Args:
|
157
|
+
state: The current workflow state
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
Updated workflow state with notification status
|
161
|
+
"""
|
162
|
+
logger.info("Starting recruiter notification")
|
163
|
+
|
164
|
+
# Check if notification already exists in state
|
165
|
+
if state.get("notification_status"):
|
166
|
+
logger.info("Notification already sent, skipping")
|
167
|
+
return state
|
168
|
+
|
169
|
+
# Get recruiter email from settings (or state if available)
|
170
|
+
recruiter_email = state.get("recruiter_email", settings.DEFAULT_RECRUITER_EMAIL)
|
171
|
+
|
172
|
+
if not recruiter_email:
|
173
|
+
error_message = "Missing recruiter email for notification"
|
174
|
+
logger.error(error_message)
|
175
|
+
state["errors"].append({
|
176
|
+
"step": "recruiter_notifier",
|
177
|
+
"error": error_message
|
178
|
+
})
|
179
|
+
state["notification_status"] = {"status": "failed", "reason": "missing_email"}
|
180
|
+
return state
|
181
|
+
|
182
|
+
try:
|
183
|
+
# Create email content
|
184
|
+
email_content = create_recruiter_email_content(state)
|
185
|
+
|
186
|
+
# Send email
|
187
|
+
email_sent = send_email(
|
188
|
+
recipient_email=recruiter_email,
|
189
|
+
subject=email_content["subject"],
|
190
|
+
body=email_content["plain_text"],
|
191
|
+
html_content=email_content["html_content"]
|
192
|
+
)
|
193
|
+
|
194
|
+
if email_sent:
|
195
|
+
state["notification_status"] = {
|
196
|
+
"status": "sent",
|
197
|
+
"recipient": recruiter_email,
|
198
|
+
"timestamp": state["timestamp"]
|
199
|
+
}
|
200
|
+
state["status"] = "notification_sent"
|
201
|
+
logger.info(f"Recruiter notification sent to {recruiter_email}")
|
202
|
+
else:
|
203
|
+
state["notification_status"] = {"status": "failed", "reason": "email_error"}
|
204
|
+
state["errors"].append({
|
205
|
+
"step": "recruiter_notifier",
|
206
|
+
"error": "Failed to send email to recruiter"
|
207
|
+
})
|
208
|
+
logger.error(f"Failed to send notification to {recruiter_email}")
|
209
|
+
|
210
|
+
except Exception as e:
|
211
|
+
error_message = f"Error sending recruiter notification: {str(e)}"
|
212
|
+
logger.error(error_message)
|
213
|
+
|
214
|
+
state["errors"].append({
|
215
|
+
"step": "recruiter_notifier",
|
216
|
+
"error": error_message
|
217
|
+
})
|
218
|
+
|
219
|
+
state["notification_status"] = {
|
220
|
+
"status": "failed",
|
221
|
+
"reason": str(e)
|
222
|
+
}
|
223
|
+
|
224
|
+
return state
|