first commit v2

This commit is contained in:
feder-cr 2024-08-22 10:42:49 +01:00
parent 2dcd7943e8
commit d8b7e7fda6
18 changed files with 663 additions and 1398 deletions

3
.gitignore vendored
View File

@ -11,4 +11,5 @@ data_folder*
generated_cv*
resume.html
.vscode
chrome_profile
chrome_profile
lib*

View File

@ -1,38 +0,0 @@
remote: true
experienceLevel:
internship: true
entry: false
associate: true
mid-senior level: false
director: true
executive: false
jobTypes:
full-time: true
contract: false
part-time: false
temporary: true
internship: false
other: false
volunteer: true
date:
all time: false
month: true
week: false
24 hours: true
positions:
- Software developer
locations:
- Germany
distance: 100
companyBlacklist:
- Noir
- Crossover
titleBlacklist:

View File

@ -1,128 +0,0 @@
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"
skills:
leadership: 6
problemSolving: 4
criticalThinking: 3
adaptability: 2
perfectionism: 2
blockchain: 3
iot: 3
python: 3
fullStackDevelopment: 3
databaseManagement: 3
versionControl: 3
agileMethodologies: 2
devOpsPractices: 2
algorithmDesign: 3
mobileAppDevelopment: 2
softwareArchitecture: 3
teamCollaboration: 2
documentation: 2
java: 3
cSharp: 3
c: 3
cPlusPlus: 3
javascript: 3
php: 3
sql: 3
noSql: 3
mysql: 3
firebase: 3
continuousIntegration: 2
continuousDeployment: 2
optimization: 3
languages:
- language: "Italian"
proficiency: "Native"
- language: "English"
proficiency: "Fluent"
- language: "Spanish"
proficiency: "Intermediate"
interests:
- "Open Source Contributing"
- "Machine Learning"
- "Hiking"
- "Chess"

View File

@ -1,3 +0,0 @@
email: myemaillinkedin@gmail.com
password: ImpossiblePassowrd10
openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR

179
gpt.py
View File

@ -4,7 +4,7 @@ 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
@ -25,7 +25,7 @@ class LLMLogger:
@staticmethod
def log_request(prompts, parsed_reply: Dict[str, Dict]):
calls_log = os.path.join(os.getcwd(), "open_ai_calls.json")
calls_log = os.path.join(Path("data_folder/output"), "open_ai_calls.json")
if isinstance(prompts, StringPromptValue):
prompts = prompts.text
elif isinstance(prompts, Dict):
@ -94,7 +94,6 @@ class LoggerChatModel:
response_metadata = llmresult.response_metadata
id_ = llmresult.id
usage_metadata = llmresult.usage_metadata
parsed_result = {
"content": content,
"response_metadata": {
@ -116,11 +115,8 @@ class LoggerChatModel:
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
)
ChatOpenAI(model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.8)
)
@property
def job_description(self):
return self.job.description
@ -148,10 +144,11 @@ class GPTAnswerer:
def set_job(self, job):
self.job = job
self.job.set_summarize_job_description(
self.summarize_job_description(self.job.description)
)
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
@ -160,40 +157,11 @@ class GPTAnswerer:
chain = prompt | self.llm_cheap | StrOutputParser()
output = chain.invoke({"text": text})
return output
def get_resume_html(self):
resume_markdown_prompt = ChatPromptTemplate.from_template(strings.resume_markdown_template)
fusion_job_description_resume_prompt = ChatPromptTemplate.from_template(strings.fusion_job_description_resume_template)
resume_markdown_chain = resume_markdown_prompt | self.llm_cheap | StrOutputParser()
fusion_job_description_resume_chain = fusion_job_description_resume_prompt | self.llm_cheap | StrOutputParser()
casual_markdown_path = os.path.abspath("resume_template/casual_markdown.js")
reorganize_header_path = os.path.abspath("resume_template/reorganizeHeader.js")
resume_css_path = os.path.abspath("resume_template/resume.css")
html_template = strings.html_template.format(casual_markdown=casual_markdown_path, reorganize_header=reorganize_header_path, resume_css=resume_css_path)
composed_chain = (
resume_markdown_chain
| (lambda output: {"job_description": self.job.summarize_job_description, "formatted_resume": output})
| fusion_job_description_resume_chain
| (lambda formatted_resume: html_template + formatted_resume)
)
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
chains = {
@ -211,40 +179,118 @@ class GPTAnswerer:
"interests": self._create_chain(strings.interests_template),
"cover_letter": self._create_chain(strings.coverletter_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, Cover letter"
)
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})
output = chain.invoke({"resume": self.resume, "job_description": self.job_description})
return output
resume_section = getattr(self.resume, section_name, None)
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 the resume.")
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_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})
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:
@ -265,3 +311,20 @@ class GPTAnswerer:
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"

1
job.py
View File

@ -9,6 +9,7 @@ class Job:
apply_method: str
description: str = ""
summarize_job_description: str = ""
pdf_path: str = ""
def set_summarize_job_description(self, summarize_job_description):
self.summarize_job_description = summarize_job_description

132
job_application_profile.py Normal file
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

@ -1,57 +1,73 @@
class LinkedInBotFacade:
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 = {
"credentials_set": False,
"api_key_set": False,
"resume_set": False,
"gpt_answerer_set": False,
"parameters_set": False,
"logged_in": False
}
self.state = LinkedInBotState()
self.job_application_profile = None
self.resume = None
self.email = None
self.password = None
self.parameters = None
def set_resume(self, resume):
if not resume:
raise ValueError("Plain text resume cannot be empty.")
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["resume_set"] = True
self.state.job_application_profile_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.")
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
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_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):
if not parameters:
raise ValueError("Parameters cannot be None or empty.")
self._validate_non_empty(parameters, "Parameters")
self.parameters = parameters
self.apply_component.set_parameters(parameters)
self.state["parameters_set"] = True
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.state.validate_state(['credentials_set'])
self.login_component.set_secrets(self.email, self.password)
self.login_component.start()
self.state["logged_in"] = True
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()
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

