first commit v2
This commit is contained in:
parent
2dcd7943e8
commit
d8b7e7fda6
3
.gitignore
vendored
3
.gitignore
vendored
@ -11,4 +11,5 @@ data_folder*
|
||||
generated_cv*
|
||||
resume.html
|
||||
.vscode
|
||||
chrome_profile
|
||||
chrome_profile
|
||||
lib*
|
@ -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:
|
@ -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"
|
@ -1,3 +0,0 @@
|
||||
email: myemaillinkedin@gmail.com
|
||||
password: ImpossiblePassowrd10
|
||||
openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR
|
179
gpt.py
179
gpt.py
@ -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
1
job.py
@ -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
132
job_application_profile.py
Normal 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")
|
@ -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.")
|
||||
|
@ -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
|
||||
|
@ -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
191
main.py
@ -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)}")
|
||||
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
127
resume.py
127
resume.py
@ -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)
|
||||
)
|
@ -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> => <TAG*gt;
|
||||
md.formatTag = function (html) { return html.replace(/</g,'<').replace(/\>/g,'>'); }
|
||||
|
||||
// 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 < > tab to 3 space, support marker using ^^^
|
||||
block = block.replace(/</g,'<').replace(/\>/g,'>')
|
||||
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,'`') + '</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)}() );
|
@ -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);
|
@ -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 }
|
240
strings.py
240
strings.py
@ -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 candidate’s 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 candidate’s
|
||||
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 candidate’s 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
105
utils.py
@ -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
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user