first commit
This commit is contained in:
commit
af518b7d7b
42
data_folder/config.yaml
Normal file
42
data_folder/config.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
remote: [true/false]
|
||||
|
||||
experienceLevel:
|
||||
internship: [true/false]
|
||||
entry: [true/false]
|
||||
associate: [true/false]
|
||||
mid-senior level: [true/false]
|
||||
director: [true/false]
|
||||
executive: [true/false]
|
||||
|
||||
jobTypes:
|
||||
full-time: [true/false]
|
||||
contract: [true/false]
|
||||
part-time: [true/false]
|
||||
temporary: [true/false]
|
||||
internship: [true/false]
|
||||
other: [true/false]
|
||||
volunteer: [true/false]
|
||||
|
||||
date:
|
||||
all time: [true/false]
|
||||
month: [true/false]
|
||||
week: [true/false]
|
||||
24 hours: [true/false]
|
||||
|
||||
positions:
|
||||
- [Positions 1]
|
||||
- [Positions 2]
|
||||
|
||||
locations:
|
||||
- [Locations 1]
|
||||
- [Locations 2]
|
||||
|
||||
distance: [0 or 5 or 10 or 25 or 50 or 100]
|
||||
|
||||
companyBlacklist:
|
||||
- [Company Name 1]
|
||||
- [Company Name 2]
|
||||
|
||||
titleBlacklist:
|
||||
- [Word 1]
|
||||
- [Word 2]
|
105
data_folder/plain_text_resume.yaml
Normal file
105
data_folder/plain_text_resume.yaml
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"personalInformation": {
|
||||
"name": "[Your name]",
|
||||
"surname": "[Your surname]",
|
||||
"dateOfBirth": "[Your Date of Birth]",
|
||||
"country": "[Your country]",
|
||||
"city": "[Your city]",
|
||||
"address": "[Your address]",
|
||||
"phonePrefix": "[Your phone number prefix]",
|
||||
"phone": "[Your phone number]",
|
||||
"email": "[Your email]",
|
||||
"github": "[Link]",
|
||||
"linkedin": "[Link]"
|
||||
},
|
||||
"selfIdentification": {
|
||||
"gender": "[Specific gender identification]",
|
||||
"pronouns": "[Your Pronouns]",
|
||||
"veteran": "[Yes/No]",
|
||||
"disability": "[Yes/No]",
|
||||
"ethnicity": "[Specify ethnicity]"
|
||||
},
|
||||
"legalAuthorization": {
|
||||
"euWorkAuthorization": "[Yes/No]",
|
||||
"usWorkAuthorization": "[Yes/No]",
|
||||
"requiresUsVisa": "[Yes/No]",
|
||||
"legallyAllowedToWorkInUs": "[Yes/No]",
|
||||
"requiresUsSponsorship": "[Yes/No]",
|
||||
"requiresEuVisa": "[Yes/No]",
|
||||
"legallyAllowedToWorkInEu": "[Yes/No]",
|
||||
"requiresEuSponsorship": "[Yes/No]"
|
||||
},
|
||||
"workPreferences": {
|
||||
"remoteWork": "[Yes/No]",
|
||||
"inPersonWork": "[Yes/No]",
|
||||
"openToRelocation": "[Yes/No]",
|
||||
"willingToCompleteAssessments": "[Yes/No]",
|
||||
"willingToUndergoDrugTests": "[Yes/No]",
|
||||
"willingToUndergoBackgroundChecks": "[Yes/No]"
|
||||
},
|
||||
"educationDetails": [
|
||||
{
|
||||
"degree": "[Bachelor's/Master's/Ph.D.]",
|
||||
"university": "[Name of University]",
|
||||
"gpa": "[Your GPA]",
|
||||
"graduationYear": "[Year of Graduation]",
|
||||
"fieldOfStudy": "[Your Field of Study]",
|
||||
"skillsAcquired": {
|
||||
"blockchain": "[Years]",
|
||||
"iot": "[Years]",
|
||||
"python": "[Years]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"experienceDetails": [
|
||||
{
|
||||
"position": "[Job Title]",
|
||||
"company": "[Company Name]",
|
||||
"employmentPeriod": "[Month, Year] - [Month, Year]",
|
||||
"location": "[City, Country]",
|
||||
"industry": "[Industry of the Company]",
|
||||
"keyResponsibilities": {
|
||||
"responsibility1": "[Years]",
|
||||
"responsibility2": "[Years]",
|
||||
"responsibility3": "[Years]"
|
||||
},
|
||||
"skillsAcquired": {
|
||||
"php": "[Years]",
|
||||
"python": "[Years]",
|
||||
"leadership": "[Years]",
|
||||
"problemSolving": "[Years]",
|
||||
"criticalThinking": "[Years]",
|
||||
"adaptability": "[Years]",
|
||||
"perfectionism": "[Years]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"projects": {
|
||||
"project1": "[Description of significant projects you've worked on + if available repo link]",
|
||||
"project2": "[Description of significant projects you've worked on + if available repo link]"
|
||||
},
|
||||
"availability": {
|
||||
"noticePeriod": "[Specify notice period]"
|
||||
},
|
||||
"salaryExpectations": {
|
||||
"salaryRangeUSD": "[Specify your salary expectations in USD]"
|
||||
},
|
||||
"certifications": [
|
||||
"[Certification 1]",
|
||||
"[Certification 2]",
|
||||
"[Certification 3]"
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"language": "Italian",
|
||||
"proficiency": "Native"
|
||||
},
|
||||
{
|
||||
"language": "English",
|
||||
"proficiency": "Professional"
|
||||
}
|
||||
],
|
||||
"interests": [
|
||||
"[List any hobbies or interests relevant to your professional profile]"
|
||||
]
|
||||
}
|
3
data_folder/secrets.yaml
Normal file
3
data_folder/secrets.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
email: [Your Linkedin email]
|
||||
password: [Your Linkedin password]
|
||||
openai_api_key: [OpenAi API key, tutorial -> https://medium.com/@lorenzozar/how-to-get-your-own-openai-api-key-f4d44e60c327]
|
39
data_folder_example/config.yaml
Normal file
39
data_folder_example/config.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
remote: true
|
||||
|
||||
experienceLevel:
|
||||
internship: false
|
||||
entry: true
|
||||
associate: true
|
||||
mid-senior level: true
|
||||
director: true
|
||||
executive: true
|
||||
|
||||
jobTypes:
|
||||
full-time: true
|
||||
contract: true
|
||||
part-time: true
|
||||
temporary: true
|
||||
internship: false
|
||||
other: true
|
||||
volunteer: false
|
||||
|
||||
date:
|
||||
all time: false
|
||||
month: false
|
||||
week: true
|
||||
24 hours: false
|
||||
|
||||
positions:
|
||||
- Software engineer
|
||||
|
||||
locations:
|
||||
- Germany
|
||||
|
||||
distance: 100
|
||||
|
||||
companyBlacklist:
|
||||
- Noir
|
||||
- Crossover
|
||||
|
||||
titleBlacklist:
|
||||
- Stage
|
95
data_folder_example/plain_text_resume.yaml
Normal file
95
data_folder_example/plain_text_resume.yaml
Normal file
@ -0,0 +1,95 @@
|
||||
personal_information:
|
||||
name: "Mario"
|
||||
surname: "Rossi"
|
||||
dateOfBirth: "15/09/1988"
|
||||
country: "Italy"
|
||||
city: "Milan"
|
||||
address: "Via Montenapoleone 10, 20121 Milan"
|
||||
phonePrefix: "+39"
|
||||
phone: "3351234567"
|
||||
email: "mario.rossi@techcode.it"
|
||||
github: "https://github.com/mario-rossi-dev"
|
||||
linkedin: "https://www.linkedin.com/in/mario-rossi-developer/"
|
||||
|
||||
self_identification:
|
||||
gender: "Male"
|
||||
pronouns: "He/Him"
|
||||
veteran: false
|
||||
disability: false
|
||||
ethnicity: "Mediterranean"
|
||||
|
||||
legal_authorization:
|
||||
euWorkAuthorization: true
|
||||
usWorkAuthorization: false
|
||||
requiresUsVisa: true
|
||||
legallyAllowedToWorkInUs: false
|
||||
requiresUsSponsorship: true
|
||||
requiresEuVisa: false
|
||||
legallyAllowedToWorkInEu: true
|
||||
requiresEuSponsorship: false
|
||||
|
||||
work_preferences:
|
||||
remoteWork: true
|
||||
inPersonWork: true
|
||||
openToRelocation: true
|
||||
willingToCompleteAssessments: true
|
||||
willingToUndergoDrugTests: true
|
||||
willingToUndergoBackgroundChecks: true
|
||||
|
||||
education_details:
|
||||
- degree: "Master"
|
||||
university: "Politecnico di Milano"
|
||||
gpa: "3.8/4"
|
||||
graduationYear: "2012"
|
||||
fieldOfStudy: "Computer Engineering"
|
||||
skillsAcquired:
|
||||
artificialIntelligence: "4"
|
||||
dataScience: "3"
|
||||
cloudComputing: "3"
|
||||
|
||||
experience_details:
|
||||
- position: "Senior Software Engineer"
|
||||
company: "TechInnovate S.p.A."
|
||||
employmentPeriod: "06/2018 - Present"
|
||||
location: "Milan, Italy"
|
||||
industry: "FinTech"
|
||||
keyResponsibilities:
|
||||
responsibility1: "Led development of real-time trading algorithm, improving transaction speed by 40%"
|
||||
responsibility2: "Implemented CI/CD pipeline, reducing deployment time from days to hours"
|
||||
responsibility3: "Mentored junior developers, increasing team productivity by 25% over 6 months"
|
||||
skillsAcquired:
|
||||
java: "5"
|
||||
springBoot: "4"
|
||||
kubernetes: "3"
|
||||
aws: "4"
|
||||
microservices: "4"
|
||||
agileMethodologies: "5"
|
||||
|
||||
projects:
|
||||
project1: "Developed a high-frequency trading platform using Java and Spring Boot, processing over 1 million transactions per second"
|
||||
project2: "Led the migration of legacy systems to a microservices architecture, improving system reliability by 99.99%"
|
||||
|
||||
availability:
|
||||
noticePeriod: "3 months"
|
||||
|
||||
salary_expectations:
|
||||
salaryRangeUSD: "90000"
|
||||
|
||||
certifications:
|
||||
- "AWS Certified Solutions Architect"
|
||||
- "Oracle Certified Professional, Java SE 11 Developer"
|
||||
- "Certified Scrum Master"
|
||||
|
||||
languages:
|
||||
- language: "Italian"
|
||||
proficiency: "Native"
|
||||
- language: "English"
|
||||
proficiency: "Fluent"
|
||||
- language: "Spanish"
|
||||
proficiency: "Intermediate"
|
||||
|
||||
interests:
|
||||
- "Open Source Contributing"
|
||||
- "Machine Learning"
|
||||
- "Hiking"
|
||||
- "Chess"
|
3
data_folder_example/secrets.yaml
Normal file
3
data_folder_example/secrets.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
email: myemaillinkedin@gmail.com
|
||||
password: ImpossiblePassowrd10
|
||||
openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR
|
279
gpt.py
Normal file
279
gpt.py
Normal file
@ -0,0 +1,279 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from langchain_core.messages.ai import AIMessage
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompt_values import StringPromptValue
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_openai import ChatOpenAI
|
||||
from Levenshtein import distance
|
||||
|
||||
import strings
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class LLMLogger:
|
||||
|
||||
def __init__(self, llm: ChatOpenAI):
|
||||
self.llm = llm
|
||||
|
||||
@staticmethod
|
||||
def log_request(prompts, parsed_reply: Dict[str, Dict]):
|
||||
calls_log = os.path.join(os.getcwd(), "open_ai_calls.json")
|
||||
if isinstance(prompts, StringPromptValue):
|
||||
prompts = prompts.text
|
||||
elif isinstance(prompts, Dict):
|
||||
# Convert prompts to a dictionary if they are not in the expected format
|
||||
prompts = {
|
||||
f"prompt_{i+1}": prompt.content
|
||||
for i, prompt in enumerate(prompts.messages)
|
||||
}
|
||||
else:
|
||||
prompts = {
|
||||
f"prompt_{i+1}": prompt.content
|
||||
for i, prompt in enumerate(prompts.messages)
|
||||
}
|
||||
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Extract token usage details from the response
|
||||
token_usage = parsed_reply["usage_metadata"]
|
||||
output_tokens = token_usage["output_tokens"]
|
||||
input_tokens = token_usage["input_tokens"]
|
||||
total_tokens = token_usage["total_tokens"]
|
||||
|
||||
# Extract model details from the response
|
||||
model_name = parsed_reply["response_metadata"]["model_name"]
|
||||
prompt_price_per_token = 0.00000015
|
||||
completion_price_per_token = 0.0000006
|
||||
|
||||
# Calculate the total cost of the API call
|
||||
total_cost = (input_tokens * prompt_price_per_token) + (
|
||||
output_tokens * completion_price_per_token
|
||||
)
|
||||
|
||||
# Create a log entry with all relevant information
|
||||
log_entry = {
|
||||
"model": model_name,
|
||||
"time": current_time,
|
||||
"prompts": prompts,
|
||||
"replies": parsed_reply["content"], # Response content
|
||||
"total_tokens": total_tokens,
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
"total_cost": total_cost,
|
||||
}
|
||||
|
||||
# Write the log entry to the log file in JSON format
|
||||
with open(calls_log, "a", encoding="utf-8") as f:
|
||||
json_string = json.dumps(log_entry, ensure_ascii=False, indent=4)
|
||||
f.write(json_string + "\n")
|
||||
|
||||
|
||||
class LoggerChatModel:
|
||||
|
||||
def __init__(self, llm: ChatOpenAI):
|
||||
self.llm = llm
|
||||
|
||||
def __call__(self, messages: List[Dict[str, str]]) -> str:
|
||||
# Call the LLM with the provided messages and log the response.
|
||||
reply = self.llm(messages)
|
||||
parsed_reply = self.parse_llmresult(reply)
|
||||
LLMLogger.log_request(prompts=messages, parsed_reply=parsed_reply)
|
||||
return reply
|
||||
|
||||
def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]:
|
||||
# Parse the LLM result into a structured format.
|
||||
content = llmresult.content
|
||||
response_metadata = llmresult.response_metadata
|
||||
id_ = llmresult.id
|
||||
usage_metadata = llmresult.usage_metadata
|
||||
|
||||
parsed_result = {
|
||||
"content": content,
|
||||
"response_metadata": {
|
||||
"model_name": response_metadata.get("model_name", ""),
|
||||
"system_fingerprint": response_metadata.get("system_fingerprint", ""),
|
||||
"finish_reason": response_metadata.get("finish_reason", ""),
|
||||
"logprobs": response_metadata.get("logprobs", None),
|
||||
},
|
||||
"id": id_,
|
||||
"usage_metadata": {
|
||||
"input_tokens": usage_metadata.get("input_tokens", 0),
|
||||
"output_tokens": usage_metadata.get("output_tokens", 0),
|
||||
"total_tokens": usage_metadata.get("total_tokens", 0),
|
||||
},
|
||||
}
|
||||
return parsed_result
|
||||
|
||||
|
||||
class GPTAnswerer:
|
||||
def __init__(self, openai_api_key):
|
||||
self.llm_cheap = LoggerChatModel(
|
||||
ChatOpenAI(
|
||||
model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.8
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def job_description(self):
|
||||
return self.job.description
|
||||
|
||||
@staticmethod
|
||||
def find_best_match(text: str, options: list[str]) -> str:
|
||||
# Find the best match for the given text from a list of options using Levenshtein distance.
|
||||
distances = [
|
||||
(option, distance(text.lower(), option.lower())) for option in options
|
||||
]
|
||||
best_option = min(distances, key=lambda x: x[1])[0]
|
||||
return best_option
|
||||
|
||||
@staticmethod
|
||||
def _remove_placeholders(text: str) -> str:
|
||||
# Remove placeholder text from a string.
|
||||
text = text.replace("PLACEHOLDER", "")
|
||||
return text.strip()
|
||||
|
||||
@staticmethod
|
||||
def _preprocess_template_string(template: str) -> str:
|
||||
# Preprocess a template string to remove unnecessary indentation.
|
||||
return textwrap.dedent(template)
|
||||
|
||||
def set_resume(self, resume):
|
||||
self.resume = resume
|
||||
|
||||
def set_job(self, job):
|
||||
self.job = job
|
||||
self.job.set_summarize_job_description(
|
||||
self.summarize_job_description(self.job.description)
|
||||
)
|
||||
|
||||
def summarize_job_description(self, text: str) -> str:
|
||||
strings.summarize_prompt_template = self._preprocess_template_string(
|
||||
strings.summarize_prompt_template
|
||||
)
|
||||
prompt = ChatPromptTemplate.from_template(strings.summarize_prompt_template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"text": text})
|
||||
return output
|
||||
|
||||
"""def get_resume_html(self):
|
||||
latex_resume_template = self._preprocess_template_string(strings.latex_resume_template)
|
||||
prompt = ChatPromptTemplate.from_template(latex_resume_template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"resume": self.resume, "job_description": self.job.summarize_job_description})
|
||||
return output"""
|
||||
|
||||
|
||||
def get_resume_html(self):
|
||||
# Crea i prompt a partire dai template
|
||||
prepare_info_prompt = ChatPromptTemplate.from_template(strings.prepare_info_template)
|
||||
format_resume_prompt = ChatPromptTemplate.from_template(strings.format_resume_template)
|
||||
review_and_optimize_prompt = ChatPromptTemplate.from_template(strings.review_and_optimize_template)
|
||||
|
||||
# Creazione delle catene
|
||||
prepare_info_chain = prepare_info_prompt | self.llm_cheap | StrOutputParser()
|
||||
format_resume_chain = format_resume_prompt | self.llm_cheap | StrOutputParser()
|
||||
review_and_optimize_chain = review_and_optimize_prompt | self.llm_cheap | StrOutputParser()
|
||||
|
||||
composed_chain = (
|
||||
prepare_info_chain
|
||||
| (lambda output: {"formatted_resume": output})
|
||||
| format_resume_chain
|
||||
| (lambda output: {"final_resume_html": output})
|
||||
| review_and_optimize_chain
|
||||
)
|
||||
|
||||
try:
|
||||
output = composed_chain.invoke({
|
||||
"resume": self.resume,
|
||||
"job_description": self.job.summarize_job_description
|
||||
})
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
#print(f"Error during elaboration: {e}")
|
||||
pass
|
||||
|
||||
|
||||
def _create_chain(self, template: str):
|
||||
prompt = ChatPromptTemplate.from_template(template)
|
||||
return prompt | self.llm_cheap | StrOutputParser()
|
||||
|
||||
def answer_question_textual_wide_range(self, question: str) -> str:
|
||||
# Define chains for each section of the resume
|
||||
self.chains = {
|
||||
"personal_information": self._create_chain(strings.personal_information_template),
|
||||
"self_identification": self._create_chain(strings.self_identification_template),
|
||||
"legal_authorization": self._create_chain(strings.legal_authorization_template),
|
||||
"work_preferences": self._create_chain(strings.work_preferences_template),
|
||||
"education_details": self._create_chain(strings.education_details_template),
|
||||
"experience_details": self._create_chain(strings.experience_details_template),
|
||||
"projects": self._create_chain(strings.projects_template),
|
||||
"availability": self._create_chain(strings.availability_template),
|
||||
"salary_expectations": self._create_chain(strings.salary_expectations_template),
|
||||
"certifications": self._create_chain(strings.certifications_template),
|
||||
"languages": self._create_chain(strings.languages_template),
|
||||
"interests": self._create_chain(strings.interests_template),
|
||||
}
|
||||
section_prompt = (
|
||||
f"For the following question: '{question}', which section of the resume is relevant? "
|
||||
"Respond with one of the following: Personal information, Self-Identification, Legal Authorization, "
|
||||
"Work Preferences, Education Details, Experience Details, Projects, Availability, Salary Expectations, "
|
||||
"Certifications, Languages, Interests."
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_template(section_prompt)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"question": question})
|
||||
section_name = output.lower().replace(" ", "_")
|
||||
|
||||
resume_section = getattr(self.resume, section_name, None)
|
||||
if resume_section is None:
|
||||
raise ValueError(f"Section '{section_name}' not found in the resume.")
|
||||
|
||||
# Use the corresponding chain to answer the question
|
||||
chain = self.chains.get(section_name)
|
||||
if chain is None:
|
||||
raise ValueError(f"Chain not defined for section '{section_name}'")
|
||||
output_str = chain.invoke({"resume_section": resume_section, "question": question})
|
||||
return output_str
|
||||
|
||||
def answer_question_textual(self, question: str) -> str:
|
||||
template = self._preprocess_template_string(strings.resume_stuff_template)
|
||||
prompt = ChatPromptTemplate.from_template(template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"resume": self.resume, "question": question})
|
||||
return output
|
||||
|
||||
def answer_question_numeric(self, question: str, default_experience: int = 3) -> int:
|
||||
func_template = self._preprocess_template_string(strings.numeric_question_template)
|
||||
prompt = ChatPromptTemplate.from_template(func_template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output_str = chain.invoke({"resume": self.resume, "question": question, "default_experience": default_experience})
|
||||
try:
|
||||
output = self.extract_number_from_string(output_str)
|
||||
except ValueError:
|
||||
output = default_experience
|
||||
return output
|
||||
|
||||
def extract_number_from_string(self, output_str):
|
||||
numbers = re.findall(r"\d+", output_str)
|
||||
if numbers:
|
||||
return int(numbers[0])
|
||||
else:
|
||||
raise ValueError("No numbers found in the string")
|
||||
|
||||
def answer_question_from_options(self, question: str, options: list[str]) -> str:
|
||||
func_template = self._preprocess_template_string(strings.options_template)
|
||||
prompt = ChatPromptTemplate.from_template(func_template)
|
||||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output_str = chain.invoke({"resume": self.resume, "question": question, "options": options})
|
||||
best_option = self.find_best_match(output_str, options)
|
||||
return best_option
|
33
job.py
Normal file
33
job.py
Normal file
@ -0,0 +1,33 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Job:
|
||||
title: str
|
||||
company: str
|
||||
location: str
|
||||
link: str
|
||||
apply_method: str
|
||||
description: str = ""
|
||||
summarize_job_description: str = ""
|
||||
|
||||
def set_summarize_job_description(self, summarize_job_description):
|
||||
self.summarize_job_description = summarize_job_description
|
||||
|
||||
def set_job_description(self, description):
|
||||
self.description = description
|
||||
|
||||
def formatted_job_information(self):
|
||||
"""
|
||||
Formats the job information as a markdown string.
|
||||
"""
|
||||
job_information = f"""
|
||||
# Job Description
|
||||
## Job Information
|
||||
- Position: {self.title}
|
||||
- At: {self.company}
|
||||
- Location: {self.location}
|
||||
|
||||
## Description
|
||||
{self.description or 'No description provided.'}
|
||||
"""
|
||||
return job_information.strip()
|
92
linkedIn_authenticator.py
Normal file
92
linkedIn_authenticator.py
Normal file
@ -0,0 +1,92 @@
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
class LinkedInAuthenticator:
|
||||
|
||||
def __init__(self, driver=None):
|
||||
self.driver = driver
|
||||
self.email = ""
|
||||
self.password = ""
|
||||
|
||||
def set_secrets(self, email, password):
|
||||
self.email = email
|
||||
self.password = password
|
||||
|
||||
def start(self):
|
||||
"""Start the Chrome browser and attempt to log in to LinkedIn."""
|
||||
print("Starting Chrome browser to log in to LinkedIn.")
|
||||
self.driver.get('https://www.linkedin.com')
|
||||
self.wait_for_page_load()
|
||||
if not self.is_logged_in():
|
||||
self.handle_login()
|
||||
|
||||
def handle_login(self):
|
||||
"""Handle the LinkedIn login process."""
|
||||
print("Navigating to the LinkedIn login page...")
|
||||
self.driver.get("https://www.linkedin.com/login")
|
||||
try:
|
||||
self.enter_credentials()
|
||||
self.submit_login_form()
|
||||
except NoSuchElementException:
|
||||
print("Could not log in to LinkedIn. Please check your credentials.")
|
||||
self.handle_security_check()
|
||||
|
||||
def enter_credentials(self):
|
||||
"""Enter the user's email and password into the login form."""
|
||||
try:
|
||||
email_field = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "username"))
|
||||
)
|
||||
email_field.send_keys(self.email)
|
||||
password_field = self.driver.find_element(By.ID, "password")
|
||||
password_field.send_keys(self.password)
|
||||
except TimeoutException:
|
||||
print("Login form not found. Aborting login.")
|
||||
|
||||
def submit_login_form(self):
|
||||
"""Submit the LinkedIn login form."""
|
||||
try:
|
||||
login_button = self.driver.find_element(By.XPATH, '//button[@type="submit"]')
|
||||
login_button.click()
|
||||
except NoSuchElementException:
|
||||
print("Login button not found. Please verify the page structure.")
|
||||
|
||||
def handle_security_check(self):
|
||||
"""Handle LinkedIn security checks if triggered."""
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.url_contains('https://www.linkedin.com/checkpoint/challengesV2/')
|
||||
)
|
||||
print("Security checkpoint detected. Please complete the challenge.")
|
||||
WebDriverWait(self.driver, 300).until(
|
||||
EC.url_contains('https://www.linkedin.com/feed/')
|
||||
)
|
||||
print("Security check completed")
|
||||
except TimeoutException:
|
||||
print("Security check not completed. Please try again later.")
|
||||
|
||||
def is_logged_in(self):
|
||||
"""Check if the user is already logged in to LinkedIn."""
|
||||
self.driver.get('https://www.linkedin.com/feed')
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, 'share-box-feed-entry__trigger'))
|
||||
)
|
||||
buttons = self.driver.find_elements(By.CLASS_NAME, 'share-box-feed-entry__trigger')
|
||||
if any(button.text.strip() == 'Start a post' for button in buttons):
|
||||
print("User is already logged in.")
|
||||
return True
|
||||
except TimeoutException:
|
||||
pass
|
||||
return False
|
||||
|
||||
def wait_for_page_load(self, timeout=10):
|
||||
"""Wait for the page to fully load."""
|
||||
try:
|
||||
WebDriverWait(self.driver, timeout).until(
|
||||
lambda d: d.execute_script('return document.readyState') == 'complete'
|
||||
)
|
||||
except TimeoutException:
|
||||
print("Page load timed out.")
|
57
linkedIn_bot_facade.py
Normal file
57
linkedIn_bot_facade.py
Normal file
@ -0,0 +1,57 @@
|
||||
class LinkedInBotFacade:
|
||||
|
||||
def __init__(self, login_component, apply_component):
|
||||
self.login_component = login_component
|
||||
self.apply_component = apply_component
|
||||
self.state = {
|
||||
"credentials_set": False,
|
||||
"api_key_set": False,
|
||||
"resume_set": False,
|
||||
"gpt_answerer_set": False,
|
||||
"parameters_set": False,
|
||||
"logged_in": False
|
||||
}
|
||||
|
||||
def set_resume(self, resume):
|
||||
if not resume:
|
||||
raise ValueError("Plain text resume cannot be empty.")
|
||||
self.resume = resume
|
||||
self.state["resume_set"] = True
|
||||
|
||||
def set_secrets(self, email, password): # Aggiunto openai_api_key
|
||||
if not email or not password :
|
||||
raise ValueError("Email and password cannot be empty.")
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.state["credentials_set"] = True
|
||||
|
||||
def set_gpt_answerer(self, gpt_answerer_component):
|
||||
self.gpt_answerer = gpt_answerer_component
|
||||
self.gpt_answerer.set_resume(self.resume)
|
||||
self.apply_component.set_gpt_answerer(self.gpt_answerer)
|
||||
self.state["gpt_answerer_set"] = True
|
||||
|
||||
def set_parameters(self, parameters):
|
||||
if not parameters:
|
||||
raise ValueError("Parameters cannot be None or empty.")
|
||||
self.parameters = parameters
|
||||
self.apply_component.set_parameters(parameters)
|
||||
self.state["parameters_set"] = True
|
||||
|
||||
def start_login(self):
|
||||
if not self.state["credentials_set"]:
|
||||
raise ValueError("Email and password must be set before logging in.")
|
||||
self.login_component.set_secrets(self.email, self.password)
|
||||
self.login_component.start()
|
||||
self.state["logged_in"] = True
|
||||
|
||||
def start_apply(self):
|
||||
if not self.state["logged_in"]:
|
||||
raise ValueError("You must be logged in before applying.")
|
||||
if not self.state["resume_set"]:
|
||||
raise ValueError("Plain text resume must be set before applying.")
|
||||
if not self.state["gpt_answerer_set"]:
|
||||
raise ValueError("GPT Answerer must be set before applying.")
|
||||
if not self.state["parameters_set"]:
|
||||
raise ValueError("Parameters must be set before applying.")
|
||||
self.apply_component.start_applying()
|
348
linkedIn_easy_applier.py
Normal file
348
linkedIn_easy_applier.py
Normal file
@ -0,0 +1,348 @@
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
from datetime import date
|
||||
from typing import List, Optional, Any, Tuple
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import Select, WebDriverWait
|
||||
import tempfile
|
||||
import time
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
import io
|
||||
import time
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
class LinkedInEasyApplier:
|
||||
def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any):
|
||||
if resume_dir is None or not os.path.exists(resume_dir):
|
||||
resume_dir = None
|
||||
self.driver = driver
|
||||
self.resume_dir = resume_dir
|
||||
self.set_old_answers = set_old_answers
|
||||
self.gpt_answerer = gpt_answerer
|
||||
|
||||
def job_apply(self, job: Any):
|
||||
self.driver.get(job.link)
|
||||
time.sleep(random.uniform(3, 5))
|
||||
try:
|
||||
easy_apply_button = self._find_easy_apply_button()
|
||||
job_description = self._get_job_description()
|
||||
job.set_job_description(job_description)
|
||||
easy_apply_button.click()
|
||||
self.gpt_answerer.set_job(job)
|
||||
self._fill_application_form()
|
||||
except Exception:
|
||||
tb_str = traceback.format_exc()
|
||||
self._discard_application()
|
||||
raise Exception(f"Failed to apply to job! Original exception: \nTraceback:\n{tb_str}")
|
||||
|
||||
|
||||
def _find_easy_apply_button(self) -> WebElement:
|
||||
buttons = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_all_elements_located(
|
||||
(By.XPATH, '//button[contains(@class, "jobs-apply-button") and contains(., "Easy Apply")]')
|
||||
)
|
||||
)
|
||||
for index, button in enumerate(buttons):
|
||||
try:
|
||||
return WebDriverWait(self.driver, 10).until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.XPATH, f'(//button[contains(@class, "jobs-apply-button") and contains(., "Easy Apply")])[{index + 1}]')
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
raise Exception("No clickable 'Easy Apply' button found")
|
||||
|
||||
def _get_job_description(self) -> str:
|
||||
try:
|
||||
see_more_button = self.driver.find_element(By.XPATH, '//button[@aria-label="Click to see more description"]')
|
||||
see_more_button.click()
|
||||
time.sleep(2)
|
||||
description = self.driver.find_element(By.CLASS_NAME, 'jobs-description-content__text').text
|
||||
self._scroll_page()
|
||||
return description
|
||||
except NoSuchElementException:
|
||||
tb_str = traceback.format_exc()
|
||||
raise Exception("Job description 'See more' button not found: \nTraceback:\n{tb_str}")
|
||||
except Exception :
|
||||
tb_str = traceback.format_exc()
|
||||
raise Exception(f"Error getting Job description: \nTraceback:\n{tb_str}")
|
||||
|
||||
def _scroll_page(self) -> None:
|
||||
scrollable_element = self.driver.find_element(By.TAG_NAME, 'html')
|
||||
# utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=False)
|
||||
# utils.scroll_slow(self.driver, scrollable_element, step=300, reverse=True)
|
||||
|
||||
def _fill_application_form(self):
|
||||
while True:
|
||||
self.fill_up()
|
||||
self._next_or_submit()
|
||||
|
||||
def _next_or_submit(self):
|
||||
next_button = self.driver.find_element(By.CLASS_NAME, "artdeco-button--primary")
|
||||
button_text = next_button.text.lower()
|
||||
if 'submit application' in button_text:
|
||||
self._unfollow_company()
|
||||
time.sleep(random.uniform(1.5, 2.5))
|
||||
next_button.click()
|
||||
time.sleep(random.uniform(3.0, 5.0))
|
||||
self._check_for_errors()
|
||||
|
||||
|
||||
def _unfollow_company(self) -> None:
|
||||
try:
|
||||
follow_checkbox = self.driver.find_element(
|
||||
By.XPATH, "//label[contains(.,'to stay up to date with their page.')]")
|
||||
follow_checkbox.click()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _check_for_errors(self) -> None:
|
||||
error_elements = self.driver.find_elements(By.CLASS_NAME, 'artdeco-inline-feedback--error')
|
||||
if error_elements:
|
||||
raise Exception(f"Failed answering or file upload. {str([e.text for e in error_elements])}")
|
||||
|
||||
def _discard_application(self) -> None:
|
||||
try:
|
||||
self.driver.find_element(By.CLASS_NAME, 'artdeco-modal__dismiss').click()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
self.driver.find_elements(By.CLASS_NAME, 'artdeco-modal__confirm-dialog-btn')[0].click()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def fill_up(self) -> None:
|
||||
easy_apply_content = self.driver.find_element(By.CLASS_NAME, 'jobs-easy-apply-content')
|
||||
pb4_elements = easy_apply_content.find_elements(By.CLASS_NAME, 'pb4')
|
||||
for element in pb4_elements:
|
||||
self._process_form_element(element)
|
||||
|
||||
|
||||
def _process_form_element(self, element: WebElement) -> None:
|
||||
try:
|
||||
if self._is_upload_field(element):
|
||||
self._handle_upload_fields(element)
|
||||
else:
|
||||
self._fill_additional_questions()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _is_upload_field(self, element: WebElement) -> bool:
|
||||
try:
|
||||
element.find_element(By.XPATH, ".//input[@type='file']")
|
||||
return True
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
|
||||
def _handle_upload_fields(self, element: WebElement) -> None:
|
||||
file_upload_elements = self.driver.find_elements(By.XPATH, "//input[@type='file']")
|
||||
for element in file_upload_elements:
|
||||
parent = element.find_element(By.XPATH, "..")
|
||||
self.driver.execute_script("arguments[0].classList.remove('hidden')", element)
|
||||
if 'resume' in parent.text.lower():
|
||||
if self.resume_dir != None:
|
||||
resume_path = self.resume_dir.resolve()
|
||||
if self.resume_dir != None and resume_path.exists() and resume_path.is_file():
|
||||
element.send_keys(str(resume_path))
|
||||
else:
|
||||
self._create_and_upload_resume(element)
|
||||
elif 'cover' in parent.text.lower():
|
||||
self._create_and_upload_cover_letter(element)
|
||||
|
||||
def _create_and_upload_resume(self, element):
|
||||
"""Creates a resume in PDF format, uploads it using the upload element, and ensures cleanup of the temporary file."""
|
||||
max_retries = 3
|
||||
retry_delay = 1 # seconds
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
html_string = self.gpt_answerer.get_resume_html()
|
||||
html_string = html_string.replace('\n', '')
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
|
||||
pdf_path = temp_file.name
|
||||
|
||||
# Convert HTML to PDF
|
||||
pisa_status = pisa.CreatePDF(html_string, dest=temp_file)
|
||||
element.send_keys(pdf_path)
|
||||
if pisa_status.err:
|
||||
raise Exception(f"PDF generation failed with error: {pisa_status.err}")
|
||||
time.sleep(2)
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(retry_delay)
|
||||
else:
|
||||
tb_str = traceback.format_exc()
|
||||
raise Exception(f"Max retries reached. Upload failed: \nTraceback:\n{tb_str}")
|
||||
|
||||
finally:
|
||||
if os.path.exists(pdf_path):
|
||||
os.remove(pdf_path)
|
||||
|
||||
def _upload_resume(self, element: WebElement) -> None:
|
||||
element.send_keys(str(self.resume_dir))
|
||||
|
||||
def _create_and_upload_cover_letter(self, element: WebElement) -> None:
|
||||
cover_letter = self.gpt_answerer.answer_question_textual_wide_range("Write a cover letter")
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf_file:
|
||||
letter_path = temp_pdf_file.name
|
||||
c = canvas.Canvas(letter_path, pagesize=letter)
|
||||
width, height = letter
|
||||
text_object = c.beginText(100, height - 100)
|
||||
text_object.setFont("Helvetica", 12)
|
||||
text_object.textLines(cover_letter)
|
||||
c.drawText(text_object)
|
||||
c.save()
|
||||
element.send_keys(letter_path)
|
||||
|
||||
def _fill_additional_questions(self) -> None:
|
||||
form_sections = self.driver.find_elements(By.CLASS_NAME, 'jobs-easy-apply-form-section__grouping')
|
||||
for section in form_sections:
|
||||
self._process_question(section)
|
||||
|
||||
def _process_question(self, section: WebElement) -> None:
|
||||
if self._handle_terms_of_service(section):
|
||||
return
|
||||
self._handle_radio_question(section)
|
||||
self._handle_textbox_question(section)
|
||||
self._handle_date_question(section)
|
||||
self._handle_dropdown_question(section)
|
||||
|
||||
def _handle_terms_of_service(self, element: WebElement) -> bool:
|
||||
try:
|
||||
question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element')
|
||||
checkbox = question.find_element(By.TAG_NAME, 'label')
|
||||
question_text = question.text.lower()
|
||||
if 'terms of service' in question_text or 'privacy policy' in question_text or 'terms of use' in question_text:
|
||||
checkbox.click()
|
||||
return True
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _handle_radio_question(self, element: WebElement) -> None:
|
||||
try:
|
||||
question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element')
|
||||
radios = question.find_elements(By.CLASS_NAME, 'fb-text-selectable__option')
|
||||
if not radios:
|
||||
return
|
||||
|
||||
question_text = element.text.lower()
|
||||
options = [radio.text.lower() for radio in radios]
|
||||
|
||||
answer = self._get_answer_from_set('radio', question_text, options)
|
||||
if not answer:
|
||||
answer = self.gpt_answerer.answer_question_from_options(question_text, options)
|
||||
|
||||
self._select_radio(radios, answer)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _handle_textbox_question(self, element: WebElement) -> None:
|
||||
try:
|
||||
question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element')
|
||||
question_text = question.find_element(By.TAG_NAME, 'label').text.lower()
|
||||
text_field = self._find_text_field(question)
|
||||
|
||||
is_numeric = self._is_numeric_field(text_field)
|
||||
answer = self._get_answer_from_set('numeric' if is_numeric else 'text', question_text)
|
||||
|
||||
if not answer:
|
||||
answer = self.gpt_answerer.answer_question_numeric(question_text) if is_numeric else self.gpt_answerer.answer_question_textual_wide_range(question_text)
|
||||
|
||||
self._enter_text(text_field, answer)
|
||||
self._handle_form_errors(element, question_text, answer, text_field)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _handle_date_question(self, element: WebElement) -> None:
|
||||
try:
|
||||
date_picker = element.find_element(By.CLASS_NAME, 'artdeco-datepicker__input')
|
||||
date_picker.clear()
|
||||
date_picker.send_keys(date.today().strftime("%m/%d/%y"))
|
||||
time.sleep(3)
|
||||
date_picker.send_keys(Keys.RETURN)
|
||||
time.sleep(2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _handle_dropdown_question(self, element: WebElement) -> None:
|
||||
try:
|
||||
question = element.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element')
|
||||
question_text = question.find_element(By.TAG_NAME, 'label').text.lower()
|
||||
dropdown = question.find_element(By.TAG_NAME, 'select')
|
||||
select = Select(dropdown)
|
||||
options = [option.text for option in select.options]
|
||||
|
||||
answer = self._get_answer_from_set('dropdown', question_text, options)
|
||||
if not answer:
|
||||
answer = self.gpt_answerer.answer_question_from_options(question_text, options)
|
||||
|
||||
self._select_dropdown(dropdown, answer)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _get_answer_from_set(self, question_type: str, question_text: str, options: Optional[List[str]] = None) -> Optional[str]:
|
||||
for entry in self.set_old_answers:
|
||||
if isinstance(entry, tuple) and len(entry) == 3:
|
||||
if entry[0] == question_type and question_text in entry[1].lower():
|
||||
answer = entry[2]
|
||||
return answer if options is None or answer in options else None
|
||||
return None
|
||||
|
||||
def _find_text_field(self, question: WebElement) -> WebElement:
|
||||
try:
|
||||
return question.find_element(By.TAG_NAME, 'input')
|
||||
except NoSuchElementException:
|
||||
return question.find_element(By.TAG_NAME, 'textarea')
|
||||
|
||||
def _is_numeric_field(self, field: WebElement) -> bool:
|
||||
field_type = field.get_attribute('type').lower()
|
||||
if 'numeric' in field_type:
|
||||
return True
|
||||
class_attribute = field.get_attribute("id")
|
||||
return class_attribute and 'numeric' in class_attribute
|
||||
|
||||
def _enter_text(self, element: WebElement, text: str) -> None:
|
||||
element.clear()
|
||||
element.send_keys(text)
|
||||
|
||||
def _select_dropdown(self, element: WebElement, text: str) -> None:
|
||||
select = Select(element)
|
||||
select.select_by_visible_text(text)
|
||||
|
||||
def _select_radio(self, radios: List[WebElement], answer: str) -> None:
|
||||
for radio in radios:
|
||||
if answer in radio.text.lower():
|
||||
radio.find_element(By.TAG_NAME, 'label').click()
|
||||
return
|
||||
radios[-1].find_element(By.TAG_NAME, 'label').click()
|
||||
|
||||
def _handle_form_errors(self, element: WebElement, question_text: str, answer: str, text_field: WebElement) -> None:
|
||||
try:
|
||||
error = element.find_element(By.CLASS_NAME, 'artdeco-inline-feedback--error')
|
||||
error_text = error.text.lower()
|
||||
new_answer = self.gpt_answerer.try_fix_answer(question_text, answer, error_text)
|
||||
self._enter_text(text_field, new_answer)
|
||||
except NoSuchElementException:
|
||||
pass
|
227
linkedIn_job_manager.py
Normal file
227
linkedIn_job_manager.py
Normal file
@ -0,0 +1,227 @@
|
||||
import csv
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
import traceback
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
import utils
|
||||
from job import Job
|
||||
from linkedIn_easy_applier import LinkedInEasyApplier
|
||||
|
||||
|
||||
class EnvironmentKeys:
|
||||
def __init__(self):
|
||||
self.skip_apply = self._read_env_key_bool("SKIP_APPLY")
|
||||
self.disable_description_filter = self._read_env_key_bool("DISABLE_DESCRIPTION_FILTER")
|
||||
|
||||
@staticmethod
|
||||
def _read_env_key(key: str) -> str:
|
||||
return os.getenv(key, "")
|
||||
|
||||
@staticmethod
|
||||
def _read_env_key_bool(key: str) -> bool:
|
||||
return os.getenv(key) == "True"
|
||||
|
||||
class LinkedInJobManager:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
self.set_old_answers = set()
|
||||
self.easy_applier_component = None
|
||||
|
||||
def set_parameters(self, parameters):
|
||||
self.company_blacklist = parameters.get('companyBlacklist', []) or []
|
||||
self.title_blacklist = parameters.get('titleBlacklist', []) or []
|
||||
self.positions = parameters.get('positions', [])
|
||||
self.locations = parameters.get('locations', [])
|
||||
self.base_search_url = self.get_base_search_url(parameters)
|
||||
self.seen_jobs = []
|
||||
resume_path = parameters.get('uploads', {}).get('resume', None)
|
||||
if resume_path is not None and Path(resume_path).exists():
|
||||
self.resume_dir = Path(resume_path)
|
||||
else:
|
||||
self.resume_dir = None
|
||||
self.output_file_directory = Path(parameters['outputFileDirectory'])
|
||||
self.env_config = EnvironmentKeys()
|
||||
self.old_question()
|
||||
|
||||
def set_gpt_answerer(self, gpt_answerer):
|
||||
self.gpt_answerer = gpt_answerer
|
||||
|
||||
def old_question(self):
|
||||
"""
|
||||
Load old answers from a CSV file into a dictionary.
|
||||
"""
|
||||
self.set_old_answers = {}
|
||||
file_path = 'data_folder/output/old_Questions.csv'
|
||||
with open(file_path, 'r', newline='', encoding='utf-8', errors='ignore') as file:
|
||||
csv_reader = csv.reader(file, delimiter=',', quotechar='"')
|
||||
for row in csv_reader:
|
||||
if len(row) == 3:
|
||||
answer_type, question_text, answer = row
|
||||
self.set_old_answers[(answer_type.lower(), question_text.lower())] = answer
|
||||
|
||||
|
||||
def start_applying(self):
|
||||
self.easy_applier_component = LinkedInEasyApplier(
|
||||
self.driver, self.resume_dir, self.set_old_answers, self.gpt_answerer
|
||||
)
|
||||
searches = list(product(self.positions, self.locations))
|
||||
random.shuffle(searches)
|
||||
page_sleep = 0
|
||||
minimum_time = 60 * 15
|
||||
minimum_page_time = time.time() + minimum_time
|
||||
|
||||
for position, location in searches:
|
||||
location_url = "&location=" + location
|
||||
job_page_number = -1
|
||||
print(f"Starting the search for {position} in {location}.")
|
||||
|
||||
try:
|
||||
while True:
|
||||
page_sleep += 1
|
||||
job_page_number += 1
|
||||
print(f"Going to job page {job_page_number}")
|
||||
self.next_job_page(position, location_url, job_page_number)
|
||||
time.sleep(random.uniform(1.5, 3.5))
|
||||
print("Starting the application process for this page...")
|
||||
self.apply_jobs()
|
||||
print("Applying to jobs on this page has been completed!")
|
||||
|
||||
time_left = minimum_page_time - time.time()
|
||||
if time_left > 0:
|
||||
print(f"Sleeping for {time_left} seconds.")
|
||||
time.sleep(time_left)
|
||||
minimum_page_time = time.time() + minimum_time
|
||||
if page_sleep % 5 == 0:
|
||||
sleep_time = random.randint(5, 34)
|
||||
print(f"Sleeping for {sleep_time / 60} minutes.")
|
||||
time.sleep(sleep_time)
|
||||
page_sleep += 1
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
time_left = minimum_page_time - time.time()
|
||||
if time_left > 0:
|
||||
print(f"Sleeping for {time_left} seconds.")
|
||||
time.sleep(time_left)
|
||||
minimum_page_time = time.time() + minimum_time
|
||||
if page_sleep % 5 == 0:
|
||||
sleep_time = random.randint(50, 90)
|
||||
print(f"Sleeping for {sleep_time / 60} minutes.")
|
||||
time.sleep(sleep_time)
|
||||
page_sleep += 1
|
||||
|
||||
def apply_jobs(self):
|
||||
try:
|
||||
try:
|
||||
no_jobs_element = self.driver.find_element(By.CLASS_NAME, 'jobs-search-two-pane__no-results-banner--expand')
|
||||
if 'No matching jobs found' in no_jobs_element.text or 'unfortunately, things aren' in self.driver.page_source.lower():
|
||||
raise Exception("No more jobs on this page")
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
|
||||
job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list")
|
||||
utils.scroll_slow(self.driver, job_results)
|
||||
utils.scroll_slow(self.driver, job_results, step=300, reverse=True)
|
||||
|
||||
job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item')
|
||||
|
||||
if not job_list_elements:
|
||||
raise Exception("No job class elements found on page")
|
||||
|
||||
job_list = [Job(*self.extract_job_information_from_tile(job_element)) for job_element in job_list_elements]
|
||||
|
||||
for job in job_list:
|
||||
if self.is_blacklisted(job.title, job.company, job.link):
|
||||
print(f"Blacklisted {job.title} at {job.company}, skipping...")
|
||||
self.write_to_file(job.company, job.location, job.title, job.link, "skipped")
|
||||
continue
|
||||
|
||||
try:
|
||||
if job.apply_method not in {"Continue", "Applied", "Apply"}:
|
||||
self.easy_applier_component.job_apply(job)
|
||||
except Exception:
|
||||
self.write_to_file(job.company, job.location, job.title, job.link, "failed")
|
||||
continue
|
||||
self.write_to_file(job.company, job.location, job.title, job.link, "success")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
def write_to_file(self, company, job_title, link, job_location, file_name):
|
||||
to_write = [company, job_title, link, job_location]
|
||||
file_path = self.output_file_directory / f"{file_name}.csv"
|
||||
with open(file_path, 'a', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(to_write)
|
||||
|
||||
def record_gpt_answer(self, answer_type, question_text, gpt_response):
|
||||
to_write = [answer_type, question_text, gpt_response]
|
||||
file_path = self.output_file_directory / "registered_jobs.csv"
|
||||
try:
|
||||
with open(file_path, 'a', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(to_write)
|
||||
except Exception as e:
|
||||
print(f"Error writing registered job: {e}")
|
||||
print(f"Details: Answer type: {answer_type}, Question: {question_text}")
|
||||
|
||||
def get_base_search_url(self, parameters):
|
||||
remote_url = "f_CF=f_WRA" if parameters['remote'] else ""
|
||||
experience_url = "f_E=" + "%2C".join(
|
||||
str(i+1) for i, v in enumerate(parameters.get('experienceLevel', [])) if v
|
||||
)
|
||||
distance_url = "?distance=" + str(parameters['distance'])
|
||||
job_types_url = "f_JT=" + "%2C".join(
|
||||
k[0].upper() for k, v in parameters.get('experienceLevel', {}).items() if v
|
||||
)
|
||||
date_url = next(
|
||||
(v for k, v in {
|
||||
"all time": "", "month": "&f_TPR=r2592000", "week": "&f_TPR=r604800", "24 hours": "&f_TPR=r86400"
|
||||
}.items() if parameters.get('date', {}).get(k)), ""
|
||||
)
|
||||
easy_apply_url = "&f_LF=f_AL"
|
||||
return f"{distance_url}&{remote_url}&{job_types_url}&{experience_url}{easy_apply_url}{date_url}"
|
||||
|
||||
def next_job_page(self, position, location, job_page):
|
||||
self.driver.get(f"https://www.linkedin.com/jobs/search/{self.base_search_url}&keywords={position}{location}&start={job_page * 25}")
|
||||
|
||||
def extract_job_information_from_tile(self, job_tile):
|
||||
job_title, company, job_location, apply_method, link = "", "", "", "", ""
|
||||
try:
|
||||
job_title = job_tile.find_element(By.CLASS_NAME, 'job-card-list__title').text
|
||||
link = job_tile.find_element(By.CLASS_NAME, 'job-card-list__title').get_attribute('href').split('?')[0]
|
||||
company = job_tile.find_element(By.CLASS_NAME, 'job-card-container__primary-description').text
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
hiring_line = job_tile.find_element(By.XPATH, '//span[contains(.,\' is hiring for this\')]')
|
||||
hiring_line_text = hiring_line.text
|
||||
name_terminating_index = hiring_line_text.find(' is hiring for this')
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
job_location = job_tile.find_element(By.CLASS_NAME, 'job-card-container__metadata-item').text
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
apply_method = job_tile.find_element(By.CLASS_NAME, 'job-card-container__apply-method').text
|
||||
except:
|
||||
apply_method = "Applied"
|
||||
|
||||
return job_title, company, job_location, link, apply_method
|
||||
|
||||
def is_blacklisted(self, job_title, company, link):
|
||||
job_title_words = job_title.lower().split(' ')
|
||||
company_lower = company.lower()
|
||||
title_blacklisted = any(word in job_title_words for word in self.title_blacklist)
|
||||
company_blacklisted = company_lower in (word.lower() for word in self.company_blacklist)
|
||||
link_seen = link in self.seen_jobs
|
||||
return title_blacklisted or company_blacklisted or link_seen
|
182
main.py
Normal file
182
main.py
Normal file
@ -0,0 +1,182 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service as ChromeService
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
import click
|
||||
|
||||
from utils import chromeBrowserOptions
|
||||
from gpt import GPTAnswerer
|
||||
from linkedIn_authenticator import LinkedInAuthenticator
|
||||
from linkedIn_bot_facade import LinkedInBotFacade
|
||||
from linkedIn_job_manager import LinkedInJobManager
|
||||
from resume import Resume
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""Custom exception for configuration errors."""
|
||||
pass
|
||||
|
||||
class ConfigValidator:
|
||||
@staticmethod
|
||||
def validate_email(email: str) -> bool:
|
||||
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
return re.match(email_regex, email) is not None
|
||||
|
||||
@staticmethod
|
||||
def validate_config(config_yaml_path: Path) -> dict:
|
||||
try:
|
||||
with open(config_yaml_path, 'r') as stream:
|
||||
parameters = yaml.safe_load(stream)
|
||||
except yaml.YAMLError as exc:
|
||||
raise ConfigError(f"Error reading config file: {exc}")
|
||||
except FileNotFoundError:
|
||||
raise ConfigError(f"Config file not found: {config_yaml_path}")
|
||||
|
||||
mandatory_params = [
|
||||
'remote', 'experienceLevel',
|
||||
'jobTypes', 'date', 'positions', 'locations', 'distance'
|
||||
]
|
||||
|
||||
for param in mandatory_params:
|
||||
if param not in parameters:
|
||||
raise ConfigError(f"Missing parameter in config file: {param}")
|
||||
|
||||
if not isinstance(parameters['remote'], bool):
|
||||
raise ConfigError("'remote' must be a boolean value.")
|
||||
|
||||
experience_level = parameters.get('experienceLevel', {})
|
||||
job_types = parameters.get('jobTypes', {})
|
||||
date = parameters.get('date', {})
|
||||
|
||||
if not experience_level or not any(experience_level.values()):
|
||||
raise ConfigError("At least one experience level must be selected.")
|
||||
if not job_types or not any(job_types.values()):
|
||||
raise ConfigError("At least one job type must be selected.")
|
||||
if not date or not any(date.values()):
|
||||
raise ConfigError("At least one date filter must be selected.")
|
||||
|
||||
approved_distances = {0, 5, 10, 25, 50, 100}
|
||||
if parameters['distance'] not in approved_distances:
|
||||
raise ConfigError(f"Invalid distance value. Must be one of: {approved_distances}")
|
||||
if not parameters['positions']:
|
||||
raise ConfigError("'positions' list cannot be empty.")
|
||||
if not parameters['locations']:
|
||||
raise ConfigError("'locations' list cannot be empty.")
|
||||
return parameters
|
||||
|
||||
@staticmethod
|
||||
def validate_secrets(secrets_yaml_path: Path) -> tuple:
|
||||
try:
|
||||
with open(secrets_yaml_path, 'r') as stream:
|
||||
secrets = yaml.safe_load(stream)
|
||||
except yaml.YAMLError as exc:
|
||||
raise ConfigError(f"Error reading secrets file: {exc}")
|
||||
except FileNotFoundError:
|
||||
raise ConfigError(f"Secrets file not found: {secrets_yaml_path}")
|
||||
|
||||
mandatory_secrets = ['email', 'password', 'openai_api_key']
|
||||
|
||||
for secret in mandatory_secrets:
|
||||
if secret not in secrets:
|
||||
raise ConfigError(f"Missing secret: {secret}")
|
||||
|
||||
if not ConfigValidator.validate_email(secrets['email']):
|
||||
raise ConfigError("Invalid email format in secrets file.")
|
||||
if not secrets['password']:
|
||||
raise ConfigError("Password cannot be empty in secrets file.")
|
||||
if not secrets['openai_api_key']:
|
||||
raise ConfigError("OpenAI API key cannot be empty in secrets file.")
|
||||
|
||||
return secrets['email'], str(secrets['password']), secrets['openai_api_key']
|
||||
|
||||
class FileManager:
|
||||
@staticmethod
|
||||
def find_file(name_containing: str, with_extension: str, at_path: Path) -> Path:
|
||||
for file in at_path.iterdir():
|
||||
if name_containing.lower() in file.name.lower() and file.suffix.lower() == with_extension.lower():
|
||||
return file
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_data_folder(app_data_folder: Path) -> tuple:
|
||||
if not app_data_folder.exists() or not app_data_folder.is_dir():
|
||||
raise FileNotFoundError(f"Data folder not found: {app_data_folder}")
|
||||
|
||||
secrets_file = app_data_folder / 'secrets.yaml'
|
||||
config_file = app_data_folder / 'config.yaml'
|
||||
plain_text_resume_file = app_data_folder / 'plain_text_resume.yaml'
|
||||
|
||||
missing_files = []
|
||||
if not config_file.exists():
|
||||
missing_files.append('config.yaml')
|
||||
if not plain_text_resume_file.exists():
|
||||
missing_files.append('plain_text_resume.yaml')
|
||||
|
||||
if missing_files:
|
||||
raise FileNotFoundError(f"Missing files in the data folder: {', '.join(missing_files)}")
|
||||
|
||||
output_folder = app_data_folder / 'output'
|
||||
output_folder.mkdir(exist_ok=True)
|
||||
return secrets_file, config_file, plain_text_resume_file, output_folder
|
||||
|
||||
@staticmethod
|
||||
def file_paths_to_dict(resume_file: Path, plain_text_resume_file: Path) -> dict:
|
||||
if not resume_file.exists():
|
||||
raise FileNotFoundError(f"Resume file not found: {resume_file}")
|
||||
if not plain_text_resume_file.exists():
|
||||
raise FileNotFoundError(f"Plain text resume file not found: {plain_text_resume_file}")
|
||||
return {'resume': resume_file, 'plainTextResume': plain_text_resume_file}
|
||||
|
||||
def init_browser():
|
||||
try:
|
||||
options = chromeBrowserOptions()
|
||||
service = ChromeService(ChromeDriverManager().install())
|
||||
return webdriver.Chrome(service=service, options=options)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to initialize browser: {str(e)}")
|
||||
|
||||
def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str):
|
||||
try:
|
||||
browser = init_browser()
|
||||
login_component = LinkedInAuthenticator(browser)
|
||||
apply_component = LinkedInJobManager(browser)
|
||||
gpt_answerer_component = GPTAnswerer(openai_api_key)
|
||||
|
||||
with open(parameters['uploads']['plainTextResume'], "r") as file:
|
||||
plain_text_resume_file = file.read()
|
||||
|
||||
resume_object = Resume(plain_text_resume_file)
|
||||
bot = LinkedInBotFacade(login_component, apply_component)
|
||||
bot.set_secrets(email, password)
|
||||
bot.set_resume(resume_object)
|
||||
bot.set_gpt_answerer(gpt_answerer_component)
|
||||
bot.set_parameters(parameters)
|
||||
bot.start_login()
|
||||
bot.start_apply()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error running the bot: {str(e)}")
|
||||
|
||||
@click.command()
|
||||
@click.option('--resume', type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), help="Path to the resume PDF file")
|
||||
def main(resume: Path = None):
|
||||
try:
|
||||
data_folder = Path("data_folder")
|
||||
secrets_file, config_file, plain_text_resume_file, output_folder = FileManager.validate_data_folder(data_folder)
|
||||
parameters = ConfigValidator.validate_config(config_file)
|
||||
email, password, openai_api_key = ConfigValidator.validate_secrets(secrets_file)
|
||||
parameters['uploads'] = FileManager.file_paths_to_dict(resume, plain_text_resume_file)
|
||||
parameters['outputFileDirectory'] = output_folder
|
||||
|
||||
create_and_run_bot(email, password, parameters, openai_api_key)
|
||||
except ConfigError as ce:
|
||||
print(f"Configuration error: {str(ce)}")
|
||||
except FileNotFoundError as fnf:
|
||||
print(f"File not found: {str(fnf)}")
|
||||
except RuntimeError as re:
|
||||
print(f"Runtime error: {str(re)}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
456
open_ai_calls.json
Normal file
456
open_ai_calls.json
Normal file
File diff suppressed because one or more lines are too long
157
readme.md
Normal file
157
readme.md
Normal file
@ -0,0 +1,157 @@
|
||||
# LinkedIn_AIHawk
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Features](#features)
|
||||
3. [Installation](#installation)
|
||||
4. [Configuration](#configuration)
|
||||
5. [Usage](#usage)
|
||||
6. [Optional Resume Feature](#optional-resume-feature)
|
||||
7. [Documentation](#documentation)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
9. [Contributors](#contributors)
|
||||
10. [License](#license)
|
||||
11. [Conclusion](#conclusion)
|
||||
|
||||
## Introduction
|
||||
|
||||
LinkedIn_AIHawk is a cutting-edge, automated tool designed to revolutionize the job search and application process on LinkedIn. In today's fiercely competitive job market, where opportunities can vanish in the blink of an eye, this program offers job seekers a significant advantage. By leveraging the power of automation and artificial intelligence, LinkedIn_AIHawk enables users to apply to a vast number of relevant positions efficiently and in a personalized manner, maximizing their chances of landing their dream job.
|
||||
|
||||
### The Challenge of Modern Job Hunting
|
||||
|
||||
In the digital age, the job search landscape has undergone a dramatic transformation. While online platforms like LinkedIn have opened up a world of opportunities, they have also intensified competition. Job seekers often find themselves spending countless hours scrolling through listings, tailoring applications, and repetitively filling out forms. This process can be not only time-consuming but also emotionally draining, leading to job search fatigue and missed opportunities.
|
||||
|
||||
### Enter LinkedIn_AIHawk: Your Personal Job Search Assistant
|
||||
|
||||
LinkedIn_AIHawk steps in as a game-changing solution to these challenges. It's not just a tool; it's your tireless, 24/7 job search partner. By automating the most time-consuming aspects of the job search process, it allows you to focus on what truly matters - preparing for interviews and developing your professional skills.
|
||||
|
||||
## Features
|
||||
|
||||
1. **Intelligent Job Search Automation**
|
||||
- Customizable search criteria
|
||||
- Continuous scanning for new openings
|
||||
- Smart filtering to exclude irrelevant listings
|
||||
|
||||
2. **Rapid and Efficient Application Submission**
|
||||
- One-click applications using LinkedIn's "Easy Apply" feature
|
||||
- Form auto-fill using your profile information
|
||||
- Automatic document attachment (resume, cover letter)
|
||||
|
||||
3. **AI-Powered Personalization**
|
||||
- Dynamic response generation for employer-specific questions
|
||||
- Tone and style matching to fit company culture
|
||||
- Keyword optimization for improved application relevance
|
||||
|
||||
4. **Volume Management with Quality**
|
||||
- Bulk application capability
|
||||
- Quality control measures
|
||||
- Detailed application tracking
|
||||
|
||||
5. **Intelligent Filtering and Blacklisting**
|
||||
- Company blacklist to avoid unwanted employers
|
||||
- Title filtering to focus on relevant positions
|
||||
|
||||
6. **Dynamic Resume Generation**
|
||||
- Automatically creates tailored resumes for each application
|
||||
- Customizes resume content based on job requirements
|
||||
|
||||
7. **Secure Data Handling**
|
||||
- Manages sensitive information securely using YAML files
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/your-repo/LinkedInJobBot.git
|
||||
cd LinkedInJobBot
|
||||
```
|
||||
|
||||
2. **Install the required packages:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Install ChromeDriver:**
|
||||
```bash
|
||||
python -c "from webdriver_manager.chrome import ChromeDriverManager; ChromeDriverManager().install()"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
LinkedIn_AIHawk relies on three main configuration files:
|
||||
|
||||
### 1. secrets.yaml
|
||||
|
||||
Contains sensitive information. Never share or commit this file to version control.
|
||||
|
||||
- `email`: Your LinkedIn account email
|
||||
- `password`: Your LinkedIn account password
|
||||
- `openai_api_key`: Your OpenAI API key for GPT integration
|
||||
|
||||
### 2. config.yaml
|
||||
|
||||
Defines your job search parameters and bot behavior.
|
||||
|
||||
- `remote`: Set to `true` to include remote jobs, `false` to exclude them
|
||||
- `experienceLevel`: Set desired experience levels to `true`
|
||||
- `jobTypes`: Set desired job types to `true`
|
||||
- `date`: Choose one time range for job postings
|
||||
- `positions`: List job titles you're interested in
|
||||
- `locations`: List locations you want to search in
|
||||
- `distance`: Set the radius for your job search (in miles)
|
||||
- `companyBlacklist`: List companies you want to exclude from your search
|
||||
- `titleBlacklist`: List keywords in job titles you want to avoid
|
||||
|
||||
### 3. plain_text_resume_template.yaml
|
||||
|
||||
Contains your resume information in a structured format. Fill it out with your personal details, education, work experience, and skills. This information is used to auto-fill application forms and generate customized resumes.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Prepare the Data Folder:**
|
||||
Ensure that your data_folder contains the following files:
|
||||
- `secrets.yaml`
|
||||
- `config.yaml`
|
||||
- `plain_text_resume.yaml`
|
||||
- `resume.pdf` (optional)
|
||||
|
||||
2. **Run the Bot:**
|
||||
```bash
|
||||
python main.py [--resume PATH_TO_RESUME_PDF]
|
||||
```
|
||||
|
||||
## Optional Resume Feature
|
||||
|
||||
LinkedIn_AIHawk offers flexibility in how it handles your resume:
|
||||
|
||||
- **Using a Specific Resume:**
|
||||
If you want to use a specific PDF resume for all applications, run the bot with the `--resume` option:
|
||||
```bash
|
||||
python main.py --resume /path/to/your/resume.pdf
|
||||
```
|
||||
|
||||
- **Dynamic Resume Generation:**
|
||||
If you don't use the `--resume` option, the bot will automatically generate a unique resume for each application. This feature uses the information from your `plain_text_resume.yaml` file and tailors it to each specific job application, potentially increasing your chances of success by customizing your resume for each position.
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed information on each component and their respective roles, please refer to the [Documentation](docs/documentation.md) file.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **ChromeDriver Issues:** Ensure ChromeDriver is compatible with your installed Chrome version.
|
||||
- **Missing Files:** Verify that all necessary files are present in the data folder.
|
||||
- **Invalid YAML:** Check your YAML files for syntax errors.
|
||||
|
||||
## Contributors
|
||||
|
||||
- [feder-cr](https://github.com/your-github-profile) - Creator and Maintainer
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Conclusion
|
||||
|
||||
LinkedIn_AIHawk provides a significant advantage in the modern job market by automating and enhancing the job application process. With features like dynamic resume generation and AI-powered personalization, it offers unparalleled flexibility and efficiency. Whether you're a job seeker aiming to maximize your chances of landing a job, a recruiter looking to streamline application submissions, or a career advisor seeking to offer better services, LinkedIn_AIHawk is an invaluable resource. By leveraging cutting-edge automation and artificial intelligence, this tool not only saves time but also significantly increases the effectiveness and quality of job applications in today's competitive landscape.
|
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
127
resume.py
Normal file
127
resume.py
Normal file
@ -0,0 +1,127 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
import yaml
|
||||
|
||||
@dataclass
|
||||
class PersonalInformation:
|
||||
name: str
|
||||
surname: str
|
||||
dateOfBirth: str
|
||||
country: str
|
||||
city: str
|
||||
address: str
|
||||
phone: str
|
||||
phonePrefix: str
|
||||
email: str
|
||||
github: str
|
||||
linkedin: str
|
||||
|
||||
@dataclass
|
||||
class SelfIdentification:
|
||||
gender: str
|
||||
pronouns: str
|
||||
veteran: str
|
||||
disability: str
|
||||
ethnicity: str
|
||||
|
||||
@dataclass
|
||||
class LegalAuthorization:
|
||||
euWorkAuthorization: str
|
||||
usWorkAuthorization: str
|
||||
requiresUsVisa: str
|
||||
legallyAllowedToWorkInUs: str
|
||||
requiresUsSponsorship: str
|
||||
requiresEuVisa: str
|
||||
legallyAllowedToWorkInEu: str
|
||||
requiresEuSponsorship: str
|
||||
|
||||
@dataclass
|
||||
class WorkPreferences:
|
||||
remoteWork: str
|
||||
inPersonWork: str
|
||||
openToRelocation: str
|
||||
willingToCompleteAssessments: str
|
||||
willingToUndergoDrugTests: str
|
||||
willingToUndergoBackgroundChecks: str
|
||||
|
||||
@dataclass
|
||||
class Education:
|
||||
degree: str
|
||||
university: str
|
||||
gpa: str
|
||||
graduationYear: str
|
||||
fieldOfStudy: str
|
||||
skillsAcquired: Dict[str, str]
|
||||
|
||||
@dataclass
|
||||
class Experience:
|
||||
position: str
|
||||
company: str
|
||||
employmentPeriod: str
|
||||
location: str
|
||||
industry: str
|
||||
keyResponsibilities: Dict[str, str]
|
||||
skillsAcquired: Dict[str, str]
|
||||
|
||||
@dataclass
|
||||
class Availability:
|
||||
noticePeriod: str
|
||||
|
||||
@dataclass
|
||||
class SalaryExpectations:
|
||||
salaryRangeUSD: str
|
||||
|
||||
@dataclass
|
||||
class Language:
|
||||
language: str
|
||||
proficiency: str
|
||||
|
||||
class Resume:
|
||||
def __init__(self, yaml_str: str):
|
||||
data = yaml.safe_load(yaml_str)
|
||||
self.personal_information = PersonalInformation(**data['personal_information'])
|
||||
self.self_identification = SelfIdentification(**data['self_identification'])
|
||||
self.legal_authorization = LegalAuthorization(**data['legal_authorization'])
|
||||
self.work_preferences = WorkPreferences(**data['work_preferences'])
|
||||
self.education_details = [Education(**edu) for edu in data['education_details']]
|
||||
self.experience_details = [Experience(**exp) for exp in data['experience_details']]
|
||||
self.projects = data['projects']
|
||||
self.availability = Availability(**data['availability'])
|
||||
self.salary_expectations = SalaryExpectations(**data['salary_expectations'])
|
||||
self.certifications = data['certifications']
|
||||
self.languages = [Language(**lang) for lang in data['languages']]
|
||||
self.interests = data['interests']
|
||||
|
||||
def __str__(self):
|
||||
def format_dict(dict_obj):
|
||||
return "\n".join(f"{key}: {value}" for key, value in dict_obj.items())
|
||||
|
||||
def format_dataclass(obj):
|
||||
return "\n".join(f"{field.name}: {getattr(obj, field.name)}" for field in obj.__dataclass_fields__.values())
|
||||
|
||||
return ("Personal Information:\n" + format_dataclass(self.personal_information) + "\n\n"
|
||||
"Self Identification:\n" + format_dataclass(self.self_identification) + "\n\n"
|
||||
"Legal Authorization:\n" + format_dataclass(self.legal_authorization) + "\n\n"
|
||||
"Work Preferences:\n" + format_dataclass(self.work_preferences) + "\n\n"
|
||||
"Education Details:\n" + "\n".join(
|
||||
f" - {edu.degree} in {edu.fieldOfStudy} from {edu.university}, "
|
||||
f"GPA: {edu.gpa}, Graduation Year: {edu.graduationYear}\n"
|
||||
f" Skills Acquired:\n{format_dict(edu.skillsAcquired)}"
|
||||
for edu in self.education_details
|
||||
) + "\n\n"
|
||||
"Experience Details:\n" + "\n".join(
|
||||
f" - {exp.position} at {exp.company} ({exp.employmentPeriod}), {exp.location}, {exp.industry}\n"
|
||||
f" Key Responsibilities:\n{format_dict(exp.keyResponsibilities)}\n"
|
||||
f" Skills Acquired:\n{format_dict(exp.skillsAcquired)}"
|
||||
for exp in self.experience_details
|
||||
) + "\n\n"
|
||||
"Projects:\n" + "\n".join(f" - {proj}" for proj in self.projects.values()) + "\n\n"
|
||||
f"Availability: {self.availability.noticePeriod}\n\n"
|
||||
f"Salary Expectations: {self.salary_expectations.salaryRangeUSD}\n\n"
|
||||
"Certifications: " + ", ".join(self.certifications) + "\n\n"
|
||||
"Languages:\n" + "\n".join(
|
||||
f" - {lang.language} ({lang.proficiency})"
|
||||
for lang in self.languages
|
||||
) + "\n\n"
|
||||
"Interests:\n" + ", ".join(self.interests)
|
||||
)
|
539
strings.py
Normal file
539
strings.py
Normal file
@ -0,0 +1,539 @@
|
||||
prepare_info_template = """
|
||||
**Prompt for HR Expert and Resume Writer:**
|
||||
|
||||
Act as an HR expert and skilled resume writer specializing in creating ATS-compatible resumes. Your task is to identify and outline the key skills and requirements necessary for the position of this job, using the provided job description and resume. Use the job description as input to extract all relevant information and optimize the resume to highlight the relevant skills and experiences for the role.
|
||||
|
||||
### Information to Collect and Analyze
|
||||
- **Resume:**
|
||||
{resume}
|
||||
|
||||
- **Job Description:**
|
||||
{job_description}
|
||||
|
||||
### Analysis and Planning
|
||||
|
||||
1. **Analyze the Job Description**:
|
||||
- Identify the required technical and soft skills.
|
||||
- List the essential educational qualifications and certifications.
|
||||
- Describe the relevant work experiences.
|
||||
- Reflect on the role's evolution, considering future trends.
|
||||
|
||||
2. **Analyze the Current Resume**:
|
||||
- Identify the existing skills and experiences in the resume.
|
||||
|
||||
3. **Optimize the Resume**:
|
||||
- Plan the resume to highlight experiences and skills relevant to the job requirements.
|
||||
- Ensure it includes pertinent keywords, a clear structure, and is tailored to emphasize the candidate's strengths and achievements.
|
||||
- Avoid including information not requested by the job description.
|
||||
|
||||
### Creating a "Smart" Resume
|
||||
|
||||
- **ATS Compatibility**: Ensure the resume is optimized for applicant tracking systems (ATS) by including relevant keywords.
|
||||
- **Adaptation to the Job Description**: Strategically tailor the resume to reflect the skills and experiences required by the job description.
|
||||
- **Highlighting Strengths**: Customize the resume to showcase the most relevant skills, experiences, and achievements for the role.
|
||||
- **Clear Structure**: Use a clear and readable structure.
|
||||
- **Showcasing Experiences and Achievements**: Provide guidance on effectively presenting experience, skills, and achievements in a compelling and professional manner.
|
||||
- **Formatting and Design**: Offer advice on formatting and design to maintain readability and professionalism, ensuring the resume stands out in a competitive job market.
|
||||
|
||||
### Final Result
|
||||
|
||||
Your analysis and the optimized resume should be structured in a clear and organized document, with distinct sections for each point listed above. Each section should contain:
|
||||
|
||||
|
||||
The results should be provided in **Markdown** format, Provide only the markdown code for the resume, without any explanations or additional text and also without ```md ```
|
||||
"""
|
||||
|
||||
format_resume_template = """
|
||||
Transform the provided Markdown output into HTML format. Ensure that the HTML document uses a simple, clear style that maintains ATS compatibility and is easy for recruiters to read.
|
||||
|
||||
### HTML Styling and Design Guidelines
|
||||
|
||||
1. **Font and Color Scheme**:
|
||||
- Use a standard sans-serif font such as "Arial" or "Verdana."
|
||||
- Ensure dark text on a light background for optimal readability.
|
||||
|
||||
2. **Headings and Titles**:
|
||||
- **Main Titles**:
|
||||
- Font: Arial, sans-serif
|
||||
- Font Size: 16 px
|
||||
- Color: Black
|
||||
- Alignment: Left-aligned
|
||||
- Spacing: 20 px above and below the title
|
||||
- Formatting: Bold
|
||||
|
||||
- **Subtitles**:
|
||||
- Font: Arial, sans-serif
|
||||
- Font Size: 14 px
|
||||
- Color: Black
|
||||
- Alignment: Left-aligned
|
||||
- Spacing: 10 px above and below the subtitle
|
||||
- Formatting: Bold
|
||||
|
||||
3. **Spacing and Margins**:
|
||||
- General Margins: 1 inch (2.54 cm) on all sides
|
||||
- Spacing between Titles and Content:
|
||||
- After Main Titles: 20 px of spacing
|
||||
- After Subtitles: 10 px of spacing
|
||||
- Text Alignment: Left-aligned
|
||||
|
||||
4. **Color Matching**:
|
||||
- Use a consistent color scheme for headings and body text.
|
||||
- Avoid bright colors and multiple shades to ensure clarity.
|
||||
|
||||
### Output Requirements
|
||||
|
||||
- **Convert Markdown to HTML**: Use the provided content and apply the specified styles.
|
||||
- **Apply Styling**: Follow the guidelines for font, size, color, and spacing.
|
||||
- **Ensure ATS Compatibility**: Maintain clear formatting to ensure the document is easily parsed by ATS systems.
|
||||
|
||||
Provide only the HTML code for the resume, without any explanations or additional text and also without ```HTML ```, Ensure the final HTML document is simple, professional, and optimized for ATS.
|
||||
|
||||
Resume in Markdown format:
|
||||
{formatted_resume}
|
||||
"""
|
||||
|
||||
review_and_optimize_template = """
|
||||
Act as an HR expert and resume writer. Your task is to meticulously review and optimize the resume to ensure it stands out in a competitive job market.
|
||||
|
||||
### Tasks:
|
||||
|
||||
- **Proofreading:**
|
||||
- Carefully check for spelling, grammar, and punctuation errors.
|
||||
|
||||
- **Enhance Clarity and Impact:**
|
||||
- Improve the clarity and impact of the content.
|
||||
- Refine the language use and optimize the structure to highlight the candidate’s strengths and achievements.
|
||||
- Ensure there is a clear visual distinction between section titles and the content.
|
||||
|
||||
- **Final Output:**
|
||||
- Ensure the resume is professionally polished, visually appealing, and formatted correctly in HTML.
|
||||
- Ensure that section titles are centered.
|
||||
- Ensure there is a noticeable visual difference between section titles and the content.
|
||||
- Remove any unnecessary content, such as "Salary Expectations: €50,000."
|
||||
- Make sure the resume does not contain any placeholder text or irrelevant information.
|
||||
|
||||
**Resume:**
|
||||
{final_resume_html}
|
||||
|
||||
Provide only the HTML code for the resume, without any explanations or additional text and also without ```HTML ```
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Personal Information Template
|
||||
personal_information_template = """
|
||||
Answer the following question based on the provided personal information.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: John Doe, born on 01/01/1990, living in Milan, Italy.
|
||||
Question: What is your city?
|
||||
Milan
|
||||
|
||||
Personal Information: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# Personal Information Template
|
||||
personal_information_template = """
|
||||
Answer the following question based on the provided personal information.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: John Doe, born on 01/01/1990, living in Milan, Italy.
|
||||
Question: What is your city?
|
||||
Milan
|
||||
|
||||
Personal Information: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Self Identification Template
|
||||
self_identification_template = """
|
||||
Answer the following question based on the provided self-identification details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: Male, uses he/him pronouns, not a veteran, no disability.
|
||||
Question: What are your gender?
|
||||
Male
|
||||
|
||||
Self-Identification: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Legal Authorization Template
|
||||
legal_authorization_template = """
|
||||
Answer the following question based on the provided legal authorization details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: Authorized to work in the EU, no US visa required.
|
||||
Question: Are you legally allowed to work in the EU?
|
||||
Yes
|
||||
|
||||
Legal Authorization: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Work Preferences Template
|
||||
work_preferences_template = """
|
||||
Answer the following question based on the provided work preferences.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
|
||||
## Example
|
||||
My resume: Open to remote work, willing to relocate.
|
||||
Question: Are you open to remote work?
|
||||
Yes
|
||||
|
||||
Work Preferences: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Education Details Template
|
||||
education_details_template = """
|
||||
Answer the following question based on the provided education details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Bachelor's degree in Computer Science with experience in Python.
|
||||
Question: Do you have experience with Python?
|
||||
Yes, I have experience with Python.
|
||||
|
||||
Education Details: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Experience Details Template
|
||||
experience_details_template = """
|
||||
Answer the following question based on the provided experience details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: 3 years as a software developer with leadership experience.
|
||||
Question: Do you have leadership experience?
|
||||
Yes, I have 3 years of leadership experience.
|
||||
|
||||
Experience Details: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Projects Template
|
||||
projects_template = """
|
||||
Answer the following question based on the provided project details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Led the development of a mobile app, repository available.
|
||||
Question: Have you led any projects?
|
||||
Yes, led the development of a mobile app
|
||||
|
||||
Projects: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Availability Template
|
||||
availability_template = """
|
||||
Answer the following question based on the provided availability details.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- Keep the answer under 140 characters.
|
||||
- Use periods only if the answer has multiple sentences.
|
||||
|
||||
## Example
|
||||
My resume: Available to start immediately.
|
||||
Question: When can you start?
|
||||
I can start immediately.
|
||||
|
||||
Availability: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Salary Expectations Template
|
||||
salary_expectations_template = """
|
||||
Answer the following question based on the provided salary expectations.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- Keep the answer under 140 characters.
|
||||
- Use periods only if the answer has multiple sentences.
|
||||
|
||||
## Example
|
||||
My resume: Looking for a salary in the range of 50k-60k USD.
|
||||
Question: What are your salary expectations?
|
||||
55000.
|
||||
|
||||
Salary Expectations: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Certifications Template
|
||||
certifications_template = """
|
||||
Answer the following question based on the provided certifications.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Certified in Project Management Professional (PMP).
|
||||
Question: Do you have PMP certification?
|
||||
Yes, I am PMP certified.
|
||||
|
||||
Certifications: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Languages Template
|
||||
languages_template = """
|
||||
Answer the following question based on the provided language skills.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If it seems likely that you have the experience, even if not explicitly defined, answer as if you have the experience.
|
||||
- If unsure, respond with "I have no experience with that, but I learn fast" or "Not yet, but willing to learn."
|
||||
- Keep the answer under 140 characters.
|
||||
|
||||
## Example
|
||||
My resume: Fluent in Italian and English.
|
||||
Question: What languages do you speak?
|
||||
Fluent in Italian and English.
|
||||
|
||||
Languages: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
# Interests Template
|
||||
interests_template = """
|
||||
Answer the following question based on the provided interests.
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- Keep the answer under 140 characters.
|
||||
- Use periods only if the answer has multiple sentences.
|
||||
|
||||
## Example
|
||||
My resume: Interested in AI and data science.
|
||||
Question: What are your interests?
|
||||
AI and data science.
|
||||
|
||||
Interests: {resume_section}
|
||||
Question: {question}
|
||||
"""
|
||||
|
||||
summarize_prompt_template = """
|
||||
As a seasoned HR expert, your task is to identify and outline the key skills and requirements necessary for the position of this job. Use the provided job description as input to extract all relevant information. This will involve conducting a thorough analysis of the job's responsibilities and the industry standards. You should consider both the technical and soft skills needed to excel in this role. Additionally, specify any educational qualifications, certifications, or experiences that are essential. Your analysis should also reflect on the evolving nature of this role, considering future trends and how they might affect the required competencies.
|
||||
|
||||
Rules:
|
||||
Remove boilerplate text
|
||||
Include only relevant information to match the job description against the resume
|
||||
|
||||
# Analysis Requirements
|
||||
Your analysis should include the following sections:
|
||||
Technical Skills: List all the specific technical skills required for the role based on the responsibilities described in the job description.
|
||||
Soft Skills: Identify the necessary soft skills, such as communication abilities, problem-solving, time management, etc.
|
||||
Educational Qualifications and Certifications: Specify the essential educational qualifications and certifications for the role.
|
||||
Professional Experience: Describe the relevant work experiences that are required or preferred.
|
||||
Role Evolution: Analyze how the role might evolve in the future, considering industry trends and how these might influence the required skills.
|
||||
|
||||
# Final Result:
|
||||
Your analysis should be structured in a clear and organized document with distinct sections for each of the points listed above. Each section should contain:
|
||||
This comprehensive overview will serve as a guideline for the recruitment process, ensuring the identification of the most qualified candidates.
|
||||
|
||||
# Job Description:
|
||||
```
|
||||
{text}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Job Description Summary"""
|
||||
|
||||
|
||||
coverletter_template = """
|
||||
The following is a resume, a job description, and an answered question using this information, being answered by the person who's resume it is (first person).
|
||||
|
||||
## Rules
|
||||
- Answer questions directly.
|
||||
- If seems likely that you have the experience, even if is not explicitly defined, answer as if you have the experience.
|
||||
- Find relations between the job description and the resume, and answer questions about that.
|
||||
- Only add periods if the answer has multiple sentences/paragraphs.
|
||||
- If the question is "cover letter," answer with a cover letter based on job_description, but using my resume details
|
||||
|
||||
## Job Description:
|
||||
```
|
||||
{job_description}
|
||||
```
|
||||
|
||||
## My resume:
|
||||
```
|
||||
{resume}
|
||||
```
|
||||
|
||||
## Question:
|
||||
{question}
|
||||
|
||||
## """
|
||||
|
||||
|
||||
resume_stuff_template = """
|
||||
The following is a resume, personal data, and an answered question using this information, being answered by the person who's resume it is (first person).
|
||||
|
||||
## Rules
|
||||
- Answer questions directly
|
||||
- If seems likely that you have the experience, even if is not explicitly defined, answer as if you have the experience
|
||||
- If you cannot answer the question, answer things like "I have no experience with that, but I learn fast, very fast", "not yet, but I will learn"...
|
||||
- The answer must not be longer than a tweet (140 characters)
|
||||
- Only add periods if the answer has multiple sentences/paragraphs
|
||||
|
||||
## Example 1
|
||||
My resume: I'm a software engineer with 10 years of experience in swift .
|
||||
Question: What is your experience with swift?
|
||||
10 years
|
||||
|
||||
-----
|
||||
|
||||
## My resume:
|
||||
```
|
||||
{resume}
|
||||
```
|
||||
|
||||
## Question:
|
||||
{question}
|
||||
|
||||
## """
|
||||
|
||||
|
||||
numeric_question_template = """The following is a resume and an answered question about the resume, being answered by the person who's resume it is (first person).
|
||||
|
||||
## Rules
|
||||
- Answer the question directly (only number).
|
||||
- Regarding work experience just check the Experience Details -> Skills Acquired section.
|
||||
- Regarding experience in general just check the section Experience Details -> Skills Acquired and also Education Details -> Skills Acquired.
|
||||
- If it seems likely that you have the experience based on the resume, even if not explicitly stated on the resume, answer as if you have the experience.
|
||||
- If you cannot answer the question, provide answers like "I have no experience with that, but I learn fast, very fast", "not yet, but I will learn".
|
||||
- The answer must not be larger than a tweet (140 characters).
|
||||
|
||||
## Example
|
||||
My resume: I'm a software engineer with 10 years of experience on both swift and python.
|
||||
Question: how much years experience with swift?
|
||||
10
|
||||
|
||||
-----
|
||||
|
||||
## My resume:
|
||||
```
|
||||
{resume}
|
||||
```
|
||||
|
||||
## Question:
|
||||
{question}
|
||||
|
||||
## """
|
||||
|
||||
options_template = """The following is a resume and an answered question about the resume, the answer is one of the options.
|
||||
|
||||
## Rules
|
||||
- Never choose the default/placeholder option, examples are: 'Select an option', 'None', 'Choose from the options below', etc.
|
||||
- The answer must be one of the options.
|
||||
- The answer must exclusively contain one of the options.
|
||||
|
||||
## Example
|
||||
My resume: I'm a software engineer with 10 years of experience on swift, python, C, C++.
|
||||
Question: How many years of experience do you have on python?
|
||||
Options: [1-2, 3-5, 6-10, 10+]
|
||||
10+
|
||||
|
||||
-----
|
||||
|
||||
## My resume:
|
||||
```
|
||||
{resume}
|
||||
```
|
||||
|
||||
## Question:
|
||||
{question}
|
||||
|
||||
## Options:
|
||||
{options}
|
||||
|
||||
## """
|
||||
|
||||
|
||||
try_to_fix_template = """\
|
||||
The objective is to fix the text of a form input on a web page.
|
||||
|
||||
## Rules
|
||||
- Use the error to fix the original text.
|
||||
- The error "Please enter a valid answer" usually means the text is too large, shorten the reply to less than a tweet.
|
||||
- For errors like "Enter a whole number between 3 and 30", just need a number.
|
||||
|
||||
-----
|
||||
|
||||
## Form Question
|
||||
{question}
|
||||
|
||||
## Input
|
||||
{input}
|
||||
|
||||
## Error
|
||||
{error}
|
||||
|
||||
## Fixed Input
|
||||
"""
|
||||
|
||||
func_summarize_prompt_template = """
|
||||
Following are two texts, one with placeholders and one without, the second text uses information from the first text to fill the placeholders.
|
||||
|
||||
## Rules
|
||||
- A placeholder is a string like "[[placeholder]]". E.g. "[[company]]", "[[job_title]]", "[[years_of_experience]]"...
|
||||
- The task is to remove the placeholders from the text.
|
||||
- If there is no information to fill a placeholder, remove the placeholder, and adapt the text accordingly.
|
||||
- No placeholders should remain in the text.
|
||||
|
||||
## Example
|
||||
Text with placeholders: "I'm a software engineer engineer with 10 years of experience on [placeholder] and [placeholder]."
|
||||
Text without placeholders: "I'm a software engineer with 10 years of experience."
|
||||
|
||||
-----
|
||||
|
||||
## Text with placeholders:
|
||||
{text_with_placeholders}
|
||||
|
||||
## Text without placeholders:"""
|
68
utils.py
Normal file
68
utils.py
Normal file
@ -0,0 +1,68 @@
|
||||
import random
|
||||
import time
|
||||
from selenium import webdriver
|
||||
|
||||
headless = False
|
||||
chromeProfilePath = r"/home/.config/google-chrome/linkedin_profile"
|
||||
|
||||
def is_scrollable(element):
|
||||
"""Controlla se un elemento è scrollabile."""
|
||||
scroll_height = element.get_attribute("scrollHeight")
|
||||
client_height = element.get_attribute("clientHeight")
|
||||
return int(scroll_height) > int(client_height)
|
||||
|
||||
def scroll_slow(driver, scrollable_element, start=0, end=3600, step=100, reverse=False):
|
||||
if reverse:
|
||||
start, end = end, start
|
||||
step = -step
|
||||
|
||||
if step == 0:
|
||||
raise ValueError("Step cannot be zero.")
|
||||
|
||||
# Script di scrolling che utilizza scrollTop
|
||||
script_scroll_to = "arguments[0].scrollTop = arguments[1];"
|
||||
|
||||
try:
|
||||
if scrollable_element.is_displayed():
|
||||
if not is_scrollable(scrollable_element):
|
||||
print("The element is not scrollable.")
|
||||
return
|
||||
if (step > 0 and start >= end) or (step < 0 and start <= end):
|
||||
print("No scrolling will occur due to incorrect start/end values.")
|
||||
return
|
||||
for position in range(start, end, step):
|
||||
try:
|
||||
driver.execute_script(script_scroll_to, scrollable_element, position)
|
||||
except Exception as e:
|
||||
print(f"Error during scrolling: {e}")
|
||||
time.sleep(random.uniform(1.0, 2.6))
|
||||
driver.execute_script(script_scroll_to, scrollable_element, end)
|
||||
time.sleep(1)
|
||||
else:
|
||||
print("The element is not visible.")
|
||||
except Exception as e:
|
||||
print(f"Exception occurred: {e}")
|
||||
|
||||
def chromeBrowserOptions():
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument('--no-sandbox')
|
||||
options.add_argument("--ignore-certificate-errors")
|
||||
options.add_argument("--disable-extensions")
|
||||
options.add_argument('--disable-gpu')
|
||||
options.add_argument('--disable-dev-shm-usage')
|
||||
options.add_argument('--remote-debugging-port=9222')
|
||||
if(headless):
|
||||
options.add_argument("--headless")
|
||||
options.add_argument("--start-maximized")
|
||||
options.add_argument("--disable-blink-features")
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_experimental_option('useAutomationExtension', False)
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
if(len(chromeProfilePath)>0):
|
||||
initialPath = chromeProfilePath[0:chromeProfilePath.rfind("/")]
|
||||
profileDir = chromeProfilePath[chromeProfilePath.rfind("/")+1:]
|
||||
options.add_argument('--user-data-dir=' +initialPath)
|
||||
options.add_argument("--profile-directory=" +profileDir)
|
||||
else:
|
||||
options.add_argument("--incognito")
|
||||
return options
|
Loading…
Reference in New Issue
Block a user