@ -1,12 +1,12 @@
import base64
import os
import random
import re
import tempfile
import time
import traceback
from datetime import date
from typing import List, Optional, Any, Tuple
import uuid
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from selenium.common.exceptions import NoSuchElementException
@ -15,27 +15,18 @@ 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.pagesizes import letter
from xhtml2pdf import pisa
import utils
from selenium.webdriver import ActionChains
import utils
class LinkedInEasyApplier:
def __init__(self, driver: Any, resume_dir: Optional[str], set_old_answers: List[Tuple[str, str, str]], gpt_answerer: Any):
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_dir = resume_dir
self.set_old_answers = set_old_answers
self.gpt_answerer = gpt_answerer
self.resume_generator_manager = resume_generator_manager
def job_apply(self, job: Any):
self.driver.get(job.link)
@ -44,55 +35,64 @@ class LinkedInEasyApplier:
easy_apply_button = self._find_easy_apply_button()
job_description = self._get_job_description()
job.set_job_description(job_description)
easy_apply_button.click()
actions = ActionChains(self.driver)
actions.move_to_element(easy_apply_button).click().perform()
self.gpt_answerer.set_job(job)
self._fill_application_form()
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:
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}]')
)
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")]')
)
except Exception as e:
pass
)
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"]')
see_more_button.click()
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
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 :
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)
#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):
def _fill_application_form(self, job):
while True:
self.fill_up()
self.fill_up(job)
if self._next_or_submit():
break
@ -110,7 +110,6 @@ class LinkedInEasyApplier:
time.sleep(random.uniform(3.0, 5.0))
self._check_for_errors()
def _unfollow_company(self) -> None:
try:
follow_checkbox = self.driver.find_element(
@ -133,88 +132,57 @@ class LinkedInEasyApplier:
except Exception as e:
pass
def fill_up(self) -> None:
try:
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)
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) -> None:
try:
if self._is_upload_field(element):
self._handle_upload_fields(element)
else:
self._fill_additional_questions()
except Exception as e:
pass
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:
try:
element.find_element(By.XPATH, ".//input[@type='file']")
return True
except NoSuchElementException:
return False
return bool(element.find_elements(By.XPATH, ".//input[@type='file']"))
def _handle_upload_fields(self, element: WebElement) -> None:
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)
if 'resume' in parent.text.lower():
if self.resume_dir != None:
output = self.gpt_answerer.resume_or_cover(parent.text.lower())
if 'resume' in output:
if self.resume_dir:
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():
if resume_path.exists() and resume_path.is_file():
element.send_keys(str(resume_path))
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):
max_retries = 3
retry_delay = 1
def _create_and_upload_resume(self, element, job):
folder_path = 'generated_cv'
if not os.path.exists(folder_path):
os.makedirs(folder_path)
for attempt in range(max_retries):
try:
html_string = self.gpt_answerer.get_resume_html()
with tempfile.NamedTemporaryFile(delete=False, suffix='.html', mode='w', encoding='utf-8') as temp_html_file:
temp_html_file.write(html_string)
file_name_HTML = temp_html_file.name
file_name_pdf = f"resume_{uuid.uuid4().hex}.pdf"
file_path_pdf = os.path.join(folder_path, file_name_pdf)
with open(file_path_pdf, "wb") as f:
f.write(base64.b64decode(utils.HTML_to_PDF(file_name_HTML)))
element.send_keys(os.path.abspath(file_path_pdf))
time.sleep(2) # Give some time for the upload process
os.remove(file_name_HTML)
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}")
def _upload_resume(self, element: WebElement) -> None:
element.send_keys(str(self.resume_dir))
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)
width, height = letter
_, height = letter
text_object = c.beginText(100, height - 100)
text_object.setFont("Helvetica", 12)
text_object.textLines(cover_letter)
@ -225,131 +193,84 @@ class LinkedInEasyApplier:
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)
self._process_form_section(section)
def _process_question(self, section: WebElement) -> None:
def _process_form_section(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)
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:
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
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 _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()
def _find_and_handle_radio_question(self, section: WebElement) -> bool:
radios = section.find_elements(By.CLASS_NAME, 'fb-text-selectable__option')
if radios:
question_text = section.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)
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
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)
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)
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]
answer_date = self.gpt_answerer.answer_question_date()
self._enter_text(date_field, answer_date.strftime("%Y-%m-%d"))
return True
return False
def _find_and_handle_dropdown_question(self, section: WebElement) -> bool:
dropdowns = section.find_elements(By.CLASS_NAME, 'fb-dropdown__select')
if dropdowns:
dropdown = dropdowns[0]
question_text = section.text.lower()
select = Select(dropdown)
answer = self.gpt_answerer.answer_question_from_options(question_text, [option.text.lower() for option in select.options])
self._select_dropdown_option(select, answer)
return True
return False
def _is_numeric_field(self, element: WebElement) -> bool:
input_type = element.get_attribute('type')
return input_type == 'number'
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()
if radio.text.lower() == answer.lower():
radio.click()
break
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
def _select_dropdown_option(self, select: Select, answer: str) -> None:
for option in select.options:
if option.text.lower() == answer.lower():
select.select_by_visible_text(option.text)
break

View File

