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.
@@ -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