This commit is contained in:
feder-cr 2024-08-23 20:02:45 +01:00
parent 27100f3455
commit 06b176aa37
10 changed files with 15 additions and 18 deletions

327
src/gpt.py Normal file
View file

@ -0,0 +1,327 @@
import json
import os
import re
import textwrap
from datetime import datetime
from typing import Dict, List
from pathlib import Path
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 src.strings as 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(Path("data_folder/output"), "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:
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:
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 set_job_application_profile(self, job_application_profile):
self.job_application_profile = job_application_profile
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 _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
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),
"cover_letter": self._create_chain(strings.coverletter_template),
}
section_prompt = """
You are assisting a bot designed to automatically apply for jobs on LinkedIn. The bot receives various questions about job applications and needs to determine the most relevant section of the resume to provide an accurate response.
For the following question: '{question}', determine which section of the resume is most relevant.
Respond with exactly one of the following options:
- Personal information
- Self Identification
- Legal Authorization
- Work Preferences
- Education Details
- Experience Details
- Projects
- Availability
- Salary Expectations
- Certifications
- Languages
- Interests
- Cover letter
Here are detailed guidelines to help you choose the correct section:
1. **Personal Information**:
- **Purpose**: Contains your basic contact details and online profiles.
- **Use When**: The question is about how to contact you or requests links to your professional online presence.
- **Examples**: Email address, phone number, LinkedIn profile, GitHub repository, personal website.
2. **Self Identification**:
- **Purpose**: Covers personal identifiers and demographic information.
- **Use When**: The question pertains to your gender, pronouns, veteran status, disability status, or ethnicity.
- **Examples**: Gender, pronouns, veteran status, disability status, ethnicity.
3. **Legal Authorization**:
- **Purpose**: Details your work authorization status and visa requirements.
- **Use When**: The question asks about your ability to work in specific countries or if you need sponsorship or visas.
- **Examples**: Work authorization in EU and US, visa requirements, legally allowed to work.
4. **Work Preferences**:
- **Purpose**: Specifies your preferences regarding work conditions and job roles.
- **Use When**: The question is about your preferences for remote work, in-person work, relocation, and willingness to undergo assessments or background checks.
- **Examples**: Remote work, in-person work, open to relocation, willingness to complete assessments.
5. **Education Details**:
- **Purpose**: Contains information about your academic qualifications.
- **Use When**: The question concerns your degrees, universities attended, GPA, and relevant coursework.
- **Examples**: Degree, university, GPA, field of study, exams.
6. **Experience Details**:
- **Purpose**: Details your professional work history and key responsibilities.
- **Use When**: The question pertains to your job roles, responsibilities, and achievements in previous positions.
- **Examples**: Job positions, company names, key responsibilities, skills acquired.
7. **Projects**:
- **Purpose**: Highlights specific projects you have worked on.
- **Use When**: The question asks about particular projects, their descriptions, or links to project repositories.
- **Examples**: Project names, descriptions, links to project repositories.
8. **Availability**:
- **Purpose**: Provides information on your availability for new roles.
- **Use When**: The question is about how soon you can start a new job or your notice period.
- **Examples**: Notice period, availability to start.
9. **Salary Expectations**:
- **Purpose**: Covers your expected salary range.
- **Use When**: The question pertains to your salary expectations or compensation requirements.
- **Examples**: Desired salary range.
10. **Certifications**:
- **Purpose**: Lists your professional certifications or licenses.
- **Use When**: The question involves your certifications or qualifications from recognized organizations.
- **Examples**: Certification names, issuing bodies, dates of validity.
11. **Languages**:
- **Purpose**: Describes the languages you can speak and your proficiency levels.
- **Use When**: The question asks about your language skills or proficiency in specific languages.
- **Examples**: Languages spoken, proficiency levels.
12. **Interests**:
- **Purpose**: Details your personal or professional interests.
- **Use When**: The question is about your hobbies, interests, or activities outside of work.
- **Examples**: Personal hobbies, professional interests.
13. **Cover Letter**:
- **Purpose**: Contains your personalized cover letter or statement.
- **Use When**: The question involves your cover letter or specific written content intended for the job application.
- **Examples**: Cover letter content, personalized statements.
Provide only the exact name of the section from the list above with no additional text.
"""
prompt = ChatPromptTemplate.from_template(section_prompt)
chain = prompt | self.llm_cheap | StrOutputParser()
output = chain.invoke({"question": question})
section_name = output.lower().replace(" ", "_")
if section_name == "cover_letter":
chain = chains.get(section_name)
output = chain.invoke({"resume": self.resume, "job_description": self.job_description})
return output
resume_section = getattr(self.resume, section_name, None) or getattr(self.job_application_profile, section_name, None)
if resume_section is None:
raise ValueError(f"Section '{section_name}' not found in either resume or job_application_profile.")
chain = chains.get(section_name)
if chain is None:
raise ValueError(f"Chain not defined for section '{section_name}'")
return chain.invoke({"resume_section": resume_section, "question": question})
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_educations": self.resume.education_details,"resume_jobs": self.resume.experience_details,"resume_projects": self.resume.projects , "question": question})
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
def resume_or_cover(self, phrase: str) -> str:
# Define the prompt template
prompt_template = """
Given the following phrase, respond with only 'resume' if the phrase is about a resume, or 'cover' if it's about a cover letter. Do not provide any additional information or explanations.
phrase: {phrase}
"""
prompt = ChatPromptTemplate.from_template(prompt_template)
chain = prompt | self.llm_cheap | StrOutputParser()
response = chain.invoke({"phrase": phrase})
if "resume" in response:
return "resume"
elif "cover" in response:
return "cover"
else:
return "resume"