@ -1,4 +1,3 @@
import csv
import os
import random
import time
@ -10,6 +9,7 @@ from selenium.webdriver.common.by import By
import utils
from job import Job
from linkedIn_easy_applier import LinkedInEasyApplier
import json
class EnvironmentKeys:
@ -45,15 +45,15 @@ class LinkedInJobManager:
self.resume_dir = None
self.output_file_directory = Path(parameters['outputFileDirectory'])
self.env_config = EnvironmentKeys()
self.old_question()
#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.
"""
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):
@ -62,13 +62,11 @@ class LinkedInJobManager:
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
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
)
self.easy_applier_component = LinkedInEasyApplier(self.driver, self.resume_dir, self.set_old_answers, self.gpt_answerer, self.resume_generator_manager)
searches = list(product(self.positions, self.locations))
random.shuffle(searches)
page_sleep = 0
@ -127,50 +125,50 @@ class LinkedInJobManager:
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]
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.company, job.location, job.title, job.link, "skipped")
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.company, job.location, job.title, job.link, "failed")
continue
self.write_to_file(job.company, job.location, job.title, job.link, "success")
self.write_to_file(job, "failed")
continue
except Exception as e:
traceback.format_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:
utils.printred(f"Error writing registered job: {e}")
utils.printred(f"Details: Answer type: {answer_type}, Question: {question_text}")
def write_to_file(self, job, file_name):
data = {
"company": job.company,
"job_title": job.title,
"link": job.link,
"job_location": job.location,
"pdf_path": job.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 = []
@ -205,12 +203,6 @@ class LinkedInJobManager:
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:

191
main.py
View File

@ -1,113 +1,104 @@
import os
import re
import sys
from pathlib import Path
import yaml
import click
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
import click
from selenium.common.exceptions import WebDriverException, TimeoutException
sys.path.append('C:\\Users\\loren\\OneDrive\\Desktop\\Nuovacartella\\LinkedIn-GPT-EasyApplyBot-master\\LinkedIn_AIHawk_automatic_job_application\\lib_resume_builder_AIHawk')
from lib_resume_builder_AIHawk import Resume,StyleManager,FacadeManager,ResumeGenerator
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
from job_application_profile import JobApplicationProfile
# Suppress stderr
sys.stderr = open(os.devnull, 'w')
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
return re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email) is not None
@staticmethod
def validate_yaml_file(yaml_path: Path) -> dict:
try:
with open(yaml_path, 'r') as stream:
return yaml.safe_load(stream)
except yaml.YAMLError as exc:
raise ConfigError(f"Error reading file {yaml_path}: {exc}")
except FileNotFoundError:
raise ConfigError(f"File not found: {yaml_path}")
@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 {config_yaml_path}: {exc}")
except FileNotFoundError:
raise ConfigError(f"Config file not found: {config_yaml_path}")
parameters = ConfigValidator.validate_yaml_file(config_yaml_path)
required_keys = {
'remote': bool,
'experienceLevel': dict,
'jobTypes': dict,
'date': dict,
'positions': list,
'locations': list,
'distance': int,
'companyBlacklist': list,
'titleBlacklist': list
}
for key, expected_type in required_keys.items():
if key not in parameters or not isinstance(parameters[key], expected_type):
raise ConfigError(f"Missing or invalid key '{key}' in config file {config_yaml_path}")
# Validate 'remote'
if 'remote' not in parameters or not isinstance(parameters['remote'], bool):
raise ConfigError(f"'remote' in config file {config_yaml_path} must be a boolean value.")
experience_levels = ['internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive']
for level in experience_levels:
if not isinstance(parameters['experienceLevel'].get(level), bool):
raise ConfigError(f"Experience level '{level}' must be a boolean in config file {config_yaml_path}")
# Validate 'experienceLevel'
experience_level = parameters.get('experienceLevel', {})
valid_experience_levels = [
'internship', 'entry', 'associate', 'mid-senior level', 'director', 'executive'
]
for level in valid_experience_levels:
if level not in experience_level or not isinstance(experience_level[level], bool):
raise ConfigError(f"Experience level '{level}' must be a boolean value in config file {config_yaml_path}.")
job_types = ['full-time', 'contract', 'part-time', 'temporary', 'internship', 'other', 'volunteer']
for job_type in job_types:
if not isinstance(parameters['jobTypes'].get(job_type), bool):
raise ConfigError(f"Job type '{job_type}' must be a boolean in config file {config_yaml_path}")
# Validate 'jobTypes'
job_types = parameters.get('jobTypes', {})
valid_job_types = [
'full-time', 'contract', 'part-time', 'temporary', 'internship', 'other', 'volunteer'
]
for job_type in valid_job_types:
if job_type not in job_types or not isinstance(job_types[job_type], bool):
raise ConfigError(f"Job type '{job_type}' must be a boolean value in config file {config_yaml_path}.")
date_filters = ['all time', 'month', 'week', '24 hours']
for date_filter in date_filters:
if not isinstance(parameters['date'].get(date_filter), bool):
raise ConfigError(f"Date filter '{date_filter}' must be a boolean in config file {config_yaml_path}")
# Validate 'date'
date = parameters.get('date', {})
valid_dates = ['all time', 'month', 'week', '24 hours']
for date_filter in valid_dates:
if date_filter not in date or not isinstance(date[date_filter], bool):
raise ConfigError(f"Date filter '{date_filter}' must be a boolean value in config file {config_yaml_path}.")
if not all(isinstance(pos, str) for pos in parameters['positions']):
raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}")
if not all(isinstance(loc, str) for loc in parameters['locations']):
raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}")
# Validate 'positions'
positions = parameters.get('positions', [])
if not isinstance(positions, list) or not all(isinstance(pos, str) for pos in positions):
raise ConfigError(f"'positions' must be a list of strings in config file {config_yaml_path}.")
# Validate 'locations'
locations = parameters.get('locations', [])
if not isinstance(locations, list) or not all(isinstance(loc, str) for loc in locations):
raise ConfigError(f"'locations' must be a list of strings in config file {config_yaml_path}.")
# Validate 'distance'
approved_distances = {0, 5, 10, 25, 50, 100}
distance = parameters.get('distance')
if distance not in approved_distances:
if parameters['distance'] not in approved_distances:
raise ConfigError(f"Invalid distance value in config file {config_yaml_path}. Must be one of: {approved_distances}")
# Validate 'companyBlacklist'
company_blacklist = parameters.get('companyBlacklist', [])
if not isinstance(company_blacklist, list) or not all(isinstance(comp, str) for comp in company_blacklist):
company_blacklist = []
parameters['companyBlacklist'] = company_blacklist
for blacklist in ['companyBlacklist', 'titleBlacklist']:
if not all(isinstance(item, str) for item in parameters.get(blacklist, [])):
parameters[blacklist] = []
# Validate 'titleBlacklist'
title_blacklist = parameters.get('titleBlacklist', [])
if not isinstance(title_blacklist, list) or not all(isinstance(word, str) for word in title_blacklist):
title_blacklist = []
parameters['titleBlacklist'] = title_blacklist
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 {secrets_yaml_path}: {exc}")
except FileNotFoundError:
raise ConfigError(f"Secrets file not found: {secrets_yaml_path}")
secrets = ConfigValidator.validate_yaml_file(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 in file {secrets_yaml_path}: {secret}")
raise ConfigError(f"Missing secret '{secret}' in file {secrets_yaml_path}")
if not ConfigValidator.validate_email(secrets['email']):
raise ConfigError(f"Invalid email format in secrets file {secrets_yaml_path}.")
if not secrets['password']:
@ -120,48 +111,38 @@ class ConfigValidator:
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
return next((file for file in at_path.iterdir() if name_containing.lower() in file.name.lower() and file.suffix.lower() == with_extension.lower()), 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')
required_files = ['secrets.yaml', 'config.yaml', 'plain_text_resume.yaml']
missing_files = [file for file in required_files if not (app_data_folder / file).exists()]
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
return (app_data_folder / 'secrets.yaml', app_data_folder / 'config.yaml', app_data_folder / 'plain_text_resume.yaml', output_folder)
@staticmethod
def file_paths_to_dict(resume_file: Path | None, plain_text_resume_file: Path) -> dict:
if not plain_text_resume_file.exists():
raise FileNotFoundError(f"Plain text resume file not found: {plain_text_resume_file}")
result = {'plainTextResume': plain_text_resume_file}
if resume_file is not None:
if resume_file:
if not resume_file.exists():
raise FileNotFoundError(f"Resume file not found: {resume_file}")
result['resume'] = resume_file
return result
def init_browser():
def init_browser() -> webdriver.Chrome:
try:
options = chromeBrowserOptions()
service = ChromeService(ChromeDriverManager().install())
@ -172,33 +153,50 @@ def init_browser():
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)
plain_text_resume = file.read()
resume_object = Resume(plain_text_resume)
job_application_profile_object = JobApplicationProfile(plain_text_resume)
style_manager = StyleManager()
resume_generator = ResumeGenerator()
resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output"))
os.system('cls' if os.name == 'nt' else 'clear')
resume_generator_manager.choose_style()
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_job_application_profile_and_resume(job_application_profile_object, resume_object)
bot.set_gpt_answerer_and_resume_generator(gpt_answerer_component, resume_generator_manager)
bot.set_parameters(parameters)
bot.start_login()
bot.start_apply()
except WebDriverException as e:
print(f"WebDriver error occurred: {e}")
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)}")
@ -208,8 +206,9 @@ def main(resume: Path = None):
print("Ensure all required files are present in the data folder.")
print("Refer to the file setup guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
except RuntimeError as re:
print(f"Runtime error: {str(re)}")
print("Check browser setup and other runtime issues.")
print("Refer to the configuration and troubleshooting guide: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration")
except Exception as e:
print(f"An unexpected error occurred: {str(e)}")

Binary file not shown.

127
resume.py
View File

@ -1,127 +0,0 @@
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)
)

View File

@ -1,202 +0,0 @@
/*****************************************************************************
* casual-markdown - a lightweight regexp-base markdown parser with TOC support
* 2022/07/31, v0.90, refine frontmatter (simple yaml)
* 2023/04/12, v0.92, addCopyButton for code-block
*
* Copyright (c) 2022-2023, Casualwriter (MIT Licensed)
* https://github.com/casualwriter/casual-markdown
*****************************************************************************/
;(function(){
// define md object, and extent function (which is a dummy function)
var md = { yaml:{}, before: function (str) {return str}, after: function (str) {return str} }
// function for REGEXP to convert html tag. ie. <TAG> => &lt;TAG*gt;
md.formatTag = function (html) { return html.replace(/</g,'&lt;').replace(/\>/g,'&gt;'); }
// frontmatter for simple YAML (support multi-level, but string value only)
md.formatYAML = function (front, matter) {
var level = {}, latest = md.yaml;
matter.replace( /^\s*#(.*)$/gm, '' ).replace( /^( *)([^:^\n]+):(.*)$/gm, function(m, sp, key,val) {
level[sp] = level[sp] || latest
latest = level[sp][key.trim()] = val.trim() || {}
for (e in level) if(e>sp) level[e]=null;
} );
return ''
}
//===== format code-block, highlight remarks/keywords for code/sql
md.formatCode = function (match, title, block) {
// convert tag <> to &lt; &gt; tab to 3 space, support marker using ^^^
block = block.replace(/</g,'&lt;').replace(/\>/g,'&gt;')
block = block.replace(/\t/g,' ').replace(/\^\^\^(.+?)\^\^\^/g, '<mark>$1</mark>')
// highlight comment and keyword based on title := none | sql | code
if (title.toLowerCase(title) == 'sql') {
block = block.replace(/^\-\-(.*)/gm,'<rem>--$1</rem>').replace(/\s\-\-(.*)/gm,' <rem>--$1</rem>')
block = block.replace(/(\s?)(function|procedure|return|if|then|else|end|loop|while|or|and|case|when)(\s)/gim,'$1<b>$2</b>$3')
block = block.replace(/(\s?)(select|update|delete|insert|create|from|where|group by|having|set)(\s)/gim,'$1<b>$2</b>$3')
} else if ((title||'none')!=='none') {
block = block.replace(/^\/\/(.*)/gm,'<rem>//$1</rem>').replace(/\s\/\/(.*)/gm,' <rem>//$1</rem>')
block = block.replace(/(\s?)(function|procedure|return|exit|if|then|else|end|loop|while|or|and|case|when)(\s)/gim,'$1<b>$2</b>$3')
block = block.replace(/(\s?)(var|let|const|=>|for|next|do|while|loop|continue|break|switch|try|catch|finally)(\s)/gim,'$1<b>$2</b>$3')
}
return '<pre title="' + title + '"><button onclick="md.clipboard(this)">copy</button><code>' + block + '</code></pre>'
}
// copy to clipboard for code-block
md.clipboard = function (e) {
navigator.clipboard.writeText( e.parentNode.innerText.replace('copy\n','') )
e.innerText = 'copied'
}
//===== parse markdown string into HTML string (exclude code-block)
md.parser = function( mdstr ) {
// apply yaml variables
for (var name in this.yaml) mdstr = mdstr.replace( new RegExp('\{\{\\s*'+name+'\\s*\}\}', 'gm'), this.yaml[name] )
// table syntax
mdstr = mdstr.replace(/\n(.+?)\n.*?\-\-\s?\|\s?\-\-.*?\n([\s\S]*?)\n\s*?\n/g, function (m,p1,p2) {
var thead = p1.replace(/^\|(.+)/gm,'$1').replace(/(.+)\|$/gm,'$1').replace(/\|/g,'<th>')
var tbody = p2.replace(/^\|(.+)/gm,'$1').replace(/(.+)\|$/gm,'$1')
tbody = tbody.replace(/(.+)/gm,'<tr><td>$1</td></tr>').replace(/\|/g,'<td>')
return '\n<table>\n<thead>\n<th>' + thead + '\n</thead>\n<tbody>' + tbody + '\n</tbody></table>\n\n'
} )
// horizontal rule => <hr>
mdstr = mdstr.replace(/^-{3,}|^\_{3,}|^\*{3,}$/gm, '<hr>').replace(/\n\n<hr\>/g, '\n<br><hr>')
// header => <h1>..<h5>
mdstr = mdstr.replace(/^##### (.*?)\s*#*$/gm, '<h5>$1</h5>')
.replace(/^#### (.*?)\s*#*$/gm, '<h4>$1</h4>')
.replace(/^### (.*?)\s*#*$/gm, '<h3>$1</h3>')
.replace(/^## (.*?)\s*#*$/gm, '<h2>$1</h2>')
.replace(/^# (.*?)\s*#*$/gm, '<h1>$1</h1>')
.replace(/^<h(\d)\>(.*?)\s*{(.*)}\s*<\/h\d\>$/gm, '<h$1 id="$3">$2</h$1>')
// inline code-block: `code-block` => <code>code-block</code>
mdstr = mdstr.replace(/``(.*?)``/gm, function(m,p){ return '<code>' + md.formatTag(p).replace(/`/g,'&#96;') + '</code>'} )
mdstr = mdstr.replace(/`(.*?)`/gm, '<code>$1</code>' )
// blockquote, max 2 levels => <blockquote>{text}</blockquote>
mdstr = mdstr.replace(/^\>\> (.*$)/gm, '<blockquote><blockquote>$1</blockquote></blockquote>')
mdstr = mdstr.replace(/^\> (.*$)/gm, '<blockquote>$1</blockquote>')
mdstr = mdstr.replace(/<\/blockquote\>\n<blockquote\>/g, '\n<br>' )
mdstr = mdstr.replace(/<\/blockquote\>\n<br\><blockquote\>/g, '\n<br>' )
// image syntax: ![title](url) => <img alt="title" src="url" />
mdstr = mdstr.replace(/!\[(.*?)\]\((.*?) "(.*?)"\)/gm, '<img alt="$1" src="$2" $3 />')
mdstr = mdstr.replace(/!\[(.*?)\]\((.*?)\)/gm, '<img alt="$1" src="$2" width="90%" />')
// links syntax: [title "title"](url) => <a href="url" title="title">text</a>
mdstr = mdstr.replace(/\[(.*?)\]\((.*?) "new"\)/gm, '<a href="$2" target=_new>$1</a>')
mdstr = mdstr.replace(/\[(.*?)\]\((.*?) "(.*?)"\)/gm, '<a href="$2" title="$3">$1</a>')
mdstr = mdstr.replace(/([<\s])(https?\:\/\/.*?)([\s\>])/gm, '$1<a href="$2">$2</a>$3')
mdstr = mdstr.replace(/\[(.*?)\]\(\)/gm, '<a href="$1">$1</a>')
mdstr = mdstr.replace(/\[(.*?)\]\((.*?)\)/gm, '<a href="$2">$1</a>')
// unordered/ordered list, max 2 levels => <ul><li>..</li></ul>, <ol><li>..</li></ol>
mdstr = mdstr.replace(/^[\*+-][ .](.*)/gm, '<ul><li>$1</li></ul>' )
mdstr = mdstr.replace(/^\d\d?[ .](.*)/gm, '<ol><li>$1</li></ol>' )
mdstr = mdstr.replace(/^\s{2,6}[\*+-][ .](.*)/gm, '<ul><ul><li>$1</li></ul></ul>' )
mdstr = mdstr.replace(/^\s{2,6}\d[ .](.*)/gm, '<ul><ol><li>$1</li></ol></ul>' )
mdstr = mdstr.replace(/<\/[ou]l\>\n\n?<[ou]l\>/g, '\n' )
mdstr = mdstr.replace(/<\/[ou]l\>\n<[ou]l\>/g, '\n' )
// text decoration: bold, italic, underline, strikethrough, highlight
mdstr = mdstr.replace(/\*\*\*(\w.*?[^\\])\*\*\*/gm, '<b><em>$1</em></b>')
mdstr = mdstr.replace(/\*\*(\w.*?[^\\])\*\*/gm, '<b>$1</b>')
mdstr = mdstr.replace(/\*(\w.*?[^\\])\*/gm, '<em>$1</em>')
mdstr = mdstr.replace(/___(\w.*?[^\\])___/gm, '<b><em>$1</em></b>')
mdstr = mdstr.replace(/__(\w.*?[^\\])__/gm, '<u>$1</u>')
// mdstr = mdstr.replace(/_(\w.*?[^\\])_/gm, '<u>$1</u>') // NOT support!!
mdstr = mdstr.replace(/\^\^\^(.+?)\^\^\^/gm, '<mark>$1</mark>')
mdstr = mdstr.replace(/\^\^(\w.*?)\^\^/gm, '<ins>$1</ins>')
mdstr = mdstr.replace(/~~(\w.*?)~~/gm, '<del>$1</del>')
// line break and paragraph => <br/> <p>
mdstr = mdstr.replace(/ \n/g, '\n<br/>').replace(/\n\s*\n/g, '\n<p>\n')
// indent as code-block
mdstr = mdstr.replace(/^ {4,10}(.*)/gm, function(m,p) { return '<pre><code>' + md.formatTag(p) + '</code></pre>'} )
mdstr = mdstr.replace(/^\t(.*)/gm, function(m,p) { return '<pre><code>' + md.formatTag(p) + '</code></pre>'} )
mdstr = mdstr.replace(/<\/code\><\/pre\>\n<pre\><code\>/g, '\n' )
// Escaping Characters
return mdstr.replace(/\\([`_~\*\+\-\.\^\\\<\>\(\)\[\]])/gm, '$1' )
}
//===== parse markdown string into HTML content (cater code-block)
md.html = function (mdText) {
// replace \r\n to \n, and handle front matter for simple YAML
mdText = mdText.replace(/\r\n/g, '\n').replace( /^---+\s*\n([\s\S]*?)\n---+\s*\n/, md.formatYAML )
// handle code-block.
mdText = mdText.replace(/\n~~~/g,'\n```').replace(/\n``` *(.*?)\n([\s\S]*?)\n``` *\n/g, md.formatCode)
// split by "<code>", skip for code-block and process normal text
var pos1=0, pos2=0, mdHTML = ''
while ( (pos1 = mdText.indexOf('<code>')) >= 0 ) {
pos2 = mdText.indexOf('</code>', pos1 )
mdHTML += md.after( md.parser( md.before( mdText.substr(0,pos1) ) ) )
mdHTML += mdText.substr(pos1, (pos2>0? pos2-pos1+7 : mdtext.length) )
mdText = mdText.substr( pos2 + 7 )
}
return '<div class="markdown">' + mdHTML + md.after( md.parser( md.before(mdText) ) ) + '</div>'
}
//===== TOC support
md.toc = function (srcDiv, tocDiv, options ) {
// select elements, set title
var tocSelector = (options&&options.css) || 'h1,h2,h3,h4'
var tocTitle = (options&&options.title) || 'Table of Contents'
var toc = document.getElementById(srcDiv).querySelectorAll( tocSelector )
var html = '<div class="toc"><ul>' + (tocTitle=='none'? '' : '<h3>' + tocTitle + '</h3>');
// loop for each element,add <li> element with class in TAG name.
for (var i=0; i<toc.length; i++ ) {
if (toc[i].id.substr(0,6)=='no-toc') continue;
if (!toc[i].id) toc[i].id = "toc-item-" + i;
html += '<li class="' + toc[i].nodeName + '" title="#' + toc[i].id + '" onclick="location=this.title">'
html += toc[i].textContent + '</a></li>';
}
document.getElementById(tocDiv).innerHTML = html + "</ul>";
//===== scrollspy support (ps: add to document.body if element(scrollspy) not found)
if ( options && options.scrollspy ) {
(document.getElementById(options.scrollspy)||document).onscroll = function () {
// get TOC elements, and viewport position
var list = document.getElementById(tocDiv).querySelectorAll('li')
var divScroll = document.getElementById(options.scrollspy) || document.documentElement
var divHeight = divScroll.clientHeight || divScroll.offsetHeight
// loop for each TOC element, add/remove scrollspy class
for (var i=0; i<list.length; i++) {
var div = document.getElementById( list[i].title.substr(1) )
var pos = (div? div.offsetTop - divScroll.scrollTop + 10: 0 )
if ( pos>0 && pos<divHeight ) {
list[i].className = list[i].className.replace('active','') + ' active' // classList.add( 'active' );
} else {
list[i].className = list[i].className.replace('active','') // classList.remove( 'active' );
}
}
}
}
//===== end of scrollspy
}
if (typeof exports==='object') {
module.exports=md;
} else if (typeof define==='function') {
define(function(){return md;});
} else {
this.md=md;
}
}).call( function(){ return this||(typeof window!=='undefined'?window:global)}() );

View File

@ -1,43 +0,0 @@
// reorganizeHeader.js
function reorganizeHeader() {
const h1 = document.querySelector('h1');
if (!h1) return;
let currentNode = h1;
const headerInfoElements = [];
const contactInfoElements = [];
let firstAnchorFound = false;
while (currentNode && currentNode.tagName !== 'H2') {
if (currentNode.tagName === 'A') {
if (!firstAnchorFound) {
headerInfoElements.push(currentNode);
firstAnchorFound = true;
} else {
contactInfoElements.push(currentNode);
}
} else {
headerInfoElements.push(currentNode);
}
currentNode = currentNode.nextElementSibling;
}
const newHeader = document.createElement('header');
const headerInfoDiv = document.createElement('div');
headerInfoDiv.className = 'header-info';
headerInfoElements.forEach(el => {
headerInfoDiv.appendChild(el);
});
const contactInfoDiv = document.createElement('div');
contactInfoDiv.className = 'contact-info';
contactInfoElements.forEach(el => {
contactInfoDiv.appendChild(el);
});
newHeader.appendChild(headerInfoDiv);
newHeader.appendChild(contactInfoDiv);
const h2 = document.querySelector('h2');
if (h2) {
h2.parentNode.insertBefore(newHeader, h2);
}
}
setTimeout(function() {
reorganizeHeader();
}, 1000);

View File

@ -1,156 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Roboto:wght@400&display=swap');
/* Reset generale per uniformità */
body, h2, h3, h4, p, ul, ol {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}
body {
line-height: 1.6;
margin: auto;
padding: 20px;
max-width: 1024px;
color: #333;
background-color: #f8f9fa;
}
/* Header Style */
header {
background-color: #e9ecef; /* Sfondo leggermente più scuro */
padding: 15px 20px 0 20px; /* Padding: 15px sopra, 20px a destra e a sinistra, 0px sotto */
border-bottom: 2px solid #d1d1d1; /* Bordo sottile per separare il header dal resto del contenuto */
font-family: 'Roboto', sans-serif; /* Font per il testo generico */
display: flex;
justify-content: space-between;
align-items: center;
}
.header-info {
flex: 1;
}
.header-info h1 {
margin: 0;
font-size: 30px; /* Dimensione del font aumentata per il nome */
color: #000; /* Colore nero per il testo del nome */
font-family: 'Montserrat', sans-serif; /* Font più accattivante per il nome */
}
.header-info a {
color: #0056b3; /* Colore blu intenso per i link */
text-decoration: none;
font-weight: bold;
}
.header-info a:hover {
text-decoration: underline;
}
.contact-info {
flex: 0;
text-align: center; /* Centratura del testo */
}
.contact-info a {
display: block;
color: #0056b3; /* Colore blu intenso per i link */
text-decoration: none;
margin-bottom: 5px; /* Spaziatura tra i contatti */
font-size: 14px; /* Dimensione del font per i dettagli di contatto */
font-family: 'Roboto', sans-serif; /* Font moderno per i dettagli di contatto */
font-weight: bold;
}
.contact-info a:hover {
color: #ff5722; /* Colore arancione per i link al passaggio del mouse */
}
.contact-info a:visited {
color: #666; /* Colore grigio scuro per i link visitati */
}
/* Stile per i titoli delle sezioni */
h2 {
font-size: 24px;
color: #0056b3;
border-bottom: 1px solid #d1d1d1;
margin-bottom: 10px;
padding-bottom: 5px;
font-family: 'Montserrat', sans-serif;
}
/* Stile per le esperienze professionali */
h3 {
font-size: 20px;
color: #212529;
margin-top: 15px;
margin-bottom: 5px;
}
em {
color: #555;
}
ol, ul {
margin-left: 20px;
margin-bottom: 15px;
}
li {
margin-bottom: 8px;
line-height: 1.5;
}
/* Stile per le sezioni secondarie */
p {
margin-bottom: 15px;
}
b {
color: #212529;
}
/* Stile per i link */
a {
color: #0056b3;
}
a:hover {
color: #ff5722;
text-decoration: underline;
}
/* Responsività per schermi più piccoli */
@media (max-width: 768px) {
header {
flex-direction: column;
text-align: center;
}
.contact-info {
text-align: center;
margin-top: 10px;
}
}
.markdown code { background:#f0f0f0; color:navy; border-radius:6px; padding:2px; }
.markdown pre { background:#f0f0f0; margin:12px; border:1px solid #ddd; padding:20px 12px; border-radius:6px; }
.markdown pre:hover button { display:block; }
.markdown pre button { display:none; position:relative; float:right; top:-16px }
.markdown blockquote { background:#f0f0f0; border-left:6px solid grey; padding:8px }
.markdown table { margin:12px; border-collapse: collapse; }
.markdown th { border:1px solid grey; background:lightgrey; padding:6px; }
.markdown td { border:1px solid grey; padding:6px; }
.markdown tr:nth-child(even) { background:#f0f0f0; }
.markdown ins { color:#890604 }
.markdown rem { color:#198964 }
.toc ul { padding: 0 12px; }
.toc h3 { color:#0057b7; border-bottom:1px dotted grey }
.toc .H1 { list-style-type:none; font-weight:600; margin:4px; background:#eee }
.toc .H2 { list-style-type:none; font-weight:600; margin:4px; }
.toc .H3 { margin-left:2em }
.toc .H4 { margin-left:4em }
.toc .active { color:#0057b7 }
.toc li:hover { background:#f0f0f0 }

View File

@ -1,144 +1,3 @@
resume_markdown_template = """
Act as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is twofold:
1. **Review and Extract Information**: Carefully examine the candidate's current resume to extract the following critical details:
- Work experience
- Educational background
- Relevant skills
- Achievements
- Certifications
2. **Optimize the Resume**: Using the provided template, create a highly optimized resume for the relevant industry. The resume should:
- Include commonly required skills and keywords for the industry
- Utilize ATS-friendly phrases and terminology to ensure compatibility with automated systems
- Highlight strengths and achievements relevant to the industry
- Present experience, skills, and accomplishments in a compelling and professional manner
- Maintain a clear, that is easily readable by both ATS and human reviewers
Provide guidance on how to enhance the presentation of the information to maximize impact and readability. Offer advice on tailoring the content to general industry standards, ensuring the resume passes ATS filters and captures the attention of recruiters, thereby increasing the candidates chances of securing an interview.
## Information to Collect and Analyze
- **My information eesume:**
{resume}
## Template to Use
```
# [Full Name]
[Your City, Your Country](Maps link)
[Your Prefix Phone number](tel: Your Prefix Phone number)
[Your Email](mailto:Your Email)
[LinkedIn](Link LinkedIn account)
[GitHub](Link GitHub account)
## Summary
[Brief professional summary highlighting your experience, key skills, and career objectives. 2-3 sentences.]
## Skills
- **Skill1:** [details (max 15 word)]
- **Skill2:** [details (max 15 word)]
- **Skill3:** [details (max 15 word)]
- **Skill4:** [details (max 15 word)]
- **Skill4:** [details (max 15 word)]
- **Skill5:** [details (max 15 word)]
## Working Experience
### [Job Title]
**[Company Name]** [City, State]
*[Start Date End Date]*
1. [Achievement or responsibility]
2. [Achievement or responsibility]
3. [Achievement or responsibility]
4. [Achievement or responsibility]
5. [Achievement or responsibility]
### [Job Title]
**[Company Name]** [City, State]
*[Start Date End Date]*
1. [Achievement or responsibility]
2. [Achievement or responsibility]
3. [Achievement or responsibility]
4. [Achievement or responsibility]
5. [Achievement or responsibility]
### [Job Title]
**[Company Name]** [City, State]
*[Start Date End Date]*
1. [Achievement or responsibility]
2. [Achievement or responsibility]
3. [Achievement or responsibility]
4. [Achievement or responsibility]
5. [Achievement or responsibility]
## Education
**[Degree] in [Field of Study]**
[University Name] [City, State]
*Graduated: [Month Year]*
## Certifications
1. [Certification Name]
2. [Certification Name]
3. [Certification Name]
## Projects
### [Project Name]
1. [Brief description of the project and your role]
### [Project Name]
1. [Brief description of the project and your role]
### [Project Name]
1. [Brief description of the project and your role]
## Languages
1. **[Language]:** [Proficiency Level]
2. **[Language]:** [Proficiency Level]
```
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 ```markdown ```
"""
fusion_job_description_resume_template = """
Act as an HR expert and resume writer with a strategic approach. Customize the resume to highlight the candidates
strengths, skills, and achievements that are most relevant to the provided job description.
Use a smart and targeted approach, incorporating key skills and abilities as well as important aspects of the job
description into the resume.
Ensure that the resume grabs the attention of hiring managers within the first few seconds and uses specific keywords and phrases from the job description to pass through Applicant Tracking Systems (ATS).
Important Note: While making the necessary adjustments to align the resume with the job description, ensure that the overall structure of the resume remains intact. Do not drastically alter the organization of the document, but optimize it to highlight the most relevant points for the desired position.
- **Most important infomation on job descrption:**
{job_description}
- **My information resume:**
{formatted_resume}
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 ```markdown ```
"""
html_template = """
<!DOCTYPE html>
<title>Resume</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="{casual_markdown}"></script>
<script src="{reorganize_header}"></script>
<link rel="stylesheet" href="{resume_css}">
<body onload="document.body.innerHTML=md.html(document.body.innerHTML); document.body.style.display='block';">
"""
# Personal Information Template
personal_information_template = """
Answer the following question based on the provided personal information.
@ -420,61 +279,74 @@ Please write the cover letter in a way that directly addresses the job role and
```
"""
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 = """
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:
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).
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. Prestigious universities and advanced coursework may indicate solid theoretical knowledge. However, give less weight to academic skills compared to practical experience and projects. For example, a degree from a high-level university should influence answers to technical questions minimally.
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 for lower amounts of experience (up to 5 years). Use related skills and projects to estimate these numbers reasonably.
- **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).
- 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:
## Example 1
```
{resume}
## Curriculum
I am a software engineer with 10 years of experience in Swift and Python. I have worked on projects including an i work 2 years with MQTT protocol.
## Question
How many years of experience do you have with IoT?
## 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.

