Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import time | |
| import pytz | |
| import PyPDF2 | |
| import requests | |
| import streamlit as st | |
| from agno.agent import Agent | |
| from phi.utils.log import logger | |
| from phi.tools.zoom import ZoomTool | |
| from agno.tools.email import EmailTools | |
| from datetime import datetime, timedelta | |
| from agno.models.openai import OpenAIChat | |
| from streamlit_pdf_viewer import pdf_viewer | |
| from typing import Literal, Tuple, Dict, Optional | |
| # Class for controlling zoom access | |
| 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 | |
| # This method fetches and returns a valid Zoom access token, either from cache or by making an API call. | |
| def get_access_token(self) -> str: | |
| # If token exists and hasn't expired, return it (avoids unnecessary API calls). | |
| if self.access_token and time.time() < self.token_expires_at: | |
| return str(self.access_token) | |
| # Sets up headers and payload for the Zoom OAuth token request. | |
| headers = {"Content-Type": "application/x-www-form-urlencoded"} | |
| data = { | |
| "grant_type": "account_credentials", | |
| "account_id": self.account_id | |
| } | |
| # Makes a POST request to Zoom's token endpoint using the client secret for authentication. | |
| try: | |
| resp = requests.post( | |
| self.token_url, | |
| headers= headers, | |
| data = data, | |
| auth = (self.client_secret) | |
| ) | |
| resp.raise_for_status() | |
| token_info = resp.json() | |
| self.access_token = token_info["access_token"] | |
| expires_in = token_info["expires_in"] | |
| self.token_expires_at = time.time() + expires_in + 60 | |
| self._set_parent_token(str(self.access_token)) | |
| return str(self.access_token) | |
| # Logs and safely handles any network or response issues. | |
| except requests.RequestException as e: | |
| logger.error(f"Error Fetching access token: {e}") | |
| return "" | |
| def _set_parent_token(self, token: str) -> None: | |
| """ | |
| Helper Function to set the token in 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, LLMs, 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 | |
| """ | |
| } | |
| # safely initialize only the required keys in st.session_state with default values, preventing errors during use in a Streamlit app. | |
| def init_session_state() -> None: | |
| """Initialize only the necessary session state variables.""" | |
| defaults = { | |
| "candidate_email": "", | |
| "openai_api_key" : "", | |
| "resume_text": "", | |
| "analysis_complete": False, | |
| "is_selected": False, | |
| "zoom_account_id": "", | |
| "zoom_client_id": "", | |
| "zoom_client_secret": "", | |
| "email_sender": "", | |
| "email_passkey": "", | |
| "company_name": "", | |
| "current_pdf": None | |
| } | |
| for key, value in defaults.items(): | |
| if key not in st.session_state: | |
| st.session_state[key] = value | |
| # This function returns a function if the API is already initialized | |
| 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 before procedding!") | |
| return None | |
| return Agent( | |
| model = OpenAIChat(id="gpt-4.1-nano", api_key = st.session_state.openai_api_key), | |
| description = "You are a expert Technical Recruiter who analyzes resumes", | |
| instructions=[ | |
| "Analyze the resume against the provided job requirements", | |
| "Be linient with AI/ML candidates who show strong potential", | |
| "Consider project experience as valid experience", | |
| "Value hands-on-experience with key technologies", | |
| "Return the result in a JSON response with selection decision and feedback" | |
| ], | |
| markdown=True | |
| ) | |
| def create_email_agent() -> Agent: | |
| return Agent( | |
| model = OpenAIChat( | |
| id = "gpt-4.1-nano", | |
| api_key=st.session_state.openai_api_key | |
| ), | |
| description="You are a expert technical recruiter coordinator handling email communications.", | |
| instructions=[ | |
| "Draft and send professional recruitment emails", | |
| "Act like a huma writing an email and eliminate unnecessary capital letters", | |
| "Maintain a friendly yet professional tone", | |
| f"Always end the mail with exactly: 'Best Regards\nTeam HR at {st.session_state.company_name}", | |
| "Never include the sender's or receiver's name in the signature", | |
| f"The name of the company is {st.session_state.company_name}" | |
| ], | |
| markdown=True, | |
| show_tool_calls=True | |
| ) | |
| def create_scheduler_agent() -> Agent: | |
| zoom_tools = CustomZoomTool( | |
| account_id = st.session_state.zoom_account_id, | |
| client_id = st.session_state.zoom_client_id, | |
| client_secret = st.session_state.zoom_client_secret | |
| ) | |
| return Agent( | |
| name = "Interview Scheduler", | |
| model = OpenAIChat( | |
| id = "gpt-4o-mini", | |
| 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 (9AM - 5PM IST)", | |
| "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 = "" # Initialize the variable here | |
| for page in pdf_reader.pages: | |
| page_text = page.extract_text() | |
| if page_text: # Some pages may return None | |
| text += page_text | |
| return text | |
| except Exception as e: | |
| st.error(f"Error while parsing PDF File: {str(e)}") | |
| return "" | |
| def analyze_resume(resume_text: str, | |
| role: Literal["ai_ml_engineer", "frontend_engineer", "backend_engineer"], | |
| analyzer: Agent) -> Tuple[bool, str]: | |
| try: | |
| resp = 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, just like this: | |
| {{ | |
| "selected": true, | |
| "feedback": "The resume shows strong alignment with the AI/ML Engineer role, particularly in TensorFlow and Python.", | |
| "matching_skills": ["Python", "TensorFlow", "Scikit-learn"], | |
| "missing_skills": ["Kubernetes", "Docker"], | |
| "experience_level": "mid" | |
| }} | |
| Evaluation Criteria: | |
| 1. Match at least 70% of required skills. | |
| 2. Consider both theoritical knowledge and pratical knowledge. | |
| 3. Value project experience and real-world applications. | |
| 4. Consider transferrable skills from similar technologies. | |
| 5. Look for evidence for continuous learning and adaptability. | |
| Important: Return ONLY the JSON object without any formatting or backticks. | |
| """ | |
| ) | |
| assistant_message = next((msg.content for msg in resp.messages if msg.role == "assistant"), None) | |
| if not assistant_message: | |
| raise ValueError("No assistant message found in the response") | |
| result = json.loads(assistant_message.strip()) | |
| if not isinstance(result, dict) or not all(k in result for k in ["selected", "feedback"]): | |
| raise ValueError("Invalid Response Format") | |
| return result["selected"], result["feedback"] | |
| except (json.JSONDecodeError, ValueError) as e: | |
| st.error(f"Error, while decoding JSON or due to format: {str(e)}") | |
| return False, f"Error while analyzing resume: {str(e)}" | |
| def send_selection_email(email_agent: Agent, to_email: str, role: str) -> None: | |
| """ | |
| Send a selection email with a congratuations. | |
| """ | |
| 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 {st.session_state.company_name}. | |
| """ | |
| ) | |
| def send_rejection_email(email_agent: Agent, to_email : str, role : str, feedback : str) -> None: | |
| """ | |
| Send a rejection mail 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. Avoid unnecessary capital 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 Regards, | |
| Team HR at {st.session_state.company_name} | |
| Do not include any name 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: | |
| """ | |
| Schedule interview during business hours (9 AM - 5 PM IST) | |
| """ | |
| try: | |
| # getting the current time | |
| ist_tz = pytz.timezone("Asia/Kolkata") | |
| current_time_ist = datetime.now(ist_tz) | |
| tomorrow_ist = current_time_ist + timedelta(days=1) | |
| interview_time = tomorrow_ist.replace(hour=11, minute=0, second=0, microsecond=0) | |
| formatted_time = interview_time.strftime("%Y-%m-%dT%H:%M:%S") | |
| meeting_resp = scheduler.run( | |
| f"""Schedule a 60-minute technical interview with these specifications: | |
| - Title: '{role} Technical Interview' | |
| - Date: {formatted_time} | |
| - Timezone: IST (Indian Standard Time) | |
| - Attendee: {candidate_email} | |
| Important Notes: | |
| - The meeting must be between 9 AM - 5 PM IST | |
| - Use IST (UTC+5:30) timezone for all communications | |
| - Include timezone information in the meeting details | |
| - Ask him to be confident and not so nervous and prepare well for the interview | |
| - Also, include a small joke or sarcasm to make him relax. | |
| """ | |
| ) | |
| st.success("Interview Scheduled Successfully! Check your email for details.") | |
| except Exception as e: | |
| logger.error(f"Error scheduling Interview: {str(e)}") | |
| st.error("Unable to schedule interview. Please try again") | |
| def main() -> None: | |
| st.title("HeyHR Aide π’") | |
| init_session_state() | |
| with st.sidebar: | |
| st.header("Configurations") | |
| # OpenAI configurations | |
| st.subheader("OpenAI Configurations") | |
| api_key = st.text_input("OpenAI API Key", placeholder="API key here", type="password", value=st.session_state.openai_api_key, help="Get your OpenAI API Key from platform.openai.com") | |
| if api_key: st.session_state.openai_api_key = api_key | |
| # Zoom Settings | |
| st.subheader("Zoom Configurations") | |
| zoom_account_id = st.text_input("Zoom Account ID", type="password", value=st.session_state.zoom_account_id) | |
| zoom_client_id = st.text_input("Zoom Client ID", type="password", value=st.session_state.zoom_client_id) | |
| zoom_client_secret = st.text_input("Zoom Client Secret", type="password", value=st.session_state.zoom_client_secret) | |
| # Email Settings | |
| email_sender = st.text_input("Sender Email", value=st.session_state.email_sender, help="Email address to send from") | |
| email_passkey = st.text_input("Enter Email App Password", value=st.session_state.email_passkey, type="password", help="App-specific password for email") | |
| company_name = st.text_input("Company Name", value=st.session_state.company_name, help = "Name to use in email communications") | |
| if zoom_account_id: st.session_state.zoom_account_id = zoom_account_id | |
| if zoom_client_id: st.session_state.zoom_client_id = zoom_client_id | |
| if zoom_client_secret: st.session_state.zoom_client_secret = zoom_client_secret | |
| if email_sender: st.session_state.email_sender = email_sender | |
| if email_passkey: st.session_state.email_passkey = email_passkey | |
| if company_name: st.session_state.company_name = company_name | |
| required_configs = { | |
| "OpenAI API Key": st.session_state.openai_api_key, | |
| "Zoom Account ID": st.session_state.zoom_account_id, | |
| "Zoom Client ID": st.session_state.zoom_client_id, | |
| "Zoom Client Secret": st.session_state.zoom_client_secret, | |
| "Email Sender": st.session_state.email_sender, | |
| "Email Password": st.session_state.email_passkey, | |
| "Company Name": st.session_state.company_name | |
| } | |
| missing_config = [k for k, v in required_configs.items() if not v] | |
| if missing_config: | |
| st.warning(f"Pleas configure the following in the sidebar: {', '.join(missing_config)}") | |
| return | |
| if not st.session_state.openai_api_key: | |
| st.warning("Please enter your OpenAI API Key in the sidebar to continue") | |
| return | |
| role = st.selectbox("Select the role you're applying for: ", ["ai_ml_engineer", "frontend_engineer", "backend_engineer"]) | |
| with st.expander("View Required Skills", expanded = True): st.markdown(ROLE_REQUIREMENTS[role]) | |
| # Add a "New Application" button before the resume upload | |
| if st.button("New Application π"): | |
| # Clear all the application related status | |
| keys_to_clear = ["resume_text", "analysis_complete", "is_selected", "candidate_email", "current_pdf"] | |
| for key in keys_to_clear: | |
| if key in st.session_state: | |
| st.session_state[key] = None if key == "current_pdf" else "" | |
| st.rerun() | |
| resume_file = st.file_uploader("Upload your resume (PDF)", type = ["pdf"], key = "resume_uploaded") | |
| if resume_file is not None and resume_file != st.session_state.get("current_pdf"): | |
| st.session_state.current_pdf = resume_file | |
| st.session_state.resume_text = "" | |
| st.session_state.analysis_complete = False | |
| st.session_state.is_selected = False | |
| st.rerun() | |
| if resume_file: | |
| st.subheader("Uploaded Resume") | |
| col1, col2 = st.columns([4, 1]) | |
| with col1: | |
| import tempfile, os | |
| with tempfile.NamedTemporaryFile(delete = False, suffix=".pdf") as tmp_file: | |
| tmp_file.write(resume_file.read()) | |
| tmp_file_path = tmp_file.name | |
| resume_file.seek(0) | |
| try: | |
| pdf_viewer(tmp_file_path) | |
| finally: | |
| os.unlink(tmp_file_path) | |
| with col2: | |
| st.download_button(label="Download", | |
| data = resume_file, | |
| file_name=resume_file.name, | |
| mime="application/pdf") | |
| # Process the resume text | |
| if 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( | |
| "Candidate's 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 the resume..."): | |
| resume_analyzer = create_resume_analyzer() | |
| email_agent = create_email_agent() | |
| 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\n----------\nSelected: {is_selected}\nFeedback: {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("Your skillset are amazing, but unfortunately we have to move forward with other candidates as it doesn't match our requirements") | |
| st.write(f"Feedback: {feedback}") | |
| # Send Rejection mail | |
| with st.spinner("Sending Feedback Mail.."): | |
| try: | |
| send_rejection_email( | |
| email_agent = email_agent, | |
| to_email = email, | |
| role = role, | |
| feedback = feedback | |
| ) | |
| st.info("We've sent you a email with detailed feedback.") | |
| except Exception as e: | |
| logger.error(f"Error sending rejection mail: {str(e)}") | |
| st.error("Could not send the 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() |