Latest code changes committed
Browse files- README.md +63 -1
- app.py +203 -0
- pyproject.toml +29 -0
- src/ats/__init__.py +0 -0
- src/ats/constants.py +47 -0
- src/ats/crews/lead_filter_crew/config/agents.yaml +11 -0
- src/ats/crews/lead_filter_crew/config/tasks.yaml +38 -0
- src/ats/crews/lead_filter_crew/lead_filter_crew.py +35 -0
- src/ats/crews/lead_response_crew/config/agents.yaml +10 -0
- src/ats/crews/lead_response_crew/config/tasks.yaml +26 -0
- src/ats/crews/lead_response_crew/lead_response_crew.py +35 -0
- src/ats/crews/lead_score_crew/config/agents.yaml +10 -0
- src/ats/crews/lead_score_crew/config/tasks.yaml +40 -0
- src/ats/crews/lead_score_crew/lead_score_crew.py +35 -0
- src/ats/crews/resume_parser_crew/config/agents.yaml +12 -0
- src/ats/crews/resume_parser_crew/config/tasks.yaml +8 -0
- src/ats/crews/resume_parser_crew/resume_parser_crew.py +38 -0
- src/ats/crews/resume_parser_crew/tools/resume_parser_tool.py +213 -0
- src/ats/crews/resume_score_crew/config/agents.yaml +10 -0
- src/ats/crews/resume_score_crew/config/tasks.yaml +40 -0
- src/ats/crews/resume_score_crew/resume_score_crew.py +35 -0
- src/ats/crews/rewrite_resume_crew/config/agents.yaml +16 -0
- src/ats/crews/rewrite_resume_crew/config/tasks.yaml +11 -0
- src/ats/crews/rewrite_resume_crew/rewrite_resume_crew.py +38 -0
- src/ats/crews/rewrite_resume_crew/tools/rewrite_resume_tool.py +145 -0
- src/ats/crews/web_scraper_crew/config/agents.yaml +15 -0
- src/ats/crews/web_scraper_crew/config/tasks.yaml +28 -0
- src/ats/crews/web_scraper_crew/web_scraper_crew.py +45 -0
- src/ats/email_responses/Amelia Cole.txt +18 -0
- src/ats/email_responses/Jacob Reed.txt +20 -0
- src/ats/email_responses/John Carter.txt +17 -0
- src/ats/email_responses/Nora Blake.txt +20 -0
- src/ats/email_responses/Owen Mitchell.txt +18 -0
- src/ats/email_responses/Sophia Williams.txt +17 -0
- src/ats/main.py +496 -0
- src/ats/types.py +63 -0
- src/ats/utils/candidateUtils.py +181 -0
- uv.lock +0 -0
README.md
CHANGED
|
@@ -1 +1,63 @@
|
|
| 1 |
-
# ATS-Crewai
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ATS-Crewai
|
| 2 |
+
|
| 3 |
+
Welcome to the ATS Flow project, powered by [crewAI](https://crewai.com). This example demonstrates how you can leverage Flows from crewAI to automate the process of scoring leads, including data collection, analysis, and scoring. By utilizing Flows, the process becomes much simpler and more efficient.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
This flow will guide you through the process of setting up an automated lead scoring system. Here's a brief overview of what will happen in this flow:
|
| 8 |
+
|
| 9 |
+
1. **Load Leads**: The flow starts by loading job description and the multiple resumes of the candidates.
|
| 10 |
+
|
| 11 |
+
2. **Score Leads**: The `ATS-CrewAI` is kicked off to score the loaded leads based on predefined criteria.
|
| 12 |
+
|
| 13 |
+
3. **Human in the Loop**: The top 3 candidates are presented for human review, allowing for additional feedback or proceeding with writing emails.
|
| 14 |
+
|
| 15 |
+
4. **Write and Save Emails**: Emails are generated and saved for all leads.
|
| 16 |
+
|
| 17 |
+
By following this flow, you can efficiently automate the process of scoring leads, leveraging the power of multiple AI agents to handle different aspects of the lead scoring workflow.
|
| 18 |
+
|
| 19 |
+
## Installation
|
| 20 |
+
|
| 21 |
+
Ensure you have Python >=3.10 <=3.13 installed on your system. First, if you haven't already, install CrewAI:
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
pip install crewai
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
Next, navigate to your project directory and install the dependencies:
|
| 28 |
+
|
| 29 |
+
1. First lock the dependencies and then install them:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
crewai install
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Customizing & Dependencies
|
| 36 |
+
|
| 37 |
+
**Add your `OPENAI_API_KEY` into the `.env` file**
|
| 38 |
+
**Add your `SERPER_API_KEY` into the `.env` file**
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
## Running the Project
|
| 42 |
+
|
| 43 |
+
### Run the Flow
|
| 44 |
+
|
| 45 |
+
To kickstart your crew of AI agents and begin task execution, run this from the root folder of your project:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
crewai run
|
| 49 |
+
```
|
| 50 |
+
```bash
|
| 51 |
+
uv run kickoff
|
| 52 |
+
```
|
| 53 |
+
### Plot the Flow
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
uv run plot
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
This command initializes the lead_score_flow, assembling the agents and assigning them tasks as defined in your configuration.
|
| 60 |
+
|
| 61 |
+
When you kickstart the flow, it will orchestrate multiple crews to perform the tasks. The flow will first collect lead data, then analyze the data, score the leads and generate email drafts.
|
| 62 |
+
|
| 63 |
+
|
app.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# job_match_scorer_app.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import os
|
| 4 |
+
import tempfile
|
| 5 |
+
from src.ats.main import employer_kickoff,candidate_kickoff,improve_resume_for_ats
|
| 6 |
+
from src.ats.utils.candidateUtils import get_resume_text,display_resume
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
# Streamlit app
|
| 11 |
+
st.set_page_config(
|
| 12 |
+
page_title="Resume Match Scorer",
|
| 13 |
+
layout="centered"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Sidebar - Role selection (placed above API key input)
|
| 17 |
+
st.sidebar.title("🧭 Select Role")
|
| 18 |
+
selected_role = st.sidebar.selectbox("I am a...", ["Candidate", "Employer"])
|
| 19 |
+
|
| 20 |
+
# Sidebar - API Key input
|
| 21 |
+
st.sidebar.title("🔑 OpenAI API Key")
|
| 22 |
+
api_key = st.sidebar.text_input("Enter your OpenAI API Key", type="password")
|
| 23 |
+
# Conditional UI based on role
|
| 24 |
+
if selected_role == "Employer":
|
| 25 |
+
st.title("ATS Match Tool (For Employer)")
|
| 26 |
+
st.info(
|
| 27 |
+
"Upload a **Job Description** and multiple **Candidate Resumes** (PDF or DOCX). The system will:\n"
|
| 28 |
+
"1. **Parse and match** each resume against the job description.\n"
|
| 29 |
+
"2. **Score and rank** the candidates.\n"
|
| 30 |
+
"3. **Send personalized emails** to each candidate – selected or rejected – based on their match score."
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Inputs
|
| 34 |
+
job_description = st.text_area("📝 Paste Job Description", height=300, placeholder="Paste the full job description here")
|
| 35 |
+
|
| 36 |
+
uploaded_resumes = st.file_uploader(
|
| 37 |
+
"📎 Upload Resume Files (PDF or DOCX)",
|
| 38 |
+
type=["pdf", "docx"],
|
| 39 |
+
accept_multiple_files=True
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
if st.button("🔍 Score Resumes"):
|
| 43 |
+
if not api_key:
|
| 44 |
+
st.error("Please enter your OpenAI API key in the sidebar.")
|
| 45 |
+
elif not job_description.strip():
|
| 46 |
+
st.error("Please enter the Job Description.")
|
| 47 |
+
elif not uploaded_resumes:
|
| 48 |
+
st.error("Please upload at least one resume.")
|
| 49 |
+
else:
|
| 50 |
+
os.environ["OPENAI_API_KEY"]=api_key
|
| 51 |
+
|
| 52 |
+
with st.spinner("Analyzing resumes... This may take a minute."):
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
resume_texts = []
|
| 56 |
+
id=0
|
| 57 |
+
if uploaded_resumes:
|
| 58 |
+
for file in uploaded_resumes:
|
| 59 |
+
id+=1
|
| 60 |
+
text = get_resume_text(file)
|
| 61 |
+
resume_texts.append({
|
| 62 |
+
"id":str(id),
|
| 63 |
+
"filename": file.name,
|
| 64 |
+
"content": text
|
| 65 |
+
})
|
| 66 |
+
#Crewai flow
|
| 67 |
+
agent=employer_kickoff(job_description,resume_texts)
|
| 68 |
+
st.session_state.agent = agent # Save for later use
|
| 69 |
+
|
| 70 |
+
if hasattr(agent.state, "top_candidates"):
|
| 71 |
+
st.markdown("## Top Candidates Identified")
|
| 72 |
+
|
| 73 |
+
for c in agent.state.top_candidates:
|
| 74 |
+
st.markdown(
|
| 75 |
+
f"**Name:** {c.name}<br>"
|
| 76 |
+
f"**Score:** {c.score}<br>"
|
| 77 |
+
f"**Reason:** {c.reason}<br><hr>",
|
| 78 |
+
unsafe_allow_html=True
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
st.success("✅ Email sent successfully to all candidates")
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
st.error(f"Error processing files: {e}")
|
| 85 |
+
|
| 86 |
+
elif selected_role == "Candidate":
|
| 87 |
+
|
| 88 |
+
# --- Title and Mode Selector ---
|
| 89 |
+
st.title("🎯 Resume Assistant: Score or Improve Your Resume")
|
| 90 |
+
st.info(
|
| 91 |
+
"This tool offers two powerful features:\n"
|
| 92 |
+
"1. **Score your resume against a job description** to see how well it fits.\n"
|
| 93 |
+
"2. **Review and rewrite your resume** to improve your ATS (Applicant Tracking System) compatibility.\n\n"
|
| 94 |
+
"➡️ Select your goal below to get started."
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# --- Mode Selector ---
|
| 98 |
+
mode = st.radio(
|
| 99 |
+
"What would you like to do?",
|
| 100 |
+
options=["Score Resume Against Job Description", "Rewrite Resume for ATS Compatibility"],
|
| 101 |
+
index=0
|
| 102 |
+
)
|
| 103 |
+
# --- Page Logic Based on Mode ---
|
| 104 |
+
if mode == "Score Resume Against Job Description":
|
| 105 |
+
st.subheader("🧪 Resume Match Scorer (For Candidates)")
|
| 106 |
+
|
| 107 |
+
job_url = st.text_input("Job Posting URL", placeholder="Enter the full URL here")
|
| 108 |
+
uploaded_resume = st.file_uploader(
|
| 109 |
+
"📎 Upload Your Resume (PDF or DOCX)",
|
| 110 |
+
type=["pdf", "docx"],
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
if st.button("🔍 Score Resume"):
|
| 114 |
+
if not api_key:
|
| 115 |
+
st.error("Please enter your OpenAI API key in the sidebar.")
|
| 116 |
+
elif not job_url.strip():
|
| 117 |
+
st.error("Please enter the URL to parse.")
|
| 118 |
+
elif not uploaded_resume:
|
| 119 |
+
st.error("Please upload your resume.")
|
| 120 |
+
else:
|
| 121 |
+
os.environ["OPENAI_API_KEY"] = api_key
|
| 122 |
+
|
| 123 |
+
with st.spinner("Analyzing your resume... This may take a minute."):
|
| 124 |
+
try:
|
| 125 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 126 |
+
temp_file_path = os.path.join(temp_dir, uploaded_resume.name)
|
| 127 |
+
with open(temp_file_path, "wb") as f:
|
| 128 |
+
f.write(uploaded_resume.getvalue())
|
| 129 |
+
|
| 130 |
+
agent = candidate_kickoff(job_url, temp_file_path)
|
| 131 |
+
st.session_state.agent = agent
|
| 132 |
+
|
| 133 |
+
if hasattr(agent.state, "candidate_score"):
|
| 134 |
+
st.title("Resume Scoring Summary")
|
| 135 |
+
|
| 136 |
+
st.subheader("📊 Candidate Score")
|
| 137 |
+
st.metric(label="ATS Score", value=f"{agent.state.candidate_score.score} / 100")
|
| 138 |
+
|
| 139 |
+
with st.expander("📋 Detailed Evaluation Reasoning", expanded=True):
|
| 140 |
+
st.write(agent.state.candidate_score.reason)
|
| 141 |
+
|
| 142 |
+
if agent.state.candidate_score.score >= 80:
|
| 143 |
+
st.success("Strong match — highly recommended for interview.")
|
| 144 |
+
elif agent.state.candidate_score.score >= 60:
|
| 145 |
+
st.warning("Moderate match — candidate shows potential, but has some gaps.")
|
| 146 |
+
else:
|
| 147 |
+
st.error("Low match — candidate lacks key qualifications for this role.")
|
| 148 |
+
|
| 149 |
+
except Exception as e:
|
| 150 |
+
st.error(f"Error processing files: {e}")
|
| 151 |
+
|
| 152 |
+
elif mode == "Rewrite Resume for ATS Compatibility":
|
| 153 |
+
st.subheader("🛠️ Resume Rewriter for ATS Compatibility")
|
| 154 |
+
uploaded_resume = st.file_uploader("📎 Upload Your Resume (PDF or DOCX)", type=["pdf", "docx"])
|
| 155 |
+
# Optional job description URL
|
| 156 |
+
job_url = st.text_input("🔗 Optional: Enter Job Description URL (for tailored rewriting)")
|
| 157 |
+
|
| 158 |
+
if st.button("🧠 Improve Resume"):
|
| 159 |
+
if not api_key:
|
| 160 |
+
st.error("Please enter your OpenAI API key in the sidebar.")
|
| 161 |
+
elif not uploaded_resume:
|
| 162 |
+
st.error("Please upload your resume.")
|
| 163 |
+
else:
|
| 164 |
+
os.environ["OPENAI_API_KEY"] = api_key
|
| 165 |
+
|
| 166 |
+
with st.spinner("Rewriting your resume... Please wait."):
|
| 167 |
+
try:
|
| 168 |
+
resume_data = get_resume_text(uploaded_resume)
|
| 169 |
+
agent = improve_resume_for_ats(resume_data,job_url)
|
| 170 |
+
if hasattr(agent.state, "initial_score") and agent.state.initial_score is not None:
|
| 171 |
+
st.subheader("📊 Current Resume Score")
|
| 172 |
+
st.metric(label="ATS Score", value=f"{agent.state.initial_score.score} / 100")
|
| 173 |
+
|
| 174 |
+
with st.expander("📋 Detailed Evaluation Reasoning", expanded=True):
|
| 175 |
+
st.write(agent.state.initial_score.reason)
|
| 176 |
+
|
| 177 |
+
if hasattr(agent.state, "improved_resume") and agent.state.improved_resume is not None:
|
| 178 |
+
st.subheader("📊 Improved Resume Score")
|
| 179 |
+
st.metric(label="ATS Score", value=f"{agent.state.improved_resume.score} / 100")
|
| 180 |
+
resume_score= int(agent.state.improved_resume.score)
|
| 181 |
+
if resume_score<85:
|
| 182 |
+
feedback="""We've made the maximum improvements to your resume.
|
| 183 |
+
"While it's still scoring below the target of 85, this version reflects the most optimized
|
| 184 |
+
version based on your input. For further enhancement,
|
| 185 |
+
consider tailoring specific experiences or achievements more closely to the job role."""
|
| 186 |
+
else:
|
| 187 |
+
feedback="Here's a revised version of your resume with better ATS optimization:"
|
| 188 |
+
st.success(feedback)
|
| 189 |
+
st.markdown(agent.state.improved_resume.resume_data)
|
| 190 |
+
with st.expander("📋 Detailed Evaluation Feedback", expanded=True):
|
| 191 |
+
st.write(agent.state.improved_resume.feedback)
|
| 192 |
+
improved_resume=agent.state.improved_resume.resume_data
|
| 193 |
+
st.download_button(
|
| 194 |
+
label="⬇️ Download Improved Resume",
|
| 195 |
+
data=improved_resume.encode("utf-8"),
|
| 196 |
+
file_name="Resume_New.md",
|
| 197 |
+
mime="text/markdown"
|
| 198 |
+
)
|
| 199 |
+
else:
|
| 200 |
+
st.success("Your resume is already highly optimized for Applicant Tracking Systems (ATS), with a strong compatibility score")
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
st.error(f"Error improving resume: {e}")
|
pyproject.toml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "ats-crewai"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11,<3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"crewai[tools]",
|
| 9 |
+
"langchain-tools",
|
| 10 |
+
"crewai-tools",
|
| 11 |
+
"google-auth-oauthlib",
|
| 12 |
+
"google-api-python-client",
|
| 13 |
+
"pyvis",
|
| 14 |
+
"asyncio",
|
| 15 |
+
"onnxruntime==1.15.0",
|
| 16 |
+
"numpy<2.0.0",
|
| 17 |
+
"streamlit",
|
| 18 |
+
"firecrawl-py",
|
| 19 |
+
"spacy>=3.0.0",
|
| 20 |
+
"nltk>=3.8",
|
| 21 |
+
"en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl",
|
| 22 |
+
"python-docx>=0.8.11",
|
| 23 |
+
"docx2txt>=0.9",
|
| 24 |
+
"pymupdf>=1.25.5",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[project.optional-dependencies]
|
| 28 |
+
win = ["python-magic-bin>=0.4.14"] # Windows-only
|
| 29 |
+
linux = ["python-magic>=0.4.27"] # Linux-only
|
src/ats/__init__.py
ADDED
|
File without changes
|
src/ats/constants.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
JOB_DESCRIPTION = """
|
| 2 |
+
# Junior React Developer
|
| 3 |
+
|
| 4 |
+
**Position:** Junior React Developer
|
| 5 |
+
**Duration:** 12-month contract with the possibility of extension based on performance and project needs.
|
| 6 |
+
|
| 7 |
+
We are seeking a motivated Junior React Developer to join our team and assist in the development of our cutting-edge Next.js web application. This project integrates the Vercel AI SDK to enhance user experience with advanced AI-driven features.
|
| 8 |
+
|
| 9 |
+
**Key Responsibilities:**
|
| 10 |
+
- Develop and maintain React components and Next.js applications.
|
| 11 |
+
- Integrate AI-driven features using the Vercel AI SDK.
|
| 12 |
+
- Collaborate with senior developers to design and implement new features.
|
| 13 |
+
- Optimize application performance and ensure responsiveness across different devices.
|
| 14 |
+
- Participate in code reviews and contribute to best practices.
|
| 15 |
+
- Troubleshoot and debug issues to ensure the highest quality of the web application.
|
| 16 |
+
|
| 17 |
+
**Qualifications:**
|
| 18 |
+
- 1-2 years of experience in front-end development with React and Next.js.
|
| 19 |
+
- Proficiency in JavaScript, TypeScript, CSS, and HTML.
|
| 20 |
+
- Experience with Git and RESTful APIs.
|
| 21 |
+
- Familiarity with Vercel AI SDK is a plus.
|
| 22 |
+
- Strong problem-solving skills and attention to detail.
|
| 23 |
+
- Excellent communication and teamwork abilities.
|
| 24 |
+
- Ability to work independently and take initiative on projects.
|
| 25 |
+
|
| 26 |
+
**What We Offer:**
|
| 27 |
+
- Opportunity to work with cutting-edge technologies and AI integration.
|
| 28 |
+
- Collaborative and supportive work environment.
|
| 29 |
+
- Mentorship from senior developers to help grow your skills.
|
| 30 |
+
- Potential for role extension and career advancement within the company.
|
| 31 |
+
- Flexible working hours and the possibility of remote work.
|
| 32 |
+
|
| 33 |
+
This role is ideal for someone looking to grow their skills in Next.js, React, and AI-powered web applications while contributing to impactful projects.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
SKILLS = [
|
| 37 |
+
"React",
|
| 38 |
+
"Next.js",
|
| 39 |
+
"JavaScript",
|
| 40 |
+
"TypeScript",
|
| 41 |
+
"Vercel AI SDK",
|
| 42 |
+
"CSS",
|
| 43 |
+
"HTML",
|
| 44 |
+
"Git",
|
| 45 |
+
"REST APIs",
|
| 46 |
+
"CrewAI",
|
| 47 |
+
]
|
src/ats/crews/lead_filter_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hr_evaluation_agent:
|
| 2 |
+
role: >
|
| 3 |
+
Senior HR Evaluation Expert
|
| 4 |
+
goal: >
|
| 5 |
+
Evaluate candidates based on their ID, name, bio, skills, and years of experience, comparing them against the provided job description.
|
| 6 |
+
Provide a matching score (out of 100) along with a detailed reasoning for the evaluation.
|
| 7 |
+
backstory: >
|
| 8 |
+
As a Senior HR Evaluation Expert, you have extensive experience in assessing candidate profiles for technical and non-technical roles.
|
| 9 |
+
You specialize in evaluating candidates by analyzing their skills, years of experience, career summary, and overall potential fit
|
| 10 |
+
with the job description. You prioritize clear, structured assessments with transparent scoring and actionable feedback.
|
| 11 |
+
llm: openai/gpt-4o-mini
|
src/ats/crews/lead_filter_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
evaluate_candidate:
|
| 2 |
+
description: >
|
| 3 |
+
Evaluate a candidate's bio, skills, and years of experience based on the provided job description.
|
| 4 |
+
|
| 5 |
+
Use your expertise to assess how well the candidate matches the job requirements. Focus only on:
|
| 6 |
+
- Skill match
|
| 7 |
+
- Relevant experience
|
| 8 |
+
- Cultural fit
|
| 9 |
+
- Growth potential
|
| 10 |
+
|
| 11 |
+
CANDIDATE INFORMATION
|
| 12 |
+
---------------------
|
| 13 |
+
Candidate ID: {candidate_id}
|
| 14 |
+
Name: {name}
|
| 15 |
+
Bio:
|
| 16 |
+
{bio}
|
| 17 |
+
Skills:
|
| 18 |
+
{skills}
|
| 19 |
+
Years of Experience: {years_of_exp}
|
| 20 |
+
|
| 21 |
+
JOB DESCRIPTION
|
| 22 |
+
----------------
|
| 23 |
+
{job_description}
|
| 24 |
+
|
| 25 |
+
ADDITIONAL INSTRUCTIONS
|
| 26 |
+
------------------------
|
| 27 |
+
Your final answer MUST return a JSON object in the following format:
|
| 28 |
+
{
|
| 29 |
+
"id": "<candidate_id>",
|
| 30 |
+
"name":"<candidate_name>",
|
| 31 |
+
"result": "Pass" or "Fail",
|
| 32 |
+
"reason": detailed reasoning explaining the result is "Pass" or "Fail"
|
| 33 |
+
}
|
| 34 |
+
Do not include any explanation, score, or extra text outside this JSON object.
|
| 35 |
+
|
| 36 |
+
expected_output: >
|
| 37 |
+
A JSON object with "candidate_id" ,"result" ("Pass" or "Fail") and "reason" fields only.
|
| 38 |
+
agent: hr_evaluation_agent
|
src/ats/crews/lead_filter_crew/lead_filter_crew.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
from src.ats.types import CandidateFilter
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@CrewBase
|
| 7 |
+
class LeadFilterCrew:
|
| 8 |
+
"""Lead Filter Crew"""
|
| 9 |
+
|
| 10 |
+
agents_config = "config/agents.yaml"
|
| 11 |
+
tasks_config = "config/tasks.yaml"
|
| 12 |
+
|
| 13 |
+
@agent
|
| 14 |
+
def hr_evaluation_agent(self) -> Agent:
|
| 15 |
+
return Agent(
|
| 16 |
+
config=self.agents_config["hr_evaluation_agent"],
|
| 17 |
+
verbose=True,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@task
|
| 21 |
+
def evaluate_candidate_task(self) -> Task:
|
| 22 |
+
return Task(
|
| 23 |
+
config=self.tasks_config["evaluate_candidate"],
|
| 24 |
+
output_pydantic=CandidateFilter,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
@crew
|
| 28 |
+
def crew(self) -> Crew:
|
| 29 |
+
"""Creates the Lead Filter Crew"""
|
| 30 |
+
return Crew(
|
| 31 |
+
agents=self.agents,
|
| 32 |
+
tasks=self.tasks,
|
| 33 |
+
process=Process.sequential,
|
| 34 |
+
verbose=True,
|
| 35 |
+
)
|
src/ats/crews/lead_response_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
email_followup_agent:
|
| 2 |
+
role: >
|
| 3 |
+
HR Coordinator
|
| 4 |
+
goal: >
|
| 5 |
+
Compose personalized follow-up emails to candidates based on their bio and whether they are being pursued for the job.
|
| 6 |
+
If we are proceeding, request availability for a Zoom call. Otherwise, send a polite rejection email.
|
| 7 |
+
backstory: >
|
| 8 |
+
You are an HR professional named Sunitha L V who works at XYZ Technologies Pvt Ltd. You are known with excellent communication skills and a talent for crafting personalized and thoughtful
|
| 9 |
+
emails to job candidates. You understand the importance of maintaining a positive and professional tone in all correspondence.
|
| 10 |
+
llm: openai/gpt-4o-mini
|
src/ats/crews/lead_response_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
send_followup_email:
|
| 2 |
+
description: >
|
| 3 |
+
Compose personalized follow-up emails for candidates who applied to a specific job.
|
| 4 |
+
|
| 5 |
+
You will use the candidate's name, reason of acceptance or rejection, and whether the company wants to proceed with them to generate the email.
|
| 6 |
+
If the candidate is proceeding, ask them for their availability for a Zoom call in the upcoming days.
|
| 7 |
+
If not, send a polite rejection email.
|
| 8 |
+
|
| 9 |
+
CANDIDATE DETAILS
|
| 10 |
+
-----------------
|
| 11 |
+
Candidate ID: {candidate_id}
|
| 12 |
+
Name: {name}
|
| 13 |
+
Reason:
|
| 14 |
+
{reason}
|
| 15 |
+
|
| 16 |
+
PROCEEDING WITH CANDIDATE: {proceed_with_candidate}
|
| 17 |
+
|
| 18 |
+
ADDITIONAL INSTRUCTIONS
|
| 19 |
+
-----------------------
|
| 20 |
+
- If we are proceeding, ask for their availability for a Zoom call within the next few days.
|
| 21 |
+
- If we are not proceeding, send a polite rejection email, acknowledging their effort in applying and appreciating their time.
|
| 22 |
+
|
| 23 |
+
expected_output: >
|
| 24 |
+
A personalized email based on the candidate's information. It should be professional and respectful,
|
| 25 |
+
either inviting them for a Zoom call or letting them know we are pursuing other candidates.
|
| 26 |
+
agent: email_followup_agent
|
src/ats/crews/lead_response_crew/lead_response_crew.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@CrewBase
|
| 6 |
+
class LeadResponseCrew:
|
| 7 |
+
"""Lead Response Crew"""
|
| 8 |
+
|
| 9 |
+
agents_config = "config/agents.yaml"
|
| 10 |
+
tasks_config = "config/tasks.yaml"
|
| 11 |
+
|
| 12 |
+
@agent
|
| 13 |
+
def email_followup_agent(self) -> Agent:
|
| 14 |
+
return Agent(
|
| 15 |
+
config=self.agents_config["email_followup_agent"],
|
| 16 |
+
verbose=True,
|
| 17 |
+
allow_delegation=False,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@task
|
| 21 |
+
def send_followup_email_task(self) -> Task:
|
| 22 |
+
return Task(
|
| 23 |
+
config=self.tasks_config["send_followup_email"],
|
| 24 |
+
verbose=True,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
@crew
|
| 28 |
+
def crew(self) -> Crew:
|
| 29 |
+
"""Creates the Lead Response Crew"""
|
| 30 |
+
return Crew(
|
| 31 |
+
agents=self.agents,
|
| 32 |
+
tasks=self.tasks,
|
| 33 |
+
process=Process.sequential,
|
| 34 |
+
verbose=True,
|
| 35 |
+
)
|
src/ats/crews/lead_score_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hr_evaluation_agent:
|
| 2 |
+
role: >
|
| 3 |
+
Senior HR Evaluation Expert
|
| 4 |
+
goal: >
|
| 5 |
+
Evaluate resumes by scoring them against job descriptions when available, or assessing ATS compliance when no job description is provided.
|
| 6 |
+
backstory: >
|
| 7 |
+
As a Senior HR Evaluation Expert, you have extensive experience in assessing candidate profiles. You excel at
|
| 8 |
+
evaluating how well candidates match job descriptions by analyzing their skills, experience, cultural fit, and
|
| 9 |
+
growth potential. Your professional background allows you to provide comprehensive evaluations with clear reasoning.
|
| 10 |
+
llm: openai/gpt-4o-mini
|
src/ats/crews/lead_score_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
evaluate_candidate:
|
| 2 |
+
description: >
|
| 3 |
+
Evaluate a candidate's resume using job description when available, or ATS best practices when no job description is provided.
|
| 4 |
+
|
| 5 |
+
## Evaluation Modes:
|
| 6 |
+
A) When job description is provided:
|
| 7 |
+
- Calculate skill match percentage
|
| 8 |
+
- Assess experience relevance (years, domain expertise)
|
| 9 |
+
- Identify cultural fit indicators
|
| 10 |
+
- Evaluate growth potential
|
| 11 |
+
|
| 12 |
+
B) When no job description:
|
| 13 |
+
- Score ATS compliance (0-100)
|
| 14 |
+
- Check action verb density
|
| 15 |
+
- Count quantified achievements
|
| 16 |
+
- Assess formatting effectiveness
|
| 17 |
+
- Verify keyword optimization
|
| 18 |
+
|
| 19 |
+
CANDIDATE BIO
|
| 20 |
+
-------------
|
| 21 |
+
Candidate ID: {candidate_id}
|
| 22 |
+
Name: {name}
|
| 23 |
+
Bio:
|
| 24 |
+
{bio}
|
| 25 |
+
|
| 26 |
+
JOB DESCRIPTION
|
| 27 |
+
---------------
|
| 28 |
+
{job_description?JOB DESCRIPTION:## No Job Description Provided - Using ATS Standards##}
|
| 29 |
+
|
| 30 |
+
ADDITIONAL INSTRUCTIONS
|
| 31 |
+
-----------------------
|
| 32 |
+
Your final answer MUST include:
|
| 33 |
+
- The candidates unique ID
|
| 34 |
+
- A score between 1 and 100. Don't use numbers like 100, 75, or 50. Instead, use specific numbers like 87, 63, or 42.
|
| 35 |
+
- A detailed reasoning, considering the candidate’s skill match, experience, cultural fit, and growth potential.
|
| 36 |
+
{additional_instructions}
|
| 37 |
+
|
| 38 |
+
expected_output: >
|
| 39 |
+
A very specific score from 1 to 100 for the candidate, along with a detailed reasoning explaining why you assigned this score.
|
| 40 |
+
agent: hr_evaluation_agent
|
src/ats/crews/lead_score_crew/lead_score_crew.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
from src.ats.types import CandidateScore
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@CrewBase
|
| 7 |
+
class LeadScoreCrew:
|
| 8 |
+
"""Lead Score Crew"""
|
| 9 |
+
|
| 10 |
+
agents_config = "config/agents.yaml"
|
| 11 |
+
tasks_config = "config/tasks.yaml"
|
| 12 |
+
|
| 13 |
+
@agent
|
| 14 |
+
def hr_evaluation_agent(self) -> Agent:
|
| 15 |
+
return Agent(
|
| 16 |
+
config=self.agents_config["hr_evaluation_agent"],
|
| 17 |
+
verbose=True,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@task
|
| 21 |
+
def evaluate_candidate_task(self) -> Task:
|
| 22 |
+
return Task(
|
| 23 |
+
config=self.tasks_config["evaluate_candidate"],
|
| 24 |
+
output_pydantic=CandidateScore,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
@crew
|
| 28 |
+
def crew(self) -> Crew:
|
| 29 |
+
"""Creates the Lead Score Crew"""
|
| 30 |
+
return Crew(
|
| 31 |
+
agents=self.agents,
|
| 32 |
+
tasks=self.tasks,
|
| 33 |
+
process=Process.sequential,
|
| 34 |
+
verbose=True,
|
| 35 |
+
)
|
src/ats/crews/resume_parser_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
resume_parser_agent:
|
| 2 |
+
role: >
|
| 3 |
+
Resume Parser agent
|
| 4 |
+
goal: >
|
| 5 |
+
Parse and extract structured candidate personal and professional information from the given resume file.
|
| 6 |
+
backstory: >
|
| 7 |
+
You are an AI assistant specialized in processing resumes using natural language and automated parsing tools.
|
| 8 |
+
Your job is to extract valuable candidate information from resume file for further evaluation by recruiters.
|
| 9 |
+
llm: openai/gpt-4o-mini
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
src/ats/crews/resume_parser_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
resume_parser_task:
|
| 2 |
+
description: >
|
| 3 |
+
Parse the given resume file and extract structured information like name, education, skills, and experience.
|
| 4 |
+
Resume file path :{file_path}
|
| 5 |
+
|
| 6 |
+
expected_output: >
|
| 7 |
+
A structured summary of the candidate’s resume including name, contact information, education, work experience, and key skills.
|
| 8 |
+
agent: resume_parser_agent
|
src/ats/crews/resume_parser_crew/resume_parser_crew.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
from src.ats.crews.resume_parser_crew.tools.resume_parser_tool import ResumeParserTool
|
| 4 |
+
from src.ats.types import ResumeData
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@CrewBase
|
| 8 |
+
class ResumeParserCrew:
|
| 9 |
+
"""Resume Parser Crew"""
|
| 10 |
+
|
| 11 |
+
agents_config = "config/agents.yaml"
|
| 12 |
+
tasks_config = "config/tasks.yaml"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@agent
|
| 16 |
+
def resume_parser_agent(self) -> Agent:
|
| 17 |
+
return Agent(
|
| 18 |
+
config=self.agents_config["resume_parser_agent"],
|
| 19 |
+
verbose=True,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
@task
|
| 23 |
+
def resume_parser_task(self) -> Task:
|
| 24 |
+
return Task(
|
| 25 |
+
config=self.tasks_config["resume_parser_task"],
|
| 26 |
+
tools=[ResumeParserTool()],
|
| 27 |
+
output_pydantic=ResumeData
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
@crew
|
| 31 |
+
def crew(self) -> Crew:
|
| 32 |
+
"""Creates the Resume Parser Crew"""
|
| 33 |
+
return Crew(
|
| 34 |
+
agents=self.agents,
|
| 35 |
+
tasks=self.tasks,
|
| 36 |
+
process=Process.sequential,
|
| 37 |
+
verbose=True,
|
| 38 |
+
)
|
src/ats/crews/resume_parser_crew/tools/resume_parser_tool.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai.tools import BaseTool
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
import os
|
| 4 |
+
from typing import Type
|
| 5 |
+
import re
|
| 6 |
+
import docx2txt
|
| 7 |
+
import fitz # PyMuPDF
|
| 8 |
+
|
| 9 |
+
class ResumeParserInput(BaseModel):
|
| 10 |
+
file_path: str = Field(
|
| 11 |
+
...,
|
| 12 |
+
description="Path to the resume file (PDF/DOCX) to be parsed."
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
class ResumeParserTool(BaseTool):
|
| 16 |
+
name: str = "Custom Resume Parser"
|
| 17 |
+
description: str = (
|
| 18 |
+
"Parses resumes from a file path (PDF/DOCX) and returns structured markdown-formatted data "
|
| 19 |
+
"including name, email, phone, skills, education, experience, and social links."
|
| 20 |
+
)
|
| 21 |
+
args_schema: Type[BaseModel] = ResumeParserInput
|
| 22 |
+
|
| 23 |
+
def _extract_text(self, file_path: str) -> str:
|
| 24 |
+
if file_path.lower().endswith('.pdf'):
|
| 25 |
+
doc = fitz.open(file_path)
|
| 26 |
+
return "\n".join([page.get_text() for page in doc])
|
| 27 |
+
elif file_path.lower().endswith('.docx'):
|
| 28 |
+
return docx2txt.process(file_path)
|
| 29 |
+
else:
|
| 30 |
+
raise ValueError("Unsupported file type. Only PDF and DOCX are supported.")
|
| 31 |
+
|
| 32 |
+
def _extract_field(self, text: str) -> dict:
|
| 33 |
+
data = {}
|
| 34 |
+
|
| 35 |
+
# Basic fields using regex
|
| 36 |
+
data["email"] = re.search(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+", text)
|
| 37 |
+
print("TEXT : ",text)
|
| 38 |
+
# Phone number extraction
|
| 39 |
+
phone_pattern = re.compile(r"""
|
| 40 |
+
(?:\+?\d{1,3}[-.\s]?)? # Optional country code
|
| 41 |
+
\(?\d{3}\)?[-.\s]? # Area code with optional parentheses
|
| 42 |
+
\d{3}[-.\s]? # First 3 digits
|
| 43 |
+
\d{4} # Last 4 digits
|
| 44 |
+
""", re.VERBOSE)
|
| 45 |
+
|
| 46 |
+
phone_match = phone_pattern.search(text)
|
| 47 |
+
data["mobile_number"] = phone_match if phone_match else None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
data["linkedin"] = re.search(r"(https?://)?(www\.)?linkedin\.com/[^\s]+", text)
|
| 51 |
+
data["github"] = re.search(r"(https?://)?(www\.)?github\.com/\S+", text)
|
| 52 |
+
|
| 53 |
+
# Extract name: heuristic (first non-empty line with more than one word)
|
| 54 |
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
| 55 |
+
data["name"] = lines[0] if lines and len(lines[0].split()) <= 5 else "Not found"
|
| 56 |
+
|
| 57 |
+
# Extract sections using keywords
|
| 58 |
+
lower_text = text.lower()
|
| 59 |
+
|
| 60 |
+
def split_experience_entries(experience_text):
|
| 61 |
+
job_entries = []
|
| 62 |
+
|
| 63 |
+
# Remove bullet characters and normalize spacing
|
| 64 |
+
cleaned = re.sub(r"[\u2022\u2023\u25E6\u2043\u2219\u00b7\u2027\u25CF\u25CB\u25A0\u25A1\u25AA\u25AB\uF0B7]", "-", experience_text)
|
| 65 |
+
cleaned = re.sub(r"\s{2,}", " ", cleaned) # Collapse multiple spaces
|
| 66 |
+
cleaned = cleaned.replace('\n', ' ').strip()
|
| 67 |
+
|
| 68 |
+
# Match job entries like: Role | Company | Dates
|
| 69 |
+
job_pattern = re.compile(r'([^\|]+?\|\s*[^\|]+?\|\s*[^|]+?)(?=\s+[^\|]+?\s+\|\s+[^\|]+?\s+\||\Z)', re.DOTALL)
|
| 70 |
+
|
| 71 |
+
matches = job_pattern.findall(cleaned)
|
| 72 |
+
for match in matches:
|
| 73 |
+
job_entries.append(match.strip())
|
| 74 |
+
|
| 75 |
+
return job_entries
|
| 76 |
+
|
| 77 |
+
def split_skills(text):
|
| 78 |
+
return [s.strip() for s in re.split(r'[,\n]', text) if s.strip()]
|
| 79 |
+
|
| 80 |
+
def split_education_entries(text):
|
| 81 |
+
return [line.strip() for line in text.split('\n') if line.strip()]
|
| 82 |
+
|
| 83 |
+
def split_project_entries(text):
|
| 84 |
+
return [line.strip() for line in text.split('\n\n') if line.strip()]
|
| 85 |
+
|
| 86 |
+
def split_certifications(text):
|
| 87 |
+
return [line.strip() for line in text.split('\n') if line.strip()]
|
| 88 |
+
|
| 89 |
+
# def extract_section(possible_keywords):
|
| 90 |
+
# if isinstance(possible_keywords, str):
|
| 91 |
+
# possible_keywords = [possible_keywords]
|
| 92 |
+
|
| 93 |
+
# for keyword in possible_keywords:
|
| 94 |
+
# idx = lower_text.find(keyword.lower())
|
| 95 |
+
# if idx != -1:
|
| 96 |
+
# end_idx = lower_text.find('\n\n', idx)
|
| 97 |
+
# return text[idx:end_idx].strip() if end_idx != -1 else text[idx:].strip()
|
| 98 |
+
# return "Not found"
|
| 99 |
+
|
| 100 |
+
def extract_experience(text):
|
| 101 |
+
experience_pattern = r"""
|
| 102 |
+
(?:(?:\bwith|\bpossess|\bover|\baround)?\s*) # Common prefixes
|
| 103 |
+
(\d+(?:\.\d+)?) # Number (e.g., 3 or 2.5)
|
| 104 |
+
\s* # Optional space
|
| 105 |
+
(?:\+?\s*\d+)? # Optional range like 3-5
|
| 106 |
+
\s* # Optional space
|
| 107 |
+
(?:years?|yrs?) # Variations of "years"
|
| 108 |
+
(?:\s+of\s+(?:total\s+)??experience)? # Optional "of experience"
|
| 109 |
+
"""
|
| 110 |
+
match = re.search(experience_pattern, text, re.IGNORECASE | re.VERBOSE)
|
| 111 |
+
if match:
|
| 112 |
+
try:
|
| 113 |
+
return float(match.group(1))
|
| 114 |
+
except (ValueError, AttributeError):
|
| 115 |
+
return None
|
| 116 |
+
return None
|
| 117 |
+
|
| 118 |
+
def extract_section(text, section_keywords, all_headers):
|
| 119 |
+
if isinstance(section_keywords, str):
|
| 120 |
+
section_keywords = [section_keywords]
|
| 121 |
+
|
| 122 |
+
# Create a combined regex pattern for headers (used to detect section boundaries)
|
| 123 |
+
headers_pattern = '|'.join([re.escape(h) for h in all_headers])
|
| 124 |
+
|
| 125 |
+
for keyword in section_keywords:
|
| 126 |
+
# Regex to find the keyword as a header
|
| 127 |
+
pattern = re.compile(rf'({keyword})\s*\n', flags=re.IGNORECASE)
|
| 128 |
+
match = pattern.search(text)
|
| 129 |
+
if match:
|
| 130 |
+
start_idx = match.start()
|
| 131 |
+
|
| 132 |
+
# Match only when a known header appears alone or nearly alone on a line
|
| 133 |
+
next_header_pattern = re.compile(
|
| 134 |
+
rf'\n\s*({headers_pattern})\s*[:\-]?\s*\n',
|
| 135 |
+
flags=re.IGNORECASE
|
| 136 |
+
)
|
| 137 |
+
next_match = next_header_pattern.search(text, pos=match.end())
|
| 138 |
+
end_idx = next_match.start() if next_match else len(text)
|
| 139 |
+
|
| 140 |
+
return text[start_idx:end_idx].strip()
|
| 141 |
+
|
| 142 |
+
return "Not found"
|
| 143 |
+
|
| 144 |
+
# Common headers (all potential headers to detect section boundaries)
|
| 145 |
+
all_headers = ["Experience", "Education", "Skills", "Certifications", "Projects", "Summary", "Objective", "Achievements"]
|
| 146 |
+
|
| 147 |
+
objective_text = extract_section(
|
| 148 |
+
text,
|
| 149 |
+
section_keywords=["Objective", "Career Objective", "Professional Summary", "Summary"],
|
| 150 |
+
all_headers=all_headers
|
| 151 |
+
)
|
| 152 |
+
data["objective"] = objective_text if objective_text != "Not found" else None
|
| 153 |
+
|
| 154 |
+
skills_text = extract_section(
|
| 155 |
+
text,
|
| 156 |
+
section_keywords=["Skills", "Technical Skills", "Core Competencies"],
|
| 157 |
+
all_headers=all_headers
|
| 158 |
+
)
|
| 159 |
+
data["skills"] = split_skills(skills_text)
|
| 160 |
+
|
| 161 |
+
education_text = extract_section(
|
| 162 |
+
text,
|
| 163 |
+
section_keywords=["Education", "Academic Background", "Educational Qualifications"],
|
| 164 |
+
all_headers=all_headers
|
| 165 |
+
)
|
| 166 |
+
data["education"] = split_education_entries(education_text)
|
| 167 |
+
|
| 168 |
+
data["experience_years"] = extract_experience(text)
|
| 169 |
+
|
| 170 |
+
experience_text = extract_section(text,
|
| 171 |
+
section_keywords=["Experience", "Professional Experience", "Work Experience"],
|
| 172 |
+
all_headers=all_headers
|
| 173 |
+
)
|
| 174 |
+
if experience_text.lower().startswith(("professional experience", "work experience", "experience")):
|
| 175 |
+
experience_text = experience_text.split('\n', 1)[-1].strip()
|
| 176 |
+
data["experience_details"] = split_experience_entries(experience_text)
|
| 177 |
+
|
| 178 |
+
projects_text = extract_section(
|
| 179 |
+
text,
|
| 180 |
+
section_keywords=["Projects", "Key Projects", "Academic Projects"],
|
| 181 |
+
all_headers=all_headers
|
| 182 |
+
)
|
| 183 |
+
data["projects"] = split_project_entries(projects_text)
|
| 184 |
+
|
| 185 |
+
certs_text = extract_section(
|
| 186 |
+
text,
|
| 187 |
+
section_keywords=["Certifications", "Licenses", "Certificates"],
|
| 188 |
+
all_headers=all_headers
|
| 189 |
+
)
|
| 190 |
+
data["certifications"] = split_certifications(certs_text)
|
| 191 |
+
|
| 192 |
+
# Post-process regex matches
|
| 193 |
+
for k in ["email", "mobile_number", "linkedin", "github"]:
|
| 194 |
+
if data[k]:
|
| 195 |
+
data[k] = data[k].group()
|
| 196 |
+
else:
|
| 197 |
+
data[k] = "Not found"
|
| 198 |
+
|
| 199 |
+
return data
|
| 200 |
+
|
| 201 |
+
def _run(self, file_path: str) -> str:
|
| 202 |
+
try:
|
| 203 |
+
if not os.path.isfile(file_path):
|
| 204 |
+
return f"❌ Error: File does not exist at {file_path}"
|
| 205 |
+
|
| 206 |
+
text = self._extract_text(file_path)
|
| 207 |
+
extracted = self._extract_field(text)
|
| 208 |
+
|
| 209 |
+
result = [f"**{k.replace('_', ' ').title()}**: {v}" for k, v in extracted.items()]
|
| 210 |
+
return "\n".join(result)
|
| 211 |
+
|
| 212 |
+
except Exception as e:
|
| 213 |
+
return f"❌ An error occurred during parsing: {str(e)}"
|
src/ats/crews/resume_score_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
resume_score_agent:
|
| 2 |
+
role: >
|
| 3 |
+
Senior HR Evaluation Expert
|
| 4 |
+
goal: >
|
| 5 |
+
Evaluate a structured resume (as a ResumeData object) against a provided job description and return a score from 0 to 100, along with detailed reasoning for the score.
|
| 6 |
+
backstory: >
|
| 7 |
+
As a Senior HR Evaluation Expert, you have extensive experience in assessing structured resume data.
|
| 8 |
+
You analyze resumes by comparing the candidate's skills, education, experience, certifications, projects, and online presence (LinkedIn/GitHub) against the job description.
|
| 9 |
+
You account for relevance, depth, and alignment with the role. You deliver clear, structured, and insightful evaluations that help determine candidate-job fit.
|
| 10 |
+
llm: openai/gpt-4o-mini
|
src/ats/crews/resume_score_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
resume_score_task:
|
| 2 |
+
description: >
|
| 3 |
+
Evaluate a candidate's structured resume data against the provided job description.
|
| 4 |
+
|
| 5 |
+
Use your expertise to carefully assess how well the candidate fits the job requirements. Consider key factors such as:
|
| 6 |
+
- Skill match
|
| 7 |
+
- Relevant experience (years + project details)
|
| 8 |
+
- Educational background
|
| 9 |
+
- Certifications
|
| 10 |
+
- Project experience
|
| 11 |
+
- Cultural fit and growth potential (use LinkedIn/GitHub if available)
|
| 12 |
+
|
| 13 |
+
CANDIDATE RESUME DATA
|
| 14 |
+
---------------------
|
| 15 |
+
Name: {name}
|
| 16 |
+
Email: {email}
|
| 17 |
+
Mobile Number: {mobile_number}
|
| 18 |
+
Skills: {skills}
|
| 19 |
+
Education: {education}
|
| 20 |
+
Objective: {objective}
|
| 21 |
+
Total Experience (Years): {experience_years}
|
| 22 |
+
Experience Details: {experience_details}
|
| 23 |
+
Projects: {projects}
|
| 24 |
+
Certifications: {certifications}
|
| 25 |
+
LinkedIn: {linkedin}
|
| 26 |
+
GitHub: {github}
|
| 27 |
+
|
| 28 |
+
JOB DESCRIPTION
|
| 29 |
+
---------------
|
| 30 |
+
{job_description}
|
| 31 |
+
|
| 32 |
+
ADDITIONAL INSTRUCTIONS
|
| 33 |
+
-----------------------
|
| 34 |
+
Your final answer MUST include:
|
| 35 |
+
- A score between 1 and 100. Don't use numbers like 100, 75, or 50. Instead, use specific numbers like 87, 63, or 42.
|
| 36 |
+
- A detailed reasoning, considering the candidate’s skill match, experience, education, projects, and growth potential.
|
| 37 |
+
|
| 38 |
+
expected_output: >
|
| 39 |
+
A specific score (1-100) and detailed reasoning for why that score was given based on the candidate's resume data and the job description.
|
| 40 |
+
agent: resume_score_agent
|
src/ats/crews/resume_score_crew/resume_score_crew.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
from src.ats.types import CandidateScore
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@CrewBase
|
| 7 |
+
class ResumeScoreCrew:
|
| 8 |
+
"""Resume Score Crew"""
|
| 9 |
+
|
| 10 |
+
agents_config = "config/agents.yaml"
|
| 11 |
+
tasks_config = "config/tasks.yaml"
|
| 12 |
+
|
| 13 |
+
@agent
|
| 14 |
+
def resume_score_agent(self) -> Agent:
|
| 15 |
+
return Agent(
|
| 16 |
+
config=self.agents_config["resume_score_agent"],
|
| 17 |
+
#verbose=True,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@task
|
| 21 |
+
def resume_score_task(self) -> Task:
|
| 22 |
+
return Task(
|
| 23 |
+
config=self.tasks_config["resume_score_task"],
|
| 24 |
+
output_pydantic=CandidateScore,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
@crew
|
| 28 |
+
def crew(self) -> Crew:
|
| 29 |
+
"""Creates the Resume Score Crew"""
|
| 30 |
+
return Crew(
|
| 31 |
+
agents=self.agents,
|
| 32 |
+
tasks=self.tasks,
|
| 33 |
+
process=Process.sequential,
|
| 34 |
+
#verbose=True,
|
| 35 |
+
)
|
src/ats/crews/rewrite_resume_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
rewrite_resume_agent:
|
| 2 |
+
role: >
|
| 3 |
+
ATS Resume Rewriting Agent
|
| 4 |
+
goal: >
|
| 5 |
+
Rewrite the uploaded resume to improve its compatibility with Applicant Tracking Systems (ATS).
|
| 6 |
+
If a job description is provided, tailor the resume to match it for higher alignment and score.
|
| 7 |
+
backstory: >
|
| 8 |
+
You are an AI assistant specializing in enhancing resumes for optimal ATS parsing and matching.
|
| 9 |
+
You understand how to structure content, use relevant keywords, and present skills in a format
|
| 10 |
+
that improves visibility in automated screening systems. If a job description is available,
|
| 11 |
+
you analyze it and strategically tailor the resume to reflect relevant skills, experiences, and keywords.
|
| 12 |
+
If not, you rewrite the resume with general best practices for ATS optimization.
|
| 13 |
+
llm: openai/gpt-4o-mini
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
|
src/ats/crews/rewrite_resume_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
rewrite_resume_task:
|
| 2 |
+
description: >
|
| 3 |
+
Rewrite the resume contents {resume_data} to improve its compatibility with Applicant Tracking Systems (ATS).
|
| 4 |
+
If a job description is provided via {job_description}, tailor the resume to align with the job requirements.
|
| 5 |
+
Otherwise, apply general ATS optimization best practices.
|
| 6 |
+
|
| 7 |
+
expected_output: >
|
| 8 |
+
A rewritten version of the resume that is optimized for ATS.
|
| 9 |
+
If a job description was provided, the output should be customized to highlight relevant experience, skills, and keywords matching the job.
|
| 10 |
+
Otherwise, the resume should be improved for general ATS compatibility.
|
| 11 |
+
agent: rewrite_resume_agent
|
src/ats/crews/rewrite_resume_crew/rewrite_resume_crew.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
from src.ats.crews.rewrite_resume_crew.tools.rewrite_resume_tool import RewriteResumeTool
|
| 4 |
+
from src.ats.types import Resume_Final
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@CrewBase
|
| 8 |
+
class RewriteResumeCrew:
|
| 9 |
+
"""Rewrite Resume Crew"""
|
| 10 |
+
|
| 11 |
+
agents_config = "config/agents.yaml"
|
| 12 |
+
tasks_config = "config/tasks.yaml"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@agent
|
| 16 |
+
def rewrite_resume_agent(self) -> Agent:
|
| 17 |
+
return Agent(
|
| 18 |
+
config=self.agents_config["rewrite_resume_agent"],
|
| 19 |
+
verbose=True,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
@task
|
| 23 |
+
def rewrite_resume_task(self) -> Task:
|
| 24 |
+
return Task(
|
| 25 |
+
config=self.tasks_config["rewrite_resume_task"],
|
| 26 |
+
tools=[RewriteResumeTool()],
|
| 27 |
+
output_pydantic=Resume_Final,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
@crew
|
| 31 |
+
def crew(self) -> Crew:
|
| 32 |
+
"""Creates the Rewrite Resume Crew"""
|
| 33 |
+
return Crew(
|
| 34 |
+
agents=self.agents,
|
| 35 |
+
tasks=self.tasks,
|
| 36 |
+
process=Process.sequential,
|
| 37 |
+
verbose=True,
|
| 38 |
+
)
|
src/ats/crews/rewrite_resume_crew/tools/rewrite_resume_tool.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai.tools import BaseTool
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import Type,Optional,Union
|
| 4 |
+
import re
|
| 5 |
+
import spacy
|
| 6 |
+
|
| 7 |
+
nlp = spacy.load("en_core_web_sm")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ResumeInput(BaseModel):
|
| 11 |
+
resume_data: Union[str, dict] = Field(..., description="Raw text of the resume or dictionary containing resume data")
|
| 12 |
+
job_description: Optional[Union[str, dict]] = Field(default=None, description="Job description text or dictionary containing job description")
|
| 13 |
+
|
| 14 |
+
class RewriteResumeTool(BaseTool):
|
| 15 |
+
name: str = "ATS Resume Rewriter"
|
| 16 |
+
description: str = (
|
| 17 |
+
"Analyzes and rewrites a resume to align with a job description by integrating missing keywords "
|
| 18 |
+
"into relevant sections while preserving formatting."
|
| 19 |
+
)
|
| 20 |
+
args_schema: Type[BaseModel] = ResumeInput
|
| 21 |
+
|
| 22 |
+
def _extract_keywords(self, jd_text: str) -> set:
|
| 23 |
+
doc = nlp(jd_text)
|
| 24 |
+
return {
|
| 25 |
+
token.lemma_.lower()
|
| 26 |
+
for token in doc
|
| 27 |
+
if token.pos_ in {"NOUN", "PROPN", "ADJ", "VERB"} and not token.is_stop
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def _check_missing_keywords(self, resume_text: str, jd_keywords: set) -> set:
|
| 31 |
+
resume_doc = nlp(resume_text.lower())
|
| 32 |
+
resume_tokens = {token.lemma_ for token in resume_doc if not token.is_stop}
|
| 33 |
+
return jd_keywords - resume_tokens
|
| 34 |
+
|
| 35 |
+
def _insert_keywords_contextually(self, resume: str, missing_keywords: set) -> str:
|
| 36 |
+
sections = self._split_sections(resume)
|
| 37 |
+
injected_keywords = set()
|
| 38 |
+
rewritten_sections = []
|
| 39 |
+
|
| 40 |
+
for header, content in sections:
|
| 41 |
+
section_lower = header.lower()
|
| 42 |
+
|
| 43 |
+
if section_lower in {"summary", "objective", "professional summary"}:
|
| 44 |
+
enriched = self._enrich_section(content, missing_keywords, injected_keywords)
|
| 45 |
+
elif section_lower in {"skills"}:
|
| 46 |
+
enriched = self._add_to_comma_list(content, missing_keywords, injected_keywords)
|
| 47 |
+
elif section_lower in {"experience", "work experience"}:
|
| 48 |
+
enriched = self._enrich_section(content, missing_keywords, injected_keywords)
|
| 49 |
+
else:
|
| 50 |
+
enriched = content
|
| 51 |
+
|
| 52 |
+
rewritten_sections.append(f"{header}\n{enriched}")
|
| 53 |
+
|
| 54 |
+
rewritten_resume = "\n\n".join(rewritten_sections)
|
| 55 |
+
return rewritten_resume.strip()
|
| 56 |
+
|
| 57 |
+
def _split_sections(self, resume_text: str) -> list:
|
| 58 |
+
# Match lines that look like section headers (title case, max ~40 chars)
|
| 59 |
+
pattern = re.compile(r"^(?:[A-Z][A-Za-z\s]{2,40})$", re.MULTILINE)
|
| 60 |
+
matches = list(pattern.finditer(resume_text))
|
| 61 |
+
sections = []
|
| 62 |
+
|
| 63 |
+
for i in range(len(matches)):
|
| 64 |
+
start = matches[i].end()
|
| 65 |
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(resume_text)
|
| 66 |
+
header = matches[i].group().strip()
|
| 67 |
+
content = resume_text[start:end].strip()
|
| 68 |
+
sections.append((header, content))
|
| 69 |
+
|
| 70 |
+
return sections or [("Resume", resume_text)]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _enrich_section(self, text: str, keywords: set, used_keywords: set) -> str:
|
| 74 |
+
doc = nlp(text)
|
| 75 |
+
sentences = [sent.text for sent in doc.sents]
|
| 76 |
+
enriched = []
|
| 77 |
+
|
| 78 |
+
for sentence in sentences:
|
| 79 |
+
enriched.append(sentence)
|
| 80 |
+
|
| 81 |
+
# Add new sentence if keyword is not used
|
| 82 |
+
new_sentences = []
|
| 83 |
+
for keyword in keywords - used_keywords:
|
| 84 |
+
new_sentences.append(f"Demonstrates strong proficiency in {keyword}.")
|
| 85 |
+
used_keywords.add(keyword)
|
| 86 |
+
|
| 87 |
+
if new_sentences:
|
| 88 |
+
enriched.append(" ".join(new_sentences))
|
| 89 |
+
|
| 90 |
+
return " ".join(enriched)
|
| 91 |
+
|
| 92 |
+
def _add_to_comma_list(self, text: str, keywords: set, used_keywords: set) -> str:
|
| 93 |
+
skills = [s.strip() for s in re.split(r"[,\n]", text) if s.strip()]
|
| 94 |
+
for kw in keywords:
|
| 95 |
+
if kw not in [s.lower() for s in skills]:
|
| 96 |
+
skills.append(kw)
|
| 97 |
+
used_keywords.add(kw)
|
| 98 |
+
return ", ".join(skills)
|
| 99 |
+
|
| 100 |
+
def _run(self, resume_data: Union[str, dict], job_description: Optional[Union[str, dict]] = None) -> dict:
|
| 101 |
+
try:
|
| 102 |
+
# Extract resume text
|
| 103 |
+
resume_text = resume_data.get('description') if isinstance(resume_data, dict) else resume_data
|
| 104 |
+
|
| 105 |
+
if not resume_text or not isinstance(resume_text, str):
|
| 106 |
+
return {"resume_data": "", "feedback": "❌ Error: Invalid resume data format"}
|
| 107 |
+
|
| 108 |
+
# Extract job description text
|
| 109 |
+
jd_text = None
|
| 110 |
+
if job_description:
|
| 111 |
+
jd_text = job_description.get('description') if isinstance(job_description, dict) else job_description
|
| 112 |
+
if not jd_text or not isinstance(jd_text, str):
|
| 113 |
+
return {"resume_data": "", "feedback": "❌ Error: Invalid job description format"}
|
| 114 |
+
|
| 115 |
+
# If job description is provided
|
| 116 |
+
if jd_text:
|
| 117 |
+
jd_keywords = self._extract_keywords(jd_text)
|
| 118 |
+
missing_keywords = self._check_missing_keywords(resume_text, jd_keywords)
|
| 119 |
+
rewritten_resume = self._insert_keywords_contextually(resume_text, missing_keywords)
|
| 120 |
+
|
| 121 |
+
feedback = (
|
| 122 |
+
f"✅ Resume rewritten based on provided job description.\n"
|
| 123 |
+
f"📝 Missing Keywords Identified: {', '.join(sorted(missing_keywords)) or 'None'}\n"
|
| 124 |
+
f"🔧 Keywords were integrated contextually into relevant sections (e.g., Summary, Skills, Experience).\n"
|
| 125 |
+
f"🎯 ATS Optimization Complete."
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
else:
|
| 129 |
+
# Fallback to general enrichment (for now, return original)
|
| 130 |
+
rewritten_resume = resume_text # You can optionally enhance for generic ATS optimization.
|
| 131 |
+
feedback = (
|
| 132 |
+
"ℹ️ No job description provided. Resume kept unchanged.\n"
|
| 133 |
+
"Consider tailoring your resume for each role to improve ATS compatibility."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"resume_data": rewritten_resume,
|
| 138 |
+
"feedback": feedback
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
return {
|
| 143 |
+
"resume_data": "",
|
| 144 |
+
"feedback": f"❌ Error preparing resume data: {str(e)}"
|
| 145 |
+
}
|
src/ats/crews/web_scraper_crew/config/agents.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
web_scraper_agent:
|
| 2 |
+
role: >
|
| 3 |
+
Web Scraper
|
| 4 |
+
goal: >
|
| 5 |
+
Use the ScrapeWebsiteTool to extract the full job description from the given URL.
|
| 6 |
+
backstory: >
|
| 7 |
+
A web scraping expert capable of extracting targeted web content using specialized tools.
|
| 8 |
+
You are provided with a URL and expected to return relevant, readable job description content.
|
| 9 |
+
llm: openai/gpt-4o-mini
|
| 10 |
+
instructions: >
|
| 11 |
+
You must use the FirecrawlScrapeWebsiteTool provided to you to scrape the content of the job description from the URL.
|
| 12 |
+
Do not attempt to generate or guess the content — only use the tool's output. Your task is done when the job description content is extracted and clearly presented.
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
src/ats/crews/web_scraper_crew/config/tasks.yaml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
web_scraper_task:
|
| 2 |
+
description: >
|
| 3 |
+
Extract the job description from the given URL: {job_description}
|
| 4 |
+
|
| 5 |
+
Use ONLY the tool named `scrape_website`.
|
| 6 |
+
|
| 7 |
+
Format your response exactly like this:
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
Thought: I need to extract content from the URL
|
| 11 |
+
Action: scrape_website
|
| 12 |
+
Action Input: {"url": "{job_description}"}
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
Wait for the tool to complete.
|
| 16 |
+
|
| 17 |
+
Then return:
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
Thought: I now can give a great answer
|
| 21 |
+
Final Answer: <the full extracted text>
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Do not call any tool not listed. Only one action at a time. Do not invent the answer.
|
| 25 |
+
|
| 26 |
+
expected_output: >
|
| 27 |
+
The extracted job description as plain text.
|
| 28 |
+
agent: web_scraper_agent
|
src/ats/crews/web_scraper_crew/web_scraper_crew.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai import Agent, Crew, Process, Task
|
| 2 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 3 |
+
#from src.ats.crews.web_scraper_crew.tools.webscraper_tool import WebscraperTool
|
| 4 |
+
from crewai_tools import ScrapeWebsiteTool
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@CrewBase
|
| 9 |
+
class WebScraperCrew:
|
| 10 |
+
"""Lead Filter Crew"""
|
| 11 |
+
|
| 12 |
+
agents_config = "config/agents.yaml"
|
| 13 |
+
tasks_config = "config/tasks.yaml"
|
| 14 |
+
|
| 15 |
+
@agent
|
| 16 |
+
def web_scraper_agent(self) -> Agent:
|
| 17 |
+
return Agent(
|
| 18 |
+
config=self.agents_config["web_scraper_agent"],
|
| 19 |
+
tools=[ScrapeWebsiteTool(
|
| 20 |
+
args={
|
| 21 |
+
'pageOptions': {
|
| 22 |
+
'onlyMainContent': True,
|
| 23 |
+
'includeHtml': False
|
| 24 |
+
},
|
| 25 |
+
'timeout': 60000 # 60 seconds
|
| 26 |
+
}
|
| 27 |
+
)],
|
| 28 |
+
verbose=True,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
@task
|
| 32 |
+
def web_scraper_task(self) -> Task:
|
| 33 |
+
return Task(
|
| 34 |
+
config=self.tasks_config["web_scraper_task"],
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
@crew
|
| 38 |
+
def crew(self) -> Crew:
|
| 39 |
+
"""Creates the Web Scraper Crew"""
|
| 40 |
+
return Crew(
|
| 41 |
+
agents=self.agents,
|
| 42 |
+
tasks=self.tasks,
|
| 43 |
+
process=Process.sequential,
|
| 44 |
+
verbose=True,
|
| 45 |
+
)
|
src/ats/email_responses/Amelia Cole.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Subject: Next Steps in Your Application Process
|
| 2 |
+
|
| 3 |
+
Dear Amelia,
|
| 4 |
+
|
| 5 |
+
I hope this message finds you well! Thank you for your application for the Senior Frontend Developer position at XYZ Technologies Pvt Ltd.
|
| 6 |
+
|
| 7 |
+
After reviewing your impressive resume, I am pleased to inform you that we would like to proceed with your application. Your expertise in React.js and Next.js, along with your quantifiable achievement of improving performance by 30% in your previous roles, strongly aligns with what we are seeking. We believe that your skills and potential will be a valuable addition to our team.
|
| 8 |
+
|
| 9 |
+
To discuss this opportunity further, could you please provide your availability for a Zoom call in the upcoming days? We would love to connect and learn more about your experiences.
|
| 10 |
+
|
| 11 |
+
Looking forward to hearing from you soon!
|
| 12 |
+
|
| 13 |
+
Best regards,
|
| 14 |
+
Sunitha L V
|
| 15 |
+
HR Coordinator
|
| 16 |
+
XYZ Technologies Pvt Ltd
|
| 17 |
+
[Your Email Address]
|
| 18 |
+
[Your Phone Number]
|
src/ats/email_responses/Jacob Reed.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Subject: Next Steps in Your Application for Junior React Developer Position
|
| 2 |
+
|
| 3 |
+
Dear Jacob,
|
| 4 |
+
|
| 5 |
+
I hope this message finds you well.
|
| 6 |
+
|
| 7 |
+
I am pleased to inform you that we were very impressed with your application for the Junior React Developer position at XYZ Technologies Pvt Ltd. Your three years of experience in frontend development, particularly with React.js and Next.js, truly exceeds the requirements we were looking for. Additionally, your track record at PixelSoft Labs in optimizing application performance and collaborating with UI/UX teams aligns perfectly with our expectations for this role.
|
| 8 |
+
|
| 9 |
+
We also appreciate your involvement in mentoring junior developers and your participation in code reviews, which indicates a great cultural fit with our collaborative work environment.
|
| 10 |
+
|
| 11 |
+
We would love to move forward with your application and discuss this opportunity further. Could you please provide your availability for a Zoom call in the upcoming days?
|
| 12 |
+
|
| 13 |
+
Looking forward to hearing from you soon.
|
| 14 |
+
|
| 15 |
+
Best regards,
|
| 16 |
+
|
| 17 |
+
Sunitha L V
|
| 18 |
+
HR Coordinator
|
| 19 |
+
XYZ Technologies Pvt Ltd
|
| 20 |
+
[Your Contact Information]
|
src/ats/email_responses/John Carter.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Subject: Thank You for Your Application at XYZ Technologies
|
| 2 |
+
|
| 3 |
+
Dear John,
|
| 4 |
+
|
| 5 |
+
Thank you for taking the time to apply for the Junior React Developer position at XYZ Technologies. We appreciate the effort you put into your application and your keen interest in joining our team.
|
| 6 |
+
|
| 7 |
+
After careful consideration, we have decided to move forward with candidates who more closely meet the experience requirements for this role. While we were impressed by your foundational knowledge and passion for front-end development, the absence of practical experience, particularly in collaborating on team projects and using key frameworks, was a determining factor in our decision.
|
| 8 |
+
|
| 9 |
+
We encourage you to continue developing your skills and gaining experience in the field. Please feel free to apply for future opportunities that align with your qualifications, as we recognize the potential in your journey.
|
| 10 |
+
|
| 11 |
+
Thank you once again for your interest in XYZ Technologies. We wish you the best of luck in your job search and future endeavors.
|
| 12 |
+
|
| 13 |
+
Warm regards,
|
| 14 |
+
Sunitha L V
|
| 15 |
+
HR Coordinator
|
| 16 |
+
XYZ Technologies Pvt Ltd
|
| 17 |
+
[Your Contact Information]
|
src/ats/email_responses/Nora Blake.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Subject: Thank You for Your Application at XYZ Technologies
|
| 2 |
+
|
| 3 |
+
Dear Nora,
|
| 4 |
+
|
| 5 |
+
I hope this message finds you well.
|
| 6 |
+
|
| 7 |
+
Thank you for taking the time to apply for the Junior React Developer position at XYZ Technologies Pvt Ltd. We sincerely appreciate the effort you put into your application and your interest in joining our team.
|
| 8 |
+
|
| 9 |
+
After careful consideration, we have decided to pursue other candidates for this particular role. While your strong background as a Frontend Developer with 6 years of experience is impressive, we feel that your extensive skill set may align better with more senior positions rather than the junior level we are currently looking to fill.
|
| 10 |
+
|
| 11 |
+
Please know that this decision was not easy and reflects the specific criteria we have set for this role. We genuinely appreciate your time and effort in applying, and we encourage you to explore further opportunities with us in the future that may be a better fit for your skills and experience.
|
| 12 |
+
|
| 13 |
+
Thank you once again for your interest in XYZ Technologies. We wish you all the best in your job search and future endeavors.
|
| 14 |
+
|
| 15 |
+
Warm regards,
|
| 16 |
+
|
| 17 |
+
Sunitha L V
|
| 18 |
+
HR Coordinator
|
| 19 |
+
XYZ Technologies Pvt Ltd
|
| 20 |
+
[Your Contact Information]
|
src/ats/email_responses/Owen Mitchell.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Subject: Your Application at XYZ Technologies Pvt Ltd
|
| 2 |
+
|
| 3 |
+
Dear Owen,
|
| 4 |
+
|
| 5 |
+
Thank you very much for your application for the Junior React Developer position at XYZ Technologies Pvt Ltd. We truly appreciate the time and effort you put into your application and the impressive experience you have as a Python Developer.
|
| 6 |
+
|
| 7 |
+
After careful consideration, we regret to inform you that we will not be moving forward with your application for this role. While your background in backend development is commendable, we are specifically seeking candidates with more extensive experience in front-end technologies such as React, Next.js, TypeScript, CSS, and HTML, as well as familiarity with Vercel AI SDK.
|
| 8 |
+
|
| 9 |
+
We value your interest in our company, and we encourage you to keep an eye on future openings that may align more closely with your skill set. We will retain your resume on file for any suitable positions that may arise.
|
| 10 |
+
|
| 11 |
+
Thank you again for considering a career with us. We wish you all the best in your job search and future professional endeavors.
|
| 12 |
+
|
| 13 |
+
Warm regards,
|
| 14 |
+
|
| 15 |
+
Sunitha L V
|
| 16 |
+
HR Coordinator
|
| 17 |
+
XYZ Technologies Pvt Ltd
|
| 18 |
+
[Your Contact Information]
|
src/ats/email_responses/Sophia Williams.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Subject: Next Steps in Your Application Process
|
| 2 |
+
|
| 3 |
+
Dear Sophia,
|
| 4 |
+
|
| 5 |
+
I hope this message finds you well! I want to take a moment to express our appreciation for your interest in the position at XYZ Technologies Pvt Ltd and for the time you invested in the application process.
|
| 6 |
+
|
| 7 |
+
After reviewing your profile, I am pleased to inform you that we were impressed by your strong skill set in React/Next.js development, along with your valuable experience at TechNova Solutions and PixelSoft Labs. Your adaptability, capacity for growth, and openness to exploring emerging technologies are characteristics we highly value at our company.
|
| 8 |
+
|
| 9 |
+
We would love to discuss the next steps in the hiring process with you. Could you please share your availability for a Zoom call within the next few days? We look forward to the opportunity to connect and learn more about you and how you can contribute to our team.
|
| 10 |
+
|
| 11 |
+
Thank you once again for your interest in joining XYZ Technologies Pvt Ltd. We are excited about the possibility of working together!
|
| 12 |
+
|
| 13 |
+
Best regards,
|
| 14 |
+
Sunitha L V
|
| 15 |
+
HR Coordinator
|
| 16 |
+
XYZ Technologies Pvt Ltd
|
| 17 |
+
[Contact Information]
|
src/ats/main.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
import asyncio
|
| 3 |
+
from typing import List,Dict
|
| 4 |
+
|
| 5 |
+
from crewai.flow.flow import Flow, listen, or_, router, start
|
| 6 |
+
from pydantic import BaseModel,Field
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from src.ats.crews.lead_response_crew.lead_response_crew import LeadResponseCrew
|
| 9 |
+
from src.ats.crews.lead_score_crew.lead_score_crew import LeadScoreCrew
|
| 10 |
+
from src.ats.crews.lead_filter_crew.lead_filter_crew import LeadFilterCrew
|
| 11 |
+
from src.ats.crews.web_scraper_crew.web_scraper_crew import WebScraperCrew
|
| 12 |
+
from src.ats.crews.resume_parser_crew.resume_parser_crew import ResumeParserCrew
|
| 13 |
+
from src.ats.crews.resume_score_crew.resume_score_crew import ResumeScoreCrew
|
| 14 |
+
from src.ats.crews.rewrite_resume_crew.rewrite_resume_crew import RewriteResumeCrew
|
| 15 |
+
from src.ats.types import Candidate, CandidateScore, ScoredCandidate,CandidateFilter,ResumeData,Resume_Final
|
| 16 |
+
from src.ats.utils.candidateUtils import combine_candidates_with_scores,extract_candidate_info,get_resume_text,send_email
|
| 17 |
+
import csv
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class LeadScoreState(BaseModel):
|
| 21 |
+
jd:str=""
|
| 22 |
+
candidate_resumes:List[Dict] = []
|
| 23 |
+
candidates: List[Candidate] = []
|
| 24 |
+
failed_candidates: List[CandidateFilter] = []
|
| 25 |
+
candidate_score: List[CandidateScore] = []
|
| 26 |
+
candidate_filters:List[CandidateFilter] = []
|
| 27 |
+
hydrated_candidates: List[ScoredCandidate] = []
|
| 28 |
+
top_candidates: List[ScoredCandidate] = []
|
| 29 |
+
scored_leads_feedback: str = ""
|
| 30 |
+
|
| 31 |
+
class CandidateScoreState(BaseModel):
|
| 32 |
+
jd:str=""
|
| 33 |
+
file_path:str = ""
|
| 34 |
+
resume_data:ResumeData | None = None
|
| 35 |
+
candidate_score: CandidateScore | None = None
|
| 36 |
+
|
| 37 |
+
class ImproveResumeState(BaseModel):
|
| 38 |
+
jd:str=""
|
| 39 |
+
resume_data:str = ""
|
| 40 |
+
initial_score:CandidateScore | None = None
|
| 41 |
+
improved_resume: Resume_Final | None = None
|
| 42 |
+
is_rewrite:bool=False
|
| 43 |
+
rewrite_count:int=0
|
| 44 |
+
rewrite_score:CandidateScore | None = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
#Employer flow
|
| 48 |
+
class LeadScoreFlow(Flow[LeadScoreState]):
|
| 49 |
+
@start()
|
| 50 |
+
def load_leads(self):
|
| 51 |
+
id=0
|
| 52 |
+
candidates=[]
|
| 53 |
+
for resume_file in self.state.candidate_resumes:
|
| 54 |
+
# Step 1: Extract structured candidate info
|
| 55 |
+
id+=1
|
| 56 |
+
candidate_info = extract_candidate_info(resume_file["content"],resume_file["id"])
|
| 57 |
+
candidates.append(candidate_info)
|
| 58 |
+
with open("candidates_info.csv", "w", newline="") as f:
|
| 59 |
+
writer = csv.writer(f)
|
| 60 |
+
writer.writerow(["id", "name", "email", "bio","years_of_exp","skills"])
|
| 61 |
+
for candidate in candidates:
|
| 62 |
+
writer.writerow(
|
| 63 |
+
[
|
| 64 |
+
candidate.id,
|
| 65 |
+
candidate.name,
|
| 66 |
+
candidate.email,
|
| 67 |
+
candidate.bio,
|
| 68 |
+
candidate.years_of_exp,
|
| 69 |
+
candidate.skills
|
| 70 |
+
]
|
| 71 |
+
)
|
| 72 |
+
#print("Candidates info saved to candidates_info.csv")
|
| 73 |
+
# Update the state with the loaded candidates
|
| 74 |
+
self.state.candidates = candidates
|
| 75 |
+
|
| 76 |
+
@listen(load_leads)
|
| 77 |
+
async def filter_leads(self):
|
| 78 |
+
#print("First level filtering of leads")
|
| 79 |
+
tasks = []
|
| 80 |
+
|
| 81 |
+
async def filter_candidate(candidate: Candidate):
|
| 82 |
+
result = await (
|
| 83 |
+
LeadFilterCrew()
|
| 84 |
+
.crew()
|
| 85 |
+
.kickoff_async(
|
| 86 |
+
inputs={
|
| 87 |
+
"candidate_id": candidate.id,
|
| 88 |
+
"name": candidate.name,
|
| 89 |
+
"bio": candidate.bio,
|
| 90 |
+
"years_of_exp": candidate.years_of_exp,
|
| 91 |
+
"skills": candidate.skills,
|
| 92 |
+
"job_description": self.state.jd,
|
| 93 |
+
}
|
| 94 |
+
)
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
self.state.candidate_filters.append(result.pydantic)
|
| 98 |
+
|
| 99 |
+
for candidate in self.state.candidates:
|
| 100 |
+
#print("Scoring candidate:", candidate.name)
|
| 101 |
+
task = asyncio.create_task(filter_candidate(candidate))
|
| 102 |
+
tasks.append(task)
|
| 103 |
+
|
| 104 |
+
candidate_filters = await asyncio.gather(*tasks)
|
| 105 |
+
#print("Finished filtering leads: ", len(candidate_filters))
|
| 106 |
+
with open("filtered_candidates.csv", "w", newline="") as f:
|
| 107 |
+
writer = csv.writer(f)
|
| 108 |
+
writer.writerow(["id", "result", "reason"])
|
| 109 |
+
for candidate in self.state.candidate_filters:
|
| 110 |
+
writer.writerow(
|
| 111 |
+
[
|
| 112 |
+
candidate.id,
|
| 113 |
+
candidate.result,
|
| 114 |
+
candidate.reason,
|
| 115 |
+
]
|
| 116 |
+
)
|
| 117 |
+
#print("Filtered Candidates info saved to filtered_candidates.csv")
|
| 118 |
+
|
| 119 |
+
#Filter failed candidates as a seperate list
|
| 120 |
+
self.state.failed_candidates = [
|
| 121 |
+
cand
|
| 122 |
+
for cand in self.state.candidate_filters
|
| 123 |
+
if cand.result == "Fail"
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
# Create a lookup dictionary from candidates using their ID
|
| 127 |
+
id_to_email = {candidate.id: candidate.email for candidate in self.state.candidates}
|
| 128 |
+
# Set the email in failed_candidates by matching ID for email sending purpose
|
| 129 |
+
for candidate in self.state.failed_candidates:
|
| 130 |
+
candidate_id = candidate.id
|
| 131 |
+
candidate.email = id_to_email.get(candidate_id)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
#set of passed IDs
|
| 135 |
+
passed_ids = {
|
| 136 |
+
cf.id
|
| 137 |
+
for cf in self.state.candidate_filters
|
| 138 |
+
if cf.result == "Pass"
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#Filter candidates list based on passed ids
|
| 142 |
+
self.state.candidates = [
|
| 143 |
+
cand
|
| 144 |
+
for cand in self.state.candidates
|
| 145 |
+
if cand.id in passed_ids
|
| 146 |
+
]
|
| 147 |
+
|
| 148 |
+
@listen(or_(filter_leads, "scored_leads_feedback"))
|
| 149 |
+
async def score_leads(self):
|
| 150 |
+
#print("Scoring leads")
|
| 151 |
+
#Create a lookup dictionary from resumes
|
| 152 |
+
resume_lookup = {resume["id"]: resume["content"] for resume in self.state.candidate_resumes}
|
| 153 |
+
# Update each candidate's bio using the lookup
|
| 154 |
+
for candidate in self.state.candidates:
|
| 155 |
+
if candidate.id in resume_lookup:
|
| 156 |
+
candidate.bio = resume_lookup[candidate.id]
|
| 157 |
+
|
| 158 |
+
tasks = []
|
| 159 |
+
|
| 160 |
+
async def score_single_candidate(candidate: Candidate):
|
| 161 |
+
result = await (
|
| 162 |
+
LeadScoreCrew()
|
| 163 |
+
.crew()
|
| 164 |
+
.kickoff_async(
|
| 165 |
+
inputs={
|
| 166 |
+
"candidate_id": candidate.id,
|
| 167 |
+
"name": candidate.name,
|
| 168 |
+
"bio": candidate.bio,
|
| 169 |
+
"job_description": self.state.jd,
|
| 170 |
+
"additional_instructions": self.state.scored_leads_feedback,
|
| 171 |
+
}
|
| 172 |
+
)
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
self.state.candidate_score.append(result.pydantic)
|
| 176 |
+
|
| 177 |
+
for candidate in self.state.candidates:
|
| 178 |
+
#print("Scoring candidate:", candidate.name)
|
| 179 |
+
task = asyncio.create_task(score_single_candidate(candidate))
|
| 180 |
+
tasks.append(task)
|
| 181 |
+
|
| 182 |
+
candidate_scores = await asyncio.gather(*tasks)
|
| 183 |
+
#print("Finished scoring leads")
|
| 184 |
+
with open("scored_candidates.csv", "w", newline="") as f:
|
| 185 |
+
writer = csv.writer(f)
|
| 186 |
+
writer.writerow(["id", "score", "reason"])
|
| 187 |
+
for candidate in self.state.candidate_score:
|
| 188 |
+
writer.writerow(
|
| 189 |
+
[
|
| 190 |
+
candidate.id,
|
| 191 |
+
candidate.score,
|
| 192 |
+
candidate.reason,
|
| 193 |
+
]
|
| 194 |
+
)
|
| 195 |
+
#print("Scored Candidates info saved to scored_candidates.csv")
|
| 196 |
+
|
| 197 |
+
@router(score_leads)
|
| 198 |
+
def human_in_the_loop(self):
|
| 199 |
+
#print("Finding the top 3 candidates for human to review")
|
| 200 |
+
|
| 201 |
+
# Combine candidates with their scores using the helper function
|
| 202 |
+
self.state.hydrated_candidates = combine_candidates_with_scores(
|
| 203 |
+
self.state.candidates, self.state.candidate_score
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# Sort the scored candidates by their score in descending order
|
| 207 |
+
sorted_candidates = sorted(
|
| 208 |
+
self.state.hydrated_candidates, key=lambda c: c.score, reverse=True
|
| 209 |
+
)
|
| 210 |
+
self.state.hydrated_candidates = sorted_candidates
|
| 211 |
+
|
| 212 |
+
# Select the top 3 candidates
|
| 213 |
+
self.state.top_candidates = sorted_candidates[:3]
|
| 214 |
+
|
| 215 |
+
# Present options to the user
|
| 216 |
+
# print("\nPlease choose an option:")
|
| 217 |
+
# print("1. Quit")
|
| 218 |
+
# print("2. Redo lead scoring with additional feedback")
|
| 219 |
+
# print("3. Proceed with writing emails to all leads")
|
| 220 |
+
|
| 221 |
+
#Commenting for execution without interruption
|
| 222 |
+
#choice = input("Enter the number of your choice: ")
|
| 223 |
+
choice="3"
|
| 224 |
+
|
| 225 |
+
if choice == "1":
|
| 226 |
+
#print("Exiting the program.")
|
| 227 |
+
exit()
|
| 228 |
+
elif choice == "2":
|
| 229 |
+
feedback = input(
|
| 230 |
+
"\nPlease provide additional feedback on what you're looking for in candidates:\n"
|
| 231 |
+
)
|
| 232 |
+
self.state.scored_leads_feedback = feedback
|
| 233 |
+
#print("\nRe-running lead scoring with your feedback...")
|
| 234 |
+
return "scored_leads_feedback"
|
| 235 |
+
elif choice == "3":
|
| 236 |
+
#print("\nProceeding to write emails to all leads.")
|
| 237 |
+
return "generate_emails"
|
| 238 |
+
else:
|
| 239 |
+
#print("\nInvalid choice. Please try again.")
|
| 240 |
+
return "human_in_the_loop"
|
| 241 |
+
|
| 242 |
+
@listen("generate_emails")
|
| 243 |
+
async def write_and_save_emails(self):
|
| 244 |
+
import re
|
| 245 |
+
from pathlib import Path
|
| 246 |
+
|
| 247 |
+
#print("Writing and saving emails for all leads.")
|
| 248 |
+
|
| 249 |
+
# Determine the top 3 candidates to proceed with
|
| 250 |
+
top_candidate_ids = {
|
| 251 |
+
candidate.id for candidate in self.state.hydrated_candidates[:3]
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
tasks = []
|
| 255 |
+
|
| 256 |
+
# Create the directory 'email_responses' if it doesn't exist
|
| 257 |
+
output_dir = Path(__file__).parent / "email_responses"
|
| 258 |
+
#print("output_dir:", output_dir)
|
| 259 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 260 |
+
|
| 261 |
+
async def write_email(candidate):
|
| 262 |
+
# Check if the candidate is among the top 3
|
| 263 |
+
proceed_with_candidate = candidate.id in top_candidate_ids
|
| 264 |
+
|
| 265 |
+
# Kick off the LeadResponseCrew for each candidate
|
| 266 |
+
result = await (
|
| 267 |
+
LeadResponseCrew()
|
| 268 |
+
.crew()
|
| 269 |
+
.kickoff_async(
|
| 270 |
+
inputs={
|
| 271 |
+
"candidate_id": candidate.id,
|
| 272 |
+
"name": candidate.name,
|
| 273 |
+
"reason": candidate.reason,
|
| 274 |
+
"proceed_with_candidate": proceed_with_candidate,
|
| 275 |
+
}
|
| 276 |
+
)
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# Sanitize the candidate's name to create a valid filename
|
| 280 |
+
safe_name = re.sub(r"[^a-zA-Z0-9_\- ]", "", candidate.name)
|
| 281 |
+
filename = f"{safe_name}.txt"
|
| 282 |
+
#print("Filename:", filename)
|
| 283 |
+
|
| 284 |
+
# Write the email content to a text file
|
| 285 |
+
file_path = output_dir / filename
|
| 286 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 287 |
+
f.write(result.raw)
|
| 288 |
+
|
| 289 |
+
#Send the corresponding email to each candidate
|
| 290 |
+
send_email(file_path,candidate.email)
|
| 291 |
+
|
| 292 |
+
# Return a message indicating the email was saved
|
| 293 |
+
return f"Email sent for {candidate.name} as {filename} to {candidate.email}"
|
| 294 |
+
|
| 295 |
+
#Create a composite list for all candidates
|
| 296 |
+
candidate_list = self.state.hydrated_candidates + self.state.failed_candidates
|
| 297 |
+
|
| 298 |
+
# Create tasks for all candidates
|
| 299 |
+
for candidate in candidate_list:
|
| 300 |
+
task = asyncio.create_task(write_email(candidate))
|
| 301 |
+
tasks.append(task)
|
| 302 |
+
|
| 303 |
+
# Run all email-writing tasks concurrently and collect results
|
| 304 |
+
email_results = await asyncio.gather(*tasks)
|
| 305 |
+
|
| 306 |
+
# After all emails have been generated and saved
|
| 307 |
+
#print("\nAll emails have been written and saved to 'email_responses' folder.")
|
| 308 |
+
# for message in email_results:
|
| 309 |
+
# print(message)
|
| 310 |
+
def reset(self):
|
| 311 |
+
self.agents = []
|
| 312 |
+
self.tasks = []
|
| 313 |
+
self.memory = None
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
# Candidate flow
|
| 317 |
+
class CandidateScoreFlow(Flow[CandidateScoreState]):
|
| 318 |
+
@start()
|
| 319 |
+
def extract_job_descrpn(self):
|
| 320 |
+
result = WebScraperCrew().crew().kickoff(
|
| 321 |
+
inputs={
|
| 322 |
+
"job_description": self.state.jd,
|
| 323 |
+
}
|
| 324 |
+
)
|
| 325 |
+
# Extract the actual string
|
| 326 |
+
job_description = str(result)
|
| 327 |
+
#print("Extracted website content:", job_description)
|
| 328 |
+
#print(self.state.file_path)
|
| 329 |
+
|
| 330 |
+
# Save result to state
|
| 331 |
+
self.state.jd = job_description
|
| 332 |
+
|
| 333 |
+
@listen(extract_job_descrpn)
|
| 334 |
+
def parse_resume(self):
|
| 335 |
+
#Extract data from resume
|
| 336 |
+
result =ResumeParserCrew().crew().kickoff(
|
| 337 |
+
inputs={
|
| 338 |
+
"file_path": self.state.file_path
|
| 339 |
+
}
|
| 340 |
+
)
|
| 341 |
+
self.state.resume_data=result.pydantic
|
| 342 |
+
#print(self.state.resume_data)
|
| 343 |
+
|
| 344 |
+
@listen(parse_resume)
|
| 345 |
+
def score_resume(self):
|
| 346 |
+
result = ResumeScoreCrew().crew().kickoff(
|
| 347 |
+
inputs={
|
| 348 |
+
"name": self.state.resume_data.name,
|
| 349 |
+
"email": self.state.resume_data.email,
|
| 350 |
+
"mobile_number": self.state.resume_data.mobile_number,
|
| 351 |
+
"skills": self.state.resume_data.skills,
|
| 352 |
+
"education": self.state.resume_data.education,
|
| 353 |
+
"objective": self.state.resume_data.objective,
|
| 354 |
+
"experience_years": self.state.resume_data.experience_years,
|
| 355 |
+
"experience_details": self.state.resume_data.experience_details,
|
| 356 |
+
"projects": self.state.resume_data.projects,
|
| 357 |
+
"certifications": self.state.resume_data.certifications,
|
| 358 |
+
"linkedin": self.state.resume_data.linkedin,
|
| 359 |
+
"github": self.state.resume_data.github,
|
| 360 |
+
"job_description": self.state.jd,
|
| 361 |
+
}
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
self.state.candidate_score = result.pydantic
|
| 366 |
+
def reset(self):
|
| 367 |
+
self.agents = []
|
| 368 |
+
self.tasks = []
|
| 369 |
+
self.memory = None
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
# Improve resume flow
|
| 373 |
+
class ImproveResumeFlow(Flow[ImproveResumeState]):
|
| 374 |
+
@start()
|
| 375 |
+
def extract_job_descrpn(self):
|
| 376 |
+
if self.state.jd:
|
| 377 |
+
result = WebScraperCrew().crew().kickoff(
|
| 378 |
+
inputs={
|
| 379 |
+
"job_description": self.state.jd,
|
| 380 |
+
}
|
| 381 |
+
)
|
| 382 |
+
# Extract the actual string
|
| 383 |
+
job_description = str(result)
|
| 384 |
+
else:
|
| 385 |
+
job_description=""
|
| 386 |
+
#print("Extracted website content:", job_description)
|
| 387 |
+
|
| 388 |
+
# Save result to state
|
| 389 |
+
self.state.jd = job_description
|
| 390 |
+
|
| 391 |
+
@listen(or_("extract_job_descrpn", "rewrite_resume"))
|
| 392 |
+
def score_resume(self):
|
| 393 |
+
if self.state.is_rewrite:
|
| 394 |
+
resume_data=self.state.improved_resume.resume_data
|
| 395 |
+
else:
|
| 396 |
+
resume_data=self.state.resume_data
|
| 397 |
+
|
| 398 |
+
result = LeadScoreCrew().crew().kickoff(
|
| 399 |
+
inputs={
|
| 400 |
+
"candidate_id": "1",
|
| 401 |
+
"name": "",
|
| 402 |
+
"bio": resume_data,
|
| 403 |
+
"job_description": self.state.jd,
|
| 404 |
+
"additional_instructions": "",
|
| 405 |
+
}
|
| 406 |
+
)
|
| 407 |
+
if self.state.is_rewrite:
|
| 408 |
+
self.state.rewrite_score= result.pydantic
|
| 409 |
+
self.state.improved_resume.score=self.state.rewrite_score.score
|
| 410 |
+
#print("REWRITE SCORE IS ",self.state.improved_resume.score)
|
| 411 |
+
else:
|
| 412 |
+
self.state.initial_score= result.pydantic
|
| 413 |
+
#print("INITIAL SCORE IS ",self.state.initial_score.score)
|
| 414 |
+
@router("score_resume")
|
| 415 |
+
def rewrite_condition_check(self):
|
| 416 |
+
if self.state.is_rewrite:
|
| 417 |
+
resume_score=self.state.rewrite_score.score
|
| 418 |
+
else:
|
| 419 |
+
resume_score=self.state.initial_score.score
|
| 420 |
+
#print("REWRITE COUNT ",self.state.rewrite_count)
|
| 421 |
+
if int(resume_score) < 85 and self.state.rewrite_count<=2:
|
| 422 |
+
return "improve_resume"
|
| 423 |
+
|
| 424 |
+
@listen("improve_resume")
|
| 425 |
+
def rewrite_resume(self):
|
| 426 |
+
#Rewrite resume
|
| 427 |
+
#print("IN REWRITE RESUME job description is : ",self.state.jd)
|
| 428 |
+
result =RewriteResumeCrew().crew().kickoff(
|
| 429 |
+
inputs={
|
| 430 |
+
"resume_data": self.state.resume_data,
|
| 431 |
+
"job_description": self.state.jd,
|
| 432 |
+
}
|
| 433 |
+
)
|
| 434 |
+
self.state.improved_resume=result.pydantic
|
| 435 |
+
self.state.is_rewrite=True
|
| 436 |
+
self.state.rewrite_count+=1
|
| 437 |
+
|
| 438 |
+
def reset(self):
|
| 439 |
+
self.agents = []
|
| 440 |
+
self.tasks = []
|
| 441 |
+
self.memory = None
|
| 442 |
+
|
| 443 |
+
def employer_kickoff(jd,candidate_resumes):
|
| 444 |
+
"""
|
| 445 |
+
Run the flow.
|
| 446 |
+
"""
|
| 447 |
+
lead_score_flow = LeadScoreFlow()
|
| 448 |
+
lead_score_flow.reset()
|
| 449 |
+
lead_score_flow.kickoff(inputs={"jd":jd,"candidate_resumes":candidate_resumes})
|
| 450 |
+
plot()
|
| 451 |
+
return lead_score_flow
|
| 452 |
+
|
| 453 |
+
def candidate_kickoff(jd,file_path):
|
| 454 |
+
"""
|
| 455 |
+
Run the flow.
|
| 456 |
+
"""
|
| 457 |
+
cand_score_flow = CandidateScoreFlow()
|
| 458 |
+
cand_score_flow.reset()
|
| 459 |
+
cand_score_flow.kickoff(inputs={"jd":jd,"file_path":file_path})
|
| 460 |
+
cand_plot()
|
| 461 |
+
return cand_score_flow
|
| 462 |
+
|
| 463 |
+
def improve_resume_for_ats(resume_data,jd):
|
| 464 |
+
"""
|
| 465 |
+
Run the flow.
|
| 466 |
+
"""
|
| 467 |
+
improve_resume_flow = ImproveResumeFlow()
|
| 468 |
+
improve_resume_flow.reset()
|
| 469 |
+
improve_resume_flow.kickoff(inputs={"jd":jd,"resume_data":resume_data})
|
| 470 |
+
improve_resume_plot()
|
| 471 |
+
return improve_resume_flow
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def plot():
|
| 475 |
+
"""
|
| 476 |
+
Plot the flow.
|
| 477 |
+
"""
|
| 478 |
+
lead_score_flow = LeadScoreFlow()
|
| 479 |
+
lead_score_flow.plot()
|
| 480 |
+
|
| 481 |
+
def cand_plot():
|
| 482 |
+
"""
|
| 483 |
+
Plot the flow.
|
| 484 |
+
"""
|
| 485 |
+
cand_score_flow = CandidateScoreFlow()
|
| 486 |
+
cand_score_flow.plot()
|
| 487 |
+
|
| 488 |
+
def improve_resume_plot():
|
| 489 |
+
"""
|
| 490 |
+
Plot the flow.
|
| 491 |
+
"""
|
| 492 |
+
improve_resume_flow = ImproveResumeFlow()
|
| 493 |
+
improve_resume_flow.plot()
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
|
src/ats/types.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel,Field , EmailStr, HttpUrl
|
| 2 |
+
from typing_extensions import Literal
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class JobDescription(BaseModel):
|
| 8 |
+
title: str
|
| 9 |
+
description: str
|
| 10 |
+
skills: str
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Candidate(BaseModel):
|
| 14 |
+
id: str
|
| 15 |
+
name: str
|
| 16 |
+
email: str
|
| 17 |
+
bio: str
|
| 18 |
+
years_of_exp:str
|
| 19 |
+
skills: str
|
| 20 |
+
|
| 21 |
+
class CandidateFilter(BaseModel):
|
| 22 |
+
id: str
|
| 23 |
+
name:str
|
| 24 |
+
email: str
|
| 25 |
+
result: Literal["Pass", "Fail"] = Field(
|
| 26 |
+
None, description="The Pass or Fail result"
|
| 27 |
+
)
|
| 28 |
+
reason:str
|
| 29 |
+
|
| 30 |
+
class CandidateScore(BaseModel):
|
| 31 |
+
id: str
|
| 32 |
+
score: int
|
| 33 |
+
reason: str
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ScoredCandidate(BaseModel):
|
| 37 |
+
id: str
|
| 38 |
+
name: str
|
| 39 |
+
email: str
|
| 40 |
+
bio: str
|
| 41 |
+
skills: str
|
| 42 |
+
score: int
|
| 43 |
+
reason: str
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ResumeData(BaseModel):
|
| 47 |
+
name: str
|
| 48 |
+
email: str
|
| 49 |
+
mobile_number: str
|
| 50 |
+
skills: List[str]
|
| 51 |
+
education: List[str]
|
| 52 |
+
objective:Optional[List[str]]
|
| 53 |
+
experience_years: Optional[float]
|
| 54 |
+
experience_details: Optional[List[str]]
|
| 55 |
+
projects: Optional[List[str]]
|
| 56 |
+
certifications: Optional[List[str]]
|
| 57 |
+
linkedin: Optional[str]
|
| 58 |
+
github: Optional[str]
|
| 59 |
+
|
| 60 |
+
class Resume_Final(BaseModel):
|
| 61 |
+
resume_data:str
|
| 62 |
+
feedback:str
|
| 63 |
+
score:str
|
src/ats/utils/candidateUtils.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
from src.ats.types import Candidate, CandidateScore, ScoredCandidate
|
| 4 |
+
import csv
|
| 5 |
+
from openai import OpenAI,OpenAIError
|
| 6 |
+
import docx
|
| 7 |
+
import pdfplumber
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import csv
|
| 11 |
+
import smtplib
|
| 12 |
+
from email.mime.text import MIMEText
|
| 13 |
+
import base64
|
| 14 |
+
import streamlit as st
|
| 15 |
+
|
| 16 |
+
def extract_text_from_pdf(file):
|
| 17 |
+
text = ""
|
| 18 |
+
with pdfplumber.open(file) as pdf:
|
| 19 |
+
for page in pdf.pages:
|
| 20 |
+
text += page.extract_text() + "\n"
|
| 21 |
+
return text
|
| 22 |
+
|
| 23 |
+
def extract_text_from_docx(file):
|
| 24 |
+
doc = docx.Document(file)
|
| 25 |
+
text = "\n".join([para.text for para in doc.paragraphs])
|
| 26 |
+
return text
|
| 27 |
+
|
| 28 |
+
def get_resume_text(file):
|
| 29 |
+
if file.type == "application/pdf":
|
| 30 |
+
return extract_text_from_pdf(file)
|
| 31 |
+
elif file.type in ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword"]:
|
| 32 |
+
return extract_text_from_docx(file)
|
| 33 |
+
else:
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
def extract_candidate_info(resume_text,id)-> Candidate:
|
| 37 |
+
prompt = (
|
| 38 |
+
f"Extract the following information from the candidate resume:\n"
|
| 39 |
+
f"- Full Name\n"
|
| 40 |
+
f"- Email Address\n"
|
| 41 |
+
f"- A short biodata (concise 3-4 line professional summary)\n"
|
| 42 |
+
f"- Total Years of Professional Experience\n"
|
| 43 |
+
f"- List of Key Skills (comma-separated)\n\n"
|
| 44 |
+
f"Resume:\n{resume_text}\n\n"
|
| 45 |
+
f"Respond in the following JSON format:\n"
|
| 46 |
+
f"""{{
|
| 47 |
+
"name": "<name>",
|
| 48 |
+
"email": "<email>",
|
| 49 |
+
"biodata": "<biodata>",
|
| 50 |
+
"years_of_experience": "<years>",
|
| 51 |
+
"skills": ["<skill1>", "<skill2>", "..."],
|
| 52 |
+
}}"""
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
#To generate specific JSON structure from openai
|
| 56 |
+
candidate_schema = {
|
| 57 |
+
"name": "extract_candidate_info",
|
| 58 |
+
"description": "Extracts structured candidate information from a resume.",
|
| 59 |
+
"parameters": Candidate.model_json_schema()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
client = OpenAI()
|
| 63 |
+
try:
|
| 64 |
+
response = client.chat.completions.create(
|
| 65 |
+
model="gpt-4o-mini",
|
| 66 |
+
messages=[
|
| 67 |
+
{"role": "system", "content": "You are an expert resume screener and recruiter."},
|
| 68 |
+
{"role": "user", "content": prompt}
|
| 69 |
+
],
|
| 70 |
+
functions=[candidate_schema],
|
| 71 |
+
function_call={"name": "extract_candidate_info"},
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
#print("RESPONSE : ",response)
|
| 75 |
+
|
| 76 |
+
# Get and parse the arguments
|
| 77 |
+
function_args = response.choices[0].message.function_call.arguments
|
| 78 |
+
except OpenAIError as e:
|
| 79 |
+
print(f"OpenAI API call failed: {e}")
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
data = json.loads(function_args)
|
| 84 |
+
except json.JSONDecodeError:
|
| 85 |
+
# fallback if model responds badly
|
| 86 |
+
return None
|
| 87 |
+
data["id"] = str(id)
|
| 88 |
+
return Candidate(**data)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def combine_candidates_with_scores(
|
| 92 |
+
candidates: List[Candidate], candidate_scores: List[CandidateScore]
|
| 93 |
+
) -> List[ScoredCandidate]:
|
| 94 |
+
"""
|
| 95 |
+
Combine the candidates with their scores using a dictionary for efficient lookups.
|
| 96 |
+
"""
|
| 97 |
+
print("COMBINING CANDIDATES WITH SCORES")
|
| 98 |
+
print("SCORES:", candidate_scores)
|
| 99 |
+
print("CANDIDATES:", candidates)
|
| 100 |
+
# Create a dictionary to map score IDs to their corresponding CandidateScore objects
|
| 101 |
+
score_dict = {score.id: score for score in candidate_scores}
|
| 102 |
+
print("SCORE DICT:", score_dict)
|
| 103 |
+
|
| 104 |
+
scored_candidates = []
|
| 105 |
+
for candidate in candidates:
|
| 106 |
+
score = score_dict.get(candidate.id)
|
| 107 |
+
if score:
|
| 108 |
+
scored_candidates.append(
|
| 109 |
+
ScoredCandidate(
|
| 110 |
+
id=candidate.id,
|
| 111 |
+
name=candidate.name,
|
| 112 |
+
email=candidate.email,
|
| 113 |
+
bio=candidate.bio,
|
| 114 |
+
skills=candidate.skills,
|
| 115 |
+
score=score.score,
|
| 116 |
+
reason=score.reason,
|
| 117 |
+
)
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
print("SCORED CANDIDATES:", scored_candidates)
|
| 121 |
+
with open("lead_scores.csv", "w", newline="") as f:
|
| 122 |
+
writer = csv.writer(f)
|
| 123 |
+
writer.writerow(["id", "name", "email", "score"])
|
| 124 |
+
for candidate in scored_candidates:
|
| 125 |
+
writer.writerow(
|
| 126 |
+
[
|
| 127 |
+
candidate.id,
|
| 128 |
+
candidate.name,
|
| 129 |
+
candidate.email,
|
| 130 |
+
candidate.score
|
| 131 |
+
]
|
| 132 |
+
)
|
| 133 |
+
print("Lead scores saved to lead_scores.csv")
|
| 134 |
+
return scored_candidates
|
| 135 |
+
|
| 136 |
+
def send_email(file_path,to_email):
|
| 137 |
+
EMAIL_ADDRESS = os.getenv("EMAIL_ADDRESS")
|
| 138 |
+
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
|
| 139 |
+
SMTP_SERVER = 'smtp.gmail.com'
|
| 140 |
+
SMTP_PORT = 587
|
| 141 |
+
try:
|
| 142 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 143 |
+
lines = f.readlines()
|
| 144 |
+
|
| 145 |
+
if not lines[0].lower().startswith("subject:"):
|
| 146 |
+
raise ValueError(f"File {file_path} does not start with 'Subject:'")
|
| 147 |
+
|
| 148 |
+
subject = lines[0][8:].strip()
|
| 149 |
+
body = "".join(lines[1:]).strip()
|
| 150 |
+
|
| 151 |
+
msg = MIMEText(body)
|
| 152 |
+
msg['Subject'] = subject
|
| 153 |
+
msg['From'] = EMAIL_ADDRESS
|
| 154 |
+
msg['To'] = to_email
|
| 155 |
+
|
| 156 |
+
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
| 157 |
+
server.starttls()
|
| 158 |
+
server.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
|
| 159 |
+
server.send_message(msg)
|
| 160 |
+
print(f"Email sent to {to_email}")
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
print(f"Error sending to {to_email}: {e}")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def display_resume(file_bytes: bytes, file_name: str):
|
| 167 |
+
"""Displays the uploaded PDF in an iframe."""
|
| 168 |
+
base64_pdf = base64.b64encode(file_bytes).decode("utf-8")
|
| 169 |
+
pdf_display = f"""
|
| 170 |
+
<iframe
|
| 171 |
+
src="data:application/pdf;base64,{base64_pdf}"
|
| 172 |
+
width="100%"
|
| 173 |
+
height="600px"
|
| 174 |
+
type="application/pdf"
|
| 175 |
+
>
|
| 176 |
+
</iframe>
|
| 177 |
+
"""
|
| 178 |
+
st.markdown(f"### Preview of {file_name}")
|
| 179 |
+
st.markdown(pdf_display, unsafe_allow_html=True)
|
| 180 |
+
|
| 181 |
+
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|