View file

@ -0,0 +1,132 @@
from dataclasses import dataclass
from typing import Dict, List
import yaml
@dataclass
class SelfIdentification:
gender: str
pronouns: str
veteran: str
disability: str
ethnicity: str
@dataclass
class LegalAuthorization:
eu_work_authorization: str
us_work_authorization: str
requires_us_visa: str
legally_allowed_to_work_in_us: str
requires_us_sponsorship: str
requires_eu_visa: str
legally_allowed_to_work_in_eu: str
requires_eu_sponsorship: str
@dataclass
class WorkPreferences:
remote_work: str
in_person_work: str
open_to_relocation: str
willing_to_complete_assessments: str
willing_to_undergo_drug_tests: str
willing_to_undergo_background_checks: str
@dataclass
class Availability:
notice_period: str
@dataclass
class SalaryExpectations:
salary_range_usd: str
@dataclass
class JobApplicationProfile:
self_identification: SelfIdentification
legal_authorization: LegalAuthorization
work_preferences: WorkPreferences
availability: Availability
salary_expectations: SalaryExpectations
def __init__(self, yaml_str: str):
try:
data = yaml.safe_load(yaml_str)
except yaml.YAMLError as e:
raise ValueError("Error parsing YAML file.") from e
except Exception as e:
raise RuntimeError("An unexpected error occurred while parsing the YAML file.") from e
if not isinstance(data, dict):
raise TypeError("YAML data must be a dictionary.")
# Process self_identification
try:
self.self_identification = SelfIdentification(**data['self_identification'])
except KeyError as e:
raise KeyError(f"Required field {e} is missing in self_identification data.") from e
except TypeError as e:
raise TypeError(f"Error in self_identification data: {e}") from e
except AttributeError as e:
raise AttributeError("Attribute error in self_identification processing.") from e
except Exception as e:
raise RuntimeError("An unexpected error occurred while processing self_identification.") from e
# Process legal_authorization
try:
self.legal_authorization = LegalAuthorization(**data['legal_authorization'])
except KeyError as e:
raise KeyError(f"Required field {e} is missing in legal_authorization data.") from e
except TypeError as e:
raise TypeError(f"Error in legal_authorization data: {e}") from e
except AttributeError as e:
raise AttributeError("Attribute error in legal_authorization processing.") from e
except Exception as e:
raise RuntimeError("An unexpected error occurred while processing legal_authorization.") from e
# Process work_preferences
try:
self.work_preferences = WorkPreferences(**data['work_preferences'])
except KeyError as e:
raise KeyError(f"Required field {e} is missing in work_preferences data.") from e
except TypeError as e:
raise TypeError(f"Error in work_preferences data: {e}") from e
except AttributeError as e:
raise AttributeError("Attribute error in work_preferences processing.") from e
except Exception as e:
raise RuntimeError("An unexpected error occurred while processing work_preferences.") from e
# Process availability
try:
self.availability = Availability(**data['availability'])
except KeyError as e:
raise KeyError(f"Required field {e} is missing in availability data.") from e
except TypeError as e:
raise TypeError(f"Error in availability data: {e}") from e
except AttributeError as e:
raise AttributeError("Attribute error in availability processing.") from e
except Exception as e:
raise RuntimeError("An unexpected error occurred while processing availability.") from e
# Process salary_expectations
try:
self.salary_expectations = SalaryExpectations(**data['salary_expectations'])
except KeyError as e:
raise KeyError(f"Required field {e} is missing in salary_expectations data.") from e
except TypeError as e:
raise TypeError(f"Error in salary_expectations data: {e}") from e
except AttributeError as e:
raise AttributeError("Attribute error in salary_expectations processing.") from e
except Exception as e:
raise RuntimeError("An unexpected error occurred while processing salary_expectations.") from e
# Process additional fields
def __str__(self):
def format_dataclass(obj):
return "\n".join(f"{field.name}: {getattr(obj, field.name)}" for field in obj.__dataclass_fields__.values())
return (f"Self Identification:\n{format_dataclass(self.self_identification)}\n\n"
f"Legal Authorization:\n{format_dataclass(self.legal_authorization)}\n\n"
f"Work Preferences:\n{format_dataclass(self.work_preferences)}\n\n"
f"Availability: {self.availability.notice_period}\n\n"
f"Salary Expectations: {self.salary_expectations.salary_range_usd}\n\n")

