sunithalv commited on
Commit
3241f25
·
1 Parent(s): 9282a86

Latest code changes committed

Browse files
Files changed (38) hide show
  1. README.md +63 -1
  2. app.py +203 -0
  3. pyproject.toml +29 -0
  4. src/ats/__init__.py +0 -0
  5. src/ats/constants.py +47 -0
  6. src/ats/crews/lead_filter_crew/config/agents.yaml +11 -0
  7. src/ats/crews/lead_filter_crew/config/tasks.yaml +38 -0
  8. src/ats/crews/lead_filter_crew/lead_filter_crew.py +35 -0
  9. src/ats/crews/lead_response_crew/config/agents.yaml +10 -0
  10. src/ats/crews/lead_response_crew/config/tasks.yaml +26 -0
  11. src/ats/crews/lead_response_crew/lead_response_crew.py +35 -0
  12. src/ats/crews/lead_score_crew/config/agents.yaml +10 -0
  13. src/ats/crews/lead_score_crew/config/tasks.yaml +40 -0
  14. src/ats/crews/lead_score_crew/lead_score_crew.py +35 -0
  15. src/ats/crews/resume_parser_crew/config/agents.yaml +12 -0
  16. src/ats/crews/resume_parser_crew/config/tasks.yaml +8 -0
  17. src/ats/crews/resume_parser_crew/resume_parser_crew.py +38 -0
  18. src/ats/crews/resume_parser_crew/tools/resume_parser_tool.py +213 -0
  19. src/ats/crews/resume_score_crew/config/agents.yaml +10 -0
  20. src/ats/crews/resume_score_crew/config/tasks.yaml +40 -0
  21. src/ats/crews/resume_score_crew/resume_score_crew.py +35 -0
  22. src/ats/crews/rewrite_resume_crew/config/agents.yaml +16 -0
  23. src/ats/crews/rewrite_resume_crew/config/tasks.yaml +11 -0
  24. src/ats/crews/rewrite_resume_crew/rewrite_resume_crew.py +38 -0
  25. src/ats/crews/rewrite_resume_crew/tools/rewrite_resume_tool.py +145 -0
  26. src/ats/crews/web_scraper_crew/config/agents.yaml +15 -0
  27. src/ats/crews/web_scraper_crew/config/tasks.yaml +28 -0
  28. src/ats/crews/web_scraper_crew/web_scraper_crew.py +45 -0
  29. src/ats/email_responses/Amelia Cole.txt +18 -0
  30. src/ats/email_responses/Jacob Reed.txt +20 -0
  31. src/ats/email_responses/John Carter.txt +17 -0
  32. src/ats/email_responses/Nora Blake.txt +20 -0
  33. src/ats/email_responses/Owen Mitchell.txt +18 -0
  34. src/ats/email_responses/Sophia Williams.txt +17 -0
  35. src/ats/main.py +496 -0
  36. src/ats/types.py +63 -0
  37. src/ats/utils/candidateUtils.py +181 -0
  38. 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