105
utils.py
View File

@ -1,15 +1,9 @@
import json
import os
import random
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium import webdriver
import time
import glob
from webdriver_manager.chrome import ChromeDriverManager
headless = False
from selenium import webdriver
chromeProfilePath = os.path.join(os.getcwd(), "chrome_profile", "linkedin_profile")
def ensure_chrome_profile():
@ -53,68 +47,36 @@ def scroll_slow(driver, scrollable_element, start=0, end=3600, step=100, reverse
except Exception as e:
print(f"Exception occurred: {e}")
def HTML_to_PDF(FilePath):
# Validate and prepare file paths
if not os.path.isfile(FilePath):
raise FileNotFoundError(f"The specified file does not exist: {FilePath}")
FilePath = f"file:///{os.path.abspath(FilePath).replace(os.sep, '/')}"
# Set up Chrome options
chrome_options = webdriver.ChromeOptions()
# Initialize Chrome driver
service = ChromeService(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)
try:
# Load the HTML file
driver.get(FilePath)
time.sleep(3)
start_time = time.time()
pdf_base64 = driver.execute_cdp_cmd("Page.printToPDF", {
"printBackground": True,
"landscape": False,
"paperWidth": 10,
"paperHeight": 11,
"marginTop": 0,
"marginBottom": 0,
"marginLeft": 0,
"marginRight": 0,
"displayHeaderFooter": False,
"preferCSSPageSize": True,
"generateDocumentOutline": False,
"generateTaggedPDF": False,
"transferMode": "ReturnAsBase64"
})
if time.time() - start_time > 120:
raise TimeoutError("PDF generation exceeded the specified timeout limit.")
return pdf_base64['data']
except WebDriverException as e:
raise RuntimeError(f"WebDriver exception occurred: {e}")
finally:
# Ensure the driver is closed
driver.quit()
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"])
# Assicurati che la directory del profilo Chrome esista
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("--single-process") # Esegui Chrome in un solo processo
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)
@ -122,8 +84,11 @@ def chromeBrowserOptions():
options.add_argument('--user-data-dir=' + initialPath)
options.add_argument("--profile-directory=" + profileDir)
else:
options.add_argument("--incognito")
options.add_argument("--incognito")"""
initialPath = os.path.dirname(chromeProfilePath)
profileDir = os.path.basename(chromeProfilePath)
options.add_argument('--user-data-dir=' + initialPath)
options.add_argument("--profile-directory=" + profileDir)
return options