View file

@ -0,0 +1,87 @@
import time
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):
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):
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.")
time.sleep(35) #TODO fix better
self.handle_security_check()
def enter_credentials(self):
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):
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):
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):
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):
try:
WebDriverWait(self.driver, timeout).until(
lambda d: d.execute_script('return document.readyState') == 'complete'
)
except TimeoutException:
print("Page load timed out.")

View file

@ -0,0 +1,73 @@
class LinkedInBotState:
def __init__(self):
self.reset()
def reset(self):
self.credentials_set = False
self.api_key_set = False
self.job_application_profile_set = False
self.gpt_answerer_set = False
self.parameters_set = False
self.logged_in = False
def validate_state(self, required_keys):
for key in required_keys:
if not getattr(self, key):
raise ValueError(f"{key.replace('_', ' ').capitalize()} must be set before proceeding.")
class LinkedInBotFacade:
def __init__(self, login_component, apply_component):
self.login_component = login_component
self.apply_component = apply_component
self.state = LinkedInBotState()
self.job_application_profile = None
self.resume = None
self.email = None
self.password = None
self.parameters = None
def set_job_application_profile_and_resume(self, job_application_profile, resume):
self._validate_non_empty(job_application_profile, "Job application profile")
self._validate_non_empty(resume, "Resume")
self.job_application_profile = job_application_profile
self.resume = resume
self.state.job_application_profile_set = True
def set_secrets(self, email, password):
self._validate_non_empty(email, "Email")
self._validate_non_empty(password, "Password")
self.email = email
self.password = password
self.state.credentials_set = True
def set_gpt_answerer_and_resume_generator(self, gpt_answerer_component, resume_generator_manager):
self._ensure_job_profile_and_resume_set()
gpt_answerer_component.set_job_application_profile(self.job_application_profile)
gpt_answerer_component.set_resume(self.resume)
self.apply_component.set_gpt_answerer(gpt_answerer_component)
self.apply_component.set_resume_generator_manager(resume_generator_manager)
self.state.gpt_answerer_set = True
def set_parameters(self, parameters):
self._validate_non_empty(parameters, "Parameters")
self.parameters = parameters
self.apply_component.set_parameters(parameters)
self.state.parameters_set = True
def start_login(self):
self.state.validate_state(['credentials_set'])
self.login_component.set_secrets(self.email, self.password)
self.login_component.start()
self.state.logged_in = True
def start_apply(self):
self.state.validate_state(['logged_in', 'job_application_profile_set', 'gpt_answerer_set', 'parameters_set'])
self.apply_component.start_applying()
def _validate_non_empty(self, value, name):
if not value:
raise ValueError(f"{name} cannot be empty.")
def _ensure_job_profile_and_resume_set(self):
if not self.state.job_application_profile_set:
raise ValueError("Job application profile and resume must be set before proceeding.")

