initial code
This commit is contained in:
parent
a979b2baa5
commit
42cee9621a
4 changed files with 498 additions and 0 deletions
4
ai_agent_tutorials/ai_recruitment_agent_team/.gitignore
vendored
Normal file
4
ai_agent_tutorials/ai_recruitment_agent_team/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Environment variables and secrets
|
||||
.env
|
||||
.env.*
|
||||
*.env
|
||||
0
ai_agent_tutorials/ai_recruitment_agent_team/README.md
Normal file
0
ai_agent_tutorials/ai_recruitment_agent_team/README.md
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
from typing import Literal, Tuple, Dict, Optional
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
import PyPDF2
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import streamlit as st
|
||||
from phi.agent import Agent
|
||||
from phi.model.openai import OpenAIChat
|
||||
from phi.tools.email import EmailTools
|
||||
from phi.tools.zoom import ZoomTool
|
||||
from phi.utils.log import logger
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Constants
|
||||
FROM_EMAIL = "ryomensukuna64@gmail.com"
|
||||
|
||||
ACCOUNT_ID = os.getenv("ZOOM_ACCOUNT_ID")
|
||||
CLIENT_ID = os.getenv("ZOOM_CLIENT_ID")
|
||||
CLIENT_SECRET = os.getenv("ZOOM_CLIENT_SECRET")
|
||||
|
||||
|
||||
|
||||
|
||||
class CustomZoomTool(ZoomTool):
|
||||
def __init__(
|
||||
self,
|
||||
account_id: Optional[str] = None,
|
||||
client_id: Optional[str] = None,
|
||||
client_secret: Optional[str] = None,
|
||||
name: str = "zoom_tool",
|
||||
):
|
||||
super().__init__(
|
||||
account_id=account_id,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
name=name
|
||||
)
|
||||
self.token_url = "https://zoom.us/oauth/token"
|
||||
self.access_token = None
|
||||
self.token_expires_at = 0
|
||||
|
||||
def get_access_token(self) -> str:
|
||||
if self.access_token and time.time() < self.token_expires_at:
|
||||
return str(self.access_token)
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
data = {"grant_type": "account_credentials", "account_id": self.account_id}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
self.token_url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
auth=(self.client_id, self.client_secret) # Use basic auth
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
token_info = response.json()
|
||||
self.access_token = token_info["access_token"]
|
||||
expires_in = token_info["expires_in"]
|
||||
self.token_expires_at = time.time() + expires_in - 60
|
||||
|
||||
# Update this line to use the helper method
|
||||
self._set_parent_token(str(self.access_token))
|
||||
return str(self.access_token)
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Error fetching access token: {e}")
|
||||
return ""
|
||||
|
||||
def _set_parent_token(self, token: str) -> None:
|
||||
"""Helper method to set the token in the parent ZoomTool class"""
|
||||
if token:
|
||||
self._ZoomTool__access_token = token
|
||||
|
||||
|
||||
# Role requirements as a constant dictionary
|
||||
ROLE_REQUIREMENTS: Dict[str, str] = {
|
||||
"ai_ml_engineer": """
|
||||
Required Skills:
|
||||
- Python, PyTorch/TensorFlow
|
||||
- Machine Learning algorithms and frameworks
|
||||
- Deep Learning and Neural Networks
|
||||
- Data preprocessing and analysis
|
||||
- MLOps and model deployment
|
||||
- RAG, LLM, Finetuning and Prompt Engineering
|
||||
""",
|
||||
|
||||
"frontend_engineer": """
|
||||
Required Skills:
|
||||
- React/Vue.js/Angular
|
||||
- HTML5, CSS3, JavaScript/TypeScript
|
||||
- Responsive design
|
||||
- State management
|
||||
- Frontend testing
|
||||
""",
|
||||
|
||||
"backend_engineer": """
|
||||
Required Skills:
|
||||
- Python/Java/Node.js
|
||||
- REST APIs
|
||||
- Database design and management
|
||||
- System architecture
|
||||
- Cloud services (AWS/GCP/Azure)
|
||||
- Kubernetes, Docker, CI/CD
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
def init_session_state() -> None:
|
||||
"""Initialize only necessary session state variables."""
|
||||
defaults = {
|
||||
'candidate_email': "",
|
||||
'openai_api_key': "",
|
||||
'resume_text': "",
|
||||
'analysis_complete': False,
|
||||
'is_selected': False
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if key not in st.session_state:
|
||||
st.session_state[key] = value
|
||||
|
||||
|
||||
def create_resume_analyzer() -> Agent:
|
||||
"""Creates and returns a resume analysis agent."""
|
||||
if not st.session_state.openai_api_key:
|
||||
st.error("Please enter your OpenAI API key first.")
|
||||
return None
|
||||
|
||||
return Agent(
|
||||
model=OpenAIChat(
|
||||
id="gpt-4o",
|
||||
api_key=st.session_state.openai_api_key
|
||||
),
|
||||
description="You are an expert technical recruiter who analyzes resumes.",
|
||||
instructions=[
|
||||
"Analyze the resume against the provided job requirements",
|
||||
"Be lenient with AI/ML candidates who show strong potential",
|
||||
"Consider project experience as valid experience",
|
||||
"Value hands-on experience with key technologies",
|
||||
"Return a JSON response with selection decision and feedback"
|
||||
],
|
||||
markdown=True
|
||||
)
|
||||
|
||||
def create_email_agent() -> Agent:
|
||||
return Agent(
|
||||
model=OpenAIChat(
|
||||
id="gpt-4o",
|
||||
api_key=st.session_state.openai_api_key
|
||||
),
|
||||
tools=[EmailTools(
|
||||
receiver_email=st.session_state.candidate_email,
|
||||
sender_email=FROM_EMAIL,
|
||||
sender_name="AI Recruitment Team",
|
||||
sender_passkey=os.getenv("EMAIL_PASSKEY")
|
||||
)],
|
||||
description="You are a professional recruitment coordinator handling email communications.",
|
||||
instructions=[
|
||||
"Draft and send professional recruitment emails",
|
||||
"Act like a human writing an email and use all lowercase letters",
|
||||
"Maintain a friendly yet professional tone",
|
||||
"Always end emails with exactly: 'best,\nthe ai recruiting team'",
|
||||
"Never include the sender's or receiver's name in the signature",
|
||||
"The name of the company is 'AI Recruiting Team'"
|
||||
],
|
||||
markdown=True,
|
||||
show_tool_calls=True
|
||||
)
|
||||
|
||||
|
||||
def create_scheduler_agent() -> Agent:
|
||||
|
||||
zoom_tools = CustomZoomTool(account_id=ACCOUNT_ID, client_id=CLIENT_ID, client_secret=CLIENT_SECRET)
|
||||
|
||||
return Agent(
|
||||
name="Interview Scheduler",
|
||||
model=OpenAIChat(
|
||||
id="gpt-4o",
|
||||
api_key=st.session_state.openai_api_key
|
||||
),
|
||||
tools=[zoom_tools],
|
||||
description="You are an interview scheduling coordinator.",
|
||||
instructions=[
|
||||
"You are an expert at scheduling technical interviews using Zoom.",
|
||||
"Schedule interviews during business hours (9 AM - 5 PM EST)",
|
||||
"Create meetings with proper titles and descriptions",
|
||||
"Ensure all meeting details are included in responses",
|
||||
"Use ISO 8601 format for dates",
|
||||
"Handle scheduling errors gracefully"
|
||||
],
|
||||
markdown=True,
|
||||
show_tool_calls=True
|
||||
)
|
||||
|
||||
|
||||
def extract_text_from_pdf(pdf_file) -> str:
|
||||
try:
|
||||
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
||||
text = ""
|
||||
for page in pdf_reader.pages:
|
||||
text += page.extract_text()
|
||||
return text
|
||||
except Exception as e:
|
||||
st.error(f"Error extracting PDF text: {str(e)}")
|
||||
return ""
|
||||
|
||||
|
||||
def analyze_resume(
|
||||
resume_text: str,
|
||||
role: Literal["ai_ml_engineer", "frontend_engineer", "backend_engineer"],
|
||||
analyzer: Agent
|
||||
) -> Tuple[bool, str]:
|
||||
try:
|
||||
response = analyzer.run(f"""
|
||||
Please analyze this resume against the following requirements and provide your response in valid JSON format:
|
||||
Role Requirements:
|
||||
{ROLE_REQUIREMENTS[role]}
|
||||
Resume Text:
|
||||
{resume_text}
|
||||
Your response must be a valid JSON object like this:
|
||||
{{
|
||||
"selected": true/false,
|
||||
"feedback": "Detailed feedback explaining the decision",
|
||||
"matching_skills": ["skill1", "skill2"],
|
||||
"missing_skills": ["skill3", "skill4"],
|
||||
"experience_level": "junior/mid/senior"
|
||||
}}
|
||||
Evaluation criteria:
|
||||
1. Match at least 70% of required skills
|
||||
2. Consider both theoretical knowledge and practical experience
|
||||
3. Value project experience and real-world applications
|
||||
4. Consider transferable skills from similar technologies
|
||||
5. Look for evidence of continuous learning and adaptability
|
||||
Important: Return ONLY the JSON object without any markdown formatting or backticks.
|
||||
""")
|
||||
|
||||
# Extract the assistant's message content
|
||||
assistant_message = None
|
||||
for message in response.messages:
|
||||
if message.role == 'assistant':
|
||||
assistant_message = message.content
|
||||
break
|
||||
|
||||
if not assistant_message:
|
||||
raise ValueError("No assistant message found in response.")
|
||||
result = json.loads(assistant_message.strip())
|
||||
|
||||
if not isinstance(result, dict):
|
||||
raise ValueError("Response is not a JSON object")
|
||||
|
||||
if "selected" not in result or "feedback" not in result:
|
||||
raise ValueError("Missing required fields in response")
|
||||
|
||||
return result["selected"], result["feedback"]
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
st.error(f"Error processing response: {str(e)}")
|
||||
return False, f"Error analyzing resume: {str(e)}"
|
||||
|
||||
|
||||
def send_selection_email(email_agent: Agent, to_email: str, role: str) -> None:
|
||||
email_agent.run(
|
||||
f"""
|
||||
Send an email to {to_email} regarding their selection for the {role} position.
|
||||
The email should:
|
||||
1. Congratulate them on being selected
|
||||
2. Explain the next steps in the process
|
||||
3. Mention that they will receive interview details shortly
|
||||
4. The name of the company is 'AI Recruiting Team'
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def send_rejection_email(email_agent: Agent, to_email: str, role: str, feedback: str) -> None:
|
||||
"""
|
||||
Send a rejection email with constructive feedback.
|
||||
"""
|
||||
email_agent.run(
|
||||
f"""
|
||||
Send an email to {to_email} regarding their application for the {role} position.
|
||||
Use this specific style:
|
||||
1. use all lowercase letters
|
||||
2. be empathetic and human
|
||||
3. mention specific feedback from: {feedback}
|
||||
4. encourage them to upskill and try again
|
||||
5. suggest some learning resources based on missing skills
|
||||
6. end the email with exactly:
|
||||
best,
|
||||
the ai recruiting team
|
||||
|
||||
Do not include any names in the signature.
|
||||
The tone should be like a human writing a quick but thoughtful email.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def schedule_interview(scheduler: Agent, candidate_email: str, email_agent: Agent, role: str) -> None:
|
||||
"""
|
||||
Basic interview scheduler that creates a Zoom meeting and sends email details.
|
||||
Schedules interviews during business hours (9 AM - 5 PM EST).
|
||||
"""
|
||||
try:
|
||||
# Calculate tomorrow at 2 PM EST (instead of UTC)
|
||||
tomorrow = datetime.now() + timedelta(days=1)
|
||||
interview_time = tomorrow.replace(hour=14, minute=0, second=0, microsecond=0)
|
||||
|
||||
# 1. Schedule the meeting with EST timezone specification
|
||||
meeting_response = scheduler.run(
|
||||
f"Schedule a 60-minute meeting titled '{role} Technical Interview' "
|
||||
f"for tomorrow at 2 PM EST (Eastern Time) 2024 with attendee {candidate_email}. "
|
||||
f"Ensure the meeting is between 9 AM - 5 PM EST only."
|
||||
)
|
||||
|
||||
# 2. Send email notification
|
||||
email_agent.run(
|
||||
f"Send an email to {candidate_email} about their scheduled interview for "
|
||||
f"the {role} position. Include the Zoom meeting link from: {meeting_response}. "
|
||||
f"Make sure to specify that the time is in EST timezone."
|
||||
)
|
||||
|
||||
st.success("Interview scheduled successfully!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scheduling interview: {str(e)}")
|
||||
st.error("Unable to schedule interview. Please try again.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function to run the Streamlit application."""
|
||||
st.title("AI Recruitment System")
|
||||
|
||||
# Initialize session state
|
||||
init_session_state()
|
||||
|
||||
# Sidebar for API key
|
||||
with st.sidebar:
|
||||
st.header("Configuration")
|
||||
api_key = st.text_input(
|
||||
"Enter your OpenAI API key",
|
||||
type="password",
|
||||
value=st.session_state.openai_api_key,
|
||||
help="Get your API key from platform.openai.com"
|
||||
)
|
||||
if api_key:
|
||||
st.session_state.openai_api_key = api_key
|
||||
|
||||
# Main content
|
||||
if not st.session_state.openai_api_key:
|
||||
st.warning("Please enter your OpenAI API key in the sidebar to continue.")
|
||||
return
|
||||
|
||||
# Role selection with requirements display
|
||||
role = st.selectbox(
|
||||
"Select the role you're applying for:",
|
||||
["ai_ml_engineer", "frontend_engineer", "backend_engineer"]
|
||||
)
|
||||
|
||||
# Display requirements for selected role
|
||||
with st.expander("View Required Skills", expanded=True):
|
||||
st.markdown(ROLE_REQUIREMENTS[role])
|
||||
|
||||
# Resume upload and processing
|
||||
resume_file = st.file_uploader("Upload your resume (PDF)", type=["pdf"])
|
||||
if resume_file and not st.session_state.resume_text:
|
||||
with st.spinner("Processing your resume..."):
|
||||
resume_text = extract_text_from_pdf(resume_file)
|
||||
if resume_text:
|
||||
st.session_state.resume_text = resume_text
|
||||
st.success("Resume processed successfully!")
|
||||
else:
|
||||
st.error("Could not process the PDF. Please try again.")
|
||||
|
||||
# Email input with session state
|
||||
email = st.text_input(
|
||||
"Your email address",
|
||||
value=st.session_state.candidate_email,
|
||||
key="email_input"
|
||||
)
|
||||
st.session_state.candidate_email = email
|
||||
|
||||
# Analysis and next steps
|
||||
if st.session_state.resume_text and email and not st.session_state.analysis_complete:
|
||||
if st.button("Analyze Resume"):
|
||||
with st.spinner("Analyzing your resume..."):
|
||||
resume_analyzer = create_resume_analyzer()
|
||||
email_agent = create_email_agent() # Create email agent here
|
||||
|
||||
if resume_analyzer and email_agent:
|
||||
print("DEBUG: Starting resume analysis")
|
||||
is_selected, feedback = analyze_resume(
|
||||
st.session_state.resume_text,
|
||||
role,
|
||||
resume_analyzer
|
||||
)
|
||||
print(f"DEBUG: Analysis complete - Selected: {is_selected}, Feedback: {feedback}")
|
||||
|
||||
if is_selected:
|
||||
st.success("Congratulations! Your skills match our requirements.")
|
||||
st.session_state.analysis_complete = True
|
||||
st.session_state.is_selected = True
|
||||
st.rerun()
|
||||
else:
|
||||
st.warning("Unfortunately, your skills don't match our requirements.")
|
||||
st.write(f"Feedback: {feedback}")
|
||||
|
||||
# Send rejection email
|
||||
with st.spinner("Sending feedback email..."):
|
||||
try:
|
||||
send_rejection_email(
|
||||
email_agent=email_agent,
|
||||
to_email=email,
|
||||
role=role,
|
||||
feedback=feedback
|
||||
)
|
||||
st.info("We've sent you an email with detailed feedback.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending rejection email: {e}")
|
||||
st.error("Could not send feedback email. Please try again.")
|
||||
|
||||
if st.session_state.get('analysis_complete') and st.session_state.get('is_selected', False):
|
||||
st.success("Congratulations! Your skills match our requirements.")
|
||||
st.info("Click 'Proceed with Application' to continue with the interview process.")
|
||||
|
||||
if st.button("Proceed with Application", key="proceed_button"):
|
||||
print("DEBUG: Proceed button clicked") # Debug
|
||||
with st.spinner("🔄 Processing your application..."):
|
||||
try:
|
||||
print("DEBUG: Creating email agent") # Debug
|
||||
email_agent = create_email_agent()
|
||||
print(f"DEBUG: Email agent created: {email_agent}") # Debug
|
||||
|
||||
print("DEBUG: Creating scheduler agent") # Debug
|
||||
scheduler_agent = create_scheduler_agent()
|
||||
print(f"DEBUG: Scheduler agent created: {scheduler_agent}") # Debug
|
||||
|
||||
# 3. Send selection email
|
||||
with st.status("📧 Sending confirmation email...", expanded=True) as status:
|
||||
print(f"DEBUG: Attempting to send email to {st.session_state.candidate_email}") # Debug
|
||||
send_selection_email(
|
||||
email_agent,
|
||||
st.session_state.candidate_email,
|
||||
role
|
||||
)
|
||||
print("DEBUG: Email sent successfully") # Debug
|
||||
status.update(label="✅ Confirmation email sent!")
|
||||
|
||||
# 4. Schedule interview
|
||||
with st.status("📅 Scheduling interview...", expanded=True) as status:
|
||||
print("DEBUG: Attempting to schedule interview") # Debug
|
||||
schedule_interview(
|
||||
scheduler_agent,
|
||||
st.session_state.candidate_email,
|
||||
email_agent,
|
||||
role
|
||||
)
|
||||
print("DEBUG: Interview scheduled successfully") # Debug
|
||||
status.update(label="✅ Interview scheduled!")
|
||||
|
||||
print("DEBUG: All processes completed successfully") # Debug
|
||||
st.success("""
|
||||
🎉 Application Successfully Processed!
|
||||
|
||||
Please check your email for:
|
||||
1. Selection confirmation ✅
|
||||
2. Interview details with Zoom link 🔗
|
||||
|
||||
Next steps:
|
||||
1. Review the role requirements
|
||||
2. Prepare for your technical interview
|
||||
3. Join the interview 5 minutes early
|
||||
""")
|
||||
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Error occurred: {str(e)}") # Debug
|
||||
print(f"DEBUG: Error type: {type(e)}") # Debug
|
||||
import traceback
|
||||
print(f"DEBUG: Full traceback: {traceback.format_exc()}") # Debug
|
||||
st.error(f"An error occurred: {str(e)}")
|
||||
st.error("Please try again or contact support.")
|
||||
|
||||
# Reset button
|
||||
if st.sidebar.button("Reset Application"):
|
||||
for key in st.session_state.keys():
|
||||
if key != 'openai_api_key':
|
||||
del st.session_state[key]
|
||||
st.rerun()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue