michael-agent 1.0.1__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.
- michael_agent/dashboard/static/__init__.py +0 -0
- michael_agent/dashboard/templates/__init__.py +0 -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.2.dist-info}/METADATA +1 -1
- {michael_agent-1.0.1.dist-info → michael_agent-1.0.2.dist-info}/RECORD +15 -4
- {michael_agent-1.0.1.dist-info → michael_agent-1.0.2.dist-info}/WHEEL +0 -0
- {michael_agent-1.0.1.dist-info → michael_agent-1.0.2.dist-info}/top_level.txt +0 -0
File without changes
|
File without changes
|
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
|
@@ -0,0 +1,139 @@
|
|
1
|
+
"""
|
2
|
+
Job Description Generator Node
|
3
|
+
Generates detailed job descriptions using Azure OpenAI
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
import logging
|
8
|
+
from typing import Dict, Any
|
9
|
+
from langchain_openai import AzureChatOpenAI
|
10
|
+
from dotenv import load_dotenv
|
11
|
+
|
12
|
+
# Import the JD creation utility (from your existing code)
|
13
|
+
import sys
|
14
|
+
import os
|
15
|
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
|
16
|
+
|
17
|
+
# Import config
|
18
|
+
from config import settings
|
19
|
+
|
20
|
+
# Configure logging
|
21
|
+
logging.basicConfig(level=logging.INFO)
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
def create_llm():
|
25
|
+
"""Create and configure the LLM with Azure OpenAI"""
|
26
|
+
try:
|
27
|
+
api_key = settings.AZURE_OPENAI_KEY or settings.AZURE_OPENAI_API_KEY or settings.OPENAI_API_KEY
|
28
|
+
endpoint = settings.AZURE_OPENAI_ENDPOINT or settings.OPENAI_API_BASE
|
29
|
+
api_version = settings.AZURE_OPENAI_API_VERSION or settings.OPENAI_API_VERSION
|
30
|
+
|
31
|
+
return AzureChatOpenAI(
|
32
|
+
temperature=0.3,
|
33
|
+
deployment_name=settings.AZURE_OPENAI_DEPLOYMENT, # Using deployment_name instead of deployment
|
34
|
+
azure_endpoint=endpoint,
|
35
|
+
api_key=api_key,
|
36
|
+
api_version=api_version,
|
37
|
+
)
|
38
|
+
except Exception as e:
|
39
|
+
logger.error(f"Error initializing Azure OpenAI: {str(e)}")
|
40
|
+
return None
|
41
|
+
|
42
|
+
def get_jd_prompt(job_data: Dict[str, Any]) -> str:
|
43
|
+
"""Generate the prompt for job description creation"""
|
44
|
+
position = job_data.get('position', 'Software Engineer') # Default to Software Engineer if position not provided
|
45
|
+
return f"""
|
46
|
+
You are an expert HR content writer specializing in job descriptions.
|
47
|
+
Create a comprehensive job description for a {position} role.
|
48
|
+
|
49
|
+
Include the following sections:
|
50
|
+
|
51
|
+
- Introduction to the role and company
|
52
|
+
- Work tasks and responsibilities
|
53
|
+
- Required qualifications and skills
|
54
|
+
- Preferred skills and experience
|
55
|
+
- Compensation and benefits information
|
56
|
+
- About the organization
|
57
|
+
- Application process information
|
58
|
+
- include only the required skills and preferred skills in the job description
|
59
|
+
|
60
|
+
Location: {job_data.get('location', 'Not specified')}
|
61
|
+
Business area: {job_data.get('business_area', 'Not specified')}
|
62
|
+
Employment type: {job_data.get('employment_type', 'Full-time')}
|
63
|
+
Experience level: {job_data.get('experience_level', 'Not specified')}
|
64
|
+
Work arrangement: {job_data.get('work_arrangement', 'Not specified')}
|
65
|
+
|
66
|
+
Required skills: {', '.join(job_data.get('required_skills', []))}
|
67
|
+
Preferred skills: {', '.join(job_data.get('preferred_skills', []))}
|
68
|
+
|
69
|
+
Write in professional English, be concise yet comprehensive, and highlight the value
|
70
|
+
proposition for potential candidates.
|
71
|
+
"""
|
72
|
+
|
73
|
+
def generate_job_description(state: Dict[str, Any]) -> Dict[str, Any]:
|
74
|
+
"""
|
75
|
+
LangGraph node to generate a job description
|
76
|
+
|
77
|
+
Args:
|
78
|
+
state: The current workflow state
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Updated workflow state with job description
|
82
|
+
"""
|
83
|
+
logger.info("Starting job description generation")
|
84
|
+
|
85
|
+
# Check if job description already exists in state
|
86
|
+
if state.get("job_description") and state.get("job_description_text"):
|
87
|
+
logger.info("Job description already exists, skipping generation")
|
88
|
+
return state
|
89
|
+
|
90
|
+
try:
|
91
|
+
# Create the language model
|
92
|
+
llm = create_llm()
|
93
|
+
if not llm:
|
94
|
+
raise ValueError("Failed to initialize Azure OpenAI client")
|
95
|
+
|
96
|
+
# Prepare job data (use sample data if not provided)
|
97
|
+
job_data = state.get("job_description", {})
|
98
|
+
if not job_data:
|
99
|
+
# Use default job data if none provided
|
100
|
+
job_data = {
|
101
|
+
"position": "Software Engineer",
|
102
|
+
"location": "Remote",
|
103
|
+
"business_area": "Engineering",
|
104
|
+
"employment_type": "Full-time",
|
105
|
+
"experience_level": "Mid-level",
|
106
|
+
"work_arrangement": "Remote",
|
107
|
+
"required_skills": ["Python", "JavaScript", "API Development"],
|
108
|
+
"preferred_skills": ["Azure", "CI/CD", "TypeScript"]
|
109
|
+
}
|
110
|
+
|
111
|
+
# Generate the prompt
|
112
|
+
prompt = get_jd_prompt(job_data)
|
113
|
+
|
114
|
+
# Invoke the language model
|
115
|
+
response = llm.invoke(prompt)
|
116
|
+
generated_text = response.content
|
117
|
+
|
118
|
+
# Update the state with the generated job description
|
119
|
+
state["job_description"] = job_data
|
120
|
+
state["job_description_text"] = generated_text
|
121
|
+
state["status"] = "jd_generated"
|
122
|
+
|
123
|
+
logger.info("Job description generated successfully")
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
error_message = f"Error generating job description: {str(e)}"
|
127
|
+
logger.error(error_message)
|
128
|
+
|
129
|
+
# Add error to state
|
130
|
+
state["errors"].append({
|
131
|
+
"step": "jd_generator",
|
132
|
+
"error": error_message
|
133
|
+
})
|
134
|
+
|
135
|
+
# Set fallback job description text if needed
|
136
|
+
if not state.get("job_description_text"):
|
137
|
+
state["job_description_text"] = "Default job description text for fallback purposes."
|
138
|
+
|
139
|
+
return state
|
@@ -0,0 +1,156 @@
|
|
1
|
+
"""
|
2
|
+
JD Poster Node
|
3
|
+
Mock posts job descriptions to external platforms
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
import json
|
8
|
+
import logging
|
9
|
+
import requests
|
10
|
+
import time
|
11
|
+
import uuid
|
12
|
+
from typing import Dict, Any, List
|
13
|
+
from datetime import datetime
|
14
|
+
|
15
|
+
# Import config
|
16
|
+
from config import settings
|
17
|
+
|
18
|
+
# Configure logging
|
19
|
+
logging.basicConfig(level=logging.INFO)
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
def mock_post_to_linkedin(job_data: Dict[str, Any]) -> Dict[str, Any]:
|
23
|
+
"""Mock posting a job to LinkedIn"""
|
24
|
+
logger.info("Mocking job post to LinkedIn")
|
25
|
+
# Simulate API call delay
|
26
|
+
time.sleep(0.5)
|
27
|
+
|
28
|
+
return {
|
29
|
+
"platform": "LinkedIn",
|
30
|
+
"status": "success",
|
31
|
+
"post_id": f"li-{uuid.uuid4()}",
|
32
|
+
"timestamp": datetime.now().isoformat()
|
33
|
+
}
|
34
|
+
|
35
|
+
def mock_post_to_indeed(job_data: Dict[str, Any]) -> Dict[str, Any]:
|
36
|
+
"""Mock posting a job to Indeed"""
|
37
|
+
logger.info("Mocking job post to Indeed")
|
38
|
+
# Simulate API call delay
|
39
|
+
time.sleep(0.5)
|
40
|
+
|
41
|
+
return {
|
42
|
+
"platform": "Indeed",
|
43
|
+
"status": "success",
|
44
|
+
"post_id": f"ind-{uuid.uuid4()}",
|
45
|
+
"timestamp": datetime.now().isoformat()
|
46
|
+
}
|
47
|
+
|
48
|
+
def mock_post_to_glassdoor(job_data: Dict[str, Any]) -> Dict[str, Any]:
|
49
|
+
"""Mock posting a job to Glassdoor"""
|
50
|
+
logger.info("Mocking job post to Glassdoor")
|
51
|
+
# Simulate API call delay
|
52
|
+
time.sleep(0.5)
|
53
|
+
|
54
|
+
return {
|
55
|
+
"platform": "Glassdoor",
|
56
|
+
"status": "success",
|
57
|
+
"post_id": f"gd-{uuid.uuid4()}",
|
58
|
+
"timestamp": datetime.now().isoformat()
|
59
|
+
}
|
60
|
+
|
61
|
+
def save_job_description(job_id: str, job_data: Dict[str, Any], job_text: str) -> str:
|
62
|
+
"""Save the job description to a file"""
|
63
|
+
# Ensure log directory exists
|
64
|
+
job_logs_dir = settings.JOB_DESCRIPTIONS_DIR
|
65
|
+
os.makedirs(job_logs_dir, exist_ok=True)
|
66
|
+
|
67
|
+
# Generate timestamp for filename if no job_id provided
|
68
|
+
if not job_id:
|
69
|
+
job_id = datetime.now().strftime("%Y%m%d%H%M%S")
|
70
|
+
|
71
|
+
# Create the job description file
|
72
|
+
file_path = os.path.join(job_logs_dir, f"{job_id}.json")
|
73
|
+
|
74
|
+
# Prepare data to save
|
75
|
+
data_to_save = {
|
76
|
+
"job_id": job_id,
|
77
|
+
"timestamp": datetime.now().isoformat(),
|
78
|
+
"job_data": job_data,
|
79
|
+
"job_description": job_text
|
80
|
+
}
|
81
|
+
|
82
|
+
# Save to file
|
83
|
+
with open(file_path, "w") as f:
|
84
|
+
json.dump(data_to_save, f, indent=2)
|
85
|
+
|
86
|
+
logger.info(f"Job description saved to {file_path}")
|
87
|
+
return file_path
|
88
|
+
|
89
|
+
def post_job_description(state: Dict[str, Any]) -> Dict[str, Any]:
|
90
|
+
"""
|
91
|
+
LangGraph node to post job descriptions to job boards
|
92
|
+
|
93
|
+
Args:
|
94
|
+
state: The current workflow state
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Updated workflow state with posting results
|
98
|
+
"""
|
99
|
+
logger.info("Starting job posting")
|
100
|
+
|
101
|
+
# Check if job description already exists in state
|
102
|
+
if not state.get("job_description") or not state.get("job_description_text"):
|
103
|
+
error_message = "No job description available for posting"
|
104
|
+
logger.error(error_message)
|
105
|
+
|
106
|
+
# Add error to state
|
107
|
+
state["errors"].append({
|
108
|
+
"step": "jd_poster",
|
109
|
+
"error": error_message
|
110
|
+
})
|
111
|
+
|
112
|
+
return state
|
113
|
+
|
114
|
+
try:
|
115
|
+
job_id = state.get("job_id", "")
|
116
|
+
job_data = state.get("job_description", {})
|
117
|
+
job_text = state.get("job_description_text", "")
|
118
|
+
|
119
|
+
# Save job description to file
|
120
|
+
job_file_path = save_job_description(job_id, job_data, job_text)
|
121
|
+
|
122
|
+
# Ensure the file path is stored in state
|
123
|
+
state["job_file_path"] = job_file_path
|
124
|
+
|
125
|
+
# Mock post to job platforms
|
126
|
+
posting_results = []
|
127
|
+
|
128
|
+
# LinkedIn
|
129
|
+
linkedin_result = mock_post_to_linkedin(job_data)
|
130
|
+
posting_results.append(linkedin_result)
|
131
|
+
|
132
|
+
# Indeed
|
133
|
+
indeed_result = mock_post_to_indeed(job_data)
|
134
|
+
posting_results.append(indeed_result)
|
135
|
+
|
136
|
+
# Glassdoor
|
137
|
+
glassdoor_result = mock_post_to_glassdoor(job_data)
|
138
|
+
posting_results.append(glassdoor_result)
|
139
|
+
|
140
|
+
# Update state with results
|
141
|
+
state["job_posting_results"] = posting_results
|
142
|
+
state["status"] = "job_posted"
|
143
|
+
|
144
|
+
logger.info(f"Job posted successfully to {len(posting_results)} platforms")
|
145
|
+
|
146
|
+
except Exception as e:
|
147
|
+
error_message = f"Error posting job description: {str(e)}"
|
148
|
+
logger.error(error_message)
|
149
|
+
|
150
|
+
# Add error to state
|
151
|
+
state["errors"].append({
|
152
|
+
"step": "jd_poster",
|
153
|
+
"error": error_message
|
154
|
+
})
|
155
|
+
|
156
|
+
return state
|