View file

@ -0,0 +1,387 @@
import base64
import json
import os
import random
import re
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
from selenium.webdriver import ActionChains
import src.utils as utils
class LinkedInEasyApplier:
def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any, resume_generator_manager):
if resume_dir is None or not os.path.exists(resume_dir):
resume_dir = None
self.driver = driver
self.resume_path = resume_dir
self.set_old_answers = set_old_answers
self.gpt_answerer = gpt_answerer
self.resume_generator_manager = resume_generator_manager
self.all_data = self._load_questions_from_json()
def _load_questions_from_json(self) -> List[dict]:
output_file = 'answers.json'
try:
try:
with open(output_file, 'r') as f:
try:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("JSON file format is incorrect. Expected a list of questions.")
except json.JSONDecodeError:
data = []
except FileNotFoundError:
data = []
return data
except Exception:
tb_str = traceback.format_exc()
raise Exception(f"Error loading questions data from JSON file: \nTraceback:\n{tb_str}")
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)
actions = ActionChains(self.driver)
actions.move_to_element(easy_apply_button).click().perform()
self.gpt_answerer.set_job(job)
self._fill_application_form(job)
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:
attempt = 0
while attempt < 2:
self._scroll_page()
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, _ in enumerate(buttons):
try:
button = 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}]')
)
)
return button
except Exception as e:
pass
if attempt == 0:
self.driver.refresh()
time.sleep(3)
attempt += 1
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"]')
actions = ActionChains(self.driver)
actions.move_to_element(see_more_button).click().perform()
time.sleep(2)
description = self.driver.find_element(By.CLASS_NAME, 'jobs-description-content__text').text
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, job):
while True:
self.fill_up(job)
if self._next_or_submit():
break
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(1.5, 2.5))
return True
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, job) -> 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, job)
def _process_form_element(self, element: WebElement, job) -> None:
if self._is_upload_field(element):
self._handle_upload_fields(element, job)
else:
self._fill_additional_questions()
def _is_upload_field(self, element: WebElement) -> bool:
return bool(element.find_elements(By.XPATH, ".//input[@type='file']"))
def _handle_upload_fields(self, element: WebElement, job) -> 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)
output = self.gpt_answerer.resume_or_cover(parent.text.lower())
if 'resume' in output:
if self.resume_path is not None and self.resume_path.resolve().is_file():
element.send_keys(str(self.resume_path.resolve()))
else:
self._create_and_upload_resume(element, job)
elif 'cover' in output:
self._create_and_upload_cover_letter(element)
def _create_and_upload_resume(self, element, job):
folder_path = 'generated_cv'
os.makedirs(folder_path, exist_ok=True)
try:
file_path_pdf = os.path.join(folder_path, f"CV_{random.randint(0, 9999)}.pdf")
with open(file_path_pdf, "xb") as f:
f.write(base64.b64decode(self.resume_generator_manager.pdf_base64(job_description_text=job.description)))
element.send_keys(os.path.abspath(file_path_pdf))
job.pdf_path = os.path.abspath(file_path_pdf)
time.sleep(2)
except Exception:
tb_str = traceback.format_exc()
raise Exception(f"Upload failed: \nTraceback:\n{tb_str}")
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)
_, 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_form_section(section)
def _process_form_section(self, section: WebElement) -> None:
if self._handle_terms_of_service(section):
return
if self._find_and_handle_radio_question(section):
return
if self._find_and_handle_textbox_question(section):
return
if self._find_and_handle_date_question(section):
return
if self._find_and_handle_dropdown_question(section):
return
def _handle_terms_of_service(self, element: WebElement) -> bool:
checkbox = element.find_elements(By.TAG_NAME, 'label')
if checkbox and any(term in checkbox[0].text.lower() for term in ['terms of service', 'privacy policy', 'terms of use']):
checkbox[0].click()
return True
return False
def _find_and_handle_radio_question(self, section: WebElement) -> bool:
question = section.find_element(By.CLASS_NAME, 'jobs-easy-apply-form-element')
radios = question.find_elements(By.CLASS_NAME, 'fb-text-selectable__option')
if radios:
question_text = section.text.lower()
options = [radio.text.lower() for radio in radios]
existing_answer = None
for item in self.all_data:
if self._sanitize_text(question_text) in item['question'] and item['type'] == 'radio':
existing_answer = item
break
if existing_answer:
self._select_radio(radios, existing_answer['answer'])
return True
answer = self.gpt_answerer.answer_question_from_options(question_text, options)
self._save_questions_to_json({'type': 'radio', 'question': question_text, 'answer': answer})
self._select_radio(radios, answer)
return True
return False
def _find_and_handle_textbox_question(self, section: WebElement) -> bool:
text_fields = section.find_elements(By.TAG_NAME, 'input') + section.find_elements(By.TAG_NAME, 'textarea')
if text_fields:
text_field = text_fields[0]
question_text = section.find_element(By.TAG_NAME, 'label').text.lower()
is_numeric = self._is_numeric_field(text_field)
if is_numeric:
question_type = 'numeric'
answer = self.gpt_answerer.answer_question_numeric(question_text)
else:
question_type = 'textbox'
answer = self.gpt_answerer.answer_question_textual_wide_range(question_text)
existing_answer = None
for item in self.all_data:
if item['question'] == self._sanitize_text(question_text) and item['type'] == question_type:
existing_answer = item
break
if existing_answer:
self._enter_text(text_field, existing_answer['answer'])
return True
self._save_questions_to_json({'type': question_type, 'question': question_text, 'answer': answer})
self._enter_text(text_field, answer)
return True
return False
def _find_and_handle_date_question(self, section: WebElement) -> bool:
date_fields = section.find_elements(By.CLASS_NAME, 'artdeco-datepicker__input ')
if date_fields:
date_field = date_fields[0]
question_text = section.text.lower()
answer_date = self.gpt_answerer.answer_question_date()
answer_text = answer_date.strftime("%Y-%m-%d")
existing_answer = None
for item in self.all_data:
if self._sanitize_text(question_text) in item['question'] and item['type'] == 'date':
existing_answer = item
break
if existing_answer:
self._enter_text(date_field, existing_answer['answer'])
return True
self._save_questions_to_json({'type': 'date', 'question': question_text, 'answer': answer_text})
self._enter_text(date_field, answer_text)
return True
return False
def _find_and_handle_dropdown_question(self, section: WebElement) -> bool:
try:
question = section.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')
if dropdown:
select = Select(dropdown)
options = [option.text for option in select.options]
existing_answer = None
for item in self.all_data:
if self._sanitize_text(question_text) in item['question'] and item['type'] == 'dropdown':
existing_answer = item
break
if existing_answer:
self._select_dropdown_option(dropdown, existing_answer['answer'])
return True
answer = self.gpt_answerer.answer_question_from_options(question_text, options)
self._save_questions_to_json({'type': 'dropdown', 'question': question_text, 'answer': answer})
self._select_dropdown_option(dropdown, answer)
return True
except Exception:
return False
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_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 _select_dropdown_option(self, element: WebElement, text: str) -> None:
select = Select(element)
select.select_by_visible_text(text)
def _save_questions_to_json(self, question_data: dict) -> None:
output_file = 'answers.json'
question_data['question'] = self._sanitize_text(question_data['question'])
try:
try:
with open(output_file, 'r') as f:
try:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("JSON file format is incorrect. Expected a list of questions.")
except json.JSONDecodeError:
data = []
except FileNotFoundError:
data = []
data.append(question_data)
with open(output_file, 'w') as f:
json.dump(data, f, indent=4)
except Exception:
tb_str = traceback.format_exc()
raise Exception(f"Error saving questions data to JSON file: \nTraceback:\n{tb_str}")
def _sanitize_text(self, text: str) -> str:
sanitized_text = text.lower()
sanitized_text = sanitized_text.strip()
sanitized_text = sanitized_text.replace('"', '')
sanitized_text = sanitized_text.replace('\\', '')
sanitized_text = re.sub(r'[\x00-\x1F\x7F]', '', sanitized_text)
sanitized_text = sanitized_text.replace('\n', ' ').replace('\r', '')
sanitized_text = sanitized_text.rstrip(',')
return sanitized_text

