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