michael-agent 1.0.0__py3-none-any.whl → 1.0.2__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.
Files changed (30) hide show
  1. michael_agent/config/__init__.py +0 -0
  2. michael_agent/config/settings.py +66 -0
  3. michael_agent/dashboard/__init__.py +0 -0
  4. michael_agent/dashboard/app.py +1450 -0
  5. michael_agent/dashboard/static/__init__.py +0 -0
  6. michael_agent/dashboard/templates/__init__.py +0 -0
  7. michael_agent/langgraph_workflow/__init__.py +0 -0
  8. michael_agent/langgraph_workflow/graph_builder.py +358 -0
  9. michael_agent/langgraph_workflow/nodes/__init__.py +0 -0
  10. michael_agent/langgraph_workflow/nodes/assessment_handler.py +177 -0
  11. michael_agent/langgraph_workflow/nodes/jd_generator.py +139 -0
  12. michael_agent/langgraph_workflow/nodes/jd_poster.py +156 -0
  13. michael_agent/langgraph_workflow/nodes/question_generator.py +295 -0
  14. michael_agent/langgraph_workflow/nodes/recruiter_notifier.py +224 -0
  15. michael_agent/langgraph_workflow/nodes/resume_analyzer.py +631 -0
  16. michael_agent/langgraph_workflow/nodes/resume_ingestor.py +225 -0
  17. michael_agent/langgraph_workflow/nodes/sentiment_analysis.py +309 -0
  18. michael_agent/utils/__init__.py +0 -0
  19. michael_agent/utils/email_utils.py +140 -0
  20. michael_agent/utils/id_mapper.py +14 -0
  21. michael_agent/utils/jd_utils.py +34 -0
  22. michael_agent/utils/lms_api.py +226 -0
  23. michael_agent/utils/logging_utils.py +192 -0
  24. michael_agent/utils/monitor_utils.py +289 -0
  25. michael_agent/utils/node_tracer.py +88 -0
  26. {michael_agent-1.0.0.dist-info → michael_agent-1.0.2.dist-info}/METADATA +2 -2
  27. michael_agent-1.0.2.dist-info/RECORD +32 -0
  28. michael_agent-1.0.0.dist-info/RECORD +0 -7
  29. {michael_agent-1.0.0.dist-info → michael_agent-1.0.2.dist-info}/WHEEL +0 -0
  30. {michael_agent-1.0.0.dist-info → michael_agent-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,34 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ from typing import Dict, Any, Optional
5
+
6
+ from config import settings
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def load_job_description(job_id: str = None, job_file_path: str = None) -> Dict[str, Any]:
11
+ """Load job description from file"""
12
+ try:
13
+ # If path is provided directly, use it
14
+ if job_file_path and os.path.exists(job_file_path):
15
+ with open(job_file_path, 'r') as f:
16
+ job_data = json.load(f)
17
+ return job_data
18
+
19
+ # If job_id is provided, look for the file in job_descriptions directory
20
+ if job_id:
21
+ # Look only in the root job_descriptions directory
22
+ main_path = os.path.join("job_descriptions", f"{job_id}.json")
23
+ if os.path.exists(main_path):
24
+ with open(main_path, 'r') as f:
25
+ job_data = json.load(f)
26
+ return job_data
27
+
28
+ logger.error(f"Job description file not found for job_id {job_id} at {main_path}")
29
+
30
+ logger.error("No valid job ID or file path provided")
31
+ return None
32
+ except Exception as e:
33
+ logger.error(f"Error loading job description: {e}")
34
+ return None
@@ -0,0 +1,226 @@
1
+ """
2
+ LMS (Learning Management System) API integration utilities
3
+ Supports integration with HackerRank, TestGorilla, or custom LMS APIs
4
+ """
5
+
6
+ import os
7
+ import requests
8
+ import json
9
+ import logging
10
+ from typing import Dict, List, Optional, Any
11
+
12
+ from config import settings
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class LMSIntegration:
17
+ """Base class for LMS integrations"""
18
+
19
+ def __init__(self):
20
+ self.api_url = settings.LMS_API_URL
21
+ self.api_key = settings.LMS_API_KEY
22
+
23
+ def send_assessment(self, candidate_email, candidate_name, position_name):
24
+ """Send an assessment to a candidate (to be implemented by subclasses)"""
25
+ raise NotImplementedError("Subclasses must implement this method")
26
+
27
+ def get_assessment_results(self, assessment_id):
28
+ """Get the results of an assessment (to be implemented by subclasses)"""
29
+ raise NotImplementedError("Subclasses must implement this method")
30
+
31
+ class HackerRankIntegration(LMSIntegration):
32
+ """Integration with HackerRank API"""
33
+
34
+ def __init__(self):
35
+ super().__init__()
36
+ self.headers = {
37
+ "Content-Type": "application/json",
38
+ "Authorization": f"Bearer {self.api_key}"
39
+ }
40
+
41
+ def send_assessment(self, candidate_email, candidate_name, position_name):
42
+ """Send a HackerRank test to a candidate"""
43
+ try:
44
+ # Find test ID based on position name (example - would need to be customized)
45
+ test_id = self._get_test_id_for_position(position_name)
46
+
47
+ # Prepare API payload
48
+ payload = {
49
+ "test_id": test_id,
50
+ "candidates": [
51
+ {
52
+ "email": candidate_email,
53
+ "first_name": candidate_name.split()[0],
54
+ "last_name": candidate_name.split()[-1] if len(candidate_name.split()) > 1 else ""
55
+ }
56
+ ],
57
+ "subject": f"Assessment for {position_name} position",
58
+ "message": f"Dear {candidate_name.split()[0]},\n\nPlease complete the following assessment for the {position_name} position."
59
+ }
60
+
61
+ # Make API request to send test invitation
62
+ response = requests.post(
63
+ f"{self.api_url}/tests/{test_id}/candidates/invite",
64
+ headers=self.headers,
65
+ json=payload
66
+ )
67
+
68
+ response.raise_for_status()
69
+ result = response.json()
70
+
71
+ logger.info(f"Assessment sent to {candidate_email} via HackerRank")
72
+ return {
73
+ "success": True,
74
+ "assessment_id": result.get("assessment_id"),
75
+ "invitation_id": result.get("invitation_id"),
76
+ "status": "sent"
77
+ }
78
+ except requests.exceptions.RequestException as e:
79
+ logger.error(f"Error sending HackerRank assessment: {str(e)}")
80
+ return {
81
+ "success": False,
82
+ "error": str(e)
83
+ }
84
+
85
+ def get_assessment_results(self, assessment_id):
86
+ """Get the results of a HackerRank assessment"""
87
+ try:
88
+ response = requests.get(
89
+ f"{self.api_url}/assessments/{assessment_id}",
90
+ headers=self.headers
91
+ )
92
+
93
+ response.raise_for_status()
94
+ result = response.json()
95
+
96
+ return {
97
+ "success": True,
98
+ "status": result.get("status"),
99
+ "score": result.get("score"),
100
+ "completed_at": result.get("completed_at"),
101
+ "results": result.get("results")
102
+ }
103
+ except requests.exceptions.RequestException as e:
104
+ logger.error(f"Error getting HackerRank assessment results: {str(e)}")
105
+ return {
106
+ "success": False,
107
+ "error": str(e)
108
+ }
109
+
110
+ def _get_test_id_for_position(self, position_name):
111
+ """
112
+ Map position name to test ID
113
+ This is a simplified example - in practice, you would have a mapping or search functionality
114
+ """
115
+ # Example mapping - would need to be customized
116
+ position_to_test_map = {
117
+ "Software Engineer": "12345",
118
+ "Data Scientist": "23456",
119
+ "DevOps Engineer": "34567",
120
+ # Add more mappings as needed
121
+ }
122
+
123
+ # Default to a general assessment if position not found
124
+ return position_to_test_map.get(position_name, "default_test_id")
125
+
126
+ class TestGorillaIntegration(LMSIntegration):
127
+ """Integration with TestGorilla API"""
128
+
129
+ def __init__(self):
130
+ super().__init__()
131
+ self.headers = {
132
+ "Content-Type": "application/json",
133
+ "Authorization": f"Token {self.api_key}"
134
+ }
135
+
136
+ def send_assessment(self, candidate_email, candidate_name, position_name):
137
+ """Send a TestGorilla assessment to a candidate"""
138
+ try:
139
+ # Find assessment ID based on position name
140
+ assessment_id = self._get_assessment_id_for_position(position_name)
141
+
142
+ # Prepare API payload
143
+ payload = {
144
+ "email": candidate_email,
145
+ "first_name": candidate_name.split()[0],
146
+ "last_name": candidate_name.split()[-1] if len(candidate_name.split()) > 1 else "",
147
+ "language": "en",
148
+ "assessment": assessment_id
149
+ }
150
+
151
+ # Make API request to send invitation
152
+ response = requests.post(
153
+ f"{self.api_url}/candidates/",
154
+ headers=self.headers,
155
+ json=payload
156
+ )
157
+
158
+ response.raise_for_status()
159
+ result = response.json()
160
+
161
+ logger.info(f"Assessment sent to {candidate_email} via TestGorilla")
162
+ return {
163
+ "success": True,
164
+ "candidate_id": result.get("id"),
165
+ "invitation_url": result.get("invitation_url"),
166
+ "status": "invited"
167
+ }
168
+ except requests.exceptions.RequestException as e:
169
+ logger.error(f"Error sending TestGorilla assessment: {str(e)}")
170
+ return {
171
+ "success": False,
172
+ "error": str(e)
173
+ }
174
+
175
+ def get_assessment_results(self, candidate_id):
176
+ """Get the results of a TestGorilla assessment"""
177
+ try:
178
+ response = requests.get(
179
+ f"{self.api_url}/candidates/{candidate_id}/",
180
+ headers=self.headers
181
+ )
182
+
183
+ response.raise_for_status()
184
+ result = response.json()
185
+
186
+ return {
187
+ "success": True,
188
+ "status": result.get("status"),
189
+ "score": result.get("score"),
190
+ "completed_at": result.get("completed_at"),
191
+ "test_results": result.get("test_results", [])
192
+ }
193
+ except requests.exceptions.RequestException as e:
194
+ logger.error(f"Error getting TestGorilla assessment results: {str(e)}")
195
+ return {
196
+ "success": False,
197
+ "error": str(e)
198
+ }
199
+
200
+ def _get_assessment_id_for_position(self, position_name):
201
+ """
202
+ Map position name to assessment ID
203
+ This is a simplified example - in practice, you would have a mapping or search functionality
204
+ """
205
+ # Example mapping - would need to be customized
206
+ position_to_assessment_map = {
207
+ "Software Engineer": "abc123",
208
+ "Data Scientist": "def456",
209
+ "DevOps Engineer": "ghi789",
210
+ # Add more mappings as needed
211
+ }
212
+
213
+ # Default to a general assessment if position not found
214
+ return position_to_assessment_map.get(position_name, "default_assessment_id")
215
+
216
+ def get_lms_client() -> LMSIntegration:
217
+ """Factory function to get the appropriate LMS client based on configuration"""
218
+ lms_type = settings.LMS_TYPE.lower()
219
+
220
+ if lms_type == "hackerrank":
221
+ return HackerRankIntegration()
222
+ elif lms_type == "testgorilla":
223
+ return TestGorillaIntegration()
224
+ else:
225
+ logger.warning(f"Unsupported LMS type: {lms_type}, falling back to HackerRank")
226
+ return HackerRankIntegration()
@@ -0,0 +1,192 @@
1
+ """
2
+ Logging utilities for SmartRecruitAgent
3
+ Provides consistent logging configuration and helpers
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import logging
10
+ import traceback
11
+ from datetime import datetime
12
+ from functools import wraps
13
+ from typing import Dict, Any, Callable, Optional
14
+
15
+ # Import config
16
+ from config import settings
17
+
18
+ # Configure the base logging system
19
+ def setup_logger(name: str, log_file: Optional[str] = None, level: int = logging.INFO) -> logging.Logger:
20
+ """Set up and configure a logger instance"""
21
+ logger = logging.getLogger(name)
22
+ logger.setLevel(level)
23
+
24
+ # Create formatter
25
+ formatter = logging.Formatter(
26
+ '%(asctime)s | %(levelname)s | %(name)s | %(message)s',
27
+ datefmt='%Y-%m-%d %H:%M:%S'
28
+ )
29
+
30
+ # Add console handler
31
+ console_handler = logging.StreamHandler(sys.stdout)
32
+ console_handler.setFormatter(formatter)
33
+ logger.addHandler(console_handler)
34
+
35
+ # Add file handler if log file is specified
36
+ if log_file:
37
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
38
+ file_handler = logging.FileHandler(log_file)
39
+ file_handler.setFormatter(formatter)
40
+ logger.addHandler(file_handler)
41
+
42
+ return logger
43
+
44
+ # Create the workflow logger
45
+ workflow_logger = setup_logger(
46
+ 'workflow',
47
+ os.path.join(settings.LOG_DIR, 'workflow.log')
48
+ )
49
+
50
+ # Create the state transitions logger
51
+ state_logger = setup_logger(
52
+ 'state_transitions',
53
+ os.path.join(settings.LOG_DIR, 'state_transitions.log')
54
+ )
55
+
56
+ def log_state_transition(before_state: Dict[str, Any], after_state: Dict[str, Any], step_name: str) -> None:
57
+ """Log state transitions between workflow steps"""
58
+ # Create a simplified log of what changed
59
+ changes = {}
60
+
61
+ # Track only the keys that changed
62
+ for key in after_state:
63
+ if key not in before_state or before_state[key] != after_state[key]:
64
+ # For complex objects, just note they changed rather than logging entire content
65
+ if isinstance(after_state[key], dict) or isinstance(after_state[key], list):
66
+ changes[key] = f"{type(after_state[key]).__name__} modified"
67
+ else:
68
+ changes[key] = after_state[key]
69
+
70
+ # Record any errors added in this step
71
+ errors = []
72
+ if "errors" in after_state and isinstance(after_state["errors"], list):
73
+ before_errors_count = len(before_state.get("errors", []))
74
+ if len(after_state["errors"]) > before_errors_count:
75
+ errors = after_state["errors"][before_errors_count:]
76
+
77
+ # Create the log entry
78
+ log_entry = {
79
+ "timestamp": datetime.now().isoformat(),
80
+ "step": step_name,
81
+ "job_id": after_state.get("job_id", "unknown"),
82
+ "candidate": after_state.get("candidate_name", "unknown"),
83
+ "status": after_state.get("status", "unknown"),
84
+ "changes": changes,
85
+ "errors": errors
86
+ }
87
+
88
+ # Add a JSON serialization helper function
89
+ def json_serializable(obj):
90
+ if isinstance(obj, (str, int, float, bool, type(None))):
91
+ return obj
92
+ elif isinstance(obj, (list, tuple)):
93
+ return [json_serializable(item) for item in obj]
94
+ elif isinstance(obj, dict):
95
+ return {k: json_serializable(v) for k, v in obj.items() if isinstance(k, str)}
96
+ return str(obj) # Convert non-serializable objects to strings
97
+
98
+ # Modify the log_entry or use the helper
99
+ serializable_log_entry = json_serializable(log_entry)
100
+ # Log to file
101
+ state_logger.info(json.dumps(serializable_log_entry))
102
+
103
+ def log_step(func: Callable) -> Callable:
104
+ """Decorator for logging workflow step execution with detailed tracking"""
105
+ @wraps(func)
106
+ def wrapper(state: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]:
107
+ step_name = func.__name__
108
+ job_id = state.get("job_id", "unknown")
109
+
110
+ # Log step start
111
+ workflow_logger.info(f"Starting step: {step_name} for job {job_id}")
112
+
113
+ # Store the state before execution for comparison
114
+ before_state = {**state}
115
+
116
+ try:
117
+ # Execute the step
118
+ result_state = func(state, *args, **kwargs)
119
+
120
+ # Handle case when function returns None instead of state
121
+ if result_state is None:
122
+ workflow_logger.debug(f"Step {step_name} returned None; using original state.")
123
+ result_state = before_state
124
+
125
+ # Update status to reflect step completion
126
+ if isinstance(result_state, dict) and "status" in result_state:
127
+ result_state["status"] = f"completed_{step_name}"
128
+
129
+ # Log successful completion
130
+ workflow_logger.info(f"Completed step: {step_name} for job {job_id}")
131
+
132
+ # Log state transitions
133
+ log_state_transition(before_state, result_state, step_name)
134
+
135
+ return result_state
136
+
137
+ except Exception as e:
138
+ # Log the exception with traceback
139
+ error_msg = f"Error in step {step_name}: {str(e)}"
140
+ workflow_logger.error(error_msg)
141
+ workflow_logger.error(traceback.format_exc())
142
+
143
+ # Update state with error information
144
+ if "errors" not in state:
145
+ state["errors"] = []
146
+
147
+ state["errors"].append({
148
+ "step": step_name,
149
+ "timestamp": datetime.now().isoformat(),
150
+ "error": str(e),
151
+ "traceback": traceback.format_exc()
152
+ })
153
+
154
+ if "status" in state:
155
+ state["status"] = f"error_in_{step_name}"
156
+
157
+ # Log state transition with error
158
+ log_state_transition(before_state, state, f"{step_name}_error")
159
+
160
+ return state
161
+
162
+ return wrapper
163
+
164
+ def save_state_snapshot(state: Dict[str, Any], step: str) -> None:
165
+ """Save a snapshot of the current state to a JSON file"""
166
+ try:
167
+ # Create a timestamped filename
168
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
169
+ job_id = state.get("job_id", "unknown")
170
+ filename = f"{timestamp}_{job_id}_{step}.json"
171
+
172
+ # Create the snapshots directory
173
+ snapshots_dir = os.path.join(settings.LOG_DIR, "snapshots")
174
+ os.makedirs(snapshots_dir, exist_ok=True)
175
+
176
+ # Create a clean copy of the state (remove any objects that can't be serialized)
177
+ clean_state = {}
178
+ for key, value in state.items():
179
+ try:
180
+ # Test if it can be serialized
181
+ json.dumps({key: value})
182
+ clean_state[key] = value
183
+ except (TypeError, OverflowError):
184
+ clean_state[key] = f"<non-serializable: {type(value).__name__}>"
185
+
186
+ # Write the state to file
187
+ with open(os.path.join(snapshots_dir, filename), 'w') as f:
188
+ json.dump(clean_state, f, indent=2, default=str)
189
+
190
+ workflow_logger.debug(f"State snapshot saved: {filename}")
191
+ except Exception as e:
192
+ workflow_logger.error(f"Failed to save state snapshot: {str(e)}")