219
src/linkedIn_job_manager.py Normal file
View file

@ -0,0 +1,219 @@
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 src.utils as utils
from job import Job
from src.linkedIn_easy_applier import LinkedInEasyApplier
import json
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_path = Path(resume_path)
else:
self.resume_path = 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 set_resume_generator_manager(self, resume_generator_manager):
self.resume_generator_manager = resume_generator_manager
""" def old_question(self):
self.set_old_answers = {}
file_path = 'data_folder/output/old_Questions.csv'
if os.path.exists(file_path):
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_path, self.set_old_answers, self.gpt_answerer, self.resume_generator_manager)
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
utils.printyellow(f"Starting the search for {position} in {location}.")
try:
while True:
page_sleep += 1
job_page_number += 1
utils.printyellow(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))
utils.printyellow("Starting the application process for this page...")
self.apply_jobs()
utils.printyellow("Applying to jobs on this page has been completed!")
time_left = minimum_page_time - time.time()
if time_left > 0:
utils.printyellow(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)
utils.printyellow(f"Sleeping for {sleep_time / 60} minutes.")
time.sleep(sleep_time)
page_sleep += 1
except Exception:
traceback.format_exc()
pass
time_left = minimum_page_time - time.time()
if time_left > 0:
utils.printyellow(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)
utils.printyellow(f"Sleeping for {sleep_time / 60} minutes.")
time.sleep(sleep_time)
page_sleep += 1
def apply_jobs(self):
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):
utils.printyellow(f"Blacklisted {job.title} at {job.company}, skipping...")
self.write_to_file(job, "skipped")
continue
try:
if job.apply_method not in {"Continue", "Applied", "Apply"}:
self.easy_applier_component.job_apply(job)
self.write_to_file(job, "success")
except Exception as e:
utils.printred(traceback.format_exc())
self.write_to_file(job, "failed")
continue
def write_to_file(self, job, file_name):
pdf_path = Path(job.pdf_path).resolve()
pdf_path = pdf_path.as_uri()
data = {
"company": job.company,
"job_title": job.title,
"link": job.link,
"job_location": job.location,
"pdf_path": pdf_path
}
file_path = self.output_file_directory / f"{file_name}.json"
if not file_path.exists():
with open(file_path, 'w', encoding='utf-8') as f:
json.dump([data], f, indent=4)
else:
with open(file_path, 'r+', encoding='utf-8') as f:
try:
existing_data = json.load(f)
except json.JSONDecodeError:
existing_data = []
existing_data.append(data)
f.seek(0)
json.dump(existing_data, f, indent=4)
f.truncate()
def get_base_search_url(self, parameters):
url_parts = []
if parameters['remote']:
url_parts.append("f_CF=f_WRA")
experience_levels = [str(i+1) for i, v in enumerate(parameters.get('experienceLevel', [])) if v]
if experience_levels:
url_parts.append(f"f_E={','.join(experience_levels)}")
url_parts.append(f"distance={parameters['distance']}")
job_types = [key[0].upper() for key, value in parameters.get('jobTypes', {}).items() if value]
if job_types:
url_parts.append(f"f_JT={','.join(job_types)}")
date_mapping = {
"all time": "",
"month": "&f_TPR=r2592000",
"week": "&f_TPR=r604800",
"24 hours": "&f_TPR=r86400"
}
date_param = next((v for k, v in date_mapping.items() if parameters.get('date', {}).get(k)), "")
url_parts.append("f_LF=f_AL") # Easy Apply
base_url = "&".join(url_parts)
return f"?{base_url}{date_param}"
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:
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(' ')
title_blacklisted = any(word in job_title_words for word in self.title_blacklist)
company_blacklisted = company.strip().lower() in (word.strip().lower() for word in self.company_blacklist)
link_seen = link in self.seen_jobs
return title_blacklisted or company_blacklisted or link_seen

434
src/strings.py Normal file
View file

@ -0,0 +1,434 @@
# 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 = """
Compose a brief and impactful cover letter based on the provided job description and resume. The letter should be no longer than three paragraphs and should be written in a professional, yet conversational tone. Avoid using any placeholders, and ensure that the letter flows naturally and is tailored to the job.
Analyze the job description to identify key qualifications and requirements. Introduce the candidate succinctly, aligning their career objectives with the role. Highlight relevant skills and experiences from the resume that directly match the jobs demands, using specific examples to illustrate these qualifications. Reference notable aspects of the company, such as its mission or values, that resonate with the candidates professional goals. Conclude with a strong statement of why the candidate is a good fit for the position, expressing a desire to discuss further.
Please write the cover letter in a way that directly addresses the job role and the companys characteristics, ensuring it remains concise and engaging without unnecessary embellishments. The letter should be formatted into paragraphs and should not include a greeting or signature.
## Rules:
- Provide only the text of the cover letter.
- Do not include any introductions, explanations, or additional information.
- The letter should be formatted into paragraph.
## Job Description:
```
{job_description}
```
## My resume:
```
{resume}
```
"""
numeric_question_template = """
Read the following resume carefully and answer the specific questions regarding the candidate's experience with a number of years. Follow these strategic guidelines when responding:
1. **Related and Inferred Experience:**
- **Similar Technologies:** If experience with a technology is not explicitly stated, but the candidate has experience with similar or related technologies, respond with a number of years that reflects this related experience. For example, if the candidate has experience with Python and projects that involve technologies similar to Java, consider a plausible number of years for Java.
- **Projects and Studies:** Examine the candidates projects and studies to infer skills not explicitly mentioned. Complex and advanced projects often indicate deeper expertise. For instance, if a project involves MQTT, you might infer IoT experience even if it's not explicitly mentioned.
2. **Indirect Experience and Academic Background:**
- **Relevant Projects:** Consider the types of projects the candidate has worked on and the time spent on each project. Advanced projects suggest deeper skills. For example, a project involving MQTT packet parsing suggests MQTT and possibly IoT skills.
- **Roles and Responsibilities:** Evaluate the roles and responsibilities held. If a role suggests knowledge of specific technologies or skills, provide a number based on that experience.
- **Type of University and Studies:** Also consider the type of university and the duration of studies.
3. **Inference Over Default Response:** Always strive to infer experience based on the available information. If direct experience cannot be confirmed, use related skills, projects, and academic background to estimate a plausible number of years. Avoid defaulting to 0 if you can infer any relevant experience.
4. **Handling Experience Estimates:**
- **For Low Experience (up to 5 years):** It is acceptable to provide inferred experience a lot. Aim to keep the values as high as possible avoid using "0" as a response unless absolutely necessary.
- **For High Experience:** For high levels of experience, ensure the number provided is as certain as possible and based on clear evidence from the resume. Avoid making inferences for high experience levels unless the evidence is strong.
## Rules
- Answer the question directly (only number).
## Example 1
```
## Curriculum
I had a degree in computer science. I have worked 2 years with MQTT protocol.
## Question
How many years of experience do you have with IoT?
## Answer
2
```
## Example 1
```
## Curriculum
I had a degree in computer science.
## Question
How many years of experience do you have with Bash?
## Answer
2
```
## Example 2
```
## Curriculum
I am a software engineer with 5 years of experience in Swift and Python. I have worked on a AI project.
## Question
How many years of experience do you have with AI?
## Answer
2
```
## Resume:
```
{resume_educations}
{resume_jobs}
{resume_projects}
```
## Question:
{question}
---
When responding, consider all available information, including projects, work experience, and academic background, to provide an accurate and well-reasoned answer. Make every effort to infer relevant experience and avoid defaulting to 0 if any related experience can be estimated.
"""
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:"""

103
src/utils.py Normal file
View file

@ -0,0 +1,103 @@
import os
import random
import time
from selenium import webdriver
chromeProfilePath = os.path.join(os.getcwd(), "chrome_profile", "linkedin_profile")
def ensure_chrome_profile():
profile_dir = os.path.dirname(chromeProfilePath)
if not os.path.exists(profile_dir):
os.makedirs(profile_dir)
if not os.path.exists(chromeProfilePath):
os.makedirs(chromeProfilePath)
return chromeProfilePath
def is_scrollable(element):
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_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():
ensure_chrome_profile()
options = webdriver.ChromeOptions()
options.add_argument("--start-maximized") # Avvia il browser a schermo intero
options.add_argument("--no-sandbox") # Disabilita la sandboxing per migliorare le prestazioni
options.add_argument("--disable-dev-shm-usage") # Utilizza una directory temporanea per la memoria condivisa
options.add_argument("--ignore-certificate-errors") # Ignora gli errori dei certificati SSL
options.add_argument("--disable-extensions") # Disabilita le estensioni del browser
options.add_argument("--disable-gpu") # Disabilita l'accelerazione GPU
options.add_argument("window-size=1200x800") # Imposta la dimensione della finestra del browser
options.add_argument("--disable-background-timer-throttling") # Disabilita il throttling dei timer in background
options.add_argument("--disable-backgrounding-occluded-windows") # Disabilita la sospensione delle finestre occluse
options.add_argument("--disable-translate") # Disabilita il traduttore automatico
options.add_argument("--disable-popup-blocking") # Disabilita il blocco dei popup
options.add_argument("--no-first-run") # Disabilita la configurazione iniziale del browser
options.add_argument("--no-default-browser-check") # Disabilita il controllo del browser predefinito
options.add_argument("--disable-logging") # Disabilita il logging
options.add_argument("--disable-autofill") # Disabilita l'autocompletamento dei moduli
options.add_argument("--disable-plugins") # Disabilita i plugin del browser
options.add_argument("--disable-animations") # Disabilita le animazioni
options.add_argument("--disable-cache") # Disabilita la cache
options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"]) # Esclude switch della modalità automatica e logging
# Preferenze per contenuti
prefs = {
"profile.default_content_setting_values.images": 2, # Disabilita il caricamento delle immagini
"profile.managed_default_content_settings.stylesheets": 2, # Disabilita il caricamento dei fogli di stile
}
options.add_experimental_option("prefs", prefs)
if len(chromeProfilePath) > 0:
initialPath = os.path.dirname(chromeProfilePath)
profileDir = os.path.basename(chromeProfilePath)
options.add_argument('--user-data-dir=' + initialPath)
options.add_argument("--profile-directory=" + profileDir)
else:
options.add_argument("--incognito")
return options
def printred(text):
# Codice colore ANSI per il rosso
RED = "\033[91m"
RESET = "\033[0m"
# Stampa il testo in rosso
print(f"{RED}{text}{RESET}")
def printyellow(text):
# Codice colore ANSI per il giallo
YELLOW = "\033[93m"
RESET = "\033[0m"
# Stampa il testo in giallo
print(f"{YELLOW}{text}{RESET}")